For the complete documentation index, see llms.txt. This page is also available as Markdown.

75608 sc low production registrar s default trusted certs prefix 1 voids revokecert on the next routine signer registration attacker holding the revoked ca key registers a tee signer that

Report ID: #75608 Report Type: Smart Contract Report severity: Low Target: https://github.com/base/contracts/tree/v8.1.0/src/multiproof Impacts:

  • Registering a malicious or unauthorized TEE enclave signer in the TEEProverRegistry without valid attestation (PCR0 mismatch or missing attestation)

Description

Distinction from Cantina TEE Audit 2 Finding #5

Cantina #5 (verbatim recommendation): "Trusted certificates can only be added by a trusted party, this reduces the impact of this issue. Consider adding comments to mention the fact that revoked certificates can be trusted again." Severity: Informational. Team's only response: a single NatSpec line at NitroEnclaveVerifier.sol:345 via PR-251.

The new material in this report:

  • Production hardcodes the bypass-shaped prefix-len. base/bin/prover-registrar/src/cli.rs:44 defines DEFAULT_TRUSTED_CERTS_PREFIX: u8 = 1, used unmodified at cli.rs:509,522. Every registrar-produced attestation carries journal.trustedCertsPrefixLen = 1. Pass 1 of _verifyJournal therefore only checks the root; every Nitro intermediate is in the suffix; _cacheNewCert rewrites every intermediate's storage entry on every successful verification. The "trusted party" Cantina relied on for mitigation is the registrar -- the same component whose default config produces the bypass-shaped journal.

  • Off-chain registrar does not consult on-chain revocation state. base/crates/proof/tee/registrar/src/driver.rs:404-435 checks AWS-published CRLs only, fail-open on errors. It does not read NitroEnclaveVerifier.trustedIntermediateCerts. revokeCert exists for the case where the operator suspects compromise before AWS publishes; in that interval the registrar continues forwarding registrations through the suspect CA, every one of which restores the cache.

  • End-to-end chain to a registered TEE signer. With the revoked CA key, an attacker mints a leaf for an attacker-controlled keypair, runs the open-source enclave image (PCR0 deterministic), and submits via the registrar. After cache restore + signer registration, signerImageHash[attacker] == TEE_IMAGE_HASH. TEEVerifier.verify admits proofs signed by the attacker's key (subject to the proposer gate); AggregateVerifier._verifyTeeProof admits them via the same TEEVerifier.verify call path. The PoC drives this end-to-end through registerSigner and TEEVerifier.verify.

Per Immunefi's known-issue rules: "Simply recognizing an attack vector or a general risk does not qualify as knowing the issue; both the exploit method and its impact must be fully understood" -- Cantina recognized the vector and bounded the impact via a mitigation the production code does not implement. "If the issue was known but not acted upon ... the submission may still be eligible" -- the team's only action on Cantina #5 was a one-line NatSpec; no code remediation. "Different execution methods or new insights can make it a distinct issue" -- the production-default prefix-len, the registrar's CRL-only filtering posture, and the chain to accepted TEE proofs are new material.

Vulnerability Details

_verifyJournal (NitroEnclaveVerifier.sol:597) splits chain validation into two passes:

// Pass 1 -- only iterates indices < trustedCertsPrefixLen, checks on-chain trust
for (uint256 i = 0; i < journal.trustedCertsPrefixLen; i++) {
    bytes32 certHash = journal.certs[i];
    if (i == 0) {
        if (certHash != rootCert) {
            journal.result = VerificationResult.RootCertNotTrusted;
            return journal;
        }
        continue;
    }
    uint64 expiry = trustedIntermediateCerts[certHash];                  // line 615
    if (block.timestamp > expiry) {
        journal.result = VerificationResult.IntermediateCertsNotTrusted;
        return journal;
    }
}
// Pass 2 -- only the journal-supplied expiry, never storage
for (uint256 i = journal.trustedCertsPrefixLen; i < journal.certs.length; i++) {
    uint64 expiry = journal.certExpiries[i];                             // line 623
    if (block.timestamp > expiry) {
        journal.result = VerificationResult.InvalidTimestamp;
        return journal;
    }
}
// Timestamp checks omitted; on success, _cacheNewCert is invoked.
_cacheNewCert(journal);                                                  // line 634

_cacheNewCert (NitroEnclaveVerifier.sol:574):

revokeCert (NitroEnclaveVerifier.sol:347-353) zeros the storage entry. With trustedCertsPrefixLen = 1, Pass 1 only checks the root and skips every intermediate; Pass 2 reads journal-supplied expiries; _cacheNewCert rewrites every intermediate's trustedIntermediateCerts entry to journal.certExpiries[i]. There is no on-chain state distinguishing "previously revoked" from "never seen". Mainnet relevance: DEFAULT_TRUSTED_CERTS_PREFIX = 1 is unoverrideable in the shipped registrar binary (no CLI flag, no config field, no network conditional). The prover-side type comment at boundless.rs:75-76 documents the intent: "Number of trusted certificates in the chain (typically 1 for root-only)."

Chain into a registered signer admitted by AggregateVerifier

registerSigner (TEEProverRegistry.sol:145) calls NITRO_VERIFIER.verify, then on Success records isRegisteredSigner[enclaveAddress] = true (line 170) and signerImageHash[enclaveAddress] = pcr0Hash (line 171). TEEVerifier.verify (TEEVerifier.sol:81,86,94) admits proofs when proposer, registered-signer, and image-hash gates all pass. AggregateVerifier._verifyTeeProof (AggregateVerifier.sol:870-899, TEE_IMAGE_HASH at line 893, TEE_VERIFIER.verify(...) at line 899) accepts the resulting TEE proof.

Attack chain after revokeCert(C):

  1. Attacker holds C's private key (the premise of the threat that motivated the revocation).

  2. Attacker mints a leaf cert under C for a keypair they control.

  3. Attacker runs the open-source enclave image -- PCR0 deterministically equals the live TEE_IMAGE_HASH.

  4. The Nitro attestation document carries [root, C, attacker_leaf], attacker's leaf public key, and matching PCR0.

  5. The off-chain prover (or the attacker simulating it) packages the attestation with trustedCertsPrefixLen = 1 (the production default).

  6. Manager forwards via registerSigner. _verifyJournal returns Success; _cacheNewCert rewrites C. registerSigner records the attacker's enclave address with signerImageHash = TEE_IMAGE_HASH.

  7. The attacker's keypair signs TEE proofs that pass all three TEEVerifier.verify gates (proposer, registered-signer, image-hash). AggregateVerifier._verifyTeeProof (AggregateVerifier.sol:870-899) delegates to TEE_VERIFIER.verify(...) at line 899, so any proof admitted by TEEVerifier.verify is admitted by AggregateVerifier's TEE path on the same journal.

NatSpec note

NitroEnclaveVerifier.sol:345: "A revoked cert can be trusted again by reproving it." A literal reading -- any reproving rebuilds the cache, including reproving with the same compromised CA -- leaves revokeCert with no on-chain effect under the production prefix-len configuration. The function is onlyOwnerOrRevoker, emits a dedicated CertRevoked event, declares a dedicated CertificateNotFound error, and is the contract's only on-chain handle for responding to upstream-CA compromise. The narrower reading consistent with that surface -- re-trust applies after the upstream issue is resolved (e.g., AWS rotates the intermediate, attacker key recovered) -- is not enforced by the contract or the registrar.

Impact

Primary -- Tier B (High): SC bracket "Registering a malicious or unauthorized TEE enclave signer in the TEEProverRegistry without valid attestation (PCR0 mismatch or missing attestation)." After revokeCert(C), the registry has explicitly withdrawn its trust from chains anchored on C. An attestation traversing C is, per the registry's published on-chain trust state, an unauthorized attestation -- exactly the condition the High bracket targets. The bypass admits exactly such an attestation; the chain (PoC-verified) reaches AggregateVerifier-accepted TEE proofs signed by the actor revokeCert was deployed to lock out.

Floor -- Tier A (Medium): SC bracket "Griefing that causes damage to users or the protocol without direct profit motive for the attacker." The on-chain emergency primitive revokeCert has no durability under the production registrar configuration. No attacker key, no Base-infrastructure compromise: routine signer registration restores the cache.

Downgrade clauses do not apply

Program scope applies two downgrade clauses to TEE-related reports. Neither lands cleanly here:

  • "Any report relying on an invalid TEE or ZK proof will be downgraded, especially TEE proofs unless it can be shown that a key compromise is unnecessary." The Tier B PoC submits a cryptographically valid Nitro attestation: AWS root signs the intermediate, the (revoked) intermediate signs the attacker's leaf, the leaf attests a real PCR0 measured by an actual enclave running the open-source image. No signature is invalid; no PCR0 is missing or mismatched. The bug is not that an invalid proof is accepted -- the bug is that a valid proof under an explicitly-revoked CA is accepted, contradicting the on-chain trust state. The clause's qualifier ("unless it can be shown that a key compromise is unnecessary") also rules out automatic downgrade: Tier A demonstrates the on-chain effect with no attacker key.

  • "Downgrade valid reports if the attack requires compromising Base-operated infrastructure." The Tier B chain does not require compromising Base's registrar, sequencer, or any Base-operated key. It requires the attacker to obtain the revoked CA's key -- which is the premise that motivates revokeCert in the first place. revokeCert is the on-chain incident-response primitive for upstream-CA compromise; reports that demonstrate its bypass under exactly the threat model it was designed for cannot be downgraded by appealing to that same threat model. Routine target-group enrollment for an attacker-run enclave is also operationally plausible without Base-side compromise (any AWS Nitro instance under the still-valid intermediate that the operator's discovery layer accepts).

The High bracket text reads "Registering a malicious or unauthorized TEE enclave signer in the TEEProverRegistry without valid attestation (PCR0 mismatch or missing attestation)." The parenthetical is illustrative, not exhaustive -- the load-bearing qualifier is "without valid attestation". Under the registry's own published trust state (revokeCert(C)), an attestation through C is not valid for registry trust purposes.

References

  • base/bin/prover-registrar/src/cli.rs:44,509,522 -- DEFAULT_TRUSTED_CERTS_PREFIX = 1, hardcoded.

  • base/crates/proof/tee/nitro-attestation-prover/src/boundless.rs:75-76,276, direct.rs:66 -- prover plumbs prefix-len into journal.trustedCertsPrefixLen.

  • base/crates/proof/tee/registrar/src/driver.rs:404-435 -- registrar's CRL check (AWS-only, fail-open, no on-chain consultation).

  • contracts/src/multiproof/tee/NitroEnclaveVerifier.sol:347-353 -- revokeCert.

  • contracts/src/multiproof/tee/NitroEnclaveVerifier.sol:574-579 -- _cacheNewCert (unconditional overwrite past prefix).

  • contracts/src/multiproof/tee/NitroEnclaveVerifier.sol:597-636 -- _verifyJournal: Pass 1 (606-620), Pass 2 (622-628).

  • contracts/src/multiproof/tee/NitroEnclaveVerifier.sol:345 -- NatSpec note added in PR-251.

  • contracts/src/multiproof/tee/NitroEnclaveVerifier.sol:487 -- verify proofSubmitter gate.

  • contracts/interfaces/multiproof/tee/INitroEnclaveVerifier.sol:56 -- VerifierJournal.trustedCertsPrefixLen.

  • contracts/src/multiproof/tee/TEEProverRegistry.sol:145,170,171 -- registerSigner writes isRegisteredSigner, signerImageHash.

  • contracts/src/multiproof/tee/TEEVerifier.sol:81,86,94 -- proposer / registered-signer / image-hash gates.

  • contracts/src/multiproof/AggregateVerifier.sol:870-899 -- _verifyTeeProof; TEE_VERIFIER.verify(...) at line 899.

  • contract-deployments/sepolia/2026-04-20-activate-multiproof/script/SetupNitroEnclaveVerifier.s.sol:39-41 -- Sepolia wires proofSubmitter to the registry proxy.

Cited line numbers from local snapshots: contracts at commit 01dad23 (tag v8.1.0), base at v0.8.0-rc.28.

Proof of Concept

Three Foundry tests across two files, fully inlined below. Mocking is limited to IRiscZeroVerifier (the off-chain ZK verifier; the defect lives in the Solidity trust-cache transition). Tier B lives in a separate file because TEEProverRegistry pins source pragma =0.8.15 while NitroEnclaveVerifier transitively requires >=0.8.20 via ISP1Verifier; no single source pragma satisfies both, so the Tier B file deploys NitroEnclaveVerifier from artifact bytecode via vm.deployCode and interacts via a minimal interface.

Tier A -- contracts/test/multiproof/NitroRevocationBypass.t.sol

Tier B -- contracts/test/multiproof/NitroRevocationBypassTierB.t.sol

How to run

Run output:

Suggested Fix

Maintain a separate revocation sentinel that survives _cacheNewCert overwrites and reject revoked hashes everywhere a cert is consulted. Diffs against the current NitroEnclaveVerifier.sol:

The off-chain registrar should additionally read the on-chain revocation state and reject attestations whose chains traverse hashes in revokedCerts, closing the gap between the on-chain emergency action and the off-chain forwarding path. Re-trust must require an explicit admin action (e.g., a new unrevokeCert owner-only function that clears the sentinel).

Was this helpful?