Skip to content

Commit de07760

Browse files
committed
feat(tee-registrar): pre-check on-chain revokedCerts before CRL revocation
Adds an optional NitroEnclaveVerifier client to the registrar so that each CRL cycle pre-screens parsed cert chains against the on-chain revokedCerts mapping (CHAIN-4194 / Immunefi #75608 hardening) before issuing any revokeCert tx. Backwards compatible: the pre-check fail-opens against deployments that do not yet expose revokedCerts (selector-not-found reverts are caught, logged via warn!, and counted by the new onchain_revocation_check_errors metric). The companion onchain PR (base/contracts#276) is therefore not required to be deployed first or in lockstep — the registrar functions normally against either the existing or upgraded NitroEnclaveVerifier. Production CLI/env compatibility preserved: --crl-nitro-verifier-address and BASE_REGISTRAR_CRL_NITRO_VERIFIER_ADDRESS unchanged. Removes dead BoundlessConfig::nitro_verifier_address field and the unused --nitro-verifier-address flag (was wired to BoundlessArgs but never consumed by BoundlessProver).
1 parent 99b5457 commit de07760

11 files changed

Lines changed: 730 additions & 107 deletions

File tree

bin/prover-registrar/src/cli.rs

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ use base_proof_tee_registrar::{
2222
AwsDiscoveryConfig, AwsTargetGroupDiscovery, BoundlessConfig, CrlConfig,
2323
DEFAULT_CRL_FETCH_TIMEOUT_SECS, DEFAULT_MAX_ATTESTATION_AGE_SECS, DEFAULT_MAX_CONCURRENCY,
2424
DEFAULT_MAX_RECOVERY_ATTEMPTS, DEFAULT_MAX_TX_RETRIES, DEFAULT_TX_RETRY_DELAY_SECS,
25-
DEFAULT_UNHEALTHY_REGISTRATION_WINDOW_SECS, DriverConfig, ProverClient, ProvingConfig,
26-
RegistrarConfig, RegistrarError, RegistrarMetrics, RegistrationDriver, RegistryContractClient,
25+
DEFAULT_UNHEALTHY_REGISTRATION_WINDOW_SECS, DriverConfig, NitroVerifierClient,
26+
NitroVerifierContractClient, ProverClient, ProvingConfig, RegistrarConfig, RegistrarError,
27+
RegistrarMetrics, RegistrationDriver, RegistryContractClient,
2728
};
2829
use base_tx_manager::{BaseTxMetrics, SignerConfig, SimpleTxManager, TxManagerConfig};
2930
use clap::{Args, Parser, ValueEnum};
@@ -201,10 +202,6 @@ struct BoundlessArgs {
201202
)]
202203
boundless_max_recovery_attempts: u32,
203204

204-
/// `NitroEnclaveVerifier` contract address for certificate caching (optional).
205-
#[arg(long, env = cli_env!("NITRO_VERIFIER_ADDRESS"))]
206-
nitro_verifier_address: Option<Address>,
207-
208205
/// Maximum age (in seconds) of a recovered proof's attestation timestamp
209206
/// before it is considered stale. Should be slightly below the on-chain
210207
/// `MAX_AGE` to account for clock skew. Defaults to 3300 s (55 minutes).
@@ -226,8 +223,15 @@ struct CrlArgs {
226223
#[arg(long, env = cli_env!("CRL_CHECK_ENABLED"), default_value_t = false)]
227224
crl_check_enabled: bool,
228225

229-
/// `NitroEnclaveVerifier` contract address for `revokeCert` calls.
230-
/// Required when `--crl-check-enabled` is set.
226+
/// `NitroEnclaveVerifier` contract address. Required when
227+
/// `--crl-check-enabled` is set; consulted both for the durable on-chain
228+
/// `revokedCerts` pre-check and as the destination for outgoing
229+
/// `revokeCert` transactions.
230+
///
231+
/// The flag/env name retains the `crl-` prefix for backward compatibility
232+
/// with existing production deployments (introduced in #1984); operators
233+
/// in the wild set `BASE_REGISTRAR_CRL_NITRO_VERIFIER_ADDRESS` and
234+
/// renaming would silently break their startup.
231235
#[arg(long, env = cli_env!("CRL_NITRO_VERIFIER_ADDRESS"))]
232236
crl_nitro_verifier_address: Option<Address>,
233237

@@ -318,7 +322,6 @@ impl Cli {
318322
image_id: parse_image_id(image_id_hex)?,
319323
poll_interval: Duration::from_secs(self.boundless.boundless_poll_interval_secs),
320324
timeout: Duration::from_secs(self.boundless.boundless_timeout_secs),
321-
nitro_verifier_address: self.boundless.nitro_verifier_address,
322325
max_recovery_attempts: self.boundless.boundless_max_recovery_attempts,
323326
max_attestation_age: Duration::from_secs(
324327
self.boundless.max_attestation_age_secs,
@@ -497,6 +500,18 @@ impl Cli {
497500
config.l1_rpc_url.clone(),
498501
);
499502

503+
// Optional `NitroEnclaveVerifier` client for the on-chain durable
504+
// revocation pre-check. Built only when CRL checking is enabled and
505+
// the verifier address is configured; otherwise the driver falls
506+
// back to the AWS-CRL-only path (fail-open).
507+
let nitro_verifier: Option<Arc<dyn NitroVerifierClient>> =
508+
match (config.crl.enabled, config.crl.nitro_verifier_address) {
509+
(true, Some(verifier_address)) => Some(Arc::new(
510+
NitroVerifierContractClient::new(verifier_address, config.l1_rpc_url.clone()),
511+
)),
512+
_ => None,
513+
};
514+
500515
// ── 6. Build proof provider ──────────────────────────────────────────
501516
let proof_provider: Box<dyn AttestationProofProvider> = match config.proving {
502517
ProvingConfig::Boundless(ref boundless) => Box::new(BoundlessProver {
@@ -559,6 +574,7 @@ impl Cli {
559574
tx_manager,
560575
signer_client,
561576
driver_config,
577+
nitro_verifier,
562578
)
563579
.run()
564580
.await;

crates/proof/contracts/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,9 @@ pub use tee_prover_registry::{
3434
};
3535

3636
mod nitro_enclave_verifier;
37-
pub use nitro_enclave_verifier::INitroEnclaveVerifier;
37+
pub use nitro_enclave_verifier::{
38+
INitroEnclaveVerifier, NitroEnclaveVerifierClient, NitroEnclaveVerifierContractClient,
39+
};
3840

3941
mod error;
4042
pub use error::ContractError;
Lines changed: 101 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,79 @@
11
//! `NitroEnclaveVerifier` contract bindings.
22
//!
33
//! Used by the registrar to revoke intermediate certificates that appear on
4-
//! AWS Nitro CRL distribution lists. Only the `revokeCert` function is
5-
//! needed — all other `NitroEnclaveVerifier` interactions happen through the
6-
//! `TEEProverRegistry` during signer registration.
4+
//! AWS Nitro CRL distribution lists, and to consult the on-chain durable
5+
//! revocation sentinel before submitting registrations. All other
6+
//! `NitroEnclaveVerifier` interactions happen through the `TEEProverRegistry`.
77
8+
use alloy_primitives::{Address, FixedBytes};
9+
use alloy_provider::RootProvider;
810
use alloy_sol_types::sol;
11+
use async_trait::async_trait;
12+
13+
use crate::ContractError;
914

1015
// Interface mirrored from the canonical contract source:
11-
// https://github.com/base/contracts/blob/leopoldjoy/chain-3890-add-revoker-role-to-nitroenclaveverifier-for-automated-cert/src/multiproof/tee/NitroEnclaveVerifier.sol
16+
// https://github.com/base/contracts/blob/main/src/L1/proofs/tee/NitroEnclaveVerifier.sol
1217
sol! {
1318
/// `NitroEnclaveVerifier` contract interface (revocation subset).
1419
#[sol(rpc)]
1520
interface INitroEnclaveVerifier {
1621
/// Revokes a cached intermediate certificate by its accumulated path digest.
1722
function revokeCert(bytes32 certHash) external;
23+
24+
/// Returns whether the given accumulated-path-digest hash has been
25+
/// revoked. Persistent across cache overwrites.
26+
function revokedCerts(bytes32 certHash) external view returns (bool);
27+
}
28+
}
29+
30+
/// Reads the durable revocation sentinel from the on-chain
31+
/// `NitroEnclaveVerifier`.
32+
///
33+
/// The on-chain `revokedCerts` mapping is set by `revokeCert` and persists
34+
/// across `_cacheNewCert` overwrites. Consulting it before submitting a
35+
/// registration prevents racing the cache rewrite that would otherwise
36+
/// re-trust a previously revoked intermediate (CHAIN-4194 / Immunefi #75608).
37+
#[async_trait]
38+
pub trait NitroEnclaveVerifierClient: Send + Sync {
39+
/// Returns the on-chain address of the verifier contract this client
40+
/// is bound to. Used by callers that need to send transactions to the
41+
/// same contract (e.g. `revokeCert` calls go through a tx manager,
42+
/// not this client, so the destination address must be exposed
43+
/// rather than carried through a separate config field).
44+
fn address(&self) -> Address;
45+
46+
/// Returns `true` if the given accumulated-path-digest hash is currently
47+
/// marked as revoked on-chain.
48+
async fn is_revoked(&self, cert_hash: FixedBytes<32>) -> Result<bool, ContractError>;
49+
}
50+
51+
/// Concrete implementation backed by Alloy's sol-generated contract bindings.
52+
#[derive(Debug)]
53+
pub struct NitroEnclaveVerifierContractClient {
54+
contract: INitroEnclaveVerifier::INitroEnclaveVerifierInstance<RootProvider>,
55+
}
56+
57+
impl NitroEnclaveVerifierContractClient {
58+
/// Creates a new client for the given verifier address and L1 RPC URL.
59+
pub fn new(address: Address, l1_rpc_url: url::Url) -> Self {
60+
let provider = RootProvider::new_http(l1_rpc_url);
61+
let contract = INitroEnclaveVerifier::INitroEnclaveVerifierInstance::new(address, provider);
62+
Self { contract }
63+
}
64+
}
65+
66+
#[async_trait]
67+
impl NitroEnclaveVerifierClient for NitroEnclaveVerifierContractClient {
68+
fn address(&self) -> Address {
69+
*self.contract.address()
70+
}
71+
72+
async fn is_revoked(&self, cert_hash: FixedBytes<32>) -> Result<bool, ContractError> {
73+
self.contract.revokedCerts(cert_hash).call().await.map_err(|e| ContractError::Call {
74+
context: format!("revokedCerts({cert_hash})"),
75+
source: e,
76+
})
1877
}
1978
}
2079

@@ -30,20 +89,49 @@ mod tests {
3089
const TEST_CERT_HASH: FixedBytes<32> =
3190
b256!("0000000000000000000000000000000000000000000000000000000000000001");
3291

33-
/// Expected ABI-encoded length for `revokeCert(bytes32)`:
92+
/// Expected ABI-encoded length for a `bytes32`-only call:
3493
/// 4 bytes (selector) + 32 bytes (bytes32 argument).
35-
const REVOKE_CERT_ENCODED_LEN: usize = 4 + 32;
94+
const BYTES32_CALL_ENCODED_LEN: usize = 4 + 32;
3695

96+
/// Parameterised over each `bytes32`-only call in the interface so any
97+
/// future additions only need a new `#[case]` line.
98+
///
99+
/// `selector` is computed at case-evaluation time (not const), which is
100+
/// fine because rstest case arguments are runtime expressions.
37101
#[rstest]
38-
fn revoke_cert_abi_encodes_correctly() {
39-
let call = INitroEnclaveVerifier::revokeCertCall { certHash: TEST_CERT_HASH };
40-
let encoded = call.abi_encode();
41-
assert_eq!(encoded.len(), REVOKE_CERT_ENCODED_LEN);
42-
assert_eq!(&encoded[..4], &INitroEnclaveVerifier::revokeCertCall::SELECTOR);
102+
#[case::revoke_cert(
103+
INitroEnclaveVerifier::revokeCertCall { certHash: TEST_CERT_HASH }.abi_encode(),
104+
INitroEnclaveVerifier::revokeCertCall::SELECTOR,
105+
)]
106+
#[case::revoked_certs(
107+
INitroEnclaveVerifier::revokedCertsCall { certHash: TEST_CERT_HASH }.abi_encode(),
108+
INitroEnclaveVerifier::revokedCertsCall::SELECTOR,
109+
)]
110+
fn bytes32_call_abi_encodes_correctly(
111+
#[case] encoded: Vec<u8>,
112+
#[case] selector: [u8; 4],
113+
) {
114+
assert_eq!(encoded.len(), BYTES32_CALL_ENCODED_LEN);
115+
assert_eq!(&encoded[..4], &selector);
43116
}
44117

118+
/// Selectors for every revocation-related entry point must be non-zero
119+
/// (catches a degenerate sol! macro expansion) and distinct from each
120+
/// other (catches a copy-paste collision in the interface block).
45121
#[rstest]
46-
fn selector_is_nonzero() {
47-
assert_ne!(INitroEnclaveVerifier::revokeCertCall::SELECTOR, [0u8; 4]);
122+
fn revocation_selectors_are_nonzero_and_distinct() {
123+
let selectors = [
124+
INitroEnclaveVerifier::revokeCertCall::SELECTOR,
125+
INitroEnclaveVerifier::revokedCertsCall::SELECTOR,
126+
];
127+
128+
for selector in &selectors {
129+
assert_ne!(selector, &[0u8; 4], "selector must be non-zero: {selector:?}");
130+
}
131+
for (i, a) in selectors.iter().enumerate() {
132+
for b in &selectors[i + 1..] {
133+
assert_ne!(a, b, "selectors must be distinct: {a:?} vs {b:?}");
134+
}
135+
}
48136
}
49137
}

crates/proof/tee/registrar/src/config.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,8 +48,6 @@ pub struct BoundlessConfig {
4848
pub poll_interval: Duration,
4949
/// Proof generation timeout.
5050
pub timeout: Duration,
51-
/// `NitroEnclaveVerifier` contract address for certificate caching (optional).
52-
pub nitro_verifier_address: Option<Address>,
5351
/// Maximum number of deterministic request-ID slots to probe when
5452
/// recovering in-flight proofs after an instance rotation.
5553
pub max_recovery_attempts: u32,
@@ -68,7 +66,6 @@ impl std::fmt::Debug for BoundlessConfig {
6866
.field("image_id", &self.image_id)
6967
.field("poll_interval", &self.poll_interval)
7068
.field("timeout", &self.timeout)
71-
.field("nitro_verifier_address", &self.nitro_verifier_address)
7269
.field("max_recovery_attempts", &self.max_recovery_attempts)
7370
.field("max_attestation_age", &self.max_attestation_age)
7471
.finish()
@@ -93,8 +90,10 @@ pub struct CrlConfig {
9390
/// Whether CRL checking is enabled. When disabled, no CRL fetches or
9491
/// `revokeCert` transactions are attempted. Defaults to `false`.
9592
pub enabled: bool,
96-
/// `NitroEnclaveVerifier` contract address on L1 for `revokeCert` calls.
97-
/// Required when `enabled` is `true`.
93+
/// `NitroEnclaveVerifier` contract address on L1. Required when
94+
/// `enabled` is `true`; consulted both for the durable on-chain
95+
/// `revokedCerts` pre-check and as the destination for outgoing
96+
/// `revokeCert` transactions.
9897
pub nitro_verifier_address: Option<Address>,
9998
/// HTTP timeout for CRL fetches from AWS S3 endpoints.
10099
pub fetch_timeout: Duration,

0 commit comments

Comments
 (0)