1use crate::crypto::{self, Signature, SigningKey, VerifyingKey};
43use crate::enums::GovernanceLogEntryType;
44use crate::ids::{GovernanceLogId, GovernanceLogPrefix};
45use chrono::{DateTime, Utc};
46use serde::{Deserialize, Serialize};
47use sha2::{Digest, Sha256};
48use std::collections::{HashMap, HashSet};
49
50pub use crate::enums::{AmendmentKind, KeyStatus, Standing};
51
52#[cfg(test)]
54mod vectors;
55
56pub const ENVELOPE_VERSION: u32 = 1;
58
59pub const RETROACTIVE_AFTER: chrono::Duration = chrono::Duration::seconds(60);
62
63#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
70#[error("{type_name}: expected {expected} bytes of hex, got {got:?}")]
71pub struct HexLengthError {
72 pub type_name: &'static str,
73 pub expected: usize,
74 pub got: String,
75}
76
77macro_rules! hex_bytes {
78 ($(#[$meta:meta])* $name:ident, $len:expr) => {
79 $(#[$meta])*
80 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
81 #[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
82 #[cfg_attr(feature = "sqlx", sqlx(transparent))]
83 pub struct $name([u8; $len]);
84
85 impl $name {
86 pub fn as_bytes(&self) -> &[u8; $len] {
88 &self.0
89 }
90
91 pub fn to_hex(&self) -> String {
93 hex::encode(self.0)
94 }
95 }
96
97 impl From<[u8; $len]> for $name {
98 fn from(bytes: [u8; $len]) -> Self {
99 Self(bytes)
100 }
101 }
102
103 impl TryFrom<&[u8]> for $name {
104 type Error = HexLengthError;
105
106 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
107 <[u8; $len]>::try_from(bytes).map(Self).map_err(|_| {
108 HexLengthError {
109 type_name: stringify!($name),
110 expected: $len,
111 got: hex::encode(bytes),
112 }
113 })
114 }
115 }
116
117 impl TryFrom<Vec<u8>> for $name {
118 type Error = HexLengthError;
119
120 fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
121 Self::try_from(bytes.as_slice())
122 }
123 }
124
125 impl std::str::FromStr for $name {
126 type Err = HexLengthError;
127
128 fn from_str(s: &str) -> Result<Self, Self::Err> {
129 let bytes = hex::decode(s.trim()).map_err(|_| HexLengthError {
130 type_name: stringify!($name),
131 expected: $len,
132 got: s.to_string(),
133 })?;
134 Self::try_from(bytes.as_slice())
135 }
136 }
137
138 impl std::fmt::Display for $name {
139 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
140 f.write_str(&self.to_hex())
141 }
142 }
143
144 impl std::fmt::Debug for $name {
145 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
146 write!(f, "{}({})", stringify!($name), self.to_hex())
147 }
148 }
149
150 impl Serialize for $name {
151 fn serialize<S: serde::Serializer>(
152 &self,
153 s: S,
154 ) -> Result<S::Ok, S::Error> {
155 s.serialize_str(&self.to_hex())
156 }
157 }
158
159 impl<'de> Deserialize<'de> for $name {
160 fn deserialize<D: serde::Deserializer<'de>>(
161 d: D,
162 ) -> Result<Self, D::Error> {
163 let s = String::deserialize(d)?;
164 s.parse().map_err(serde::de::Error::custom)
165 }
166 }
167
168 #[cfg(feature = "schemars")]
172 impl schemars::JsonSchema for $name {
173 fn inline_schema() -> bool {
174 true
175 }
176
177 fn schema_name() -> std::borrow::Cow<'static, str> {
178 std::borrow::Cow::Borrowed(stringify!($name))
179 }
180
181 fn schema_id() -> std::borrow::Cow<'static, str> {
182 std::borrow::Cow::Borrowed(concat!(
183 module_path!(),
184 "::",
185 stringify!($name)
186 ))
187 }
188
189 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
190 schemars::json_schema!({
191 "type": "string",
192 "pattern": format!("^[0-9a-f]{{{}}}$", $len * 2),
193 "description": format!("{} bytes, lowercase hex", $len),
194 })
195 }
196 }
197 };
198}
199
200hex_bytes!(
201 Sha256Hex,
203 32
204);
205
206hex_bytes!(
207 SignatureHex,
209 64
210);
211
212hex_bytes!(
213 PublicKeyHex,
215 32
216);
217
218hex_bytes!(
219 Blind,
222 32
223);
224
225impl Blind {
226 pub fn random() -> Self {
228 use rand::RngCore;
229 let mut bytes = [0u8; 32];
230 rand::rngs::OsRng.fill_bytes(&mut bytes);
231 Self(bytes)
232 }
233}
234
235impl From<Signature> for SignatureHex {
236 fn from(sig: Signature) -> Self {
237 Self(sig.to_bytes())
238 }
239}
240
241impl From<&SignatureHex> for Signature {
242 fn from(sig: &SignatureHex) -> Self {
243 Signature::from_bytes(&sig.0)
244 }
245}
246
247impl From<&VerifyingKey> for PublicKeyHex {
248 fn from(key: &VerifyingKey) -> Self {
249 Self(key.to_bytes())
250 }
251}
252
253impl PublicKeyHex {
254 pub fn to_verifying_key(
256 &self,
257 ) -> Result<VerifyingKey, ed25519_dalek::SignatureError> {
258 VerifyingKey::from_bytes(&self.0)
259 }
260}
261
262pub fn canonical_json(value: &serde_json::Value) -> Vec<u8> {
275 let mut out = Vec::new();
276 write_canonical(value, &mut out);
277 out
278}
279
280fn write_canonical(value: &serde_json::Value, out: &mut Vec<u8>) {
281 use serde_json::Value;
282 match value {
283 Value::Null => out.extend_from_slice(b"null"),
284 Value::Bool(b) => {
285 out.extend_from_slice(if *b { b"true" } else { b"false" })
286 }
287 Value::Number(n) => serde_json::to_writer(&mut *out, n)
288 .expect("a number always serializes"),
289 Value::String(s) => serde_json::to_writer(&mut *out, s)
290 .expect("a string always serializes"),
291 Value::Array(items) => {
292 out.push(b'[');
293 for (i, item) in items.iter().enumerate() {
294 if i > 0 {
295 out.push(b',');
296 }
297 write_canonical(item, out);
298 }
299 out.push(b']');
300 }
301 Value::Object(map) => {
302 let mut keys: Vec<&String> = map.keys().collect();
303 keys.sort_unstable();
304 out.push(b'{');
305 for (i, key) in keys.into_iter().enumerate() {
306 if i > 0 {
307 out.push(b',');
308 }
309 serde_json::to_writer(&mut *out, key)
310 .expect("a string always serializes");
311 out.push(b':');
312 write_canonical(&map[key], out);
313 }
314 out.push(b'}');
315 }
316 }
317}
318
319pub fn data_hash(data: &serde_json::Value) -> Sha256Hex {
321 Sha256Hex(Sha256::digest(canonical_json(data)).into())
322}
323
324#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
329pub struct Envelope {
330 pub agora_governance_log: u32,
332 pub id: GovernanceLogId,
333 pub entry_type: GovernanceLogEntryType,
334 pub created_at: i64,
336 pub prev_hash: Option<Sha256Hex>,
337 pub data_hash: Sha256Hex,
338}
339
340impl Envelope {
341 pub fn new(
343 id: GovernanceLogId,
344 entry_type: GovernanceLogEntryType,
345 created_at: DateTime<Utc>,
346 prev_hash: Option<Sha256Hex>,
347 data_hash: Sha256Hex,
348 ) -> Self {
349 Self {
350 agora_governance_log: ENVELOPE_VERSION,
351 id,
352 entry_type,
353 created_at: created_at.timestamp_micros(),
354 prev_hash,
355 data_hash,
356 }
357 }
358
359 pub fn created_at(&self) -> DateTime<Utc> {
361 DateTime::from_timestamp_micros(self.created_at)
362 .expect("an Envelope only ever holds an in-range timestamp")
363 }
364
365 pub fn preimage(&self) -> Vec<u8> {
367 serde_json::to_vec(self).expect("an Envelope always serializes")
368 }
369
370 pub fn entry_hash(&self) -> Sha256Hex {
372 Sha256Hex(Sha256::digest(self.preimage()).into())
373 }
374}
375
376pub fn truncate_to_micros(t: DateTime<Utc>) -> DateTime<Utc> {
379 DateTime::from_timestamp_micros(t.timestamp_micros())
380 .expect("a timestamp that came from a DateTime is in range")
381}
382
383pub fn truncate_to_seconds(t: DateTime<Utc>) -> DateTime<Utc> {
386 DateTime::from_timestamp(t.timestamp(), 0)
387 .expect("a timestamp that came from a DateTime is in range")
388}
389
390pub fn is_retroactive(
394 created_at: DateTime<Utc>,
395 signed_at: DateTime<Utc>,
396) -> bool {
397 signed_at - created_at > RETROACTIVE_AFTER
398}
399
400#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
406#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
407#[cfg_attr(feature = "schemars", schemars(inline))]
408pub struct GovernanceAttestation {
409 pub envelope_version: u32,
411 pub chain_seq: u64,
414 pub prev_hash: Option<Sha256Hex>,
416 pub data_hash: Sha256Hex,
418 pub entry_hash: Sha256Hex,
420 pub signature: SignatureHex,
423 pub signed_at: DateTime<Utc>,
426 pub retroactive: bool,
430}
431
432#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
436#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
437#[cfg_attr(feature = "schemars", schemars(inline))]
438pub struct GovernanceChainLink {
439 pub id: GovernanceLogId,
440 pub entry_type: GovernanceLogEntryType,
441 pub created_at: DateTime<Utc>,
442 pub attestation: GovernanceAttestation,
443 #[serde(default, skip_serializing_if = "Option::is_none")]
450 pub data: Option<serde_json::Value>,
451}
452
453#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
456#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
457#[cfg_attr(feature = "schemars", schemars(inline))]
458pub struct GovernanceSigningKey {
459 pub algorithm: String,
461 pub public_key: PublicKeyHex,
462 pub envelope_version: u32,
464}
465
466impl GovernanceSigningKey {
467 pub fn new(key: &VerifyingKey) -> Self {
469 Self {
470 algorithm: "ed25519".to_string(),
471 public_key: key.into(),
472 envelope_version: ENVELOPE_VERSION,
473 }
474 }
475}
476
477pub const AMENDMENT_VERSION: u32 = 1;
483
484#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
486pub enum AmendmentError {
487 #[error("agora_governance_amendment is {0}, not {AMENDMENT_VERSION}")]
488 UnsupportedVersion(u32),
489 #[error("kind `redaction` requires a `redaction`")]
490 MissingRedaction,
491 #[error("`redaction` is only valid on kind `redaction`")]
492 UnexpectedRedaction,
493 #[error("amendment target {0} is not an entry of this chain")]
494 UnknownTarget(GovernanceLogId),
495 #[error("amendment target {0} is not an earlier entry")]
496 ForwardReference(GovernanceLogId),
497 #[error("target_entry_hash is not {0}'s entry_hash")]
498 WrongTargetHash(GovernanceLogId),
499}
500
501#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
507#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
508#[cfg_attr(feature = "schemars", schemars(inline))]
509pub struct Amendment {
510 pub agora_governance_amendment: u32,
512 pub target: GovernanceLogId,
513 pub target_entry_hash: Sha256Hex,
515 pub kind: AmendmentKind,
516 #[serde(default)]
519 pub authority: Option<GovernanceLogId>,
520 pub basis: String,
523 pub note: String,
525 #[serde(default, skip_serializing_if = "Option::is_none")]
528 pub rationale: Option<String>,
529 #[serde(default, skip_serializing_if = "Option::is_none")]
531 pub redaction: Option<Redaction>,
532}
533
534#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
536#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
537#[cfg_attr(feature = "schemars", schemars(inline))]
538pub struct Redaction {
539 pub fields: Vec<String>,
542 pub resulting_data_hash: Sha256Hex,
545}
546
547impl Amendment {
548 pub fn new(
554 target: GovernanceLogId,
555 target_entry_hash: Sha256Hex,
556 kind: AmendmentKind,
557 basis: impl Into<String>,
558 note: impl Into<String>,
559 ) -> Result<Self, AmendmentError> {
560 if kind == AmendmentKind::Redaction {
561 return Err(AmendmentError::MissingRedaction);
562 }
563 Ok(Self {
564 agora_governance_amendment: AMENDMENT_VERSION,
565 target,
566 target_entry_hash,
567 kind,
568 authority: None,
569 basis: basis.into(),
570 note: note.into(),
571 rationale: None,
572 redaction: None,
573 })
574 }
575
576 pub fn with_authority(mut self, authority: GovernanceLogId) -> Self {
578 self.authority = Some(authority);
579 self
580 }
581
582 pub fn with_rationale(mut self, rationale: impl Into<String>) -> Self {
584 self.rationale = Some(rationale.into());
585 self
586 }
587
588 #[allow(clippy::too_many_arguments)]
598 pub fn redaction(
599 amendment_id: &GovernanceLogId,
600 target: GovernanceLogId,
601 target_entry_hash: Sha256Hex,
602 basis: impl Into<String>,
603 note: impl Into<String>,
604 fields: Vec<String>,
605 data: &serde_json::Value,
606 blind: Blind,
607 ) -> Result<(Self, serde_json::Value), RedactError> {
608 let redacted = redact_data(data, &fields, amendment_id, blind)?;
609 Ok((
610 Self {
611 agora_governance_amendment: AMENDMENT_VERSION,
612 target,
613 target_entry_hash,
614 kind: AmendmentKind::Redaction,
615 authority: None,
616 basis: basis.into(),
617 note: note.into(),
618 rationale: None,
619 redaction: Some(Redaction {
620 fields,
621 resulting_data_hash: data_hash(&redacted),
622 }),
623 },
624 redacted,
625 ))
626 }
627
628 pub fn validate(&self) -> Result<(), AmendmentError> {
631 if self.agora_governance_amendment != AMENDMENT_VERSION {
632 return Err(AmendmentError::UnsupportedVersion(
633 self.agora_governance_amendment,
634 ));
635 }
636 match (self.kind, &self.redaction) {
637 (AmendmentKind::Redaction, None) => {
638 Err(AmendmentError::MissingRedaction)
639 }
640 (k, Some(_)) if k != AmendmentKind::Redaction => {
641 Err(AmendmentError::UnexpectedRedaction)
642 }
643 _ => Ok(()),
644 }
645 }
646}
647
648pub fn kind_standing(kind: AmendmentKind) -> Option<Standing> {
651 match kind {
652 AmendmentKind::NonPrecedential => Some(Standing::NonPrecedential),
653 AmendmentKind::Overruled => Some(Standing::Overruled),
654 AmendmentKind::Superseded => Some(Standing::Superseded),
655 AmendmentKind::Reinstated => Some(Standing::InForce),
656 AmendmentKind::Correction
657 | AmendmentKind::Redaction
658 | AmendmentKind::Reattested => None,
659 }
660}
661
662pub fn standing(kinds: impl IntoIterator<Item = AmendmentKind>) -> Standing {
665 kinds
666 .into_iter()
667 .filter_map(kind_standing)
668 .last()
669 .unwrap_or_default()
670}
671
672pub const BLIND_KEY: &str = "_blind";
675
676pub fn is_redactable(entry_type: GovernanceLogEntryType) -> bool {
680 !matches!(
681 entry_type,
682 GovernanceLogEntryType::Amendment | GovernanceLogEntryType::KeyRotation
683 )
684}
685
686#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
688pub enum BlindError {
689 #[error("a redactable entry's data must be a JSON object")]
690 NotAnObject,
691 #[error(
692 "data already has a {BLIND_KEY:?} key; the writer supplies it, not the caller"
693 )]
694 AlreadyBlinded,
695}
696
697pub fn blind_data(
715 data: &serde_json::Value,
716 blind: Blind,
717) -> Result<serde_json::Value, BlindError> {
718 let mut out = data.clone();
719 let object = out.as_object_mut().ok_or(BlindError::NotAnObject)?;
720 if object.contains_key(BLIND_KEY) {
721 return Err(BlindError::AlreadyBlinded);
722 }
723 object.insert(BLIND_KEY.to_string(), blind.to_hex().into());
724 Ok(out)
725}
726
727#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
729pub enum RedactError {
730 #[error("pointer {0:?} does not resolve in the entry's data")]
731 Unresolved(String),
732 #[error("the empty pointer would redact the whole entry")]
733 WholeEntry,
734 #[error("a redaction names at least one pointer")]
735 NoFields,
736 #[error("{0:?} is the entry's blind; every redaction replaces it already")]
737 BlindPointer(String),
738}
739
740pub fn redaction_marker(amendment_id: &GovernanceLogId) -> String {
742 format!("[redacted by {amendment_id}]")
743}
744
745pub fn redact_data(
761 data: &serde_json::Value,
762 fields: &[String],
763 amendment_id: &GovernanceLogId,
764 blind: Blind,
765) -> Result<serde_json::Value, RedactError> {
766 if fields.is_empty() {
767 return Err(RedactError::NoFields);
768 }
769 let blind_pointer = format!("/{BLIND_KEY}");
770 let marker = serde_json::Value::String(redaction_marker(amendment_id));
771 let mut out = data.clone();
772 for pointer in fields {
773 if pointer.is_empty() {
774 return Err(RedactError::WholeEntry);
775 }
776 if *pointer == blind_pointer {
777 return Err(RedactError::BlindPointer(pointer.clone()));
778 }
779 let slot = out
780 .pointer_mut(pointer)
781 .ok_or_else(|| RedactError::Unresolved(pointer.clone()))?;
782 *slot = marker.clone();
783 }
784 if let Some(object) = out.as_object_mut() {
787 object.insert(BLIND_KEY.to_string(), blind.to_hex().into());
788 }
789 Ok(out)
790}
791
792#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
794#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
795#[cfg_attr(feature = "schemars", schemars(inline))]
796pub struct AmendmentNotice {
797 pub id: GovernanceLogId,
798 pub kind: AmendmentKind,
799 #[serde(default)]
800 pub authority: Option<GovernanceLogId>,
801 pub basis: String,
802 pub note: String,
803 #[serde(default, skip_serializing_if = "Option::is_none")]
805 pub rationale: Option<String>,
806 pub created_at: DateTime<Utc>,
807}
808
809impl AmendmentNotice {
810 pub fn new(
812 id: GovernanceLogId,
813 created_at: DateTime<Utc>,
814 amendment: &Amendment,
815 ) -> Self {
816 Self {
817 id,
818 kind: amendment.kind,
819 authority: amendment.authority.clone(),
820 basis: amendment.basis.clone(),
821 note: amendment.note.clone(),
822 rationale: amendment.rationale.clone(),
823 created_at,
824 }
825 }
826}
827
828pub const KEY_ROTATION_VERSION: u32 = 1;
834
835pub const PUBLISHED_KEYS: &[&str] =
845 &["ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a"];
846
847#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
849#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
850#[cfg_attr(feature = "schemars", schemars(inline))]
851#[serde(rename_all = "snake_case")]
852pub enum RotationReason {
853 Routine,
855 Compromise,
858}
859
860#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
862#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
863#[cfg_attr(feature = "schemars", schemars(inline))]
864pub struct TrustedHead {
865 pub id: GovernanceLogId,
866 pub chain_seq: u64,
867 pub entry_hash: Sha256Hex,
868}
869
870#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
872pub enum RotationError {
873 #[error("agora_governance_key_rotation is {0}, not {KEY_ROTATION_VERSION}")]
874 UnsupportedVersion(u32),
875 #[error("new_key is not a valid Ed25519 public key")]
876 BadNewKey,
877 #[error(
878 "the proof of possession does not verify for this rotation at this position"
879 )]
880 BadProof,
881 #[error("a compromise rotation must name last_trusted")]
882 MissingLastTrusted,
883 #[error("a routine rotation must not name last_trusted")]
884 UnexpectedLastTrusted,
885 #[error("old_key is not the key that was in force")]
886 WrongOldKey,
887 #[error("last_trusted does not name an earlier entry of this chain")]
888 UnknownLastTrusted,
889 #[error(
890 "a compromise rotation to a key outside the trust anchor authenticates nothing"
891 )]
892 UnanchoredNewKey,
893 #[error("new_key has already held this chain; a key is never brought back")]
894 ReusedKey,
895 #[error("last_trusted names an entry an earlier compromise repudiated")]
896 RepudiatedLastTrusted,
897}
898
899#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
906#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
907#[cfg_attr(feature = "schemars", schemars(inline))]
908pub struct KeyRotation {
909 pub agora_governance_key_rotation: u32,
911 pub reason: RotationReason,
912 pub old_key: PublicKeyHex,
913 pub new_key: PublicKeyHex,
914 pub proof: SignatureHex,
918 pub proof_signed_at: i64,
920 #[serde(default, skip_serializing_if = "Option::is_none")]
922 pub last_trusted: Option<TrustedHead>,
923 pub note: String,
924}
925
926#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
933pub struct RotationStatement {
934 pub agora_governance_key_rotation: u32,
936 pub reason: RotationReason,
937 pub old_key: PublicKeyHex,
938 pub new_key: PublicKeyHex,
939 pub prev_hash: Option<Sha256Hex>,
940}
941
942impl RotationStatement {
943 pub fn new(
945 reason: RotationReason,
946 old_key: PublicKeyHex,
947 new_key: PublicKeyHex,
948 prev_hash: Option<Sha256Hex>,
949 ) -> Self {
950 Self {
951 agora_governance_key_rotation: KEY_ROTATION_VERSION,
952 reason,
953 old_key,
954 new_key,
955 prev_hash,
956 }
957 }
958
959 pub fn preimage(&self) -> Vec<u8> {
961 serde_json::to_vec(self).expect("a RotationStatement always serializes")
962 }
963
964 pub fn hash(&self) -> Sha256Hex {
966 Sha256Hex(Sha256::digest(self.preimage()).into())
967 }
968}
969
970impl KeyRotation {
971 pub fn routine(
976 old_key: PublicKeyHex,
977 new_signing_key: &SigningKey,
978 prev_hash: Option<Sha256Hex>,
979 now: DateTime<Utc>,
980 note: impl Into<String>,
981 ) -> Self {
982 Self::build(
983 RotationReason::Routine,
984 old_key,
985 new_signing_key,
986 None,
987 prev_hash,
988 now,
989 note,
990 )
991 }
992
993 pub fn compromise(
1002 old_key: PublicKeyHex,
1003 new_signing_key: &SigningKey,
1004 last_trusted: TrustedHead,
1005 prev_hash: Option<Sha256Hex>,
1006 now: DateTime<Utc>,
1007 note: impl Into<String>,
1008 ) -> Self {
1009 Self::build(
1010 RotationReason::Compromise,
1011 old_key,
1012 new_signing_key,
1013 Some(last_trusted),
1014 prev_hash,
1015 now,
1016 note,
1017 )
1018 }
1019
1020 #[allow(clippy::too_many_arguments)]
1021 fn build(
1022 reason: RotationReason,
1023 old_key: PublicKeyHex,
1024 new_signing_key: &SigningKey,
1025 last_trusted: Option<TrustedHead>,
1026 prev_hash: Option<Sha256Hex>,
1027 now: DateTime<Utc>,
1028 note: impl Into<String>,
1029 ) -> Self {
1030 let new_key = PublicKeyHex::from(&new_signing_key.verifying_key());
1031 let proof_signed_at = truncate_to_seconds(now).timestamp();
1032 let statement =
1033 RotationStatement::new(reason, old_key, new_key, prev_hash);
1034 let proof = crypto::sign(
1035 new_signing_key,
1036 statement.hash().as_bytes(),
1037 proof_signed_at,
1038 );
1039 Self {
1040 agora_governance_key_rotation: KEY_ROTATION_VERSION,
1041 reason,
1042 old_key,
1043 new_key,
1044 proof: proof.into(),
1045 proof_signed_at,
1046 last_trusted,
1047 note: note.into(),
1048 }
1049 }
1050
1051 pub fn statement(&self, prev_hash: Option<Sha256Hex>) -> RotationStatement {
1053 RotationStatement::new(
1054 self.reason,
1055 self.old_key,
1056 self.new_key,
1057 prev_hash,
1058 )
1059 }
1060
1061 pub fn verify_proof(
1065 &self,
1066 prev_hash: Option<Sha256Hex>,
1067 ) -> Result<(), RotationError> {
1068 if self.agora_governance_key_rotation != KEY_ROTATION_VERSION {
1069 return Err(RotationError::UnsupportedVersion(
1070 self.agora_governance_key_rotation,
1071 ));
1072 }
1073 match (self.reason, &self.last_trusted) {
1074 (RotationReason::Compromise, None) => {
1075 return Err(RotationError::MissingLastTrusted);
1076 }
1077 (RotationReason::Routine, Some(_)) => {
1078 return Err(RotationError::UnexpectedLastTrusted);
1079 }
1080 _ => {}
1081 }
1082 let new_key = self
1083 .new_key
1084 .to_verifying_key()
1085 .map_err(|_| RotationError::BadNewKey)?;
1086 crypto::verify(
1087 &new_key,
1088 self.statement(prev_hash).hash().as_bytes(),
1089 self.proof_signed_at,
1090 &Signature::from(&self.proof),
1091 )
1092 .then_some(())
1093 .ok_or(RotationError::BadProof)
1094 }
1095}
1096
1097#[derive(Debug, Clone, Default, PartialEq, Eq)]
1105pub struct KeyAnchor {
1106 keys: HashSet<PublicKeyHex>,
1107}
1108
1109impl KeyAnchor {
1110 pub fn published() -> Self {
1112 PUBLISHED_KEYS
1113 .iter()
1114 .map(|k| {
1115 k.parse()
1116 .expect("PUBLISHED_KEYS are valid 32-byte hex keys")
1117 })
1118 .collect()
1119 }
1120
1121 pub fn pinned(key: PublicKeyHex) -> Self {
1123 std::iter::once(key).collect()
1124 }
1125
1126 pub fn with(mut self, key: PublicKeyHex) -> Self {
1128 self.keys.insert(key);
1129 self
1130 }
1131
1132 pub fn contains(&self, key: &PublicKeyHex) -> bool {
1133 self.keys.contains(key)
1134 }
1135
1136 pub fn is_empty(&self) -> bool {
1137 self.keys.is_empty()
1138 }
1139
1140 pub fn keys(&self) -> impl Iterator<Item = &PublicKeyHex> {
1142 self.keys.iter()
1143 }
1144}
1145
1146impl FromIterator<PublicKeyHex> for KeyAnchor {
1147 fn from_iter<I: IntoIterator<Item = PublicKeyHex>>(iter: I) -> Self {
1148 Self {
1149 keys: iter.into_iter().collect(),
1150 }
1151 }
1152}
1153
1154#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1157#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1158#[cfg_attr(feature = "schemars", schemars(inline))]
1159pub struct GovernanceKeyRecord {
1160 pub public_key: PublicKeyHex,
1161 pub from_seq: u64,
1163 #[serde(default)]
1167 pub through_seq: Option<u64>,
1168 pub status: KeyStatus,
1169 #[serde(default)]
1172 pub introduced_by: Option<GovernanceLogId>,
1173 #[serde(default)]
1175 pub retired_by: Option<GovernanceLogId>,
1176}
1177
1178#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1183#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1184#[cfg_attr(feature = "schemars", schemars(inline))]
1185pub struct GovernanceSigningKeys {
1186 pub keys: Vec<GovernanceKeyRecord>,
1188}
1189
1190#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1192#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1193#[cfg_attr(feature = "schemars", schemars(inline))]
1194pub struct EntryVerdict {
1195 pub id: GovernanceLogId,
1196 pub chain_seq: u64,
1197 pub signature_valid: bool,
1200 pub link_valid: bool,
1203 #[serde(default)]
1209 pub content_matches: Option<bool>,
1210 pub retroactive: bool,
1212 pub out_of_order: bool,
1215 #[serde(default)]
1217 pub amended_by: Vec<GovernanceLogId>,
1218 #[serde(default, skip_serializing_if = "Option::is_none")]
1221 pub signed_by: Option<PublicKeyHex>,
1222 #[serde(default)]
1225 pub redacted: bool,
1226 #[serde(default, skip_serializing_if = "Option::is_none")]
1228 pub redacted_data_hash: Option<Sha256Hex>,
1229 #[serde(default)]
1232 pub repudiated: bool,
1233 #[serde(default)]
1236 pub reattested_by: Vec<GovernanceLogId>,
1237 #[serde(default, skip_serializing_if = "Option::is_none")]
1239 pub problem: Option<String>,
1240}
1241
1242#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1244#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1245#[cfg_attr(feature = "schemars", schemars(inline))]
1246pub struct GovernanceVerification {
1247 pub public_key: PublicKeyHex,
1249 pub ok: bool,
1255 #[serde(default)]
1257 pub head: Option<GovernanceLogId>,
1258 pub entries: Vec<EntryVerdict>,
1260 #[serde(default)]
1262 pub keys: Vec<GovernanceKeyRecord>,
1263 #[serde(default)]
1268 pub unanchored_keys: Vec<PublicKeyHex>,
1269 #[serde(default)]
1271 pub repudiated: Vec<GovernanceLogId>,
1272}
1273
1274impl GovernanceVerification {
1275 pub fn settle(mut self) -> Self {
1277 self.ok = self.entries.iter().all(|e| {
1278 e.signature_valid
1279 && e.link_valid
1280 && e.content_matches != Some(false)
1281 && e.problem.is_none()
1282 });
1283 self
1284 }
1285
1286 pub fn check_content(
1296 &mut self,
1297 link: &GovernanceChainLink,
1298 data: &serde_json::Value,
1299 ) -> bool {
1300 let hash = data_hash(data);
1301 let Some(entry) = self.entries.iter_mut().find(|e| e.id == link.id)
1302 else {
1303 return false;
1304 };
1305 let ok = hash == link.attestation.data_hash
1306 || entry.redacted_data_hash == Some(hash);
1307 entry.content_matches = Some(ok);
1308 ok
1309 }
1310}
1311
1312pub fn attest(
1323 key: &SigningKey,
1324 envelope: &Envelope,
1325 chain_seq: u64,
1326 signed_at: DateTime<Utc>,
1327) -> GovernanceAttestation {
1328 let signed_at = truncate_to_seconds(signed_at);
1329 let entry_hash = envelope.entry_hash();
1330 let signature =
1331 crypto::sign(key, entry_hash.as_bytes(), signed_at.timestamp());
1332 GovernanceAttestation {
1333 envelope_version: envelope.agora_governance_log,
1334 chain_seq,
1335 prev_hash: envelope.prev_hash,
1336 data_hash: envelope.data_hash,
1337 entry_hash,
1338 signature: signature.into(),
1339 signed_at,
1340 retroactive: is_retroactive(envelope.created_at(), signed_at),
1341 }
1342}
1343
1344#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1350pub enum LinkError {
1351 #[error(
1352 "envelope version {0} is not supported (this verifier knows {ENVELOPE_VERSION})"
1353 )]
1354 UnsupportedVersion(u32),
1355 #[error("entry_hash does not recompute from the envelope fields")]
1356 HashMismatch,
1357 #[error("signature does not verify under the published key")]
1358 BadSignature,
1359}
1360
1361pub fn recompute_entry_hash(link: &GovernanceChainLink) -> Sha256Hex {
1363 Envelope::new(
1364 link.id.clone(),
1365 link.entry_type,
1366 link.created_at,
1367 link.attestation.prev_hash,
1368 link.attestation.data_hash,
1369 )
1370 .entry_hash()
1371}
1372
1373pub fn verify_link(
1375 link: &GovernanceChainLink,
1376 key: &VerifyingKey,
1377) -> Result<(), LinkError> {
1378 let a = &link.attestation;
1379 if a.envelope_version != ENVELOPE_VERSION {
1380 return Err(LinkError::UnsupportedVersion(a.envelope_version));
1381 }
1382 if recompute_entry_hash(link) != a.entry_hash {
1383 return Err(LinkError::HashMismatch);
1384 }
1385 if !crypto::verify(
1386 key,
1387 a.entry_hash.as_bytes(),
1388 a.signed_at.timestamp(),
1389 &Signature::from(&a.signature),
1390 ) {
1391 return Err(LinkError::BadSignature);
1392 }
1393 Ok(())
1394}
1395
1396pub fn verify_data(
1398 link: &GovernanceChainLink,
1399 data: &serde_json::Value,
1400) -> bool {
1401 data_hash(data) == link.attestation.data_hash
1402}
1403
1404fn prefix_problem(link: &GovernanceChainLink) -> Option<String> {
1406 let prefix = link.id.prefix();
1407 let reserved =
1408 matches!(prefix, GovernanceLogPrefix::Amd | GovernanceLogPrefix::Key);
1409 let expected = match link.entry_type {
1410 GovernanceLogEntryType::Amendment => Some(GovernanceLogPrefix::Amd),
1411 GovernanceLogEntryType::KeyRotation => Some(GovernanceLogPrefix::Key),
1412 _ => None,
1413 };
1414 match expected {
1415 Some(want) if prefix != want => Some(format!(
1416 "the id of a {} entry must be in the {want}- series, not {}",
1417 link.entry_type, link.id
1418 )),
1419 None if reserved => Some(format!(
1420 "{prefix}- ids are reserved for amendment and key_rotation \
1421 entries, but {} is a {}",
1422 link.id, link.entry_type
1423 )),
1424 _ => None,
1425 }
1426}
1427
1428fn amendment_target(
1430 amendment: &Amendment,
1431 seq: u64,
1432 seq_of: &HashMap<&str, u64>,
1433 links: &[&GovernanceChainLink],
1434) -> Result<u64, AmendmentError> {
1435 amendment.validate()?;
1436 let target = *seq_of.get(amendment.target.as_str()).ok_or_else(|| {
1437 AmendmentError::UnknownTarget(amendment.target.clone())
1438 })?;
1439 if target >= seq {
1440 return Err(AmendmentError::ForwardReference(amendment.target.clone()));
1441 }
1442 if links[target as usize - 1].attestation.entry_hash
1443 != amendment.target_entry_hash
1444 {
1445 return Err(AmendmentError::WrongTargetHash(amendment.target.clone()));
1446 }
1447 Ok(target)
1448}
1449
1450struct KeyWalk {
1452 history: Vec<(GovernanceKeyRecord, VerifyingKey)>,
1454 unanchored: Vec<PublicKeyHex>,
1455 seen: HashSet<PublicKeyHex>,
1459 repudiated: HashSet<u64>,
1461}
1462
1463impl KeyWalk {
1464 fn new(genesis: &VerifyingKey, anchor: &KeyAnchor) -> Self {
1465 let public_key = PublicKeyHex::from(genesis);
1466 Self {
1467 history: vec![(
1468 GovernanceKeyRecord {
1469 public_key,
1470 from_seq: 1,
1471 through_seq: None,
1472 status: KeyStatus::Active,
1473 introduced_by: None,
1474 retired_by: None,
1475 },
1476 *genesis,
1477 )],
1478 unanchored: if anchor.contains(&public_key) {
1479 Vec::new()
1480 } else {
1481 vec![public_key]
1482 },
1483 seen: HashSet::from([public_key]),
1484 repudiated: HashSet::new(),
1485 }
1486 }
1487
1488 fn in_force(&self, seq: u64) -> (PublicKeyHex, VerifyingKey) {
1490 let (record, key) = self
1491 .history
1492 .iter()
1493 .rev()
1494 .find(|(r, _)| r.from_seq <= seq)
1495 .unwrap_or(&self.history[0]);
1496 (record.public_key, *key)
1497 }
1498
1499 fn active(&self) -> PublicKeyHex {
1501 self.history
1502 .last()
1503 .map(|(r, _)| r.public_key)
1504 .expect("the genesis key is always in the history")
1505 }
1506
1507 fn close(
1508 &mut self,
1509 through_seq: u64,
1510 status: KeyStatus,
1511 by: &GovernanceLogId,
1512 ) {
1513 if let Some((record, _)) = self.history.last_mut() {
1514 record.through_seq = Some(through_seq);
1515 record.status = status;
1516 record.retired_by = Some(by.clone());
1517 }
1518 }
1519
1520 fn open(
1521 &mut self,
1522 public_key: PublicKeyHex,
1523 key: VerifyingKey,
1524 from_seq: u64,
1525 by: &GovernanceLogId,
1526 ) {
1527 self.history.push((
1528 GovernanceKeyRecord {
1529 public_key,
1530 from_seq,
1531 through_seq: None,
1532 status: KeyStatus::Active,
1533 introduced_by: Some(by.clone()),
1534 retired_by: None,
1535 },
1536 key,
1537 ));
1538 }
1539
1540 fn apply(
1542 &mut self,
1543 rotation: &KeyRotation,
1544 link: &GovernanceChainLink,
1545 seq: u64,
1546 links: &[&GovernanceChainLink],
1547 anchor: &KeyAnchor,
1548 ) -> Result<(), RotationError> {
1549 rotation.verify_proof(link.attestation.prev_hash)?;
1550 let new_key = rotation
1551 .new_key
1552 .to_verifying_key()
1553 .map_err(|_| RotationError::BadNewKey)?;
1554 if self.seen.contains(&rotation.new_key) {
1555 return Err(RotationError::ReusedKey);
1556 }
1557 match rotation.reason {
1558 RotationReason::Routine => {
1559 if rotation.old_key != self.in_force(seq).0 {
1560 return Err(RotationError::WrongOldKey);
1561 }
1562 self.close(seq, KeyStatus::Retired, &link.id);
1563 self.open(rotation.new_key, new_key, seq + 1, &link.id);
1564 self.seen.insert(rotation.new_key);
1565 if !anchor.contains(&rotation.new_key) {
1566 self.unanchored.push(rotation.new_key);
1567 }
1568 Ok(())
1569 }
1570 RotationReason::Compromise => {
1571 let head = rotation
1572 .last_trusted
1573 .as_ref()
1574 .ok_or(RotationError::MissingLastTrusted)?;
1575 let trusted_seq = head.chain_seq;
1576 let names_an_earlier_entry = trusted_seq >= 1
1577 && trusted_seq < seq
1578 && links[trusted_seq as usize - 1].id == head.id
1579 && links[trusted_seq as usize - 1].attestation.entry_hash
1580 == head.entry_hash;
1581 if !names_an_earlier_entry {
1582 return Err(RotationError::UnknownLastTrusted);
1583 }
1584 if self.repudiated.contains(&trusted_seq) {
1587 return Err(RotationError::RepudiatedLastTrusted);
1588 }
1589 if rotation.old_key != self.in_force(trusted_seq).0 {
1592 return Err(RotationError::WrongOldKey);
1593 }
1594 if !anchor.contains(&rotation.new_key) {
1595 return Err(RotationError::UnanchoredNewKey);
1596 }
1597 self.history.retain(|(r, _)| r.from_seq <= trusted_seq);
1598 self.close(trusted_seq, KeyStatus::Compromised, &link.id);
1599 self.open(rotation.new_key, new_key, seq, &link.id);
1600 self.seen.insert(rotation.new_key);
1601 self.repudiated.extend(trusted_seq + 1..seq);
1602 Ok(())
1603 }
1604 }
1605 }
1606}
1607
1608pub fn verify_chain(
1631 links: &[GovernanceChainLink],
1632 genesis_key: &VerifyingKey,
1633 anchor: &KeyAnchor,
1634) -> GovernanceVerification {
1635 let mut links: Vec<&GovernanceChainLink> = links.iter().collect();
1636 links.sort_by_key(|l| l.attestation.chain_seq);
1637
1638 let mut seq_of: HashMap<&str, u64> = HashMap::new();
1639 let mut duplicates: HashSet<&str> = HashSet::new();
1640 for (i, link) in links.iter().enumerate() {
1641 if seq_of.insert(link.id.as_str(), i as u64 + 1).is_some() {
1642 duplicates.insert(link.id.as_str());
1643 }
1644 }
1645
1646 let mut walk = KeyWalk::new(genesis_key, anchor);
1647 let mut amendments: Vec<(u64, GovernanceLogId, Amendment, u64)> =
1649 Vec::new();
1650 let mut entries: Vec<EntryVerdict> = Vec::with_capacity(links.len());
1651 let mut prev: Option<&GovernanceChainLink> = None;
1652
1653 for (i, link) in links.iter().enumerate() {
1654 let a = &link.attestation;
1655 let expected_seq = i as u64 + 1;
1656 let mut problems: Vec<String> = Vec::new();
1657
1658 if let Some(problem) = prefix_problem(link) {
1659 problems.push(problem);
1660 }
1661 if duplicates.contains(link.id.as_str()) {
1662 problems.push(format!("{} appears more than once", link.id));
1663 }
1664
1665 let carries_meaning = matches!(
1669 link.entry_type,
1670 GovernanceLogEntryType::Amendment
1671 | GovernanceLogEntryType::KeyRotation
1672 );
1673 let mut amendment: Option<Amendment> = None;
1674 let mut rotation: Option<KeyRotation> = None;
1675 let mut content_matches: Option<bool> = None;
1676 match &link.data {
1677 Some(data) => {
1678 let matched = data_hash(data) == a.data_hash;
1679 content_matches = Some(matched);
1680 if !matched {
1681 if carries_meaning {
1682 problems.push(
1683 "`data` does not hash to the attested data_hash"
1684 .into(),
1685 );
1686 }
1687 } else {
1688 match link.entry_type {
1689 GovernanceLogEntryType::Amendment => {
1690 match serde_json::from_value(data.clone()) {
1691 Ok(v) => amendment = Some(v),
1692 Err(e) => problems.push(format!(
1693 "amendment `data` is malformed: {e}"
1694 )),
1695 }
1696 }
1697 GovernanceLogEntryType::KeyRotation => {
1698 match serde_json::from_value(data.clone()) {
1699 Ok(v) => rotation = Some(v),
1700 Err(e) => problems.push(format!(
1701 "key_rotation `data` is malformed: {e}"
1702 )),
1703 }
1704 }
1705 _ => {}
1706 }
1707 }
1708 }
1709 None if carries_meaning => problems.push(format!(
1710 "a {} entry must carry its `data`",
1711 link.entry_type
1712 )),
1713 None => {}
1714 }
1715
1716 let declared_key = rotation
1720 .as_ref()
1721 .filter(|r| {
1722 r.reason == RotationReason::Compromise
1723 && anchor.contains(&r.new_key)
1724 && !walk.seen.contains(&r.new_key)
1725 })
1726 .and_then(|r| r.new_key.to_verifying_key().ok());
1727 let (key_hex, key) = match declared_key {
1728 Some(k) => (PublicKeyHex::from(&k), k),
1729 None => walk.in_force(expected_seq),
1730 };
1731 if declared_key.is_none()
1734 && let Some(r) = rotation
1735 .as_ref()
1736 .filter(|r| r.reason == RotationReason::Compromise)
1737 {
1738 problems.push(
1739 if walk.seen.contains(&r.new_key) {
1740 RotationError::ReusedKey
1741 } else {
1742 RotationError::UnanchoredNewKey
1743 }
1744 .to_string(),
1745 );
1746 }
1747
1748 let (hash_ok, signature_valid) = match verify_link(link, &key) {
1749 Ok(()) => (true, true),
1750 Err(LinkError::BadSignature) => {
1751 problems.push(LinkError::BadSignature.to_string());
1752 (true, false)
1753 }
1754 Err(e) => {
1755 problems.push(e.to_string());
1756 (false, false)
1757 }
1758 };
1759
1760 let mut link_valid = hash_ok;
1761 if a.chain_seq != expected_seq {
1762 link_valid = false;
1763 problems.push(format!(
1764 "chain_seq {} where {expected_seq} was expected",
1765 a.chain_seq
1766 ));
1767 }
1768 let expected_prev = prev.map(|p| p.attestation.entry_hash);
1769 if a.prev_hash != expected_prev {
1770 link_valid = false;
1771 problems.push(match (a.prev_hash, expected_prev) {
1772 (Some(_), None) => "first entry names a predecessor".into(),
1773 (None, Some(_)) => "prev_hash is null mid-chain".into(),
1774 _ => "prev_hash is not the previous entry's entry_hash".into(),
1775 });
1776 }
1777 let out_of_order = prev.is_some_and(|p| link.created_at < p.created_at);
1778
1779 let authentic = signature_valid && link_valid;
1783 if let Some(rotation) = rotation.as_ref().filter(|_| authentic)
1784 && let Err(e) =
1785 walk.apply(rotation, link, expected_seq, &links, anchor)
1786 {
1787 problems.push(e.to_string());
1788 }
1789 if let Some(amendment) = amendment.filter(|_| authentic) {
1790 match amendment_target(&amendment, expected_seq, &seq_of, &links) {
1791 Ok(target) => amendments.push((
1792 expected_seq,
1793 link.id.clone(),
1794 amendment,
1795 target,
1796 )),
1797 Err(e) => problems.push(e.to_string()),
1798 }
1799 }
1800
1801 entries.push(EntryVerdict {
1802 id: link.id.clone(),
1803 chain_seq: a.chain_seq,
1804 signature_valid,
1805 link_valid,
1806 content_matches,
1807 retroactive: is_retroactive(link.created_at, a.signed_at),
1808 out_of_order,
1809 amended_by: Vec::new(),
1810 signed_by: Some(key_hex),
1811 redacted: false,
1812 redacted_data_hash: None,
1813 repudiated: false,
1814 reattested_by: Vec::new(),
1815 problem: (!problems.is_empty()).then(|| problems.join("; ")),
1816 });
1817 prev = Some(link);
1818 }
1819
1820 let mut applied = vec![false; amendments.len()];
1824 loop {
1825 let mut changed = false;
1826 for (i, (seq, id, amendment, target)) in amendments.iter().enumerate() {
1827 if applied[i]
1828 || amendment.kind != AmendmentKind::Reattested
1829 || walk.repudiated.contains(seq)
1830 {
1831 continue;
1832 }
1833 applied[i] = true;
1834 entries[*target as usize - 1].reattested_by.push(id.clone());
1835 walk.repudiated.remove(target);
1836 changed = true;
1837 }
1838 if !changed {
1839 break;
1840 }
1841 }
1842
1843 for (seq, id, amendment, target) in &amendments {
1844 if walk.repudiated.contains(seq) {
1845 continue;
1846 }
1847 let entry = &mut entries[*target as usize - 1];
1848 entry.amended_by.push(id.clone());
1849 if let Some(redaction) = &amendment.redaction {
1850 entry.redacted = true;
1851 entry.redacted_data_hash = Some(redaction.resulting_data_hash);
1852 }
1853 }
1854
1855 for (i, link) in links.iter().enumerate() {
1857 if entries[i].content_matches == Some(false)
1858 && let (Some(data), Some(hash)) =
1859 (&link.data, entries[i].redacted_data_hash)
1860 && data_hash(data) == hash
1861 {
1862 entries[i].content_matches = Some(true);
1863 }
1864 }
1865
1866 let mut seen = HashSet::new();
1867 walk.unanchored.retain(|key| seen.insert(*key));
1868
1869 let mut repudiated = Vec::new();
1870 for (i, entry) in entries.iter_mut().enumerate() {
1871 if walk.repudiated.contains(&(i as u64 + 1)) {
1872 entry.repudiated = true;
1873 repudiated.push(entry.id.clone());
1874 }
1875 }
1876
1877 GovernanceVerification {
1878 public_key: walk.active(),
1879 ok: false,
1880 head: prev.map(|p| p.id.clone()),
1881 entries,
1882 keys: walk.history.into_iter().map(|(record, _)| record).collect(),
1883 unanchored_keys: walk.unanchored,
1884 repudiated,
1885 }
1886 .settle()
1887}
1888
1889#[cfg(test)]
1890mod tests {
1891 use super::*;
1892 use crate::crypto::generate_keypair;
1893 use serde_json::json;
1894
1895 pub(super) fn gov(n: u32) -> GovernanceLogId {
1896 format!("GOV-2026-{n:04}").parse().unwrap()
1897 }
1898
1899 pub(super) fn amd(n: u32) -> GovernanceLogId {
1900 format!("AMD-2026-{n:04}").parse().unwrap()
1901 }
1902
1903 pub(super) fn key_id(n: u32) -> GovernanceLogId {
1904 format!("KEY-2026-{n:04}").parse().unwrap()
1905 }
1906
1907 pub(super) fn at(secs: i64) -> DateTime<Utc> {
1908 DateTime::from_timestamp(1_700_000_000 + secs, 123_456_789).unwrap()
1909 }
1910
1911 fn anchored(key: &VerifyingKey) -> KeyAnchor {
1913 KeyAnchor::pinned(key.into())
1914 }
1915
1916 pub(super) fn link(
1917 key: &SigningKey,
1918 n: u32,
1919 prev: Option<&GovernanceChainLink>,
1920 data: &serde_json::Value,
1921 signed_at: DateTime<Utc>,
1922 ) -> GovernanceChainLink {
1923 let created_at = truncate_to_micros(at(n as i64 * 10));
1924 let envelope = Envelope::new(
1925 gov(n),
1926 GovernanceLogEntryType::CouncilDecision,
1927 created_at,
1928 prev.map(|p| p.attestation.entry_hash),
1929 data_hash(data),
1930 );
1931 let attestation = attest(
1932 key,
1933 &envelope,
1934 prev.map_or(1, |p| p.attestation.chain_seq + 1),
1935 signed_at,
1936 );
1937 GovernanceChainLink {
1938 id: gov(n),
1939 entry_type: GovernanceLogEntryType::CouncilDecision,
1940 created_at,
1941 attestation,
1942 data: None,
1943 }
1944 }
1945
1946 pub(super) fn chain(key: &SigningKey, n: u32) -> Vec<GovernanceChainLink> {
1947 let mut out: Vec<GovernanceChainLink> = Vec::new();
1948 for i in 1..=n {
1949 let data = json!({"title": format!("Decision {i}"), "outcome": "approved"});
1950 let l = link(key, i, out.last(), &data, at(i as i64 * 10 + 1));
1951 out.push(l);
1952 }
1953 out
1954 }
1955
1956 pub(super) struct Chain {
1960 pub(super) links: Vec<GovernanceChainLink>,
1961 gov: u32,
1962 amd: u32,
1963 key: u32,
1964 }
1965
1966 impl Chain {
1967 pub(super) fn new() -> Self {
1968 Self {
1969 links: Vec::new(),
1970 gov: 0,
1971 amd: 0,
1972 key: 0,
1973 }
1974 }
1975
1976 pub(super) fn prev_hash(&self) -> Option<Sha256Hex> {
1977 self.links.last().map(|l| l.attestation.entry_hash)
1978 }
1979
1980 pub(super) fn hash_at(&self, seq: usize) -> Sha256Hex {
1982 self.links[seq - 1].attestation.entry_hash
1983 }
1984
1985 pub(super) fn next_amd(&self) -> GovernanceLogId {
1987 amd(self.amd + 1)
1988 }
1989
1990 pub(super) fn push(
1991 &mut self,
1992 signer: &SigningKey,
1993 id: GovernanceLogId,
1994 entry_type: GovernanceLogEntryType,
1995 data: serde_json::Value,
1996 carry: bool,
1997 ) -> GovernanceLogId {
1998 let n = self.links.len() as i64 + 1;
1999 let created_at = truncate_to_micros(at(n * 10));
2000 let envelope = Envelope::new(
2001 id.clone(),
2002 entry_type,
2003 created_at,
2004 self.prev_hash(),
2005 data_hash(&data),
2006 );
2007 let attestation =
2008 attest(signer, &envelope, n as u64, at(n * 10 + 1));
2009 self.links.push(GovernanceChainLink {
2010 id: id.clone(),
2011 entry_type,
2012 created_at,
2013 attestation,
2014 data: carry.then_some(data),
2015 });
2016 id
2017 }
2018
2019 pub(super) fn entry(
2022 &mut self,
2023 signer: &SigningKey,
2024 data: serde_json::Value,
2025 ) -> GovernanceLogId {
2026 self.gov += 1;
2027 let id = gov(self.gov);
2028 self.push(
2029 signer,
2030 id,
2031 GovernanceLogEntryType::CouncilDecision,
2032 data,
2033 false,
2034 )
2035 }
2036
2037 pub(super) fn decision(
2038 &mut self,
2039 signer: &SigningKey,
2040 ) -> GovernanceLogId {
2041 let data = json!({"title": format!("Decision {}", self.gov + 1)});
2042 self.entry(signer, data)
2043 }
2044
2045 pub(super) fn amend(
2046 &mut self,
2047 signer: &SigningKey,
2048 amendment: &Amendment,
2049 ) -> GovernanceLogId {
2050 self.amd += 1;
2051 let id = amd(self.amd);
2052 self.push(
2053 signer,
2054 id,
2055 GovernanceLogEntryType::Amendment,
2056 serde_json::to_value(amendment).unwrap(),
2057 true,
2058 )
2059 }
2060
2061 pub(super) fn rotate(
2062 &mut self,
2063 signer: &SigningKey,
2064 rotation: &KeyRotation,
2065 ) -> GovernanceLogId {
2066 self.key += 1;
2067 let id = key_id(self.key);
2068 self.push(
2069 signer,
2070 id,
2071 GovernanceLogEntryType::KeyRotation,
2072 serde_json::to_value(rotation).unwrap(),
2073 true,
2074 )
2075 }
2076 }
2077
2078 #[test]
2081 fn canonical_json_sorts_keys_at_every_level() {
2082 let v = json!({"b": {"z": 1, "a": [{"y": 2, "x": 3}]}, "a": null});
2083 assert_eq!(
2084 canonical_json(&v),
2085 br#"{"a":null,"b":{"a":[{"x":3,"y":2}],"z":1}}"#
2086 );
2087 }
2088
2089 #[test]
2090 fn canonical_json_is_compact_and_escapes_like_serde() {
2091 let v = json!({"s": "tab\there \"q\" ünïcode \u{1F600}", "n": [1, -2, 3.5, true, false]});
2092 let bytes = canonical_json(&v);
2093 let text = std::str::from_utf8(&bytes).unwrap();
2094 assert_eq!(
2095 text,
2096 r#"{"n":[1,-2,3.5,true,false],"s":"tab\there \"q\" ünïcode 😀"}"#
2097 );
2098 }
2099
2100 #[test]
2101 fn canonical_json_ignores_insertion_order() {
2102 let mut a = serde_json::Map::new();
2103 a.insert("z".into(), json!(1));
2104 a.insert("a".into(), json!(2));
2105 let mut b = serde_json::Map::new();
2106 b.insert("a".into(), json!(2));
2107 b.insert("z".into(), json!(1));
2108 assert_eq!(
2109 canonical_json(&serde_json::Value::Object(a)),
2110 canonical_json(&serde_json::Value::Object(b))
2111 );
2112 }
2113
2114 #[test]
2115 fn canonical_json_empty_containers() {
2116 assert_eq!(canonical_json(&json!({})), b"{}");
2117 assert_eq!(canonical_json(&json!([])), b"[]");
2118 assert_eq!(
2119 canonical_json(&json!({"a": {}, "b": []})),
2120 br#"{"a":{},"b":[]}"#
2121 );
2122 }
2123
2124 #[test]
2127 fn preimage_is_declaration_ordered_json() {
2128 let e = Envelope::new(
2129 gov(1),
2130 GovernanceLogEntryType::AppealsCourtDecision,
2131 at(0),
2132 None,
2133 data_hash(&json!({})),
2134 );
2135 let text = String::from_utf8(e.preimage()).unwrap();
2136 assert!(text.starts_with(r#"{"agora_governance_log":1,"id":"GOV-2026-0001","entry_type":"appeals_court_decision","created_at":1700000000123456,"prev_hash":null,"data_hash":""#), "{text}");
2137 }
2138
2139 #[test]
2140 fn every_envelope_field_changes_the_hash() {
2141 let base = Envelope::new(
2142 gov(1),
2143 GovernanceLogEntryType::CouncilDecision,
2144 at(0),
2145 None,
2146 data_hash(&json!({"a":1})),
2147 );
2148 let h = base.entry_hash();
2149 let mut e = base.clone();
2150 e.id = gov(2);
2151 assert_ne!(e.entry_hash(), h);
2152 let mut e = base.clone();
2153 e.entry_type = GovernanceLogEntryType::PolicyChange;
2154 assert_ne!(e.entry_hash(), h);
2155 let mut e = base.clone();
2156 e.created_at += 1;
2157 assert_ne!(e.entry_hash(), h);
2158 let mut e = base.clone();
2159 e.prev_hash = Some(h);
2160 assert_ne!(e.entry_hash(), h);
2161 let mut e = base.clone();
2162 e.data_hash = data_hash(&json!({"a":2}));
2163 assert_ne!(e.entry_hash(), h);
2164 assert_eq!(base.entry_hash(), h, "and it is deterministic");
2165 }
2166
2167 #[test]
2168 fn truncation_matches_what_the_envelope_carries() {
2169 let t = at(0);
2170 assert_eq!(truncate_to_micros(t).timestamp_subsec_nanos(), 123_456_000);
2171 assert_eq!(truncate_to_seconds(t).timestamp_subsec_nanos(), 0);
2172 assert_eq!(
2173 Envelope::new(
2174 gov(1),
2175 GovernanceLogEntryType::CouncilDecision,
2176 t,
2177 None,
2178 data_hash(&json!(null))
2179 )
2180 .created_at,
2181 truncate_to_micros(t).timestamp_micros()
2182 );
2183 }
2184
2185 #[test]
2188 fn hex_newtypes_round_trip_and_reject_wrong_lengths() {
2189 let h = data_hash(&json!(1));
2190 let s = serde_json::to_string(&h).unwrap();
2191 assert_eq!(s.len(), 66);
2192 let back: Sha256Hex = serde_json::from_str(&s).unwrap();
2193 assert_eq!(back, h);
2194 assert!(serde_json::from_str::<Sha256Hex>("\"abcd\"").is_err());
2195 assert!("zz".repeat(32).parse::<Sha256Hex>().is_err());
2196 assert!(Sha256Hex::try_from(vec![0u8; 31]).is_err());
2197 assert_eq!(format!("{h:?}"), format!("Sha256Hex({h})"));
2198 }
2199
2200 #[test]
2203 fn attest_then_verify_link() {
2204 let (key, pk) = generate_keypair();
2205 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
2206 assert_eq!(verify_link(&l, &pk), Ok(()));
2207 assert!(verify_data(&l, &json!({"a": 1})));
2208 assert!(!verify_data(&l, &json!({"a": 2})));
2209 assert!(!l.attestation.retroactive);
2210 }
2211
2212 #[test]
2213 fn wrong_key_fails_signature_only() {
2214 let (key, _) = generate_keypair();
2215 let (_, other) = generate_keypair();
2216 let l = link(&key, 1, None, &json!({}), at(5));
2217 assert_eq!(verify_link(&l, &other), Err(LinkError::BadSignature));
2218 }
2219
2220 #[test]
2221 fn tampering_with_any_attested_field_is_detected() {
2222 let (key, pk) = generate_keypair();
2223 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
2224
2225 let mut t = l.clone();
2226 t.created_at += chrono::Duration::microseconds(1);
2227 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
2228
2229 let mut t = l.clone();
2230 t.entry_type = GovernanceLogEntryType::StewardVeto;
2231 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
2232
2233 let mut t = l.clone();
2234 t.attestation.data_hash = data_hash(&json!({"a": 2}));
2235 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
2236
2237 let mut t = l.clone();
2240 t.attestation.signed_at -= chrono::Duration::seconds(1);
2241 assert_eq!(verify_link(&t, &pk), Err(LinkError::BadSignature));
2242
2243 let mut t = l.clone();
2244 t.attestation.envelope_version = 2;
2245 assert_eq!(verify_link(&t, &pk), Err(LinkError::UnsupportedVersion(2)));
2246 }
2247
2248 #[test]
2249 fn a_good_chain_verifies_in_any_input_order() {
2250 let (key, pk) = generate_keypair();
2251 let mut c = chain(&key, 4);
2252 c.reverse();
2253 let v = verify_chain(&c, &pk, &anchored(&pk));
2254 assert!(v.ok, "{v:#?}");
2255 assert_eq!(v.head, Some(gov(4)));
2256 assert_eq!(
2257 v.entries.iter().map(|e| e.chain_seq).collect::<Vec<_>>(),
2258 [1, 2, 3, 4]
2259 );
2260 assert!(v.entries.iter().all(|e| e.content_matches.is_none()
2261 && e.problem.is_none()
2262 && !e.out_of_order));
2263 assert_eq!(v.public_key, PublicKeyHex::from(&pk));
2264 }
2265
2266 #[test]
2267 fn empty_chain_is_ok_with_no_head() {
2268 let (_, pk) = generate_keypair();
2269 let v = verify_chain(&[], &pk, &anchored(&pk));
2270 assert!(v.ok);
2271 assert!(v.head.is_none());
2272 assert!(v.entries.is_empty());
2273 }
2274
2275 #[test]
2276 fn a_changed_entry_breaks_its_signature_and_the_next_link() {
2277 let (key, pk) = generate_keypair();
2278 let mut c = chain(&key, 3);
2279 let rewritten = link(
2282 &key,
2283 2,
2284 Some(&c[0]),
2285 &json!({"title": "Decision 2", "outcome": "REJECTED"}),
2286 at(21),
2287 );
2288 c[1] = rewritten;
2289 let v = verify_chain(&c, &pk, &anchored(&pk));
2290 assert!(!v.ok);
2291 assert!(
2292 v.entries[1].signature_valid && v.entries[1].link_valid,
2293 "the rewrite itself is well-formed: {:#?}",
2294 v.entries[1]
2295 );
2296 assert!(
2297 !v.entries[2].link_valid,
2298 "but entry 3 no longer points at it: {:#?}",
2299 v.entries[2]
2300 );
2301 assert!(
2302 v.entries[2]
2303 .problem
2304 .as_deref()
2305 .unwrap()
2306 .contains("prev_hash")
2307 );
2308 }
2309
2310 #[test]
2311 fn a_removed_entry_is_a_gap_and_a_broken_link() {
2312 let (key, pk) = generate_keypair();
2313 let mut c = chain(&key, 3);
2314 c.remove(1);
2315 let v = verify_chain(&c, &pk, &anchored(&pk));
2316 assert!(!v.ok);
2317 assert!(v.entries[0].link_valid);
2318 let p = v.entries[1].problem.as_deref().unwrap();
2319 assert!(p.contains("chain_seq 3 where 2 was expected"), "{p}");
2320 assert!(p.contains("prev_hash"), "{p}");
2321 }
2322
2323 #[test]
2324 fn a_second_genesis_is_rejected() {
2325 let (key, pk) = generate_keypair();
2326 let mut c = chain(&key, 2);
2327 let rogue = link(&key, 2, None, &json!({}), at(21));
2328 c[1] = rogue;
2329 let v = verify_chain(&c, &pk, &anchored(&pk));
2330 assert!(!v.ok);
2331 assert!(
2332 v.entries[1]
2333 .problem
2334 .as_deref()
2335 .unwrap()
2336 .contains("null mid-chain")
2337 );
2338 }
2339
2340 #[test]
2341 fn retroactive_and_out_of_order_are_recomputed_not_copied() {
2342 let (key, pk) = generate_keypair();
2343 let first = link(&key, 1, None, &json!({}), at(10 + 3600));
2344 let created = truncate_to_micros(at(5));
2347 let envelope = Envelope::new(
2348 gov(2),
2349 GovernanceLogEntryType::CouncilDecision,
2350 created,
2351 Some(first.attestation.entry_hash),
2352 data_hash(&json!({})),
2353 );
2354 let attestation = attest(&key, &envelope, 2, at(6));
2355 let mut second = GovernanceChainLink {
2356 id: gov(2),
2357 entry_type: GovernanceLogEntryType::CouncilDecision,
2358 created_at: created,
2359 attestation,
2360 data: None,
2361 };
2362 second.attestation.retroactive = true; let v = verify_chain(&[first, second], &pk, &anchored(&pk));
2364 assert!(v.ok, "{v:#?}");
2365 assert!(v.entries[0].retroactive);
2366 assert!(!v.entries[1].retroactive, "recomputed from timestamps");
2367 assert!(v.entries[1].out_of_order);
2368 }
2369
2370 #[test]
2371 fn content_mismatch_settles_to_not_ok() {
2372 let (key, pk) = generate_keypair();
2373 let c = chain(&key, 1);
2374 let mut v = verify_chain(&c, &pk, &anchored(&pk));
2375 v.entries[0].content_matches = Some(true);
2376 assert!(v.clone().settle().ok);
2377 v.entries[0].content_matches = Some(false);
2378 assert!(!v.settle().ok);
2379 }
2380
2381 #[test]
2384 fn an_amendment_fills_amended_by_on_its_target() {
2385 let (key, pk) = generate_keypair();
2386 let mut c = Chain::new();
2387 let target = c.decision(&key);
2388 c.decision(&key);
2389 let amendment = Amendment::new(
2390 target,
2391 c.hash_at(1),
2392 AmendmentKind::NonPrecedential,
2393 "§1 (Red Team Cases Recharacterized)",
2394 "diagnostic finding — not citable as moderation precedent",
2395 )
2396 .unwrap()
2397 .with_authority(gov(5))
2398 .with_rationale("§5 leaves the ruling itself standing");
2399 let id = c.amend(&key, &amendment);
2400
2401 let v = verify_chain(&c.links, &pk, &anchored(&pk));
2402 assert!(v.ok, "{v:#?}");
2403 assert_eq!(v.entries[0].amended_by, vec![id]);
2404 assert!(v.entries[1].amended_by.is_empty());
2405 assert!(!v.entries[0].redacted);
2406 assert_eq!(v.head, Some(amd(1)));
2407 assert_eq!(v.entries[2].content_matches, Some(true));
2409 assert_eq!(v.entries[0].content_matches, None);
2410 assert_eq!(standing([amendment.kind]), Standing::NonPrecedential);
2411 }
2412
2413 #[test]
2414 fn an_amendment_must_name_an_earlier_entry_by_its_exact_hash() {
2415 let (key, pk) = generate_keypair();
2416 let problem = |c: &Chain, at: usize| -> String {
2417 verify_chain(&c.links, &pk, &anchored(&pk)).entries[at]
2418 .problem
2419 .clone()
2420 .unwrap_or_default()
2421 };
2422
2423 let mut c = Chain::new();
2424 c.decision(&key);
2425 let unknown = Amendment::new(
2426 gov(99),
2427 c.hash_at(1),
2428 AmendmentKind::Overruled,
2429 "b",
2430 "n",
2431 )
2432 .unwrap();
2433 c.amend(&key, &unknown);
2434 assert!(
2435 problem(&c, 1).contains("is not an entry of this chain"),
2436 "{}",
2437 problem(&c, 1)
2438 );
2439 assert!(!verify_chain(&c.links, &pk, &anchored(&pk)).ok);
2440
2441 let mut c = Chain::new();
2443 c.decision(&key);
2444 let forward = Amendment::new(
2445 gov(2),
2446 c.hash_at(1),
2447 AmendmentKind::Overruled,
2448 "b",
2449 "n",
2450 )
2451 .unwrap();
2452 c.amend(&key, &forward);
2453 c.decision(&key);
2454 assert!(
2455 problem(&c, 1).contains("not an earlier entry"),
2456 "{}",
2457 problem(&c, 1)
2458 );
2459
2460 let mut c = Chain::new();
2462 let target = c.decision(&key);
2463 let wrong_hash = Amendment::new(
2464 target,
2465 data_hash(&json!("some other entry")),
2466 AmendmentKind::Overruled,
2467 "b",
2468 "n",
2469 )
2470 .unwrap();
2471 c.amend(&key, &wrong_hash);
2472 assert!(problem(&c, 1).contains("entry_hash"), "{}", problem(&c, 1));
2473 assert!(
2474 verify_chain(&c.links, &pk, &anchored(&pk)).entries[0]
2475 .amended_by
2476 .is_empty()
2477 );
2478 }
2479
2480 #[test]
2481 fn redaction_shape_violations_fail_verification() {
2482 let (key, pk) = generate_keypair();
2483 let amend_with = |mutate: &dyn Fn(&mut Amendment)| -> String {
2484 let mut c = Chain::new();
2485 let target = c.decision(&key);
2486 let mut amendment = Amendment::new(
2487 target,
2488 c.hash_at(1),
2489 AmendmentKind::Correction,
2490 "b",
2491 "n",
2492 )
2493 .unwrap();
2494 mutate(&mut amendment);
2495 c.amend(&key, &amendment);
2496 let v = verify_chain(&c.links, &pk, &anchored(&pk));
2497 assert!(!v.ok, "{v:#?}");
2498 v.entries[1].problem.clone().unwrap_or_default()
2499 };
2500
2501 assert_eq!(
2504 Amendment::new(
2505 gov(1),
2506 data_hash(&json!(null)),
2507 AmendmentKind::Redaction,
2508 "b",
2509 "n"
2510 ),
2511 Err(AmendmentError::MissingRedaction)
2512 );
2513 let p = amend_with(&|a| a.kind = AmendmentKind::Redaction);
2514 assert!(p.contains("requires a `redaction`"), "{p}");
2515
2516 let p = amend_with(&|a| {
2517 a.redaction = Some(Redaction {
2518 fields: vec!["/x".into()],
2519 resulting_data_hash: data_hash(&json!({})),
2520 })
2521 });
2522 assert!(p.contains("only valid on kind"), "{p}");
2523
2524 let p = amend_with(&|a| a.agora_governance_amendment = 2);
2525 assert!(p.contains("agora_governance_amendment is 2"), "{p}");
2526 }
2527
2528 #[test]
2529 fn standing_is_the_last_amendment_that_changes_it() {
2530 use AmendmentKind::*;
2531 assert_eq!(standing([]), Standing::InForce);
2532 assert_eq!(standing([Correction, Redaction]), Standing::InForce);
2533 assert_eq!(
2534 standing([Correction, NonPrecedential, Redaction]),
2535 Standing::NonPrecedential
2536 );
2537 assert_eq!(standing([Overruled, Reinstated]), Standing::InForce);
2538 assert_eq!(standing([Reinstated, Superseded]), Standing::Superseded);
2539 assert_eq!(kind_standing(Reattested), None);
2540 assert_eq!(kind_standing(Reinstated), Some(Standing::InForce));
2541 }
2542
2543 #[test]
2544 fn a_redaction_verifies_against_the_amendment_and_nothing_else() {
2545 let (key, pk) = generate_keypair();
2546 let data = json!({
2547 "finding": "upheld",
2548 "subject": {"handle": "someone", "detail": "personal"},
2549 });
2550 let mut c = Chain::new();
2551 let target = c.entry(&key, data.clone());
2552 let amendment_id = c.next_amd();
2553 let (amendment, redacted) = Amendment::redaction(
2554 &amendment_id,
2555 target,
2556 c.hash_at(1),
2557 "GDPR Art. 17(1)(a)",
2558 "personal data removed on request",
2559 vec!["/subject/handle".into(), "/subject/detail".into()],
2560 &data,
2561 Blind::from([7; 32]),
2562 )
2563 .unwrap();
2564 assert_eq!(c.amend(&key, &amendment), amendment_id);
2565 assert_eq!(redacted[BLIND_KEY], json!(Blind::from([7; 32])));
2566
2567 let mut v = verify_chain(&c.links, &pk, &anchored(&pk));
2568 assert!(v.ok, "{v:#?}");
2569 assert!(v.entries[0].redacted);
2570 assert_eq!(v.entries[0].redacted_data_hash, Some(data_hash(&redacted)));
2571 assert_eq!(
2572 redacted["subject"]["handle"],
2573 json!(format!("[redacted by {amendment_id}]"))
2574 );
2575 assert_eq!(redacted["finding"], json!("upheld"), "and nothing else");
2576
2577 assert!(v.check_content(&c.links[0], &redacted));
2579 assert_eq!(v.entries[0].content_matches, Some(true));
2580 assert!(v.clone().settle().ok);
2581 assert!(v.check_content(&c.links[0], &data));
2583
2584 let mut tampered = redacted.clone();
2586 tampered["finding"] = json!("overturned");
2587 assert!(!v.check_content(&c.links[0], &tampered));
2588 assert_eq!(v.entries[0].content_matches, Some(false));
2589 assert!(!v.settle().ok);
2590 }
2591
2592 #[test]
2593 fn content_that_matches_nothing_fails_without_an_amendment() {
2594 let (key, pk) = generate_keypair();
2595 let mut c = Chain::new();
2596 c.entry(&key, json!({"a": 1}));
2597 let mut v = verify_chain(&c.links, &pk, &anchored(&pk));
2598 assert!(!v.check_content(&c.links[0], &json!({"a": 2})));
2599 assert!(!v.clone().settle().ok);
2600 let other = link(&key, 9, None, &json!({}), at(9));
2602 assert!(!v.check_content(&other, &json!({})));
2603 }
2604
2605 #[test]
2606 fn tampering_with_an_amendments_data_is_caught_by_the_data_hash() {
2607 let (key, pk) = generate_keypair();
2608 let mut c = Chain::new();
2609 let target = c.decision(&key);
2610 let amendment = Amendment::new(
2611 target,
2612 c.hash_at(1),
2613 AmendmentKind::Overruled,
2614 "b",
2615 "overruled by a later decision",
2616 )
2617 .unwrap();
2618 c.amend(&key, &amendment);
2619 c.links[1].data.as_mut().unwrap()["note"] =
2620 json!("reinstated, actually");
2621
2622 let v = verify_chain(&c.links, &pk, &anchored(&pk));
2623 assert!(!v.ok, "{v:#?}");
2624 let p = v.entries[1].problem.as_deref().unwrap();
2625 assert!(p.contains("does not hash to the attested data_hash"), "{p}");
2626 assert!(
2627 v.entries[0].amended_by.is_empty(),
2628 "an unreadable amendment has no effect"
2629 );
2630 assert!(
2631 v.entries[1].signature_valid && v.entries[1].link_valid,
2632 "the envelope is untouched — only the content is not what it \
2633 committed to"
2634 );
2635 }
2636
2637 #[test]
2638 fn an_amendment_or_rotation_must_carry_its_data() {
2639 let (key, pk) = generate_keypair();
2640 let mut c = Chain::new();
2641 let target = c.decision(&key);
2642 let amendment = Amendment::new(
2643 target,
2644 c.hash_at(1),
2645 AmendmentKind::Correction,
2646 "b",
2647 "n",
2648 )
2649 .unwrap();
2650 c.amend(&key, &amendment);
2651 let rotation = KeyRotation::routine(
2652 (&pk).into(),
2653 &key,
2654 c.prev_hash(),
2655 at(35),
2656 "n",
2657 );
2658 c.rotate(&key, &rotation);
2659 c.links[1].data = None;
2660 c.links[2].data = None;
2661
2662 let v = verify_chain(&c.links, &pk, &anchored(&pk));
2663 assert!(!v.ok, "{v:#?}");
2664 for (i, entry_type) in [(1, "amendment"), (2, "key_rotation")] {
2665 let p = v.entries[i].problem.as_deref().unwrap();
2666 assert!(p.contains(&format!("a {entry_type} entry")), "{p}");
2667 assert!(p.contains("must carry its `data`"), "{p}");
2668 }
2669 }
2670
2671 #[test]
2672 fn the_id_series_must_match_the_entry_type() {
2673 let (key, pk) = generate_keypair();
2674 let mut c = Chain::new();
2675 let target = c.decision(&key);
2676 let amendment = Amendment::new(
2677 target,
2678 c.hash_at(1),
2679 AmendmentKind::Correction,
2680 "b",
2681 "n",
2682 )
2683 .unwrap();
2684 c.push(
2685 &key,
2686 gov(7),
2687 GovernanceLogEntryType::Amendment,
2688 serde_json::to_value(&amendment).unwrap(),
2689 true,
2690 );
2691 let v = verify_chain(&c.links, &pk, &anchored(&pk));
2692 assert!(!v.ok, "{v:#?}");
2693 let p = v.entries[1].problem.as_deref().unwrap();
2694 assert!(p.contains("must be in the AMD- series"), "{p}");
2695
2696 let mut c = Chain::new();
2697 c.push(
2698 &key,
2699 key_id(1),
2700 GovernanceLogEntryType::CouncilDecision,
2701 json!({}),
2702 false,
2703 );
2704 let v = verify_chain(&c.links, &pk, &anchored(&pk));
2705 let p = v.entries[0].problem.as_deref().unwrap();
2706 assert!(p.contains("reserved"), "{p}");
2707 }
2708
2709 #[test]
2710 fn redact_data_replaces_whole_values_and_refuses_the_rest() {
2711 let id = amd(3);
2712 let blind = Blind::from([9; 32]);
2713 let data = json!({"a": {"b": [1, {"c": "secret"}]}, "d/e": "slash"});
2714 let out = redact_data(
2715 &data,
2716 &["/a/b/1/c".into(), "/d~1e".into()],
2717 &id,
2718 blind,
2719 )
2720 .unwrap();
2721 assert_eq!(out["a"]["b"][1]["c"], json!(redaction_marker(&id)));
2722 assert_eq!(out["d/e"], json!(redaction_marker(&id)));
2723 assert_eq!(out["a"]["b"][0], json!(1), "untouched");
2724 assert_eq!(out[BLIND_KEY], json!(blind), "a legacy entry gains one");
2725
2726 assert_eq!(
2727 redact_data(&data, &["/a/nope".into()], &id, blind),
2728 Err(RedactError::Unresolved("/a/nope".into()))
2729 );
2730 assert_eq!(
2731 redact_data(&data, &["".into()], &id, blind),
2732 Err(RedactError::WholeEntry)
2733 );
2734 assert_eq!(
2735 redact_data(&data, &[], &id, blind),
2736 Err(RedactError::NoFields)
2737 );
2738 assert_eq!(
2739 redact_data(&data, &["/_blind".into()], &id, blind),
2740 Err(RedactError::BlindPointer("/_blind".into()))
2741 );
2742 }
2743
2744 #[test]
2745 fn blind_data_is_for_objects_and_is_the_writers_to_supply() {
2746 let blind = Blind::from([1; 32]);
2747 let out = blind_data(&json!({"finding": "upheld"}), blind).unwrap();
2748 assert_eq!(out, json!({"finding": "upheld", "_blind": blind}));
2749 assert_eq!(
2750 blind_data(&json!([1]), blind),
2751 Err(BlindError::NotAnObject)
2752 );
2753 assert_eq!(blind_data(&out, blind), Err(BlindError::AlreadyBlinded));
2754 assert_ne!(Blind::random(), Blind::random());
2755
2756 use GovernanceLogEntryType::*;
2757 assert!(!is_redactable(Amendment) && !is_redactable(KeyRotation));
2758 assert!(
2759 is_redactable(CouncilDecision) && is_redactable(EmergencyAction)
2760 );
2761 }
2762
2763 #[test]
2767 fn a_removed_value_cannot_be_confirmed_by_guessing_it() {
2768 fn confirms(
2770 public: &serde_json::Value,
2771 pointer: &str,
2772 guess: &str,
2773 hash: Sha256Hex,
2774 ) -> bool {
2775 let mut attempt = public.clone();
2776 *attempt.pointer_mut(pointer).unwrap() = json!(guess);
2777 data_hash(&attempt) == hash
2778 }
2779 let legacy = json!({"finding": "upheld", "handle": "someone", "city": "Utrecht"});
2780
2781 let written = blind_data(&legacy, Blind::random()).unwrap();
2783 let first = redact_data(
2784 &written,
2785 &["/handle".into()],
2786 &amd(1),
2787 Blind::random(),
2788 )
2789 .unwrap();
2790 assert!(!confirms(&first, "/handle", "someone", data_hash(&written)));
2791
2792 let second =
2795 redact_data(&first, &["/city".into()], &amd(2), Blind::random())
2796 .unwrap();
2797 assert!(!confirms(&second, "/city", "Utrecht", data_hash(&first)));
2798
2799 let mut stripped =
2803 redact_data(&legacy, &["/handle".into()], &amd(3), Blind::random())
2804 .unwrap();
2805 let legacy_first = stripped.clone();
2806 stripped.as_object_mut().unwrap().remove(BLIND_KEY);
2807 assert!(confirms(
2808 &stripped,
2809 "/handle",
2810 "someone",
2811 data_hash(&legacy)
2812 ));
2813 let legacy_second = redact_data(
2815 &legacy_first,
2816 &["/city".into()],
2817 &amd(4),
2818 Blind::random(),
2819 )
2820 .unwrap();
2821 assert!(!confirms(
2822 &legacy_second,
2823 "/city",
2824 "Utrecht",
2825 data_hash(&legacy_first)
2826 ));
2827 }
2828
2829 #[test]
2832 fn a_routine_rotation_moves_the_chain_to_the_new_key() {
2833 let (old, old_pk) = generate_keypair();
2834 let (new, new_pk) = generate_keypair();
2835 let anchor = anchored(&old_pk).with((&new_pk).into());
2836 let mut c = Chain::new();
2837 c.decision(&old);
2838 let rotation = KeyRotation::routine(
2839 (&old_pk).into(),
2840 &new,
2841 c.prev_hash(),
2842 at(25),
2843 "scheduled rotation",
2844 );
2845 let rotation_id = c.rotate(&old, &rotation);
2846 c.decision(&new);
2847
2848 let v = verify_chain(&c.links, &old_pk, &anchor);
2849 assert!(v.ok, "{v:#?}");
2850 assert_eq!(v.public_key, (&new_pk).into());
2851 assert_eq!(
2852 v.entries[1].signed_by,
2853 Some((&old_pk).into()),
2854 "the rotation itself is signed by the old key"
2855 );
2856 assert_eq!(v.entries[2].signed_by, Some((&new_pk).into()));
2857 assert!(v.unanchored_keys.is_empty());
2858 assert!(v.repudiated.is_empty());
2859 assert_eq!(v.keys.len(), 2);
2860 assert_eq!(v.keys[0].public_key, (&old_pk).into());
2861 assert_eq!(v.keys[0].from_seq, 1);
2862 assert_eq!(v.keys[0].through_seq, Some(2));
2863 assert_eq!(v.keys[0].status, KeyStatus::Retired);
2864 assert_eq!(v.keys[0].introduced_by, None);
2865 assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
2866 assert_eq!(v.keys[1].from_seq, 3);
2867 assert_eq!(v.keys[1].through_seq, None);
2868 assert_eq!(v.keys[1].status, KeyStatus::Active);
2869 assert_eq!(v.keys[1].introduced_by.as_ref(), Some(&rotation_id));
2870 }
2871
2872 #[test]
2873 fn the_old_key_cannot_sign_after_a_routine_rotation() {
2874 let (old, old_pk) = generate_keypair();
2875 let (new, new_pk) = generate_keypair();
2876 let anchor = anchored(&old_pk).with((&new_pk).into());
2877 let mut c = Chain::new();
2878 c.decision(&old);
2879 let rotation = KeyRotation::routine(
2880 (&old_pk).into(),
2881 &new,
2882 c.prev_hash(),
2883 at(25),
2884 "scheduled",
2885 );
2886 c.rotate(&old, &rotation);
2887 c.decision(&old);
2888
2889 let v = verify_chain(&c.links, &old_pk, &anchor);
2890 assert!(!v.ok, "{v:#?}");
2891 assert!(!v.entries[2].signature_valid);
2892 assert!(
2893 v.entries[2].link_valid,
2894 "the linkage is fine; the key is not"
2895 );
2896 }
2897
2898 #[test]
2899 fn a_routine_rotation_to_an_unanchored_key_is_followed_and_reported() {
2900 let (old, old_pk) = generate_keypair();
2901 let (new, new_pk) = generate_keypair();
2902 let mut c = Chain::new();
2903 c.decision(&old);
2904 let rotation = KeyRotation::routine(
2905 (&old_pk).into(),
2906 &new,
2907 c.prev_hash(),
2908 at(25),
2909 "scheduled",
2910 );
2911 c.rotate(&old, &rotation);
2912 c.decision(&new);
2913
2914 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk));
2917 assert!(v.ok, "{v:#?}");
2918 assert_eq!(v.unanchored_keys, vec![PublicKeyHex::from(&new_pk)]);
2919 assert_eq!(v.public_key, (&new_pk).into());
2920 }
2921
2922 #[test]
2923 fn a_forged_proof_of_possession_is_rejected() {
2924 let (old, old_pk) = generate_keypair();
2925 let (_, new_pk) = generate_keypair();
2926 let (thief, _) = generate_keypair();
2927 let mut c = Chain::new();
2928 c.decision(&old);
2929 let mut rotation = KeyRotation::routine(
2932 (&old_pk).into(),
2933 &thief,
2934 c.prev_hash(),
2935 at(25),
2936 "scheduled",
2937 );
2938 rotation.new_key = (&new_pk).into();
2939 let statement = rotation.statement(c.prev_hash());
2940 rotation.proof = crypto::sign(
2941 &old,
2942 statement.hash().as_bytes(),
2943 rotation.proof_signed_at,
2944 )
2945 .into();
2946 c.rotate(&old, &rotation);
2947
2948 assert_eq!(
2949 rotation.verify_proof(c.links[1].attestation.prev_hash),
2950 Err(RotationError::BadProof)
2951 );
2952 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk));
2953 assert!(!v.ok, "{v:#?}");
2954 assert!(
2955 v.entries[1]
2956 .problem
2957 .as_deref()
2958 .unwrap()
2959 .contains("proof of possession")
2960 );
2961 assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
2962 }
2963
2964 #[test]
2965 fn a_proof_does_not_replay_at_another_position() {
2966 let (old, old_pk) = generate_keypair();
2967 let (new, new_pk) = generate_keypair();
2968 let anchor = anchored(&old_pk).with((&new_pk).into());
2969 let mut c = Chain::new();
2970 c.decision(&old);
2971 let rotation = KeyRotation::routine(
2973 (&old_pk).into(),
2974 &new,
2975 c.prev_hash(),
2976 at(25),
2977 "scheduled",
2978 );
2979 c.decision(&old);
2981 c.rotate(&old, &rotation);
2982
2983 let v = verify_chain(&c.links, &old_pk, &anchor);
2984 assert!(!v.ok, "{v:#?}");
2985 assert!(
2986 v.entries[2]
2987 .problem
2988 .as_deref()
2989 .unwrap()
2990 .contains("proof of possession")
2991 );
2992 assert_eq!(v.public_key, (&old_pk).into());
2993 }
2994
2995 #[test]
2996 fn a_compromise_repudiates_the_window_and_a_reattestation_restores_one() {
2997 let (old, old_pk) = generate_keypair();
2998 let (new, new_pk) = generate_keypair();
2999 let anchor = anchored(&old_pk).with((&new_pk).into());
3000 let mut c = Chain::new();
3001 c.decision(&old); c.decision(&old); let reattested = c.decision(&old); let rotation = KeyRotation::compromise(
3005 (&old_pk).into(),
3006 &new,
3007 TrustedHead {
3008 id: gov(1),
3009 chain_seq: 1,
3010 entry_hash: c.hash_at(1),
3011 },
3012 c.prev_hash(),
3013 at(45),
3014 "signing key exfiltrated",
3015 );
3016 let rotation_id = c.rotate(&new, &rotation); let vouch = Amendment::new(
3018 reattested,
3019 c.hash_at(3),
3020 AmendmentKind::Reattested,
3021 "Art. VII",
3022 "independently verified; the Steward vouches for it",
3023 )
3024 .unwrap();
3025 let vouch_id = c.amend(&new, &vouch); let v = verify_chain(&c.links, &old_pk, &anchor);
3028 assert!(
3029 v.ok,
3030 "repudiation is a declared state, not a defect: {v:#?}"
3031 );
3032 assert_eq!(v.repudiated, vec![gov(2)]);
3033 assert!(v.entries[1].repudiated);
3034 assert!(!v.entries[2].repudiated);
3035 assert_eq!(v.entries[2].reattested_by, vec![vouch_id.clone()]);
3036 assert_eq!(v.entries[2].amended_by, vec![vouch_id]);
3037 assert_eq!(v.entries[3].signed_by, Some((&new_pk).into()));
3038 assert_eq!(v.public_key, (&new_pk).into());
3039 assert_eq!(v.keys.len(), 2);
3040 assert_eq!(v.keys[0].status, KeyStatus::Compromised);
3041 assert_eq!(
3042 v.keys[0].through_seq,
3043 Some(1),
3044 "trusted through the last trusted entry, not through the \
3045 declaration"
3046 );
3047 assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
3048 assert_eq!(v.keys[1].from_seq, 4, "the declaration is its own first");
3049 }
3050
3051 #[test]
3052 fn a_stolen_key_cannot_be_rotated_back_in() {
3053 let (k1, k1_pk) = generate_keypair();
3057 let (k2, k2_pk) = generate_keypair();
3058 let anchor = anchored(&k1_pk).with((&k2_pk).into());
3059 let mut c = Chain::new();
3060 c.decision(&k1); let real = KeyRotation::compromise(
3062 (&k1_pk).into(),
3063 &k2,
3064 TrustedHead {
3065 id: gov(1),
3066 chain_seq: 1,
3067 entry_hash: c.hash_at(1),
3068 },
3069 c.prev_hash(),
3070 at(25),
3071 "signing key exfiltrated",
3072 );
3073 c.rotate(&k2, &real); c.decision(&k2); let honest = verify_chain(&c.links, &k1_pk, &anchor);
3076 assert!(honest.ok, "{honest:#?}");
3077
3078 let hijack = KeyRotation::compromise(
3079 (&k2_pk).into(),
3080 &k1,
3081 TrustedHead {
3082 id: gov(2),
3083 chain_seq: 3,
3084 entry_hash: c.hash_at(3),
3085 },
3086 c.prev_hash(),
3087 at(45),
3088 "the Steward's key is the compromised one, trust me",
3089 );
3090 c.rotate(&k1, &hijack); c.decision(&k1); let v = verify_chain(&c.links, &k1_pk, &anchor);
3094 assert!(!v.ok);
3095 assert_eq!(v.public_key, (&k2_pk).into(), "the chain stays with K2");
3096 assert!(!v.entries[3].signature_valid, "{:#?}", v.entries[3]);
3097 assert!(!v.entries[4].signature_valid, "K1 signs nothing again");
3098 assert_eq!(v.keys.len(), 2);
3099 assert_eq!(v.keys[1].status, KeyStatus::Active);
3100 }
3101
3102 #[test]
3103 fn a_routine_rotation_cannot_reuse_a_key_either() {
3104 let (k1, k1_pk) = generate_keypair();
3105 let (k2, k2_pk) = generate_keypair();
3106 let anchor = anchored(&k1_pk).with((&k2_pk).into());
3107 let mut c = Chain::new();
3108 c.decision(&k1);
3109 let out = KeyRotation::routine(
3110 (&k1_pk).into(),
3111 &k2,
3112 c.prev_hash(),
3113 at(15),
3114 "",
3115 );
3116 c.rotate(&k1, &out);
3117 let back = KeyRotation::routine(
3118 (&k2_pk).into(),
3119 &k1,
3120 c.prev_hash(),
3121 at(25),
3122 "",
3123 );
3124 c.rotate(&k2, &back);
3125 let v = verify_chain(&c.links, &k1_pk, &anchor);
3126 assert!(!v.ok);
3127 assert!(
3128 v.entries[2]
3129 .problem
3130 .as_deref()
3131 .unwrap()
3132 .contains("never brought back"),
3133 "{:#?}",
3134 v.entries[2]
3135 );
3136 assert_eq!(v.public_key, (&k2_pk).into());
3137 }
3138
3139 #[test]
3140 fn a_forged_entry_has_no_effects() {
3141 let (steward, steward_pk) = generate_keypair();
3142 let (forger, _) = generate_keypair();
3143 let anchor = anchored(&steward_pk);
3144 let mut c = Chain::new();
3145 let target = c.decision(&steward); let fake = Amendment::new(
3147 target,
3148 c.hash_at(1),
3149 AmendmentKind::Overruled,
3150 "none",
3151 "overruled, says nobody with the key",
3152 )
3153 .unwrap();
3154 c.amend(&forger, &fake); let grab = KeyRotation::routine(
3156 (&steward_pk).into(),
3157 &forger,
3158 c.prev_hash(),
3159 at(35),
3160 "",
3161 );
3162 c.rotate(&forger, &grab); let v = verify_chain(&c.links, &steward_pk, &anchor);
3165 assert!(!v.ok);
3166 assert!(v.entries[0].amended_by.is_empty(), "{:#?}", v.entries[0]);
3167 assert_eq!(v.public_key, (&steward_pk).into());
3168 assert_eq!(v.keys.len(), 1);
3169 assert!(v.unanchored_keys.is_empty());
3170 }
3171
3172 #[test]
3173 fn a_repeated_id_is_a_problem() {
3174 let (key, pk) = generate_keypair();
3175 let mut c = Chain::new();
3176 c.decision(&key);
3177 c.gov = 0;
3178 c.decision(&key); let v = verify_chain(&c.links, &pk, &anchored(&pk));
3180 assert!(!v.ok);
3181 assert!(v.entries.iter().all(|e| {
3182 e.problem
3183 .as_deref()
3184 .is_some_and(|p| p.contains("more than once"))
3185 }));
3186 }
3187
3188 #[test]
3189 fn a_second_compromise_cannot_anchor_inside_the_first_window() {
3190 let (k1, k1_pk) = generate_keypair();
3191 let (k2, k2_pk) = generate_keypair();
3192 let (k3, k3_pk) = generate_keypair();
3193 let anchor =
3194 anchored(&k1_pk).with((&k2_pk).into()).with((&k3_pk).into());
3195 let mut c = Chain::new();
3196 c.decision(&k1); c.decision(&k1); let first = KeyRotation::compromise(
3199 (&k1_pk).into(),
3200 &k2,
3201 TrustedHead {
3202 id: gov(1),
3203 chain_seq: 1,
3204 entry_hash: c.hash_at(1),
3205 },
3206 c.prev_hash(),
3207 at(35),
3208 "",
3209 );
3210 c.rotate(&k2, &first); let second = KeyRotation::compromise(
3212 (&k1_pk).into(),
3213 &k3,
3214 TrustedHead {
3215 id: gov(2),
3216 chain_seq: 2,
3217 entry_hash: c.hash_at(2),
3218 },
3219 c.prev_hash(),
3220 at(45),
3221 "",
3222 );
3223 c.rotate(&k3, &second); let v = verify_chain(&c.links, &k1_pk, &anchor);
3226 assert!(!v.ok);
3227 let p = v.entries[3].problem.as_deref().unwrap();
3228 assert!(p.contains("repudiated"), "{p}");
3229 assert_eq!(v.keys.len(), 2, "K1 and K2; K3 never took the chain");
3230 assert_eq!(v.keys[0].through_seq, Some(1));
3231 }
3232
3233 #[test]
3234 fn an_unanchored_compromise_fails_closed() {
3235 let (old, old_pk) = generate_keypair();
3236 let (new, _) = generate_keypair();
3237 let mut c = Chain::new();
3238 c.decision(&old);
3239 c.decision(&old);
3240 let rotation = KeyRotation::compromise(
3241 (&old_pk).into(),
3242 &new,
3243 TrustedHead {
3244 id: gov(1),
3245 chain_seq: 1,
3246 entry_hash: c.hash_at(1),
3247 },
3248 c.prev_hash(),
3249 at(45),
3250 "trust me",
3251 );
3252 c.rotate(&new, &rotation);
3253
3254 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk));
3257 assert!(!v.ok, "{v:#?}");
3258 let p = v.entries[2].problem.as_deref().unwrap();
3259 assert!(p.contains("outside the trust anchor"), "{p}");
3260 assert!(!v.entries[2].signature_valid, "checked under the old key");
3261 assert!(v.repudiated.is_empty(), "and nothing is repudiated");
3262 assert_eq!(v.public_key, (&old_pk).into());
3263 }
3264
3265 #[test]
3269 fn a_thiefs_rotation_is_void_once_a_compromise_names_an_earlier_head() {
3270 let (steward, steward_pk) = generate_keypair();
3271 let (thief, thief_pk) = generate_keypair();
3272 let (recovery, recovery_pk) = generate_keypair();
3273 let anchor = anchored(&steward_pk).with((&recovery_pk).into());
3274
3275 let mut c = Chain::new();
3276 c.decision(&steward); let stolen = KeyRotation::routine(
3278 (&steward_pk).into(),
3279 &thief,
3280 c.prev_hash(),
3281 at(25),
3282 "routine",
3283 );
3284 c.rotate(&steward, &stolen); c.decision(&thief); let declaration = KeyRotation::compromise(
3288 (&steward_pk).into(),
3289 &recovery,
3290 TrustedHead {
3291 id: gov(1),
3292 chain_seq: 1,
3293 entry_hash: c.hash_at(1),
3294 },
3295 c.prev_hash(),
3296 at(55),
3297 "key stolen; everything after GOV-2026-0001 is disclaimed",
3298 );
3299 c.rotate(&recovery, &declaration); let v = verify_chain(&c.links, &steward_pk, &anchor);
3302 assert!(v.ok, "{v:#?}");
3303 assert_eq!(v.repudiated, vec![key_id(1), gov(2)]);
3304 assert!(v.entries[1].repudiated && v.entries[2].repudiated);
3305 assert_eq!(
3306 v.unanchored_keys,
3307 vec![PublicKeyHex::from(&thief_pk)],
3308 "and the theft was visible as it happened"
3309 );
3310 assert_eq!(v.public_key, (&recovery_pk).into());
3311 assert_eq!(v.keys.len(), 2, "the thief's key is not part of history");
3312 assert_eq!(v.keys[0].public_key, (&steward_pk).into());
3313 assert_eq!(v.keys[0].status, KeyStatus::Compromised);
3314 assert_eq!(v.keys[1].public_key, (&recovery_pk).into());
3315 }
3316
3317 #[test]
3318 fn a_compromise_must_name_a_real_head_and_the_key_that_held_it() {
3319 let (old, old_pk) = generate_keypair();
3320 let (new, new_pk) = generate_keypair();
3321 let anchor = anchored(&old_pk).with((&new_pk).into());
3322 let head = |c: &Chain| TrustedHead {
3323 id: gov(1),
3324 chain_seq: 1,
3325 entry_hash: c.hash_at(1),
3326 };
3327
3328 let mut c = Chain::new();
3330 c.decision(&old);
3331 let mut trusted = head(&c);
3332 trusted.entry_hash = data_hash(&json!("nope"));
3333 let rotation = KeyRotation::compromise(
3334 (&old_pk).into(),
3335 &new,
3336 trusted,
3337 c.prev_hash(),
3338 at(45),
3339 "n",
3340 );
3341 c.rotate(&new, &rotation);
3342 let v = verify_chain(&c.links, &old_pk, &anchor);
3343 let p = v.entries[1].problem.as_deref().unwrap();
3344 assert!(p.contains("last_trusted"), "{p}");
3345
3346 let (other, _) = generate_keypair();
3348 let mut c = Chain::new();
3349 c.decision(&old);
3350 let rotation = KeyRotation::compromise(
3351 (&other.verifying_key()).into(),
3352 &new,
3353 head(&c),
3354 c.prev_hash(),
3355 at(45),
3356 "n",
3357 );
3358 c.rotate(&new, &rotation);
3359 let v = verify_chain(&c.links, &old_pk, &anchor);
3360 let p = v.entries[1].problem.as_deref().unwrap();
3361 assert!(p.contains("old_key is not the key"), "{p}");
3362
3363 let mut c = Chain::new();
3365 c.decision(&old);
3366 let mut rotation = KeyRotation::routine(
3367 (&old_pk).into(),
3368 &new,
3369 c.prev_hash(),
3370 at(45),
3371 "n",
3372 );
3373 rotation.last_trusted = Some(head(&c));
3374 c.rotate(&old, &rotation);
3375 let v = verify_chain(&c.links, &old_pk, &anchor);
3376 let p = v.entries[1].problem.as_deref().unwrap();
3377 assert!(p.contains("must not name last_trusted"), "{p}");
3378 }
3379
3380 #[test]
3381 fn a_genesis_key_outside_the_anchor_is_reported_not_rejected() {
3382 let (key, pk) = generate_keypair();
3383 let c = chain(&key, 2);
3384 let v = verify_chain(&c, &pk, &KeyAnchor::default());
3385 assert!(v.ok, "{v:#?}");
3386 assert_eq!(v.unanchored_keys, vec![PublicKeyHex::from(&pk)]);
3387 assert_eq!(v.keys.len(), 1);
3388 assert_eq!(v.keys[0].status, KeyStatus::Active);
3389 assert_eq!(v.keys[0].from_seq, 1);
3390 assert_eq!(v.keys[0].through_seq, None);
3391 }
3392
3393 #[test]
3394 fn published_keys_are_curve_points_and_make_an_anchor() {
3395 let anchor = KeyAnchor::published();
3396 assert!(!anchor.is_empty());
3397 assert_eq!(anchor.keys().count(), PUBLISHED_KEYS.len());
3398 for published in PUBLISHED_KEYS {
3399 let key: PublicKeyHex = published.parse().unwrap();
3400 assert!(anchor.contains(&key));
3401 assert!(
3402 key.to_verifying_key().is_ok(),
3403 "{published} is not a valid Ed25519 public key"
3404 );
3405 }
3406 let (_, other) = generate_keypair();
3407 assert!(!anchor.contains(&(&other).into()));
3408 assert!(
3409 anchor
3410 .clone()
3411 .with((&other).into())
3412 .contains(&(&other).into())
3413 );
3414 }
3415
3416 #[test]
3419 fn amendments_and_rotations_round_trip_as_entry_data() {
3420 let (key, pk) = generate_keypair();
3421 let amendment = Amendment::new(
3422 gov(1),
3423 data_hash(&json!("x")),
3424 AmendmentKind::Superseded,
3425 "Art. VI § 2",
3426 "superseded by GOV-2026-0009",
3427 )
3428 .unwrap()
3429 .with_authority(gov(9))
3430 .with_rationale("the later decision covers the same subject");
3431 let value = serde_json::to_value(&amendment).unwrap();
3432 assert_eq!(value["kind"], "superseded");
3433 assert_eq!(value["agora_governance_amendment"], 1);
3434 assert!(value.get("redaction").is_none(), "{value}");
3435 assert_eq!(
3436 serde_json::from_value::<Amendment>(value).unwrap(),
3437 amendment
3438 );
3439
3440 let rotation = KeyRotation::compromise(
3441 (&pk).into(),
3442 &key,
3443 TrustedHead {
3444 id: gov(1),
3445 chain_seq: 1,
3446 entry_hash: data_hash(&json!("x")),
3447 },
3448 Some(data_hash(&json!("prev"))),
3449 at(0),
3450 "note",
3451 );
3452 let value = serde_json::to_value(&rotation).unwrap();
3453 assert_eq!(value["reason"], "compromise");
3454 assert_eq!(value["last_trusted"]["chain_seq"], 1);
3455 assert_eq!(
3456 serde_json::from_value::<KeyRotation>(value).unwrap(),
3457 rotation
3458 );
3459
3460 let notice = AmendmentNotice::new(amd(1), at(0), &amendment);
3461 let value = serde_json::to_value(¬ice).unwrap();
3462 assert_eq!(value["id"], "AMD-2026-0001");
3463 assert_eq!(value["note"], "superseded by GOV-2026-0009");
3464 assert_eq!(
3465 serde_json::from_value::<AmendmentNotice>(value).unwrap(),
3466 notice
3467 );
3468 }
3469
3470 #[test]
3474 fn an_amendment_verifies_with_or_without_its_optional_fields() {
3475 let (key, pk) = generate_keypair();
3476 let mut c = Chain::new();
3477 let target = c.decision(&key);
3478 let bare = Amendment::new(
3479 target.clone(),
3480 c.hash_at(1),
3481 AmendmentKind::Correction,
3482 "clerical",
3483 "typo in the citation",
3484 )
3485 .unwrap();
3486 assert!(
3487 serde_json::to_value(&bare)
3488 .unwrap()
3489 .get("rationale")
3490 .is_none()
3491 );
3492 c.amend(&key, &bare);
3493 let full = bare.clone().with_rationale("at length: …");
3494 c.amend(&key, &full);
3495 let mut future = serde_json::to_value(&full).unwrap();
3497 future["superseded_by_something_new"] = json!(["later"]);
3498 c.push(
3499 &key,
3500 amd(3),
3501 GovernanceLogEntryType::Amendment,
3502 future,
3503 true,
3504 );
3505
3506 let v = verify_chain(&c.links, &pk, &anchored(&pk));
3507 assert!(v.ok, "{v:#?}");
3508 assert_eq!(
3509 v.entries[0].amended_by,
3510 vec![amd(1), amd(2), amd(3)],
3511 "all three name the target"
3512 );
3513 }
3514
3515 #[test]
3518 fn pre_0_26_wire_still_deserializes() {
3519 let link: GovernanceChainLink = serde_json::from_value(json!({
3520 "id": "GOV-2026-0001",
3521 "entry_type": "council_decision",
3522 "created_at": "2023-11-14T22:13:20.123456Z",
3523 "attestation": {
3524 "envelope_version": 1,
3525 "chain_seq": 1,
3526 "prev_hash": null,
3527 "data_hash": "00".repeat(32),
3528 "entry_hash": "11".repeat(32),
3529 "signature": "22".repeat(64),
3530 "signed_at": "2023-11-14T22:13:21Z",
3531 "retroactive": false,
3532 },
3533 }))
3534 .unwrap();
3535 assert!(link.data.is_none());
3536 assert!(
3537 serde_json::to_value(&link).unwrap().get("data").is_none(),
3538 "and a link without data does not grow a null field"
3539 );
3540
3541 let verdict: EntryVerdict = serde_json::from_value(json!({
3542 "id": "GOV-2026-0001",
3543 "chain_seq": 1,
3544 "signature_valid": true,
3545 "link_valid": true,
3546 "content_matches": null,
3547 "retroactive": false,
3548 "out_of_order": false,
3549 "amended_by": [],
3550 }))
3551 .unwrap();
3552 assert!(!verdict.repudiated && !verdict.redacted);
3553 assert!(verdict.signed_by.is_none());
3554 assert!(verdict.reattested_by.is_empty());
3555
3556 let verification: GovernanceVerification =
3557 serde_json::from_value(json!({
3558 "public_key": "33".repeat(32),
3559 "ok": true,
3560 "head": "GOV-2026-0001",
3561 "entries": [],
3562 }))
3563 .unwrap();
3564 assert!(verification.keys.is_empty());
3565 assert!(verification.unanchored_keys.is_empty());
3566 assert!(verification.repudiated.is_empty());
3567 }
3568
3569 #[test]
3570 fn the_report_round_trips() {
3571 let (key, pk) = generate_keypair();
3572 let mut c = Chain::new();
3573 let target = c.decision(&key);
3574 let amendment = Amendment::new(
3575 target,
3576 c.hash_at(1),
3577 AmendmentKind::Overruled,
3578 "b",
3579 "n",
3580 )
3581 .unwrap();
3582 c.amend(&key, &amendment);
3583 let v = verify_chain(&c.links, &pk, &anchored(&pk));
3584 let text = serde_json::to_string(&v).unwrap();
3585 assert_eq!(
3586 serde_json::from_str::<GovernanceVerification>(&text).unwrap(),
3587 v
3588 );
3589
3590 let keys = GovernanceSigningKeys { keys: v.keys };
3591 let value = serde_json::to_value(&keys).unwrap();
3592 assert!(value["keys"].is_array(), "an object, not a bare array");
3593 assert_eq!(value["keys"][0]["status"], "active");
3594 assert_eq!(
3595 serde_json::from_value::<GovernanceSigningKeys>(value).unwrap(),
3596 keys
3597 );
3598 }
3599
3600 #[test]
3604 fn envelope_v1_preimage_and_hash_are_pinned() {
3605 let envelope = Envelope::new(
3606 gov(6),
3607 GovernanceLogEntryType::CouncilDecision,
3608 at(0),
3609 Some(Sha256Hex::from([0x11; 32])),
3610 data_hash(&json!({"outcome": "approved", "title": "Ratification"})),
3611 );
3612 assert_eq!(
3613 String::from_utf8(envelope.preimage()).unwrap(),
3614 "{\"agora_governance_log\":1,\"id\":\"GOV-2026-0006\",\
3615 \"entry_type\":\"council_decision\",\
3616 \"created_at\":1700000000123456,\
3617 \"prev_hash\":\"1111111111111111111111111111111111111111111111111111111111111111\",\
3618 \"data_hash\":\"a4adf645ae3f60c56484d01aea87d6d490321d7fc66b1607df14b023fe567c7b\"}"
3619 );
3620 assert_eq!(
3621 envelope.entry_hash().to_hex(),
3622 "ba27577432f81e415f1c01cc4cfabab6070e3ac50fd468fffe195ef19c0e9464"
3623 );
3624 }
3625
3626 #[cfg(feature = "agora-client")]
3633 #[tokio::test]
3634 #[ignore = "networked: hits the live platform"]
3635 async fn the_published_key_is_the_one_the_platform_serves() {
3636 let client = crate::client::Client::new(
3637 url::Url::parse("https://subliminal.technology").unwrap(),
3638 )
3639 .unwrap();
3640 let served = client.get_governance_signing_key().await.unwrap();
3641 let newest: PublicKeyHex = PUBLISHED_KEYS
3642 .last()
3643 .expect("PUBLISHED_KEYS is never empty")
3644 .parse()
3645 .unwrap();
3646 assert_eq!(served.algorithm, "ed25519");
3647 assert_eq!(
3648 served.public_key, newest,
3649 "the platform serves {} but the newest key in PUBLISHED_KEYS is \
3650 {newest} — if this is a rotation, agentkit publishes the new \
3651 key FIRST and the rotation entry second",
3652 served.public_key
3653 );
3654 }
3655
3656 #[cfg(feature = "schemars")]
3657 #[test]
3658 fn wire_schemas_are_ref_free() {
3659 use crate::responses::inline_schema_for;
3660 for (name, schema) in [
3661 (
3662 "GovernanceAttestation",
3663 inline_schema_for::<GovernanceAttestation>(),
3664 ),
3665 (
3666 "GovernanceChainLink",
3667 inline_schema_for::<GovernanceChainLink>(),
3668 ),
3669 (
3670 "GovernanceSigningKey",
3671 inline_schema_for::<GovernanceSigningKey>(),
3672 ),
3673 (
3674 "GovernanceVerification",
3675 inline_schema_for::<GovernanceVerification>(),
3676 ),
3677 (
3678 "Vec<GovernanceChainLink>",
3679 inline_schema_for::<Vec<GovernanceChainLink>>(),
3680 ),
3681 ("Amendment", inline_schema_for::<Amendment>()),
3682 ("Redaction", inline_schema_for::<Redaction>()),
3683 ("AmendmentNotice", inline_schema_for::<AmendmentNotice>()),
3684 ("KeyRotation", inline_schema_for::<KeyRotation>()),
3685 ("TrustedHead", inline_schema_for::<TrustedHead>()),
3686 (
3687 "GovernanceKeyRecord",
3688 inline_schema_for::<GovernanceKeyRecord>(),
3689 ),
3690 (
3691 "GovernanceSigningKeys",
3692 inline_schema_for::<GovernanceSigningKeys>(),
3693 ),
3694 ("AmendmentKind", inline_schema_for::<AmendmentKind>()),
3695 ("Standing", inline_schema_for::<Standing>()),
3696 ("KeyStatus", inline_schema_for::<KeyStatus>()),
3697 ("RotationReason", inline_schema_for::<RotationReason>()),
3698 ] {
3699 let text = serde_json::to_string(&schema).unwrap();
3700 assert!(!text.contains("$ref"), "{name} must be $ref-free: {text}");
3701 assert!(
3702 !text.contains("$defs"),
3703 "{name} must be $defs-free: {text}"
3704 );
3705 }
3706 let text =
3707 serde_json::to_string(&inline_schema_for::<Sha256Hex>()).unwrap();
3708 assert!(text.contains("^[0-9a-f]{64}$"), "{text}");
3709 let text = serde_json::to_string(&inline_schema_for::<SignatureHex>())
3710 .unwrap();
3711 assert!(text.contains("^[0-9a-f]{128}$"), "{text}");
3712 }
3713}