Skip to main content

Ephemeral keys

Oyster provides the following keys generated during the startup phase:

  • x25519, secret key located at /app/id.sec, public key located at /app/id.pub
  • secp256k1, secret key located at /app/ecdsa.sec, public key located at /app/ecdsa.pub

Note that these keys are ephemeral and will be different across deployments or enclave restarts.

tip

You can always generate your own keys as part of your application. Using the above keys is just convenient and is especially useful combined with attestations as we will see later.

This tutorial will guide you through using the ephemeral keys available to sign messages and verifying that they were signed by a TEE.

Create a signing server

tip

If you are in the echo-server directory after the previous tutorials, you might want to go back to the scratch directory by running cd ..

Create a new Rust project for the signing server using

cargo new --bin signing-server && cd signing-server

Add dependencies using

cargo add k256 --features k256/ecdsa-core hex

Add code for the signing server using

cat > src/main.rs <<EOF
use std::io::Write;

fn main() -> std::io::Result<()> {
let key_bytes: [u8; 32] = std::fs::read("/key")?.as_slice().try_into().unwrap();
let key = k256::ecdsa::SigningKey::from_bytes(&key_bytes.into()).unwrap();
let message = "hello";
let signature = key.sign_recoverable(message.as_bytes()).unwrap();
let signature_bytes =
hex::encode(signature.0.to_bytes()) + &hex::encode(&[signature.1.to_byte() + 27]);

let listener = std::net::TcpListener::bind("0.0.0.0:8080")?;

listener
.incoming()
.try_for_each(|stream| -> Result<_, std::io::Error> {
stream?.write_all(signature_bytes.as_bytes())
})
}
EOF

Feel free to pause and understand how the signer works. Whenever clients connect to the server, it responds with a signature against a hello message. Note that it expects to read the key from /key which is where we will be mounting the enclave key.

Create a verifier

Create a verifier using

cat > src/verifier.rs <<EOF
fn main() {
let signature_hex = std::env::args().nth(1).expect("missing signature arg");
let message = "hello";
let signature_bytes = hex::decode(signature_hex).expect("invalid signature");
let signature = k256::ecdsa::Signature::from_slice(&signature_bytes[0..64])
.expect("failed to parse signature");
let recovery_id = k256::ecdsa::RecoveryId::from_byte(signature_bytes[64] - 27)
.expect("failed to parse recovery id");
let verifying_key =
k256::ecdsa::VerifyingKey::recover_from_msg(message.as_bytes(), &signature, recovery_id)
.expect("failed to recover pubkey");
let pubkey = hex::encode(&verifying_key.to_encoded_point(false).as_bytes()[1..]);

println!("Pubkey: {}", pubkey);
}
EOF

Feel free to pause and understand how the verifier works. It takes a signature and outputs the pubkey extracted from it (similar to ecrecover in solidity).

Add it to the build tree using

cat >> Cargo.toml <<EOF

[[bin]]
name = "verifier"
path = "src/verifier.rs"
EOF

Publish a Docker image

Create a Dockerfile to help build and package the signing server into a Docker image:

cat > Dockerfile <<EOF
FROM rust:alpine AS builder
WORKDIR /usr/src/app
COPY . .
RUN apk add musl-dev
RUN cargo build --release

FROM alpine
COPY --from=builder /usr/src/app/target/release/signing-server /usr/local/bin/signing-server
CMD ["signing-server"]
EOF

Build and push the image to Docker Hub using

# Replace <username> with your Docker username
sudo docker build -t <username>/signing-server:latest --push .

Create a docker compose file

As before, create a file named docker-compose.yml describing the service being deployed:

# Replace <username> with your Docker username
cat > docker-compose.yml <<EOF
services:
signing-server:
image: <username>/signing-server
network_mode: host
restart: unless-stopped
volumes:
- /app/ecdsa.sec:/key
EOF

We use the volumes option to mount the key in the right place in the Docker container.

Deploy the enclave

Deploy the enclave image using:

# replace <key> with private key of the wallet

# for amd64
oyster-cvm deploy --wallet-private-key <key> --duration-in-minutes 15 --docker-compose docker-compose.yml --arch amd64

# for arm64
oyster-cvm deploy --wallet-private-key <key> --duration-in-minutes 15 --docker-compose docker-compose.yml

Get the signature

Connect to the server to get the signature:

nc <ip> 8080

Recover the public key from the signature

Recover the public key from the signature using

# Replace <signature> with the signature obtained above
cargo run --bin verifier -- <signature>

Get the public key from an attestation

Verify a remote attestation to get the public key:

# Replace <ip> with the IP you obtained above
# Replace <image_id> with the image id obtained above
oyster-cvm verify --enclave-ip <ip> --image-id <image_id> -p 1301

Note the use of -p 1301. It specifies the port of the attestation server which attests to the public key corresponding to the secp256k1 secret key inside the enclave.

Does the Enclave public key match the public key recovered in the previous step?

tip

If you are trying to do verification in Solidity, you can convert the public key to an address and verify it against an ecrecover.

Exercise

Run through the tutorial again by deploying a new enclave. Do you still see the same public key? This is what we mean by the ephemeral nature of the keys, it is different for every run of the enclave.