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, rotate
11//! the trust roots and move the governance parameters — 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`; `IdentityNames.claim` 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 three parameters are capped by the `MAX_*`
198/// constants (`ParameterTooLarge`).
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 uint64 proofLifetime_,
215 uint64 maxFutureAttestationSkew_,
216 uint64 futureObservationAllowance_
217 ) external;
218
219 /// The identity platform this verifier serves: `keccak256` of the
220 /// platform's bare name. The Proof Verifier refuses to register it
221 /// under another platform.
222 function platformId() external view returns (bytes32);
223 /// What a submission must carry: one Notary Fee per attestation
224 /// the profile requires — two, for a TLSNotary profile.
225 function quote() external view returns (uint256);
226
227 function notaryService() external view returns (address);
228 function honkVerifier() external view returns (address);
229 /// The code hash of the artifact wired: the only handle on WHICH
230 /// circuit a deployed bb verifier answers for.
231 function honkVerifierCodehash() external view returns (bytes32);
232 function protocolParameters()
233 external
234 view
235 returns (uint64 proofLifetime, uint64 maxFutureAttestationSkew, uint64 futureObservationAllowance);
236 /// Rotate the trust roots. The same rules as `initialize`: the
237 /// code hash names the artifact, and the call fails if the
238 /// address does not hold it.
239 function setTrustRoots(address notary_, address honkVerifier_, bytes32 honkVerifierCodehash_) external;
240 function setProtocolParameters(
241 uint64 proofLifetime_,
242 uint64 maxFutureAttestationSkew_,
243 uint64 futureObservationAllowance_
244 ) external;
245
246 /// Ceilings on the three parameters, in seconds.
247 function MAX_PROOF_LIFETIME() external view returns (uint64);
248 function MAX_FUTURE_ATTESTATION_SKEW() external view returns (uint64);
249 function MAX_FUTURE_OBSERVATION_ALLOWANCE() external view returns (uint64);
250
251 function owner() external view returns (address);
252 function pendingOwner() external view returns (address);
253 function transferOwnership(address newOwner) external;
254 function acceptOwnership() external;
255
256 event TrustRootsChanged(address notary, address honkVerifier, bytes32 honkVerifierCodehash);
257 event ProtocolParametersChanged(
258 uint64 proofLifetime, uint64 maxFutureAttestationSkew, uint64 futureObservationAllowance
259 );
260
261 /// A profile that verifies no attestation holds a Notary Service,
262 /// or one that verifies some holds none.
263 error WrongNotaryForProfile(bytes32 platformId, address notary);
264 error ParameterTooLarge(uint64 provided, uint64 limit);
265 error ZeroAddress();
266 /// The verifier at that address is not the artifact named.
267 error WrongVerifierArtifact(bytes32 expected, bytes32 found);
268 }
269 }
270}
271
272pub use tls_notary_platform_verifier_inner::{
273 TlsNotaryPlatformVerifier,
274 TlsNotaryPlatformVerifier as GitHubPlatformVerifier,
275 TlsNotaryPlatformVerifier as XPlatformVerifier,
276};
277
278/// Bindings for `ceremony/GooglePlatformVerifier.sol` — the `google/v1`
279/// profile.
280///
281/// A different shape from the other two: no notarized session, so no Notary
282/// Service, no fee, no proof lifetime and no attestation skew. The evidence
283/// is a signed ID Token whose `exp` is the whole validity ceiling, and the
284/// signing keys it trusts are read from `GoogleJwtRoots`.
285///
286/// `notary_` must therefore be the ZERO address: the base refuses a Notary
287/// Service for a profile whose Attestation Count is zero
288/// (`WrongNotaryForProfile`), because `notaryService()` would otherwise
289/// report a collaborator nothing on this path calls. `jwtRoots_` must be
290/// nonzero (`ZeroAddress`). The code hash and the allowance follow the same
291/// rules as the TLSNotary verifiers'; the lifetime and skew read back as
292/// zero.
293#[allow(clippy::too_many_arguments, unused_attributes)]
294mod google_platform_verifier_inner {
295 use alloy::sol;
296
297 sol! {
298 #[sol(rpc)]
299 interface GooglePlatformVerifier {
300 #[derive(Debug, PartialEq, Eq)]
301 function initialize(
302 address owner_,
303 address notary_,
304 address honkVerifier_,
305 bytes32 honkVerifierCodehash_,
306 uint64 futureObservationAllowance_,
307 address jwtRoots_
308 ) external;
309
310 /// `keccak256("google")`.
311 function platformId() external view returns (bytes32);
312 /// Always zero: the profile verifies nothing that charges, and
313 /// `verify` refuses any value sent.
314 function quote() external view returns (uint256);
315
316 /// The root list the trusted moduli are read through.
317 function jwtRoots() external view returns (address);
318 function setJwtRoots(address roots) external;
319
320 function notaryService() external view returns (address);
321 function honkVerifier() external view returns (address);
322 function honkVerifierCodehash() external view returns (bytes32);
323 function protocolParameters()
324 external
325 view
326 returns (uint64 proofLifetime, uint64 maxFutureAttestationSkew, uint64 futureObservationAllowance);
327 function setTrustRoots(address notary_, address honkVerifier_, bytes32 honkVerifierCodehash_) external;
328 function setProtocolParameters(
329 uint64 proofLifetime_,
330 uint64 maxFutureAttestationSkew_,
331 uint64 futureObservationAllowance_
332 ) external;
333
334 function MAX_PROOF_LIFETIME() external view returns (uint64);
335 function MAX_FUTURE_ATTESTATION_SKEW() external view returns (uint64);
336 function MAX_FUTURE_OBSERVATION_ALLOWANCE() external view returns (uint64);
337
338 function owner() external view returns (address);
339 function pendingOwner() external view returns (address);
340 function transferOwnership(address newOwner) external;
341 function acceptOwnership() external;
342
343 event JwtRootsChanged(address roots);
344 event TrustRootsChanged(address notary, address honkVerifier, bytes32 honkVerifierCodehash);
345 event ProtocolParametersChanged(
346 uint64 proofLifetime, uint64 maxFutureAttestationSkew, uint64 futureObservationAllowance
347 );
348
349 error WrongNotaryForProfile(bytes32 platformId, address notary);
350 error ParameterTooLarge(uint64 provided, uint64 limit);
351 error ZeroAddress();
352 error WrongVerifierArtifact(bytes32 expected, bytes32 found);
353 }
354 }
355}
356
357pub use google_platform_verifier_inner::GooglePlatformVerifier;
358
359#[cfg(test)]
360mod tests {
361 use alloy::sol_types::SolCall;
362
363 use super::*;
364 use crate::Artifacts;
365
366 /// A Platform Verifier binding and its vendored artifact come from one
367 /// tree, so every bound selector is one the compiled contract answers.
368 /// The initializer is the one that matters: an `initialize` the proxy's
369 /// implementation has no function for reaches its fallback, and the
370 /// proxy is left uninitialized for anyone to claim.
371 #[test]
372 fn every_bound_platform_verifier_selector_exists_in_its_artifact() {
373 let artifacts = Artifacts::embedded();
374 let check = |contract: &str, sig: &str, selector: [u8; 4]| {
375 let methods = artifacts.method_identifiers(contract).unwrap();
376 let found = methods
377 .get(sig)
378 .unwrap_or_else(|| panic!("{contract} has no {sig}"));
379 assert_eq!(*found, alloy::hex::encode(selector), "{contract}.{sig}");
380 };
381
382 macro_rules! bound {
383 ($contract:expr, $iface:ident, [$($call:ident),* $(,)?]) => {
384 $(check($contract, $iface::$call::SIGNATURE, $iface::$call::SELECTOR);)*
385 };
386 }
387
388 for contract in ["XPlatformVerifier", "GitHubPlatformVerifier"] {
389 bound!(
390 contract,
391 TlsNotaryPlatformVerifier,
392 [
393 initializeCall,
394 platformIdCall,
395 quoteCall,
396 notaryServiceCall,
397 honkVerifierCall,
398 honkVerifierCodehashCall,
399 protocolParametersCall,
400 setTrustRootsCall,
401 setProtocolParametersCall,
402 MAX_PROOF_LIFETIMECall,
403 MAX_FUTURE_ATTESTATION_SKEWCall,
404 MAX_FUTURE_OBSERVATION_ALLOWANCECall,
405 ownerCall,
406 pendingOwnerCall,
407 transferOwnershipCall,
408 acceptOwnershipCall,
409 ]
410 );
411 }
412 bound!(
413 "GooglePlatformVerifier",
414 GooglePlatformVerifier,
415 [
416 initializeCall,
417 platformIdCall,
418 quoteCall,
419 jwtRootsCall,
420 setJwtRootsCall,
421 notaryServiceCall,
422 honkVerifierCall,
423 honkVerifierCodehashCall,
424 protocolParametersCall,
425 setTrustRootsCall,
426 setProtocolParametersCall,
427 MAX_PROOF_LIFETIMECall,
428 MAX_FUTURE_ATTESTATION_SKEWCall,
429 MAX_FUTURE_OBSERVATION_ALLOWANCECall,
430 ownerCall,
431 pendingOwnerCall,
432 transferOwnershipCall,
433 acceptOwnershipCall,
434 ]
435 );
436
437 // The two initializers differ in shape, and the artifacts say so:
438 // the TLSNotary one is not on Google's contract, nor the reverse.
439 assert_ne!(
440 TlsNotaryPlatformVerifier::initializeCall::SELECTOR,
441 GooglePlatformVerifier::initializeCall::SELECTOR
442 );
443 let google = artifacts
444 .method_identifiers("GooglePlatformVerifier")
445 .unwrap();
446 assert!(
447 !google.contains_key(TlsNotaryPlatformVerifier::initializeCall::SIGNATURE)
448 );
449 let x = artifacts.method_identifiers("XPlatformVerifier").unwrap();
450 assert!(!x.contains_key(GooglePlatformVerifier::initializeCall::SIGNATURE));
451 }
452}