Skip to main content

libid_contracts/
platform_verifier.rs

1//! Deploying a launch Platform Verifier: which contract serves which
2//! platform, what it initializes with, and the rules its `initialize`
3//! enforces — checked here, off chain, before a transaction is built.
4//!
5//! `PlatformVerifierBase.__PlatformVerifierBase_init` refuses four things a
6//! deployer would otherwise rediscover at the proxy's constructor revert:
7//! a Notary Service that does not match what the profile notarizes (a
8//! TLSNotary profile must hold one, Google must hold none), a code hash
9//! that is zero, `keccak256("")` or not the hash of the code at the Honk
10//! verifier's address, a parameter over its ceiling, and a zero owner or
11//! root list. [`Initializer::call`] reads the code hash off the chain, checks
12//! the rest, and builds the exact `initialize` call;
13//! [`deploy_platform_verifier`] puts the implementation behind a fresh
14//! ERC1967 proxy with it.
15//!
16//! The Honk verifier a Platform Verifier pins is vendored here too
17//! ([`circuits`](crate::circuits)): bb-generated in `libid-circuits` from
18//! the circuit's verification key, deployed with its libraries linked by
19//! [`deploy_honk_verifiers`](crate::circuits::deploy_honk_verifiers). Which
20//! circuit a platform proves under is [`PlatformVerifier::circuit`]; the
21//! contract pins whichever address governance names, by address AND by
22//! code hash.
23
24use alloy::{
25    primitives::{
26        keccak256,
27        Address,
28        B256,
29    },
30    providers::Provider,
31    sol_types::SolCall,
32};
33
34use crate::{
35    artifacts::Artifacts,
36    bindings::ceremony::{
37        GooglePlatformVerifier,
38        TlsNotaryPlatformVerifier,
39    },
40    circuits::Circuit,
41    deploy::deploy_behind_proxy,
42    error::{
43        Error,
44        Result,
45    },
46};
47
48/// Ceiling on `proofLifetime`, in seconds: `PlatformVerifierBase.MAX_PROOF_LIFETIME`.
49pub const MAX_PROOF_LIFETIME: u64 = 30 * 24 * 60 * 60;
50/// Ceiling on `maxFutureAttestationSkew`, in seconds:
51/// `PlatformVerifierBase.MAX_FUTURE_ATTESTATION_SKEW`.
52pub const MAX_FUTURE_ATTESTATION_SKEW: u64 = 24 * 60 * 60;
53/// Ceiling on `futureObservationAllowance`, in seconds:
54/// `PlatformVerifierBase.MAX_FUTURE_OBSERVATION_ALLOWANCE`.
55pub const MAX_FUTURE_OBSERVATION_ALLOWANCE: u64 = 24 * 60 * 60;
56
57/// One of the three launch Platform Verifiers.
58#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
59pub enum PlatformVerifier {
60    /// `x/v1`: `XPlatformVerifier`, a TLSNotary profile.
61    X,
62    /// `github/v1`: `GitHubPlatformVerifier`, a TLSNotary profile.
63    GitHub,
64    /// `google/v1`: `GooglePlatformVerifier`, a signed-token profile that
65    /// notarizes nothing.
66    Google,
67}
68
69impl PlatformVerifier {
70    /// Every launch verifier.
71    pub const ALL: [Self; 3] = [Self::X, Self::GitHub, Self::Google];
72
73    /// The contract, which is also its `.sol` file and its entry in
74    /// [`COVERED`](crate::artifacts::COVERED).
75    pub const fn contract(self) -> &'static str {
76        match self {
77            Self::X => "XPlatformVerifier",
78            Self::GitHub => "GitHubPlatformVerifier",
79            Self::Google => "GooglePlatformVerifier",
80        }
81    }
82
83    /// The platform's bare name, as `CeremonyProfile` spells it. libID
84    /// namespaces only its own strings.
85    pub const fn platform(self) -> &'static str {
86        match self {
87            Self::X => "x",
88            Self::GitHub => "github",
89            Self::Google => "google",
90        }
91    }
92
93    /// What the deployed contract answers to `platformId()`: `keccak256`
94    /// of the bare name.
95    pub fn platform_id(self) -> B256 {
96        keccak256(self.platform().as_bytes())
97    }
98
99    /// The ceremony circuit this platform's proofs are made under, and so
100    /// which vendored Honk verifier its `honk_verifier` should be.
101    pub const fn circuit(self) -> Circuit {
102        match self {
103            Self::X | Self::GitHub => Circuit::BearerLink,
104            Self::Google => Circuit::OidcGoogle,
105        }
106    }
107
108    /// Whether the profile notarizes any session, and so whether its
109    /// verifier holds a Notary Service. `CeremonyProfile.attestationCount`
110    /// is two for the TLSNotary profiles and zero for Google; the
111    /// `libid-profiles` table says the same, and a test pins the two
112    /// together.
113    pub const fn notarizes(self) -> bool {
114        match self {
115            Self::X | Self::GitHub => true,
116            Self::Google => false,
117        }
118    }
119}
120
121/// What a TLSNotary Platform Verifier (`x/v1`, `github/v1`) initializes
122/// with: its trust roots and governance parameters.
123#[derive(Clone, Copy, Debug, PartialEq, Eq)]
124pub struct TlsNotaryRoots {
125    /// Governance. Rotates the roots, moves the parameters, upgrades.
126    pub owner: Address,
127    /// The Notary Service both attestations are authenticated through.
128    /// Required: the profile pins one (REQ-COMMON-18).
129    pub notary_service: Address,
130    /// The bb-generated UltraHonk verifier for this platform's circuit. Its
131    /// code hash is read off chain and pinned beside it.
132    pub honk_verifier: Address,
133    /// Maximum age of the token attestation, in seconds; at most
134    /// [`MAX_PROOF_LIFETIME`].
135    pub proof_lifetime: u64,
136    /// Maximum lead over block time an attestation may carry, in seconds;
137    /// at most [`MAX_FUTURE_ATTESTATION_SKEW`].
138    pub max_future_attestation_skew: u64,
139    /// How far ahead of block time the evidence time may run, in seconds;
140    /// at most [`MAX_FUTURE_OBSERVATION_ALLOWANCE`].
141    pub future_observation_allowance: u64,
142}
143
144/// What the Google Platform Verifier initializes with. No Notary Service:
145/// the profile notarizes nothing, and the base refuses one. No lifetime and
146/// no skew: the signed `exp` is the whole validity ceiling.
147#[derive(Clone, Copy, Debug, PartialEq, Eq)]
148pub struct GoogleRoots {
149    /// Governance.
150    pub owner: Address,
151    /// The bb-generated UltraHonk verifier for the Google OIDC circuit.
152    pub honk_verifier: Address,
153    /// How far ahead of block time the signed `exp` may run, in seconds; at
154    /// most [`MAX_FUTURE_OBSERVATION_ALLOWANCE`]. Google's runs about an
155    /// hour ahead.
156    pub future_observation_allowance: u64,
157    /// The `GoogleJwtRoots` proxy the trusted moduli are read through.
158    pub jwt_roots: Address,
159}
160
161/// What one Platform Verifier is initialized with.
162#[derive(Clone, Copy, Debug, PartialEq, Eq)]
163pub enum Initializer {
164    X(TlsNotaryRoots),
165    GitHub(TlsNotaryRoots),
166    Google(GoogleRoots),
167}
168
169/// A built `initialize` call, typed by shape. Feed
170/// [`abi_encode`](Self::abi_encode) to an ERC1967 proxy as its init data —
171/// through [`deploy_platform_verifier`], [`deploy_proxy`](crate::deploy::deploy_proxy),
172/// or as part of the creation code a [factory](crate::factory) deploy takes.
173#[derive(Clone, Debug, PartialEq, Eq)]
174pub enum InitializeCall {
175    /// `XPlatformVerifier.initialize` or `GitHubPlatformVerifier.initialize`
176    /// (one signature).
177    TlsNotary(TlsNotaryPlatformVerifier::initializeCall),
178    /// `GooglePlatformVerifier.initialize`.
179    Google(GooglePlatformVerifier::initializeCall),
180}
181
182impl InitializeCall {
183    /// The ABI-encoded call.
184    pub fn abi_encode(&self) -> Vec<u8> {
185        match self {
186            Self::TlsNotary(call) => call.abi_encode(),
187            Self::Google(call) => call.abi_encode(),
188        }
189    }
190
191    /// The code hash the call pins.
192    pub fn honk_verifier_codehash(&self) -> B256 {
193        match self {
194            Self::TlsNotary(call) => call.honkVerifierCodehash_,
195            Self::Google(call) => call.honkVerifierCodehash_,
196        }
197    }
198}
199
200impl Initializer {
201    /// Which verifier this initializes.
202    pub const fn verifier(&self) -> PlatformVerifier {
203        match self {
204            Self::X(_) => PlatformVerifier::X,
205            Self::GitHub(_) => PlatformVerifier::GitHub,
206            Self::Google(_) => PlatformVerifier::Google,
207        }
208    }
209
210    /// The Honk verifier it pins.
211    pub const fn honk_verifier(&self) -> Address {
212        match self {
213            Self::X(roots) | Self::GitHub(roots) => roots.honk_verifier,
214            Self::Google(roots) => roots.honk_verifier,
215        }
216    }
217
218    /// The rules `initialize` enforces that need no chain: a nonzero owner,
219    /// a nonzero Honk verifier, a Notary Service where the profile notarizes
220    /// (the Google shape cannot carry one at all), a nonzero root list for
221    /// Google, and every parameter under its ceiling. The code hash is the
222    /// one rule left to [`call`](Self::call).
223    pub fn check(&self) -> Result<()> {
224        let contract = self.verifier().contract();
225        let refuse = |detail: String| Error::Initializer {
226            detail: format!("{contract}: {detail}"),
227        };
228        let nonzero = |what: &str, address: Address| {
229            if address == Address::ZERO {
230                return Err(refuse(format!("{what} is the zero address")));
231            }
232            Ok(())
233        };
234        let capped = |what: &str, value: u64, limit: u64| {
235            if value > limit {
236                return Err(refuse(format!(
237                    "{what} {value}s exceeds the ceiling {limit}s"
238                )));
239            }
240            Ok(())
241        };
242        match self {
243            Self::X(roots) | Self::GitHub(roots) => {
244                nonzero("owner", roots.owner)?;
245                nonzero("honk verifier", roots.honk_verifier)?;
246                if roots.notary_service == Address::ZERO {
247                    return Err(refuse(
248                        "notary service is the zero address, but the profile \
249                         notarizes two sessions and must pin the Notary Service \
250                         they are authenticated through"
251                            .into(),
252                    ));
253                }
254                capped("proof lifetime", roots.proof_lifetime, MAX_PROOF_LIFETIME)?;
255                capped(
256                    "max future attestation skew",
257                    roots.max_future_attestation_skew,
258                    MAX_FUTURE_ATTESTATION_SKEW,
259                )?;
260                capped(
261                    "future observation allowance",
262                    roots.future_observation_allowance,
263                    MAX_FUTURE_OBSERVATION_ALLOWANCE,
264                )
265            }
266            Self::Google(roots) => {
267                nonzero("owner", roots.owner)?;
268                nonzero("honk verifier", roots.honk_verifier)?;
269                nonzero("jwt roots", roots.jwt_roots)?;
270                capped(
271                    "future observation allowance",
272                    roots.future_observation_allowance,
273                    MAX_FUTURE_OBSERVATION_ALLOWANCE,
274                )
275            }
276        }
277    }
278
279    /// Build the `initialize` call: [`check`](Self::check), then read the
280    /// code hash of the Honk verifier through `provider` and pin it. Fails
281    /// when the address holds no code — the contract would refuse the
282    /// resulting hash, and a verifier that is not deployed yet is the
283    /// mis-wiring the check exists to catch.
284    pub async fn call<P: Provider>(&self, provider: &P) -> Result<InitializeCall> {
285        self.check()?;
286        let codehash =
287            codehash_at(provider, self.honk_verifier())
288                .await
289                .map_err(|e| Error::Initializer {
290                    detail: format!("{}: honk verifier: {e}", self.verifier().contract()),
291                })?;
292        Ok(match self {
293            Self::X(roots) | Self::GitHub(roots) => {
294                InitializeCall::TlsNotary(TlsNotaryPlatformVerifier::initializeCall {
295                    owner_: roots.owner,
296                    notary_: roots.notary_service,
297                    honkVerifier_: roots.honk_verifier,
298                    honkVerifierCodehash_: codehash,
299                    proofLifetime_: roots.proof_lifetime,
300                    maxFutureAttestationSkew_: roots.max_future_attestation_skew,
301                    futureObservationAllowance_: roots.future_observation_allowance,
302                })
303            }
304            Self::Google(roots) => {
305                InitializeCall::Google(GooglePlatformVerifier::initializeCall {
306                    owner_: roots.owner,
307                    // A profile whose Attestation Count is zero must not
308                    // reach a Notary Service (REQ-COMMON-05D); the base
309                    // refuses one.
310                    notary_: Address::ZERO,
311                    honkVerifier_: roots.honk_verifier,
312                    honkVerifierCodehash_: codehash,
313                    futureObservationAllowance_: roots.future_observation_allowance,
314                    jwtRoots_: roots.jwt_roots,
315                })
316            }
317        })
318    }
319}
320
321/// The code hash of the account at `address`, as `EXTCODEHASH` reports it
322/// for an account with code: `keccak256` of its runtime bytecode. An
323/// account without code is an error rather than `keccak256("")` or zero,
324/// because `setTrustRoots` refuses both and a caller comparing against the
325/// hash of nothing has nothing to pin.
326pub async fn codehash_at<P: Provider>(provider: &P, address: Address) -> Result<B256> {
327    let code = provider
328        .get_code_at(address)
329        .await
330        .map_err(|e| Error::Rpc {
331            detail: format!("failed to read code at {address}: {e}"),
332        })?;
333    if code.is_empty() {
334        return Err(Error::Rpc {
335            detail: format!("no code at {address}"),
336        });
337    }
338    Ok(keccak256(&code))
339}
340
341/// Deploy the verifier's implementation from `artifacts` and put it behind
342/// a fresh ERC1967 proxy initialized with `init` — the code hash read off
343/// the chain, the rules checked first. Returns the proxy address, which is
344/// the Platform Verifier a Proof Verifier registers with `setVerifier`.
345///
346/// `sender` opts into explicit nonce management (see
347/// [`deploy_contract_from`](crate::deploy::deploy_contract_from)).
348pub async fn deploy_platform_verifier<P: Provider>(
349    provider: &P,
350    artifacts: &Artifacts,
351    init: &Initializer,
352    sender: Option<Address>,
353) -> Result<Address> {
354    let contract = init.verifier().contract();
355    match init.call(provider).await? {
356        InitializeCall::TlsNotary(call) => {
357            deploy_behind_proxy(provider, artifacts, contract, &call, sender).await
358        }
359        InitializeCall::Google(call) => {
360            deploy_behind_proxy(provider, artifacts, contract, &call, sender).await
361        }
362    }
363}
364
365#[cfg(test)]
366mod tests {
367    use super::*;
368    use crate::artifacts::COVERED;
369
370    fn tls() -> TlsNotaryRoots {
371        TlsNotaryRoots {
372            owner: Address::repeat_byte(0x01),
373            notary_service: Address::repeat_byte(0x02),
374            honk_verifier: Address::repeat_byte(0x03),
375            proof_lifetime: 3600,
376            max_future_attestation_skew: 300,
377            future_observation_allowance: 300,
378        }
379    }
380
381    fn google() -> GoogleRoots {
382        GoogleRoots {
383            owner: Address::repeat_byte(0x01),
384            honk_verifier: Address::repeat_byte(0x03),
385            future_observation_allowance: 7200,
386            jwt_roots: Address::repeat_byte(0x04),
387        }
388    }
389
390    /// Whether a verifier holds a Notary Service is derived from the
391    /// generated profile table, as the contract derives it from
392    /// `CeremonyProfile.attestationCount`.
393    #[test]
394    fn notarizes_follows_the_profile_table() {
395        for verifier in PlatformVerifier::ALL {
396            let profile = libid_profiles::LAUNCH
397                .iter()
398                .find(|p| p.platform == verifier.platform())
399                .unwrap_or_else(|| panic!("{verifier:?} has no launch profile"));
400            assert_eq!(
401                verifier.notarizes(),
402                profile.attestation_count() != 0,
403                "{verifier:?}"
404            );
405            assert_eq!(verifier.platform_id(), keccak256(profile.platform));
406        }
407        assert_eq!(PlatformVerifier::ALL.len(), libid_profiles::LAUNCH.len());
408    }
409
410    /// Every circuit has a platform proving under it: a vendored verifier
411    /// no platform pins would be dead weight in every consumer's binary.
412    #[test]
413    fn every_circuit_serves_a_platform() {
414        for circuit in Circuit::ALL {
415            assert!(
416                PlatformVerifier::ALL.iter().any(|v| v.circuit() == circuit),
417                "{circuit:?} serves no platform"
418            );
419        }
420    }
421
422    /// Every verifier's contract is one the crate vendors.
423    #[test]
424    fn every_verifier_is_covered() {
425        for verifier in PlatformVerifier::ALL {
426            let contract = verifier.contract();
427            assert!(
428                COVERED.contains(&(contract, contract)),
429                "{contract} is not in COVERED"
430            );
431        }
432    }
433
434    #[test]
435    fn well_formed_initializers_pass() {
436        Initializer::X(tls()).check().unwrap();
437        Initializer::GitHub(tls()).check().unwrap();
438        Initializer::Google(google()).check().unwrap();
439    }
440
441    #[test]
442    fn a_tls_notary_profile_must_pin_a_notary_service() {
443        let err = Initializer::GitHub(TlsNotaryRoots {
444            notary_service: Address::ZERO,
445            ..tls()
446        })
447        .check()
448        .unwrap_err();
449        assert!(matches!(err, Error::Initializer { .. }), "{err}");
450        assert!(err.to_string().contains("notary service"), "{err}");
451        assert!(err.to_string().contains("GitHubPlatformVerifier"), "{err}");
452    }
453
454    #[test]
455    fn parameters_over_their_ceilings_are_refused() {
456        let over = [
457            Initializer::X(TlsNotaryRoots {
458                proof_lifetime: MAX_PROOF_LIFETIME + 1,
459                ..tls()
460            }),
461            Initializer::X(TlsNotaryRoots {
462                max_future_attestation_skew: MAX_FUTURE_ATTESTATION_SKEW + 1,
463                ..tls()
464            }),
465            Initializer::X(TlsNotaryRoots {
466                future_observation_allowance: MAX_FUTURE_OBSERVATION_ALLOWANCE + 1,
467                ..tls()
468            }),
469            Initializer::Google(GoogleRoots {
470                future_observation_allowance: MAX_FUTURE_OBSERVATION_ALLOWANCE + 1,
471                ..google()
472            }),
473        ];
474        for init in over {
475            let err = init.check().unwrap_err();
476            assert!(err.to_string().contains("exceeds the ceiling"), "{err}");
477        }
478        // At the ceiling is allowed.
479        Initializer::X(TlsNotaryRoots {
480            proof_lifetime: MAX_PROOF_LIFETIME,
481            max_future_attestation_skew: MAX_FUTURE_ATTESTATION_SKEW,
482            future_observation_allowance: MAX_FUTURE_OBSERVATION_ALLOWANCE,
483            ..tls()
484        })
485        .check()
486        .unwrap();
487    }
488
489    #[test]
490    fn zero_addresses_are_refused() {
491        let cases: [(Initializer, &str); 5] = [
492            (
493                Initializer::X(TlsNotaryRoots {
494                    owner: Address::ZERO,
495                    ..tls()
496                }),
497                "owner",
498            ),
499            (
500                Initializer::X(TlsNotaryRoots {
501                    honk_verifier: Address::ZERO,
502                    ..tls()
503                }),
504                "honk verifier",
505            ),
506            (
507                Initializer::Google(GoogleRoots {
508                    owner: Address::ZERO,
509                    ..google()
510                }),
511                "owner",
512            ),
513            (
514                Initializer::Google(GoogleRoots {
515                    honk_verifier: Address::ZERO,
516                    ..google()
517                }),
518                "honk verifier",
519            ),
520            (
521                Initializer::Google(GoogleRoots {
522                    jwt_roots: Address::ZERO,
523                    ..google()
524                }),
525                "jwt roots",
526            ),
527        ];
528        for (init, what) in cases {
529            let err = init.check().unwrap_err();
530            assert!(err.to_string().contains(what), "{what}: {err}");
531        }
532    }
533}