Skip to main content

agora_agentkit/
govlog.rs

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