This repo provides solidity contracts for the verification of attestations generated by AWS Nitro Enclaves, as outlined in this doc.
AWS's attestation verification documentation disables CRL checks in its sample flow
here.
This library supports operational revocation with an authorized revoker: operators monitor AWS CRLs
off-chain and call CertManager.revokeCert / revokeCerts for affected certificate identity keys.
Nitro attestations are signed with ECDSA over the NIST P-384 curve. Verifying a P-384 signature
on-chain requires modular inversion, which is computed via the MODEXP precompile — and the Fusaka
upgrade (EIP-7883, live on Base) raises MODEXP pricing enough that the old fully on-chain flow no
longer fits in a block.
This library therefore uses hinted verification: the modular inverses are computed off-chain
and supplied in calldata as "hints". The contract does not trust them — before using each hint
inv for a value b modulo m, it checks b · inv ≡ 1 (mod m) and reverts ("bad inverse hint")
otherwise. Because the moduli are prime the inverse is unique, so a wrong hint can only cause a
revert, never a forged signature. The acceptance rule is identical to a standard on-chain ECDSA-384
verification — hints are purely a gas optimization.
The legacy non-hinted entrypoints (verifyCACert, verifyClientCert, validateAttestation) are
retained only as reverting stubs; use the *WithHints functions below.
For the full design, security argument, and measured gas, see docs/hinted-p384-nitro-attestation.md.
Deploy in this order (the verifier references are immutable):
P384VerifierCertManager(p384Verifier)— pins the AWS Nitro root CA and sets the deployer as owner/revoker.NitroValidator(certManager, p384Verifier)
After deployment, move ownership to the production admin and set the operational revoker with
transferOwnership / setRevoker.
Verification has two phases. Certificate chains are reused across many attestations from the same enclave, so the chain is verified and cached once, after which each attestation only pays for its own document signature.
- Cold phase — verify & cache the certificate chain (once per chain). For each CA cert in the
cabundle, then the leaf cert, compute the inverse hints off-chain and submit them:certManager.verifyCACertWithHints(caCert, parentCertHash, hints)(useROOT_CA_CERT_HASH/keccak256(rootCert)for the first non-root CA;0is only for the pinned root itself)certManager.verifyClientCertWithHints(leafCert, parentCertHash, hints)
- Validation — verify the document signature. Once the chain is cached:
validator.validateAttestationWithHints(attestationTbs, signature, attestationHints)- Precondition: the whole
cabundle+ leaf must already be cached from phase 1. This call re-walks the chain with empty hints (relying on the cache), so an uncached chain reverts with"inverse hint underflow".
Splitting attestation into attestationTbs (to-be-signed bytes) and signature is cheapest
off-chain, but validator.decodeAttestationTbs(attestation) is available on-chain too.
tools/p384_hints.js is a reference generator (Node.js, no dependencies):
node tools/p384_hints.js cert --cert <0x DER cert> --pubkey <0x parent pubkey>
node tools/p384_hints.js attestation --attestation <0x COSE> --pubkey <0x leaf pubkey>Production callers should reimplement this in their backend language; the contract re-verifies every hint, so the generator is trusted only for liveness, never for correctness.
CertManager does not fetch or parse AWS CRLs on-chain. Instead, an authorized revoker address
marks certificates revoked after checking AWS CRLs off-chain. Revocation is keyed by the
certificate's (issuer, serial) identity — keccak256(issuerHash, serialHash), the same identity
AWS CRLs use to list revoked certs — not by keccak256(certBytes). Raw cert bytes are not a
stable identity: ECDSA signatures are malleable (the (r, n-s) twin also verifies) and DER is
re-encodable, so a byte-keyed revocation could be bypassed by a re-encoded twin of the revoked cert.
Keying on the signature-protected (issuer, serial) pair closes that gap and lets operators revoke
straight from CRL data. Compute the key with CertManager.computeCertId(certDER) (or replicate it
off-chain). Revoked certificates are rejected on both cold verification and cached reuse,
independently of notAfter. Cached descendants are also rejected when their cached parent chain
contains a revoked certificate.
- The deployer starts as both
ownerandrevoker. - The owner can call
transferOwnership,setRevoker,unrevokeCert, and revokeROOT_CA_CERT_HASHas an emergency global halt (the root is identified by its pinned hash, since it is never parsed on-chain). - The revoker can call
revokeCertorrevokeCertsfor non-root certificate identity keys. loadVerifiedis a raw cache read; returned metadata does not imply the certificate is currently trusted.
pragma solidity ^0.8.0;
import {NitroValidator} from "@nitro-validator/NitroValidator.sol";
import {CertManager} from "@nitro-validator/CertManager.sol";
import {CborDecode} from "@nitro-validator/CborDecode.sol";
contract Validator {
using CborDecode for bytes;
uint256 public constant MAX_AGE = 60 minutes;
bytes32 public constant PCR0 = keccak256("some PCR0 value");
NitroValidator public immutable validator;
constructor(NitroValidator validator_) {
validator = validator_;
}
// Assumes the attestation's certificate chain has already been cached on the CertManager via
// verifyCACertWithHints / verifyClientCertWithHints (see the cold phase above).
function registerSigner(
bytes calldata attestationTbs,
bytes calldata signature,
bytes calldata attestationHints // computed off-chain
) external {
NitroValidator.Ptrs memory ptrs =
validator.validateAttestationWithHints(attestationTbs, signature, attestationHints);
bytes32 pcr0 = attestationTbs.keccak(ptrs.pcrs[0]);
require(pcr0 == PCR0, "invalid pcr0 in attestation");
require(ptrs.timestamp / 1000 + MAX_AGE > block.timestamp, "attestation too old");
bytes memory publicKey = attestationTbs.slice(ptrs.publicKey);
// do something with the public key, user data, etc
}
}As a Foundry dependency:
forge install base/nitro-validatorThen map the @nitro-validator/ prefix used in the examples above to the package's src/ in your
remappings.txt:
@nitro-validator/=lib/nitro-validator/src/
The library vendors its only third-party dependency (the P-384 verifier) under src/vendor/, so no
additional submodules are required beyond forge-std.
Verification proves an attestation is genuine; some properties are intentionally left to the integrator (see docs):
- Freshness / replay — the contract does not compare the attestation
timestamp(milliseconds) toblock.timestamp(seconds) or match thenonceto a challenge. Enforce freshness yourself if you need it. - Signature malleability — low-S is not enforced (AWS does not emit low-S), so the malleable
twin
(r, n-s)also verifies. Never use the signature as a uniqueness key; dedupe on canonical attestation fields instead. - Enclave policy — checking
pcrs/moduleIDagainst the enclave image(s) you trust is your responsibility. - Revocation operations — the contract enforces the on-chain revoked set, but an off-chain
operator must monitor AWS CRLs and submit the affected certificate identity keys
(
keccak256(issuerHash, serialHash), computed viacomputeCertIdor directly from the CRL's issuer/serial entries).
forge buildforge testThe off-chain witness generator is cross-checked for byte-identical parity against the on-chain Solidity reference under FFI (requires Node.js):
NITRO_RUN_FFI=true forge test --ffi --match-test test_OffchainWitness