@@ -70,6 +70,19 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
7070 /// @dev Mapping from ZkCoProcessorType to its corresponding verifierProofId representation
7171 mapping (ZkCoProcessorType => bytes32 ) private _verifierProofIds;
7272
73+ /// @dev Persistent revocation sentinel for intermediate certificates.
74+ ///
75+ /// `revokeCert` zeroes `trustedIntermediateCerts[certHash]`, but the suffix-cache
76+ /// path in `_cacheNewCert` would otherwise restore that entry on the next
77+ /// successful verification whose chain traverses the revoked hash. This
78+ /// mapping survives `_cacheNewCert` overwrites and is consulted in
79+ /// `_verifyJournal`, `_cacheNewCert`, and `checkTrustedIntermediateCerts`,
80+ /// making `revokeCert` durable independently of the journal's `trustedCertsPrefixLen`.
81+ ///
82+ /// Re-trust requires an explicit `unrevokeCert` admin call; it is never
83+ /// granted as a side effect of verification.
84+ mapping (bytes32 => bool ) public revokedCerts;
85+
7386 // ============ Custom Errors ============
7487
7588 /// @dev Error thrown when an unsupported or unknown ZK coprocessor type is used
@@ -114,6 +127,9 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
114127 /// @dev Thrown when caller is neither the owner nor the revoker
115128 error CallerNotOwnerOrRevoker ();
116129
130+ /// @dev Thrown when `unrevokeCert` is called for a hash that is not currently revoked
131+ error CertificateNotRevoked (bytes32 certHash );
132+
117133 // ============ Events ============
118134
119135 /// @dev Emitted when a new verifier program ID is added/updated
@@ -146,6 +162,9 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
146162 /// @dev Event emitted when a certificate is revoked
147163 event CertRevoked (bytes32 certHash );
148164
165+ /// @dev Event emitted when a previously revoked certificate is explicitly re-trusted
166+ event CertUnrevoked (bytes32 certHash );
167+
149168 /// @dev Event emitted when the maximum time difference is updated
150169 event MaxTimeDiffUpdated (uint64 newMaxTimeDiff );
151170
@@ -265,6 +284,11 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
265284 revert RootCertMismatch (rootCertHash, certs[0 ]);
266285 }
267286 for (uint256 j = 1 ; j < certs.length ; j++ ) {
287+ // Stop counting at any revoked entry so off-chain callers cannot derive a
288+ // prefix-len that walks past a revoked cert and then claim it as the trusted boundary.
289+ if (revokedCerts[certs[j]]) {
290+ break ;
291+ }
268292 uint64 expiry = trustedIntermediateCerts[certs[j]];
269293 if (block .timestamp > expiry) {
270294 break ;
@@ -332,7 +356,7 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
332356 }
333357
334358 /**
335- * @dev Revokes a trusted intermediate certificate
359+ * @dev Revokes a trusted intermediate certificate.
336360 * @param certHash Hash of the certificate to revoke
337361 *
338362 * Requirements:
@@ -342,16 +366,46 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
342366 * This function allows the owner or revoker to revoke compromised intermediate certificates
343367 * without affecting the root certificate or other trusted certificates.
344368 *
345- * Note: A revoked cert can be trusted again by reproving it.
369+ * Durability: in addition to clearing `trustedIntermediateCerts[certHash]`, this
370+ * function flips the persistent `revokedCerts[certHash]` sentinel. The sentinel
371+ * survives subsequent `_cacheNewCert` overwrites and causes both `_verifyJournal`
372+ * and `checkTrustedIntermediateCerts` to reject any chain whose suffix traverses
373+ * the revoked hash, regardless of the journal's `trustedCertsPrefixLen`. Reproving
374+ * the same chain therefore cannot silently restore trust; re-trust requires an
375+ * explicit `unrevokeCert` call by the owner.
346376 */
347377 function revokeCert (bytes32 certHash ) external onlyOwnerOrRevoker {
348378 if (trustedIntermediateCerts[certHash] == 0 ) {
349379 revert CertificateNotFound (certHash);
350380 }
351381 delete trustedIntermediateCerts[certHash];
382+ revokedCerts[certHash] = true ;
352383 emit CertRevoked (certHash);
353384 }
354385
386+ /**
387+ * @dev Explicitly re-trusts a previously revoked intermediate certificate.
388+ * @param certHash Hash of the certificate to un-revoke
389+ *
390+ * Requirements:
391+ * - Only callable by contract owner
392+ * - Certificate must currently be marked as revoked
393+ *
394+ * Clearing the revocation sentinel does not by itself restore the cached
395+ * expiry; the next successful verification whose chain traverses `certHash`
396+ * will re-cache it via `_cacheNewCert`. This two-step design (admin clears
397+ * the sentinel, verification re-caches the expiry) keeps re-trust an
398+ * explicit, owner-only action while still letting the normal cache path
399+ * supply the up-to-date `notAfter` timestamp.
400+ */
401+ function unrevokeCert (bytes32 certHash ) external onlyOwner {
402+ if (! revokedCerts[certHash]) {
403+ revert CertificateNotRevoked (certHash);
404+ }
405+ delete revokedCerts[certHash];
406+ emit CertUnrevoked (certHash);
407+ }
408+
355409 /**
356410 * @dev Updates the verifier program ID, adding the new version to the supported set
357411 * @param zkCoProcessor Type of ZK coprocessor
@@ -570,10 +624,25 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
570624 * This function automatically adds any certificates beyond the trusted length
571625 * to the trusted intermediate certificates set. This optimizes future verifications
572626 * by expanding the known trusted certificate set based on successful verifications.
627+ *
628+ * Revoked entries terminate caching: once `revokedCerts[certHash]` is set by
629+ * `revokeCert`, no successful verification will silently restore the cache,
630+ * regardless of the journal's `trustedCertsPrefixLen`. Because `certs[i+1]` is
631+ * signed by `certs[i]`, every descendant of a revoked cert inherits its trust
632+ * from a revoked parent and must not be cached either — so we `break` rather
633+ * than `continue` on the first revoked entry, matching `checkTrustedIntermediateCerts`.
634+ *
635+ * Note: in current control flow this guard is unreachable because `_verifyJournal`
636+ * Pass 2 already rejects any journal whose suffix contains a revoked digest before
637+ * `_cacheNewCert` is invoked. The check is retained as defense-in-depth against
638+ * future refactors. Re-trust requires an explicit `unrevokeCert`.
573639 */
574640 function _cacheNewCert (VerifierJournal memory journal ) internal {
575641 for (uint256 i = journal.trustedCertsPrefixLen; i < journal.certs.length ; i++ ) {
576642 bytes32 certHash = journal.certs[i];
643+ if (revokedCerts[certHash]) {
644+ break ;
645+ }
577646 trustedIntermediateCerts[certHash] = journal.certExpiries[i];
578647 }
579648 }
@@ -586,9 +655,19 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
586655 * This function performs comprehensive validation:
587656 * 1. Checks if the initial ZK verification was successful
588657 * 2. Validates the root certificate matches the trusted root
589- * 3. Ensures all trusted certificates are still valid (not revoked)
590- * 4. Validates the attestation timestamp is within acceptable range
591- * 5. Caches newly discovered certificates for future use
658+ * 3. Ensures all trusted certificates in the prefix are still valid (not revoked, not expired)
659+ * 4. Ensures no certificate in the suffix has been revoked, regardless of `trustedCertsPrefixLen`
660+ * 5. Validates the attestation timestamp is within acceptable range
661+ * 6. Caches newly discovered certificates for future use
662+ *
663+ * The suffix-side revocation check (step 4) is the load-bearing fix for the
664+ * `revokeCert` durability gap exposed under the production
665+ * `trustedCertsPrefixLen = 1` configuration. Without it, Pass 1 only walks
666+ * the root and a journal whose chain traverses a revoked intermediate in
667+ * the suffix would succeed and then re-cache the revoked entry via
668+ * `_cacheNewCert`. Rejecting any suffix entry present in `revokedCerts`
669+ * makes revocation durable independently of the journal-supplied prefix
670+ * length.
592671 *
593672 * The timestamp validation converts milliseconds to seconds and checks:
594673 * - Attestation is not too old (timestamp + maxTimeDiff > block.timestamp)
@@ -605,7 +684,8 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
605684 journal.result = VerificationResult.RootCertNotTrusted;
606685 return journal;
607686 }
608- // Check every trusted certificate to ensure none have been revoked
687+ // Pass 1: trusted prefix — root must match the on-chain root, and every
688+ // intermediate must still hold a non-expired cached entry.
609689 for (uint256 i = 0 ; i < journal.trustedCertsPrefixLen; i++ ) {
610690 bytes32 certHash = journal.certs[i];
611691 if (i == 0 ) {
@@ -615,14 +695,31 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
615695 }
616696 continue ;
617697 }
698+ // `revokeCert` zeroes `trustedIntermediateCerts[certHash]`, so the
699+ // expiry check below already catches a revoked cert reached through
700+ // the prefix path. The explicit `revokedCerts` guard is retained as
701+ // defense-in-depth against future code paths that might re-cache
702+ // before this loop runs.
703+ if (revokedCerts[certHash]) {
704+ journal.result = VerificationResult.IntermediateCertsNotTrusted;
705+ return journal;
706+ }
618707 uint64 expiry = trustedIntermediateCerts[certHash];
619708 if (block .timestamp > expiry) {
620709 journal.result = VerificationResult.IntermediateCertsNotTrusted;
621710 return journal;
622711 }
623712 }
624- // Check any remaining certificates in the chain that are not yet trusted
713+ // Pass 2: suffix — journal-supplied expiries plus a hard reject on any
714+ // cert that the operator has explicitly revoked. This is the path that
715+ // closes the production `trustedCertsPrefixLen = 1` bypass: a revoked
716+ // intermediate in the suffix can no longer pass verification and then
717+ // be silently re-cached.
625718 for (uint256 i = journal.trustedCertsPrefixLen; i < journal.certs.length ; i++ ) {
719+ if (revokedCerts[journal.certs[i]]) {
720+ journal.result = VerificationResult.IntermediateCertsNotTrusted;
721+ return journal;
722+ }
626723 uint64 expiry = journal.certExpiries[i];
627724 if (block .timestamp > expiry) {
628725 journal.result = VerificationResult.InvalidTimestamp;
@@ -702,8 +799,8 @@ contract NitroEnclaveVerifier is Ownable, INitroEnclaveVerifier, ISemver {
702799 }
703800
704801 /// @notice Semantic version.
705- /// @custom:semver 0.3 .0
802+ /// @custom:semver 0.4 .0
706803 function version () public pure virtual returns (string memory ) {
707- return "0.3 .0 " ;
804+ return "0.4 .0 " ;
708805 }
709806}
0 commit comments