Skip to main content

agora_agentkit/govlog/
root.rs

1//! Root certificates: what makes a governance signing key the chain's.
2//!
3//! The online key signs entries; the root keys sign nothing but
4//! [`KeyCertStatement`]s, offline, on hardware. A key holds the chain iff
5//! a [`KeyCertificate`] says so at that position:
6//!
7//! ```text
8//! signed_bytes = "agora-governance-root-v1\n" || canonical_json(statement)
9//! signature    = Ed25519( root_key, signed_bytes )      -- no timestamp
10//! ```
11//!
12//! The prefix is domain separation: nothing else a root key might ever
13//! sign begins with it.
14
15use super::{
16    PublicKeyHex, Sha256Hex, SignatureHex, TrustedHead, canonical_json,
17};
18use crate::crypto::Signature;
19use serde::{Deserialize, Serialize};
20use std::collections::HashSet;
21
22/// What every root signature begins with
23pub const ROOT_DOMAIN: &[u8] = b"agora-governance-root-v1\n";
24
25/// The [`KeyCertStatement`] version this module produces and verifies
26pub const KEY_CERT_VERSION: u32 = 1;
27
28/// The governance root keys this build of agentkit trusts.
29///
30/// Generated on-device and attested; see `governance/root` in the agora
31/// repository. Changing the set is a release, not a chain entry.
32pub const ROOT_KEYS: &[&str] = &[
33    // YubiKey 5 Nano 12585769
34    "d29ed152161d23d75cec48ade38859db07f48f3dc15a337179a8f20b13f12cd5",
35    // YubiKey 5Ci 17129552
36    "200e8efe32391d7f4a1d763c4de0739acfe2e63e69d8be7e19b29d3f5075fb53",
37];
38
39/// How many of [`ROOT_KEYS`] must sign a [`KeyCertificate`]
40pub const ROOT_THRESHOLD: usize = 1;
41
42/// What a certified key is being certified as
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
45#[cfg_attr(feature = "schemars", schemars(inline))]
46#[serde(rename_all = "snake_case")]
47pub enum CertPurpose {
48    // The key the chain started under, certified after the fact.
49    Genesis,
50    // The incoming key of a `RotationReason::Routine`.
51    Routine,
52    // The incoming key of a `RotationReason::Compromise`.
53    Compromise,
54}
55
56/// What a root key signs: `key` holds the chain from `from_seq`.
57///
58/// Every field is always serialized, `null` when absent, and unknown
59/// fields are refused: the bytes a verifier rebuilds are exactly the bytes
60/// the Steward read before touching the device.
61#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
62#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
63#[cfg_attr(feature = "schemars", schemars(inline))]
64#[serde(deny_unknown_fields)]
65pub struct KeyCertStatement {
66    /// Always [`KEY_CERT_VERSION`]
67    pub agora_governance_key_cert: u32,
68    /// The online signing key being certified
69    pub key: PublicKeyHex,
70    pub purpose: CertPurpose,
71    /// The first `chain_seq` `key` signs
72    pub from_seq: u64,
73    /// The rotation entry's own `prev_hash`, so a certificate is good at
74    /// one position of one chain. `null` for [`CertPurpose::Genesis`].
75    pub prev_hash: Option<Sha256Hex>,
76    /// [`CertPurpose::Compromise`] only: the last entry trusted under the
77    /// outgoing key. The root says where the window opens, not the online
78    /// key that may be the thief's.
79    pub last_trusted: Option<TrustedHead>,
80}
81
82impl KeyCertStatement {
83    /// `key` is the key the chain started under
84    pub fn genesis(key: PublicKeyHex) -> Self {
85        Self {
86            agora_governance_key_cert: KEY_CERT_VERSION,
87            key,
88            purpose: CertPurpose::Genesis,
89            from_seq: 1,
90            prev_hash: None,
91            last_trusted: None,
92        }
93    }
94
95    /// `key` takes over after the routine rotation appended at `seq`,
96    /// whose `prev_hash` is `prev_hash`
97    pub fn routine(
98        key: PublicKeyHex,
99        seq: u64,
100        prev_hash: Option<Sha256Hex>,
101    ) -> Self {
102        Self {
103            agora_governance_key_cert: KEY_CERT_VERSION,
104            key,
105            purpose: CertPurpose::Routine,
106            from_seq: seq + 1,
107            prev_hash,
108            last_trusted: None,
109        }
110    }
111
112    /// `key` signs the compromise declaration appended at `seq` and
113    /// everything after it
114    pub fn compromise(
115        key: PublicKeyHex,
116        seq: u64,
117        prev_hash: Option<Sha256Hex>,
118        last_trusted: TrustedHead,
119    ) -> Self {
120        Self {
121            agora_governance_key_cert: KEY_CERT_VERSION,
122            key,
123            purpose: CertPurpose::Compromise,
124            from_seq: seq,
125            prev_hash,
126            last_trusted: Some(last_trusted),
127        }
128    }
129
130    /// The exact bytes a root key signs
131    pub fn signed_bytes(&self) -> Vec<u8> {
132        let value = serde_json::to_value(self)
133            .expect("a KeyCertStatement always serializes");
134        let mut out = ROOT_DOMAIN.to_vec();
135        out.extend(canonical_json(&value));
136        out
137    }
138}
139
140/// One root key's signature over [`KeyCertStatement::signed_bytes`]
141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
142#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
143#[cfg_attr(feature = "schemars", schemars(inline))]
144#[serde(deny_unknown_fields)]
145pub struct RootSignature {
146    pub root_key: PublicKeyHex,
147    pub signature: SignatureHex,
148}
149
150/// A [`KeyCertStatement`] and the root signatures over it
151#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
152#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
153#[cfg_attr(feature = "schemars", schemars(inline))]
154#[serde(deny_unknown_fields)]
155pub struct KeyCertificate {
156    pub statement: KeyCertStatement,
157    pub signatures: Vec<RootSignature>,
158}
159
160/// A certificate does not certify what it was presented for
161#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
162pub enum CertificateError {
163    #[error("agora_governance_key_cert is {0}, not {KEY_CERT_VERSION}")]
164    UnsupportedVersion(u32),
165    #[error(
166        "the certificate is for a different key, purpose or chain position"
167    )]
168    WrongStatement,
169    #[error(
170        "{valid} valid root signature(s) where {needed} are needed; \
171         unknown and repeated signers count for nothing"
172    )]
173    BelowThreshold { valid: usize, needed: usize },
174}
175
176impl KeyCertificate {
177    /// `statement`, not yet signed by anyone
178    pub fn unsigned(statement: KeyCertStatement) -> Self {
179        Self {
180            statement,
181            signatures: Vec::new(),
182        }
183    }
184
185    /// This certificate, plus `signature`
186    pub fn with(mut self, signature: RootSignature) -> Self {
187        self.signatures.push(signature);
188        self
189    }
190
191    /// At least [`threshold`](RootSet::threshold) distinct keys of `roots`
192    /// signed this statement.
193    ///
194    /// A signature by a key outside `roots`, a second one by the same key,
195    /// or one that does not verify is ignored rather than fatal: a future
196    /// root set may overlap this one.
197    pub fn verify(&self, roots: &RootSet) -> Result<(), CertificateError> {
198        let version = self.statement.agora_governance_key_cert;
199        if version != KEY_CERT_VERSION {
200            return Err(CertificateError::UnsupportedVersion(version));
201        }
202        let message = self.statement.signed_bytes();
203        let mut signers = HashSet::new();
204        for s in &self.signatures {
205            if !roots.contains(&s.root_key) {
206                continue;
207            }
208            let Ok(key) = s.root_key.to_verifying_key() else {
209                continue;
210            };
211            if key
212                .verify_strict(&message, &Signature::from(&s.signature))
213                .is_ok()
214            {
215                signers.insert(s.root_key);
216            }
217        }
218        if signers.len() >= roots.threshold() {
219            Ok(())
220        } else {
221            Err(CertificateError::BelowThreshold {
222                valid: signers.len(),
223                needed: roots.threshold(),
224            })
225        }
226    }
227
228    /// [`verify`](Self::verify), and the statement is exactly `expected` —
229    /// which the verifier derives from the chain, never from the
230    /// certificate
231    pub fn verify_for(
232        &self,
233        expected: &KeyCertStatement,
234        roots: &RootSet,
235    ) -> Result<(), CertificateError> {
236        if self.statement != *expected {
237            return Err(CertificateError::WrongStatement);
238        }
239        self.verify(roots)
240    }
241}
242
243/// The root keys a verifier trusts, and how many must agree
244#[derive(Debug, Clone, PartialEq, Eq)]
245pub struct RootSet {
246    keys: HashSet<PublicKeyHex>,
247    threshold: usize,
248}
249
250impl RootSet {
251    /// [`ROOT_KEYS`] at [`ROOT_THRESHOLD`]
252    pub fn published() -> Self {
253        Self::new(
254            ROOT_KEYS.iter().map(|k| {
255                k.parse().expect("ROOT_KEYS are valid 32-byte hex keys")
256            }),
257            ROOT_THRESHOLD,
258        )
259    }
260
261    /// A `threshold` of zero would certify anything, so it is raised to one
262    pub fn new(
263        keys: impl IntoIterator<Item = PublicKeyHex>,
264        threshold: usize,
265    ) -> Self {
266        Self {
267            keys: keys.into_iter().collect(),
268            threshold: threshold.max(1),
269        }
270    }
271
272    pub fn contains(&self, key: &PublicKeyHex) -> bool {
273        self.keys.contains(key)
274    }
275
276    pub fn threshold(&self) -> usize {
277        self.threshold
278    }
279
280    /// The root keys, in no particular order
281    pub fn keys(&self) -> impl Iterator<Item = &PublicKeyHex> {
282        self.keys.iter()
283    }
284}