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