Skip to main content

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}