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