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