Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 42 additions & 1 deletion interfaces/L1/proofs/tee/INitroEnclaveVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,26 @@ interface INitroEnclaveVerifier {
*/
function revoker() external view returns (address);

/**
* @dev Returns whether the given intermediate certificate hash has been revoked.
* @param _certHash Hash of the certificate
* @return `true` if the certificate is currently marked as revoked
*
* The revocation sentinel is persistent across `_cacheNewCert` overwrites and
* blocks both verification (via `_verifyJournal`) and the off-chain
* `checkTrustedIntermediateCerts` helper from re-trusting the hash. Re-trust
* requires an explicit `unrevokeCert` call.
*/
function revokedCerts(bytes32 _certHash) external view returns (bool);

/**
* @dev Returns the cached `notAfter` timestamp (seconds) for an intermediate certificate.
* @param _certHash Hash of the certificate
* @return Cached expiry timestamp; `0` indicates the certificate is not currently
* cached (either never seen, expired-and-evicted, or revoked).
*/
function trustedIntermediateCerts(bytes32 _certHash) external view returns (uint64);

/**
* @dev Retrieves the configuration for a specific coprocessor
* @param _zkCoProcessor Type of ZK coprocessor (RiscZero or Succinct)
Expand Down Expand Up @@ -261,15 +281,36 @@ interface INitroEnclaveVerifier {
external;

/**
* @dev Revokes a trusted intermediate certificate
* @dev Revokes a trusted intermediate certificate.
* @param _certHash Hash of the certificate to revoke
*
* Requirements:
* - Only callable by contract owner or revoker
* - Certificate must exist in the trusted set
*
* In addition to clearing the cached entry, this flips a persistent
* revocation sentinel that survives later cache writes. Subsequent
* verifications whose chain traverses the revoked hash are rejected
* regardless of the journal-supplied `trustedCertsPrefixLen`. Re-trust
* requires an explicit `unrevokeCert` call.
*/
function revokeCert(bytes32 _certHash) external;

/**
* @dev Explicitly re-trusts a previously revoked intermediate certificate.
* @param _certHash Hash of the certificate to un-revoke
*
* Requirements:
* - Only callable by contract owner
* - Certificate must currently be marked as revoked
*
* Clears the persistent revocation sentinel. The cached expiry is not
* restored here; the next successful verification whose chain traverses
* `_certHash` will re-cache it via `_cacheNewCert` with the journal-supplied
* `notAfter` timestamp.
*/
function unrevokeCert(bytes32 _certHash) external;

/**
* @dev Updates the verifier program ID, adding the new version to the supported set
* @param _zkCoProcessor Type of ZK coprocessor
Expand Down
56 changes: 56 additions & 0 deletions snapshots/abi/NitroEnclaveVerifier.json
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,25 @@
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "",
"type": "bytes32"
}
],
"name": "revokedCerts",
"outputs": [
{
"internalType": "bool",
"name": "",
"type": "bool"
}
],
"stateMutability": "view",
"type": "function"
},
{
"inputs": [],
"name": "revoker",
Expand Down Expand Up @@ -571,6 +590,19 @@
"stateMutability": "view",
"type": "function"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "certHash",
"type": "bytes32"
}
],
"name": "unrevokeCert",
"outputs": [],
"stateMutability": "nonpayable",
"type": "function"
},
{
"inputs": [
{
Expand Down Expand Up @@ -841,6 +873,19 @@
"name": "CertRevoked",
"type": "event"
},
{
"anonymous": false,
"inputs": [
{
"indexed": false,
"internalType": "bytes32",
"name": "certHash",
"type": "bytes32"
}
],
"name": "CertUnrevoked",
"type": "event"
},
{
"anonymous": false,
"inputs": [
Expand Down Expand Up @@ -1091,6 +1136,17 @@
"name": "CertificateNotFound",
"type": "error"
},
{
"inputs": [
{
"internalType": "bytes32",
"name": "certHash",
"type": "bytes32"
}
],
"name": "CertificateNotRevoked",
"type": "error"
},
{
"inputs": [],
"name": "InvalidVerifierAddress",
Expand Down
4 changes: 2 additions & 2 deletions snapshots/semver-lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@
"sourceCodeHash": "0x52926494b37fbf68ce1110d8097a399a23d15eb6e1a878eb56b0bed62c5cb926"
},
"src/L1/proofs/tee/NitroEnclaveVerifier.sol:NitroEnclaveVerifier": {
"initCodeHash": "0x82f42b4d578bfcf9dc35eaa2c4ada04a45e1eca63021bceceb2ec794b12a9dd6",
"sourceCodeHash": "0x5b2938048ff85baceb963c54138ce209890d77c63ce8791b48f36c5fda5c81e5"
"initCodeHash": "0xb4b79601bf956e3859e982028036344fc1c0b36af09686b5f0bfeeabf8540c5f",
"sourceCodeHash": "0x07f0bca150c8799aaa67ca3d960d51456add0c945d293eec233d67573371343e"
},
"src/L1/proofs/tee/TEEProverRegistry.sol:TEEProverRegistry": {
"initCodeHash": "0xfd1942e1c2f59b0aa72b33d698a948a53b6e4cf1040106f173fb5d89f63f57b0",
Expand Down
7 changes: 7 additions & 0 deletions snapshots/storageLayout/NitroEnclaveVerifier.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,5 +54,12 @@
"offset": 0,
"slot": "7",
"type": "mapping(enum ZkCoProcessorType => bytes32)"
},
{
"bytes": "32",
"label": "revokedCerts",
"offset": 0,
"slot": "8",
"type": "mapping(bytes32 => bool)"
}
]
107 changes: 98 additions & 9 deletions src/L1/proofs/tee/NitroEnclaveVerifier.sol
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
/// @dev Mapping from ZkCoProcessorType to its corresponding verifierProofId representation
mapping(ZkCoProcessorType => bytes32) private _verifierProofIds;

/// @dev Persistent revocation sentinel for intermediate certificates.
///
/// `revokeCert` zeroes `trustedIntermediateCerts[certHash]`, but the suffix-cache
/// path in `_cacheNewCert` would otherwise restore that entry on the next
/// successful verification whose chain traverses the revoked hash. This
/// mapping survives `_cacheNewCert` overwrites and is consulted in
/// `_verifyJournal`, `_cacheNewCert`, and `checkTrustedIntermediateCerts`,
/// making `revokeCert` durable independently of the journal's `trustedCertsPrefixLen`.
///
/// Re-trust requires an explicit `unrevokeCert` admin call; it is never
/// granted as a side effect of verification.
mapping(bytes32 => bool) public revokedCerts;

// ============ Custom Errors ============

/// @dev Error thrown when an unsupported or unknown ZK coprocessor type is used
Expand Down Expand Up @@ -114,6 +127,9 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
/// @dev Thrown when caller is neither the owner nor the revoker
error CallerNotOwnerOrRevoker();

/// @dev Thrown when `unrevokeCert` is called for a hash that is not currently revoked
error CertificateNotRevoked(bytes32 certHash);

// ============ Events ============

/// @dev Emitted when a new verifier program ID is added/updated
Expand Down Expand Up @@ -146,6 +162,9 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
/// @dev Event emitted when a certificate is revoked
event CertRevoked(bytes32 certHash);

/// @dev Event emitted when a previously revoked certificate is explicitly re-trusted
event CertUnrevoked(bytes32 certHash);

/// @dev Event emitted when the maximum time difference is updated
event MaxTimeDiffUpdated(uint64 newMaxTimeDiff);

Expand Down Expand Up @@ -265,6 +284,11 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
revert RootCertMismatch(rootCertHash, certs[0]);
}
for (uint256 j = 1; j < certs.length; j++) {
// Stop counting at any revoked entry so off-chain callers cannot derive a
// prefix-len that walks past a revoked cert and then claim it as the trusted boundary.
if (revokedCerts[certs[j]]) {
break;
}
uint64 expiry = trustedIntermediateCerts[certs[j]];
if (block.timestamp > expiry) {
break;
Expand Down Expand Up @@ -332,7 +356,7 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
}

/**
* @dev Revokes a trusted intermediate certificate
* @dev Revokes a trusted intermediate certificate.
* @param certHash Hash of the certificate to revoke
*
* Requirements:
Expand All @@ -342,16 +366,46 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
* This function allows the owner or revoker to revoke compromised intermediate certificates
* without affecting the root certificate or other trusted certificates.
*
* Note: A revoked cert can be trusted again by reproving it.
* Durability: in addition to clearing `trustedIntermediateCerts[certHash]`, this
* function flips the persistent `revokedCerts[certHash]` sentinel. The sentinel
* survives subsequent `_cacheNewCert` overwrites and causes both `_verifyJournal`
* and `checkTrustedIntermediateCerts` to reject any chain whose suffix traverses
* the revoked hash, regardless of the journal's `trustedCertsPrefixLen`. Reproving
* the same chain therefore cannot silently restore trust; re-trust requires an
* explicit `unrevokeCert` call by the owner.
*/
function revokeCert(bytes32 certHash) external onlyOwnerOrRevoker {
if (trustedIntermediateCerts[certHash] == 0) {
revert CertificateNotFound(certHash);
}
delete trustedIntermediateCerts[certHash];
revokedCerts[certHash] = true;
emit CertRevoked(certHash);
}

/**
* @dev Explicitly re-trusts a previously revoked intermediate certificate.
* @param certHash Hash of the certificate to un-revoke
*
* Requirements:
* - Only callable by contract owner
* - Certificate must currently be marked as revoked
*
* Clearing the revocation sentinel does not by itself restore the cached
* expiry; the next successful verification whose chain traverses `certHash`
* will re-cache it via `_cacheNewCert`. This two-step design (admin clears
* the sentinel, verification re-caches the expiry) keeps re-trust an
* explicit, owner-only action while still letting the normal cache path
* supply the up-to-date `notAfter` timestamp.
*/
function unrevokeCert(bytes32 certHash) external onlyOwner {
if (!revokedCerts[certHash]) {
revert CertificateNotRevoked(certHash);
}
delete revokedCerts[certHash];
emit CertUnrevoked(certHash);
}

/**
* @dev Updates the verifier program ID, adding the new version to the supported set
* @param zkCoProcessor Type of ZK coprocessor
Expand Down Expand Up @@ -570,10 +624,17 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
* This function automatically adds any certificates beyond the trusted length
* to the trusted intermediate certificates set. This optimizes future verifications
* by expanding the known trusted certificate set based on successful verifications.
*
* Revoked entries are skipped: once `revokedCerts[certHash]` is set by `revokeCert`,
* no successful verification will silently restore the cache, regardless of the
* journal's `trustedCertsPrefixLen`. Re-trust requires an explicit `unrevokeCert`.
*/
function _cacheNewCert(VerifierJournal memory journal) internal {
for (uint256 i = journal.trustedCertsPrefixLen; i < journal.certs.length; i++) {
bytes32 certHash = journal.certs[i];
if (revokedCerts[certHash]) {
continue;
}
trustedIntermediateCerts[certHash] = journal.certExpiries[i];
}
}
Expand All @@ -586,9 +647,19 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
* This function performs comprehensive validation:
* 1. Checks if the initial ZK verification was successful
* 2. Validates the root certificate matches the trusted root
* 3. Ensures all trusted certificates are still valid (not revoked)
* 4. Validates the attestation timestamp is within acceptable range
* 5. Caches newly discovered certificates for future use
* 3. Ensures all trusted certificates in the prefix are still valid (not revoked, not expired)
* 4. Ensures no certificate in the suffix has been revoked, regardless of `trustedCertsPrefixLen`
* 5. Validates the attestation timestamp is within acceptable range
* 6. Caches newly discovered certificates for future use
*
* The suffix-side revocation check (step 4) is the load-bearing fix for the
* `revokeCert` durability gap exposed under the production
* `trustedCertsPrefixLen = 1` configuration. Without it, Pass 1 only walks
* the root and a journal whose chain traverses a revoked intermediate in
* the suffix would succeed and then re-cache the revoked entry via
* `_cacheNewCert`. Rejecting any suffix entry present in `revokedCerts`
* makes revocation durable independently of the journal-supplied prefix
* length.
*
* The timestamp validation converts milliseconds to seconds and checks:
* - Attestation is not too old (timestamp + maxTimeDiff > block.timestamp)
Expand All @@ -605,7 +676,8 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
journal.result = VerificationResult.RootCertNotTrusted;
return journal;
}
// Check every trusted certificate to ensure none have been revoked
// Pass 1: trusted prefix — root must match the on-chain root, and every
// intermediate must still hold a non-expired cached entry.
for (uint256 i = 0; i < journal.trustedCertsPrefixLen; i++) {
bytes32 certHash = journal.certs[i];
if (i == 0) {
Expand All @@ -615,14 +687,31 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
}
continue;
}
// `revokeCert` zeroes `trustedIntermediateCerts[certHash]`, so the
// expiry check below already catches a revoked cert reached through
// the prefix path. The explicit `revokedCerts` guard is retained as
// defense-in-depth against future code paths that might re-cache
// before this loop runs.
if (revokedCerts[certHash]) {
journal.result = VerificationResult.IntermediateCertsNotTrusted;
return journal;
}
uint64 expiry = trustedIntermediateCerts[certHash];
if (block.timestamp > expiry) {
journal.result = VerificationResult.IntermediateCertsNotTrusted;
return journal;
}
}
// Check any remaining certificates in the chain that are not yet trusted
// Pass 2: suffix — journal-supplied expiries plus a hard reject on any
// cert that the operator has explicitly revoked. This is the path that
// closes the production `trustedCertsPrefixLen = 1` bypass: a revoked
// intermediate in the suffix can no longer pass verification and then
// be silently re-cached.
for (uint256 i = journal.trustedCertsPrefixLen; i < journal.certs.length; i++) {
if (revokedCerts[journal.certs[i]]) {
journal.result = VerificationResult.IntermediateCertsNotTrusted;
return journal;
}
uint64 expiry = journal.certExpiries[i];
if (block.timestamp > expiry) {
journal.result = VerificationResult.InvalidTimestamp;
Expand Down Expand Up @@ -702,8 +791,8 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
}

/// @notice Semantic version.
/// @custom:semver 0.3.0
/// @custom:semver 0.4.0
function version() public pure virtual returns (string memory) {
return "0.3.0";
return "0.4.0";
}
}
Loading
Loading