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`]). A
29//!   redaction replaces values in the target's `data` in place; the
30//!   original `entry_hash` stays on the row so later links still verify,
31//!   and the amendment's `resulting_data_hash` is what the redacted data
32//!   must now hash to. [`EntryVerdict::content_matches`] is the check.
33//! - [`KeyRotation`] (`KEY-`) moves the chain to a new signing key. A
34//!   routine rotation is signed by the old key; a compromise declaration
35//!   is signed by the new one and is authentic only if that key is in the
36//!   verifier's out-of-band [`KeyAnchor`], which is what makes a key thief
37//!   visible rather than authoritative. See [`verify_chain`].
38//!
39//! The envelope itself is unchanged by any of this: `ENVELOPE_VERSION` is
40//! still 1 and what it does and does not cover is exactly as above.
41
42use crate::crypto::{self, Signature, SigningKey, VerifyingKey};
43use crate::enums::GovernanceLogEntryType;
44use crate::ids::{GovernanceLogId, GovernanceLogPrefix};
45use chrono::{DateTime, Utc};
46use serde::{Deserialize, Serialize};
47use sha2::{Digest, Sha256};
48use std::collections::{HashMap, HashSet};
49
50pub use crate::enums::{AmendmentKind, KeyStatus, Standing};
51
52/// The shared test vectors in `vectors/govlog`; see [`vectors`]
53#[cfg(test)]
54mod vectors;
55
56/// The envelope version this module produces and verifies
57pub const ENVELOPE_VERSION: u32 = 1;
58
59/// An attestation signed more than this long after its entry was recorded
60/// is [retroactive](is_retroactive)
61pub const RETROACTIVE_AFTER: chrono::Duration = chrono::Duration::seconds(60);
62
63// ---------------------------------------------------------------------------
64// Fixed-size hex newtypes
65// ---------------------------------------------------------------------------
66
67/// A hex string of the wrong length or alphabet for the type it was parsed
68/// into
69#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
70#[error("{type_name}: expected {expected} bytes of hex, got {got:?}")]
71pub struct HexLengthError {
72    pub type_name: &'static str,
73    pub expected: usize,
74    pub got: String,
75}
76
77macro_rules! hex_bytes {
78    ($(#[$meta:meta])* $name:ident, $len:expr) => {
79        $(#[$meta])*
80        #[derive(Clone, Copy, PartialEq, Eq, Hash)]
81        #[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
82        #[cfg_attr(feature = "sqlx", sqlx(transparent))]
83        pub struct $name([u8; $len]);
84
85        impl $name {
86            /// The raw bytes
87            pub fn as_bytes(&self) -> &[u8; $len] {
88                &self.0
89            }
90
91            /// Lowercase hex, as on the wire
92            pub fn to_hex(&self) -> String {
93                hex::encode(self.0)
94            }
95        }
96
97        impl From<[u8; $len]> for $name {
98            fn from(bytes: [u8; $len]) -> Self {
99                Self(bytes)
100            }
101        }
102
103        impl TryFrom<&[u8]> for $name {
104            type Error = HexLengthError;
105
106            fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
107                <[u8; $len]>::try_from(bytes).map(Self).map_err(|_| {
108                    HexLengthError {
109                        type_name: stringify!($name),
110                        expected: $len,
111                        got: hex::encode(bytes),
112                    }
113                })
114            }
115        }
116
117        impl TryFrom<Vec<u8>> for $name {
118            type Error = HexLengthError;
119
120            fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
121                Self::try_from(bytes.as_slice())
122            }
123        }
124
125        impl std::str::FromStr for $name {
126            type Err = HexLengthError;
127
128            fn from_str(s: &str) -> Result<Self, Self::Err> {
129                let bytes = hex::decode(s.trim()).map_err(|_| HexLengthError {
130                    type_name: stringify!($name),
131                    expected: $len,
132                    got: s.to_string(),
133                })?;
134                Self::try_from(bytes.as_slice())
135            }
136        }
137
138        impl std::fmt::Display for $name {
139            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140                f.write_str(&self.to_hex())
141            }
142        }
143
144        impl std::fmt::Debug for $name {
145            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146                write!(f, "{}({})", stringify!($name), self.to_hex())
147            }
148        }
149
150        impl Serialize for $name {
151            fn serialize<S: serde::Serializer>(
152                &self,
153                s: S,
154            ) -> Result<S::Ok, S::Error> {
155                s.serialize_str(&self.to_hex())
156            }
157        }
158
159        impl<'de> Deserialize<'de> for $name {
160            fn deserialize<D: serde::Deserializer<'de>>(
161                d: D,
162            ) -> Result<Self, D::Error> {
163                let s = String::deserialize(d)?;
164                s.parse().map_err(serde::de::Error::custom)
165            }
166        }
167
168        // Hand-written for the same reason as every id newtype: a derived
169        // schema becomes a `$ref` into `$defs`, which the Claude.ai MCP
170        // connector mangles (see CLAUDE.md in the agora repo).
171        #[cfg(feature = "schemars")]
172        impl schemars::JsonSchema for $name {
173            fn inline_schema() -> bool {
174                true
175            }
176
177            fn schema_name() -> std::borrow::Cow<'static, str> {
178                std::borrow::Cow::Borrowed(stringify!($name))
179            }
180
181            fn schema_id() -> std::borrow::Cow<'static, str> {
182                std::borrow::Cow::Borrowed(concat!(
183                    module_path!(),
184                    "::",
185                    stringify!($name)
186                ))
187            }
188
189            fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
190                schemars::json_schema!({
191                    "type": "string",
192                    "pattern": format!("^[0-9a-f]{{{}}}$", $len * 2),
193                    "description": format!("{} bytes, lowercase hex", $len),
194                })
195            }
196        }
197    };
198}
199
200hex_bytes!(
201    /// A SHA-256 digest, hex on the wire
202    Sha256Hex,
203    32
204);
205
206hex_bytes!(
207    /// An Ed25519 signature, hex on the wire
208    SignatureHex,
209    64
210);
211
212hex_bytes!(
213    /// An Ed25519 public key, hex on the wire
214    PublicKeyHex,
215    32
216);
217
218impl From<Signature> for SignatureHex {
219    fn from(sig: Signature) -> Self {
220        Self(sig.to_bytes())
221    }
222}
223
224impl From<&SignatureHex> for Signature {
225    fn from(sig: &SignatureHex) -> Self {
226        Signature::from_bytes(&sig.0)
227    }
228}
229
230impl From<&VerifyingKey> for PublicKeyHex {
231    fn from(key: &VerifyingKey) -> Self {
232        Self(key.to_bytes())
233    }
234}
235
236impl PublicKeyHex {
237    /// The key, if the bytes are a valid curve point
238    pub fn to_verifying_key(
239        &self,
240    ) -> Result<VerifyingKey, ed25519_dalek::SignatureError> {
241        VerifyingKey::from_bytes(&self.0)
242    }
243}
244
245// ---------------------------------------------------------------------------
246// Canonical JSON and hashing
247// ---------------------------------------------------------------------------
248
249/// `value` as compact JSON with object keys sorted bytewise at every level.
250///
251/// `serde_json::to_vec` on a [`serde_json::Value`] is *not* canonical:
252/// with the `preserve_order` feature (on in every Agora workspace, off in
253/// this crate's own tests) objects serialize in insertion order, so the
254/// same value hashes differently depending on who built it. Strings and
255/// numbers use serde_json's own formatting, which is deterministic for a
256/// given value.
257pub fn canonical_json(value: &serde_json::Value) -> Vec<u8> {
258    let mut out = Vec::new();
259    write_canonical(value, &mut out);
260    out
261}
262
263fn write_canonical(value: &serde_json::Value, out: &mut Vec<u8>) {
264    use serde_json::Value;
265    match value {
266        Value::Null => out.extend_from_slice(b"null"),
267        Value::Bool(b) => {
268            out.extend_from_slice(if *b { b"true" } else { b"false" })
269        }
270        Value::Number(n) => serde_json::to_writer(&mut *out, n)
271            .expect("a number always serializes"),
272        Value::String(s) => serde_json::to_writer(&mut *out, s)
273            .expect("a string always serializes"),
274        Value::Array(items) => {
275            out.push(b'[');
276            for (i, item) in items.iter().enumerate() {
277                if i > 0 {
278                    out.push(b',');
279                }
280                write_canonical(item, out);
281            }
282            out.push(b']');
283        }
284        Value::Object(map) => {
285            let mut keys: Vec<&String> = map.keys().collect();
286            keys.sort_unstable();
287            out.push(b'{');
288            for (i, key) in keys.into_iter().enumerate() {
289                if i > 0 {
290                    out.push(b',');
291                }
292                serde_json::to_writer(&mut *out, key)
293                    .expect("a string always serializes");
294                out.push(b':');
295                write_canonical(&map[key], out);
296            }
297            out.push(b'}');
298        }
299    }
300}
301
302/// SHA-256 over [`canonical_json`]
303pub fn data_hash(data: &serde_json::Value) -> Sha256Hex {
304    Sha256Hex(Sha256::digest(canonical_json(data)).into())
305}
306
307/// The fields an entry's hash commits to
308///
309/// A struct rather than a [`serde_json::Value`] so the preimage serializes
310/// in declaration order whatever `preserve_order` says.
311#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
312pub struct Envelope {
313    /// Always [`ENVELOPE_VERSION`]
314    pub agora_governance_log: u32,
315    pub id: GovernanceLogId,
316    pub entry_type: GovernanceLogEntryType,
317    /// Unix microseconds — the precision Postgres stores
318    pub created_at: i64,
319    pub prev_hash: Option<Sha256Hex>,
320    pub data_hash: Sha256Hex,
321}
322
323impl Envelope {
324    /// The version-1 envelope for these fields
325    pub fn new(
326        id: GovernanceLogId,
327        entry_type: GovernanceLogEntryType,
328        created_at: DateTime<Utc>,
329        prev_hash: Option<Sha256Hex>,
330        data_hash: Sha256Hex,
331    ) -> Self {
332        Self {
333            agora_governance_log: ENVELOPE_VERSION,
334            id,
335            entry_type,
336            created_at: created_at.timestamp_micros(),
337            prev_hash,
338            data_hash,
339        }
340    }
341
342    /// `created_at` as a timestamp again
343    pub fn created_at(&self) -> DateTime<Utc> {
344        DateTime::from_timestamp_micros(self.created_at)
345            .expect("an Envelope only ever holds an in-range timestamp")
346    }
347
348    /// The bytes that are hashed
349    pub fn preimage(&self) -> Vec<u8> {
350        serde_json::to_vec(self).expect("an Envelope always serializes")
351    }
352
353    /// SHA-256 over [`preimage`](Self::preimage)
354    pub fn entry_hash(&self) -> Sha256Hex {
355        Sha256Hex(Sha256::digest(self.preimage()).into())
356    }
357}
358
359/// `t` with anything below a microsecond dropped, so the value hashed is the
360/// value Postgres will store
361pub fn truncate_to_micros(t: DateTime<Utc>) -> DateTime<Utc> {
362    DateTime::from_timestamp_micros(t.timestamp_micros())
363        .expect("a timestamp that came from a DateTime is in range")
364}
365
366/// `t` with anything below a second dropped, so `signed_at` round-trips to
367/// the integer the signature covers
368pub fn truncate_to_seconds(t: DateTime<Utc>) -> DateTime<Utc> {
369    DateTime::from_timestamp(t.timestamp(), 0)
370        .expect("a timestamp that came from a DateTime is in range")
371}
372
373/// `true` when the attestation was signed more than [`RETROACTIVE_AFTER`]
374/// after the entry was recorded — history signed after the fact, which
375/// proves the key holder vouches for it now, not that it was signed then
376pub fn is_retroactive(
377    created_at: DateTime<Utc>,
378    signed_at: DateTime<Utc>,
379) -> bool {
380    signed_at - created_at > RETROACTIVE_AFTER
381}
382
383// ---------------------------------------------------------------------------
384// Wire types
385// ---------------------------------------------------------------------------
386
387/// What the server attests about one governance log entry
388#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
389#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
390#[cfg_attr(feature = "schemars", schemars(inline))]
391pub struct GovernanceAttestation {
392    /// Envelope version; see the module docs for what `1` commits to
393    pub envelope_version: u32,
394    /// Position in the chain, from 1. An index, not part of the envelope:
395    /// the `prev_hash` links are what prove order.
396    pub chain_seq: u64,
397    /// `entry_hash` of the previous entry; `null` only for the first
398    pub prev_hash: Option<Sha256Hex>,
399    /// SHA-256 of the entry's canonical `data`
400    pub data_hash: Sha256Hex,
401    /// SHA-256 of the envelope; what the signature covers
402    pub entry_hash: Sha256Hex,
403    /// Ed25519 over `entry_hash` and `signed_at`, by the platform's
404    /// governance signing key
405    pub signature: SignatureHex,
406    /// When the signature was made. Distinct from `created_at`: see
407    /// `retroactive`.
408    pub signed_at: DateTime<Utc>,
409    /// `true` when signed well after the entry was recorded — the entries
410    /// that predate signing were attested this way, which proves the
411    /// Steward vouches for them, not that they were signed at the time
412    pub retroactive: bool,
413}
414
415/// One link of the chain as `GET /api/governance/log/chain` returns it —
416/// everything needed to verify linkage and signatures, plus `data` for the
417/// entries a verifier has to read
418#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
419#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
420#[cfg_attr(feature = "schemars", schemars(inline))]
421pub struct GovernanceChainLink {
422    pub id: GovernanceLogId,
423    pub entry_type: GovernanceLogEntryType,
424    pub created_at: DateTime<Utc>,
425    pub attestation: GovernanceAttestation,
426    /// Present for `amendment` and `key_rotation` entries, whose content
427    /// is what the chain means and is small by construction. A Council
428    /// transcript is neither, and is read one entry at a time instead.
429    /// [`verify_chain`] hashes this raw value against `data_hash` before
430    /// reading it, so unknown future fields neither break an older
431    /// verifier nor escape the signature.
432    #[serde(default, skip_serializing_if = "Option::is_none")]
433    pub data: Option<serde_json::Value>,
434}
435
436/// The platform's governance signing key, as `GET
437/// /api/governance/signing-key` publishes it
438#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
439#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
440#[cfg_attr(feature = "schemars", schemars(inline))]
441pub struct GovernanceSigningKey {
442    /// Always `"ed25519"`
443    pub algorithm: String,
444    pub public_key: PublicKeyHex,
445    /// The envelope version entries are currently signed under
446    pub envelope_version: u32,
447}
448
449impl GovernanceSigningKey {
450    /// The published form of `key`
451    pub fn new(key: &VerifyingKey) -> Self {
452        Self {
453            algorithm: "ed25519".to_string(),
454            public_key: key.into(),
455            envelope_version: ENVELOPE_VERSION,
456        }
457    }
458}
459
460// ---------------------------------------------------------------------------
461// Amendments
462// ---------------------------------------------------------------------------
463
464/// The [`Amendment`] payload version this module produces and verifies
465pub const AMENDMENT_VERSION: u32 = 1;
466
467/// An amendment is malformed
468#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
469pub enum AmendmentError {
470    #[error("agora_governance_amendment is {0}, not {AMENDMENT_VERSION}")]
471    UnsupportedVersion(u32),
472    #[error("kind `redaction` requires a `redaction`")]
473    MissingRedaction,
474    #[error("`redaction` is only valid on kind `redaction`")]
475    UnexpectedRedaction,
476    #[error("amendment target {0} is not an entry of this chain")]
477    UnknownTarget(GovernanceLogId),
478    #[error("amendment target {0} is not an earlier entry")]
479    ForwardReference(GovernanceLogId),
480    #[error("target_entry_hash is not {0}'s entry_hash")]
481    WrongTargetHash(GovernanceLogId),
482}
483
484/// The `data` of an `amendment` entry: what an earlier entry now means.
485///
486/// The target is never edited — except its `data` under a [`Redaction`] —
487/// so the amendment is the whole record of the change, and both are
488/// signed links of the same chain.
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 Amendment {
493    /// Always [`AMENDMENT_VERSION`]
494    pub agora_governance_amendment: u32,
495    pub target: GovernanceLogId,
496    /// The target's `entry_hash` — binds this to one exact entry
497    pub target_entry_hash: Sha256Hex,
498    pub kind: AmendmentKind,
499    /// The governance entry that authorizes this, when one does
500    /// (e.g. `GOV-2026-0005`). `None` for a lawful-deletion redaction.
501    #[serde(default)]
502    pub authority: Option<GovernanceLogId>,
503    /// Section or legal basis, human-readable: `"§1 (Red Team Cases
504    /// Recharacterized)"`, `"GDPR Art. 17(1)(a)"`. Never personal data.
505    pub basis: String,
506    /// The label readers and prompts show next to the target
507    pub note: String,
508    /// Why, at length — `note` is the label, this is the reasoning, and it
509    /// is part of the signed record. Never personal data.
510    #[serde(default, skip_serializing_if = "Option::is_none")]
511    pub rationale: Option<String>,
512    /// Present iff `kind` is [`AmendmentKind::Redaction`]
513    #[serde(default, skip_serializing_if = "Option::is_none")]
514    pub redaction: Option<Redaction>,
515}
516
517/// What a [`AmendmentKind::Redaction`] removed, and what is left
518#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
519#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
520#[cfg_attr(feature = "schemars", schemars(inline))]
521pub struct Redaction {
522    /// RFC 6901 JSON pointers into the target's `data` whose values were
523    /// replaced. Paths only: never the removed content, never the subject.
524    pub fields: Vec<String>,
525    /// What the target's `data` hashes to after redaction, so the redacted
526    /// content is itself verifiable and cannot be altered again silently
527    pub resulting_data_hash: Sha256Hex,
528}
529
530impl Amendment {
531    /// An amendment of `kind` against `target`.
532    ///
533    /// Redactions go through [`redaction`](Self::redaction) instead, which
534    /// is the only way to get a [`Redaction`] whose `resulting_data_hash`
535    /// is the hash of data that actually exists.
536    pub fn new(
537        target: GovernanceLogId,
538        target_entry_hash: Sha256Hex,
539        kind: AmendmentKind,
540        basis: impl Into<String>,
541        note: impl Into<String>,
542    ) -> Result<Self, AmendmentError> {
543        if kind == AmendmentKind::Redaction {
544            return Err(AmendmentError::MissingRedaction);
545        }
546        Ok(Self {
547            agora_governance_amendment: AMENDMENT_VERSION,
548            target,
549            target_entry_hash,
550            kind,
551            authority: None,
552            basis: basis.into(),
553            note: note.into(),
554            rationale: None,
555            redaction: None,
556        })
557    }
558
559    /// The amendment with the governance entry that authorizes it
560    pub fn with_authority(mut self, authority: GovernanceLogId) -> Self {
561        self.authority = Some(authority);
562        self
563    }
564
565    /// The amendment with its [`rationale`](Self::rationale)
566    pub fn with_rationale(mut self, rationale: impl Into<String>) -> Self {
567        self.rationale = Some(rationale.into());
568        self
569    }
570
571    /// A redaction of `fields` from the target's `data`, with the redacted
572    /// data it commits to.
573    ///
574    /// `amendment_id` is the id this amendment will be appended under: the
575    /// marker left behind names it, so the redaction says who ordered it.
576    /// Append both together or neither — the returned `data` is what the
577    /// target's row must hold for [`verify_chain`] to accept it.
578    pub fn redaction(
579        amendment_id: &GovernanceLogId,
580        target: GovernanceLogId,
581        target_entry_hash: Sha256Hex,
582        basis: impl Into<String>,
583        note: impl Into<String>,
584        fields: Vec<String>,
585        data: &serde_json::Value,
586    ) -> Result<(Self, serde_json::Value), RedactError> {
587        let redacted = redact_data(data, &fields, amendment_id)?;
588        Ok((
589            Self {
590                agora_governance_amendment: AMENDMENT_VERSION,
591                target,
592                target_entry_hash,
593                kind: AmendmentKind::Redaction,
594                authority: None,
595                basis: basis.into(),
596                note: note.into(),
597                rationale: None,
598                redaction: Some(Redaction {
599                    fields,
600                    resulting_data_hash: data_hash(&redacted),
601                }),
602            },
603            redacted,
604        ))
605    }
606
607    /// Version and the redaction-shape invariant — everything checkable
608    /// without the rest of the chain
609    pub fn validate(&self) -> Result<(), AmendmentError> {
610        if self.agora_governance_amendment != AMENDMENT_VERSION {
611            return Err(AmendmentError::UnsupportedVersion(
612                self.agora_governance_amendment,
613            ));
614        }
615        match (self.kind, &self.redaction) {
616            (AmendmentKind::Redaction, None) => {
617                Err(AmendmentError::MissingRedaction)
618            }
619            (k, Some(_)) if k != AmendmentKind::Redaction => {
620                Err(AmendmentError::UnexpectedRedaction)
621            }
622            _ => Ok(()),
623        }
624    }
625}
626
627/// What an amendment does to the [`Standing`] of the entry it names, when
628/// it changes it at all
629pub fn kind_standing(kind: AmendmentKind) -> Option<Standing> {
630    match kind {
631        AmendmentKind::NonPrecedential => Some(Standing::NonPrecedential),
632        AmendmentKind::Overruled => Some(Standing::Overruled),
633        AmendmentKind::Superseded => Some(Standing::Superseded),
634        AmendmentKind::Reinstated => Some(Standing::InForce),
635        AmendmentKind::Correction
636        | AmendmentKind::Redaction
637        | AmendmentKind::Reattested => None,
638    }
639}
640
641/// The [`Standing`] conferred by `kinds` — the amendments naming one entry,
642/// in chain order. The last one that changes standing wins.
643pub fn standing(kinds: impl IntoIterator<Item = AmendmentKind>) -> Standing {
644    kinds
645        .into_iter()
646        .filter_map(kind_standing)
647        .last()
648        .unwrap_or_default()
649}
650
651/// A JSON pointer in a [`Redaction`] does not resolve
652#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
653pub enum RedactError {
654    #[error("pointer {0:?} does not resolve in the entry's data")]
655    Unresolved(String),
656    #[error("the empty pointer would redact the whole entry")]
657    WholeEntry,
658}
659
660/// The marker a redaction leaves in place of a value
661pub fn redaction_marker(amendment_id: &GovernanceLogId) -> String {
662    format!("[redacted by {amendment_id}]")
663}
664
665/// `data` with the value at each RFC 6901 pointer in `fields` replaced by
666/// [`redaction_marker`].
667///
668/// Whole-value replacement only: a redaction tool that can write arbitrary
669/// replacement prose is a rewrite tool. The server and every verifier share
670/// this one definition, because what it returns is what the target's
671/// `resulting_data_hash` covers.
672pub fn redact_data(
673    data: &serde_json::Value,
674    fields: &[String],
675    amendment_id: &GovernanceLogId,
676) -> Result<serde_json::Value, RedactError> {
677    let marker = serde_json::Value::String(redaction_marker(amendment_id));
678    let mut out = data.clone();
679    for pointer in fields {
680        if pointer.is_empty() {
681            return Err(RedactError::WholeEntry);
682        }
683        let slot = out
684            .pointer_mut(pointer)
685            .ok_or_else(|| RedactError::Unresolved(pointer.clone()))?;
686        *slot = marker.clone();
687    }
688    Ok(out)
689}
690
691/// What a reader needs next to an amended entry
692#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
693#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
694#[cfg_attr(feature = "schemars", schemars(inline))]
695pub struct AmendmentNotice {
696    pub id: GovernanceLogId,
697    pub kind: AmendmentKind,
698    #[serde(default)]
699    pub authority: Option<GovernanceLogId>,
700    pub basis: String,
701    pub note: String,
702    /// See [`Amendment::rationale`]
703    #[serde(default, skip_serializing_if = "Option::is_none")]
704    pub rationale: Option<String>,
705    pub created_at: DateTime<Utc>,
706}
707
708impl AmendmentNotice {
709    /// The notice for `amendment`, appended as `id` at `created_at`
710    pub fn new(
711        id: GovernanceLogId,
712        created_at: DateTime<Utc>,
713        amendment: &Amendment,
714    ) -> Self {
715        Self {
716            id,
717            kind: amendment.kind,
718            authority: amendment.authority.clone(),
719            basis: amendment.basis.clone(),
720            note: amendment.note.clone(),
721            rationale: amendment.rationale.clone(),
722            created_at,
723        }
724    }
725}
726
727// ---------------------------------------------------------------------------
728// Key rotation
729// ---------------------------------------------------------------------------
730
731/// The [`KeyRotation`] payload version this module produces and verifies
732pub const KEY_ROTATION_VERSION: u32 = 1;
733
734/// The governance signing keys this build of agentkit trusts, oldest first.
735///
736/// This is the second channel: the crate is published from credentials the
737/// server does not hold, so a key that is served but not here is either a
738/// thief or an out-of-date agentkit, and both are worth saying out loud.
739/// Rotating means **add the new key here and release first, then append the
740/// rotation entry** — never the other way round, or every up-to-date client
741/// sees the chain move to a key it cannot anchor. CI checks the last
742/// element against what the platform serves; see `just check-published-keys`.
743pub const PUBLISHED_KEYS: &[&str] =
744    &["ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a"];
745
746/// Why the key changed
747#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
748#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
749#[cfg_attr(feature = "schemars", schemars(inline))]
750#[serde(rename_all = "snake_case")]
751pub enum RotationReason {
752    // Scheduled or voluntary; the old key signed the rotation itself.
753    Routine,
754    // The old key is in someone else's hands; the new key signed the
755    // rotation and only the trust anchor can authenticate it.
756    Compromise,
757}
758
759/// The last entry the compromised key is trusted for
760#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
761#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
762#[cfg_attr(feature = "schemars", schemars(inline))]
763pub struct TrustedHead {
764    pub id: GovernanceLogId,
765    pub chain_seq: u64,
766    pub entry_hash: Sha256Hex,
767}
768
769/// A rotation is malformed, unauthenticated, or inconsistent with the chain
770#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
771pub enum RotationError {
772    #[error("agora_governance_key_rotation is {0}, not {KEY_ROTATION_VERSION}")]
773    UnsupportedVersion(u32),
774    #[error("new_key is not a valid Ed25519 public key")]
775    BadNewKey,
776    #[error(
777        "the proof of possession does not verify for this rotation at this position"
778    )]
779    BadProof,
780    #[error("a compromise rotation must name last_trusted")]
781    MissingLastTrusted,
782    #[error("a routine rotation must not name last_trusted")]
783    UnexpectedLastTrusted,
784    #[error("old_key is not the key that was in force")]
785    WrongOldKey,
786    #[error("last_trusted does not name an earlier entry of this chain")]
787    UnknownLastTrusted,
788    #[error(
789        "a compromise rotation to a key outside the trust anchor authenticates nothing"
790    )]
791    UnanchoredNewKey,
792    #[error("new_key has already held this chain; a key is never brought back")]
793    ReusedKey,
794    #[error("last_trusted names an entry an earlier compromise repudiated")]
795    RepudiatedLastTrusted,
796}
797
798/// The `data` of a `key_rotation` entry.
799///
800/// Build one with [`routine`](Self::routine) or
801/// [`compromise`](Self::compromise): both compute the proof of possession,
802/// which is the only thing standing between "the Steward moved the chain to
803/// a new key" and "someone published a key they do not hold".
804#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
805#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
806#[cfg_attr(feature = "schemars", schemars(inline))]
807pub struct KeyRotation {
808    /// Always [`KEY_ROTATION_VERSION`]
809    pub agora_governance_key_rotation: u32,
810    pub reason: RotationReason,
811    pub old_key: PublicKeyHex,
812    pub new_key: PublicKeyHex,
813    /// `crypto::sign(new_key, ` [`RotationStatement::hash`] `,
814    /// proof_signed_at)` — the new key signing for itself, at one position
815    /// in one chain
816    pub proof: SignatureHex,
817    /// Unix seconds; what the proof signature covers
818    pub proof_signed_at: i64,
819    /// Compromise only: the last entry trusted under `old_key`
820    #[serde(default, skip_serializing_if = "Option::is_none")]
821    pub last_trusted: Option<TrustedHead>,
822    pub note: String,
823}
824
825/// What the proof of possession signs
826///
827/// A struct rather than a [`serde_json::Value`] for the same reason as
828/// [`Envelope`]: declaration-ordered serialization whatever `preserve_order`
829/// says. `prev_hash` is the rotation entry's own, so a proof lifted out of
830/// one chain position does not verify in another.
831#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
832pub struct RotationStatement {
833    /// Always [`KEY_ROTATION_VERSION`]
834    pub agora_governance_key_rotation: u32,
835    pub reason: RotationReason,
836    pub old_key: PublicKeyHex,
837    pub new_key: PublicKeyHex,
838    pub prev_hash: Option<Sha256Hex>,
839}
840
841impl RotationStatement {
842    /// The statement for a rotation at the position named by `prev_hash`
843    pub fn new(
844        reason: RotationReason,
845        old_key: PublicKeyHex,
846        new_key: PublicKeyHex,
847        prev_hash: Option<Sha256Hex>,
848    ) -> Self {
849        Self {
850            agora_governance_key_rotation: KEY_ROTATION_VERSION,
851            reason,
852            old_key,
853            new_key,
854            prev_hash,
855        }
856    }
857
858    /// The bytes that are hashed
859    pub fn preimage(&self) -> Vec<u8> {
860        serde_json::to_vec(self).expect("a RotationStatement always serializes")
861    }
862
863    /// SHA-256 over [`preimage`](Self::preimage) — what the proof covers
864    pub fn hash(&self) -> Sha256Hex {
865        Sha256Hex(Sha256::digest(self.preimage()).into())
866    }
867}
868
869impl KeyRotation {
870    /// A scheduled rotation from `old_key` to `new_signing_key`.
871    ///
872    /// The entry itself is signed by the **old** key; entries after it
873    /// verify under the new one. `prev_hash` is the rotation entry's own.
874    pub fn routine(
875        old_key: PublicKeyHex,
876        new_signing_key: &SigningKey,
877        prev_hash: Option<Sha256Hex>,
878        now: DateTime<Utc>,
879        note: impl Into<String>,
880    ) -> Self {
881        Self::build(
882            RotationReason::Routine,
883            old_key,
884            new_signing_key,
885            None,
886            prev_hash,
887            now,
888            note,
889        )
890    }
891
892    /// A declaration that `old_key` is compromised, trusted only through
893    /// `last_trusted`.
894    ///
895    /// The entry is signed by the **new** key — the old one proves nothing
896    /// any more — so a verifier accepts it only from its [`KeyAnchor`].
897    /// `last_trusted` must name an entry from before any earlier
898    /// compromise window; a reattestation inside one restores the entry,
899    /// not the ability to anchor trust there.
900    pub fn compromise(
901        old_key: PublicKeyHex,
902        new_signing_key: &SigningKey,
903        last_trusted: TrustedHead,
904        prev_hash: Option<Sha256Hex>,
905        now: DateTime<Utc>,
906        note: impl Into<String>,
907    ) -> Self {
908        Self::build(
909            RotationReason::Compromise,
910            old_key,
911            new_signing_key,
912            Some(last_trusted),
913            prev_hash,
914            now,
915            note,
916        )
917    }
918
919    #[allow(clippy::too_many_arguments)]
920    fn build(
921        reason: RotationReason,
922        old_key: PublicKeyHex,
923        new_signing_key: &SigningKey,
924        last_trusted: Option<TrustedHead>,
925        prev_hash: Option<Sha256Hex>,
926        now: DateTime<Utc>,
927        note: impl Into<String>,
928    ) -> Self {
929        let new_key = PublicKeyHex::from(&new_signing_key.verifying_key());
930        let proof_signed_at = truncate_to_seconds(now).timestamp();
931        let statement =
932            RotationStatement::new(reason, old_key, new_key, prev_hash);
933        let proof = crypto::sign(
934            new_signing_key,
935            statement.hash().as_bytes(),
936            proof_signed_at,
937        );
938        Self {
939            agora_governance_key_rotation: KEY_ROTATION_VERSION,
940            reason,
941            old_key,
942            new_key,
943            proof: proof.into(),
944            proof_signed_at,
945            last_trusted,
946            note: note.into(),
947        }
948    }
949
950    /// The statement this rotation's proof covers, at `prev_hash`
951    pub fn statement(&self, prev_hash: Option<Sha256Hex>) -> RotationStatement {
952        RotationStatement::new(
953            self.reason,
954            self.old_key,
955            self.new_key,
956            prev_hash,
957        )
958    }
959
960    /// Version, `last_trusted` shape, and the proof of possession at the
961    /// position `prev_hash` names — everything checkable without the rest
962    /// of the chain
963    pub fn verify_proof(
964        &self,
965        prev_hash: Option<Sha256Hex>,
966    ) -> Result<(), RotationError> {
967        if self.agora_governance_key_rotation != KEY_ROTATION_VERSION {
968            return Err(RotationError::UnsupportedVersion(
969                self.agora_governance_key_rotation,
970            ));
971        }
972        match (self.reason, &self.last_trusted) {
973            (RotationReason::Compromise, None) => {
974                return Err(RotationError::MissingLastTrusted);
975            }
976            (RotationReason::Routine, Some(_)) => {
977                return Err(RotationError::UnexpectedLastTrusted);
978            }
979            _ => {}
980        }
981        let new_key = self
982            .new_key
983            .to_verifying_key()
984            .map_err(|_| RotationError::BadNewKey)?;
985        crypto::verify(
986            &new_key,
987            self.statement(prev_hash).hash().as_bytes(),
988            self.proof_signed_at,
989            &Signature::from(&self.proof),
990        )
991        .then_some(())
992        .ok_or(RotationError::BadProof)
993    }
994}
995
996/// The keys a verifier trusts out of band — the half of the trust model
997/// the chain cannot supply, because a chain signed end to end by a thief
998/// is internally perfect.
999///
1000/// [`published`](Self::published) is this build's [`PUBLISHED_KEYS`];
1001/// [`pinned`](Self::pinned) is the key a client saw first and kept. Both
1002/// together is the recommendation: `KeyAnchor::published().with(pinned)`.
1003#[derive(Debug, Clone, Default, PartialEq, Eq)]
1004pub struct KeyAnchor {
1005    keys: HashSet<PublicKeyHex>,
1006}
1007
1008impl KeyAnchor {
1009    /// The keys compiled into this build of agentkit
1010    pub fn published() -> Self {
1011        PUBLISHED_KEYS
1012            .iter()
1013            .map(|k| {
1014                k.parse()
1015                    .expect("PUBLISHED_KEYS are valid 32-byte hex keys")
1016            })
1017            .collect()
1018    }
1019
1020    /// Just the one key, as a client that pinned what it saw first trusts it
1021    pub fn pinned(key: PublicKeyHex) -> Self {
1022        std::iter::once(key).collect()
1023    }
1024
1025    /// This anchor, plus `key`
1026    pub fn with(mut self, key: PublicKeyHex) -> Self {
1027        self.keys.insert(key);
1028        self
1029    }
1030
1031    pub fn contains(&self, key: &PublicKeyHex) -> bool {
1032        self.keys.contains(key)
1033    }
1034
1035    pub fn is_empty(&self) -> bool {
1036        self.keys.is_empty()
1037    }
1038
1039    /// The anchored keys, in no particular order
1040    pub fn keys(&self) -> impl Iterator<Item = &PublicKeyHex> {
1041        self.keys.iter()
1042    }
1043}
1044
1045impl FromIterator<PublicKeyHex> for KeyAnchor {
1046    fn from_iter<I: IntoIterator<Item = PublicKeyHex>>(iter: I) -> Self {
1047        Self {
1048            keys: iter.into_iter().collect(),
1049        }
1050    }
1051}
1052
1053/// One key's span of the chain, as [`verify_chain`] derives it and
1054/// `GET /api/governance/signing-keys` publishes it
1055#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1056#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1057#[cfg_attr(feature = "schemars", schemars(inline))]
1058pub struct GovernanceKeyRecord {
1059    pub public_key: PublicKeyHex,
1060    /// The first `chain_seq` this key signed
1061    pub from_seq: u64,
1062    /// The last `chain_seq` this key is trusted for; `null` while active.
1063    /// For a compromised key this is `last_trusted`, not the seq at which
1064    /// the compromise was declared.
1065    #[serde(default)]
1066    pub through_seq: Option<u64>,
1067    pub status: KeyStatus,
1068    /// The rotation entry that introduced this key; `null` for the genesis
1069    /// key, which predates the chain
1070    #[serde(default)]
1071    pub introduced_by: Option<GovernanceLogId>,
1072    /// The rotation entry that ended this key's span
1073    #[serde(default)]
1074    pub retired_by: Option<GovernanceLogId>,
1075}
1076
1077/// The signing key history as `GET /api/governance/signing-keys` returns it
1078///
1079/// An object rather than a bare array: MCP structured content needs a
1080/// top-level object.
1081#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1082#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1083#[cfg_attr(feature = "schemars", schemars(inline))]
1084pub struct GovernanceSigningKeys {
1085    /// Oldest first
1086    pub keys: Vec<GovernanceKeyRecord>,
1087}
1088
1089/// The verdict on one entry
1090#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1091#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1092#[cfg_attr(feature = "schemars", schemars(inline))]
1093pub struct EntryVerdict {
1094    pub id: GovernanceLogId,
1095    pub chain_seq: u64,
1096    /// The signature verifies over `entry_hash` and `signed_at` under the
1097    /// published key
1098    pub signature_valid: bool,
1099    /// `entry_hash` recomputes from the envelope fields, `prev_hash` is the
1100    /// previous entry's `entry_hash`, and `chain_seq` is contiguous
1101    pub link_valid: bool,
1102    /// The entry's current `data` hashes to the attested `data_hash` — or,
1103    /// when a redaction names the entry, to the redaction's
1104    /// `resulting_data_hash`. `null` when the verifier did not read `data`.
1105    /// `false` with a clean chain means the content was changed after
1106    /// attestation and no amendment says so.
1107    #[serde(default)]
1108    pub content_matches: Option<bool>,
1109    /// See [`GovernanceAttestation::retroactive`]
1110    pub retroactive: bool,
1111    /// `created_at` is earlier than the previous link's. Informational:
1112    /// chain order is what is attested, and a clock step does not break it.
1113    pub out_of_order: bool,
1114    /// Amendment entries that name this one
1115    #[serde(default)]
1116    pub amended_by: Vec<GovernanceLogId>,
1117    /// The key the signature was checked under — the one in force at this
1118    /// position, or for a compromise declaration the anchored new key
1119    #[serde(default, skip_serializing_if = "Option::is_none")]
1120    pub signed_by: Option<PublicKeyHex>,
1121    /// A redaction amendment names this entry, so its `data` has lawfully
1122    /// changed since it was attested
1123    #[serde(default)]
1124    pub redacted: bool,
1125    /// What the entry's `data` must hash to now, when it has been redacted
1126    #[serde(default, skip_serializing_if = "Option::is_none")]
1127    pub redacted_data_hash: Option<Sha256Hex>,
1128    /// Signed inside a compromise window and not reattested: the key
1129    /// holder of record disclaims it
1130    #[serde(default)]
1131    pub repudiated: bool,
1132    /// [`AmendmentKind::Reattested`] amendments vouching for this entry
1133    /// under a later, trusted key
1134    #[serde(default)]
1135    pub reattested_by: Vec<GovernanceLogId>,
1136    /// What failed, when something did
1137    #[serde(default, skip_serializing_if = "Option::is_none")]
1138    pub problem: Option<String>,
1139}
1140
1141/// A verification of the whole chain
1142#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1143#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1144#[cfg_attr(feature = "schemars", schemars(inline))]
1145pub struct GovernanceVerification {
1146    /// The key in force for the next entry — the active end of `keys`
1147    pub public_key: PublicKeyHex,
1148    /// Every entry's signature and link verified under the key in force,
1149    /// no entry's content is known to differ from what was attested, and
1150    /// every amendment and rotation is well-formed. Repudiated entries do
1151    /// not clear this by themselves: repudiation is a declared state, not
1152    /// a defect, and `repudiated` is where to look for it.
1153    pub ok: bool,
1154    /// The last entry in the chain
1155    #[serde(default)]
1156    pub head: Option<GovernanceLogId>,
1157    /// In chain order
1158    pub entries: Vec<EntryVerdict>,
1159    /// The signing key history the chain itself declares, oldest first
1160    #[serde(default)]
1161    pub keys: Vec<GovernanceKeyRecord>,
1162    /// Keys the chain moved to that this verifier's [`KeyAnchor`] does not
1163    /// vouch for. Not a failure — an out-of-date agentkit looks exactly
1164    /// like this — but it is also what a key thief looks like, so a
1165    /// reference client says so loudly.
1166    #[serde(default)]
1167    pub unanchored_keys: Vec<PublicKeyHex>,
1168    /// Entries inside a compromise window that no reattestation restored
1169    #[serde(default)]
1170    pub repudiated: Vec<GovernanceLogId>,
1171}
1172
1173impl GovernanceVerification {
1174    /// Recompute `ok` from the entries
1175    pub fn settle(mut self) -> Self {
1176        self.ok = self.entries.iter().all(|e| {
1177            e.signature_valid
1178                && e.link_valid
1179                && e.content_matches != Some(false)
1180                && e.problem.is_none()
1181        });
1182        self
1183    }
1184
1185    /// Record whether `data` is the content `link` attested — or what a
1186    /// redaction of it left behind.
1187    ///
1188    /// The chain endpoint carries `data` only for amendments and
1189    /// rotations, so this is how a caller that read an entry in full folds
1190    /// that read into the report. `false` (and a `false`
1191    /// [`content_matches`](EntryVerdict::content_matches), which clears
1192    /// [`ok`](Self::ok) on the next [`settle`](Self::settle)) when the
1193    /// entry is not in this report at all.
1194    pub fn check_content(
1195        &mut self,
1196        link: &GovernanceChainLink,
1197        data: &serde_json::Value,
1198    ) -> bool {
1199        let hash = data_hash(data);
1200        let Some(entry) = self.entries.iter_mut().find(|e| e.id == link.id)
1201        else {
1202            return false;
1203        };
1204        let ok = hash == link.attestation.data_hash
1205            || entry.redacted_data_hash == Some(hash);
1206        entry.content_matches = Some(ok);
1207        ok
1208    }
1209}
1210
1211// ---------------------------------------------------------------------------
1212// Signing
1213// ---------------------------------------------------------------------------
1214
1215/// Attest an entry: the one construction path for
1216/// [`GovernanceAttestation`], used by the server at insert and by tests
1217/// building fixtures.
1218///
1219/// `signed_at` is truncated to whole seconds, the precision the signature
1220/// covers; store the value the attestation carries, not the one passed in.
1221pub fn attest(
1222    key: &SigningKey,
1223    envelope: &Envelope,
1224    chain_seq: u64,
1225    signed_at: DateTime<Utc>,
1226) -> GovernanceAttestation {
1227    let signed_at = truncate_to_seconds(signed_at);
1228    let entry_hash = envelope.entry_hash();
1229    let signature =
1230        crypto::sign(key, entry_hash.as_bytes(), signed_at.timestamp());
1231    GovernanceAttestation {
1232        envelope_version: envelope.agora_governance_log,
1233        chain_seq,
1234        prev_hash: envelope.prev_hash,
1235        data_hash: envelope.data_hash,
1236        entry_hash,
1237        signature: signature.into(),
1238        signed_at,
1239        retroactive: is_retroactive(envelope.created_at(), signed_at),
1240    }
1241}
1242
1243// ---------------------------------------------------------------------------
1244// Verification
1245// ---------------------------------------------------------------------------
1246
1247/// Why one link failed on its own, before chain context
1248#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1249pub enum LinkError {
1250    #[error(
1251        "envelope version {0} is not supported (this verifier knows {ENVELOPE_VERSION})"
1252    )]
1253    UnsupportedVersion(u32),
1254    #[error("entry_hash does not recompute from the envelope fields")]
1255    HashMismatch,
1256    #[error("signature does not verify under the published key")]
1257    BadSignature,
1258}
1259
1260/// Recompute a link's `entry_hash` from its fields
1261pub fn recompute_entry_hash(link: &GovernanceChainLink) -> Sha256Hex {
1262    Envelope::new(
1263        link.id.clone(),
1264        link.entry_type,
1265        link.created_at,
1266        link.attestation.prev_hash,
1267        link.attestation.data_hash,
1268    )
1269    .entry_hash()
1270}
1271
1272/// Verify one link in isolation: version, hash recomputation, signature
1273pub fn verify_link(
1274    link: &GovernanceChainLink,
1275    key: &VerifyingKey,
1276) -> Result<(), LinkError> {
1277    let a = &link.attestation;
1278    if a.envelope_version != ENVELOPE_VERSION {
1279        return Err(LinkError::UnsupportedVersion(a.envelope_version));
1280    }
1281    if recompute_entry_hash(link) != a.entry_hash {
1282        return Err(LinkError::HashMismatch);
1283    }
1284    if !crypto::verify(
1285        key,
1286        a.entry_hash.as_bytes(),
1287        a.signed_at.timestamp(),
1288        &Signature::from(&a.signature),
1289    ) {
1290        return Err(LinkError::BadSignature);
1291    }
1292    Ok(())
1293}
1294
1295/// `true` when `data` is what `link` attested
1296pub fn verify_data(
1297    link: &GovernanceChainLink,
1298    data: &serde_json::Value,
1299) -> bool {
1300    data_hash(data) == link.attestation.data_hash
1301}
1302
1303/// The id series an entry type must use, and must not
1304fn prefix_problem(link: &GovernanceChainLink) -> Option<String> {
1305    let prefix = link.id.prefix();
1306    let reserved =
1307        matches!(prefix, GovernanceLogPrefix::Amd | GovernanceLogPrefix::Key);
1308    let expected = match link.entry_type {
1309        GovernanceLogEntryType::Amendment => Some(GovernanceLogPrefix::Amd),
1310        GovernanceLogEntryType::KeyRotation => Some(GovernanceLogPrefix::Key),
1311        _ => None,
1312    };
1313    match expected {
1314        Some(want) if prefix != want => Some(format!(
1315            "the id of a {} entry must be in the {want}- series, not {}",
1316            link.entry_type, link.id
1317        )),
1318        None if reserved => Some(format!(
1319            "{prefix}- ids are reserved for amendment and key_rotation \
1320             entries, but {} is a {}",
1321            link.id, link.entry_type
1322        )),
1323        _ => None,
1324    }
1325}
1326
1327/// Which earlier entry an amendment names, once it is known to be one
1328fn amendment_target(
1329    amendment: &Amendment,
1330    seq: u64,
1331    seq_of: &HashMap<&str, u64>,
1332    links: &[&GovernanceChainLink],
1333) -> Result<u64, AmendmentError> {
1334    amendment.validate()?;
1335    let target = *seq_of.get(amendment.target.as_str()).ok_or_else(|| {
1336        AmendmentError::UnknownTarget(amendment.target.clone())
1337    })?;
1338    if target >= seq {
1339        return Err(AmendmentError::ForwardReference(amendment.target.clone()));
1340    }
1341    if links[target as usize - 1].attestation.entry_hash
1342        != amendment.target_entry_hash
1343    {
1344        return Err(AmendmentError::WrongTargetHash(amendment.target.clone()));
1345    }
1346    Ok(target)
1347}
1348
1349/// The key history as the walk discovers it
1350struct KeyWalk {
1351    /// Oldest first; the last entry is the active key
1352    history: Vec<(GovernanceKeyRecord, VerifyingKey)>,
1353    unanchored: Vec<PublicKeyHex>,
1354    /// Every key that has held the chain, voided ones included. The anchor
1355    /// lists retired and compromised keys too, so without this a thief
1356    /// could declare a "compromise" that rotates back to the key they stole.
1357    seen: HashSet<PublicKeyHex>,
1358    /// `chain_seq`s inside a compromise window
1359    repudiated: HashSet<u64>,
1360}
1361
1362impl KeyWalk {
1363    fn new(genesis: &VerifyingKey, anchor: &KeyAnchor) -> Self {
1364        let public_key = PublicKeyHex::from(genesis);
1365        Self {
1366            history: vec![(
1367                GovernanceKeyRecord {
1368                    public_key,
1369                    from_seq: 1,
1370                    through_seq: None,
1371                    status: KeyStatus::Active,
1372                    introduced_by: None,
1373                    retired_by: None,
1374                },
1375                *genesis,
1376            )],
1377            unanchored: if anchor.contains(&public_key) {
1378                Vec::new()
1379            } else {
1380                vec![public_key]
1381            },
1382            seen: HashSet::from([public_key]),
1383            repudiated: HashSet::new(),
1384        }
1385    }
1386
1387    /// The key that signs entry `seq`
1388    fn in_force(&self, seq: u64) -> (PublicKeyHex, VerifyingKey) {
1389        let (record, key) = self
1390            .history
1391            .iter()
1392            .rev()
1393            .find(|(r, _)| r.from_seq <= seq)
1394            .unwrap_or(&self.history[0]);
1395        (record.public_key, *key)
1396    }
1397
1398    /// The key that signs whatever comes next
1399    fn active(&self) -> PublicKeyHex {
1400        self.history
1401            .last()
1402            .map(|(r, _)| r.public_key)
1403            .expect("the genesis key is always in the history")
1404    }
1405
1406    fn close(
1407        &mut self,
1408        through_seq: u64,
1409        status: KeyStatus,
1410        by: &GovernanceLogId,
1411    ) {
1412        if let Some((record, _)) = self.history.last_mut() {
1413            record.through_seq = Some(through_seq);
1414            record.status = status;
1415            record.retired_by = Some(by.clone());
1416        }
1417    }
1418
1419    fn open(
1420        &mut self,
1421        public_key: PublicKeyHex,
1422        key: VerifyingKey,
1423        from_seq: u64,
1424        by: &GovernanceLogId,
1425    ) {
1426        self.history.push((
1427            GovernanceKeyRecord {
1428                public_key,
1429                from_seq,
1430                through_seq: None,
1431                status: KeyStatus::Active,
1432                introduced_by: Some(by.clone()),
1433                retired_by: None,
1434            },
1435            key,
1436        ));
1437    }
1438
1439    /// Follow `rotation`, appended as `id` at `seq`
1440    fn apply(
1441        &mut self,
1442        rotation: &KeyRotation,
1443        link: &GovernanceChainLink,
1444        seq: u64,
1445        links: &[&GovernanceChainLink],
1446        anchor: &KeyAnchor,
1447    ) -> Result<(), RotationError> {
1448        rotation.verify_proof(link.attestation.prev_hash)?;
1449        let new_key = rotation
1450            .new_key
1451            .to_verifying_key()
1452            .map_err(|_| RotationError::BadNewKey)?;
1453        if self.seen.contains(&rotation.new_key) {
1454            return Err(RotationError::ReusedKey);
1455        }
1456        match rotation.reason {
1457            RotationReason::Routine => {
1458                if rotation.old_key != self.in_force(seq).0 {
1459                    return Err(RotationError::WrongOldKey);
1460                }
1461                self.close(seq, KeyStatus::Retired, &link.id);
1462                self.open(rotation.new_key, new_key, seq + 1, &link.id);
1463                self.seen.insert(rotation.new_key);
1464                if !anchor.contains(&rotation.new_key) {
1465                    self.unanchored.push(rotation.new_key);
1466                }
1467                Ok(())
1468            }
1469            RotationReason::Compromise => {
1470                let head = rotation
1471                    .last_trusted
1472                    .as_ref()
1473                    .ok_or(RotationError::MissingLastTrusted)?;
1474                let trusted_seq = head.chain_seq;
1475                let names_an_earlier_entry = trusted_seq >= 1
1476                    && trusted_seq < seq
1477                    && links[trusted_seq as usize - 1].id == head.id
1478                    && links[trusted_seq as usize - 1].attestation.entry_hash
1479                        == head.entry_hash;
1480                if !names_an_earlier_entry {
1481                    return Err(RotationError::UnknownLastTrusted);
1482                }
1483                // Trust cannot be anchored inside a window nobody trusts,
1484                // reattested or not: name an entry from before it.
1485                if self.repudiated.contains(&trusted_seq) {
1486                    return Err(RotationError::RepudiatedLastTrusted);
1487                }
1488                // The key in force at the last trusted entry — rotations
1489                // inside the window are the thief's, and void.
1490                if rotation.old_key != self.in_force(trusted_seq).0 {
1491                    return Err(RotationError::WrongOldKey);
1492                }
1493                if !anchor.contains(&rotation.new_key) {
1494                    return Err(RotationError::UnanchoredNewKey);
1495                }
1496                self.history.retain(|(r, _)| r.from_seq <= trusted_seq);
1497                self.close(trusted_seq, KeyStatus::Compromised, &link.id);
1498                self.open(rotation.new_key, new_key, seq, &link.id);
1499                self.seen.insert(rotation.new_key);
1500                self.repudiated.extend(trusted_seq + 1..seq);
1501                Ok(())
1502            }
1503        }
1504    }
1505}
1506
1507/// Verify a whole chain from `genesis_key`, following the rotations it
1508/// declares and trusting `anchor` for the ones the chain cannot prove.
1509///
1510/// Links are sorted by `chain_seq` first, so the caller's order does not
1511/// matter. `retroactive` and `out_of_order` are recomputed from the
1512/// timestamps, not copied. `content_matches` is filled only for links that
1513/// carry `data` — for the rest, see
1514/// [`check_content`](GovernanceVerification::check_content).
1515///
1516/// `genesis_key` is the key the chain started under; it is not in the
1517/// chain, so a verifier has to be told. If it is not in `anchor` it is
1518/// reported in `unanchored_keys` rather than rejected — a client pinning
1519/// what it saw first passes `KeyAnchor::pinned(key)` and gets a clean
1520/// report.
1521///
1522/// The rules the report records that no type states on its own: an id
1523/// belongs to its entry type's series and appears once (`AMD-` and `KEY-`
1524/// are reserved for the two types that carry `data`); only an entry whose
1525/// own signature and linkage verify amends anything or moves the key, so
1526/// a forged entry cannot also describe the chain; and an amendment inside
1527/// a repudiated window has no effect unless a [`AmendmentKind::Reattested`]
1528/// vouches for its own entry first, resolved to a fixpoint.
1529pub fn verify_chain(
1530    links: &[GovernanceChainLink],
1531    genesis_key: &VerifyingKey,
1532    anchor: &KeyAnchor,
1533) -> GovernanceVerification {
1534    let mut links: Vec<&GovernanceChainLink> = links.iter().collect();
1535    links.sort_by_key(|l| l.attestation.chain_seq);
1536
1537    let mut seq_of: HashMap<&str, u64> = HashMap::new();
1538    let mut duplicates: HashSet<&str> = HashSet::new();
1539    for (i, link) in links.iter().enumerate() {
1540        if seq_of.insert(link.id.as_str(), i as u64 + 1).is_some() {
1541            duplicates.insert(link.id.as_str());
1542        }
1543    }
1544
1545    let mut walk = KeyWalk::new(genesis_key, anchor);
1546    // (seq, amendment id, amendment, target seq), in chain order
1547    let mut amendments: Vec<(u64, GovernanceLogId, Amendment, u64)> =
1548        Vec::new();
1549    let mut entries: Vec<EntryVerdict> = Vec::with_capacity(links.len());
1550    let mut prev: Option<&GovernanceChainLink> = None;
1551
1552    for (i, link) in links.iter().enumerate() {
1553        let a = &link.attestation;
1554        let expected_seq = i as u64 + 1;
1555        let mut problems: Vec<String> = Vec::new();
1556
1557        if let Some(problem) = prefix_problem(link) {
1558            problems.push(problem);
1559        }
1560        if duplicates.contains(link.id.as_str()) {
1561            problems.push(format!("{} appears more than once", link.id));
1562        }
1563
1564        // The two entry types the verifier has to read. `data` is hashed
1565        // against the envelope before it is parsed, so what is read is
1566        // what was signed.
1567        let carries_meaning = matches!(
1568            link.entry_type,
1569            GovernanceLogEntryType::Amendment
1570                | GovernanceLogEntryType::KeyRotation
1571        );
1572        let mut amendment: Option<Amendment> = None;
1573        let mut rotation: Option<KeyRotation> = None;
1574        let mut content_matches: Option<bool> = None;
1575        match &link.data {
1576            Some(data) => {
1577                let matched = data_hash(data) == a.data_hash;
1578                content_matches = Some(matched);
1579                if !matched {
1580                    if carries_meaning {
1581                        problems.push(
1582                            "`data` does not hash to the attested data_hash"
1583                                .into(),
1584                        );
1585                    }
1586                } else {
1587                    match link.entry_type {
1588                        GovernanceLogEntryType::Amendment => {
1589                            match serde_json::from_value(data.clone()) {
1590                                Ok(v) => amendment = Some(v),
1591                                Err(e) => problems.push(format!(
1592                                    "amendment `data` is malformed: {e}"
1593                                )),
1594                            }
1595                        }
1596                        GovernanceLogEntryType::KeyRotation => {
1597                            match serde_json::from_value(data.clone()) {
1598                                Ok(v) => rotation = Some(v),
1599                                Err(e) => problems.push(format!(
1600                                    "key_rotation `data` is malformed: {e}"
1601                                )),
1602                            }
1603                        }
1604                        _ => {}
1605                    }
1606                }
1607            }
1608            None if carries_meaning => problems.push(format!(
1609                "a {} entry must carry its `data`",
1610                link.entry_type
1611            )),
1612            None => {}
1613        }
1614
1615        // A compromise declaration is signed by the new key, and is
1616        // authentic only if the anchor vouches for that key. Everything
1617        // else is signed by the key in force.
1618        let declared_key = rotation
1619            .as_ref()
1620            .filter(|r| {
1621                r.reason == RotationReason::Compromise
1622                    && anchor.contains(&r.new_key)
1623                    && !walk.seen.contains(&r.new_key)
1624            })
1625            .and_then(|r| r.new_key.to_verifying_key().ok());
1626        let (key_hex, key) = match declared_key {
1627            Some(k) => (PublicKeyHex::from(&k), k),
1628            None => walk.in_force(expected_seq),
1629        };
1630        // Say why a declaration was not taken at its word; the bad
1631        // signature that follows is the consequence, not the cause.
1632        if declared_key.is_none()
1633            && let Some(r) = rotation
1634                .as_ref()
1635                .filter(|r| r.reason == RotationReason::Compromise)
1636        {
1637            problems.push(
1638                if walk.seen.contains(&r.new_key) {
1639                    RotationError::ReusedKey
1640                } else {
1641                    RotationError::UnanchoredNewKey
1642                }
1643                .to_string(),
1644            );
1645        }
1646
1647        let (hash_ok, signature_valid) = match verify_link(link, &key) {
1648            Ok(()) => (true, true),
1649            Err(LinkError::BadSignature) => {
1650                problems.push(LinkError::BadSignature.to_string());
1651                (true, false)
1652            }
1653            Err(e) => {
1654                problems.push(e.to_string());
1655                (false, false)
1656            }
1657        };
1658
1659        let mut link_valid = hash_ok;
1660        if a.chain_seq != expected_seq {
1661            link_valid = false;
1662            problems.push(format!(
1663                "chain_seq {} where {expected_seq} was expected",
1664                a.chain_seq
1665            ));
1666        }
1667        let expected_prev = prev.map(|p| p.attestation.entry_hash);
1668        if a.prev_hash != expected_prev {
1669            link_valid = false;
1670            problems.push(match (a.prev_hash, expected_prev) {
1671                (Some(_), None) => "first entry names a predecessor".into(),
1672                (None, Some(_)) => "prev_hash is null mid-chain".into(),
1673                _ => "prev_hash is not the previous entry's entry_hash".into(),
1674            });
1675        }
1676        let out_of_order = prev.is_some_and(|p| link.created_at < p.created_at);
1677
1678        // Only an entry that is itself authentic moves the key or amends
1679        // anything: a forged one already fails the chain, and must not
1680        // also get to describe it.
1681        let authentic = signature_valid && link_valid;
1682        if let Some(rotation) = rotation.as_ref().filter(|_| authentic)
1683            && let Err(e) =
1684                walk.apply(rotation, link, expected_seq, &links, anchor)
1685        {
1686            problems.push(e.to_string());
1687        }
1688        if let Some(amendment) = amendment.filter(|_| authentic) {
1689            match amendment_target(&amendment, expected_seq, &seq_of, &links) {
1690                Ok(target) => amendments.push((
1691                    expected_seq,
1692                    link.id.clone(),
1693                    amendment,
1694                    target,
1695                )),
1696                Err(e) => problems.push(e.to_string()),
1697            }
1698        }
1699
1700        entries.push(EntryVerdict {
1701            id: link.id.clone(),
1702            chain_seq: a.chain_seq,
1703            signature_valid,
1704            link_valid,
1705            content_matches,
1706            retroactive: is_retroactive(link.created_at, a.signed_at),
1707            out_of_order,
1708            amended_by: Vec::new(),
1709            signed_by: Some(key_hex),
1710            redacted: false,
1711            redacted_data_hash: None,
1712            repudiated: false,
1713            reattested_by: Vec::new(),
1714            problem: (!problems.is_empty()).then(|| problems.join("; ")),
1715        });
1716        prev = Some(link);
1717    }
1718
1719    // Reattestations first, and to a fixpoint: an amendment inside a
1720    // repudiated window has no effect unless something later vouches for
1721    // it, and that something can be another reattestation.
1722    let mut applied = vec![false; amendments.len()];
1723    loop {
1724        let mut changed = false;
1725        for (i, (seq, id, amendment, target)) in amendments.iter().enumerate() {
1726            if applied[i]
1727                || amendment.kind != AmendmentKind::Reattested
1728                || walk.repudiated.contains(seq)
1729            {
1730                continue;
1731            }
1732            applied[i] = true;
1733            entries[*target as usize - 1].reattested_by.push(id.clone());
1734            walk.repudiated.remove(target);
1735            changed = true;
1736        }
1737        if !changed {
1738            break;
1739        }
1740    }
1741
1742    for (seq, id, amendment, target) in &amendments {
1743        if walk.repudiated.contains(seq) {
1744            continue;
1745        }
1746        let entry = &mut entries[*target as usize - 1];
1747        entry.amended_by.push(id.clone());
1748        if let Some(redaction) = &amendment.redaction {
1749            entry.redacted = true;
1750            entry.redacted_data_hash = Some(redaction.resulting_data_hash);
1751        }
1752    }
1753
1754    // A redacted entry's content is what the redaction left behind.
1755    for (i, link) in links.iter().enumerate() {
1756        if entries[i].content_matches == Some(false)
1757            && let (Some(data), Some(hash)) =
1758                (&link.data, entries[i].redacted_data_hash)
1759            && data_hash(data) == hash
1760        {
1761            entries[i].content_matches = Some(true);
1762        }
1763    }
1764
1765    let mut seen = HashSet::new();
1766    walk.unanchored.retain(|key| seen.insert(*key));
1767
1768    let mut repudiated = Vec::new();
1769    for (i, entry) in entries.iter_mut().enumerate() {
1770        if walk.repudiated.contains(&(i as u64 + 1)) {
1771            entry.repudiated = true;
1772            repudiated.push(entry.id.clone());
1773        }
1774    }
1775
1776    GovernanceVerification {
1777        public_key: walk.active(),
1778        ok: false,
1779        head: prev.map(|p| p.id.clone()),
1780        entries,
1781        keys: walk.history.into_iter().map(|(record, _)| record).collect(),
1782        unanchored_keys: walk.unanchored,
1783        repudiated,
1784    }
1785    .settle()
1786}
1787
1788#[cfg(test)]
1789mod tests {
1790    use super::*;
1791    use crate::crypto::generate_keypair;
1792    use serde_json::json;
1793
1794    pub(super) fn gov(n: u32) -> GovernanceLogId {
1795        format!("GOV-2026-{n:04}").parse().unwrap()
1796    }
1797
1798    pub(super) fn amd(n: u32) -> GovernanceLogId {
1799        format!("AMD-2026-{n:04}").parse().unwrap()
1800    }
1801
1802    pub(super) fn key_id(n: u32) -> GovernanceLogId {
1803        format!("KEY-2026-{n:04}").parse().unwrap()
1804    }
1805
1806    pub(super) fn at(secs: i64) -> DateTime<Utc> {
1807        DateTime::from_timestamp(1_700_000_000 + secs, 123_456_789).unwrap()
1808    }
1809
1810    /// The anchor a client that pinned the key it first saw would hold
1811    fn anchored(key: &VerifyingKey) -> KeyAnchor {
1812        KeyAnchor::pinned(key.into())
1813    }
1814
1815    pub(super) fn link(
1816        key: &SigningKey,
1817        n: u32,
1818        prev: Option<&GovernanceChainLink>,
1819        data: &serde_json::Value,
1820        signed_at: DateTime<Utc>,
1821    ) -> GovernanceChainLink {
1822        let created_at = truncate_to_micros(at(n as i64 * 10));
1823        let envelope = Envelope::new(
1824            gov(n),
1825            GovernanceLogEntryType::CouncilDecision,
1826            created_at,
1827            prev.map(|p| p.attestation.entry_hash),
1828            data_hash(data),
1829        );
1830        let attestation = attest(
1831            key,
1832            &envelope,
1833            prev.map_or(1, |p| p.attestation.chain_seq + 1),
1834            signed_at,
1835        );
1836        GovernanceChainLink {
1837            id: gov(n),
1838            entry_type: GovernanceLogEntryType::CouncilDecision,
1839            created_at,
1840            attestation,
1841            data: None,
1842        }
1843    }
1844
1845    pub(super) fn chain(key: &SigningKey, n: u32) -> Vec<GovernanceChainLink> {
1846        let mut out: Vec<GovernanceChainLink> = Vec::new();
1847        for i in 1..=n {
1848            let data = json!({"title": format!("Decision {i}"), "outcome": "approved"});
1849            let l = link(key, i, out.last(), &data, at(i as i64 * 10 + 1));
1850            out.push(l);
1851        }
1852        out
1853    }
1854
1855    /// A chain under construction: one entry per `push`, each series
1856    /// numbered on its own, `data` carried for the entries a verifier
1857    /// reads.
1858    pub(super) struct Chain {
1859        pub(super) links: Vec<GovernanceChainLink>,
1860        gov: u32,
1861        amd: u32,
1862        key: u32,
1863    }
1864
1865    impl Chain {
1866        pub(super) fn new() -> Self {
1867            Self {
1868                links: Vec::new(),
1869                gov: 0,
1870                amd: 0,
1871                key: 0,
1872            }
1873        }
1874
1875        pub(super) fn prev_hash(&self) -> Option<Sha256Hex> {
1876            self.links.last().map(|l| l.attestation.entry_hash)
1877        }
1878
1879        /// The `entry_hash` of the 1-indexed link `seq`
1880        pub(super) fn hash_at(&self, seq: usize) -> Sha256Hex {
1881            self.links[seq - 1].attestation.entry_hash
1882        }
1883
1884        /// What [`Chain::amend`] will call the next amendment
1885        pub(super) fn next_amd(&self) -> GovernanceLogId {
1886            amd(self.amd + 1)
1887        }
1888
1889        pub(super) fn push(
1890            &mut self,
1891            signer: &SigningKey,
1892            id: GovernanceLogId,
1893            entry_type: GovernanceLogEntryType,
1894            data: serde_json::Value,
1895            carry: bool,
1896        ) -> GovernanceLogId {
1897            let n = self.links.len() as i64 + 1;
1898            let created_at = truncate_to_micros(at(n * 10));
1899            let envelope = Envelope::new(
1900                id.clone(),
1901                entry_type,
1902                created_at,
1903                self.prev_hash(),
1904                data_hash(&data),
1905            );
1906            let attestation =
1907                attest(signer, &envelope, n as u64, at(n * 10 + 1));
1908            self.links.push(GovernanceChainLink {
1909                id: id.clone(),
1910                entry_type,
1911                created_at,
1912                attestation,
1913                data: carry.then_some(data),
1914            });
1915            id
1916        }
1917
1918        /// A council decision carrying `data` (which the link does not,
1919        /// as the chain endpoint does not carry transcripts)
1920        pub(super) fn entry(
1921            &mut self,
1922            signer: &SigningKey,
1923            data: serde_json::Value,
1924        ) -> GovernanceLogId {
1925            self.gov += 1;
1926            let id = gov(self.gov);
1927            self.push(
1928                signer,
1929                id,
1930                GovernanceLogEntryType::CouncilDecision,
1931                data,
1932                false,
1933            )
1934        }
1935
1936        pub(super) fn decision(
1937            &mut self,
1938            signer: &SigningKey,
1939        ) -> GovernanceLogId {
1940            let data = json!({"title": format!("Decision {}", self.gov + 1)});
1941            self.entry(signer, data)
1942        }
1943
1944        pub(super) fn amend(
1945            &mut self,
1946            signer: &SigningKey,
1947            amendment: &Amendment,
1948        ) -> GovernanceLogId {
1949            self.amd += 1;
1950            let id = amd(self.amd);
1951            self.push(
1952                signer,
1953                id,
1954                GovernanceLogEntryType::Amendment,
1955                serde_json::to_value(amendment).unwrap(),
1956                true,
1957            )
1958        }
1959
1960        pub(super) fn rotate(
1961            &mut self,
1962            signer: &SigningKey,
1963            rotation: &KeyRotation,
1964        ) -> GovernanceLogId {
1965            self.key += 1;
1966            let id = key_id(self.key);
1967            self.push(
1968                signer,
1969                id,
1970                GovernanceLogEntryType::KeyRotation,
1971                serde_json::to_value(rotation).unwrap(),
1972                true,
1973            )
1974        }
1975    }
1976
1977    // -- canonical JSON --
1978
1979    #[test]
1980    fn canonical_json_sorts_keys_at_every_level() {
1981        let v = json!({"b": {"z": 1, "a": [{"y": 2, "x": 3}]}, "a": null});
1982        assert_eq!(
1983            canonical_json(&v),
1984            br#"{"a":null,"b":{"a":[{"x":3,"y":2}],"z":1}}"#
1985        );
1986    }
1987
1988    #[test]
1989    fn canonical_json_is_compact_and_escapes_like_serde() {
1990        let v = json!({"s": "tab\there \"q\" ünïcode \u{1F600}", "n": [1, -2, 3.5, true, false]});
1991        let bytes = canonical_json(&v);
1992        let text = std::str::from_utf8(&bytes).unwrap();
1993        assert_eq!(
1994            text,
1995            r#"{"n":[1,-2,3.5,true,false],"s":"tab\there \"q\" ünïcode 😀"}"#
1996        );
1997    }
1998
1999    #[test]
2000    fn canonical_json_ignores_insertion_order() {
2001        let mut a = serde_json::Map::new();
2002        a.insert("z".into(), json!(1));
2003        a.insert("a".into(), json!(2));
2004        let mut b = serde_json::Map::new();
2005        b.insert("a".into(), json!(2));
2006        b.insert("z".into(), json!(1));
2007        assert_eq!(
2008            canonical_json(&serde_json::Value::Object(a)),
2009            canonical_json(&serde_json::Value::Object(b))
2010        );
2011    }
2012
2013    #[test]
2014    fn canonical_json_empty_containers() {
2015        assert_eq!(canonical_json(&json!({})), b"{}");
2016        assert_eq!(canonical_json(&json!([])), b"[]");
2017        assert_eq!(
2018            canonical_json(&json!({"a": {}, "b": []})),
2019            br#"{"a":{},"b":[]}"#
2020        );
2021    }
2022
2023    // -- envelope --
2024
2025    #[test]
2026    fn preimage_is_declaration_ordered_json() {
2027        let e = Envelope::new(
2028            gov(1),
2029            GovernanceLogEntryType::AppealsCourtDecision,
2030            at(0),
2031            None,
2032            data_hash(&json!({})),
2033        );
2034        let text = String::from_utf8(e.preimage()).unwrap();
2035        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}");
2036    }
2037
2038    #[test]
2039    fn every_envelope_field_changes_the_hash() {
2040        let base = Envelope::new(
2041            gov(1),
2042            GovernanceLogEntryType::CouncilDecision,
2043            at(0),
2044            None,
2045            data_hash(&json!({"a":1})),
2046        );
2047        let h = base.entry_hash();
2048        let mut e = base.clone();
2049        e.id = gov(2);
2050        assert_ne!(e.entry_hash(), h);
2051        let mut e = base.clone();
2052        e.entry_type = GovernanceLogEntryType::PolicyChange;
2053        assert_ne!(e.entry_hash(), h);
2054        let mut e = base.clone();
2055        e.created_at += 1;
2056        assert_ne!(e.entry_hash(), h);
2057        let mut e = base.clone();
2058        e.prev_hash = Some(h);
2059        assert_ne!(e.entry_hash(), h);
2060        let mut e = base.clone();
2061        e.data_hash = data_hash(&json!({"a":2}));
2062        assert_ne!(e.entry_hash(), h);
2063        assert_eq!(base.entry_hash(), h, "and it is deterministic");
2064    }
2065
2066    #[test]
2067    fn truncation_matches_what_the_envelope_carries() {
2068        let t = at(0);
2069        assert_eq!(truncate_to_micros(t).timestamp_subsec_nanos(), 123_456_000);
2070        assert_eq!(truncate_to_seconds(t).timestamp_subsec_nanos(), 0);
2071        assert_eq!(
2072            Envelope::new(
2073                gov(1),
2074                GovernanceLogEntryType::CouncilDecision,
2075                t,
2076                None,
2077                data_hash(&json!(null))
2078            )
2079            .created_at,
2080            truncate_to_micros(t).timestamp_micros()
2081        );
2082    }
2083
2084    // -- hex newtypes --
2085
2086    #[test]
2087    fn hex_newtypes_round_trip_and_reject_wrong_lengths() {
2088        let h = data_hash(&json!(1));
2089        let s = serde_json::to_string(&h).unwrap();
2090        assert_eq!(s.len(), 66);
2091        let back: Sha256Hex = serde_json::from_str(&s).unwrap();
2092        assert_eq!(back, h);
2093        assert!(serde_json::from_str::<Sha256Hex>("\"abcd\"").is_err());
2094        assert!("zz".repeat(32).parse::<Sha256Hex>().is_err());
2095        assert!(Sha256Hex::try_from(vec![0u8; 31]).is_err());
2096        assert_eq!(format!("{h:?}"), format!("Sha256Hex({h})"));
2097    }
2098
2099    // -- signing and verification --
2100
2101    #[test]
2102    fn attest_then_verify_link() {
2103        let (key, pk) = generate_keypair();
2104        let l = link(&key, 1, None, &json!({"a": 1}), at(5));
2105        assert_eq!(verify_link(&l, &pk), Ok(()));
2106        assert!(verify_data(&l, &json!({"a": 1})));
2107        assert!(!verify_data(&l, &json!({"a": 2})));
2108        assert!(!l.attestation.retroactive);
2109    }
2110
2111    #[test]
2112    fn wrong_key_fails_signature_only() {
2113        let (key, _) = generate_keypair();
2114        let (_, other) = generate_keypair();
2115        let l = link(&key, 1, None, &json!({}), at(5));
2116        assert_eq!(verify_link(&l, &other), Err(LinkError::BadSignature));
2117    }
2118
2119    #[test]
2120    fn tampering_with_any_attested_field_is_detected() {
2121        let (key, pk) = generate_keypair();
2122        let l = link(&key, 1, None, &json!({"a": 1}), at(5));
2123
2124        let mut t = l.clone();
2125        t.created_at += chrono::Duration::microseconds(1);
2126        assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
2127
2128        let mut t = l.clone();
2129        t.entry_type = GovernanceLogEntryType::StewardVeto;
2130        assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
2131
2132        let mut t = l.clone();
2133        t.attestation.data_hash = data_hash(&json!({"a": 2}));
2134        assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
2135
2136        // Back-dating the signature: the hash still recomputes, but the
2137        // signature covered the real signed_at.
2138        let mut t = l.clone();
2139        t.attestation.signed_at -= chrono::Duration::seconds(1);
2140        assert_eq!(verify_link(&t, &pk), Err(LinkError::BadSignature));
2141
2142        let mut t = l.clone();
2143        t.attestation.envelope_version = 2;
2144        assert_eq!(verify_link(&t, &pk), Err(LinkError::UnsupportedVersion(2)));
2145    }
2146
2147    #[test]
2148    fn a_good_chain_verifies_in_any_input_order() {
2149        let (key, pk) = generate_keypair();
2150        let mut c = chain(&key, 4);
2151        c.reverse();
2152        let v = verify_chain(&c, &pk, &anchored(&pk));
2153        assert!(v.ok, "{v:#?}");
2154        assert_eq!(v.head, Some(gov(4)));
2155        assert_eq!(
2156            v.entries.iter().map(|e| e.chain_seq).collect::<Vec<_>>(),
2157            [1, 2, 3, 4]
2158        );
2159        assert!(v.entries.iter().all(|e| e.content_matches.is_none()
2160            && e.problem.is_none()
2161            && !e.out_of_order));
2162        assert_eq!(v.public_key, PublicKeyHex::from(&pk));
2163    }
2164
2165    #[test]
2166    fn empty_chain_is_ok_with_no_head() {
2167        let (_, pk) = generate_keypair();
2168        let v = verify_chain(&[], &pk, &anchored(&pk));
2169        assert!(v.ok);
2170        assert!(v.head.is_none());
2171        assert!(v.entries.is_empty());
2172    }
2173
2174    #[test]
2175    fn a_changed_entry_breaks_its_signature_and_the_next_link() {
2176        let (key, pk) = generate_keypair();
2177        let mut c = chain(&key, 3);
2178        // Re-attest entry 2 with different data but the same predecessor,
2179        // as a key holder rewriting history would.
2180        let rewritten = link(
2181            &key,
2182            2,
2183            Some(&c[0]),
2184            &json!({"title": "Decision 2", "outcome": "REJECTED"}),
2185            at(21),
2186        );
2187        c[1] = rewritten;
2188        let v = verify_chain(&c, &pk, &anchored(&pk));
2189        assert!(!v.ok);
2190        assert!(
2191            v.entries[1].signature_valid && v.entries[1].link_valid,
2192            "the rewrite itself is well-formed: {:#?}",
2193            v.entries[1]
2194        );
2195        assert!(
2196            !v.entries[2].link_valid,
2197            "but entry 3 no longer points at it: {:#?}",
2198            v.entries[2]
2199        );
2200        assert!(
2201            v.entries[2]
2202                .problem
2203                .as_deref()
2204                .unwrap()
2205                .contains("prev_hash")
2206        );
2207    }
2208
2209    #[test]
2210    fn a_removed_entry_is_a_gap_and_a_broken_link() {
2211        let (key, pk) = generate_keypair();
2212        let mut c = chain(&key, 3);
2213        c.remove(1);
2214        let v = verify_chain(&c, &pk, &anchored(&pk));
2215        assert!(!v.ok);
2216        assert!(v.entries[0].link_valid);
2217        let p = v.entries[1].problem.as_deref().unwrap();
2218        assert!(p.contains("chain_seq 3 where 2 was expected"), "{p}");
2219        assert!(p.contains("prev_hash"), "{p}");
2220    }
2221
2222    #[test]
2223    fn a_second_genesis_is_rejected() {
2224        let (key, pk) = generate_keypair();
2225        let mut c = chain(&key, 2);
2226        let rogue = link(&key, 2, None, &json!({}), at(21));
2227        c[1] = rogue;
2228        let v = verify_chain(&c, &pk, &anchored(&pk));
2229        assert!(!v.ok);
2230        assert!(
2231            v.entries[1]
2232                .problem
2233                .as_deref()
2234                .unwrap()
2235                .contains("null mid-chain")
2236        );
2237    }
2238
2239    #[test]
2240    fn retroactive_and_out_of_order_are_recomputed_not_copied() {
2241        let (key, pk) = generate_keypair();
2242        let first = link(&key, 1, None, &json!({}), at(10 + 3600));
2243        // Second entry recorded *before* the first by the clock, but after it
2244        // in the chain. Valid chain, flagged order.
2245        let created = truncate_to_micros(at(5));
2246        let envelope = Envelope::new(
2247            gov(2),
2248            GovernanceLogEntryType::CouncilDecision,
2249            created,
2250            Some(first.attestation.entry_hash),
2251            data_hash(&json!({})),
2252        );
2253        let attestation = attest(&key, &envelope, 2, at(6));
2254        let mut second = GovernanceChainLink {
2255            id: gov(2),
2256            entry_type: GovernanceLogEntryType::CouncilDecision,
2257            created_at: created,
2258            attestation,
2259            data: None,
2260        };
2261        second.attestation.retroactive = true; // a lying flag on the wire
2262        let v = verify_chain(&[first, second], &pk, &anchored(&pk));
2263        assert!(v.ok, "{v:#?}");
2264        assert!(v.entries[0].retroactive);
2265        assert!(!v.entries[1].retroactive, "recomputed from timestamps");
2266        assert!(v.entries[1].out_of_order);
2267    }
2268
2269    #[test]
2270    fn content_mismatch_settles_to_not_ok() {
2271        let (key, pk) = generate_keypair();
2272        let c = chain(&key, 1);
2273        let mut v = verify_chain(&c, &pk, &anchored(&pk));
2274        v.entries[0].content_matches = Some(true);
2275        assert!(v.clone().settle().ok);
2276        v.entries[0].content_matches = Some(false);
2277        assert!(!v.settle().ok);
2278    }
2279
2280    // -- amendments --
2281
2282    #[test]
2283    fn an_amendment_fills_amended_by_on_its_target() {
2284        let (key, pk) = generate_keypair();
2285        let mut c = Chain::new();
2286        let target = c.decision(&key);
2287        c.decision(&key);
2288        let amendment = Amendment::new(
2289            target,
2290            c.hash_at(1),
2291            AmendmentKind::NonPrecedential,
2292            "§1 (Red Team Cases Recharacterized)",
2293            "diagnostic finding — not citable as moderation precedent",
2294        )
2295        .unwrap()
2296        .with_authority(gov(5))
2297        .with_rationale("§5 leaves the ruling itself standing");
2298        let id = c.amend(&key, &amendment);
2299
2300        let v = verify_chain(&c.links, &pk, &anchored(&pk));
2301        assert!(v.ok, "{v:#?}");
2302        assert_eq!(v.entries[0].amended_by, vec![id]);
2303        assert!(v.entries[1].amended_by.is_empty());
2304        assert!(!v.entries[0].redacted);
2305        assert_eq!(v.head, Some(amd(1)));
2306        // The amendment link carries `data`, so its own content is checked.
2307        assert_eq!(v.entries[2].content_matches, Some(true));
2308        assert_eq!(v.entries[0].content_matches, None);
2309        assert_eq!(standing([amendment.kind]), Standing::NonPrecedential);
2310    }
2311
2312    #[test]
2313    fn an_amendment_must_name_an_earlier_entry_by_its_exact_hash() {
2314        let (key, pk) = generate_keypair();
2315        let problem = |c: &Chain, at: usize| -> String {
2316            verify_chain(&c.links, &pk, &anchored(&pk)).entries[at]
2317                .problem
2318                .clone()
2319                .unwrap_or_default()
2320        };
2321
2322        let mut c = Chain::new();
2323        c.decision(&key);
2324        let unknown = Amendment::new(
2325            gov(99),
2326            c.hash_at(1),
2327            AmendmentKind::Overruled,
2328            "b",
2329            "n",
2330        )
2331        .unwrap();
2332        c.amend(&key, &unknown);
2333        assert!(
2334            problem(&c, 1).contains("is not an entry of this chain"),
2335            "{}",
2336            problem(&c, 1)
2337        );
2338        assert!(!verify_chain(&c.links, &pk, &anchored(&pk)).ok);
2339
2340        // Names an entry that does not exist yet.
2341        let mut c = Chain::new();
2342        c.decision(&key);
2343        let forward = Amendment::new(
2344            gov(2),
2345            c.hash_at(1),
2346            AmendmentKind::Overruled,
2347            "b",
2348            "n",
2349        )
2350        .unwrap();
2351        c.amend(&key, &forward);
2352        c.decision(&key);
2353        assert!(
2354            problem(&c, 1).contains("not an earlier entry"),
2355            "{}",
2356            problem(&c, 1)
2357        );
2358
2359        // Right id, wrong entry.
2360        let mut c = Chain::new();
2361        let target = c.decision(&key);
2362        let wrong_hash = Amendment::new(
2363            target,
2364            data_hash(&json!("some other entry")),
2365            AmendmentKind::Overruled,
2366            "b",
2367            "n",
2368        )
2369        .unwrap();
2370        c.amend(&key, &wrong_hash);
2371        assert!(problem(&c, 1).contains("entry_hash"), "{}", problem(&c, 1));
2372        assert!(
2373            verify_chain(&c.links, &pk, &anchored(&pk)).entries[0]
2374                .amended_by
2375                .is_empty()
2376        );
2377    }
2378
2379    #[test]
2380    fn redaction_shape_violations_fail_verification() {
2381        let (key, pk) = generate_keypair();
2382        let amend_with = |mutate: &dyn Fn(&mut Amendment)| -> String {
2383            let mut c = Chain::new();
2384            let target = c.decision(&key);
2385            let mut amendment = Amendment::new(
2386                target,
2387                c.hash_at(1),
2388                AmendmentKind::Correction,
2389                "b",
2390                "n",
2391            )
2392            .unwrap();
2393            mutate(&mut amendment);
2394            c.amend(&key, &amendment);
2395            let v = verify_chain(&c.links, &pk, &anchored(&pk));
2396            assert!(!v.ok, "{v:#?}");
2397            v.entries[1].problem.clone().unwrap_or_default()
2398        };
2399
2400        // `redaction` is the only way to get a well-formed one, so a
2401        // redaction kind without a `Redaction` can only be hand-built.
2402        assert_eq!(
2403            Amendment::new(
2404                gov(1),
2405                data_hash(&json!(null)),
2406                AmendmentKind::Redaction,
2407                "b",
2408                "n"
2409            ),
2410            Err(AmendmentError::MissingRedaction)
2411        );
2412        let p = amend_with(&|a| a.kind = AmendmentKind::Redaction);
2413        assert!(p.contains("requires a `redaction`"), "{p}");
2414
2415        let p = amend_with(&|a| {
2416            a.redaction = Some(Redaction {
2417                fields: vec!["/x".into()],
2418                resulting_data_hash: data_hash(&json!({})),
2419            })
2420        });
2421        assert!(p.contains("only valid on kind"), "{p}");
2422
2423        let p = amend_with(&|a| a.agora_governance_amendment = 2);
2424        assert!(p.contains("agora_governance_amendment is 2"), "{p}");
2425    }
2426
2427    #[test]
2428    fn standing_is_the_last_amendment_that_changes_it() {
2429        use AmendmentKind::*;
2430        assert_eq!(standing([]), Standing::InForce);
2431        assert_eq!(standing([Correction, Redaction]), Standing::InForce);
2432        assert_eq!(
2433            standing([Correction, NonPrecedential, Redaction]),
2434            Standing::NonPrecedential
2435        );
2436        assert_eq!(standing([Overruled, Reinstated]), Standing::InForce);
2437        assert_eq!(standing([Reinstated, Superseded]), Standing::Superseded);
2438        assert_eq!(kind_standing(Reattested), None);
2439        assert_eq!(kind_standing(Reinstated), Some(Standing::InForce));
2440    }
2441
2442    #[test]
2443    fn a_redaction_verifies_against_the_amendment_and_nothing_else() {
2444        let (key, pk) = generate_keypair();
2445        let data = json!({
2446            "finding": "upheld",
2447            "subject": {"handle": "someone", "detail": "personal"},
2448        });
2449        let mut c = Chain::new();
2450        let target = c.entry(&key, data.clone());
2451        let amendment_id = c.next_amd();
2452        let (amendment, redacted) = Amendment::redaction(
2453            &amendment_id,
2454            target,
2455            c.hash_at(1),
2456            "GDPR Art. 17(1)(a)",
2457            "personal data removed on request",
2458            vec!["/subject/handle".into(), "/subject/detail".into()],
2459            &data,
2460        )
2461        .unwrap();
2462        assert_eq!(c.amend(&key, &amendment), amendment_id);
2463
2464        let mut v = verify_chain(&c.links, &pk, &anchored(&pk));
2465        assert!(v.ok, "{v:#?}");
2466        assert!(v.entries[0].redacted);
2467        assert_eq!(v.entries[0].redacted_data_hash, Some(data_hash(&redacted)));
2468        assert_eq!(
2469            redacted["subject"]["handle"],
2470            json!(format!("[redacted by {amendment_id}]"))
2471        );
2472        assert_eq!(redacted["finding"], json!("upheld"), "and nothing else");
2473
2474        // What the server now serves verifies.
2475        assert!(v.check_content(&c.links[0], &redacted));
2476        assert_eq!(v.entries[0].content_matches, Some(true));
2477        assert!(v.clone().settle().ok);
2478        // So does the original, for anyone who kept a copy.
2479        assert!(v.check_content(&c.links[0], &data));
2480
2481        // A second edit does not.
2482        let mut tampered = redacted.clone();
2483        tampered["finding"] = json!("overturned");
2484        assert!(!v.check_content(&c.links[0], &tampered));
2485        assert_eq!(v.entries[0].content_matches, Some(false));
2486        assert!(!v.settle().ok);
2487    }
2488
2489    #[test]
2490    fn content_that_matches_nothing_fails_without_an_amendment() {
2491        let (key, pk) = generate_keypair();
2492        let mut c = Chain::new();
2493        c.entry(&key, json!({"a": 1}));
2494        let mut v = verify_chain(&c.links, &pk, &anchored(&pk));
2495        assert!(!v.check_content(&c.links[0], &json!({"a": 2})));
2496        assert!(!v.clone().settle().ok);
2497        // An entry that is not in the report at all is not a pass either.
2498        let other = link(&key, 9, None, &json!({}), at(9));
2499        assert!(!v.check_content(&other, &json!({})));
2500    }
2501
2502    #[test]
2503    fn tampering_with_an_amendments_data_is_caught_by_the_data_hash() {
2504        let (key, pk) = generate_keypair();
2505        let mut c = Chain::new();
2506        let target = c.decision(&key);
2507        let amendment = Amendment::new(
2508            target,
2509            c.hash_at(1),
2510            AmendmentKind::Overruled,
2511            "b",
2512            "overruled by a later decision",
2513        )
2514        .unwrap();
2515        c.amend(&key, &amendment);
2516        c.links[1].data.as_mut().unwrap()["note"] =
2517            json!("reinstated, actually");
2518
2519        let v = verify_chain(&c.links, &pk, &anchored(&pk));
2520        assert!(!v.ok, "{v:#?}");
2521        let p = v.entries[1].problem.as_deref().unwrap();
2522        assert!(p.contains("does not hash to the attested data_hash"), "{p}");
2523        assert!(
2524            v.entries[0].amended_by.is_empty(),
2525            "an unreadable amendment has no effect"
2526        );
2527        assert!(
2528            v.entries[1].signature_valid && v.entries[1].link_valid,
2529            "the envelope is untouched — only the content is not what it \
2530             committed to"
2531        );
2532    }
2533
2534    #[test]
2535    fn an_amendment_or_rotation_must_carry_its_data() {
2536        let (key, pk) = generate_keypair();
2537        let mut c = Chain::new();
2538        let target = c.decision(&key);
2539        let amendment = Amendment::new(
2540            target,
2541            c.hash_at(1),
2542            AmendmentKind::Correction,
2543            "b",
2544            "n",
2545        )
2546        .unwrap();
2547        c.amend(&key, &amendment);
2548        let rotation = KeyRotation::routine(
2549            (&pk).into(),
2550            &key,
2551            c.prev_hash(),
2552            at(35),
2553            "n",
2554        );
2555        c.rotate(&key, &rotation);
2556        c.links[1].data = None;
2557        c.links[2].data = None;
2558
2559        let v = verify_chain(&c.links, &pk, &anchored(&pk));
2560        assert!(!v.ok, "{v:#?}");
2561        for (i, entry_type) in [(1, "amendment"), (2, "key_rotation")] {
2562            let p = v.entries[i].problem.as_deref().unwrap();
2563            assert!(p.contains(&format!("a {entry_type} entry")), "{p}");
2564            assert!(p.contains("must carry its `data`"), "{p}");
2565        }
2566    }
2567
2568    #[test]
2569    fn the_id_series_must_match_the_entry_type() {
2570        let (key, pk) = generate_keypair();
2571        let mut c = Chain::new();
2572        let target = c.decision(&key);
2573        let amendment = Amendment::new(
2574            target,
2575            c.hash_at(1),
2576            AmendmentKind::Correction,
2577            "b",
2578            "n",
2579        )
2580        .unwrap();
2581        c.push(
2582            &key,
2583            gov(7),
2584            GovernanceLogEntryType::Amendment,
2585            serde_json::to_value(&amendment).unwrap(),
2586            true,
2587        );
2588        let v = verify_chain(&c.links, &pk, &anchored(&pk));
2589        assert!(!v.ok, "{v:#?}");
2590        let p = v.entries[1].problem.as_deref().unwrap();
2591        assert!(p.contains("must be in the AMD- series"), "{p}");
2592
2593        let mut c = Chain::new();
2594        c.push(
2595            &key,
2596            key_id(1),
2597            GovernanceLogEntryType::CouncilDecision,
2598            json!({}),
2599            false,
2600        );
2601        let v = verify_chain(&c.links, &pk, &anchored(&pk));
2602        let p = v.entries[0].problem.as_deref().unwrap();
2603        assert!(p.contains("reserved"), "{p}");
2604    }
2605
2606    #[test]
2607    fn redact_data_replaces_whole_values_and_refuses_the_rest() {
2608        let id = amd(3);
2609        let data = json!({"a": {"b": [1, {"c": "secret"}]}, "d/e": "slash"});
2610        let out = redact_data(&data, &["/a/b/1/c".into(), "/d~1e".into()], &id)
2611            .unwrap();
2612        assert_eq!(out["a"]["b"][1]["c"], json!(redaction_marker(&id)));
2613        assert_eq!(out["d/e"], json!(redaction_marker(&id)));
2614        assert_eq!(out["a"]["b"][0], json!(1), "untouched");
2615
2616        assert_eq!(
2617            redact_data(&data, &["/a/nope".into()], &id),
2618            Err(RedactError::Unresolved("/a/nope".into()))
2619        );
2620        assert_eq!(
2621            redact_data(&data, &["".into()], &id),
2622            Err(RedactError::WholeEntry)
2623        );
2624        assert_eq!(redact_data(&data, &[], &id).unwrap(), data);
2625    }
2626
2627    // -- key rotation --
2628
2629    #[test]
2630    fn a_routine_rotation_moves_the_chain_to_the_new_key() {
2631        let (old, old_pk) = generate_keypair();
2632        let (new, new_pk) = generate_keypair();
2633        let anchor = anchored(&old_pk).with((&new_pk).into());
2634        let mut c = Chain::new();
2635        c.decision(&old);
2636        let rotation = KeyRotation::routine(
2637            (&old_pk).into(),
2638            &new,
2639            c.prev_hash(),
2640            at(25),
2641            "scheduled rotation",
2642        );
2643        let rotation_id = c.rotate(&old, &rotation);
2644        c.decision(&new);
2645
2646        let v = verify_chain(&c.links, &old_pk, &anchor);
2647        assert!(v.ok, "{v:#?}");
2648        assert_eq!(v.public_key, (&new_pk).into());
2649        assert_eq!(
2650            v.entries[1].signed_by,
2651            Some((&old_pk).into()),
2652            "the rotation itself is signed by the old key"
2653        );
2654        assert_eq!(v.entries[2].signed_by, Some((&new_pk).into()));
2655        assert!(v.unanchored_keys.is_empty());
2656        assert!(v.repudiated.is_empty());
2657        assert_eq!(v.keys.len(), 2);
2658        assert_eq!(v.keys[0].public_key, (&old_pk).into());
2659        assert_eq!(v.keys[0].from_seq, 1);
2660        assert_eq!(v.keys[0].through_seq, Some(2));
2661        assert_eq!(v.keys[0].status, KeyStatus::Retired);
2662        assert_eq!(v.keys[0].introduced_by, None);
2663        assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
2664        assert_eq!(v.keys[1].from_seq, 3);
2665        assert_eq!(v.keys[1].through_seq, None);
2666        assert_eq!(v.keys[1].status, KeyStatus::Active);
2667        assert_eq!(v.keys[1].introduced_by.as_ref(), Some(&rotation_id));
2668    }
2669
2670    #[test]
2671    fn the_old_key_cannot_sign_after_a_routine_rotation() {
2672        let (old, old_pk) = generate_keypair();
2673        let (new, new_pk) = generate_keypair();
2674        let anchor = anchored(&old_pk).with((&new_pk).into());
2675        let mut c = Chain::new();
2676        c.decision(&old);
2677        let rotation = KeyRotation::routine(
2678            (&old_pk).into(),
2679            &new,
2680            c.prev_hash(),
2681            at(25),
2682            "scheduled",
2683        );
2684        c.rotate(&old, &rotation);
2685        c.decision(&old);
2686
2687        let v = verify_chain(&c.links, &old_pk, &anchor);
2688        assert!(!v.ok, "{v:#?}");
2689        assert!(!v.entries[2].signature_valid);
2690        assert!(
2691            v.entries[2].link_valid,
2692            "the linkage is fine; the key is not"
2693        );
2694    }
2695
2696    #[test]
2697    fn a_routine_rotation_to_an_unanchored_key_is_followed_and_reported() {
2698        let (old, old_pk) = generate_keypair();
2699        let (new, new_pk) = generate_keypair();
2700        let mut c = Chain::new();
2701        c.decision(&old);
2702        let rotation = KeyRotation::routine(
2703            (&old_pk).into(),
2704            &new,
2705            c.prev_hash(),
2706            at(25),
2707            "scheduled",
2708        );
2709        c.rotate(&old, &rotation);
2710        c.decision(&new);
2711
2712        // The anchor has never heard of the new key: chain-valid, and the
2713        // one thing a reference client must shout about.
2714        let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk));
2715        assert!(v.ok, "{v:#?}");
2716        assert_eq!(v.unanchored_keys, vec![PublicKeyHex::from(&new_pk)]);
2717        assert_eq!(v.public_key, (&new_pk).into());
2718    }
2719
2720    #[test]
2721    fn a_forged_proof_of_possession_is_rejected() {
2722        let (old, old_pk) = generate_keypair();
2723        let (_, new_pk) = generate_keypair();
2724        let (thief, _) = generate_keypair();
2725        let mut c = Chain::new();
2726        c.decision(&old);
2727        // A rotation to a key nobody holds: the proof is signed by the old
2728        // key (and by an unrelated one) instead of by `new_key` itself.
2729        let mut rotation = KeyRotation::routine(
2730            (&old_pk).into(),
2731            &thief,
2732            c.prev_hash(),
2733            at(25),
2734            "scheduled",
2735        );
2736        rotation.new_key = (&new_pk).into();
2737        let statement = rotation.statement(c.prev_hash());
2738        rotation.proof = crypto::sign(
2739            &old,
2740            statement.hash().as_bytes(),
2741            rotation.proof_signed_at,
2742        )
2743        .into();
2744        c.rotate(&old, &rotation);
2745
2746        assert_eq!(
2747            rotation.verify_proof(c.links[1].attestation.prev_hash),
2748            Err(RotationError::BadProof)
2749        );
2750        let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk));
2751        assert!(!v.ok, "{v:#?}");
2752        assert!(
2753            v.entries[1]
2754                .problem
2755                .as_deref()
2756                .unwrap()
2757                .contains("proof of possession")
2758        );
2759        assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
2760    }
2761
2762    #[test]
2763    fn a_proof_does_not_replay_at_another_position() {
2764        let (old, old_pk) = generate_keypair();
2765        let (new, new_pk) = generate_keypair();
2766        let anchor = anchored(&old_pk).with((&new_pk).into());
2767        let mut c = Chain::new();
2768        c.decision(&old);
2769        // Proof bound to the position right after entry 1 …
2770        let rotation = KeyRotation::routine(
2771            (&old_pk).into(),
2772            &new,
2773            c.prev_hash(),
2774            at(25),
2775            "scheduled",
2776        );
2777        // … and appended one entry later.
2778        c.decision(&old);
2779        c.rotate(&old, &rotation);
2780
2781        let v = verify_chain(&c.links, &old_pk, &anchor);
2782        assert!(!v.ok, "{v:#?}");
2783        assert!(
2784            v.entries[2]
2785                .problem
2786                .as_deref()
2787                .unwrap()
2788                .contains("proof of possession")
2789        );
2790        assert_eq!(v.public_key, (&old_pk).into());
2791    }
2792
2793    #[test]
2794    fn a_compromise_repudiates_the_window_and_a_reattestation_restores_one() {
2795        let (old, old_pk) = generate_keypair();
2796        let (new, new_pk) = generate_keypair();
2797        let anchor = anchored(&old_pk).with((&new_pk).into());
2798        let mut c = Chain::new();
2799        c.decision(&old); // 1 — the last entry anyone trusts
2800        c.decision(&old); // 2 — inside the window
2801        let reattested = c.decision(&old); // 3 — inside, later vouched for
2802        let rotation = KeyRotation::compromise(
2803            (&old_pk).into(),
2804            &new,
2805            TrustedHead {
2806                id: gov(1),
2807                chain_seq: 1,
2808                entry_hash: c.hash_at(1),
2809            },
2810            c.prev_hash(),
2811            at(45),
2812            "signing key exfiltrated",
2813        );
2814        let rotation_id = c.rotate(&new, &rotation); // 4 — signed by the NEW key
2815        let vouch = Amendment::new(
2816            reattested,
2817            c.hash_at(3),
2818            AmendmentKind::Reattested,
2819            "Art. VII",
2820            "independently verified; the Steward vouches for it",
2821        )
2822        .unwrap();
2823        let vouch_id = c.amend(&new, &vouch); // 5
2824
2825        let v = verify_chain(&c.links, &old_pk, &anchor);
2826        assert!(
2827            v.ok,
2828            "repudiation is a declared state, not a defect: {v:#?}"
2829        );
2830        assert_eq!(v.repudiated, vec![gov(2)]);
2831        assert!(v.entries[1].repudiated);
2832        assert!(!v.entries[2].repudiated);
2833        assert_eq!(v.entries[2].reattested_by, vec![vouch_id.clone()]);
2834        assert_eq!(v.entries[2].amended_by, vec![vouch_id]);
2835        assert_eq!(v.entries[3].signed_by, Some((&new_pk).into()));
2836        assert_eq!(v.public_key, (&new_pk).into());
2837        assert_eq!(v.keys.len(), 2);
2838        assert_eq!(v.keys[0].status, KeyStatus::Compromised);
2839        assert_eq!(
2840            v.keys[0].through_seq,
2841            Some(1),
2842            "trusted through the last trusted entry, not through the \
2843             declaration"
2844        );
2845        assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
2846        assert_eq!(v.keys[1].from_seq, 4, "the declaration is its own first");
2847    }
2848
2849    #[test]
2850    fn a_stolen_key_cannot_be_rotated_back_in() {
2851        // K1 is compromised and replaced by K2. Both are published, so both
2852        // are in every anchor. The thief, still holding K1, declares a
2853        // "compromise" of K2 naming K1 as the new key.
2854        let (k1, k1_pk) = generate_keypair();
2855        let (k2, k2_pk) = generate_keypair();
2856        let anchor = anchored(&k1_pk).with((&k2_pk).into());
2857        let mut c = Chain::new();
2858        c.decision(&k1); // 1
2859        let real = KeyRotation::compromise(
2860            (&k1_pk).into(),
2861            &k2,
2862            TrustedHead {
2863                id: gov(1),
2864                chain_seq: 1,
2865                entry_hash: c.hash_at(1),
2866            },
2867            c.prev_hash(),
2868            at(25),
2869            "signing key exfiltrated",
2870        );
2871        c.rotate(&k2, &real); // 2
2872        c.decision(&k2); // 3
2873        let honest = verify_chain(&c.links, &k1_pk, &anchor);
2874        assert!(honest.ok, "{honest:#?}");
2875
2876        let hijack = KeyRotation::compromise(
2877            (&k2_pk).into(),
2878            &k1,
2879            TrustedHead {
2880                id: gov(2),
2881                chain_seq: 3,
2882                entry_hash: c.hash_at(3),
2883            },
2884            c.prev_hash(),
2885            at(45),
2886            "the Steward's key is the compromised one, trust me",
2887        );
2888        c.rotate(&k1, &hijack); // 4 — signed by the stolen key
2889        c.decision(&k1); // 5
2890
2891        let v = verify_chain(&c.links, &k1_pk, &anchor);
2892        assert!(!v.ok);
2893        assert_eq!(v.public_key, (&k2_pk).into(), "the chain stays with K2");
2894        assert!(!v.entries[3].signature_valid, "{:#?}", v.entries[3]);
2895        assert!(!v.entries[4].signature_valid, "K1 signs nothing again");
2896        assert_eq!(v.keys.len(), 2);
2897        assert_eq!(v.keys[1].status, KeyStatus::Active);
2898    }
2899
2900    #[test]
2901    fn a_routine_rotation_cannot_reuse_a_key_either() {
2902        let (k1, k1_pk) = generate_keypair();
2903        let (k2, k2_pk) = generate_keypair();
2904        let anchor = anchored(&k1_pk).with((&k2_pk).into());
2905        let mut c = Chain::new();
2906        c.decision(&k1);
2907        let out = KeyRotation::routine(
2908            (&k1_pk).into(),
2909            &k2,
2910            c.prev_hash(),
2911            at(15),
2912            "",
2913        );
2914        c.rotate(&k1, &out);
2915        let back = KeyRotation::routine(
2916            (&k2_pk).into(),
2917            &k1,
2918            c.prev_hash(),
2919            at(25),
2920            "",
2921        );
2922        c.rotate(&k2, &back);
2923        let v = verify_chain(&c.links, &k1_pk, &anchor);
2924        assert!(!v.ok);
2925        assert!(
2926            v.entries[2]
2927                .problem
2928                .as_deref()
2929                .unwrap()
2930                .contains("never brought back"),
2931            "{:#?}",
2932            v.entries[2]
2933        );
2934        assert_eq!(v.public_key, (&k2_pk).into());
2935    }
2936
2937    #[test]
2938    fn a_forged_entry_has_no_effects() {
2939        let (steward, steward_pk) = generate_keypair();
2940        let (forger, _) = generate_keypair();
2941        let anchor = anchored(&steward_pk);
2942        let mut c = Chain::new();
2943        let target = c.decision(&steward); // 1
2944        let fake = Amendment::new(
2945            target,
2946            c.hash_at(1),
2947            AmendmentKind::Overruled,
2948            "none",
2949            "overruled, says nobody with the key",
2950        )
2951        .unwrap();
2952        c.amend(&forger, &fake); // 2 — not signed by the key in force
2953        let grab = KeyRotation::routine(
2954            (&steward_pk).into(),
2955            &forger,
2956            c.prev_hash(),
2957            at(35),
2958            "",
2959        );
2960        c.rotate(&forger, &grab); // 3 — likewise
2961
2962        let v = verify_chain(&c.links, &steward_pk, &anchor);
2963        assert!(!v.ok);
2964        assert!(v.entries[0].amended_by.is_empty(), "{:#?}", v.entries[0]);
2965        assert_eq!(v.public_key, (&steward_pk).into());
2966        assert_eq!(v.keys.len(), 1);
2967        assert!(v.unanchored_keys.is_empty());
2968    }
2969
2970    #[test]
2971    fn a_repeated_id_is_a_problem() {
2972        let (key, pk) = generate_keypair();
2973        let mut c = Chain::new();
2974        c.decision(&key);
2975        c.gov = 0;
2976        c.decision(&key); // GOV-2026-0001 again
2977        let v = verify_chain(&c.links, &pk, &anchored(&pk));
2978        assert!(!v.ok);
2979        assert!(v.entries.iter().all(|e| {
2980            e.problem
2981                .as_deref()
2982                .is_some_and(|p| p.contains("more than once"))
2983        }));
2984    }
2985
2986    #[test]
2987    fn a_second_compromise_cannot_anchor_inside_the_first_window() {
2988        let (k1, k1_pk) = generate_keypair();
2989        let (k2, k2_pk) = generate_keypair();
2990        let (k3, k3_pk) = generate_keypair();
2991        let anchor =
2992            anchored(&k1_pk).with((&k2_pk).into()).with((&k3_pk).into());
2993        let mut c = Chain::new();
2994        c.decision(&k1); // 1 — trusted
2995        c.decision(&k1); // 2 — inside the first window
2996        let first = KeyRotation::compromise(
2997            (&k1_pk).into(),
2998            &k2,
2999            TrustedHead {
3000                id: gov(1),
3001                chain_seq: 1,
3002                entry_hash: c.hash_at(1),
3003            },
3004            c.prev_hash(),
3005            at(35),
3006            "",
3007        );
3008        c.rotate(&k2, &first); // 3
3009        let second = KeyRotation::compromise(
3010            (&k1_pk).into(),
3011            &k3,
3012            TrustedHead {
3013                id: gov(2),
3014                chain_seq: 2,
3015                entry_hash: c.hash_at(2),
3016            },
3017            c.prev_hash(),
3018            at(45),
3019            "",
3020        );
3021        c.rotate(&k3, &second); // 4
3022
3023        let v = verify_chain(&c.links, &k1_pk, &anchor);
3024        assert!(!v.ok);
3025        let p = v.entries[3].problem.as_deref().unwrap();
3026        assert!(p.contains("repudiated"), "{p}");
3027        assert_eq!(v.keys.len(), 2, "K1 and K2; K3 never took the chain");
3028        assert_eq!(v.keys[0].through_seq, Some(1));
3029    }
3030
3031    #[test]
3032    fn an_unanchored_compromise_fails_closed() {
3033        let (old, old_pk) = generate_keypair();
3034        let (new, _) = generate_keypair();
3035        let mut c = Chain::new();
3036        c.decision(&old);
3037        c.decision(&old);
3038        let rotation = KeyRotation::compromise(
3039            (&old_pk).into(),
3040            &new,
3041            TrustedHead {
3042                id: gov(1),
3043                chain_seq: 1,
3044                entry_hash: c.hash_at(1),
3045            },
3046            c.prev_hash(),
3047            at(45),
3048            "trust me",
3049        );
3050        c.rotate(&new, &rotation);
3051
3052        // Nothing in the verifier's world vouches for the new key, so the
3053        // declaration authenticates nothing.
3054        let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk));
3055        assert!(!v.ok, "{v:#?}");
3056        let p = v.entries[2].problem.as_deref().unwrap();
3057        assert!(p.contains("outside the trust anchor"), "{p}");
3058        assert!(!v.entries[2].signature_valid, "checked under the old key");
3059        assert!(v.repudiated.is_empty(), "and nothing is repudiated");
3060        assert_eq!(v.public_key, (&old_pk).into());
3061    }
3062
3063    /// The scenario the anchor exists for: a thief with the signing key
3064    /// rotates the chain onto their own, and the Steward answers with a
3065    /// compromise declaration from before the theft.
3066    #[test]
3067    fn a_thiefs_rotation_is_void_once_a_compromise_names_an_earlier_head() {
3068        let (steward, steward_pk) = generate_keypair();
3069        let (thief, thief_pk) = generate_keypair();
3070        let (recovery, recovery_pk) = generate_keypair();
3071        let anchor = anchored(&steward_pk).with((&recovery_pk).into());
3072
3073        let mut c = Chain::new();
3074        c.decision(&steward); // 1 — the last honest entry
3075        let stolen = KeyRotation::routine(
3076            (&steward_pk).into(),
3077            &thief,
3078            c.prev_hash(),
3079            at(25),
3080            "routine",
3081        );
3082        c.rotate(&steward, &stolen); // 2 — signed with the stolen key
3083        c.decision(&thief); // 3 — the thief's own decision
3084
3085        let declaration = KeyRotation::compromise(
3086            (&steward_pk).into(),
3087            &recovery,
3088            TrustedHead {
3089                id: gov(1),
3090                chain_seq: 1,
3091                entry_hash: c.hash_at(1),
3092            },
3093            c.prev_hash(),
3094            at(55),
3095            "key stolen; everything after GOV-2026-0001 is disclaimed",
3096        );
3097        c.rotate(&recovery, &declaration); // 4
3098
3099        let v = verify_chain(&c.links, &steward_pk, &anchor);
3100        assert!(v.ok, "{v:#?}");
3101        assert_eq!(v.repudiated, vec![key_id(1), gov(2)]);
3102        assert!(v.entries[1].repudiated && v.entries[2].repudiated);
3103        assert_eq!(
3104            v.unanchored_keys,
3105            vec![PublicKeyHex::from(&thief_pk)],
3106            "and the theft was visible as it happened"
3107        );
3108        assert_eq!(v.public_key, (&recovery_pk).into());
3109        assert_eq!(v.keys.len(), 2, "the thief's key is not part of history");
3110        assert_eq!(v.keys[0].public_key, (&steward_pk).into());
3111        assert_eq!(v.keys[0].status, KeyStatus::Compromised);
3112        assert_eq!(v.keys[1].public_key, (&recovery_pk).into());
3113    }
3114
3115    #[test]
3116    fn a_compromise_must_name_a_real_head_and_the_key_that_held_it() {
3117        let (old, old_pk) = generate_keypair();
3118        let (new, new_pk) = generate_keypair();
3119        let anchor = anchored(&old_pk).with((&new_pk).into());
3120        let head = |c: &Chain| TrustedHead {
3121            id: gov(1),
3122            chain_seq: 1,
3123            entry_hash: c.hash_at(1),
3124        };
3125
3126        // A head whose hash is not that entry's.
3127        let mut c = Chain::new();
3128        c.decision(&old);
3129        let mut trusted = head(&c);
3130        trusted.entry_hash = data_hash(&json!("nope"));
3131        let rotation = KeyRotation::compromise(
3132            (&old_pk).into(),
3133            &new,
3134            trusted,
3135            c.prev_hash(),
3136            at(45),
3137            "n",
3138        );
3139        c.rotate(&new, &rotation);
3140        let v = verify_chain(&c.links, &old_pk, &anchor);
3141        let p = v.entries[1].problem.as_deref().unwrap();
3142        assert!(p.contains("last_trusted"), "{p}");
3143
3144        // An old_key that was never in force.
3145        let (other, _) = generate_keypair();
3146        let mut c = Chain::new();
3147        c.decision(&old);
3148        let rotation = KeyRotation::compromise(
3149            (&other.verifying_key()).into(),
3150            &new,
3151            head(&c),
3152            c.prev_hash(),
3153            at(45),
3154            "n",
3155        );
3156        c.rotate(&new, &rotation);
3157        let v = verify_chain(&c.links, &old_pk, &anchor);
3158        let p = v.entries[1].problem.as_deref().unwrap();
3159        assert!(p.contains("old_key is not the key"), "{p}");
3160
3161        // A routine rotation that names one anyway.
3162        let mut c = Chain::new();
3163        c.decision(&old);
3164        let mut rotation = KeyRotation::routine(
3165            (&old_pk).into(),
3166            &new,
3167            c.prev_hash(),
3168            at(45),
3169            "n",
3170        );
3171        rotation.last_trusted = Some(head(&c));
3172        c.rotate(&old, &rotation);
3173        let v = verify_chain(&c.links, &old_pk, &anchor);
3174        let p = v.entries[1].problem.as_deref().unwrap();
3175        assert!(p.contains("must not name last_trusted"), "{p}");
3176    }
3177
3178    #[test]
3179    fn a_genesis_key_outside_the_anchor_is_reported_not_rejected() {
3180        let (key, pk) = generate_keypair();
3181        let c = chain(&key, 2);
3182        let v = verify_chain(&c, &pk, &KeyAnchor::default());
3183        assert!(v.ok, "{v:#?}");
3184        assert_eq!(v.unanchored_keys, vec![PublicKeyHex::from(&pk)]);
3185        assert_eq!(v.keys.len(), 1);
3186        assert_eq!(v.keys[0].status, KeyStatus::Active);
3187        assert_eq!(v.keys[0].from_seq, 1);
3188        assert_eq!(v.keys[0].through_seq, None);
3189    }
3190
3191    #[test]
3192    fn published_keys_are_curve_points_and_make_an_anchor() {
3193        let anchor = KeyAnchor::published();
3194        assert!(!anchor.is_empty());
3195        assert_eq!(anchor.keys().count(), PUBLISHED_KEYS.len());
3196        for published in PUBLISHED_KEYS {
3197            let key: PublicKeyHex = published.parse().unwrap();
3198            assert!(anchor.contains(&key));
3199            assert!(
3200                key.to_verifying_key().is_ok(),
3201                "{published} is not a valid Ed25519 public key"
3202            );
3203        }
3204        let (_, other) = generate_keypair();
3205        assert!(!anchor.contains(&(&other).into()));
3206        assert!(
3207            anchor
3208                .clone()
3209                .with((&other).into())
3210                .contains(&(&other).into())
3211        );
3212    }
3213
3214    // -- the wire --
3215
3216    #[test]
3217    fn amendments_and_rotations_round_trip_as_entry_data() {
3218        let (key, pk) = generate_keypair();
3219        let amendment = Amendment::new(
3220            gov(1),
3221            data_hash(&json!("x")),
3222            AmendmentKind::Superseded,
3223            "Art. VI § 2",
3224            "superseded by GOV-2026-0009",
3225        )
3226        .unwrap()
3227        .with_authority(gov(9))
3228        .with_rationale("the later decision covers the same subject");
3229        let value = serde_json::to_value(&amendment).unwrap();
3230        assert_eq!(value["kind"], "superseded");
3231        assert_eq!(value["agora_governance_amendment"], 1);
3232        assert!(value.get("redaction").is_none(), "{value}");
3233        assert_eq!(
3234            serde_json::from_value::<Amendment>(value).unwrap(),
3235            amendment
3236        );
3237
3238        let rotation = KeyRotation::compromise(
3239            (&pk).into(),
3240            &key,
3241            TrustedHead {
3242                id: gov(1),
3243                chain_seq: 1,
3244                entry_hash: data_hash(&json!("x")),
3245            },
3246            Some(data_hash(&json!("prev"))),
3247            at(0),
3248            "note",
3249        );
3250        let value = serde_json::to_value(&rotation).unwrap();
3251        assert_eq!(value["reason"], "compromise");
3252        assert_eq!(value["last_trusted"]["chain_seq"], 1);
3253        assert_eq!(
3254            serde_json::from_value::<KeyRotation>(value).unwrap(),
3255            rotation
3256        );
3257
3258        let notice = AmendmentNotice::new(amd(1), at(0), &amendment);
3259        let value = serde_json::to_value(&notice).unwrap();
3260        assert_eq!(value["id"], "AMD-2026-0001");
3261        assert_eq!(value["note"], "superseded by GOV-2026-0009");
3262        assert_eq!(
3263            serde_json::from_value::<AmendmentNotice>(value).unwrap(),
3264            notice
3265        );
3266    }
3267
3268    /// The verifier hashes the raw `data` value, so a field it has never
3269    /// heard of neither breaks it nor escapes the signature — and an
3270    /// amendment written without one verifies just the same.
3271    #[test]
3272    fn an_amendment_verifies_with_or_without_its_optional_fields() {
3273        let (key, pk) = generate_keypair();
3274        let mut c = Chain::new();
3275        let target = c.decision(&key);
3276        let bare = Amendment::new(
3277            target.clone(),
3278            c.hash_at(1),
3279            AmendmentKind::Correction,
3280            "clerical",
3281            "typo in the citation",
3282        )
3283        .unwrap();
3284        assert!(
3285            serde_json::to_value(&bare)
3286                .unwrap()
3287                .get("rationale")
3288                .is_none()
3289        );
3290        c.amend(&key, &bare);
3291        let full = bare.clone().with_rationale("at length: …");
3292        c.amend(&key, &full);
3293        // And a field from a future version of the shape.
3294        let mut future = serde_json::to_value(&full).unwrap();
3295        future["superseded_by_something_new"] = json!(["later"]);
3296        c.push(
3297            &key,
3298            amd(3),
3299            GovernanceLogEntryType::Amendment,
3300            future,
3301            true,
3302        );
3303
3304        let v = verify_chain(&c.links, &pk, &anchored(&pk));
3305        assert!(v.ok, "{v:#?}");
3306        assert_eq!(
3307            v.entries[0].amended_by,
3308            vec![amd(1), amd(2), amd(3)],
3309            "all three name the target"
3310        );
3311    }
3312
3313    /// Pre-0.26 JSON — no `data`, no `signed_by`, no repudiation — still
3314    /// parses, because every field added since is `#[serde(default)]`.
3315    #[test]
3316    fn pre_0_26_wire_still_deserializes() {
3317        let link: GovernanceChainLink = serde_json::from_value(json!({
3318            "id": "GOV-2026-0001",
3319            "entry_type": "council_decision",
3320            "created_at": "2023-11-14T22:13:20.123456Z",
3321            "attestation": {
3322                "envelope_version": 1,
3323                "chain_seq": 1,
3324                "prev_hash": null,
3325                "data_hash": "00".repeat(32),
3326                "entry_hash": "11".repeat(32),
3327                "signature": "22".repeat(64),
3328                "signed_at": "2023-11-14T22:13:21Z",
3329                "retroactive": false,
3330            },
3331        }))
3332        .unwrap();
3333        assert!(link.data.is_none());
3334        assert!(
3335            serde_json::to_value(&link).unwrap().get("data").is_none(),
3336            "and a link without data does not grow a null field"
3337        );
3338
3339        let verdict: EntryVerdict = serde_json::from_value(json!({
3340            "id": "GOV-2026-0001",
3341            "chain_seq": 1,
3342            "signature_valid": true,
3343            "link_valid": true,
3344            "content_matches": null,
3345            "retroactive": false,
3346            "out_of_order": false,
3347            "amended_by": [],
3348        }))
3349        .unwrap();
3350        assert!(!verdict.repudiated && !verdict.redacted);
3351        assert!(verdict.signed_by.is_none());
3352        assert!(verdict.reattested_by.is_empty());
3353
3354        let verification: GovernanceVerification =
3355            serde_json::from_value(json!({
3356                "public_key": "33".repeat(32),
3357                "ok": true,
3358                "head": "GOV-2026-0001",
3359                "entries": [],
3360            }))
3361            .unwrap();
3362        assert!(verification.keys.is_empty());
3363        assert!(verification.unanchored_keys.is_empty());
3364        assert!(verification.repudiated.is_empty());
3365    }
3366
3367    #[test]
3368    fn the_report_round_trips() {
3369        let (key, pk) = generate_keypair();
3370        let mut c = Chain::new();
3371        let target = c.decision(&key);
3372        let amendment = Amendment::new(
3373            target,
3374            c.hash_at(1),
3375            AmendmentKind::Overruled,
3376            "b",
3377            "n",
3378        )
3379        .unwrap();
3380        c.amend(&key, &amendment);
3381        let v = verify_chain(&c.links, &pk, &anchored(&pk));
3382        let text = serde_json::to_string(&v).unwrap();
3383        assert_eq!(
3384            serde_json::from_str::<GovernanceVerification>(&text).unwrap(),
3385            v
3386        );
3387
3388        let keys = GovernanceSigningKeys { keys: v.keys };
3389        let value = serde_json::to_value(&keys).unwrap();
3390        assert!(value["keys"].is_array(), "an object, not a bare array");
3391        assert_eq!(value["keys"][0]["status"], "active");
3392        assert_eq!(
3393            serde_json::from_value::<GovernanceSigningKeys>(value).unwrap(),
3394            keys
3395        );
3396    }
3397
3398    /// The envelope is version 1 and there is a live chain signed under it:
3399    /// these bytes and this hash are load-bearing, not a snapshot to
3400    /// re-bless when something changes them.
3401    #[test]
3402    fn envelope_v1_preimage_and_hash_are_pinned() {
3403        let envelope = Envelope::new(
3404            gov(6),
3405            GovernanceLogEntryType::CouncilDecision,
3406            at(0),
3407            Some(Sha256Hex::from([0x11; 32])),
3408            data_hash(&json!({"outcome": "approved", "title": "Ratification"})),
3409        );
3410        assert_eq!(
3411            String::from_utf8(envelope.preimage()).unwrap(),
3412            "{\"agora_governance_log\":1,\"id\":\"GOV-2026-0006\",\
3413             \"entry_type\":\"council_decision\",\
3414             \"created_at\":1700000000123456,\
3415             \"prev_hash\":\"1111111111111111111111111111111111111111111111111111111111111111\",\
3416             \"data_hash\":\"a4adf645ae3f60c56484d01aea87d6d490321d7fc66b1607df14b023fe567c7b\"}"
3417        );
3418        assert_eq!(
3419            envelope.entry_hash().to_hex(),
3420            "ba27577432f81e415f1c01cc4cfabab6070e3ac50fd468fffe195ef19c0e9464"
3421        );
3422    }
3423
3424    /// The parity check the Steward's second channel exists for: the key
3425    /// the platform serves must be the newest key this build trusts.
3426    ///
3427    /// Networked, so it is `#[ignore]`d and CI runs it as its own job —
3428    /// a 5G blip should not read as a code failure. `just
3429    /// check-published-keys`.
3430    #[cfg(feature = "agora-client")]
3431    #[tokio::test]
3432    #[ignore = "networked: hits the live platform"]
3433    async fn the_published_key_is_the_one_the_platform_serves() {
3434        let client = crate::client::Client::new(
3435            url::Url::parse("https://subliminal.technology").unwrap(),
3436        )
3437        .unwrap();
3438        let served = client.get_governance_signing_key().await.unwrap();
3439        let newest: PublicKeyHex = PUBLISHED_KEYS
3440            .last()
3441            .expect("PUBLISHED_KEYS is never empty")
3442            .parse()
3443            .unwrap();
3444        assert_eq!(served.algorithm, "ed25519");
3445        assert_eq!(
3446            served.public_key, newest,
3447            "the platform serves {} but the newest key in PUBLISHED_KEYS is \
3448             {newest} — if this is a rotation, agentkit publishes the new \
3449             key FIRST and the rotation entry second",
3450            served.public_key
3451        );
3452    }
3453
3454    #[cfg(feature = "schemars")]
3455    #[test]
3456    fn wire_schemas_are_ref_free() {
3457        use crate::responses::inline_schema_for;
3458        for (name, schema) in [
3459            (
3460                "GovernanceAttestation",
3461                inline_schema_for::<GovernanceAttestation>(),
3462            ),
3463            (
3464                "GovernanceChainLink",
3465                inline_schema_for::<GovernanceChainLink>(),
3466            ),
3467            (
3468                "GovernanceSigningKey",
3469                inline_schema_for::<GovernanceSigningKey>(),
3470            ),
3471            (
3472                "GovernanceVerification",
3473                inline_schema_for::<GovernanceVerification>(),
3474            ),
3475            (
3476                "Vec<GovernanceChainLink>",
3477                inline_schema_for::<Vec<GovernanceChainLink>>(),
3478            ),
3479            ("Amendment", inline_schema_for::<Amendment>()),
3480            ("Redaction", inline_schema_for::<Redaction>()),
3481            ("AmendmentNotice", inline_schema_for::<AmendmentNotice>()),
3482            ("KeyRotation", inline_schema_for::<KeyRotation>()),
3483            ("TrustedHead", inline_schema_for::<TrustedHead>()),
3484            (
3485                "GovernanceKeyRecord",
3486                inline_schema_for::<GovernanceKeyRecord>(),
3487            ),
3488            (
3489                "GovernanceSigningKeys",
3490                inline_schema_for::<GovernanceSigningKeys>(),
3491            ),
3492            ("AmendmentKind", inline_schema_for::<AmendmentKind>()),
3493            ("Standing", inline_schema_for::<Standing>()),
3494            ("KeyStatus", inline_schema_for::<KeyStatus>()),
3495            ("RotationReason", inline_schema_for::<RotationReason>()),
3496        ] {
3497            let text = serde_json::to_string(&schema).unwrap();
3498            assert!(!text.contains("$ref"), "{name} must be $ref-free: {text}");
3499            assert!(
3500                !text.contains("$defs"),
3501                "{name} must be $defs-free: {text}"
3502            );
3503        }
3504        let text =
3505            serde_json::to_string(&inline_schema_for::<Sha256Hex>()).unwrap();
3506        assert!(text.contains("^[0-9a-f]{64}$"), "{text}");
3507        let text = serde_json::to_string(&inline_schema_for::<SignatureHex>())
3508            .unwrap();
3509        assert!(text.contains("^[0-9a-f]{128}$"), "{text}");
3510    }
3511}