Persistent keys
In the previous tutorial, we saw how to use ephemeral keys to sign messages from the enclave and verify that it came from a valid TEE. The ephemeral nature of the keys come with significant limitations - it is not possible to maintain secrets that persist across restarts and other deployments. It precludes a lot of use cases like having persistent wallets owned by the enclave, storing persistent encrypted state, etc.
This tutorial will guide you through securely obtaining persistent keys within your enclave using the Nautilus KMS.
Modify the signing server
Modify the signing server using
cat > src/main.rs <<EOF
use std::io::Write;
fn main() -> std::io::Result<()> {
let key_bytes: [u8; 32] =
ureq::get("http://127.0.0.1:1100/derive/secp256k1?path=signing-server")
.call()
.expect("failed to send kms request")
.into_body()
.read_to_vec()
.expect("failed to read body")[0..32]
.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 the key differences. Instead of reading the key from a file, the server instead queries a specific endpoint for the key. This standard endpoint belongs to the KMS Derive server and provides key derivation services for programs inside the enclave. The derive server is available on port 1100 and you can find a list of available endpoints here.
Add additional dependencies using
cargo add ureq
Update the Docker image
Update the image on Docker Hub using
# Replace <username> with your Docker username
sudo docker build -t <username>/signing-server:latest --push .
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 expected public key from the KMS
Fetch the expected public key from the KMS using
# Replace <image_id> with image id from deployment
oyster-cvm kms-derive --image-id <image_id> --path signing-server --key-type secp256k1/public
Does it match the public key recovered in the previous step?
Notice that the command uses the image id parameter to get the public key. As you might have guessed, this means that the private key that the signing server gets from the KMS depends on the image id of the enclave. This is important to ensure security of the KMS by making sure enclaves can only issue their own keys to themselves and not keys of other enclaves. It works quite well to preserve the keys across restarts as well as deployments of the same enclaves!
This does come with a caveat though - "same enclaves". If you try to change any part of the enclave, it is very likely going to change the image id and therefore the issued key. This makes it hard to update applications while maintaining the same keys and secrets. Read on to the next tutorial to see how Oyster solves this.
Exercise
As with the ephemeral keys, run through the tutorial again by deploying a new enclave. Do you still see the same public key?