Verify attestations using the Oyster SDK
In this guide, we describe how to verify attestations using the Oyster SDK. While there might be slight variations in the syntax depending on language, the concepts, structure and parameters should roughly remain the same.
Install the SDK
- Rust
cargo add oyster-sdk
Imports
- Rust
// usually what you will need
use oyster::attestation::{verify, AttestationExpectations, AWS_ROOT_KEY};
// other imports, unlikely you will need these
use oyster::attestation::{AttestationDecoded, AttestationError};
Verification interface
- Rust
#[derive(Debug)]
pub struct AttestationDecoded {
pub timestamp: usize,
pub pcrs: [[u8; 48]; 3],
pub root_public_key: Box<[u8]>,
pub public_key: Box<[u8]>,
pub user_data: Box<[u8]>,
pub image_id: [u8; 32],
}
#[derive(thiserror::Error, Debug)]
pub enum AttestationError {
#[error("failed to parse: {0}")]
ParseFailed(String),
#[error("failed to verify attestation: {0}")]
VerifyFailed(String),
#[error("http client error")]
HttpClientError(#[from] Error),
#[error("http body error")]
HttpBodyError(#[from] hyper::Error),
}
#[derive(Debug, Default, Clone)]
pub struct AttestationExpectations<'a> {
pub timestamp: Option<usize>,
// (max age, current timestamp) in milliseconds
pub age: Option<(usize, usize)>,
pub pcrs: Option<[[u8; 48]; 3]>,
pub public_key: Option<&'a [u8]>,
pub user_data: Option<&'a [u8]>,
pub root_public_key: Option<&'a [u8]>,
pub image_id: Option<&'a [u8; 32]>,
}
pub fn verify(
attestation_doc: &[u8],
expectations: AttestationExpectations,
) -> Result<AttestationDecoded, AttestationError>;
The verify function is your entrypoint into attestation verification. It expects an attestation document (in raw bytes form) and an AttestationExpectations value to define what is being verified (explained a bit later). If the attestation fails to verify, it returns an error denoting what went wrong. After verifying the attestation, it returns fields from the attestation that can be used for further processing.
For example:
// verifies the attestation, certificate chain and the root key, leaves everything else unchecked
let decoded = verify(&attestation, AttestationExpectations { root_public_key: Some(&AWS_ROOT_KEY), ..Default::default() });
Attestation expectations
Usually, attestation verification only implies verification of the attestation document structure and the certificate chain. The verify function also allows you to specify expected values for fields in the attestation in order to run additional checks against the attestation. Most commonly, you would want to verify that the attestation is coming from a real Nitro enclave that is running an expected version of code.
Verify the root of trust
The root_public_key field can be used to specify a public key to match the root of trust against. Verifying that it matches the expected AWS Nitro public key lets you ensure that the attestation came from a valid AWS Nitro Enclave with all the expected guarantees of a TEE. For convenience, the SDK provides the AWS_ROOT_KEY constant which contains the Nitro root public key.
It is a critical security check that must not be missed for pretty much any deployment. Even if you are using it for a testnet or a local devnet, you would likely still want to match it against the root key for the testnet or devnet.
Verify measurements
Measurements lets you know what code is running inside an enclave and thereby deduce the behaviour of an enclave.
It is a critical security check that must not be missed for production deployments. Unless you really know what you are doing (e.g. you intend to take divergent paths based on the measurements), you should not be verifying attestations without verifying measurements.
There are multiple ways to specify expected measurements.
The recommended and most common way is by specifying image_id, think twice before using the other method. This value is printed by the oyster-cvm deploy command, so you can directly obtain it from there. You can also use the oyster-cvm compute-image-id command to compute it from parameters similar to the deploy command like the docker compose file and any init params.
You can also specify expected measurements directly as PCRs, in case you are deploying a custom enclave image. The SDK expects PCR0, PCR1, PCR2 and PCR16 (a custom PCR) as the PCR list to verify against, if you do not have a custom PCR16, please specify a zero PCR.
Verify liveness
You can also specify expectations about the timestamp field in the attestation. The most common option is max_age to check if the attestation is too old. In addition to the max age itself, it deliberately allows you to override the "current" timestamp as well in order to support use cases where the attestation is verified as on a specific point in time. If in doubt, simply use the real current time as the timestamp.
Both the max age and the current timestamp are specified in milliseconds.
A less common option is verifying the timestamp in the attestation directly by specifying timestamp.
Verify the enclave public key
You can specify the public_key field to verify the attestation's public key. It is unlikely you will need to use this since attestations are usually a way for you to learn the public key of the enclave, rather than checking it against a public key known beforehand.
So what exactly is verified?
To summarize, the above method verifies the following:
- the attestation document is valid (in terms of structure), thereby enabling extraction and verification of attestation fields
- the attestation document is signed by a valid certificate chain, thereby enabling extraction and verification of the root public key
- all specified attestation expectations
- NONE by default