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.
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
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?
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.