libid_contracts/bindings/ceremony.rs
1//! Bindings for the ceremony verification path (`solidity/contracts/ceremony/`):
2//! the Notary Service every notarized session is authenticated through, the
3//! Proof Verifier that routes a claim to the Platform Verifier registered for
4//! its version, the three launch Platform Verifiers it routes to, and the
5//! Google JWT root list the `google/v1` verifier reads.
6//!
7//! `verify` is on none of the Platform Verifier interfaces, for the reason it
8//! is on neither `NotaryService` nor `CeremonyProofVerifier`: a contract on
9//! the route calls it with the fee attached, and the decoded claim comes back
10//! to that contract. What an operator does from here is initialize and
11//! rotate the trust roots — see
12//! [`platform_verifier`](crate::platform_verifier) for the initializer that
13//! checks the rules first.
14
15/// Bindings for `ceremony/NotaryService.sol` (which implements
16/// `INotaryService`).
17///
18/// Authenticates one attestation and charges one fee for it. The digest is
19/// derived from the attested bytes on chain, never taken from the caller
20/// (REQ-COMMON-33), which is why `verify` is not on this interface: a
21/// consumer contract calls it with the fee attached, and the decoded record
22/// comes back to that contract, not to an off-chain reader. What an operator
23/// does from here is hold the trusted key set, set the fee and withdraw what
24/// accrued.
25#[allow(clippy::too_many_arguments, unused_attributes)]
26mod notary_service_inner {
27 use alloy::sol;
28
29 sol! {
30 #[sol(rpc)]
31 interface NotaryService {
32 /// `notary_` is the first trusted key; `fee_` may be zero (a
33 /// deployment may meter at no charge, and the exact-value rule
34 /// still applies).
35 function initialize(address owner_, address notary_, uint256 fee_) external;
36 /// What one verification costs, in the chain's native asset.
37 /// Readable before a submission is built, so it can be bounded.
38 function fee() external view returns (uint256);
39 function setFee(uint256 fee_) external;
40 /// Add or remove a trusted notary key. Several are held at once so
41 /// a rotation can overlap: add the incoming key, remove the
42 /// outgoing one once nothing can still present under it.
43 function setNotary(address key, bool trusted_) external;
44 function isTrustedNotary(address key) external view returns (bool);
45 /// Fees accrue here rather than being forwarded per verification.
46 function withdraw(address to, uint256 amount) external;
47
48 function owner() external view returns (address);
49 function pendingOwner() external view returns (address);
50 function transferOwnership(address newOwner) external;
51 function acceptOwnership() external;
52
53 event FeeChanged(uint256 previousFee, uint256 newFee);
54 event NotaryTrustChanged(address indexed key, bool trusted);
55 event FeesWithdrawn(address indexed to, uint256 amount);
56 }
57 }
58}
59
60pub use notary_service_inner::NotaryService;
61
62/// Bindings for `ceremony/CeremonyProofVerifier.sol` (which implements
63/// `IProofVerifier`).
64///
65/// The Supported Version Set: which Platform Verifier answers for a
66/// `(platformId, verifierVersion)` pair. Governance registers one with
67/// `setVerifier`; `IdentityRegistry.bind` dispatches through `verify`, which is
68/// not on this interface for the same reason `NotaryService.verify` is not —
69/// it is called by the consumer contract with the fee attached.
70#[allow(clippy::too_many_arguments, unused_attributes)]
71mod proof_verifier_inner {
72 use alloy::sol;
73
74 sol! {
75 #[sol(rpc)]
76 interface CeremonyProofVerifier {
77 function initialize(address owner_) external;
78 /// Register (or, with the zero address, remove) the Platform
79 /// Verifier for a pair. The verifier must serve `platformId`.
80 function setVerifier(bytes32 platformId, uint16 verifierVersion, address verifier) external;
81 /// The Platform Verifier registered for a pair, or zero.
82 function verifierOf(bytes32 platformId, uint16 verifierVersion) external view returns (address);
83 /// What one claim under this pair costs: the registered
84 /// verifier's quote, forwarded whole.
85 function quote(bytes32 platformId, uint16 verifierVersion) external view returns (uint256);
86 /// Whether any version is registered for the platform at all.
87 function verifiesPlatform(bytes32 platformId) external view returns (bool);
88 /// This chain's identifier, as the digest construction takes it.
89 function chainId() external view returns (bytes32);
90
91 function owner() external view returns (address);
92 function pendingOwner() external view returns (address);
93 function transferOwnership(address newOwner) external;
94 function acceptOwnership() external;
95
96 event VerifierConfigured(bytes32 indexed platformId, uint16 indexed verifierVersion, address verifier);
97 }
98 }
99}
100
101pub use proof_verifier_inner::CeremonyProofVerifier;
102
103/// Bindings for `ceremony/GoogleJwtRoots.sol` — the signing keys the
104/// `google/v1` Platform Verifier trusts, and until when. Starts EMPTY:
105/// Google names bind only once a notarized reading of Google's JWKS has
106/// landed here.
107///
108/// The list is two generations of Google's key set and nothing else:
109/// `current` is the latest reading applied, `previous` the reading before
110/// it, kept for the tokens still in flight under a key Google has since
111/// dropped. A newer reading of the same set restarts `current`'s clock
112/// (`ReadingRefreshed`); a newer reading of a different set shifts `current`
113/// into `previous` and drops what `previous` held (`KeysRotated`). A
114/// generation is trusted until `READING_LIFETIME` after its reading's own
115/// `createdAt`, so there is nothing to prune or untrust by hand.
116///
117/// A rotation is an ordinary notarized session: the keeper reveals the whole
118/// `GET /oauth2/v3/certs` exchange, `rotate` hands the attested bytes and the
119/// notary's proof to the Notary Service with the Notary Fee attached (read
120/// it with `quoteRotation`, which is the service's `fee()`), and the contract
121/// reads the JWKS out of the transcript it vouched for.
122#[allow(clippy::too_many_arguments, unused_attributes)]
123mod google_jwt_roots_inner {
124 use alloy::sol;
125
126 sol! {
127 #[sol(rpc)]
128 interface GoogleJwtRoots {
129 /// One reading of Google's key set: the notary's clock, and the
130 /// limb hash of every modulus it listed, in Google's order.
131 #[derive(Debug, serde::Serialize, serde::Deserialize)]
132 struct Generation {
133 uint64 observedAt;
134 bytes32[] moduli;
135 }
136
137 function initialize(address owner_, address notary_) external;
138 /// The Notary Service a rotation is verified through.
139 function notaryService() external view returns (address);
140 function setNotaryService(address notary_) external;
141 /// What one rotation costs beyond gas: the Notary Fee, forwarded
142 /// whole. `rotate` must be sent with exactly this value.
143 function quoteRotation() external view returns (uint256);
144 /// Permissionless. `attestedData` is the ceremony-common section
145 /// 9.1 record of the JWKS session, `proof` the notary's
146 /// authentication of it (a 65-byte EIP-191 signature today). A
147 /// reading dated no later than the current generation is refused
148 /// with `NotNewer(createdAt, observedAt)`, and the revert hands
149 /// the fee back.
150 function rotate(bytes calldata attestedData, bytes calldata proof) external payable;
151
152 /// What `GooglePlatformVerifier` reads: modulus hash -> when it
153 /// stops being trusted, zero when neither generation lists it.
154 function trustedHashExpiresAt(bytes32 modulusHash) external view returns (uint256);
155 /// Both generations, as stored.
156 function currentKeys() external view returns (Generation memory current, Generation memory previous);
157 /// The current generation's `observedAt`: the notary's clock on
158 /// the reading in force.
159 function freshestObservedAt() external view returns (uint256);
160 /// True until the current generation is guaranteed trusted
161 /// `RENEWAL_MARGIN` from now; true on an empty list.
162 function needsRotation() external view returns (bool);
163 function READING_LIFETIME() external view returns (uint256);
164 function RENEWAL_MARGIN() external view returns (uint256);
165 function MAX_KEYS() external view returns (uint256);
166
167 function owner() external view returns (address);
168 function pendingOwner() external view returns (address);
169 function transferOwnership(address newOwner) external;
170 function acceptOwnership() external;
171
172 event NotaryServiceChanged(address notary);
173 /// A different set landed: `kids` and `moduli` in the order
174 /// Google listed them, `observedAt` the notary's clock.
175 event KeysRotated(uint64 observedAt, string[] kids, bytes32[] moduli);
176 /// A newer reading of the set already current.
177 event ReadingRefreshed(uint64 observedAt);
178 }
179 }
180}
181
182pub use google_jwt_roots_inner::GoogleJwtRoots;
183
184/// Bindings for the two TLSNotary Platform Verifiers, `ceremony/XPlatformVerifier.sol`
185/// and `ceremony/GitHubPlatformVerifier.sol`. One interface serves both: they
186/// differ in the revealed layout they accept, not in the surface an operator
187/// touches, and each answers for itself through `platformId()`. The same
188/// module is exported as [`XPlatformVerifier`] and [`GitHubPlatformVerifier`],
189/// so a consumer names the contract it means.
190///
191/// The initializer takes what `PlatformVerifierBase.__PlatformVerifierBase_init`
192/// takes. `notary_` is required here (nonzero): a TLSNotary profile
193/// authenticates two attestations through it, and the base refuses a zero
194/// address for a profile whose Attestation Count is nonzero
195/// (`WrongNotaryForProfile`). `honkVerifierCodehash_` must equal
196/// `address(honkVerifier_).codehash` and be neither zero nor `keccak256("")`
197/// (`WrongVerifierArtifact`). The validity window is the profile's, fixed in
198/// the contract: `protocolParameters()` reads it, and nothing sets it.
199#[allow(clippy::too_many_arguments, unused_attributes)]
200mod tls_notary_platform_verifier_inner {
201 use alloy::sol;
202
203 sol! {
204 #[sol(rpc)]
205 interface TlsNotaryPlatformVerifier {
206 /// Derives so the built call can be compared and printed by the
207 /// initializer that assembles it.
208 #[derive(Debug, PartialEq, Eq)]
209 function initialize(
210 address owner_,
211 address notary_,
212 address honkVerifier_,
213 bytes32 honkVerifierCodehash_
214 ) external;
215
216 /// The identity platform this verifier serves: `keccak256` of the
217 /// platform's bare name. The Proof Verifier refuses to register it
218 /// under another platform.
219 function platformId() external view returns (bytes32);
220 /// What a submission must carry: one Notary Fee per attestation
221 /// the profile requires — two, for a TLSNotary profile.
222 function quote() external view returns (uint256);
223
224 function notaryService() external view returns (address);
225 function honkVerifier() external view returns (address);
226 /// The code hash of the artifact wired: the only handle on WHICH
227 /// circuit a deployed bb verifier answers for.
228 function honkVerifierCodehash() external view returns (bytes32);
229 function protocolParameters()
230 external
231 pure
232 returns (uint64 proofLifetime, uint64 maxFutureAttestationSkew, uint64 futureObservationAllowance);
233 /// Rotate the trust roots. The same rules as `initialize`: the
234 /// code hash names the artifact, and the call fails if the
235 /// address does not hold it.
236 function setTrustRoots(address notary_, address honkVerifier_, bytes32 honkVerifierCodehash_) external;
237
238 function owner() external view returns (address);
239 function pendingOwner() external view returns (address);
240 function transferOwnership(address newOwner) external;
241 function acceptOwnership() external;
242
243 event TrustRootsChanged(address notary, address honkVerifier, bytes32 honkVerifierCodehash);
244
245 /// A profile that verifies no attestation holds a Notary Service,
246 /// or one that verifies some holds none.
247 error WrongNotaryForProfile(bytes32 platformId, address notary);
248 error ZeroAddress();
249 /// The verifier at that address is not the artifact named.
250 error WrongVerifierArtifact(bytes32 expected, bytes32 found);
251 }
252 }
253}
254
255pub use tls_notary_platform_verifier_inner::{
256 TlsNotaryPlatformVerifier,
257 TlsNotaryPlatformVerifier as GitHubPlatformVerifier,
258 TlsNotaryPlatformVerifier as XPlatformVerifier,
259};
260
261/// Bindings for `ceremony/GooglePlatformVerifier.sol` — the `google/v1`
262/// profile.
263///
264/// A different shape from the other two: no notarized session, so no Notary
265/// Service, no fee, no proof lifetime and no attestation skew. The evidence
266/// is a signed ID Token whose `exp` is the whole validity ceiling, and the
267/// signing keys it trusts are read from `GoogleJwtRoots`.
268///
269/// `notary_` must therefore be the ZERO address: the base refuses a Notary
270/// Service for a profile whose Attestation Count is zero
271/// (`WrongNotaryForProfile`), because `notaryService()` would otherwise
272/// report a collaborator nothing on this path calls. `jwtRoots_` must be
273/// nonzero (`ZeroAddress`). The code hash follows the same rules as the
274/// TLSNotary verifiers'. `protocolParameters()` reads the profile's
275/// allowance, and a lifetime and skew of zero.
276#[allow(clippy::too_many_arguments, unused_attributes)]
277mod google_platform_verifier_inner {
278 use alloy::sol;
279
280 sol! {
281 #[sol(rpc)]
282 interface GooglePlatformVerifier {
283 #[derive(Debug, PartialEq, Eq)]
284 function initialize(
285 address owner_,
286 address notary_,
287 address honkVerifier_,
288 bytes32 honkVerifierCodehash_,
289 address jwtRoots_
290 ) external;
291
292 /// `keccak256("google")`.
293 function platformId() external view returns (bytes32);
294 /// Always zero: the profile verifies nothing that charges, and
295 /// `verify` refuses any value sent.
296 function quote() external view returns (uint256);
297
298 /// The root list the trusted moduli are read through.
299 function jwtRoots() external view returns (address);
300 function setJwtRoots(address roots) external;
301
302 function notaryService() external view returns (address);
303 function honkVerifier() external view returns (address);
304 function honkVerifierCodehash() external view returns (bytes32);
305 function protocolParameters()
306 external
307 pure
308 returns (uint64 proofLifetime, uint64 maxFutureAttestationSkew, uint64 futureObservationAllowance);
309 function setTrustRoots(address notary_, address honkVerifier_, bytes32 honkVerifierCodehash_) external;
310
311 function owner() external view returns (address);
312 function pendingOwner() external view returns (address);
313 function transferOwnership(address newOwner) external;
314 function acceptOwnership() external;
315
316 event JwtRootsChanged(address roots);
317 event TrustRootsChanged(address notary, address honkVerifier, bytes32 honkVerifierCodehash);
318
319 error WrongNotaryForProfile(bytes32 platformId, address notary);
320 error ZeroAddress();
321 error WrongVerifierArtifact(bytes32 expected, bytes32 found);
322 }
323 }
324}
325
326pub use google_platform_verifier_inner::GooglePlatformVerifier;
327
328#[cfg(test)]
329mod tests {
330 use alloy::sol_types::SolCall;
331
332 use super::*;
333 use crate::Artifacts;
334
335 /// A Platform Verifier binding and its vendored artifact come from one
336 /// tree, so every bound selector is one the compiled contract answers.
337 /// The initializer is the one that matters: an `initialize` the proxy's
338 /// implementation has no function for reaches its fallback, and the
339 /// proxy is left uninitialized for anyone to claim.
340 #[test]
341 fn every_bound_platform_verifier_selector_exists_in_its_artifact() {
342 let artifacts = Artifacts::embedded();
343 let check = |contract: &str, sig: &str, selector: [u8; 4]| {
344 let methods = artifacts.method_identifiers(contract).unwrap();
345 let found = methods
346 .get(sig)
347 .unwrap_or_else(|| panic!("{contract} has no {sig}"));
348 assert_eq!(*found, alloy::hex::encode(selector), "{contract}.{sig}");
349 };
350
351 macro_rules! bound {
352 ($contract:expr, $iface:ident, [$($call:ident),* $(,)?]) => {
353 $(check($contract, $iface::$call::SIGNATURE, $iface::$call::SELECTOR);)*
354 };
355 }
356
357 for contract in ["XPlatformVerifier", "GitHubPlatformVerifier"] {
358 bound!(
359 contract,
360 TlsNotaryPlatformVerifier,
361 [
362 initializeCall,
363 platformIdCall,
364 quoteCall,
365 notaryServiceCall,
366 honkVerifierCall,
367 honkVerifierCodehashCall,
368 protocolParametersCall,
369 setTrustRootsCall,
370 ownerCall,
371 pendingOwnerCall,
372 transferOwnershipCall,
373 acceptOwnershipCall,
374 ]
375 );
376 }
377 bound!(
378 "GooglePlatformVerifier",
379 GooglePlatformVerifier,
380 [
381 initializeCall,
382 platformIdCall,
383 quoteCall,
384 jwtRootsCall,
385 setJwtRootsCall,
386 notaryServiceCall,
387 honkVerifierCall,
388 honkVerifierCodehashCall,
389 protocolParametersCall,
390 setTrustRootsCall,
391 ownerCall,
392 pendingOwnerCall,
393 transferOwnershipCall,
394 acceptOwnershipCall,
395 ]
396 );
397
398 // The two initializers differ in shape, and the artifacts say so:
399 // the TLSNotary one is not on Google's contract, nor the reverse.
400 assert_ne!(
401 TlsNotaryPlatformVerifier::initializeCall::SELECTOR,
402 GooglePlatformVerifier::initializeCall::SELECTOR
403 );
404 let google = artifacts
405 .method_identifiers("GooglePlatformVerifier")
406 .unwrap();
407 assert!(
408 !google.contains_key(TlsNotaryPlatformVerifier::initializeCall::SIGNATURE)
409 );
410 let x = artifacts.method_identifiers("XPlatformVerifier").unwrap();
411 assert!(!x.contains_key(GooglePlatformVerifier::initializeCall::SIGNATURE));
412 }
413}