Skip to main content

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!