Contract-based access control
In the previous tutorial, we saw how to use KMS-derived persistent keys to sign messages from the enclave and verify them. The persistent nature of the keys comes with one major limitation - it is tied to the image id. It precludes some use cases like being able to update applications while still maintaining the same keys and secrets.
The previous tutorial brought you up to speed on image-based KMS (since it uses the image id). Oyster provides another alternative which is more flexible - contract-based KMS. This variant follows a simple model - keys are derived based on a contract on a specific chain. The contract is expected to implement a specific interface that the KMS can use to determine if a given image id is considered "approved" or not. Enclaves with approved image ids can now issue themselves the key corresponding to that specific contract.
This tutorial will guide you through deploying such a contract and securely obtaining persistent keys within your enclave using the contract variant of the 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:1101/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 difference. The code is almost entirely the same as the previous version, expect the server queries port 1101 instead of 1100. This standard endpoint belongs to the contract-based KMS derive server which works the same as the regular image-based derive server, except using the contract-based key. As before, you can find a list of available endpoints here.
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 a verifier contract
Deploy a verifier contract using:
# replace <key> with private key of the wallet
oyster-cvm kms-contract deploy --wallet-private-key <key>
Make a note of the address. The command deploys this KmsVerifiable contract, which allows approvals and revocation of image by the deployer. Any contract which implements the IKMSVerifiable interface is acceptable by the KMS.
Approve the image id
Compute the image id of the enclave we are going to deploy using:
# Replace <address> with address of the contract deployed above
# for amd64
oyster-cvm compute-image-id --contract-address <address> --chain-id 42161 --docker-compose docker-compose.yml --arch amd64
# for arm64
oyster-cvm compute-image-id --contract-address <address> --chain-id 42161 --docker-compose docker-compose.yml
Approve the image id in the contract deployed above using:
# Replace <key> with private key of the wallet
# Replace <image_id> with image id computed above
# Replace <address> with address of the contract deployed above
oyster-cvm kms-contract approve --wallet-private-key <key> --image-id <image_id> --contract-address <address>
Deploy the enclave
Deploy the enclave image using:
# Replace <key> with private key of the wallet
# Replace <address> with address of the contract deployed above
# for amd64
oyster-cvm deploy --wallet-private-key <key> --contract-address <address> --chain-id 42161 --duration-in-minutes 15 --docker-compose docker-compose.yml --arch amd64
# for arm64
oyster-cvm deploy --wallet-private-key <key> --contract-address <address> --chain-id 42161 --duration-in-minutes 15 --docker-compose docker-compose.yml
Notice the additional chain-id
and contract-address
arguments.
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 <address> with address of the contract deployed above
oyster-cvm kms-derive --contract-address <address> --chain-id 42161 --path signing-server --key-type secp256k1/public
Does it match the public key recovered in the previous step?
Approve a new image id
Compute a new image id using
# Replace <address> with address of the contract deployed above
# for amd64
oyster-cvm compute-image-id --contract-address <address> --chain-id 42161 --init-params "unused:1:0:utf8:something" --docker-compose docker-compose.yml --arch amd64
# for arm64
oyster-cvm compute-image-id --contract-address <address> --chain-id 42161 --init-params "unused:1:0:utf8:something" --docker-compose docker-compose.yml
Do you see a different image id? We simply add a superfluous init parameter which does not change the behaviour of the enclave but does change the image id.
Approve the new image id in the contract deployed above using:
# Replace <key> with private key of the wallet
# Replace <new_image_id> with image id computed above
# Replace <address> with address of the contract deployed above
oyster-cvm kms-contract approve --wallet-private-key <key> --image-id <new_image_id> --contract-address <address>
Deploy another enclave
Deploy another enclave image using:
# Replace <key> with private key of the wallet
# Replace <address> with address of the contract deployed above
# for amd64
oyster-cvm deploy --wallet-private-key <key> --contract-address <address> --chain-id 42161 --init-params "unused:1:0:utf8:something" --duration-in-minutes 15 --docker-compose docker-compose.yml --arch amd64
# for arm64
oyster-cvm deploy --wallet-private-key <key> --contract-address <address> --chain-id 42161 --init-params "unused:1:0:utf8:something" --duration-in-minutes 15 --docker-compose docker-compose.yml
Notice the additional init-params
argument.
Get the signature
Connect to the server to get the signature:
nc <new_ip> 8080
Recover the public key from the signature
Recover the public key from the signature using
# Replace <new_signature> with the signature obtained above
cargo run --bin verifier -- <new_signature>
Does it still match the public key recovered in the previous steps?
We have successfully used the contract-based KMS to derive the same keys across enclaves with different image ids!