This document aims to give an in-detail overview of the entire Nitro Enclaves attestation flow and especially on the intrinsic restrictions of the Attestation Document. We will go through the attestation document definition, what is generated by the AWS Nitro system when we issue an Attestation command and how a Key Management Service should process this Attestation Document.
The purpose of attestation is to prove that an enclave is a trustworthy entity, based on the code and configuration that is running within a particular enclave. The root of trust for the enclave resides within the AWS Nitro system, which provides Attestation Documents to the enclave, signed by the AWS Nitro Attestation Public Key Infrastructure (PKI).
The root of trust component for the attestation is the Nitro hypervisor, that contains information about the enclave, including platform configuration registers (PCR) and produces an attestation document that contains details of the enclave: enclave signing key, hash of the enclave image, the instance it is attached to, the role attached to the instance and more.
Attestation documents are signed by the AWS Nitro Public Key Infrastructure (PKI), which includes a published certificate authority that can be incorporated into any service.
The enclave is able to request from the Nitro hypervisor an attestation document that allows an external service to verify caller’s identity. The attestation document generated by the Nitro system follows this specific format, encoded in Concise Binary Object Representation (CBOR) and signed using CBOR Object Signing and Encryption (COSE).
Attestation documents are signed by the AWS Nitro Attestation PKI, which includes a root certificate for the commercial AWS partitions that can be found here (SHA256 8cf60e2b2efca96c6a9e71e851d00c1b6991cc09eadbe64a6a1d1b1eb9faff7c).
The root certificate is based on an ACM PCA private key and has a lifetime of 30 years. The subject of the CA should follow this format:
CN=<partition>.nitro-enclaves, C=US, O=Amazon, OU=AWS
An example of CN: aws.nitro-enclaves
.
Below you can find the structure of an Attestation Document.
AttestationDocument = {
module_id: text, ; issuing Nitro hypervisor module ID
timestamp: uint .size 8, ; UTC time when document was created, in
; milliseconds since UNIX epoch
digest: digest, ; the digest function used for calculating the
; register values
pcrs: { + index => pcr }, ; map of all locked PCRs at the moment the
; attestation document was generated
certificate: cert, ; the infrastructure certificate used to sign this
; document, DER encoded
cabundle: [* cert], ; issuing CA bundle for infrastructure certificate
? public_key: user_data, ; an optional DER-encoded key the attestation
; consumer can use to encrypt data with
? user_data: user_data, ; additional signed user data, defined by protocol
? nonce: user_data, ; an optional cryptographic nonce provided by the
; attestation consumer as a proof of authenticity
}
cert = bytes .size (1..1024) ; DER encoded certificate
user_data = bytes .size (0..1024)
pcr = bytes .size (32/48/64) ; PCR content
index = 0..31
digest = "SHA384"
2.2.1 Attestation Document Operation
The enclave and the service that wants to attest the enclave need to first
agree on a common protocol to follow. The optional parameters in the Attestation
Document (public_key
, user_data
, nonce
) allow the enclave and the entity
to set up a variety of protocols depending on the security properties that the
service and the enclave want to guarantee to each other. Services that rely on
attestation need to define a protocol that can meet those guarantees, and the
enclave software needs to agree to and follow these protocols.
An enclave wishing to attest to a specific service will first have to open a TLS connection to that service and verify that the service's certificates are valid. These certificates will have to be included in the enclave during enclave build.
Note: A TLS session is not absolutely required, however, this integrates more readily with already existing services.
2.2.2 Attestation Document Optional Fields
Nonce - To avoid impersonation attacks where a service could re-use submitted attestation documents, attestation requires a challenge-response workflow. For example a Nitro Enclave application may connect to a service and initiate a “hello” request. The response to this request should include a nonce. The application will then use this nonce to request a new attestation document from the Nitro system. The Nitro System will respond with a signed attestation document that encapsulates the nonce, as well as the details of the application. The nonce needs to have a limited lifetime and most services should consider treating this nonce as a one-time token. The nonce guarantees that the service is talking to a live enclave and not some third party which is reusing an older attestation document.
Public Key - To allow a service to encrypt data for use by the enclave, and only the enclave, the attestation will require a public key. A public/private key pair will be generated by the application, and the application will include the public key in the attestation request. The Nitro system responds with an attestation document that contains the public key and other details of the application. The service uses the public key to encrypt the data that can be decrypted only by the enclave or to validate signatures made by the enclave. API calls that do not have mutating effects on the service side might consider not using a nonce and just encrypting responses to such a public key, since only the correct enclave could decrypt the responses. Note that this mechanism would not provide by itself protection against replay attacks.
User data - This is signed data that the enclave can include in the Attestation Document. This could have a specific meaning to the service, for example, this field could carry the API parameters, or at least a signature of the API parameters, which would guarantee that the Attestation Document could not be used for any other purpose than the intended one. It could also be used by the enclave to sign off on an operation it has completed, allowing a service to trust that the result comes from a valid source.
When you issue in enclave an Attestation call (to Nitro hypervisor) you will receive
a binary blob containing the Signed Attestation Document. The Signed Attestation Document
is a CBOR encoded, COSE signed object, using the COSE_Sign1
signature structure. The
overall validation flow can be seen in the following figure:
The COSE_Sign1
signature structure is used when only one signature is going to
be placed on a message. The parameters dealing with the content and the signature are
placed in the same pair of buckets rather than having the separation of COSE_Sign
.
The structure can be encoded as either tagged or untagged depending on the context it will
be used in. A tagged COSE_Sign1
structure is identified by the CBOR tag 18.
The CBOR object that carries the body, the signature, and the information about the body
and signature is called the COSE_Sign1
structure. The COSE_Sign1
structure is a CBOR
array. The fields of the array in order are:
[
protected: Header,
unprotected: Header,
payload: This field contains the serialized content to be signed,
signature: This field contains the computed signature value.
]
When we talk about an Attestation Document we have the following (Disclaimer: this is text representation, all the values are actually CBOR encoded):
18(/* COSE_Sign1 CBOR tag is 18 */
{1: -35}, /* This is equivalent with {algorithm: ECDS 384} */
{}, /* We have nothing in unprotected */
bstr .size (1..16384), /* Attestation Document */
bstr .size (32/48/64) /* Signature */
)
In the previous subsection we saw how the Signed Attestation Document looks like. We’re interested in extraction of the actual attestation document, validate that it was properly signed and its content is correct.
When we write an Attestation Document validator we must ensure that we have access to the root certificate during computation. This can be seen as a prerequisite for Attestation Document validation.
We’re going to receive from the Nitro hypervisor the Signed Attestation Document, which is
an CBOR encoded, COSE signed object, using the COSE_Sign1
signature structure. Briefly,
we’re going to handle a bytes blob. The main flow:
- Decode the CBOR object and map it to a
COSE_Sign1
structure; - Extract the Attestation Document from the
COSE_Sign1
structure; - Verify the certificates chain;
- Ensure that the Signed Attestation Document was correctly signed.
3.2.1. COSE decode and validate signature operations
We’re talking here about two operations: decoding from COSE and ensuring that the signature is correct. This can be done in two ways: implementing from zero the RFC from or using tools already existing tools for COSE computing.
3.2.2. Syntactical validation
We’re going to process the payload from the Signed Attestation Document. The output will be a structure which follows the Attestation Document specification. We must ensure that the following restrictions are present in the currently processed CBOR object:
3.2.2.1. Check if the required fields are present
The Attestation Document has a series of fields. For these fields we have the following restrictions:
- No field content can be null;
- List of Mandatory / Optional fields is the following:
Attestation Document Allowed Fields {
"module_id" => Mandatory
"digest" => Mandatory
"timestamp" => Mandatory
"pcrs" => Mandatory
"certificate" => Mandatory
"cabundle" => Mandatory
"public_key" => Optional
"user_data" => Optional
"nonce" => Optional
}
Note: If any of the previously stated rules is not fulfilled, we fail the validation.
3.2.2.2. Check content
For every value present in the CBOR containing the attestation document, we adhere to the following restrictions (Note: we talk about CBOR standard types):
module_id
Type: Text String
$length != 0 /* Module ID must be non-empty */
digest
Type: Text String
$value ∈ {"SHA384"} /* Digest can be exactly one of these values */
timestamp
Type: Integer
$value > 0 /* Timestamp must be greater than 0 */
pcrs
Type: Map
$size ∈ [1, 32] /* We must have at least one PCR and at most 32 */
/* The following rules apply for EACH PCR existing in the `pcrs` map.
* We'll use an additional notation: pcrIdx for PCR index.
* Note: CBOR can manage several types of keys, hence we must ensure that keys
* also have the right type.
*/
Type pcrIdx: Integer /* The type of the key must be integer */
pcrIdx ∈ [0, 32) /* A PCR index can be in this interval [0, 32) */
Type pcrs[pcrIdx]: Byte String /* The type of a PCR content must be Byte String */
$length pcrs[pcrIdx] ∈ {32, 48, 64} /* Length of PCR can be one of this values {32, 48, 64} */
cabundle
Type: Array
$length > 0 /* CA Bundle is not allowed to have 0 elements */
Type cabundle[idx]: Byte String /* CA bundle entry must have Byte String type */
$length cabundle[idx] ∈ [1, 1024] /* CA bundle entry must have length between 1 and 1024 */
public_key
Type: Byte String
$length ∈ [1, 1024]
user_data
Type: Byte String
$length ∈ [0, 512]
nonce
Type: Byte String
$length ∈ [0, 512]
3.2.3. Semantical validation
Until now, we ensured that our CBOR encoded Attestation Document has the right fields and all the restrictions are present. Now we must verify the certificates (their validity, their critical extensions, the fact that they are correctly chaining - for this we must have access to the public root certificate, this certificate being guaranteed by AWS). An Attestation Document will always have its CA bundle in a well established order. This order is as it follows:
[ROOT_CERT, INTERM_1, INTERM_2, .... INTERM_N]
0 1 2 N - 1
We must be aware of this ordering since already existing tools might require the order to be reversed. For example Java’s CertPath requires the order to be the other way around.
When we begin the validation, we start from the Attestation Document CA bundle
and generate the desired chain, where TARGET_CERTIFICATE
is the certificate
attach in the Attestation Document (message[“certificate”]
):
[TARGET_CERT, INTERM_N, ..... , INTERM_2, INTERM_1, ROOT_CERT]
Note: All certificates are X509 Certificates.
3.2.3.1. Certificates validity
For all the certificates in the newly generated list of X509 certificates we must ensure that they are still valid: if the current date and time are within the validity period given in the certificate. This also applies for the root certificate we are checking the chain against.
3.2.3.2. Certificates critical extensions: basic constraints
The X509 Public Key Infrastructure Certificate RFC states the following: the
basic constraints extension identifies whether the subject of the certificate
is a CA and how deep a certification path may exist through that CA. The
pathLenConstraint
field is meaningful only if CA is set to TRUE. In this case,
it gives the maximum number of CA certificates that may follow this certificate
in a certification path. A value of zero indicates that only an end-entity
certificate may follow in the path. Where it appears, the pathLenConstraint field
must be greater than or equal to zero. Where pathLenConstraint does not appear,
there is no limit to the allowed length of the certification path. This extension
must appear as a critical extension in all CA certificates. This extension should
appear in end entity certificates. The identifier for basic constraints is
2.5.29.19
.
id-ce-basicConstraints OBJECT IDENTIFIER ::= { id-ce 19 }
BasicConstraints ::= SEQUENCE {
CA BOOLEAN DEFAULT FALSE,
pathLenConstraint INTEGER (0..MAX) OPTIONAL
}
Based on this, we want to make sure that the certificates in our previously built chain have proper pathLenConstraint set, based on its key usage. For pseudocode please check the following subsection.
3.2.3.3. Certificates critical extensions: key usage
Based on the X509 Public Key Infrastructure Certificate RFC the key usage extension
defines the purpose (encipherment, signature, certificate signing and others) of the
key contained in the certificate. The usage restriction might be employed when a key
that could be used for more than one operation is to be restricted. The Key Usage
identifier is 2.5.29.15
.
The key usages are the following:
KeyUsage ::= BIT STRING {
digitalSignature (0),
nonRepudiation (1),
keyEncipherment (2),
dataEncipherment (3),
keyAgreement (4),
keyCertSign (5),
cRLSign (6),
encipherOnly (7),
decipherOnly (8)
}
We’re interested in digitalSignature and keyCertSign. There are some brief definitions for these two:
-
The
digitalSignature
bit is asserted when the subject public key is used with a digital signature mechanism to support security services other than non-repudiation (bit 1), certificate signing (bit 5), or revocation information signing (bit 6). -
The
keyCertSign
bit is asserted when the subject public key is used for verifying a signature on certificates. This bit may only be asserted in CA certificates.
We have the following restrictions for the root certificate:
pathLenConstraint
is greater or equal than the chain size;- Key usage:
keyCertSign
bit is present.
For all the certificates in the chain excepting the target certificate (the first one) we must ensure:
- no certificate was used to create a chain longer than its
pathLenConstraint
; - Key usage:
keyCertSign
bit is present.
For the target certificate (the first one) we must ensure:
- Key usage:
digitalSignature
bit is present; pathLenConstraint
must be undefined since this is a client certificate.
3.2.4. Certificates chain
In general, a chain of multiple certificates may be needed, comprising a certificate of the public key owner (the end entity) signed by one CA, and zero or more additional certificates of CAs signed by other CAs. Such chains, called certification paths, are required because a public key user is only initialized with a limited number of assured CA public keys.
Certification path validation procedures for the Internet PKI are based on the algorithm supplied in X.509. Certification path processing verifies the binding between the subject distinguished name and/or subject alternative name and subject public key. The binding is limited by constraints that are specified in the certificates that comprise the path and inputs that are specified by the relying party. The basic constraints and policy constraints extensions allow the certification path processing logic to automate the decision making process.
We do not use CRL, so it must be disabled when doing the validation.
Starting from our root path and the generated certificate chain, the chain validation looks like:
validateCertsPath(certChain, rootCertficate) {
/* The trust anchor is the root CA to trust */
trustAnchors.add(rootCertificate);
/* We need PKIX parameters to specify the trust anchors
* and disable the CRL validation
*/
validationParameters = new PKIXParameters(trustAnchors);
certPathValidator = CertPathValidator.getInstance(PKIX);
validationParameters.setRevocationEnabled(false);
/* We are ensuring that certificates are chained correctly */
certPathValidator.validate(certPath, validationParameters);
}