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