Skip to main content

agora_agentkit/
govlog.rs

1//! Governance log attestation: the envelope the server signs at insert and
2//! any client can verify.
3//!
4//! Every entry commits to its own fields and to the hash of the entry before
5//! it, so the log is a chain: change or remove any entry and every later
6//! link stops verifying. The envelope (version 1) is
7//!
8//! ```text
9//! data_hash  = SHA-256( canonical_json(data) )
10//! preimage   = {"agora_governance_log":1,"id":…,"entry_type":…,
11//!               "created_at":<unix micros>,"prev_hash":<hex|null>,
12//!               "data_hash":<hex>}
13//! entry_hash = SHA-256( preimage )
14//! signature  = crypto::sign( key, entry_hash, signed_at unix seconds )
15//! ```
16//!
17//! What is *not* in the envelope, on purpose: `tags` (an index the Clerk may
18//! revise), `chain_seq` (an index; the `prev_hash` links prove order), and
19//! anything derived such as the precedent summary. `signed_at` is bound by
20//! the signature rather than the hash, so a retroactive attestation — an
21//! entry signed long after it was recorded — is visible as such and cannot
22//! be quietly back-dated. See [`is_retroactive`].
23//!
24//! History is never rewritten. Two entry types amend it instead, and both
25//! are ordinary signed links whose `data` the verifier reads:
26//!
27//! - [`Amendment`] (`AMD-`) names an earlier entry and says what changed
28//!   about its force ([`Standing`]) or its content ([`Redaction`]). Its
29//!   own free text is committed to, not contained (see [`TextCommitment`]),
30//!   because an amendment is the one thing that can never be redacted. A
31//!   redaction replaces values in the target's `data` in place; the
32//!   original `entry_hash` stays on the row so later links still verify,
33//!   and the amendment's `resulting_data_hash` is what the redacted data
34//!   must now hash to. [`EntryVerdict::content_matches`] is the check.
35//! - [`KeyRotation`] (`KEY-`) moves the chain to a new signing key. A
36//!   routine rotation is signed by the old key and a compromise
37//!   declaration by the new one, but neither signature is what makes the
38//!   change authentic: a [`KeyCertificate`] from the offline root keys
39//!   ([`ROOT_KEYS`]) is, so holding the online key is never enough to
40//!   move the chain. See [`verify_chain`].
41//!
42//! The envelope itself is unchanged by any of this: `ENVELOPE_VERSION` is
43//! still 1 and what it does and does not cover is exactly as above.
44
45use crate::crypto::{self, Signature, SigningKey, VerifyingKey};
46use crate::enums::GovernanceLogEntryType;
47use crate::ids::{GovernanceLogId, GovernanceLogPrefix};
48use chrono::{DateTime, Utc};
49use serde::{Deserialize, Serialize};
50use sha2::{Digest, Sha256};
51use std::collections::{HashMap, HashSet};
52
53pub use crate::enums::{AmendmentKind, KeyStatus, Standing};
54
55mod texts;
56pub use texts::{
57    AmendmentText, AmendmentTextStatus, AmendmentTexts, CommittedText,
58    TextCommitment, TextStatus, WITHHELD_TEXT,
59};
60
61mod root;
62pub use root::{
63    CertPurpose, CertificateError, KEY_CERT_VERSION, KeyCertStatement,
64    KeyCertificate, ROOT_DOMAIN, ROOT_KEYS, ROOT_THRESHOLD, RootSet,
65    RootSignature,
66};
67
68/// The shared test vectors in `vectors/govlog`; see [`vectors`]
69#[cfg(test)]
70mod vectors;
71
72/// The envelope version this module produces and verifies
73pub const ENVELOPE_VERSION: u32 = 1;
74
75/// An attestation signed more than this long after its entry was recorded
76/// is [retroactive](is_retroactive)
77pub const RETROACTIVE_AFTER: chrono::Duration = chrono::Duration::seconds(60);
78
79// ---------------------------------------------------------------------------
80// Fixed-size hex newtypes
81// ---------------------------------------------------------------------------
82
83/// A hex string of the wrong length or alphabet for the type it was parsed
84/// into
85#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
86#[error("{type_name}: expected {expected} bytes of hex, got {got:?}")]
87pub struct HexLengthError {
88    pub type_name: &'static str,
89    pub expected: usize,
90    pub got: String,
91}
92
93macro_rules! hex_bytes {
94    ($(#[$meta:meta])* $name:ident, $len:expr) => {
95        $(#[$meta])*
96        #[derive(Clone, Copy, PartialEq, Eq, Hash)]
97        #[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
98        #[cfg_attr(feature = "sqlx", sqlx(transparent))]
99        pub struct $name([u8; $len]);
100
101        impl $name {
102            /// The raw bytes
103            pub fn as_bytes(&self) -> &[u8; $len] {
104                &self.0
105            }
106
107            /// Lowercase hex, as on the wire
108            pub fn to_hex(&self) -> String {
109                hex::encode(self.0)
110            }
111        }
112
113        impl From<[u8; $len]> for $name {
114            fn from(bytes: [u8; $len]) -> Self {
115                Self(bytes)
116            }
117        }
118
119        impl TryFrom<&[u8]> for $name {
120            type Error = HexLengthError;
121
122            fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
123                <[u8; $len]>::try_from(bytes).map(Self).map_err(|_| {
124                    HexLengthError {
125                        type_name: stringify!($name),
126                        expected: $len,
127                        got: hex::encode(bytes),
128                    }
129                })
130            }
131        }
132
133        impl TryFrom<Vec<u8>> for $name {
134            type Error = HexLengthError;
135
136            fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
137                Self::try_from(bytes.as_slice())
138            }
139        }
140
141        impl std::str::FromStr for $name {
142            type Err = HexLengthError;
143
144            fn from_str(s: &str) -> Result<Self, Self::Err> {
145                let bytes = hex::decode(s.trim()).map_err(|_| HexLengthError {
146                    type_name: stringify!($name),
147                    expected: $len,
148                    got: s.to_string(),
149                })?;
150                Self::try_from(bytes.as_slice())
151            }
152        }
153
154        impl std::fmt::Display for $name {
155            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156                f.write_str(&self.to_hex())
157            }
158        }
159
160        impl std::fmt::Debug for $name {
161            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162                write!(f, "{}({})", stringify!($name), self.to_hex())
163            }
164        }
165
166        impl Serialize for $name {
167            fn serialize<S: serde::Serializer>(
168                &self,
169                s: S,
170            ) -> Result<S::Ok, S::Error> {
171                s.serialize_str(&self.to_hex())
172            }
173        }
174
175        impl<'de> Deserialize<'de> for $name {
176            fn deserialize<D: serde::Deserializer<'de>>(
177                d: D,
178            ) -> Result<Self, D::Error> {
179                let s = String::deserialize(d)?;
180                s.parse().map_err(serde::de::Error::custom)
181            }
182        }
183
184        // Hand-written for the same reason as every id newtype: a derived
185        // schema becomes a `$ref` into `$defs`, which the Claude.ai MCP
186        // connector mangles (see CLAUDE.md in the agora repo).
187        #[cfg(feature = "schemars")]
188        impl schemars::JsonSchema for $name {
189            fn inline_schema() -> bool {
190                true
191            }
192
193            fn schema_name() -> std::borrow::Cow<'static, str> {
194                std::borrow::Cow::Borrowed(stringify!($name))
195            }
196
197            fn schema_id() -> std::borrow::Cow<'static, str> {
198                std::borrow::Cow::Borrowed(concat!(
199                    module_path!(),
200                    "::",
201                    stringify!($name)
202                ))
203            }
204
205            fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
206                schemars::json_schema!({
207                    "type": "string",
208                    "pattern": format!("^[0-9a-f]{{{}}}$", $len * 2),
209                    "description": format!("{} bytes, lowercase hex", $len),
210                })
211            }
212        }
213    };
214}
215
216hex_bytes!(
217    /// A SHA-256 digest, hex on the wire
218    Sha256Hex,
219    32
220);
221
222hex_bytes!(
223    /// An Ed25519 signature, hex on the wire
224    SignatureHex,
225    64
226);
227
228hex_bytes!(
229    /// An Ed25519 public key, hex on the wire
230    PublicKeyHex,
231    32
232);
233
234hex_bytes!(
235    /// A blinding value: 32 random bytes carried in a redactable entry's
236    /// `data` under [`BLIND_KEY`]. See [`blind_data`] for what it is for.
237    Blind,
238    32
239);
240
241hex_bytes!(
242    /// The salt of a [`TextCommitment`]: 32 random bytes kept beside the
243    /// text, and deleted with it
244    TextSalt,
245    32
246);
247
248impl Blind {
249    /// A fresh value from the operating system's random source
250    pub fn random() -> Self {
251        use rand::RngCore;
252        let mut bytes = [0u8; 32];
253        rand::rngs::OsRng.fill_bytes(&mut bytes);
254        Self(bytes)
255    }
256}
257
258impl TextSalt {
259    /// A fresh value from the operating system's random source
260    pub fn random() -> Self {
261        Self(*Blind::random().as_bytes())
262    }
263}
264
265impl From<Signature> for SignatureHex {
266    fn from(sig: Signature) -> Self {
267        Self(sig.to_bytes())
268    }
269}
270
271impl From<&SignatureHex> for Signature {
272    fn from(sig: &SignatureHex) -> Self {
273        Signature::from_bytes(&sig.0)
274    }
275}
276
277impl From<&VerifyingKey> for PublicKeyHex {
278    fn from(key: &VerifyingKey) -> Self {
279        Self(key.to_bytes())
280    }
281}
282
283impl PublicKeyHex {
284    /// The key, if the bytes are a valid curve point
285    pub fn to_verifying_key(
286        &self,
287    ) -> Result<VerifyingKey, ed25519_dalek::SignatureError> {
288        VerifyingKey::from_bytes(&self.0)
289    }
290}
291
292// ---------------------------------------------------------------------------
293// Canonical JSON and hashing
294// ---------------------------------------------------------------------------
295
296/// `value` as compact JSON with object keys sorted bytewise at every level.
297///
298/// `serde_json::to_vec` on a [`serde_json::Value`] is *not* canonical:
299/// with the `preserve_order` feature (on in every Agora workspace, off in
300/// this crate's own tests) objects serialize in insertion order, so the
301/// same value hashes differently depending on who built it. Strings and
302/// numbers use serde_json's own formatting, which is deterministic for a
303/// given value.
304pub fn canonical_json(value: &serde_json::Value) -> Vec<u8> {
305    let mut out = Vec::new();
306    write_canonical(value, &mut out);
307    out
308}
309
310fn write_canonical(value: &serde_json::Value, out: &mut Vec<u8>) {
311    use serde_json::Value;
312    match value {
313        Value::Null => out.extend_from_slice(b"null"),
314        Value::Bool(b) => {
315            out.extend_from_slice(if *b { b"true" } else { b"false" })
316        }
317        Value::Number(n) => serde_json::to_writer(&mut *out, n)
318            .expect("a number always serializes"),
319        Value::String(s) => serde_json::to_writer(&mut *out, s)
320            .expect("a string always serializes"),
321        Value::Array(items) => {
322            out.push(b'[');
323            for (i, item) in items.iter().enumerate() {
324                if i > 0 {
325                    out.push(b',');
326                }
327                write_canonical(item, out);
328            }
329            out.push(b']');
330        }
331        Value::Object(map) => {
332            let mut keys: Vec<&String> = map.keys().collect();
333            keys.sort_unstable();
334            out.push(b'{');
335            for (i, key) in keys.into_iter().enumerate() {
336                if i > 0 {
337                    out.push(b',');
338                }
339                serde_json::to_writer(&mut *out, key)
340                    .expect("a string always serializes");
341                out.push(b':');
342                write_canonical(&map[key], out);
343            }
344            out.push(b'}');
345        }
346    }
347}
348
349/// The RFC 6901 pointer to the first number in `data` that is not a 64-bit
350/// integer, if there is one.
351///
352/// Governance `data` never contains one. [`canonical_json`] writes a number
353/// the way `serde_json` does, and how that prints a float has changed
354/// between releases (`1e21` became `1e+21`); an integer past `u64` is a
355/// float to it as well, and its float parsing is not exactly rounded. A
356/// hash that is meant to be permanent cannot depend on any of that, so the
357/// writer refuses such `data` ([`blind_data`], [`redact_data`]) and a
358/// verifier reports it without hashing it. A fraction goes in a string.
359pub fn non_integer_number(data: &serde_json::Value) -> Option<String> {
360    fn find(value: &serde_json::Value, path: &mut String) -> bool {
361        use serde_json::Value::{Array, Number, Object};
362        let mark = path.len();
363        match value {
364            Number(n) => return n.is_f64(),
365            Array(items) => {
366                for (i, item) in items.iter().enumerate() {
367                    path.push_str(&format!("/{i}"));
368                    if find(item, path) {
369                        return true;
370                    }
371                    path.truncate(mark);
372                }
373            }
374            Object(map) => {
375                for (key, item) in map {
376                    path.push('/');
377                    path.push_str(&key.replace('~', "~0").replace('/', "~1"));
378                    if find(item, path) {
379                        return true;
380                    }
381                    path.truncate(mark);
382                }
383            }
384            _ => {}
385        }
386        false
387    }
388    let mut path = String::new();
389    find(data, &mut path).then_some(path)
390}
391
392/// SHA-256 over [`canonical_json`]
393pub fn data_hash(data: &serde_json::Value) -> Sha256Hex {
394    Sha256Hex(Sha256::digest(canonical_json(data)).into())
395}
396
397/// The fields an entry's hash commits to
398///
399/// A struct rather than a [`serde_json::Value`] so the preimage serializes
400/// in declaration order whatever `preserve_order` says.
401#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
402pub struct Envelope {
403    /// Always [`ENVELOPE_VERSION`]
404    pub agora_governance_log: u32,
405    pub id: GovernanceLogId,
406    pub entry_type: GovernanceLogEntryType,
407    /// Unix microseconds — the precision Postgres stores
408    pub created_at: i64,
409    pub prev_hash: Option<Sha256Hex>,
410    pub data_hash: Sha256Hex,
411}
412
413impl Envelope {
414    /// The version-1 envelope for these fields
415    pub fn new(
416        id: GovernanceLogId,
417        entry_type: GovernanceLogEntryType,
418        created_at: DateTime<Utc>,
419        prev_hash: Option<Sha256Hex>,
420        data_hash: Sha256Hex,
421    ) -> Self {
422        Self {
423            agora_governance_log: ENVELOPE_VERSION,
424            id,
425            entry_type,
426            created_at: created_at.timestamp_micros(),
427            prev_hash,
428            data_hash,
429        }
430    }
431
432    /// `created_at` as a timestamp again
433    pub fn created_at(&self) -> DateTime<Utc> {
434        DateTime::from_timestamp_micros(self.created_at)
435            .expect("an Envelope only ever holds an in-range timestamp")
436    }
437
438    /// The bytes that are hashed
439    pub fn preimage(&self) -> Vec<u8> {
440        serde_json::to_vec(self).expect("an Envelope always serializes")
441    }
442
443    /// SHA-256 over [`preimage`](Self::preimage)
444    pub fn entry_hash(&self) -> Sha256Hex {
445        Sha256Hex(Sha256::digest(self.preimage()).into())
446    }
447}
448
449/// `t` with anything below a microsecond dropped, so the value hashed is the
450/// value Postgres will store
451pub fn truncate_to_micros(t: DateTime<Utc>) -> DateTime<Utc> {
452    DateTime::from_timestamp_micros(t.timestamp_micros())
453        .expect("a timestamp that came from a DateTime is in range")
454}
455
456/// `t` with anything below a second dropped, so `signed_at` round-trips to
457/// the integer the signature covers
458pub fn truncate_to_seconds(t: DateTime<Utc>) -> DateTime<Utc> {
459    DateTime::from_timestamp(t.timestamp(), 0)
460        .expect("a timestamp that came from a DateTime is in range")
461}
462
463/// `true` when the attestation was signed more than [`RETROACTIVE_AFTER`]
464/// after the entry was recorded — history signed after the fact, which
465/// proves the key holder vouches for it now, not that it was signed then
466pub fn is_retroactive(
467    created_at: DateTime<Utc>,
468    signed_at: DateTime<Utc>,
469) -> bool {
470    signed_at - created_at > RETROACTIVE_AFTER
471}
472
473// ---------------------------------------------------------------------------
474// Wire types
475// ---------------------------------------------------------------------------
476
477/// What the server attests about one governance log entry
478#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
479#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
480#[cfg_attr(feature = "schemars", schemars(inline))]
481pub struct GovernanceAttestation {
482    /// Envelope version; see the module docs for what `1` commits to
483    pub envelope_version: u32,
484    /// Position in the chain, from 1. An index, not part of the envelope:
485    /// the `prev_hash` links are what prove order.
486    pub chain_seq: u64,
487    /// `entry_hash` of the previous entry; `null` only for the first
488    pub prev_hash: Option<Sha256Hex>,
489    /// SHA-256 of the entry's canonical `data`
490    pub data_hash: Sha256Hex,
491    /// SHA-256 of the envelope; what the signature covers
492    pub entry_hash: Sha256Hex,
493    /// Ed25519 over `entry_hash` and `signed_at`, by the platform's
494    /// governance signing key
495    pub signature: SignatureHex,
496    /// When the signature was made. Distinct from `created_at`: see
497    /// `retroactive`.
498    pub signed_at: DateTime<Utc>,
499    /// `true` when signed well after the entry was recorded — the entries
500    /// that predate signing were attested this way, which proves the
501    /// Steward vouches for them, not that they were signed at the time
502    pub retroactive: bool,
503}
504
505/// One link of the chain as `GET /api/governance/log/chain` returns it —
506/// everything needed to verify linkage and signatures, plus `data` for the
507/// entries a verifier has to read
508#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
509#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
510#[cfg_attr(feature = "schemars", schemars(inline))]
511pub struct GovernanceChainLink {
512    pub id: GovernanceLogId,
513    pub entry_type: GovernanceLogEntryType,
514    pub created_at: DateTime<Utc>,
515    pub attestation: GovernanceAttestation,
516    /// Present for `amendment` and `key_rotation` entries, whose content
517    /// is what the chain means and is small by construction. A Council
518    /// transcript is neither, and is read one entry at a time instead.
519    /// [`verify_chain`] hashes this raw value against `data_hash` before
520    /// reading it, so unknown future fields neither break an older
521    /// verifier nor escape the signature.
522    #[serde(default, skip_serializing_if = "Option::is_none")]
523    pub data: Option<serde_json::Value>,
524    /// The texts a version 2 [`Amendment`] commits to, as far as the
525    /// platform still holds them. Outside the envelope on purpose: a text
526    /// can be erased without the chain changing.
527    #[serde(
528        default,
529        skip_serializing_if = "Option::is_none",
530        deserialize_with = "read_as_written"
531    )]
532    pub texts: Option<AmendmentTexts>,
533}
534
535/// The platform's governance signing key, as `GET
536/// /api/governance/signing-key` publishes it
537#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
538#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
539#[cfg_attr(feature = "schemars", schemars(inline))]
540pub struct GovernanceSigningKey {
541    /// Always `"ed25519"`
542    pub algorithm: String,
543    pub public_key: PublicKeyHex,
544    /// The envelope version entries are currently signed under
545    pub envelope_version: u32,
546}
547
548impl GovernanceSigningKey {
549    /// The published form of `key`
550    pub fn new(key: &VerifyingKey) -> Self {
551        Self {
552            algorithm: "ed25519".to_string(),
553            public_key: key.into(),
554            envelope_version: ENVELOPE_VERSION,
555        }
556    }
557}
558
559// ---------------------------------------------------------------------------
560// Amendments
561// ---------------------------------------------------------------------------
562
563/// The [`Amendment`] payload version this module produces. Version 1,
564/// whose texts are in the signed `data`, still verifies: the platform has
565/// three, all reviewed to hold no personal data.
566pub const AMENDMENT_VERSION: u32 = 2;
567
568/// An amendment is malformed
569#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
570pub enum AmendmentError {
571    #[error("agora_governance_amendment is {0}, not 1 or {AMENDMENT_VERSION}")]
572    UnsupportedVersion(u32),
573    #[error(
574        "a version 1 amendment carries its texts and a version \
575         {AMENDMENT_VERSION} one commits to them; this does neither \
576         consistently"
577    )]
578    TextShape,
579    #[error("`{0}` beside the entry is not the text the entry committed to")]
580    TextMismatch(&'static str),
581    #[error("texts beside an entry that commits to none")]
582    UncommittedText,
583    #[error("kind `redaction` requires a `redaction`")]
584    MissingRedaction,
585    #[error("`redaction` is only valid on kind `redaction`")]
586    UnexpectedRedaction,
587    #[error("amendment target {0} is not an entry of this chain")]
588    UnknownTarget(GovernanceLogId),
589    #[error("amendment target {0} is not an earlier entry")]
590    ForwardReference(GovernanceLogId),
591    #[error("target_entry_hash is not {0}'s entry_hash")]
592    WrongTargetHash(GovernanceLogId),
593}
594
595/// The `data` of an `amendment` entry: what an earlier entry now means.
596///
597/// The target is never edited — except its `data` under a [`Redaction`] —
598/// so the amendment is the whole record of the change, and both are
599/// signed links of the same chain.
600#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
601#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
602#[cfg_attr(feature = "schemars", schemars(inline))]
603pub struct Amendment {
604    /// Always [`AMENDMENT_VERSION`]
605    pub agora_governance_amendment: u32,
606    pub target: GovernanceLogId,
607    /// The target's `entry_hash` — binds this to one exact entry
608    pub target_entry_hash: Sha256Hex,
609    pub kind: AmendmentKind,
610    /// The governance entry that authorizes this, when one does
611    /// (e.g. `GOV-2026-0005`). `None` for a lawful-deletion redaction.
612    #[serde(default)]
613    pub authority: Option<GovernanceLogId>,
614    /// Section or legal basis, human-readable: `"§1 (Red Team Cases
615    /// Recharacterized)"`, `"GDPR Art. 17(1)(a)"`. Never personal data —
616    /// and erasable, for the day that rule is broken.
617    pub basis: AmendmentText,
618    /// The label readers and prompts show next to the target
619    pub note: AmendmentText,
620    /// Why, at length — `note` is the label, this is the reasoning.
621    #[serde(default, skip_serializing_if = "Option::is_none")]
622    pub rationale: Option<AmendmentText>,
623    /// Present iff `kind` is [`AmendmentKind::Redaction`]
624    #[serde(default, skip_serializing_if = "Option::is_none")]
625    pub redaction: Option<Redaction>,
626}
627
628/// What a [`AmendmentKind::Redaction`] removed, and what is left
629#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
630#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
631#[cfg_attr(feature = "schemars", schemars(inline))]
632pub struct Redaction {
633    /// RFC 6901 JSON pointers into the target's `data` whose values were
634    /// replaced. Paths only: never the removed content, never the subject.
635    pub fields: Vec<String>,
636    /// What the target's `data` hashes to after redaction, so the redacted
637    /// content is itself verifiable and cannot be altered again silently
638    pub resulting_data_hash: Sha256Hex,
639}
640
641/// An [`Amendment`] and the texts it commits to: what a writer appends,
642/// the first as the entry's `data` and the second beside it
643#[derive(Debug, Clone, PartialEq, Eq)]
644pub struct AmendmentDraft {
645    pub amendment: Amendment,
646    pub texts: AmendmentTexts,
647}
648
649impl AmendmentDraft {
650    /// An amendment of `kind` against `target`.
651    ///
652    /// Redactions go through [`redaction`](Self::redaction) instead, which
653    /// is the only way to get a [`Redaction`] whose `resulting_data_hash`
654    /// is the hash of data that actually exists. A `&str` or `String`
655    /// text gets a [random](TextSalt::random) salt.
656    pub fn new(
657        target: GovernanceLogId,
658        target_entry_hash: Sha256Hex,
659        kind: AmendmentKind,
660        basis: impl Into<CommittedText>,
661        note: impl Into<CommittedText>,
662    ) -> Result<Self, AmendmentError> {
663        if kind == AmendmentKind::Redaction {
664            return Err(AmendmentError::MissingRedaction);
665        }
666        let (basis, note) = (basis.into(), note.into());
667        Ok(Self {
668            amendment: Amendment {
669                agora_governance_amendment: AMENDMENT_VERSION,
670                target,
671                target_entry_hash,
672                kind,
673                authority: None,
674                basis: AmendmentText::Committed(basis.commitment()),
675                note: AmendmentText::Committed(note.commitment()),
676                rationale: None,
677                redaction: None,
678            },
679            texts: AmendmentTexts {
680                basis: Some(basis),
681                note: Some(note),
682                rationale: None,
683            },
684        })
685    }
686
687    /// A redaction of `fields` from the target's `data`, with the redacted
688    /// data it commits to.
689    ///
690    /// `amendment_id` is the id this amendment will be appended under: the
691    /// marker left behind names it, so the redaction says who ordered it.
692    /// Append both together or neither — the returned `data` is what the
693    /// target's row must hold for [`verify_chain`] to accept it. `blind`
694    /// is the target's new [`Blind`]: [random](Blind::random), so a
695    /// rehearsal's `resulting_data_hash` is not the real one's.
696    #[allow(clippy::too_many_arguments)]
697    pub fn redaction(
698        amendment_id: &GovernanceLogId,
699        target: GovernanceLogId,
700        target_entry_hash: Sha256Hex,
701        basis: impl Into<CommittedText>,
702        note: impl Into<CommittedText>,
703        fields: Vec<String>,
704        data: &serde_json::Value,
705        blind: Blind,
706    ) -> Result<(Self, serde_json::Value), RedactError> {
707        let redacted = redact_data(data, &fields, amendment_id, blind)?;
708        let (basis, note) = (basis.into(), note.into());
709        Ok((
710            Self {
711                amendment: Amendment {
712                    agora_governance_amendment: AMENDMENT_VERSION,
713                    target,
714                    target_entry_hash,
715                    kind: AmendmentKind::Redaction,
716                    authority: None,
717                    basis: AmendmentText::Committed(basis.commitment()),
718                    note: AmendmentText::Committed(note.commitment()),
719                    rationale: None,
720                    redaction: Some(Redaction {
721                        fields,
722                        resulting_data_hash: data_hash(&redacted),
723                    }),
724                },
725                texts: AmendmentTexts {
726                    basis: Some(basis),
727                    note: Some(note),
728                    rationale: None,
729                },
730            },
731            redacted,
732        ))
733    }
734
735    /// The draft with the governance entry that authorizes it
736    pub fn with_authority(mut self, authority: GovernanceLogId) -> Self {
737        self.amendment.authority = Some(authority);
738        self
739    }
740
741    /// The draft with its [`rationale`](Amendment::rationale)
742    pub fn with_rationale(
743        mut self,
744        rationale: impl Into<CommittedText>,
745    ) -> Self {
746        let rationale = rationale.into();
747        self.amendment.rationale =
748            Some(AmendmentText::Committed(rationale.commitment()));
749        self.texts.rationale = Some(rationale);
750        self
751    }
752}
753
754impl Amendment {
755    /// Version, the shape of the texts for that version, and the
756    /// redaction-shape invariant — everything checkable without the rest
757    /// of the chain
758    pub fn validate(&self) -> Result<(), AmendmentError> {
759        let plain = match self.agora_governance_amendment {
760            1 => true,
761            AMENDMENT_VERSION => false,
762            other => return Err(AmendmentError::UnsupportedVersion(other)),
763        };
764        let texts =
765            [Some(&self.basis), Some(&self.note), self.rationale.as_ref()];
766        if texts.into_iter().flatten().any(|t| t.is_plain() != plain) {
767            return Err(AmendmentError::TextShape);
768        }
769        match (self.kind, &self.redaction) {
770            (AmendmentKind::Redaction, None) => {
771                Err(AmendmentError::MissingRedaction)
772            }
773            (k, Some(_)) if k != AmendmentKind::Redaction => {
774                Err(AmendmentError::UnexpectedRedaction)
775            }
776            _ => Ok(()),
777        }
778    }
779
780    /// Where each committed text stands given what is `beside` the entry;
781    /// `None` for version 1, whose texts are in the signed `data`.
782    ///
783    /// A text that is beside the entry and is not the one committed to,
784    /// or that the entry never committed to at all, is an error: someone
785    /// put words next to a signed entry that the signer did not write.
786    pub fn text_status(
787        &self,
788        beside: Option<&AmendmentTexts>,
789    ) -> Result<Option<AmendmentTextStatus>, AmendmentError> {
790        let empty = AmendmentTexts::default();
791        let beside = beside.unwrap_or(&empty);
792        let (Some(basis), Some(note)) = (
793            self.basis.status(beside.basis.as_ref()),
794            self.note.status(beside.note.as_ref()),
795        ) else {
796            return if beside.is_empty() {
797                Ok(None)
798            } else {
799                Err(AmendmentError::UncommittedText)
800            };
801        };
802        let rationale = match (&self.rationale, &beside.rationale) {
803            (None, Some(_)) => return Err(AmendmentError::UncommittedText),
804            (None, None) => None,
805            (Some(text), beside) => text.status(beside.as_ref()),
806        };
807        for (name, status) in [
808            ("basis", Some(basis)),
809            ("note", Some(note)),
810            ("rationale", rationale),
811        ] {
812            if status == Some(TextStatus::Mismatch) {
813                return Err(AmendmentError::TextMismatch(name));
814            }
815        }
816        Ok(Some(AmendmentTextStatus {
817            basis,
818            note,
819            rationale,
820        }))
821    }
822}
823
824/// What an amendment does to the [`Standing`] of the entry it names, when
825/// it changes it at all
826pub fn kind_standing(kind: AmendmentKind) -> Option<Standing> {
827    match kind {
828        AmendmentKind::NonPrecedential => Some(Standing::NonPrecedential),
829        AmendmentKind::Overruled => Some(Standing::Overruled),
830        AmendmentKind::Superseded => Some(Standing::Superseded),
831        AmendmentKind::Reinstated => Some(Standing::InForce),
832        AmendmentKind::Correction
833        | AmendmentKind::Redaction
834        | AmendmentKind::Reattested => None,
835    }
836}
837
838/// The [`Standing`] conferred by `kinds` — the amendments naming one entry,
839/// in chain order. The last one that changes standing wins.
840pub fn standing(kinds: impl IntoIterator<Item = AmendmentKind>) -> Standing {
841    kinds
842        .into_iter()
843        .filter_map(kind_standing)
844        .last()
845        .unwrap_or_default()
846}
847
848/// The top-level key of a redactable entry's `data` that holds its
849/// [`Blind`]
850pub const BLIND_KEY: &str = "_blind";
851
852/// Whether entries of this type can be redacted, and so carry a [`Blind`].
853/// Amendments and key rotations cannot: verifiers read their `data`, and a
854/// chain whose own corrections can be edited proves nothing.
855pub fn is_redactable(entry_type: GovernanceLogEntryType) -> bool {
856    !matches!(
857        entry_type,
858        GovernanceLogEntryType::Amendment | GovernanceLogEntryType::KeyRotation
859    )
860}
861
862/// `data` cannot be blinded
863#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
864pub enum BlindError {
865    #[error("a redactable entry's data must be a JSON object")]
866    NotAnObject,
867    #[error(
868        "data already has a {BLIND_KEY:?} key; the writer supplies it, not the caller"
869    )]
870    AlreadyBlinded,
871    #[error(
872        "{0:?} is a number that is not a 64-bit integer; governance data \
873         never contains one (put a fraction in a string)"
874    )]
875    NonIntegerNumber(String),
876}
877
878/// `data` with a [`Blind`] under [`BLIND_KEY`] — what a writer signs and
879/// stores for every [redactable](is_redactable) entry.
880///
881/// An entry's `data_hash` is public and permanent: the chain cannot verify
882/// without it. After a redaction everything in `data` *except* the removed
883/// values is public too, so without a blind anyone could test a guess at a
884/// removed value — a name, a handle — by putting it back and hashing. The
885/// blind is 256 bits of the preimage that [`redact_data`] replaces along
886/// with the values, so the old hash can no longer be reproduced by anyone
887/// who did not already hold the unredacted entry. It is not a secret while
888/// the entry is whole, and it is not part of the envelope: verifiers hash
889/// `data` as they always did.
890///
891/// Entries written before blinding existed have none. Their first
892/// redaction is only as safe as the removed values are hard to guess
893/// (redact the enclosing value when in doubt); it leaves a blind behind,
894/// so later ones are protected.
895pub fn blind_data(
896    data: &serde_json::Value,
897    blind: Blind,
898) -> Result<serde_json::Value, BlindError> {
899    if let Some(pointer) = non_integer_number(data) {
900        return Err(BlindError::NonIntegerNumber(pointer));
901    }
902    let mut out = data.clone();
903    let object = out.as_object_mut().ok_or(BlindError::NotAnObject)?;
904    if object.contains_key(BLIND_KEY) {
905        return Err(BlindError::AlreadyBlinded);
906    }
907    object.insert(BLIND_KEY.to_string(), blind.to_hex().into());
908    Ok(out)
909}
910
911/// A [`Redaction`] cannot be applied as asked
912#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
913pub enum RedactError {
914    #[error("pointer {0:?} does not resolve in the entry's data")]
915    Unresolved(String),
916    #[error("the empty pointer would redact the whole entry")]
917    WholeEntry,
918    #[error("a redaction names at least one pointer")]
919    NoFields,
920    #[error("{0:?} is the entry's blind; every redaction replaces it already")]
921    BlindPointer(String),
922    #[error(
923        "{0:?} is a number that is not a 64-bit integer; governance data \
924         never contains one"
925    )]
926    NonIntegerNumber(String),
927}
928
929/// The marker a redaction leaves in place of a value
930pub fn redaction_marker(amendment_id: &GovernanceLogId) -> String {
931    format!("[redacted by {amendment_id}]")
932}
933
934/// `data` with the value at each RFC 6901 pointer in `fields` replaced by
935/// [`redaction_marker`].
936///
937/// Whole-value replacement only: a redaction tool that can write arbitrary
938/// replacement prose is a rewrite tool. The server and every verifier share
939/// this one definition, because what it returns is what the target's
940/// `resulting_data_hash` covers.
941///
942/// The entry's [`Blind`] is replaced by `blind` — a fresh one, not a
943/// marker. Destroying the old value is what stops a removed value being
944/// confirmed against the entry's original `data_hash` (see [`blind_data`]);
945/// leaving a *new* one is what protects the next redaction of the same
946/// entry, whose removed values could otherwise be tested against this
947/// one's public `resulting_data_hash`. An entry that predates blinding
948/// gains one here. `blind` must be [random](Blind::random) outside tests.
949pub fn redact_data(
950    data: &serde_json::Value,
951    fields: &[String],
952    amendment_id: &GovernanceLogId,
953    blind: Blind,
954) -> Result<serde_json::Value, RedactError> {
955    if fields.is_empty() {
956        return Err(RedactError::NoFields);
957    }
958    if let Some(pointer) = non_integer_number(data) {
959        return Err(RedactError::NonIntegerNumber(pointer));
960    }
961    let blind_pointer = format!("/{BLIND_KEY}");
962    let marker = serde_json::Value::String(redaction_marker(amendment_id));
963    let mut out = data.clone();
964    for pointer in fields {
965        if pointer.is_empty() {
966            return Err(RedactError::WholeEntry);
967        }
968        if *pointer == blind_pointer {
969            return Err(RedactError::BlindPointer(pointer.clone()));
970        }
971        let slot = out
972            .pointer_mut(pointer)
973            .ok_or_else(|| RedactError::Unresolved(pointer.clone()))?;
974        *slot = marker.clone();
975    }
976    // Every entry a writer has produced is an object. Anything else has
977    // nowhere to keep a blind, and staying redactable matters more.
978    if let Some(object) = out.as_object_mut() {
979        object.insert(BLIND_KEY.to_string(), blind.to_hex().into());
980    }
981    Ok(out)
982}
983
984/// What a reader needs next to an amended entry
985#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
986#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
987#[cfg_attr(feature = "schemars", schemars(inline))]
988pub struct AmendmentNotice {
989    pub id: GovernanceLogId,
990    pub kind: AmendmentKind,
991    #[serde(default)]
992    pub authority: Option<GovernanceLogId>,
993    pub basis: String,
994    pub note: String,
995    /// See [`Amendment::rationale`]
996    #[serde(default, skip_serializing_if = "Option::is_none")]
997    pub rationale: Option<String>,
998    pub created_at: DateTime<Utc>,
999}
1000
1001impl AmendmentNotice {
1002    /// The notice for `amendment`, appended as `id` at `created_at`
1003    /// A text no longer `beside` the entry reads [`WITHHELD_TEXT`]
1004    pub fn new(
1005        id: GovernanceLogId,
1006        created_at: DateTime<Utc>,
1007        amendment: &Amendment,
1008        beside: Option<&AmendmentTexts>,
1009    ) -> Self {
1010        let beside = beside.cloned().unwrap_or_default();
1011        Self {
1012            id,
1013            kind: amendment.kind,
1014            authority: amendment.authority.clone(),
1015            basis: amendment.basis.resolve(beside.basis.as_ref()).into(),
1016            note: amendment.note.resolve(beside.note.as_ref()).into(),
1017            rationale: amendment
1018                .rationale
1019                .as_ref()
1020                .map(|r| r.resolve(beside.rationale.as_ref()).into()),
1021            created_at,
1022        }
1023    }
1024}
1025
1026// ---------------------------------------------------------------------------
1027// Key rotation
1028// ---------------------------------------------------------------------------
1029
1030/// The [`KeyRotation`] payload version this module produces and verifies
1031pub const KEY_ROTATION_VERSION: u32 = 2;
1032
1033/// The key the chain started under, as this build of agentkit knows it.
1034///
1035/// Frozen. It predates the root keys, so until the chain's first rotation
1036/// carries its retroactive [`KeyCertificate`] this list is the only
1037/// second channel a verifier has for it; every later key is certified by
1038/// [`ROOT_KEYS`] instead and never appears here.
1039pub const PUBLISHED_KEYS: &[&str] =
1040    &["ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a"];
1041
1042/// Why the key changed
1043#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1044#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1045#[cfg_attr(feature = "schemars", schemars(inline))]
1046#[serde(rename_all = "snake_case")]
1047pub enum RotationReason {
1048    // Scheduled or voluntary; the old key signed the rotation itself.
1049    Routine,
1050    // The old key is in someone else's hands; the new key signed the
1051    // rotation.
1052    Compromise,
1053}
1054
1055/// The last entry the compromised key is trusted for
1056#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1057#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1058#[cfg_attr(feature = "schemars", schemars(inline))]
1059#[serde(deny_unknown_fields)]
1060pub struct TrustedHead {
1061    pub id: GovernanceLogId,
1062    pub chain_seq: u64,
1063    pub entry_hash: Sha256Hex,
1064}
1065
1066/// A rotation is malformed, unauthenticated, or inconsistent with the chain
1067#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1068pub enum RotationError {
1069    #[error("agora_governance_key_rotation is {0}, not {KEY_ROTATION_VERSION}")]
1070    UnsupportedVersion(u32),
1071    #[error("new_key is not a valid Ed25519 public key")]
1072    BadNewKey,
1073    #[error(
1074        "the proof of possession does not verify for this rotation at this position"
1075    )]
1076    BadProof,
1077    #[error("a compromise certificate must name last_trusted")]
1078    MissingLastTrusted,
1079    #[error("certificate: {0}")]
1080    Certificate(#[from] CertificateError),
1081    #[error("outgoing_certificate: {0}")]
1082    OutgoingCertificate(CertificateError),
1083    #[error(
1084        "the chain's first rotation must carry the genesis key's \
1085         outgoing_certificate"
1086    )]
1087    MissingGenesisCertificate,
1088    #[error(
1089        "outgoing_certificate belongs on the chain's first rotation and \
1090         nowhere else"
1091    )]
1092    UnexpectedOutgoingCertificate,
1093    #[error("old_key is not the key that was in force")]
1094    WrongOldKey,
1095    #[error("last_trusted does not name an earlier entry of this chain")]
1096    UnknownLastTrusted,
1097    #[error("new_key has already held this chain; a key is never brought back")]
1098    ReusedKey,
1099    #[error("last_trusted names an entry an earlier compromise repudiated")]
1100    RepudiatedLastTrusted,
1101}
1102
1103/// The `data` of a `key_rotation` entry.
1104///
1105/// Build one with [`routine`](Self::routine) or
1106/// [`compromise`](Self::compromise): both compute the proof of possession.
1107/// The `certificate` is what authenticates the change; the proof only
1108/// shows the certified key is one somebody holds.
1109///
1110/// No free text, and unknown fields are refused: a rotation can never be
1111/// redacted, so it carries nothing anyone could need erased. Narrative
1112/// belongs in a separate, redactable entry.
1113#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1114#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1115#[cfg_attr(feature = "schemars", schemars(inline))]
1116#[serde(deny_unknown_fields)]
1117pub struct KeyRotation {
1118    /// Always [`KEY_ROTATION_VERSION`]
1119    pub agora_governance_key_rotation: u32,
1120    pub reason: RotationReason,
1121    pub old_key: PublicKeyHex,
1122    pub new_key: PublicKeyHex,
1123    /// `crypto::sign(new_key, ` [`RotationStatement::hash`] `,
1124    /// proof_signed_at)` — the new key signing for itself, at one position
1125    /// in one chain
1126    pub proof: SignatureHex,
1127    /// Unix seconds; what the proof signature covers
1128    pub proof_signed_at: i64,
1129    /// The root's word that `new_key` holds the chain from here. For a
1130    /// compromise its statement also names the last entry trusted under
1131    /// `old_key`.
1132    pub certificate: KeyCertificate,
1133    /// The [`CertPurpose::Genesis`] certificate for the key the chain
1134    /// started under, which predates the root: on the chain's first
1135    /// rotation, and only there.
1136    #[serde(default, skip_serializing_if = "Option::is_none")]
1137    pub outgoing_certificate: Option<KeyCertificate>,
1138}
1139
1140/// What the proof of possession signs
1141///
1142/// A struct rather than a [`serde_json::Value`] for the same reason as
1143/// [`Envelope`]: declaration-ordered serialization whatever `preserve_order`
1144/// says. `prev_hash` is the rotation entry's own, so a proof lifted out of
1145/// one chain position does not verify in another.
1146#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1147pub struct RotationStatement {
1148    /// Always [`KEY_ROTATION_VERSION`]
1149    pub agora_governance_key_rotation: u32,
1150    pub reason: RotationReason,
1151    pub old_key: PublicKeyHex,
1152    pub new_key: PublicKeyHex,
1153    pub prev_hash: Option<Sha256Hex>,
1154}
1155
1156impl RotationStatement {
1157    /// The statement for a rotation at the position named by `prev_hash`
1158    pub fn new(
1159        reason: RotationReason,
1160        old_key: PublicKeyHex,
1161        new_key: PublicKeyHex,
1162        prev_hash: Option<Sha256Hex>,
1163    ) -> Self {
1164        Self {
1165            agora_governance_key_rotation: KEY_ROTATION_VERSION,
1166            reason,
1167            old_key,
1168            new_key,
1169            prev_hash,
1170        }
1171    }
1172
1173    /// The bytes that are hashed
1174    pub fn preimage(&self) -> Vec<u8> {
1175        serde_json::to_vec(self).expect("a RotationStatement always serializes")
1176    }
1177
1178    /// SHA-256 over [`preimage`](Self::preimage) — what the proof covers
1179    pub fn hash(&self) -> Sha256Hex {
1180        Sha256Hex(Sha256::digest(self.preimage()).into())
1181    }
1182}
1183
1184impl KeyRotation {
1185    /// A scheduled rotation from `old_key` to `new_signing_key`.
1186    ///
1187    /// The entry itself is signed by the **old** key; entries after it
1188    /// verify under the new one. `prev_hash` is the rotation entry's own,
1189    /// and `certificate` is over [`KeyCertStatement::routine`] at it.
1190    pub fn routine(
1191        old_key: PublicKeyHex,
1192        new_signing_key: &SigningKey,
1193        prev_hash: Option<Sha256Hex>,
1194        now: DateTime<Utc>,
1195        certificate: KeyCertificate,
1196    ) -> Self {
1197        Self::build(
1198            RotationReason::Routine,
1199            old_key,
1200            new_signing_key,
1201            prev_hash,
1202            now,
1203            certificate,
1204        )
1205    }
1206
1207    /// A declaration that `old_key` is compromised, trusted only through
1208    /// the `last_trusted` its `certificate` names.
1209    ///
1210    /// The entry is signed by the **new** key — the old one proves nothing
1211    /// any more. `last_trusted` must name an entry from before any earlier
1212    /// compromise window; a reattestation inside one restores the entry,
1213    /// not the ability to anchor trust there.
1214    pub fn compromise(
1215        old_key: PublicKeyHex,
1216        new_signing_key: &SigningKey,
1217        prev_hash: Option<Sha256Hex>,
1218        now: DateTime<Utc>,
1219        certificate: KeyCertificate,
1220    ) -> Self {
1221        Self::build(
1222            RotationReason::Compromise,
1223            old_key,
1224            new_signing_key,
1225            prev_hash,
1226            now,
1227            certificate,
1228        )
1229    }
1230
1231    fn build(
1232        reason: RotationReason,
1233        old_key: PublicKeyHex,
1234        new_signing_key: &SigningKey,
1235        prev_hash: Option<Sha256Hex>,
1236        now: DateTime<Utc>,
1237        certificate: KeyCertificate,
1238    ) -> Self {
1239        let new_key = PublicKeyHex::from(&new_signing_key.verifying_key());
1240        let proof_signed_at = truncate_to_seconds(now).timestamp();
1241        let statement =
1242            RotationStatement::new(reason, old_key, new_key, prev_hash);
1243        let proof = crypto::sign(
1244            new_signing_key,
1245            statement.hash().as_bytes(),
1246            proof_signed_at,
1247        );
1248        Self {
1249            agora_governance_key_rotation: KEY_ROTATION_VERSION,
1250            reason,
1251            old_key,
1252            new_key,
1253            proof: proof.into(),
1254            proof_signed_at,
1255            certificate,
1256            outgoing_certificate: None,
1257        }
1258    }
1259
1260    /// This rotation, carrying the genesis key's retroactive certificate
1261    pub fn with_outgoing(mut self, certificate: KeyCertificate) -> Self {
1262        self.outgoing_certificate = Some(certificate);
1263        self
1264    }
1265
1266    /// The statement this rotation's proof covers, at `prev_hash`
1267    pub fn statement(&self, prev_hash: Option<Sha256Hex>) -> RotationStatement {
1268        RotationStatement::new(
1269            self.reason,
1270            self.old_key,
1271            self.new_key,
1272            prev_hash,
1273        )
1274    }
1275
1276    /// Compromise only: the last entry trusted under `old_key`, as the
1277    /// root certified it
1278    pub fn last_trusted(&self) -> Option<&TrustedHead> {
1279        self.certificate.statement.last_trusted.as_ref()
1280    }
1281
1282    /// What `certificate` must say for this rotation, appended at `seq`
1283    /// with `prev_hash`, to be authentic.
1284    ///
1285    /// Derived from the chain. Only `last_trusted` is taken from the
1286    /// certificate, because only the root can say it.
1287    pub fn expected_statement(
1288        &self,
1289        seq: u64,
1290        prev_hash: Option<Sha256Hex>,
1291    ) -> Result<KeyCertStatement, RotationError> {
1292        match self.reason {
1293            RotationReason::Routine => {
1294                Ok(KeyCertStatement::routine(self.new_key, seq, prev_hash))
1295            }
1296            RotationReason::Compromise => Ok(KeyCertStatement::compromise(
1297                self.new_key,
1298                seq,
1299                prev_hash,
1300                self.last_trusted()
1301                    .cloned()
1302                    .ok_or(RotationError::MissingLastTrusted)?,
1303            )),
1304        }
1305    }
1306
1307    /// Version and the proof of possession at the position `prev_hash`
1308    /// names
1309    pub fn verify_proof(
1310        &self,
1311        prev_hash: Option<Sha256Hex>,
1312    ) -> Result<(), RotationError> {
1313        if self.agora_governance_key_rotation != KEY_ROTATION_VERSION {
1314            return Err(RotationError::UnsupportedVersion(
1315                self.agora_governance_key_rotation,
1316            ));
1317        }
1318        let new_key = self
1319            .new_key
1320            .to_verifying_key()
1321            .map_err(|_| RotationError::BadNewKey)?;
1322        crypto::verify(
1323            &new_key,
1324            self.statement(prev_hash).hash().as_bytes(),
1325            self.proof_signed_at,
1326            &Signature::from(&self.proof),
1327        )
1328        .then_some(())
1329        .ok_or(RotationError::BadProof)
1330    }
1331
1332    /// [`verify_proof`](Self::verify_proof), and `certificate` is the
1333    /// root's for this key at this position — everything checkable
1334    /// without the rest of the chain
1335    pub fn verify_certified(
1336        &self,
1337        seq: u64,
1338        prev_hash: Option<Sha256Hex>,
1339        roots: &RootSet,
1340    ) -> Result<(), RotationError> {
1341        self.verify_proof(prev_hash)?;
1342        let expected = self.expected_statement(seq, prev_hash)?;
1343        Ok(self.certificate.verify_for(&expected, roots)?)
1344    }
1345}
1346
1347/// The genesis keys a verifier trusts out of band.
1348///
1349/// Only the key the chain started under needs one: every later key is
1350/// certified by the [`RootSet`]. [`published`](Self::published) is this
1351/// build's [`PUBLISHED_KEYS`]; [`pinned`](Self::pinned) is the key a
1352/// client saw first and kept.
1353#[derive(Debug, Clone, Default, PartialEq, Eq)]
1354pub struct KeyAnchor {
1355    keys: HashSet<PublicKeyHex>,
1356}
1357
1358impl KeyAnchor {
1359    /// The keys compiled into this build of agentkit
1360    pub fn published() -> Self {
1361        PUBLISHED_KEYS
1362            .iter()
1363            .map(|k| {
1364                k.parse()
1365                    .expect("PUBLISHED_KEYS are valid 32-byte hex keys")
1366            })
1367            .collect()
1368    }
1369
1370    /// Just the one key, as a client that pinned what it saw first trusts it
1371    pub fn pinned(key: PublicKeyHex) -> Self {
1372        std::iter::once(key).collect()
1373    }
1374
1375    /// This anchor, plus `key`
1376    pub fn with(mut self, key: PublicKeyHex) -> Self {
1377        self.keys.insert(key);
1378        self
1379    }
1380
1381    pub fn contains(&self, key: &PublicKeyHex) -> bool {
1382        self.keys.contains(key)
1383    }
1384
1385    pub fn is_empty(&self) -> bool {
1386        self.keys.is_empty()
1387    }
1388
1389    /// The anchored keys, in no particular order
1390    pub fn keys(&self) -> impl Iterator<Item = &PublicKeyHex> {
1391        self.keys.iter()
1392    }
1393}
1394
1395impl FromIterator<PublicKeyHex> for KeyAnchor {
1396    fn from_iter<I: IntoIterator<Item = PublicKeyHex>>(iter: I) -> Self {
1397        Self {
1398            keys: iter.into_iter().collect(),
1399        }
1400    }
1401}
1402
1403/// One key's span of the chain, as [`verify_chain`] derives it and
1404/// `GET /api/governance/signing-keys` publishes it
1405#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1406#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1407#[cfg_attr(feature = "schemars", schemars(inline))]
1408pub struct GovernanceKeyRecord {
1409    pub public_key: PublicKeyHex,
1410    /// The first `chain_seq` this key signed
1411    pub from_seq: u64,
1412    /// The last `chain_seq` this key is trusted for; `null` while active.
1413    /// For a compromised key this is `last_trusted`, not the seq at which
1414    /// the compromise was declared.
1415    #[serde(default)]
1416    pub through_seq: Option<u64>,
1417    pub status: KeyStatus,
1418    /// The rotation entry that introduced this key; `null` for the genesis
1419    /// key, which predates the chain
1420    #[serde(default)]
1421    pub introduced_by: Option<GovernanceLogId>,
1422    /// The rotation entry that ended this key's span
1423    #[serde(default)]
1424    pub retired_by: Option<GovernanceLogId>,
1425    /// A [`KeyCertificate`] from the root vouches for this key. `false`
1426    /// only for a genesis key whose retroactive certificate the chain does
1427    /// not carry yet.
1428    #[serde(default)]
1429    pub certified: bool,
1430}
1431
1432/// The signing key history as `GET /api/governance/signing-keys` returns it
1433///
1434/// An object rather than a bare array: MCP structured content needs a
1435/// top-level object.
1436#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1437#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1438#[cfg_attr(feature = "schemars", schemars(inline))]
1439pub struct GovernanceSigningKeys {
1440    /// Oldest first
1441    pub keys: Vec<GovernanceKeyRecord>,
1442}
1443
1444/// The verdict on one entry
1445#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1446#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1447#[cfg_attr(feature = "schemars", schemars(inline))]
1448pub struct EntryVerdict {
1449    pub id: GovernanceLogId,
1450    pub chain_seq: u64,
1451    /// The signature verifies over `entry_hash` and `signed_at` under the
1452    /// published key
1453    pub signature_valid: bool,
1454    /// `entry_hash` recomputes from the envelope fields, `prev_hash` is the
1455    /// previous entry's `entry_hash`, and `chain_seq` is contiguous
1456    pub link_valid: bool,
1457    /// The entry's current `data` hashes to the attested `data_hash` — or,
1458    /// when a redaction names the entry, to the redaction's
1459    /// `resulting_data_hash`. `null` when the verifier did not read `data`.
1460    /// `false` with a clean chain means the content was changed after
1461    /// attestation and no amendment says so.
1462    #[serde(default)]
1463    pub content_matches: Option<bool>,
1464    /// See [`GovernanceAttestation::retroactive`]
1465    pub retroactive: bool,
1466    /// `created_at` is earlier than the previous link's. Informational:
1467    /// chain order is what is attested, and a clock step does not break it.
1468    pub out_of_order: bool,
1469    /// Amendment entries that name this one
1470    #[serde(default)]
1471    pub amended_by: Vec<GovernanceLogId>,
1472    /// The key the signature was checked under — the one in force at this
1473    /// position, or for a compromise declaration the certified new key
1474    #[serde(default, skip_serializing_if = "Option::is_none")]
1475    pub signed_by: Option<PublicKeyHex>,
1476    /// A redaction amendment names this entry, so its `data` has lawfully
1477    /// changed since it was attested
1478    #[serde(default)]
1479    pub redacted: bool,
1480    /// What the entry's `data` must hash to now, when it has been redacted
1481    #[serde(default, skip_serializing_if = "Option::is_none")]
1482    pub redacted_data_hash: Option<Sha256Hex>,
1483    /// Signed inside a compromise window and not reattested: the key
1484    /// holder of record disclaims it
1485    #[serde(default)]
1486    pub repudiated: bool,
1487    /// [`AmendmentKind::Reattested`] amendments vouching for this entry
1488    /// under a later, trusted key
1489    #[serde(default)]
1490    pub reattested_by: Vec<GovernanceLogId>,
1491    /// A version 2 amendment's texts: each beside the entry and matching
1492    /// what it committed to, or withheld. A text that does not match is a
1493    /// `problem`.
1494    #[serde(default, skip_serializing_if = "Option::is_none")]
1495    pub texts: Option<AmendmentTextStatus>,
1496    /// What failed, when something did
1497    #[serde(default, skip_serializing_if = "Option::is_none")]
1498    pub problem: Option<String>,
1499}
1500
1501/// A verification of the whole chain
1502#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1503#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1504#[cfg_attr(feature = "schemars", schemars(inline))]
1505pub struct GovernanceVerification {
1506    /// The key in force for the next entry — the active end of `keys`
1507    pub public_key: PublicKeyHex,
1508    /// Every entry's signature and link verified under the key in force,
1509    /// no entry's content is known to differ from what was attested, and
1510    /// every amendment and rotation is well-formed. Repudiated entries do
1511    /// not clear this by themselves: repudiation is a declared state, not
1512    /// a defect, and `repudiated` is where to look for it.
1513    pub ok: bool,
1514    /// The last entry in the chain
1515    #[serde(default)]
1516    pub head: Option<GovernanceLogId>,
1517    /// In chain order
1518    pub entries: Vec<EntryVerdict>,
1519    /// The signing key history the chain itself declares, oldest first
1520    #[serde(default)]
1521    pub keys: Vec<GovernanceKeyRecord>,
1522    /// The genesis key, when neither this verifier's [`KeyAnchor`] nor a
1523    /// [`CertPurpose::Genesis`] certificate in the chain vouches for it.
1524    /// Not a failure, but a reference client says so loudly. Never a later
1525    /// key: those are certified or they do not hold the chain at all.
1526    #[serde(default)]
1527    pub unanchored_keys: Vec<PublicKeyHex>,
1528    /// Entries inside a compromise window that no reattestation restored
1529    #[serde(default)]
1530    pub repudiated: Vec<GovernanceLogId>,
1531}
1532
1533impl GovernanceVerification {
1534    /// Recompute `ok` from the entries
1535    pub fn settle(mut self) -> Self {
1536        self.ok = self.entries.iter().all(|e| {
1537            e.signature_valid
1538                && e.link_valid
1539                && e.content_matches != Some(false)
1540                && e.problem.is_none()
1541        });
1542        self
1543    }
1544
1545    /// Record whether `data` is the content `link` attested — or what a
1546    /// redaction of it left behind.
1547    ///
1548    /// The chain endpoint carries `data` only for amendments and
1549    /// rotations, so this is how a caller that read an entry in full folds
1550    /// that read into the report. `false` (and a `false`
1551    /// [`content_matches`](EntryVerdict::content_matches), which clears
1552    /// [`ok`](Self::ok) on the next [`settle`](Self::settle)) when the
1553    /// entry is not in this report at all.
1554    pub fn check_content(
1555        &mut self,
1556        link: &GovernanceChainLink,
1557        data: &serde_json::Value,
1558    ) -> bool {
1559        let Some(entry) = self.entries.iter_mut().find(|e| e.id == link.id)
1560        else {
1561            return false;
1562        };
1563        // Never hashed: see `non_integer_number`.
1564        let hash = non_integer_number(data).is_none().then(|| data_hash(data));
1565        let ok = hash.is_some_and(|hash| {
1566            hash == link.attestation.data_hash
1567                || entry.redacted_data_hash == Some(hash)
1568        });
1569        entry.content_matches = Some(ok);
1570        ok
1571    }
1572}
1573
1574// ---------------------------------------------------------------------------
1575// Signing
1576// ---------------------------------------------------------------------------
1577
1578/// Attest an entry: the one construction path for
1579/// [`GovernanceAttestation`], used by the server at insert and by tests
1580/// building fixtures.
1581///
1582/// `signed_at` is truncated to whole seconds, the precision the signature
1583/// covers; store the value the attestation carries, not the one passed in.
1584pub fn attest(
1585    key: &SigningKey,
1586    envelope: &Envelope,
1587    chain_seq: u64,
1588    signed_at: DateTime<Utc>,
1589) -> GovernanceAttestation {
1590    let signed_at = truncate_to_seconds(signed_at);
1591    let entry_hash = envelope.entry_hash();
1592    let signature =
1593        crypto::sign(key, entry_hash.as_bytes(), signed_at.timestamp());
1594    GovernanceAttestation {
1595        envelope_version: envelope.agora_governance_log,
1596        chain_seq,
1597        prev_hash: envelope.prev_hash,
1598        data_hash: envelope.data_hash,
1599        entry_hash,
1600        signature: signature.into(),
1601        signed_at,
1602        retroactive: is_retroactive(envelope.created_at(), signed_at),
1603    }
1604}
1605
1606// ---------------------------------------------------------------------------
1607// Verification
1608// ---------------------------------------------------------------------------
1609
1610/// Why one link failed on its own, before chain context
1611#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1612pub enum LinkError {
1613    #[error(
1614        "envelope version {0} is not supported (this verifier knows {ENVELOPE_VERSION})"
1615    )]
1616    UnsupportedVersion(u32),
1617    #[error("entry_hash does not recompute from the envelope fields")]
1618    HashMismatch,
1619    #[error("signature does not verify under the published key")]
1620    BadSignature,
1621}
1622
1623/// Recompute a link's `entry_hash` from its fields
1624pub fn recompute_entry_hash(link: &GovernanceChainLink) -> Sha256Hex {
1625    Envelope::new(
1626        link.id.clone(),
1627        link.entry_type,
1628        link.created_at,
1629        link.attestation.prev_hash,
1630        link.attestation.data_hash,
1631    )
1632    .entry_hash()
1633}
1634
1635/// Verify one link in isolation: version, hash recomputation, signature
1636pub fn verify_link(
1637    link: &GovernanceChainLink,
1638    key: &VerifyingKey,
1639) -> Result<(), LinkError> {
1640    let a = &link.attestation;
1641    if a.envelope_version != ENVELOPE_VERSION {
1642        return Err(LinkError::UnsupportedVersion(a.envelope_version));
1643    }
1644    if recompute_entry_hash(link) != a.entry_hash {
1645        return Err(LinkError::HashMismatch);
1646    }
1647    if !crypto::verify(
1648        key,
1649        a.entry_hash.as_bytes(),
1650        a.signed_at.timestamp(),
1651        &Signature::from(&a.signature),
1652    ) {
1653        return Err(LinkError::BadSignature);
1654    }
1655    Ok(())
1656}
1657
1658/// `true` when `data` is what `link` attested
1659pub fn verify_data(
1660    link: &GovernanceChainLink,
1661    data: &serde_json::Value,
1662) -> bool {
1663    data_hash(data) == link.attestation.data_hash
1664}
1665
1666/// Whether `read`, serialized again, has the shape `written` had: an
1667/// object wherever it has one, an array of the same length wherever it
1668/// has one. Missing and `null` are the same thing.
1669///
1670/// serde's derived structs also read positionally from an array, so
1671/// `[]` is a perfectly good struct of optional fields and `[2, "routine",
1672/// …]` a perfectly good rotation. No other implementation would agree,
1673/// and two verifiers that disagree about what is well-formed can be shown
1674/// two different chains.
1675fn same_shape(written: &serde_json::Value, read: &serde_json::Value) -> bool {
1676    use serde_json::Value::{Array, Null, Object};
1677    match (written, read) {
1678        (Object(w), Object(r)) => r.iter().all(|(k, r)| match w.get(k) {
1679            Some(w) => same_shape(w, r),
1680            None => r.is_null(),
1681        }),
1682        (Array(w), Array(r)) => {
1683            w.len() == r.len() && w.iter().zip(r).all(|(w, r)| same_shape(w, r))
1684        }
1685        (_, Object(_) | Array(_)) => false,
1686        (Object(_) | Array(_), Null) => false,
1687        _ => true,
1688    }
1689}
1690
1691/// `T` from the JSON it was written as, held to [`same_shape`]
1692fn read_strictly<T>(written: &serde_json::Value) -> Result<T, String>
1693where
1694    T: Serialize + serde::de::DeserializeOwned,
1695{
1696    let read: T =
1697        serde_json::from_value(written.clone()).map_err(|e| e.to_string())?;
1698    let again = serde_json::to_value(&read).map_err(|e| e.to_string())?;
1699    if same_shape(written, &again) {
1700        Ok(read)
1701    } else {
1702        Err("an array where an object belongs, or the reverse".into())
1703    }
1704}
1705
1706/// A chain from the JSON it was served as, held to [`same_shape`]: a
1707/// link is an object, and so is everything in it that should be.
1708///
1709/// Prefer this to deserializing [`GovernanceChainLink`]s directly, which
1710/// also accepts a link written as an array of its fields. Nothing serves
1711/// one; a verifier that would read it agrees with no other.
1712pub fn links_from_json(
1713    chain: &serde_json::Value,
1714) -> Result<Vec<GovernanceChainLink>, String> {
1715    chain
1716        .as_array()
1717        .ok_or("a chain is an array of links")?
1718        .iter()
1719        .map(read_strictly)
1720        .collect()
1721}
1722
1723/// [`read_strictly`] for a field that is outside any signed `data`
1724fn read_as_written<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
1725where
1726    D: serde::Deserializer<'de>,
1727    T: Serialize + serde::de::DeserializeOwned,
1728{
1729    let written = serde_json::Value::deserialize(deserializer)?;
1730    if written.is_null() {
1731        return Ok(None);
1732    }
1733    read_strictly(&written)
1734        .map(Some)
1735        .map_err(serde::de::Error::custom)
1736}
1737
1738/// The id series an entry type must use, and must not
1739fn prefix_problem(link: &GovernanceChainLink) -> Option<String> {
1740    let prefix = link.id.prefix();
1741    let reserved =
1742        matches!(prefix, GovernanceLogPrefix::Amd | GovernanceLogPrefix::Key);
1743    let expected = match link.entry_type {
1744        GovernanceLogEntryType::Amendment => Some(GovernanceLogPrefix::Amd),
1745        GovernanceLogEntryType::KeyRotation => Some(GovernanceLogPrefix::Key),
1746        _ => None,
1747    };
1748    match expected {
1749        Some(want) if prefix != want => Some(format!(
1750            "the id of a {} entry must be in the {want}- series, not {}",
1751            link.entry_type, link.id
1752        )),
1753        None if reserved => Some(format!(
1754            "{prefix}- ids are reserved for amendment and key_rotation \
1755             entries, but {} is a {}",
1756            link.id, link.entry_type
1757        )),
1758        _ => None,
1759    }
1760}
1761
1762/// Which earlier entry an amendment names, once it is known to be one
1763fn amendment_target(
1764    amendment: &Amendment,
1765    seq: u64,
1766    seq_of: &HashMap<&str, u64>,
1767    links: &[&GovernanceChainLink],
1768) -> Result<u64, AmendmentError> {
1769    amendment.validate()?;
1770    let target = *seq_of.get(amendment.target.as_str()).ok_or_else(|| {
1771        AmendmentError::UnknownTarget(amendment.target.clone())
1772    })?;
1773    if target >= seq {
1774        return Err(AmendmentError::ForwardReference(amendment.target.clone()));
1775    }
1776    if links[target as usize - 1].attestation.entry_hash
1777        != amendment.target_entry_hash
1778    {
1779        return Err(AmendmentError::WrongTargetHash(amendment.target.clone()));
1780    }
1781    Ok(target)
1782}
1783
1784/// The key history as the walk discovers it
1785struct KeyWalk {
1786    /// Oldest first; the last entry is the active key
1787    history: Vec<(GovernanceKeyRecord, VerifyingKey)>,
1788    unanchored: Vec<PublicKeyHex>,
1789    /// Every key that has held the chain, voided ones included. The anchor
1790    /// lists retired and compromised keys too, so without this a thief
1791    /// could declare a "compromise" that rotates back to the key they stole.
1792    seen: HashSet<PublicKeyHex>,
1793    /// `chain_seq`s inside a compromise window
1794    repudiated: HashSet<u64>,
1795    genesis: PublicKeyHex,
1796    /// A rotation has carried the genesis key's certificate
1797    genesis_certified: bool,
1798}
1799
1800impl KeyWalk {
1801    fn new(genesis: &VerifyingKey, anchor: &KeyAnchor) -> Self {
1802        let public_key = PublicKeyHex::from(genesis);
1803        Self {
1804            genesis: public_key,
1805            genesis_certified: false,
1806            history: vec![(
1807                GovernanceKeyRecord {
1808                    public_key,
1809                    from_seq: 1,
1810                    through_seq: None,
1811                    status: KeyStatus::Active,
1812                    introduced_by: None,
1813                    retired_by: None,
1814                    certified: false,
1815                },
1816                *genesis,
1817            )],
1818            unanchored: if anchor.contains(&public_key) {
1819                Vec::new()
1820            } else {
1821                vec![public_key]
1822            },
1823            seen: HashSet::from([public_key]),
1824            repudiated: HashSet::new(),
1825        }
1826    }
1827
1828    /// The key that signs entry `seq`
1829    fn in_force(&self, seq: u64) -> (PublicKeyHex, VerifyingKey) {
1830        let (record, key) = self
1831            .history
1832            .iter()
1833            .rev()
1834            .find(|(r, _)| r.from_seq <= seq)
1835            .unwrap_or(&self.history[0]);
1836        (record.public_key, *key)
1837    }
1838
1839    /// The key that signs whatever comes next
1840    fn active(&self) -> PublicKeyHex {
1841        self.history
1842            .last()
1843            .map(|(r, _)| r.public_key)
1844            .expect("the genesis key is always in the history")
1845    }
1846
1847    fn close(
1848        &mut self,
1849        through_seq: u64,
1850        status: KeyStatus,
1851        by: &GovernanceLogId,
1852    ) {
1853        if let Some((record, _)) = self.history.last_mut() {
1854            record.through_seq = Some(through_seq);
1855            record.status = status;
1856            record.retired_by = Some(by.clone());
1857        }
1858    }
1859
1860    fn open(
1861        &mut self,
1862        public_key: PublicKeyHex,
1863        key: VerifyingKey,
1864        from_seq: u64,
1865        by: &GovernanceLogId,
1866    ) {
1867        self.history.push((
1868            GovernanceKeyRecord {
1869                public_key,
1870                from_seq,
1871                through_seq: None,
1872                status: KeyStatus::Active,
1873                introduced_by: Some(by.clone()),
1874                retired_by: None,
1875                certified: true,
1876            },
1877            key,
1878        ));
1879    }
1880
1881    /// Follow `rotation`, appended as `id` at `seq`
1882    fn apply(
1883        &mut self,
1884        rotation: &KeyRotation,
1885        link: &GovernanceChainLink,
1886        seq: u64,
1887        links: &[&GovernanceChainLink],
1888        roots: &RootSet,
1889    ) -> Result<(), RotationError> {
1890        rotation.verify_certified(seq, link.attestation.prev_hash, roots)?;
1891        let new_key = rotation
1892            .new_key
1893            .to_verifying_key()
1894            .map_err(|_| RotationError::BadNewKey)?;
1895        if self.seen.contains(&rotation.new_key) {
1896            return Err(RotationError::ReusedKey);
1897        }
1898        // The genesis key predates the root, so the first rotation brings
1899        // its certificate along. Whether that rotation is later voided by
1900        // a compromise does not matter: the certificate is the root's
1901        // statement, not the entry's.
1902        match (&rotation.outgoing_certificate, self.genesis_certified) {
1903            (None, false) => {
1904                return Err(RotationError::MissingGenesisCertificate);
1905            }
1906            (Some(_), true) => {
1907                return Err(RotationError::UnexpectedOutgoingCertificate);
1908            }
1909            (Some(certificate), false) => certificate
1910                .verify_for(&KeyCertStatement::genesis(self.genesis), roots)
1911                .map_err(RotationError::OutgoingCertificate)?,
1912            (None, true) => {}
1913        }
1914        match rotation.reason {
1915            RotationReason::Routine => {
1916                if rotation.old_key != self.in_force(seq).0 {
1917                    return Err(RotationError::WrongOldKey);
1918                }
1919                self.close(seq, KeyStatus::Retired, &link.id);
1920                self.open(rotation.new_key, new_key, seq + 1, &link.id);
1921            }
1922            RotationReason::Compromise => {
1923                let head = rotation
1924                    .last_trusted()
1925                    .ok_or(RotationError::MissingLastTrusted)?;
1926                let trusted_seq = head.chain_seq;
1927                let names_an_earlier_entry = trusted_seq >= 1
1928                    && trusted_seq < seq
1929                    && links[trusted_seq as usize - 1].id == head.id
1930                    && links[trusted_seq as usize - 1].attestation.entry_hash
1931                        == head.entry_hash;
1932                if !names_an_earlier_entry {
1933                    return Err(RotationError::UnknownLastTrusted);
1934                }
1935                // Trust cannot be anchored inside a window nobody trusts,
1936                // reattested or not: name an entry from before it.
1937                if self.repudiated.contains(&trusted_seq) {
1938                    return Err(RotationError::RepudiatedLastTrusted);
1939                }
1940                // The key in force at the last trusted entry: a rotation
1941                // inside the window is void with the rest of it.
1942                if rotation.old_key != self.in_force(trusted_seq).0 {
1943                    return Err(RotationError::WrongOldKey);
1944                }
1945                self.history.retain(|(r, _)| r.from_seq <= trusted_seq);
1946                self.close(trusted_seq, KeyStatus::Compromised, &link.id);
1947                self.open(rotation.new_key, new_key, seq, &link.id);
1948                self.repudiated.extend(trusted_seq + 1..seq);
1949            }
1950        }
1951        self.seen.insert(rotation.new_key);
1952        if !self.genesis_certified {
1953            self.genesis_certified = true;
1954            self.history[0].0.certified = true;
1955            self.unanchored.clear();
1956        }
1957        Ok(())
1958    }
1959}
1960
1961/// Verify a whole chain from `genesis_key`, following the rotations that
1962/// `roots` certified and no others.
1963///
1964/// Links are sorted by `chain_seq` first, so the caller's order does not
1965/// matter. `retroactive` and `out_of_order` are recomputed from the
1966/// timestamps, not copied. `content_matches` is filled only for links that
1967/// carry `data` — for the rest, see
1968/// [`check_content`](GovernanceVerification::check_content).
1969///
1970/// `genesis_key` is the key the chain started under; it is not in the
1971/// chain, so a verifier has to be told. Until the chain's first rotation
1972/// certifies it, `anchor` is what vouches for it: if it is not there it
1973/// is reported in `unanchored_keys` rather than rejected — a client
1974/// pinning what it saw first passes `KeyAnchor::pinned(key)` and gets a
1975/// clean report. Pass [`RootSet::published`] for `roots` outside tests.
1976///
1977/// A rotation is authentic iff its [`KeyCertificate`] is valid for the
1978/// new key at that position; who signed the entry only follows from which
1979/// key *can* (the old one for a routine rotation, the new one once the
1980/// old is compromised). So a thief holding the online key can append
1981/// entries — which a compromise declaration then repudiates — but can
1982/// never move the chain.
1983///
1984/// The rules the report records that no type states on its own: an id
1985/// belongs to its entry type's series and appears once (`AMD-` and `KEY-`
1986/// are reserved for the two types that carry `data`); only an entry whose
1987/// own signature and linkage verify amends anything or moves the key, so
1988/// a forged entry cannot also describe the chain; and an amendment inside
1989/// a repudiated window has no effect unless a [`AmendmentKind::Reattested`]
1990/// vouches for its own entry first, resolved to a fixpoint.
1991pub fn verify_chain(
1992    links: &[GovernanceChainLink],
1993    genesis_key: &VerifyingKey,
1994    anchor: &KeyAnchor,
1995    roots: &RootSet,
1996) -> GovernanceVerification {
1997    let mut links: Vec<&GovernanceChainLink> = links.iter().collect();
1998    links.sort_by_key(|l| l.attestation.chain_seq);
1999
2000    let mut seq_of: HashMap<&str, u64> = HashMap::new();
2001    let mut duplicates: HashSet<&str> = HashSet::new();
2002    for (i, link) in links.iter().enumerate() {
2003        if seq_of.insert(link.id.as_str(), i as u64 + 1).is_some() {
2004            duplicates.insert(link.id.as_str());
2005        }
2006    }
2007
2008    let mut walk = KeyWalk::new(genesis_key, anchor);
2009    // (seq, amendment id, amendment, target seq), in chain order
2010    let mut amendments: Vec<(u64, GovernanceLogId, Amendment, u64)> =
2011        Vec::new();
2012    let mut entries: Vec<EntryVerdict> = Vec::with_capacity(links.len());
2013    let mut prev: Option<&GovernanceChainLink> = None;
2014
2015    for (i, link) in links.iter().enumerate() {
2016        let a = &link.attestation;
2017        let expected_seq = i as u64 + 1;
2018        let mut problems: Vec<String> = Vec::new();
2019
2020        if let Some(problem) = prefix_problem(link) {
2021            problems.push(problem);
2022        }
2023        if duplicates.contains(link.id.as_str()) {
2024            problems.push(format!("{} appears more than once", link.id));
2025        }
2026
2027        // The two entry types the verifier has to read. `data` is hashed
2028        // against the envelope before it is parsed, so what is read is
2029        // what was signed.
2030        let carries_meaning = matches!(
2031            link.entry_type,
2032            GovernanceLogEntryType::Amendment
2033                | GovernanceLogEntryType::KeyRotation
2034        );
2035        let mut amendment: Option<Amendment> = None;
2036        let mut rotation: Option<KeyRotation> = None;
2037        let mut content_matches: Option<bool> = None;
2038        match &link.data {
2039            Some(data) if non_integer_number(data).is_some() => {
2040                content_matches = Some(false);
2041                problems.push(format!(
2042                    "`data` has a number that is not a 64-bit integer at {:?}; \
2043                     governance data never contains one",
2044                    non_integer_number(data).unwrap_or_default()
2045                ));
2046            }
2047            Some(data) => {
2048                let matched = data_hash(data) == a.data_hash;
2049                content_matches = Some(matched);
2050                if !matched {
2051                    if carries_meaning {
2052                        problems.push(
2053                            "`data` does not hash to the attested data_hash"
2054                                .into(),
2055                        );
2056                    }
2057                } else {
2058                    match link.entry_type {
2059                        GovernanceLogEntryType::Amendment => {
2060                            match read_strictly(data) {
2061                                Ok(v) => amendment = Some(v),
2062                                Err(e) => problems.push(format!(
2063                                    "amendment `data` is malformed: {e}"
2064                                )),
2065                            }
2066                        }
2067                        GovernanceLogEntryType::KeyRotation => {
2068                            match read_strictly(data) {
2069                                Ok(v) => rotation = Some(v),
2070                                Err(e) => problems.push(format!(
2071                                    "key_rotation `data` is malformed: {e}"
2072                                )),
2073                            }
2074                        }
2075                        _ => {}
2076                    }
2077                }
2078            }
2079            None if carries_meaning => problems.push(format!(
2080                "a {} entry must carry its `data`",
2081                link.entry_type
2082            )),
2083            None => {}
2084        }
2085
2086        // A compromise declaration is signed by the new key, and is
2087        // taken at its word only if the root certified that key here.
2088        // Everything else is signed by the key in force.
2089        let declared = rotation
2090            .as_ref()
2091            .filter(|r| r.reason == RotationReason::Compromise)
2092            .map(|r| {
2093                if walk.seen.contains(&r.new_key) {
2094                    return Err(RotationError::ReusedKey);
2095                }
2096                r.verify_certified(expected_seq, a.prev_hash, roots)?;
2097                r.new_key
2098                    .to_verifying_key()
2099                    .map_err(|_| RotationError::BadNewKey)
2100            });
2101        let (key_hex, key) = match &declared {
2102            Some(Ok(k)) => (PublicKeyHex::from(k), *k),
2103            _ => walk.in_force(expected_seq),
2104        };
2105        // Say why a declaration was not taken at its word; the bad
2106        // signature that follows is the consequence, not the cause.
2107        if let Some(Err(e)) = &declared {
2108            problems.push(e.to_string());
2109        }
2110
2111        let (hash_ok, signature_valid) = match verify_link(link, &key) {
2112            Ok(()) => (true, true),
2113            Err(LinkError::BadSignature) => {
2114                problems.push(LinkError::BadSignature.to_string());
2115                (true, false)
2116            }
2117            Err(e) => {
2118                problems.push(e.to_string());
2119                (false, false)
2120            }
2121        };
2122
2123        let mut link_valid = hash_ok;
2124        if a.chain_seq != expected_seq {
2125            link_valid = false;
2126            problems.push(format!(
2127                "chain_seq {} where {expected_seq} was expected",
2128                a.chain_seq
2129            ));
2130        }
2131        let expected_prev = prev.map(|p| p.attestation.entry_hash);
2132        if a.prev_hash != expected_prev {
2133            link_valid = false;
2134            problems.push(match (a.prev_hash, expected_prev) {
2135                (Some(_), None) => "first entry names a predecessor".into(),
2136                (None, Some(_)) => "prev_hash is null mid-chain".into(),
2137                _ => "prev_hash is not the previous entry's entry_hash".into(),
2138            });
2139        }
2140        let out_of_order = prev.is_some_and(|p| link.created_at < p.created_at);
2141
2142        // Only an entry that is itself authentic moves the key or amends
2143        // anything: a forged one already fails the chain, and must not
2144        // also get to describe it.
2145        let authentic = signature_valid && link_valid;
2146        if let Some(rotation) = rotation.as_ref().filter(|_| authentic)
2147            && let Err(e) =
2148                walk.apply(rotation, link, expected_seq, &links, roots)
2149        {
2150            let problem = e.to_string();
2151            if !problems.contains(&problem) {
2152                problems.push(problem);
2153            }
2154        }
2155        // Texts are checked against what the entry signed whether or not
2156        // the amendment takes effect: a substituted text is a lie about
2157        // the record either way.
2158        let mut texts = None;
2159        if let Some(amendment) = amendment
2160            .as_ref()
2161            .filter(|a| authentic && a.validate().is_ok())
2162        {
2163            match amendment.text_status(link.texts.as_ref()) {
2164                Ok(status) => texts = status,
2165                Err(e) => problems.push(e.to_string()),
2166            }
2167        } else if link.texts.as_ref().is_some_and(|t| !t.is_empty()) {
2168            problems.push(AmendmentError::UncommittedText.to_string());
2169        }
2170        if let Some(amendment) = amendment.filter(|_| authentic) {
2171            match amendment_target(&amendment, expected_seq, &seq_of, &links) {
2172                Ok(target) => amendments.push((
2173                    expected_seq,
2174                    link.id.clone(),
2175                    amendment,
2176                    target,
2177                )),
2178                Err(e) => problems.push(e.to_string()),
2179            }
2180        }
2181
2182        entries.push(EntryVerdict {
2183            id: link.id.clone(),
2184            chain_seq: a.chain_seq,
2185            signature_valid,
2186            link_valid,
2187            content_matches,
2188            retroactive: is_retroactive(link.created_at, a.signed_at),
2189            out_of_order,
2190            amended_by: Vec::new(),
2191            signed_by: Some(key_hex),
2192            redacted: false,
2193            redacted_data_hash: None,
2194            repudiated: false,
2195            reattested_by: Vec::new(),
2196            texts,
2197            problem: (!problems.is_empty()).then(|| problems.join("; ")),
2198        });
2199        prev = Some(link);
2200    }
2201
2202    // Reattestations first, and to a fixpoint: an amendment inside a
2203    // repudiated window has no effect unless something later vouches for
2204    // it, and that something can be another reattestation.
2205    let mut applied = vec![false; amendments.len()];
2206    loop {
2207        let mut changed = false;
2208        for (i, (seq, id, amendment, target)) in amendments.iter().enumerate() {
2209            if applied[i]
2210                || amendment.kind != AmendmentKind::Reattested
2211                || walk.repudiated.contains(seq)
2212            {
2213                continue;
2214            }
2215            applied[i] = true;
2216            entries[*target as usize - 1].reattested_by.push(id.clone());
2217            walk.repudiated.remove(target);
2218            changed = true;
2219        }
2220        if !changed {
2221            break;
2222        }
2223    }
2224
2225    for (seq, id, amendment, target) in &amendments {
2226        if walk.repudiated.contains(seq) {
2227            continue;
2228        }
2229        let entry = &mut entries[*target as usize - 1];
2230        entry.amended_by.push(id.clone());
2231        if let Some(redaction) = &amendment.redaction {
2232            entry.redacted = true;
2233            entry.redacted_data_hash = Some(redaction.resulting_data_hash);
2234        }
2235    }
2236
2237    // A redacted entry's content is what the redaction left behind.
2238    for (i, link) in links.iter().enumerate() {
2239        if entries[i].content_matches == Some(false)
2240            && let (Some(data), Some(hash)) =
2241                (&link.data, entries[i].redacted_data_hash)
2242            && non_integer_number(data).is_none()
2243            && data_hash(data) == hash
2244        {
2245            entries[i].content_matches = Some(true);
2246        }
2247    }
2248
2249    let mut seen = HashSet::new();
2250    walk.unanchored.retain(|key| seen.insert(*key));
2251
2252    let mut repudiated = Vec::new();
2253    for (i, entry) in entries.iter_mut().enumerate() {
2254        if walk.repudiated.contains(&(i as u64 + 1)) {
2255            entry.repudiated = true;
2256            repudiated.push(entry.id.clone());
2257        }
2258    }
2259
2260    GovernanceVerification {
2261        public_key: walk.active(),
2262        ok: false,
2263        head: prev.map(|p| p.id.clone()),
2264        entries,
2265        keys: walk.history.into_iter().map(|(record, _)| record).collect(),
2266        unanchored_keys: walk.unanchored,
2267        repudiated,
2268    }
2269    .settle()
2270}
2271
2272#[cfg(test)]
2273mod tests {
2274    use super::*;
2275    use crate::crypto::generate_keypair;
2276    use serde_json::json;
2277
2278    pub(super) fn gov(n: u32) -> GovernanceLogId {
2279        format!("GOV-2026-{n:04}").parse().unwrap()
2280    }
2281
2282    pub(super) fn amd(n: u32) -> GovernanceLogId {
2283        format!("AMD-2026-{n:04}").parse().unwrap()
2284    }
2285
2286    pub(super) fn key_id(n: u32) -> GovernanceLogId {
2287        format!("KEY-2026-{n:04}").parse().unwrap()
2288    }
2289
2290    pub(super) fn at(secs: i64) -> DateTime<Utc> {
2291        DateTime::from_timestamp(1_700_000_000 + secs, 123_456_789).unwrap()
2292    }
2293
2294    /// The anchor a client that pinned the key it first saw would hold
2295    fn anchored(key: &VerifyingKey) -> KeyAnchor {
2296        KeyAnchor::pinned(key.into())
2297    }
2298
2299    /// `draft` under salts fixed by its texts and `seq`, so that a chain
2300    /// built twice is the same bytes twice
2301    pub(super) fn resalted(draft: &AmendmentDraft, seq: u64) -> AmendmentDraft {
2302        let fix = |field: &str, t: &Option<CommittedText>| {
2303            t.as_ref().map(|t| {
2304                let salt = Sha256::digest(format!("{seq}/{field}/{}", t.text));
2305                CommittedText::with_salt(
2306                    TextSalt::from(<[u8; 32]>::from(salt)),
2307                    t.text.clone(),
2308                )
2309            })
2310        };
2311        let texts = AmendmentTexts {
2312            basis: fix("basis", &draft.texts.basis),
2313            note: fix("note", &draft.texts.note),
2314            rationale: fix("rationale", &draft.texts.rationale),
2315        };
2316        let commit = |t: &Option<CommittedText>| {
2317            t.as_ref().map(|t| AmendmentText::Committed(t.commitment()))
2318        };
2319        AmendmentDraft {
2320            amendment: Amendment {
2321                basis: commit(&texts.basis).unwrap(),
2322                note: commit(&texts.note).unwrap(),
2323                rationale: commit(&texts.rationale),
2324                ..draft.amendment.clone()
2325            },
2326            texts,
2327        }
2328    }
2329
2330    /// `draft` as version 1 wrote it: the texts in the signed `data`
2331    pub(super) fn v1(draft: AmendmentDraft) -> Amendment {
2332        let plain =
2333            |t: Option<CommittedText>| t.map(|t| AmendmentText::Plain(t.text));
2334        Amendment {
2335            agora_governance_amendment: 1,
2336            basis: plain(draft.texts.basis).unwrap(),
2337            note: plain(draft.texts.note).unwrap(),
2338            rationale: plain(draft.texts.rationale),
2339            ..draft.amendment
2340        }
2341    }
2342
2343    /// A throwaway root key. Fixed, so the vectors are byte-stable; the
2344    /// real ones live on hardware and sign nothing in a test.
2345    pub(super) fn root(n: u8) -> SigningKey {
2346        SigningKey::from_bytes(&[0xA0 + n; 32])
2347    }
2348
2349    /// `root(1)` and `root(2)`, either of which suffices
2350    pub(super) fn roots() -> RootSet {
2351        RootSet::new(
2352            [1, 2].map(|n| PublicKeyHex::from(&root(n).verifying_key())),
2353            1,
2354        )
2355    }
2356
2357    /// `statement`, signed by each of `signers` as a root would
2358    pub(super) fn certify(
2359        signers: &[&SigningKey],
2360        statement: KeyCertStatement,
2361    ) -> KeyCertificate {
2362        use ed25519_dalek::Signer;
2363        let message = statement.signed_bytes();
2364        signers.iter().fold(
2365            KeyCertificate::unsigned(statement),
2366            |certificate, signer| {
2367                certificate.with(RootSignature {
2368                    root_key: (&signer.verifying_key()).into(),
2369                    signature: signer.sign(&message).into(),
2370                })
2371            },
2372        )
2373    }
2374
2375    /// `root(1)`'s routine certificate for `key` at `c`'s next position
2376    fn for_new_at(c: &Chain, key: &VerifyingKey) -> KeyCertificate {
2377        certify(
2378            &[&root(1)],
2379            KeyCertStatement::routine(key.into(), c.next_seq(), c.prev_hash()),
2380        )
2381    }
2382
2383    pub(super) fn link(
2384        key: &SigningKey,
2385        n: u32,
2386        prev: Option<&GovernanceChainLink>,
2387        data: &serde_json::Value,
2388        signed_at: DateTime<Utc>,
2389    ) -> GovernanceChainLink {
2390        let created_at = truncate_to_micros(at(n as i64 * 10));
2391        let envelope = Envelope::new(
2392            gov(n),
2393            GovernanceLogEntryType::CouncilDecision,
2394            created_at,
2395            prev.map(|p| p.attestation.entry_hash),
2396            data_hash(data),
2397        );
2398        let attestation = attest(
2399            key,
2400            &envelope,
2401            prev.map_or(1, |p| p.attestation.chain_seq + 1),
2402            signed_at,
2403        );
2404        GovernanceChainLink {
2405            id: gov(n),
2406            entry_type: GovernanceLogEntryType::CouncilDecision,
2407            created_at,
2408            attestation,
2409            data: None,
2410            texts: None,
2411        }
2412    }
2413
2414    pub(super) fn chain(key: &SigningKey, n: u32) -> Vec<GovernanceChainLink> {
2415        let mut out: Vec<GovernanceChainLink> = Vec::new();
2416        for i in 1..=n {
2417            let data = json!({"title": format!("Decision {i}"), "outcome": "approved"});
2418            let l = link(key, i, out.last(), &data, at(i as i64 * 10 + 1));
2419            out.push(l);
2420        }
2421        out
2422    }
2423
2424    /// A chain under construction: one entry per `push`, each series
2425    /// numbered on its own, `data` carried for the entries a verifier
2426    /// reads.
2427    pub(super) struct Chain {
2428        pub(super) links: Vec<GovernanceChainLink>,
2429        gov: u32,
2430        amd: u32,
2431        key: u32,
2432        /// Whoever signed the first entry
2433        genesis: Option<PublicKeyHex>,
2434    }
2435
2436    impl Chain {
2437        pub(super) fn new() -> Self {
2438            Self {
2439                links: Vec::new(),
2440                gov: 0,
2441                amd: 0,
2442                key: 0,
2443                genesis: None,
2444            }
2445        }
2446
2447        pub(super) fn prev_hash(&self) -> Option<Sha256Hex> {
2448            self.links.last().map(|l| l.attestation.entry_hash)
2449        }
2450
2451        /// The `entry_hash` of the 1-indexed link `seq`
2452        pub(super) fn hash_at(&self, seq: usize) -> Sha256Hex {
2453            self.links[seq - 1].attestation.entry_hash
2454        }
2455
2456        /// The `chain_seq` the next entry gets
2457        pub(super) fn next_seq(&self) -> u64 {
2458            self.links.len() as u64 + 1
2459        }
2460
2461        /// The 1-indexed link `seq`, as a compromise names it
2462        pub(super) fn head(&self, seq: usize) -> TrustedHead {
2463            TrustedHead {
2464                id: self.links[seq - 1].id.clone(),
2465                chain_seq: seq as u64,
2466                entry_hash: self.hash_at(seq),
2467            }
2468        }
2469
2470        /// The genesis certificate, if the next rotation is the first
2471        fn outgoing(&self, rotation: KeyRotation) -> KeyRotation {
2472            match (self.key, self.genesis) {
2473                (0, Some(genesis)) => rotation.with_outgoing(certify(
2474                    &[&root(1)],
2475                    KeyCertStatement::genesis(genesis),
2476                )),
2477                _ => rotation,
2478            }
2479        }
2480
2481        /// A routine rotation to `new` at the next position, certified by
2482        /// `root(1)`
2483        pub(super) fn routine(
2484            &self,
2485            old: &VerifyingKey,
2486            new: &SigningKey,
2487        ) -> KeyRotation {
2488            let statement = KeyCertStatement::routine(
2489                (&new.verifying_key()).into(),
2490                self.next_seq(),
2491                self.prev_hash(),
2492            );
2493            self.outgoing(KeyRotation::routine(
2494                old.into(),
2495                new,
2496                self.prev_hash(),
2497                at(self.next_seq() as i64 * 10 + 5),
2498                certify(&[&root(1)], statement),
2499            ))
2500        }
2501
2502        /// A compromise declaration at the next position trusting `old`
2503        /// through the 1-indexed link `trusted`, certified by `root(1)`
2504        pub(super) fn compromise(
2505            &self,
2506            old: &VerifyingKey,
2507            new: &SigningKey,
2508            trusted: usize,
2509        ) -> KeyRotation {
2510            let statement = KeyCertStatement::compromise(
2511                (&new.verifying_key()).into(),
2512                self.next_seq(),
2513                self.prev_hash(),
2514                self.head(trusted),
2515            );
2516            self.outgoing(KeyRotation::compromise(
2517                old.into(),
2518                new,
2519                self.prev_hash(),
2520                at(self.next_seq() as i64 * 10 + 5),
2521                certify(&[&root(1)], statement),
2522            ))
2523        }
2524
2525        /// What [`Chain::amend`] will call the next amendment
2526        pub(super) fn next_amd(&self) -> GovernanceLogId {
2527            amd(self.amd + 1)
2528        }
2529
2530        pub(super) fn push(
2531            &mut self,
2532            signer: &SigningKey,
2533            id: GovernanceLogId,
2534            entry_type: GovernanceLogEntryType,
2535            data: serde_json::Value,
2536            carry: bool,
2537        ) -> GovernanceLogId {
2538            self.genesis
2539                .get_or_insert_with(|| (&signer.verifying_key()).into());
2540            let n = self.links.len() as i64 + 1;
2541            let created_at = truncate_to_micros(at(n * 10));
2542            let envelope = Envelope::new(
2543                id.clone(),
2544                entry_type,
2545                created_at,
2546                self.prev_hash(),
2547                data_hash(&data),
2548            );
2549            let attestation =
2550                attest(signer, &envelope, n as u64, at(n * 10 + 1));
2551            self.links.push(GovernanceChainLink {
2552                id: id.clone(),
2553                entry_type,
2554                created_at,
2555                attestation,
2556                data: carry.then_some(data),
2557                texts: None,
2558            });
2559            id
2560        }
2561
2562        /// A council decision carrying `data` (which the link does not,
2563        /// as the chain endpoint does not carry transcripts)
2564        pub(super) fn entry(
2565            &mut self,
2566            signer: &SigningKey,
2567            data: serde_json::Value,
2568        ) -> GovernanceLogId {
2569            self.gov += 1;
2570            let id = gov(self.gov);
2571            self.push(
2572                signer,
2573                id,
2574                GovernanceLogEntryType::CouncilDecision,
2575                data,
2576                false,
2577            )
2578        }
2579
2580        pub(super) fn decision(
2581            &mut self,
2582            signer: &SigningKey,
2583        ) -> GovernanceLogId {
2584            let data = json!({"title": format!("Decision {}", self.gov + 1)});
2585            self.entry(signer, data)
2586        }
2587
2588        /// `draft`'s amendment as the entry's `data`, its texts beside it
2589        pub(super) fn amend(
2590            &mut self,
2591            signer: &SigningKey,
2592            draft: &AmendmentDraft,
2593        ) -> GovernanceLogId {
2594            let draft = resalted(draft, self.next_seq());
2595            let id = self.amend_v1(signer, &draft.amendment);
2596            let link = self.links.last_mut().unwrap();
2597            link.texts = Some(draft.texts);
2598            id
2599        }
2600
2601        /// An amendment with nothing beside it: version 1, or a version 2
2602        /// whose texts have all been withheld
2603        pub(super) fn amend_v1(
2604            &mut self,
2605            signer: &SigningKey,
2606            amendment: &Amendment,
2607        ) -> GovernanceLogId {
2608            self.amd += 1;
2609            let id = amd(self.amd);
2610            self.push(
2611                signer,
2612                id,
2613                GovernanceLogEntryType::Amendment,
2614                serde_json::to_value(amendment).unwrap(),
2615                true,
2616            )
2617        }
2618
2619        pub(super) fn rotate(
2620            &mut self,
2621            signer: &SigningKey,
2622            rotation: &KeyRotation,
2623        ) -> GovernanceLogId {
2624            self.key += 1;
2625            let id = key_id(self.key);
2626            self.push(
2627                signer,
2628                id,
2629                GovernanceLogEntryType::KeyRotation,
2630                serde_json::to_value(rotation).unwrap(),
2631                true,
2632            )
2633        }
2634    }
2635
2636    // -- canonical JSON --
2637
2638    #[test]
2639    fn canonical_json_sorts_keys_at_every_level() {
2640        let v = json!({"b": {"z": 1, "a": [{"y": 2, "x": 3}]}, "a": null});
2641        assert_eq!(
2642            canonical_json(&v),
2643            br#"{"a":null,"b":{"a":[{"x":3,"y":2}],"z":1}}"#
2644        );
2645    }
2646
2647    #[test]
2648    fn canonical_json_is_compact_and_escapes_like_serde() {
2649        let v = json!({"s": "tab\there \"q\" ünïcode \u{1F600}", "n": [1, -2, 3.5, true, false]});
2650        let bytes = canonical_json(&v);
2651        let text = std::str::from_utf8(&bytes).unwrap();
2652        assert_eq!(
2653            text,
2654            r#"{"n":[1,-2,3.5,true,false],"s":"tab\there \"q\" ünïcode 😀"}"#
2655        );
2656    }
2657
2658    #[test]
2659    fn canonical_json_ignores_insertion_order() {
2660        let mut a = serde_json::Map::new();
2661        a.insert("z".into(), json!(1));
2662        a.insert("a".into(), json!(2));
2663        let mut b = serde_json::Map::new();
2664        b.insert("a".into(), json!(2));
2665        b.insert("z".into(), json!(1));
2666        assert_eq!(
2667            canonical_json(&serde_json::Value::Object(a)),
2668            canonical_json(&serde_json::Value::Object(b))
2669        );
2670    }
2671
2672    #[test]
2673    fn canonical_json_empty_containers() {
2674        assert_eq!(canonical_json(&json!({})), b"{}");
2675        assert_eq!(canonical_json(&json!([])), b"[]");
2676        assert_eq!(
2677            canonical_json(&json!({"a": {}, "b": []})),
2678            br#"{"a":{},"b":[]}"#
2679        );
2680    }
2681
2682    // -- envelope --
2683
2684    #[test]
2685    fn preimage_is_declaration_ordered_json() {
2686        let e = Envelope::new(
2687            gov(1),
2688            GovernanceLogEntryType::AppealsCourtDecision,
2689            at(0),
2690            None,
2691            data_hash(&json!({})),
2692        );
2693        let text = String::from_utf8(e.preimage()).unwrap();
2694        assert!(text.starts_with(r#"{"agora_governance_log":1,"id":"GOV-2026-0001","entry_type":"appeals_court_decision","created_at":1700000000123456,"prev_hash":null,"data_hash":""#), "{text}");
2695    }
2696
2697    #[test]
2698    fn every_envelope_field_changes_the_hash() {
2699        let base = Envelope::new(
2700            gov(1),
2701            GovernanceLogEntryType::CouncilDecision,
2702            at(0),
2703            None,
2704            data_hash(&json!({"a":1})),
2705        );
2706        let h = base.entry_hash();
2707        let mut e = base.clone();
2708        e.id = gov(2);
2709        assert_ne!(e.entry_hash(), h);
2710        let mut e = base.clone();
2711        e.entry_type = GovernanceLogEntryType::PolicyChange;
2712        assert_ne!(e.entry_hash(), h);
2713        let mut e = base.clone();
2714        e.created_at += 1;
2715        assert_ne!(e.entry_hash(), h);
2716        let mut e = base.clone();
2717        e.prev_hash = Some(h);
2718        assert_ne!(e.entry_hash(), h);
2719        let mut e = base.clone();
2720        e.data_hash = data_hash(&json!({"a":2}));
2721        assert_ne!(e.entry_hash(), h);
2722        assert_eq!(base.entry_hash(), h, "and it is deterministic");
2723    }
2724
2725    #[test]
2726    fn truncation_matches_what_the_envelope_carries() {
2727        let t = at(0);
2728        assert_eq!(truncate_to_micros(t).timestamp_subsec_nanos(), 123_456_000);
2729        assert_eq!(truncate_to_seconds(t).timestamp_subsec_nanos(), 0);
2730        assert_eq!(
2731            Envelope::new(
2732                gov(1),
2733                GovernanceLogEntryType::CouncilDecision,
2734                t,
2735                None,
2736                data_hash(&json!(null))
2737            )
2738            .created_at,
2739            truncate_to_micros(t).timestamp_micros()
2740        );
2741    }
2742
2743    // -- hex newtypes --
2744
2745    #[test]
2746    fn hex_newtypes_round_trip_and_reject_wrong_lengths() {
2747        let h = data_hash(&json!(1));
2748        let s = serde_json::to_string(&h).unwrap();
2749        assert_eq!(s.len(), 66);
2750        let back: Sha256Hex = serde_json::from_str(&s).unwrap();
2751        assert_eq!(back, h);
2752        assert!(serde_json::from_str::<Sha256Hex>("\"abcd\"").is_err());
2753        assert!("zz".repeat(32).parse::<Sha256Hex>().is_err());
2754        assert!(Sha256Hex::try_from(vec![0u8; 31]).is_err());
2755        assert_eq!(format!("{h:?}"), format!("Sha256Hex({h})"));
2756    }
2757
2758    // -- signing and verification --
2759
2760    #[test]
2761    fn attest_then_verify_link() {
2762        let (key, pk) = generate_keypair();
2763        let l = link(&key, 1, None, &json!({"a": 1}), at(5));
2764        assert_eq!(verify_link(&l, &pk), Ok(()));
2765        assert!(verify_data(&l, &json!({"a": 1})));
2766        assert!(!verify_data(&l, &json!({"a": 2})));
2767        assert!(!l.attestation.retroactive);
2768    }
2769
2770    #[test]
2771    fn wrong_key_fails_signature_only() {
2772        let (key, _) = generate_keypair();
2773        let (_, other) = generate_keypair();
2774        let l = link(&key, 1, None, &json!({}), at(5));
2775        assert_eq!(verify_link(&l, &other), Err(LinkError::BadSignature));
2776    }
2777
2778    #[test]
2779    fn tampering_with_any_attested_field_is_detected() {
2780        let (key, pk) = generate_keypair();
2781        let l = link(&key, 1, None, &json!({"a": 1}), at(5));
2782
2783        let mut t = l.clone();
2784        t.created_at += chrono::Duration::microseconds(1);
2785        assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
2786
2787        let mut t = l.clone();
2788        t.entry_type = GovernanceLogEntryType::StewardVeto;
2789        assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
2790
2791        let mut t = l.clone();
2792        t.attestation.data_hash = data_hash(&json!({"a": 2}));
2793        assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
2794
2795        // Back-dating the signature: the hash still recomputes, but the
2796        // signature covered the real signed_at.
2797        let mut t = l.clone();
2798        t.attestation.signed_at -= chrono::Duration::seconds(1);
2799        assert_eq!(verify_link(&t, &pk), Err(LinkError::BadSignature));
2800
2801        let mut t = l.clone();
2802        t.attestation.envelope_version = 2;
2803        assert_eq!(verify_link(&t, &pk), Err(LinkError::UnsupportedVersion(2)));
2804    }
2805
2806    #[test]
2807    fn a_good_chain_verifies_in_any_input_order() {
2808        let (key, pk) = generate_keypair();
2809        let mut c = chain(&key, 4);
2810        c.reverse();
2811        let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
2812        assert!(v.ok, "{v:#?}");
2813        assert_eq!(v.head, Some(gov(4)));
2814        assert_eq!(
2815            v.entries.iter().map(|e| e.chain_seq).collect::<Vec<_>>(),
2816            [1, 2, 3, 4]
2817        );
2818        assert!(v.entries.iter().all(|e| e.content_matches.is_none()
2819            && e.problem.is_none()
2820            && !e.out_of_order));
2821        assert_eq!(v.public_key, PublicKeyHex::from(&pk));
2822    }
2823
2824    #[test]
2825    fn empty_chain_is_ok_with_no_head() {
2826        let (_, pk) = generate_keypair();
2827        let v = verify_chain(&[], &pk, &anchored(&pk), &roots());
2828        assert!(v.ok);
2829        assert!(v.head.is_none());
2830        assert!(v.entries.is_empty());
2831    }
2832
2833    #[test]
2834    fn a_changed_entry_breaks_its_signature_and_the_next_link() {
2835        let (key, pk) = generate_keypair();
2836        let mut c = chain(&key, 3);
2837        // Re-attest entry 2 with different data but the same predecessor,
2838        // as a key holder rewriting history would.
2839        let rewritten = link(
2840            &key,
2841            2,
2842            Some(&c[0]),
2843            &json!({"title": "Decision 2", "outcome": "REJECTED"}),
2844            at(21),
2845        );
2846        c[1] = rewritten;
2847        let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
2848        assert!(!v.ok);
2849        assert!(
2850            v.entries[1].signature_valid && v.entries[1].link_valid,
2851            "the rewrite itself is well-formed: {:#?}",
2852            v.entries[1]
2853        );
2854        assert!(
2855            !v.entries[2].link_valid,
2856            "but entry 3 no longer points at it: {:#?}",
2857            v.entries[2]
2858        );
2859        assert!(
2860            v.entries[2]
2861                .problem
2862                .as_deref()
2863                .unwrap()
2864                .contains("prev_hash")
2865        );
2866    }
2867
2868    #[test]
2869    fn a_removed_entry_is_a_gap_and_a_broken_link() {
2870        let (key, pk) = generate_keypair();
2871        let mut c = chain(&key, 3);
2872        c.remove(1);
2873        let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
2874        assert!(!v.ok);
2875        assert!(v.entries[0].link_valid);
2876        let p = v.entries[1].problem.as_deref().unwrap();
2877        assert!(p.contains("chain_seq 3 where 2 was expected"), "{p}");
2878        assert!(p.contains("prev_hash"), "{p}");
2879    }
2880
2881    #[test]
2882    fn a_second_genesis_is_rejected() {
2883        let (key, pk) = generate_keypair();
2884        let mut c = chain(&key, 2);
2885        let rogue = link(&key, 2, None, &json!({}), at(21));
2886        c[1] = rogue;
2887        let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
2888        assert!(!v.ok);
2889        assert!(
2890            v.entries[1]
2891                .problem
2892                .as_deref()
2893                .unwrap()
2894                .contains("null mid-chain")
2895        );
2896    }
2897
2898    #[test]
2899    fn retroactive_and_out_of_order_are_recomputed_not_copied() {
2900        let (key, pk) = generate_keypair();
2901        let first = link(&key, 1, None, &json!({}), at(10 + 3600));
2902        // Second entry recorded *before* the first by the clock, but after it
2903        // in the chain. Valid chain, flagged order.
2904        let created = truncate_to_micros(at(5));
2905        let envelope = Envelope::new(
2906            gov(2),
2907            GovernanceLogEntryType::CouncilDecision,
2908            created,
2909            Some(first.attestation.entry_hash),
2910            data_hash(&json!({})),
2911        );
2912        let attestation = attest(&key, &envelope, 2, at(6));
2913        let mut second = GovernanceChainLink {
2914            id: gov(2),
2915            entry_type: GovernanceLogEntryType::CouncilDecision,
2916            created_at: created,
2917            attestation,
2918            data: None,
2919            texts: None,
2920        };
2921        second.attestation.retroactive = true; // a lying flag on the wire
2922        let v = verify_chain(&[first, second], &pk, &anchored(&pk), &roots());
2923        assert!(v.ok, "{v:#?}");
2924        assert!(v.entries[0].retroactive);
2925        assert!(!v.entries[1].retroactive, "recomputed from timestamps");
2926        assert!(v.entries[1].out_of_order);
2927    }
2928
2929    #[test]
2930    fn content_mismatch_settles_to_not_ok() {
2931        let (key, pk) = generate_keypair();
2932        let c = chain(&key, 1);
2933        let mut v = verify_chain(&c, &pk, &anchored(&pk), &roots());
2934        v.entries[0].content_matches = Some(true);
2935        assert!(v.clone().settle().ok);
2936        v.entries[0].content_matches = Some(false);
2937        assert!(!v.settle().ok);
2938    }
2939
2940    // -- amendments --
2941
2942    #[test]
2943    fn an_amendment_fills_amended_by_on_its_target() {
2944        let (key, pk) = generate_keypair();
2945        let mut c = Chain::new();
2946        let target = c.decision(&key);
2947        c.decision(&key);
2948        let amendment = AmendmentDraft::new(
2949            target,
2950            c.hash_at(1),
2951            AmendmentKind::NonPrecedential,
2952            "§1 (Red Team Cases Recharacterized)",
2953            "diagnostic finding — not citable as moderation precedent",
2954        )
2955        .unwrap()
2956        .with_authority(gov(5))
2957        .with_rationale("§5 leaves the ruling itself standing");
2958        let id = c.amend(&key, &amendment);
2959
2960        let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
2961        assert!(v.ok, "{v:#?}");
2962        assert_eq!(v.entries[0].amended_by, vec![id]);
2963        assert!(v.entries[1].amended_by.is_empty());
2964        assert!(!v.entries[0].redacted);
2965        assert_eq!(v.head, Some(amd(1)));
2966        // The amendment link carries `data`, so its own content is checked.
2967        assert_eq!(v.entries[2].content_matches, Some(true));
2968        assert_eq!(v.entries[0].content_matches, None);
2969        assert_eq!(
2970            standing([amendment.amendment.kind]),
2971            Standing::NonPrecedential
2972        );
2973    }
2974
2975    #[test]
2976    fn an_amendment_must_name_an_earlier_entry_by_its_exact_hash() {
2977        let (key, pk) = generate_keypair();
2978        let problem = |c: &Chain, at: usize| -> String {
2979            verify_chain(&c.links, &pk, &anchored(&pk), &roots()).entries[at]
2980                .problem
2981                .clone()
2982                .unwrap_or_default()
2983        };
2984
2985        let mut c = Chain::new();
2986        c.decision(&key);
2987        let unknown = AmendmentDraft::new(
2988            gov(99),
2989            c.hash_at(1),
2990            AmendmentKind::Overruled,
2991            "b",
2992            "n",
2993        )
2994        .unwrap();
2995        c.amend(&key, &unknown);
2996        assert!(
2997            problem(&c, 1).contains("is not an entry of this chain"),
2998            "{}",
2999            problem(&c, 1)
3000        );
3001        assert!(!verify_chain(&c.links, &pk, &anchored(&pk), &roots()).ok);
3002
3003        // Names an entry that does not exist yet.
3004        let mut c = Chain::new();
3005        c.decision(&key);
3006        let forward = AmendmentDraft::new(
3007            gov(2),
3008            c.hash_at(1),
3009            AmendmentKind::Overruled,
3010            "b",
3011            "n",
3012        )
3013        .unwrap();
3014        c.amend(&key, &forward);
3015        c.decision(&key);
3016        assert!(
3017            problem(&c, 1).contains("not an earlier entry"),
3018            "{}",
3019            problem(&c, 1)
3020        );
3021
3022        // Right id, wrong entry.
3023        let mut c = Chain::new();
3024        let target = c.decision(&key);
3025        let wrong_hash = AmendmentDraft::new(
3026            target,
3027            data_hash(&json!("some other entry")),
3028            AmendmentKind::Overruled,
3029            "b",
3030            "n",
3031        )
3032        .unwrap();
3033        c.amend(&key, &wrong_hash);
3034        assert!(problem(&c, 1).contains("entry_hash"), "{}", problem(&c, 1));
3035        assert!(
3036            verify_chain(&c.links, &pk, &anchored(&pk), &roots()).entries[0]
3037                .amended_by
3038                .is_empty()
3039        );
3040    }
3041
3042    #[test]
3043    fn redaction_shape_violations_fail_verification() {
3044        let (key, pk) = generate_keypair();
3045        let amend_with = |mutate: &dyn Fn(&mut Amendment)| -> String {
3046            let mut c = Chain::new();
3047            let target = c.decision(&key);
3048            let mut amendment = AmendmentDraft::new(
3049                target,
3050                c.hash_at(1),
3051                AmendmentKind::Correction,
3052                "b",
3053                "n",
3054            )
3055            .unwrap();
3056            mutate(&mut amendment.amendment);
3057            c.amend_v1(&key, &amendment.amendment);
3058            let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3059            assert!(!v.ok, "{v:#?}");
3060            v.entries[1].problem.clone().unwrap_or_default()
3061        };
3062
3063        // `redaction` is the only way to get a well-formed one, so a
3064        // redaction kind without a `Redaction` can only be hand-built.
3065        assert_eq!(
3066            AmendmentDraft::new(
3067                gov(1),
3068                data_hash(&json!(null)),
3069                AmendmentKind::Redaction,
3070                "b",
3071                "n"
3072            ),
3073            Err(AmendmentError::MissingRedaction)
3074        );
3075        let p = amend_with(&|a| a.kind = AmendmentKind::Redaction);
3076        assert!(p.contains("requires a `redaction`"), "{p}");
3077
3078        let p = amend_with(&|a| {
3079            a.redaction = Some(Redaction {
3080                fields: vec!["/x".into()],
3081                resulting_data_hash: data_hash(&json!({})),
3082            })
3083        });
3084        assert!(p.contains("only valid on kind"), "{p}");
3085
3086        let p = amend_with(&|a| a.agora_governance_amendment = 3);
3087        assert!(p.contains("agora_governance_amendment is 3"), "{p}");
3088
3089        // A version says where the texts are, and both ways round it is
3090        // held to it.
3091        let p = amend_with(&|a| a.agora_governance_amendment = 1);
3092        assert!(p.contains("does neither consistently"), "{p}");
3093        let p = amend_with(&|a| a.note = AmendmentText::Plain("n".into()));
3094        assert!(p.contains("does neither consistently"), "{p}");
3095    }
3096
3097    #[test]
3098    fn governance_data_holds_no_number_that_is_not_a_64_bit_integer() {
3099        let fine = json!({"a": [1, -2, u64::MAX, i64::MIN], "b": {"c": "0.5"}});
3100        assert_eq!(non_integer_number(&fine), None);
3101        assert!(blind_data(&fine, Blind::random()).is_ok());
3102
3103        for (text, pointer) in [
3104            (r#"{"a": {"b/c": [1, 0.5]}}"#, "/a/b~1c/1"),
3105            (r#"{"n": 1.0}"#, "/n"),
3106            (r#"{"n": 1e3}"#, "/n"),
3107            (r#"{"n": 18446744073709551616}"#, "/n"),
3108            (r#"{"n": -9223372036854775809}"#, "/n"),
3109        ] {
3110            let data: serde_json::Value = serde_json::from_str(text).unwrap();
3111            assert_eq!(non_integer_number(&data).as_deref(), Some(pointer));
3112            assert_eq!(
3113                blind_data(&data, Blind::random()),
3114                Err(BlindError::NonIntegerNumber(pointer.into())),
3115                "the writer refuses it"
3116            );
3117            assert_eq!(
3118                redact_data(&data, &["/n".into()], &amd(1), Blind::random()),
3119                Err(RedactError::NonIntegerNumber(pointer.into()))
3120            );
3121        }
3122    }
3123
3124    #[test]
3125    fn standing_is_the_last_amendment_that_changes_it() {
3126        use AmendmentKind::*;
3127        assert_eq!(standing([]), Standing::InForce);
3128        assert_eq!(standing([Correction, Redaction]), Standing::InForce);
3129        assert_eq!(
3130            standing([Correction, NonPrecedential, Redaction]),
3131            Standing::NonPrecedential
3132        );
3133        assert_eq!(standing([Overruled, Reinstated]), Standing::InForce);
3134        assert_eq!(standing([Reinstated, Superseded]), Standing::Superseded);
3135        assert_eq!(kind_standing(Reattested), None);
3136        assert_eq!(kind_standing(Reinstated), Some(Standing::InForce));
3137    }
3138
3139    #[test]
3140    fn a_redaction_verifies_against_the_amendment_and_nothing_else() {
3141        let (key, pk) = generate_keypair();
3142        let data = json!({
3143            "finding": "upheld",
3144            "subject": {"handle": "someone", "detail": "personal"},
3145        });
3146        let mut c = Chain::new();
3147        let target = c.entry(&key, data.clone());
3148        let amendment_id = c.next_amd();
3149        let (amendment, redacted) = AmendmentDraft::redaction(
3150            &amendment_id,
3151            target,
3152            c.hash_at(1),
3153            "GDPR Art. 17(1)(a)",
3154            "personal data removed on request",
3155            vec!["/subject/handle".into(), "/subject/detail".into()],
3156            &data,
3157            Blind::from([7; 32]),
3158        )
3159        .unwrap();
3160        assert_eq!(c.amend(&key, &amendment), amendment_id);
3161        assert_eq!(redacted[BLIND_KEY], json!(Blind::from([7; 32])));
3162
3163        let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3164        assert!(v.ok, "{v:#?}");
3165        assert!(v.entries[0].redacted);
3166        assert_eq!(v.entries[0].redacted_data_hash, Some(data_hash(&redacted)));
3167        assert_eq!(
3168            redacted["subject"]["handle"],
3169            json!(format!("[redacted by {amendment_id}]"))
3170        );
3171        assert_eq!(redacted["finding"], json!("upheld"), "and nothing else");
3172
3173        // What the server now serves verifies.
3174        assert!(v.check_content(&c.links[0], &redacted));
3175        assert_eq!(v.entries[0].content_matches, Some(true));
3176        assert!(v.clone().settle().ok);
3177        // So does the original, for anyone who kept a copy.
3178        assert!(v.check_content(&c.links[0], &data));
3179
3180        // A second edit does not.
3181        let mut tampered = redacted.clone();
3182        tampered["finding"] = json!("overturned");
3183        assert!(!v.check_content(&c.links[0], &tampered));
3184        assert_eq!(v.entries[0].content_matches, Some(false));
3185        assert!(!v.settle().ok);
3186    }
3187
3188    #[test]
3189    fn content_that_matches_nothing_fails_without_an_amendment() {
3190        let (key, pk) = generate_keypair();
3191        let mut c = Chain::new();
3192        c.entry(&key, json!({"a": 1}));
3193        let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3194        assert!(!v.check_content(&c.links[0], &json!({"a": 2})));
3195        assert!(!v.clone().settle().ok);
3196        // An entry that is not in the report at all is not a pass either.
3197        let other = link(&key, 9, None, &json!({}), at(9));
3198        assert!(!v.check_content(&other, &json!({})));
3199    }
3200
3201    #[test]
3202    fn tampering_with_an_amendments_data_is_caught_by_the_data_hash() {
3203        let (key, pk) = generate_keypair();
3204        let mut c = Chain::new();
3205        let target = c.decision(&key);
3206        let amendment = AmendmentDraft::new(
3207            target,
3208            c.hash_at(1),
3209            AmendmentKind::Overruled,
3210            "b",
3211            "overruled by a later decision",
3212        )
3213        .unwrap();
3214        c.amend(&key, &amendment);
3215        c.links[1].data.as_mut().unwrap()["note"] =
3216            json!("reinstated, actually");
3217
3218        let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3219        assert!(!v.ok, "{v:#?}");
3220        let p = v.entries[1].problem.as_deref().unwrap();
3221        assert!(p.contains("does not hash to the attested data_hash"), "{p}");
3222        assert!(
3223            v.entries[0].amended_by.is_empty(),
3224            "an unreadable amendment has no effect"
3225        );
3226        assert!(
3227            v.entries[1].signature_valid && v.entries[1].link_valid,
3228            "the envelope is untouched — only the content is not what it \
3229             committed to"
3230        );
3231    }
3232
3233    #[test]
3234    fn an_amendment_or_rotation_must_carry_its_data() {
3235        let (key, pk) = generate_keypair();
3236        let mut c = Chain::new();
3237        let target = c.decision(&key);
3238        let amendment = AmendmentDraft::new(
3239            target,
3240            c.hash_at(1),
3241            AmendmentKind::Correction,
3242            "b",
3243            "n",
3244        )
3245        .unwrap();
3246        c.amend(&key, &amendment);
3247        let rotation = c.routine(&pk, &generate_keypair().0);
3248        c.rotate(&key, &rotation);
3249        c.links[1].data = None;
3250        c.links[2].data = None;
3251
3252        let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3253        assert!(!v.ok, "{v:#?}");
3254        for (i, entry_type) in [(1, "amendment"), (2, "key_rotation")] {
3255            let p = v.entries[i].problem.as_deref().unwrap();
3256            assert!(p.contains(&format!("a {entry_type} entry")), "{p}");
3257            assert!(p.contains("must carry its `data`"), "{p}");
3258        }
3259    }
3260
3261    #[test]
3262    fn the_id_series_must_match_the_entry_type() {
3263        let (key, pk) = generate_keypair();
3264        let mut c = Chain::new();
3265        let target = c.decision(&key);
3266        let amendment = AmendmentDraft::new(
3267            target,
3268            c.hash_at(1),
3269            AmendmentKind::Correction,
3270            "b",
3271            "n",
3272        )
3273        .unwrap();
3274        c.push(
3275            &key,
3276            gov(7),
3277            GovernanceLogEntryType::Amendment,
3278            serde_json::to_value(&amendment.amendment).unwrap(),
3279            true,
3280        );
3281        let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3282        assert!(!v.ok, "{v:#?}");
3283        let p = v.entries[1].problem.as_deref().unwrap();
3284        assert!(p.contains("must be in the AMD- series"), "{p}");
3285
3286        let mut c = Chain::new();
3287        c.push(
3288            &key,
3289            key_id(1),
3290            GovernanceLogEntryType::CouncilDecision,
3291            json!({}),
3292            false,
3293        );
3294        let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3295        let p = v.entries[0].problem.as_deref().unwrap();
3296        assert!(p.contains("reserved"), "{p}");
3297    }
3298
3299    #[test]
3300    fn redact_data_replaces_whole_values_and_refuses_the_rest() {
3301        let id = amd(3);
3302        let blind = Blind::from([9; 32]);
3303        let data = json!({"a": {"b": [1, {"c": "secret"}]}, "d/e": "slash"});
3304        let out = redact_data(
3305            &data,
3306            &["/a/b/1/c".into(), "/d~1e".into()],
3307            &id,
3308            blind,
3309        )
3310        .unwrap();
3311        assert_eq!(out["a"]["b"][1]["c"], json!(redaction_marker(&id)));
3312        assert_eq!(out["d/e"], json!(redaction_marker(&id)));
3313        assert_eq!(out["a"]["b"][0], json!(1), "untouched");
3314        assert_eq!(out[BLIND_KEY], json!(blind), "a legacy entry gains one");
3315
3316        assert_eq!(
3317            redact_data(&data, &["/a/nope".into()], &id, blind),
3318            Err(RedactError::Unresolved("/a/nope".into()))
3319        );
3320        assert_eq!(
3321            redact_data(&data, &["".into()], &id, blind),
3322            Err(RedactError::WholeEntry)
3323        );
3324        assert_eq!(
3325            redact_data(&data, &[], &id, blind),
3326            Err(RedactError::NoFields)
3327        );
3328        assert_eq!(
3329            redact_data(&data, &["/_blind".into()], &id, blind),
3330            Err(RedactError::BlindPointer("/_blind".into()))
3331        );
3332    }
3333
3334    #[test]
3335    fn blind_data_is_for_objects_and_is_the_writers_to_supply() {
3336        let blind = Blind::from([1; 32]);
3337        let out = blind_data(&json!({"finding": "upheld"}), blind).unwrap();
3338        assert_eq!(out, json!({"finding": "upheld", "_blind": blind}));
3339        assert_eq!(
3340            blind_data(&json!([1]), blind),
3341            Err(BlindError::NotAnObject)
3342        );
3343        assert_eq!(blind_data(&out, blind), Err(BlindError::AlreadyBlinded));
3344        assert_ne!(Blind::random(), Blind::random());
3345
3346        use GovernanceLogEntryType::*;
3347        assert!(!is_redactable(Amendment) && !is_redactable(KeyRotation));
3348        assert!(
3349            is_redactable(CouncilDecision) && is_redactable(EmergencyAction)
3350        );
3351    }
3352
3353    /// The attack blinding exists for, performed. Everything the attacker
3354    /// uses is public after a redaction: the redacted data, the entry's
3355    /// original `data_hash`, and each redaction's `resulting_data_hash`.
3356    #[test]
3357    fn a_removed_value_cannot_be_confirmed_by_guessing_it() {
3358        // Put a guess back where a marker is and see if a public hash agrees
3359        fn confirms(
3360            public: &serde_json::Value,
3361            pointer: &str,
3362            guess: &str,
3363            hash: Sha256Hex,
3364        ) -> bool {
3365            let mut attempt = public.clone();
3366            *attempt.pointer_mut(pointer).unwrap() = json!(guess);
3367            data_hash(&attempt) == hash
3368        }
3369        let legacy = json!({"finding": "upheld", "handle": "someone", "city": "Utrecht"});
3370
3371        // Blinded when written: the right guess confirms nothing.
3372        let written = blind_data(&legacy, Blind::random()).unwrap();
3373        let first = redact_data(
3374            &written,
3375            &["/handle".into()],
3376            &amd(1),
3377            Blind::random(),
3378        )
3379        .unwrap();
3380        assert!(!confirms(&first, "/handle", "someone", data_hash(&written)));
3381
3382        // A second redaction of the same entry: the first one's
3383        // `resulting_data_hash` is public too, and covers the city.
3384        let second =
3385            redact_data(&first, &["/city".into()], &amd(2), Blind::random())
3386                .unwrap();
3387        assert!(!confirms(&second, "/city", "Utrecht", data_hash(&first)));
3388
3389        // An entry from before blinding has nothing to destroy, and gains a
3390        // key it never had: strip it and the right guess does confirm. This
3391        // is the residual exposure of the entries that predate 0.27...
3392        let mut stripped =
3393            redact_data(&legacy, &["/handle".into()], &amd(3), Blind::random())
3394                .unwrap();
3395        let legacy_first = stripped.clone();
3396        stripped.as_object_mut().unwrap().remove(BLIND_KEY);
3397        assert!(confirms(
3398            &stripped,
3399            "/handle",
3400            "someone",
3401            data_hash(&legacy)
3402        ));
3403        // ...and it ends at the first redaction, which left a blind behind.
3404        let legacy_second = redact_data(
3405            &legacy_first,
3406            &["/city".into()],
3407            &amd(4),
3408            Blind::random(),
3409        )
3410        .unwrap();
3411        assert!(!confirms(
3412            &legacy_second,
3413            "/city",
3414            "Utrecht",
3415            data_hash(&legacy_first)
3416        ));
3417    }
3418
3419    // -- key rotation --
3420
3421    #[test]
3422    fn a_routine_rotation_moves_the_chain_to_the_new_key() {
3423        let (old, old_pk) = generate_keypair();
3424        let (new, new_pk) = generate_keypair();
3425        let mut c = Chain::new();
3426        c.decision(&old);
3427        let rotation = c.routine(&old_pk, &new);
3428        let rotation_id = c.rotate(&old, &rotation);
3429        c.decision(&new);
3430
3431        // Nothing but the root vouches for the new key, and nothing else
3432        // has to.
3433        let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3434        assert!(v.ok, "{v:#?}");
3435        assert_eq!(v.public_key, (&new_pk).into());
3436        assert_eq!(
3437            v.entries[1].signed_by,
3438            Some((&old_pk).into()),
3439            "the rotation itself is signed by the old key"
3440        );
3441        assert_eq!(v.entries[2].signed_by, Some((&new_pk).into()));
3442        assert!(v.unanchored_keys.is_empty());
3443        assert!(v.repudiated.is_empty());
3444        assert_eq!(v.keys.len(), 2);
3445        assert_eq!(v.keys[0].public_key, (&old_pk).into());
3446        assert_eq!(v.keys[0].from_seq, 1);
3447        assert_eq!(v.keys[0].through_seq, Some(2));
3448        assert_eq!(v.keys[0].status, KeyStatus::Retired);
3449        assert_eq!(v.keys[0].introduced_by, None);
3450        assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
3451        assert!(v.keys[0].certified, "retroactively, by the rotation");
3452        assert_eq!(v.keys[1].from_seq, 3);
3453        assert_eq!(v.keys[1].through_seq, None);
3454        assert_eq!(v.keys[1].status, KeyStatus::Active);
3455        assert_eq!(v.keys[1].introduced_by.as_ref(), Some(&rotation_id));
3456        assert!(v.keys[1].certified);
3457    }
3458
3459    #[test]
3460    fn the_old_key_cannot_sign_after_a_routine_rotation() {
3461        let (old, old_pk) = generate_keypair();
3462        let (new, _) = generate_keypair();
3463        let mut c = Chain::new();
3464        c.decision(&old);
3465        let rotation = c.routine(&old_pk, &new);
3466        c.rotate(&old, &rotation);
3467        c.decision(&old);
3468
3469        let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3470        assert!(!v.ok, "{v:#?}");
3471        assert!(!v.entries[2].signature_valid);
3472        assert!(
3473            v.entries[2].link_valid,
3474            "the linkage is fine; the key is not"
3475        );
3476    }
3477
3478    /// The scenario the root exists for: the online key alone moves
3479    /// nothing, however well-formed the rotation it signs.
3480    #[test]
3481    fn the_online_key_cannot_certify_its_own_successor() {
3482        let (old, old_pk) = generate_keypair();
3483        let (thief, _) = generate_keypair();
3484        let mut c = Chain::new();
3485        c.decision(&old);
3486        let mut rotation = c.routine(&old_pk, &thief);
3487        // Signed by the stolen online key instead of a root.
3488        let statement = rotation.certificate.statement.clone();
3489        rotation.certificate = certify(&[&old], statement);
3490        c.rotate(&old, &rotation);
3491        c.decision(&thief);
3492
3493        let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3494        assert!(!v.ok, "{v:#?}");
3495        let p = v.entries[1].problem.as_deref().unwrap();
3496        assert!(p.contains("0 valid root signature"), "{p}");
3497        assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
3498        assert!(!v.entries[2].signature_valid);
3499        assert_eq!(v.keys.len(), 1);
3500    }
3501
3502    #[test]
3503    fn a_certificate_counts_distinct_known_roots_only() {
3504        let (old, old_pk) = generate_keypair();
3505        let (new, _) = generate_keypair();
3506        let (stranger, _) = generate_keypair();
3507        let two_of_two = RootSet::new(
3508            [
3509                (&root(1).verifying_key()).into(),
3510                (&root(2).verifying_key()).into(),
3511            ],
3512            2,
3513        );
3514        let verdict = |signers: &[&SigningKey], roots: &RootSet| {
3515            let mut c = Chain::new();
3516            c.decision(&old);
3517            let mut rotation = c.routine(&old_pk, &new);
3518            let statement = rotation.certificate.statement.clone();
3519            rotation.certificate = certify(signers, statement);
3520            // The genesis certificate is held to the same threshold.
3521            let genesis = KeyCertStatement::genesis((&old_pk).into());
3522            rotation.outgoing_certificate =
3523                Some(certify(&[&root(1), &root(2)], genesis));
3524            c.rotate(&old, &rotation);
3525            verify_chain(&c.links, &old_pk, &anchored(&old_pk), roots)
3526        };
3527
3528        assert!(verdict(&[&root(1), &root(2)], &two_of_two).ok);
3529        assert!(verdict(&[&root(2)], &roots()).ok, "either root, 1-of-2");
3530
3531        for (signers, why) in [
3532            (vec![&root(1)], "below the threshold"),
3533            (vec![&root(1), &root(1)], "one root twice is one root"),
3534            (vec![&root(1), &stranger], "an unknown root is nobody"),
3535            (vec![], "unsigned"),
3536        ] {
3537            let v = verdict(&signers, &two_of_two);
3538            assert!(!v.ok, "{why}: {v:#?}");
3539            let p = v.entries[1].problem.as_deref().unwrap();
3540            assert!(p.contains("where 2 are needed"), "{why}: {p}");
3541        }
3542
3543        // An unknown signer beside a sufficient set is not an error.
3544        assert!(verdict(&[&stranger, &root(1)], &roots()).ok);
3545    }
3546
3547    #[test]
3548    fn a_certificate_is_good_for_one_statement_at_one_position() {
3549        let (old, old_pk) = generate_keypair();
3550        let (new, new_pk) = generate_keypair();
3551        let (other, other_pk) = generate_keypair();
3552        let anchor = anchored(&old_pk);
3553
3554        // Certified for the position right after entry 1, appended one
3555        // entry later. The proof of possession is rebuilt for the new
3556        // position; the certificate cannot be.
3557        let mut c = Chain::new();
3558        c.decision(&old);
3559        let early = c.routine(&old_pk, &new);
3560        c.decision(&old);
3561        let mut rotation = c.routine(&old_pk, &new);
3562        rotation.certificate = early.certificate;
3563        c.rotate(&old, &rotation);
3564        let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
3565        assert!(!v.ok, "{v:#?}");
3566        let p = v.entries[2].problem.as_deref().unwrap();
3567        assert!(
3568            p.contains("different key, purpose or chain position"),
3569            "{p}"
3570        );
3571        assert_eq!(v.public_key, (&old_pk).into());
3572
3573        // Certified for one key, presented for another.
3574        let mut c = Chain::new();
3575        c.decision(&old);
3576        let for_new = c.routine(&old_pk, &new);
3577        let mut rotation = c.routine(&old_pk, &other);
3578        rotation.certificate = for_new.certificate;
3579        c.rotate(&old, &rotation);
3580        let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
3581        let p = v.entries[1].problem.as_deref().unwrap();
3582        assert!(
3583            p.contains("different key, purpose or chain position"),
3584            "{p}"
3585        );
3586
3587        // The statement altered after signing, to match.
3588        let mut c = Chain::new();
3589        c.decision(&old);
3590        let mut rotation = c.routine(&old_pk, &other);
3591        rotation.certificate = for_new_at(&c, &new_pk);
3592        rotation.certificate.statement.key = (&other_pk).into();
3593        c.rotate(&old, &rotation);
3594        let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
3595        let p = v.entries[1].problem.as_deref().unwrap();
3596        assert!(p.contains("0 valid root signature"), "{p}");
3597
3598        // A routine certificate does not authorize a compromise.
3599        let mut c = Chain::new();
3600        c.decision(&old);
3601        c.decision(&old);
3602        let mut rotation = c.compromise(&old_pk, &new, 1);
3603        rotation.certificate = for_new_at(&c, &new_pk);
3604        c.rotate(&new, &rotation);
3605        let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
3606        assert!(!v.ok);
3607        assert!(v.repudiated.is_empty(), "{v:#?}");
3608        assert_eq!(v.public_key, (&old_pk).into());
3609    }
3610
3611    /// The same signature over the same JSON without the domain prefix —
3612    /// what a root key tricked into signing "just some JSON" would produce
3613    #[test]
3614    fn a_root_signature_without_the_domain_prefix_certifies_nothing() {
3615        use ed25519_dalek::Signer;
3616        let (old, old_pk) = generate_keypair();
3617        let (new, _) = generate_keypair();
3618        let mut c = Chain::new();
3619        c.decision(&old);
3620        let mut rotation = c.routine(&old_pk, &new);
3621        let statement = rotation.certificate.statement.clone();
3622        let bare = canonical_json(&serde_json::to_value(&statement).unwrap());
3623        assert_eq!(
3624            statement.signed_bytes(),
3625            [ROOT_DOMAIN, bare.as_slice()].concat()
3626        );
3627        rotation.certificate =
3628            KeyCertificate::unsigned(statement).with(RootSignature {
3629                root_key: (&root(1).verifying_key()).into(),
3630                signature: root(1).sign(&bare).into(),
3631            });
3632        c.rotate(&old, &rotation);
3633        let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3634        assert!(!v.ok, "{v:#?}");
3635        assert_eq!(v.public_key, (&old_pk).into());
3636    }
3637
3638    #[test]
3639    fn the_first_rotation_carries_the_genesis_certificate_and_only_it_does() {
3640        let (k1, k1_pk) = generate_keypair();
3641        let (k2, k2_pk) = generate_keypair();
3642        let (k3, _) = generate_keypair();
3643        let genesis =
3644            || certify(&[&root(1)], KeyCertStatement::genesis((&k1_pk).into()));
3645
3646        // Missing.
3647        let mut c = Chain::new();
3648        c.decision(&k1);
3649        let mut rotation = c.routine(&k1_pk, &k2);
3650        rotation.outgoing_certificate = None;
3651        c.rotate(&k1, &rotation);
3652        let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3653        assert!(!v.ok);
3654        let p = v.entries[1].problem.as_deref().unwrap();
3655        assert!(p.contains("genesis key's outgoing_certificate"), "{p}");
3656        assert_eq!(v.public_key, (&k1_pk).into());
3657
3658        // For some other key.
3659        let mut c = Chain::new();
3660        c.decision(&k1);
3661        let rotation = c.routine(&k1_pk, &k2).with_outgoing(certify(
3662            &[&root(1)],
3663            KeyCertStatement::genesis((&k2_pk).into()),
3664        ));
3665        c.rotate(&k1, &rotation);
3666        let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3667        let p = v.entries[1].problem.as_deref().unwrap();
3668        assert!(p.starts_with("outgoing_certificate:"), "{p}");
3669
3670        // Present, and it is what vouches for a genesis key no anchor
3671        // knows.
3672        let mut c = Chain::new();
3673        c.decision(&k1);
3674        let rotation = c.routine(&k1_pk, &k2);
3675        assert_eq!(rotation.outgoing_certificate, Some(genesis()));
3676        c.rotate(&k1, &rotation);
3677        let v = verify_chain(&c.links, &k1_pk, &KeyAnchor::default(), &roots());
3678        assert!(v.ok, "{v:#?}");
3679        assert!(v.unanchored_keys.is_empty(), "{v:#?}");
3680
3681        // A second one, later, is refused.
3682        let again = c.routine(&k2_pk, &k3).with_outgoing(genesis());
3683        c.rotate(&k2, &again);
3684        let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3685        assert!(!v.ok);
3686        let p = v.entries[2].problem.as_deref().unwrap();
3687        assert!(p.contains("nowhere else"), "{p}");
3688    }
3689
3690    #[test]
3691    fn a_forged_proof_of_possession_is_rejected() {
3692        let (old, old_pk) = generate_keypair();
3693        let (_, new_pk) = generate_keypair();
3694        let (thief, _) = generate_keypair();
3695        let mut c = Chain::new();
3696        c.decision(&old);
3697        // A rotation to a key nobody holds, certified in good faith: the
3698        // proof is signed by the old key instead of by `new_key` itself.
3699        let mut rotation = c.routine(&old_pk, &thief);
3700        rotation.new_key = (&new_pk).into();
3701        rotation.certificate = for_new_at(&c, &new_pk);
3702        let statement = rotation.statement(c.prev_hash());
3703        rotation.proof = crypto::sign(
3704            &old,
3705            statement.hash().as_bytes(),
3706            rotation.proof_signed_at,
3707        )
3708        .into();
3709        c.rotate(&old, &rotation);
3710
3711        assert_eq!(
3712            rotation.verify_proof(c.links[1].attestation.prev_hash),
3713            Err(RotationError::BadProof)
3714        );
3715        let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3716        assert!(!v.ok, "{v:#?}");
3717        assert!(
3718            v.entries[1]
3719                .problem
3720                .as_deref()
3721                .unwrap()
3722                .contains("proof of possession")
3723        );
3724        assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
3725    }
3726
3727    #[test]
3728    fn a_compromise_repudiates_the_window_and_a_reattestation_restores_one() {
3729        let (old, old_pk) = generate_keypair();
3730        let (new, new_pk) = generate_keypair();
3731        let mut c = Chain::new();
3732        c.decision(&old); // 1 — the last entry anyone trusts
3733        c.decision(&old); // 2 — inside the window
3734        let reattested = c.decision(&old); // 3 — inside, later vouched for
3735        let rotation = c.compromise(&old_pk, &new, 1);
3736        let rotation_id = c.rotate(&new, &rotation); // 4 — signed by the NEW key
3737        let vouch = AmendmentDraft::new(
3738            reattested,
3739            c.hash_at(3),
3740            AmendmentKind::Reattested,
3741            "Art. VII",
3742            "independently verified; the Steward vouches for it",
3743        )
3744        .unwrap();
3745        let vouch_id = c.amend(&new, &vouch); // 5
3746
3747        let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3748        assert!(
3749            v.ok,
3750            "repudiation is a declared state, not a defect: {v:#?}"
3751        );
3752        assert_eq!(v.repudiated, vec![gov(2)]);
3753        assert!(v.entries[1].repudiated);
3754        assert!(!v.entries[2].repudiated);
3755        assert_eq!(v.entries[2].reattested_by, vec![vouch_id.clone()]);
3756        assert_eq!(v.entries[2].amended_by, vec![vouch_id]);
3757        assert_eq!(v.entries[3].signed_by, Some((&new_pk).into()));
3758        assert_eq!(v.public_key, (&new_pk).into());
3759        assert_eq!(v.keys.len(), 2);
3760        assert_eq!(v.keys[0].status, KeyStatus::Compromised);
3761        assert_eq!(
3762            v.keys[0].through_seq,
3763            Some(1),
3764            "trusted through the last trusted entry, not through the \
3765             declaration"
3766        );
3767        assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
3768        assert_eq!(v.keys[1].from_seq, 4, "the declaration is its own first");
3769    }
3770
3771    /// The root says where the window opens. A declaration cannot trust
3772    /// the old key one entry further than its certificate does.
3773    #[test]
3774    fn last_trusted_is_the_roots_to_say() {
3775        let (old, old_pk) = generate_keypair();
3776        let (new, _) = generate_keypair();
3777        let mut c = Chain::new();
3778        c.decision(&old); // 1
3779        c.decision(&old); // 2
3780        let mut rotation = c.compromise(&old_pk, &new, 1);
3781        rotation.certificate.statement.last_trusted = Some(c.head(2));
3782        c.rotate(&new, &rotation);
3783        let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3784        assert!(!v.ok, "{v:#?}");
3785        assert!(v.repudiated.is_empty());
3786        assert_eq!(v.public_key, (&old_pk).into());
3787    }
3788
3789    #[test]
3790    fn a_stolen_key_cannot_be_rotated_back_in() {
3791        // K1 is compromised and replaced by K2. The thief, still holding
3792        // K1, declares a "compromise" of K2 naming K1 as the new key — and
3793        // even a root certificate would not bring a key back.
3794        let (k1, k1_pk) = generate_keypair();
3795        let (k2, k2_pk) = generate_keypair();
3796        let mut c = Chain::new();
3797        c.decision(&k1); // 1
3798        let real = c.compromise(&k1_pk, &k2, 1);
3799        c.rotate(&k2, &real); // 2
3800        c.decision(&k2); // 3
3801        let honest =
3802            verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3803        assert!(honest.ok, "{honest:#?}");
3804
3805        let hijack = c.compromise(&k2_pk, &k1, 3);
3806        c.rotate(&k1, &hijack); // 4 — signed by the stolen key
3807        c.decision(&k1); // 5
3808
3809        let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3810        assert!(!v.ok);
3811        assert_eq!(v.public_key, (&k2_pk).into(), "the chain stays with K2");
3812        let p = v.entries[3].problem.as_deref().unwrap();
3813        assert!(p.contains("never brought back"), "{p}");
3814        assert!(!v.entries[3].signature_valid, "{:#?}", v.entries[3]);
3815        assert!(!v.entries[4].signature_valid, "K1 signs nothing again");
3816        assert_eq!(v.keys.len(), 2);
3817        assert_eq!(v.keys[1].status, KeyStatus::Active);
3818    }
3819
3820    #[test]
3821    fn a_routine_rotation_cannot_reuse_a_key_either() {
3822        let (k1, k1_pk) = generate_keypair();
3823        let (k2, k2_pk) = generate_keypair();
3824        let mut c = Chain::new();
3825        c.decision(&k1);
3826        let out = c.routine(&k1_pk, &k2);
3827        c.rotate(&k1, &out);
3828        let back = c.routine(&k2_pk, &k1);
3829        c.rotate(&k2, &back);
3830        let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3831        assert!(!v.ok);
3832        assert!(
3833            v.entries[2]
3834                .problem
3835                .as_deref()
3836                .unwrap()
3837                .contains("never brought back"),
3838            "{:#?}",
3839            v.entries[2]
3840        );
3841        assert_eq!(v.public_key, (&k2_pk).into());
3842    }
3843
3844    #[test]
3845    fn a_forged_entry_has_no_effects() {
3846        let (steward, steward_pk) = generate_keypair();
3847        let (forger, _) = generate_keypair();
3848        let anchor = anchored(&steward_pk);
3849        let mut c = Chain::new();
3850        let target = c.decision(&steward); // 1
3851        let fake = AmendmentDraft::new(
3852            target,
3853            c.hash_at(1),
3854            AmendmentKind::Overruled,
3855            "none",
3856            "overruled, says nobody with the key",
3857        )
3858        .unwrap();
3859        c.amend(&forger, &fake); // 2 — not signed by the key in force
3860        // Certified, even: a certificate is not a licence to skip the old
3861        // key's signature on a routine rotation.
3862        let grab = c.routine(&steward_pk, &forger);
3863        c.rotate(&forger, &grab); // 3 — likewise
3864
3865        let v = verify_chain(&c.links, &steward_pk, &anchor, &roots());
3866        assert!(!v.ok);
3867        assert!(v.entries[0].amended_by.is_empty(), "{:#?}", v.entries[0]);
3868        assert_eq!(v.public_key, (&steward_pk).into());
3869        assert_eq!(v.keys.len(), 1);
3870        assert!(v.unanchored_keys.is_empty());
3871    }
3872
3873    #[test]
3874    fn a_repeated_id_is_a_problem() {
3875        let (key, pk) = generate_keypair();
3876        let mut c = Chain::new();
3877        c.decision(&key);
3878        c.gov = 0;
3879        c.decision(&key); // GOV-2026-0001 again
3880        let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3881        assert!(!v.ok);
3882        assert!(v.entries.iter().all(|e| {
3883            e.problem
3884                .as_deref()
3885                .is_some_and(|p| p.contains("more than once"))
3886        }));
3887    }
3888
3889    #[test]
3890    fn a_second_compromise_cannot_anchor_inside_the_first_window() {
3891        let (k1, k1_pk) = generate_keypair();
3892        let (k2, _) = generate_keypair();
3893        let (k3, _) = generate_keypair();
3894        let mut c = Chain::new();
3895        c.decision(&k1); // 1 — trusted
3896        c.decision(&k1); // 2 — inside the first window
3897        let first = c.compromise(&k1_pk, &k2, 1);
3898        c.rotate(&k2, &first); // 3
3899        let second = c.compromise(&k1_pk, &k3, 2);
3900        c.rotate(&k3, &second); // 4
3901
3902        let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3903        assert!(!v.ok);
3904        let p = v.entries[3].problem.as_deref().unwrap();
3905        assert!(p.contains("repudiated"), "{p}");
3906        assert_eq!(v.keys.len(), 2, "K1 and K2; K3 never took the chain");
3907        assert_eq!(v.keys[0].through_seq, Some(1));
3908    }
3909
3910    #[test]
3911    fn an_uncertified_compromise_fails_closed() {
3912        let (old, old_pk) = generate_keypair();
3913        let (new, new_pk) = generate_keypair();
3914        let mut c = Chain::new();
3915        c.decision(&old);
3916        c.decision(&old);
3917        let mut rotation = c.compromise(&old_pk, &new, 1);
3918        let statement = rotation.certificate.statement.clone();
3919        rotation.certificate = certify(&[&new], statement);
3920        c.rotate(&new, &rotation);
3921
3922        // Being in an anchor does not help: only the root moves the chain.
3923        let anchor = anchored(&old_pk).with((&new_pk).into());
3924        let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
3925        assert!(!v.ok, "{v:#?}");
3926        let p = v.entries[2].problem.as_deref().unwrap();
3927        assert!(p.contains("0 valid root signature"), "{p}");
3928        assert!(!v.entries[2].signature_valid, "checked under the old key");
3929        assert!(v.repudiated.is_empty(), "and nothing is repudiated");
3930        assert_eq!(v.public_key, (&old_pk).into());
3931    }
3932
3933    /// A key stolen before anyone knew: the Steward rotates routinely,
3934    /// then learns the old key was already out, and names a head from
3935    /// before the rotation. The rotation is void with the rest of the
3936    /// window.
3937    #[test]
3938    fn a_rotation_inside_the_window_is_void_with_it() {
3939        let (steward, steward_pk) = generate_keypair();
3940        let (successor, _) = generate_keypair();
3941        let (recovery, recovery_pk) = generate_keypair();
3942
3943        let mut c = Chain::new();
3944        c.decision(&steward); // 1 — the last entry anyone trusts
3945        c.decision(&steward); // 2 — the thief's, as it turns out
3946        let routine = c.routine(&steward_pk, &successor);
3947        c.rotate(&steward, &routine); // 3
3948        c.decision(&successor); // 4
3949
3950        let declaration = c.compromise(&steward_pk, &recovery, 1);
3951        c.rotate(&recovery, &declaration); // 5
3952
3953        let v = verify_chain(
3954            &c.links,
3955            &steward_pk,
3956            &anchored(&steward_pk),
3957            &roots(),
3958        );
3959        assert!(v.ok, "{v:#?}");
3960        assert_eq!(v.repudiated, vec![gov(2), key_id(1), gov(3)]);
3961        assert_eq!(v.public_key, (&recovery_pk).into());
3962        assert_eq!(v.keys.len(), 2, "the successor is not part of history");
3963        assert_eq!(v.keys[0].public_key, (&steward_pk).into());
3964        assert_eq!(v.keys[0].status, KeyStatus::Compromised);
3965        assert!(
3966            v.keys[0].certified,
3967            "the genesis certificate rode in on the voided rotation and \
3968             is the root's word all the same"
3969        );
3970        assert_eq!(v.keys[1].public_key, (&recovery_pk).into());
3971    }
3972
3973    #[test]
3974    fn a_compromise_must_name_a_real_head_and_the_key_that_held_it() {
3975        let (old, old_pk) = generate_keypair();
3976        let (new, new_pk) = generate_keypair();
3977        let anchor = anchored(&old_pk);
3978
3979        // A head whose hash is not that entry's — certified, so the only
3980        // thing wrong is what the chain says about it.
3981        let mut c = Chain::new();
3982        c.decision(&old);
3983        let mut trusted = c.head(1);
3984        trusted.entry_hash = data_hash(&json!("nope"));
3985        let statement = KeyCertStatement::compromise(
3986            (&new_pk).into(),
3987            2,
3988            c.prev_hash(),
3989            trusted,
3990        );
3991        let mut rotation = c.compromise(&old_pk, &new, 1);
3992        rotation.certificate = certify(&[&root(1)], statement);
3993        c.rotate(&new, &rotation);
3994        let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
3995        let p = v.entries[1].problem.as_deref().unwrap();
3996        assert!(p.contains("last_trusted"), "{p}");
3997
3998        // An old_key that was never in force.
3999        let (_, other_pk) = generate_keypair();
4000        let mut c = Chain::new();
4001        c.decision(&old);
4002        let rotation = c.compromise(&other_pk, &new, 1);
4003        c.rotate(&new, &rotation);
4004        let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
4005        let p = v.entries[1].problem.as_deref().unwrap();
4006        assert!(p.contains("old_key is not the key"), "{p}");
4007
4008        // A compromise whose certificate names no head at all.
4009        let mut c = Chain::new();
4010        c.decision(&old);
4011        let mut rotation = c.compromise(&old_pk, &new, 1);
4012        rotation.certificate.statement.last_trusted = None;
4013        c.rotate(&new, &rotation);
4014        let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
4015        let p = v.entries[1].problem.as_deref().unwrap();
4016        assert!(p.contains("must name last_trusted"), "{p}");
4017    }
4018
4019    #[test]
4020    fn a_rotation_carries_no_free_text() {
4021        let (old, old_pk) = generate_keypair();
4022        let (new, _) = generate_keypair();
4023        let mut c = Chain::new();
4024        c.decision(&old);
4025        let rotation = c.routine(&old_pk, &new);
4026        let mut value = serde_json::to_value(&rotation).unwrap();
4027        assert!(serde_json::from_value::<KeyRotation>(value.clone()).is_ok());
4028        value["note"] = json!("at the request of …");
4029        assert!(serde_json::from_value::<KeyRotation>(value).is_err());
4030
4031        let mut head = serde_json::to_value(c.head(1)).unwrap();
4032        head["comment"] = json!("the last one I remember signing");
4033        assert!(serde_json::from_value::<TrustedHead>(head).is_err());
4034
4035        let mut statement =
4036            serde_json::to_value(&rotation.certificate.statement).unwrap();
4037        statement["comment"] = json!("signed in the kitchen");
4038        assert!(serde_json::from_value::<KeyCertStatement>(statement).is_err());
4039    }
4040
4041    #[test]
4042    fn the_root_statement_bytes_are_pinned() {
4043        let key: PublicKeyHex = PUBLISHED_KEYS[0].parse().unwrap();
4044        assert_eq!(
4045            String::from_utf8(KeyCertStatement::genesis(key).signed_bytes())
4046                .unwrap(),
4047            "agora-governance-root-v1\n\
4048             {\"agora_governance_key_cert\":1,\"from_seq\":1,\
4049             \"key\":\"ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a\",\
4050             \"last_trusted\":null,\"prev_hash\":null,\"purpose\":\"genesis\"}"
4051        );
4052
4053        // The same statement `governance/root/root_sign.py --self-test`
4054        // pins in the agora repository: the tool that signs and the
4055        // verifiers that check must mean the same bytes.
4056        let statement = KeyCertStatement::compromise(
4057            Sha256Hex::from([0xab; 32]).to_string().parse().unwrap(),
4058            15,
4059            Some([0xcd; 32].into()),
4060            TrustedHead {
4061                id: gov(10),
4062                chain_seq: 11,
4063                entry_hash: [0xef; 32].into(),
4064            },
4065        );
4066        assert_eq!(
4067            Sha256Hex::from(<[u8; 32]>::from(Sha256::digest(
4068                statement.signed_bytes()
4069            )))
4070            .to_string(),
4071            "633685771e08be126fd12ca4eb98c77120e5868236ff541ecba85ce6a21e6b68"
4072        );
4073    }
4074
4075    #[test]
4076    fn root_keys_are_curve_points_and_make_a_root_set() {
4077        let roots = RootSet::published();
4078        assert_eq!(roots.keys().count(), ROOT_KEYS.len());
4079        assert_eq!(roots.threshold(), ROOT_THRESHOLD);
4080        assert!(ROOT_THRESHOLD >= 1 && ROOT_THRESHOLD <= ROOT_KEYS.len());
4081        for root in ROOT_KEYS {
4082            let key: PublicKeyHex = root.parse().unwrap();
4083            assert!(roots.contains(&key));
4084            assert!(
4085                key.to_verifying_key().is_ok(),
4086                "{root} is not a valid Ed25519 public key"
4087            );
4088            assert!(
4089                !PUBLISHED_KEYS.contains(root),
4090                "a root key never signs entries"
4091            );
4092        }
4093        assert_eq!(RootSet::new([], 0).threshold(), 1);
4094    }
4095
4096    #[test]
4097    fn a_genesis_key_outside_the_anchor_is_reported_not_rejected() {
4098        let (key, pk) = generate_keypair();
4099        let c = chain(&key, 2);
4100        let v = verify_chain(&c, &pk, &KeyAnchor::default(), &roots());
4101        assert!(v.ok, "{v:#?}");
4102        assert_eq!(v.unanchored_keys, vec![PublicKeyHex::from(&pk)]);
4103        assert_eq!(v.keys.len(), 1);
4104        assert_eq!(v.keys[0].status, KeyStatus::Active);
4105        assert_eq!(v.keys[0].from_seq, 1);
4106        assert_eq!(v.keys[0].through_seq, None);
4107    }
4108
4109    #[test]
4110    fn published_keys_are_curve_points_and_make_an_anchor() {
4111        let anchor = KeyAnchor::published();
4112        assert!(!anchor.is_empty());
4113        assert_eq!(anchor.keys().count(), PUBLISHED_KEYS.len());
4114        for published in PUBLISHED_KEYS {
4115            let key: PublicKeyHex = published.parse().unwrap();
4116            assert!(anchor.contains(&key));
4117            assert!(
4118                key.to_verifying_key().is_ok(),
4119                "{published} is not a valid Ed25519 public key"
4120            );
4121        }
4122        let (_, other) = generate_keypair();
4123        assert!(!anchor.contains(&(&other).into()));
4124        assert!(
4125            anchor
4126                .clone()
4127                .with((&other).into())
4128                .contains(&(&other).into())
4129        );
4130    }
4131
4132    // -- the wire --
4133
4134    #[test]
4135    fn amendments_and_rotations_round_trip_as_entry_data() {
4136        let (key, pk) = generate_keypair();
4137        let amendment = AmendmentDraft::new(
4138            gov(1),
4139            data_hash(&json!("x")),
4140            AmendmentKind::Superseded,
4141            "Art. VI § 2",
4142            "superseded by GOV-2026-0009",
4143        )
4144        .unwrap()
4145        .with_authority(gov(9))
4146        .with_rationale("the later decision covers the same subject");
4147        let AmendmentDraft { amendment, texts } = amendment;
4148        let value = serde_json::to_value(&amendment).unwrap();
4149        assert_eq!(value["kind"], "superseded");
4150        assert_eq!(value["agora_governance_amendment"], 2);
4151        assert!(value.get("redaction").is_none(), "{value}");
4152        let text = value.to_string();
4153        assert!(!text.contains("Art. VI"), "no text in signed data: {text}");
4154        assert!(!text.contains("salt"), "and no salt: {text}");
4155        assert_eq!(
4156            value["note"]["commitment"],
4157            texts
4158                .note
4159                .as_ref()
4160                .unwrap()
4161                .commitment()
4162                .commitment
4163                .to_string()
4164        );
4165        assert_eq!(
4166            serde_json::from_value::<Amendment>(value).unwrap(),
4167            amendment
4168        );
4169        let beside = serde_json::to_value(&texts).unwrap();
4170        assert_eq!(beside["basis"]["text"], "Art. VI § 2");
4171        assert_eq!(
4172            serde_json::from_value::<AmendmentTexts>(beside).unwrap(),
4173            texts
4174        );
4175
4176        // Version 1, as the platform's three are, still reads.
4177        let legacy = json!({
4178            "agora_governance_amendment": 1,
4179            "target": "GOV-2026-0001",
4180            "target_entry_hash": data_hash(&json!("x")),
4181            "kind": "non_precedential",
4182            "authority": "GOV-2026-0005",
4183            "basis": "§1",
4184            "note": "diagnostic finding",
4185        });
4186        let legacy: Amendment = serde_json::from_value(legacy).unwrap();
4187        assert_eq!(legacy.validate(), Ok(()));
4188        assert_eq!(legacy.basis, AmendmentText::Plain("§1".into()));
4189
4190        let mut c = Chain::new();
4191        c.decision(&key);
4192        let rotation = c.compromise(&pk, &generate_keypair().0, 1);
4193        let value = serde_json::to_value(&rotation).unwrap();
4194        assert_eq!(value["agora_governance_key_rotation"], 2);
4195        assert_eq!(value["reason"], "compromise");
4196        let statement = &value["certificate"]["statement"];
4197        assert_eq!(statement["purpose"], "compromise");
4198        assert_eq!(statement["last_trusted"]["chain_seq"], 1);
4199        assert_eq!(
4200            value["outgoing_certificate"]["statement"]["purpose"],
4201            "genesis"
4202        );
4203        assert!(value.get("note").is_none(), "{value}");
4204        assert_eq!(
4205            serde_json::from_value::<KeyRotation>(value).unwrap(),
4206            rotation
4207        );
4208
4209        let notice =
4210            AmendmentNotice::new(amd(1), at(0), &amendment, Some(&texts));
4211        let value = serde_json::to_value(&notice).unwrap();
4212        assert_eq!(value["id"], "AMD-2026-0001");
4213        assert_eq!(value["note"], "superseded by GOV-2026-0009");
4214        assert_eq!(
4215            serde_json::from_value::<AmendmentNotice>(value).unwrap(),
4216            notice
4217        );
4218
4219        // The rationale erased: the label stays, and nothing else moves.
4220        let erased = AmendmentTexts {
4221            rationale: None,
4222            ..texts.clone()
4223        };
4224        let notice =
4225            AmendmentNotice::new(amd(1), at(0), &amendment, Some(&erased));
4226        assert_eq!(notice.note, "superseded by GOV-2026-0009");
4227        assert_eq!(notice.rationale.as_deref(), Some(WITHHELD_TEXT));
4228        let notice = AmendmentNotice::new(amd(1), at(0), &amendment, None);
4229        assert_eq!(notice.basis, WITHHELD_TEXT);
4230        let notice = AmendmentNotice::new(amd(1), at(0), &legacy, None);
4231        assert_eq!(notice.note, "diagnostic finding");
4232    }
4233
4234    /// The verifier hashes the raw `data` value, so a field it has never
4235    /// heard of neither breaks it nor escapes the signature — and an
4236    /// amendment written without one verifies just the same.
4237    #[test]
4238    fn an_amendment_verifies_with_or_without_its_optional_fields() {
4239        let (key, pk) = generate_keypair();
4240        let mut c = Chain::new();
4241        let target = c.decision(&key);
4242        let bare = AmendmentDraft::new(
4243            target.clone(),
4244            c.hash_at(1),
4245            AmendmentKind::Correction,
4246            "clerical",
4247            "typo in the citation",
4248        )
4249        .unwrap();
4250        assert!(
4251            serde_json::to_value(&bare.amendment)
4252                .unwrap()
4253                .get("rationale")
4254                .is_none()
4255        );
4256        c.amend(&key, &bare);
4257        let full = bare.clone().with_rationale("at length: …");
4258        c.amend(&key, &full);
4259        // And a field from a future version of the shape.
4260        let mut future = serde_json::to_value(&full.amendment).unwrap();
4261        future["superseded_by_something_new"] = json!(["later"]);
4262        c.push(
4263            &key,
4264            amd(3),
4265            GovernanceLogEntryType::Amendment,
4266            future,
4267            true,
4268        );
4269
4270        let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4271        assert!(v.ok, "{v:#?}");
4272        assert_eq!(
4273            v.entries[0].amended_by,
4274            vec![amd(1), amd(2), amd(3)],
4275            "all three name the target"
4276        );
4277    }
4278
4279    /// Pre-0.26 JSON — no `data`, no `signed_by`, no repudiation — still
4280    /// parses, because every field added since is `#[serde(default)]`.
4281    #[test]
4282    fn pre_0_26_wire_still_deserializes() {
4283        let link: GovernanceChainLink = serde_json::from_value(json!({
4284            "id": "GOV-2026-0001",
4285            "entry_type": "council_decision",
4286            "created_at": "2023-11-14T22:13:20.123456Z",
4287            "attestation": {
4288                "envelope_version": 1,
4289                "chain_seq": 1,
4290                "prev_hash": null,
4291                "data_hash": "00".repeat(32),
4292                "entry_hash": "11".repeat(32),
4293                "signature": "22".repeat(64),
4294                "signed_at": "2023-11-14T22:13:21Z",
4295                "retroactive": false,
4296            },
4297        }))
4298        .unwrap();
4299        assert!(link.data.is_none());
4300        assert!(
4301            serde_json::to_value(&link).unwrap().get("data").is_none(),
4302            "and a link without data does not grow a null field"
4303        );
4304
4305        let verdict: EntryVerdict = serde_json::from_value(json!({
4306            "id": "GOV-2026-0001",
4307            "chain_seq": 1,
4308            "signature_valid": true,
4309            "link_valid": true,
4310            "content_matches": null,
4311            "retroactive": false,
4312            "out_of_order": false,
4313            "amended_by": [],
4314        }))
4315        .unwrap();
4316        assert!(!verdict.repudiated && !verdict.redacted);
4317        assert!(verdict.signed_by.is_none());
4318        assert!(verdict.reattested_by.is_empty());
4319
4320        let verification: GovernanceVerification =
4321            serde_json::from_value(json!({
4322                "public_key": "33".repeat(32),
4323                "ok": true,
4324                "head": "GOV-2026-0001",
4325                "entries": [],
4326            }))
4327            .unwrap();
4328        assert!(verification.keys.is_empty());
4329        assert!(verification.unanchored_keys.is_empty());
4330        assert!(verification.repudiated.is_empty());
4331    }
4332
4333    #[test]
4334    fn the_report_round_trips() {
4335        let (key, pk) = generate_keypair();
4336        let mut c = Chain::new();
4337        let target = c.decision(&key);
4338        let amendment = AmendmentDraft::new(
4339            target,
4340            c.hash_at(1),
4341            AmendmentKind::Overruled,
4342            "b",
4343            "n",
4344        )
4345        .unwrap();
4346        c.amend(&key, &amendment);
4347        let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4348        let text = serde_json::to_string(&v).unwrap();
4349        assert_eq!(
4350            serde_json::from_str::<GovernanceVerification>(&text).unwrap(),
4351            v
4352        );
4353
4354        let keys = GovernanceSigningKeys { keys: v.keys };
4355        let value = serde_json::to_value(&keys).unwrap();
4356        assert!(value["keys"].is_array(), "an object, not a bare array");
4357        assert_eq!(value["keys"][0]["status"], "active");
4358        assert_eq!(
4359            serde_json::from_value::<GovernanceSigningKeys>(value).unwrap(),
4360            keys
4361        );
4362    }
4363
4364    /// The envelope is version 1 and there is a live chain signed under it:
4365    /// these bytes and this hash are load-bearing, not a snapshot to
4366    /// re-bless when something changes them.
4367    #[test]
4368    fn envelope_v1_preimage_and_hash_are_pinned() {
4369        let envelope = Envelope::new(
4370            gov(6),
4371            GovernanceLogEntryType::CouncilDecision,
4372            at(0),
4373            Some(Sha256Hex::from([0x11; 32])),
4374            data_hash(&json!({"outcome": "approved", "title": "Ratification"})),
4375        );
4376        assert_eq!(
4377            String::from_utf8(envelope.preimage()).unwrap(),
4378            "{\"agora_governance_log\":1,\"id\":\"GOV-2026-0006\",\
4379             \"entry_type\":\"council_decision\",\
4380             \"created_at\":1700000000123456,\
4381             \"prev_hash\":\"1111111111111111111111111111111111111111111111111111111111111111\",\
4382             \"data_hash\":\"a4adf645ae3f60c56484d01aea87d6d490321d7fc66b1607df14b023fe567c7b\"}"
4383        );
4384        assert_eq!(
4385            envelope.entry_hash().to_hex(),
4386            "ba27577432f81e415f1c01cc4cfabab6070e3ac50fd468fffe195ef19c0e9464"
4387        );
4388    }
4389
4390    /// The parity check the Steward's second channel exists for: the live
4391    /// chain verifies under what this build has compiled in — the genesis
4392    /// key in [`PUBLISHED_KEYS`] and the roots in [`ROOT_KEYS`] — and the
4393    /// key the platform serves is the one that walk ends on.
4394    ///
4395    /// Before the first rotation that is the genesis key itself; after it,
4396    /// a key this crate has never heard of and does not need to, because
4397    /// the root certified it. A served key the roots did not certify, or a
4398    /// chain that no longer verifies, fails here within the hour.
4399    ///
4400    /// Networked, so it is `#[ignore]`d and CI runs it as its own job —
4401    /// a 5G blip should not read as a code failure. `just
4402    /// check-published-keys`.
4403    #[cfg(feature = "agora-client")]
4404    #[tokio::test]
4405    #[ignore = "networked: hits the live platform"]
4406    async fn the_published_key_is_the_one_the_platform_serves() {
4407        let client = crate::client::Client::new(
4408            url::Url::parse("https://subliminal.technology").unwrap(),
4409        )
4410        .unwrap();
4411        let served = client.get_governance_signing_key().await.unwrap();
4412        assert_eq!(served.algorithm, "ed25519");
4413
4414        let genesis: PublicKeyHex = PUBLISHED_KEYS
4415            .first()
4416            .expect("PUBLISHED_KEYS is never empty")
4417            .parse()
4418            .unwrap();
4419        let links = client.get_governance_chain().await.unwrap();
4420        let report = verify_chain(
4421            &links,
4422            &genesis.to_verifying_key().unwrap(),
4423            &KeyAnchor::published(),
4424            &RootSet::published(),
4425        );
4426        let problems: Vec<_> = report
4427            .entries
4428            .iter()
4429            .filter_map(|e| {
4430                e.problem.as_ref().map(|p| (e.id.clone(), p.clone()))
4431            })
4432            .collect();
4433        assert!(
4434            report.ok,
4435            "the live chain does not verify under this build's genesis key \
4436             and roots: {problems:#?}"
4437        );
4438        assert!(report.unanchored_keys.is_empty(), "{report:#?}");
4439        assert_eq!(
4440            served.public_key, report.public_key,
4441            "the platform serves {} but the chain, followed under the \
4442             published roots, is held by {}",
4443            served.public_key, report.public_key
4444        );
4445    }
4446
4447    #[cfg(feature = "schemars")]
4448    #[test]
4449    fn wire_schemas_are_ref_free() {
4450        use crate::responses::inline_schema_for;
4451        for (name, schema) in [
4452            (
4453                "GovernanceAttestation",
4454                inline_schema_for::<GovernanceAttestation>(),
4455            ),
4456            (
4457                "GovernanceChainLink",
4458                inline_schema_for::<GovernanceChainLink>(),
4459            ),
4460            (
4461                "GovernanceSigningKey",
4462                inline_schema_for::<GovernanceSigningKey>(),
4463            ),
4464            (
4465                "GovernanceVerification",
4466                inline_schema_for::<GovernanceVerification>(),
4467            ),
4468            (
4469                "Vec<GovernanceChainLink>",
4470                inline_schema_for::<Vec<GovernanceChainLink>>(),
4471            ),
4472            ("Amendment", inline_schema_for::<Amendment>()),
4473            ("Redaction", inline_schema_for::<Redaction>()),
4474            ("AmendmentNotice", inline_schema_for::<AmendmentNotice>()),
4475            ("KeyRotation", inline_schema_for::<KeyRotation>()),
4476            ("TrustedHead", inline_schema_for::<TrustedHead>()),
4477            (
4478                "GovernanceKeyRecord",
4479                inline_schema_for::<GovernanceKeyRecord>(),
4480            ),
4481            (
4482                "GovernanceSigningKeys",
4483                inline_schema_for::<GovernanceSigningKeys>(),
4484            ),
4485            ("AmendmentKind", inline_schema_for::<AmendmentKind>()),
4486            ("Standing", inline_schema_for::<Standing>()),
4487            ("KeyStatus", inline_schema_for::<KeyStatus>()),
4488            ("RotationReason", inline_schema_for::<RotationReason>()),
4489        ] {
4490            let text = serde_json::to_string(&schema).unwrap();
4491            assert!(!text.contains("$ref"), "{name} must be $ref-free: {text}");
4492            assert!(
4493                !text.contains("$defs"),
4494                "{name} must be $defs-free: {text}"
4495            );
4496        }
4497        let text =
4498            serde_json::to_string(&inline_schema_for::<Sha256Hex>()).unwrap();
4499        assert!(text.contains("^[0-9a-f]{64}$"), "{text}");
4500        let text = serde_json::to_string(&inline_schema_for::<SignatureHex>())
4501            .unwrap();
4502        assert!(text.contains("^[0-9a-f]{128}$"), "{text}");
4503    }
4504}