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