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 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}