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
218impl From<Signature> for SignatureHex {
219 fn from(sig: Signature) -> Self {
220 Self(sig.to_bytes())
221 }
222}
223
224impl From<&SignatureHex> for Signature {
225 fn from(sig: &SignatureHex) -> Self {
226 Signature::from_bytes(&sig.0)
227 }
228}
229
230impl From<&VerifyingKey> for PublicKeyHex {
231 fn from(key: &VerifyingKey) -> Self {
232 Self(key.to_bytes())
233 }
234}
235
236impl PublicKeyHex {
237 pub fn to_verifying_key(
239 &self,
240 ) -> Result<VerifyingKey, ed25519_dalek::SignatureError> {
241 VerifyingKey::from_bytes(&self.0)
242 }
243}
244
245pub fn canonical_json(value: &serde_json::Value) -> Vec<u8> {
258 let mut out = Vec::new();
259 write_canonical(value, &mut out);
260 out
261}
262
263fn write_canonical(value: &serde_json::Value, out: &mut Vec<u8>) {
264 use serde_json::Value;
265 match value {
266 Value::Null => out.extend_from_slice(b"null"),
267 Value::Bool(b) => {
268 out.extend_from_slice(if *b { b"true" } else { b"false" })
269 }
270 Value::Number(n) => serde_json::to_writer(&mut *out, n)
271 .expect("a number always serializes"),
272 Value::String(s) => serde_json::to_writer(&mut *out, s)
273 .expect("a string always serializes"),
274 Value::Array(items) => {
275 out.push(b'[');
276 for (i, item) in items.iter().enumerate() {
277 if i > 0 {
278 out.push(b',');
279 }
280 write_canonical(item, out);
281 }
282 out.push(b']');
283 }
284 Value::Object(map) => {
285 let mut keys: Vec<&String> = map.keys().collect();
286 keys.sort_unstable();
287 out.push(b'{');
288 for (i, key) in keys.into_iter().enumerate() {
289 if i > 0 {
290 out.push(b',');
291 }
292 serde_json::to_writer(&mut *out, key)
293 .expect("a string always serializes");
294 out.push(b':');
295 write_canonical(&map[key], out);
296 }
297 out.push(b'}');
298 }
299 }
300}
301
302pub fn data_hash(data: &serde_json::Value) -> Sha256Hex {
304 Sha256Hex(Sha256::digest(canonical_json(data)).into())
305}
306
307#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
312pub struct Envelope {
313 pub agora_governance_log: u32,
315 pub id: GovernanceLogId,
316 pub entry_type: GovernanceLogEntryType,
317 pub created_at: i64,
319 pub prev_hash: Option<Sha256Hex>,
320 pub data_hash: Sha256Hex,
321}
322
323impl Envelope {
324 pub fn new(
326 id: GovernanceLogId,
327 entry_type: GovernanceLogEntryType,
328 created_at: DateTime<Utc>,
329 prev_hash: Option<Sha256Hex>,
330 data_hash: Sha256Hex,
331 ) -> Self {
332 Self {
333 agora_governance_log: ENVELOPE_VERSION,
334 id,
335 entry_type,
336 created_at: created_at.timestamp_micros(),
337 prev_hash,
338 data_hash,
339 }
340 }
341
342 pub fn created_at(&self) -> DateTime<Utc> {
344 DateTime::from_timestamp_micros(self.created_at)
345 .expect("an Envelope only ever holds an in-range timestamp")
346 }
347
348 pub fn preimage(&self) -> Vec<u8> {
350 serde_json::to_vec(self).expect("an Envelope always serializes")
351 }
352
353 pub fn entry_hash(&self) -> Sha256Hex {
355 Sha256Hex(Sha256::digest(self.preimage()).into())
356 }
357}
358
359pub fn truncate_to_micros(t: DateTime<Utc>) -> DateTime<Utc> {
362 DateTime::from_timestamp_micros(t.timestamp_micros())
363 .expect("a timestamp that came from a DateTime is in range")
364}
365
366pub fn truncate_to_seconds(t: DateTime<Utc>) -> DateTime<Utc> {
369 DateTime::from_timestamp(t.timestamp(), 0)
370 .expect("a timestamp that came from a DateTime is in range")
371}
372
373pub fn is_retroactive(
377 created_at: DateTime<Utc>,
378 signed_at: DateTime<Utc>,
379) -> bool {
380 signed_at - created_at > RETROACTIVE_AFTER
381}
382
383#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
389#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
390#[cfg_attr(feature = "schemars", schemars(inline))]
391pub struct GovernanceAttestation {
392 pub envelope_version: u32,
394 pub chain_seq: u64,
397 pub prev_hash: Option<Sha256Hex>,
399 pub data_hash: Sha256Hex,
401 pub entry_hash: Sha256Hex,
403 pub signature: SignatureHex,
406 pub signed_at: DateTime<Utc>,
409 pub retroactive: bool,
413}
414
415#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
419#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
420#[cfg_attr(feature = "schemars", schemars(inline))]
421pub struct GovernanceChainLink {
422 pub id: GovernanceLogId,
423 pub entry_type: GovernanceLogEntryType,
424 pub created_at: DateTime<Utc>,
425 pub attestation: GovernanceAttestation,
426 #[serde(default, skip_serializing_if = "Option::is_none")]
433 pub data: Option<serde_json::Value>,
434}
435
436#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
439#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
440#[cfg_attr(feature = "schemars", schemars(inline))]
441pub struct GovernanceSigningKey {
442 pub algorithm: String,
444 pub public_key: PublicKeyHex,
445 pub envelope_version: u32,
447}
448
449impl GovernanceSigningKey {
450 pub fn new(key: &VerifyingKey) -> Self {
452 Self {
453 algorithm: "ed25519".to_string(),
454 public_key: key.into(),
455 envelope_version: ENVELOPE_VERSION,
456 }
457 }
458}
459
460pub const AMENDMENT_VERSION: u32 = 1;
466
467#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
469pub enum AmendmentError {
470 #[error("agora_governance_amendment is {0}, not {AMENDMENT_VERSION}")]
471 UnsupportedVersion(u32),
472 #[error("kind `redaction` requires a `redaction`")]
473 MissingRedaction,
474 #[error("`redaction` is only valid on kind `redaction`")]
475 UnexpectedRedaction,
476 #[error("amendment target {0} is not an entry of this chain")]
477 UnknownTarget(GovernanceLogId),
478 #[error("amendment target {0} is not an earlier entry")]
479 ForwardReference(GovernanceLogId),
480 #[error("target_entry_hash is not {0}'s entry_hash")]
481 WrongTargetHash(GovernanceLogId),
482}
483
484#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
490#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
491#[cfg_attr(feature = "schemars", schemars(inline))]
492pub struct Amendment {
493 pub agora_governance_amendment: u32,
495 pub target: GovernanceLogId,
496 pub target_entry_hash: Sha256Hex,
498 pub kind: AmendmentKind,
499 #[serde(default)]
502 pub authority: Option<GovernanceLogId>,
503 pub basis: String,
506 pub note: String,
508 #[serde(default, skip_serializing_if = "Option::is_none")]
511 pub rationale: Option<String>,
512 #[serde(default, skip_serializing_if = "Option::is_none")]
514 pub redaction: Option<Redaction>,
515}
516
517#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
519#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
520#[cfg_attr(feature = "schemars", schemars(inline))]
521pub struct Redaction {
522 pub fields: Vec<String>,
525 pub resulting_data_hash: Sha256Hex,
528}
529
530impl Amendment {
531 pub fn new(
537 target: GovernanceLogId,
538 target_entry_hash: Sha256Hex,
539 kind: AmendmentKind,
540 basis: impl Into<String>,
541 note: impl Into<String>,
542 ) -> Result<Self, AmendmentError> {
543 if kind == AmendmentKind::Redaction {
544 return Err(AmendmentError::MissingRedaction);
545 }
546 Ok(Self {
547 agora_governance_amendment: AMENDMENT_VERSION,
548 target,
549 target_entry_hash,
550 kind,
551 authority: None,
552 basis: basis.into(),
553 note: note.into(),
554 rationale: None,
555 redaction: None,
556 })
557 }
558
559 pub fn with_authority(mut self, authority: GovernanceLogId) -> Self {
561 self.authority = Some(authority);
562 self
563 }
564
565 pub fn with_rationale(mut self, rationale: impl Into<String>) -> Self {
567 self.rationale = Some(rationale.into());
568 self
569 }
570
571 pub fn redaction(
579 amendment_id: &GovernanceLogId,
580 target: GovernanceLogId,
581 target_entry_hash: Sha256Hex,
582 basis: impl Into<String>,
583 note: impl Into<String>,
584 fields: Vec<String>,
585 data: &serde_json::Value,
586 ) -> Result<(Self, serde_json::Value), RedactError> {
587 let redacted = redact_data(data, &fields, amendment_id)?;
588 Ok((
589 Self {
590 agora_governance_amendment: AMENDMENT_VERSION,
591 target,
592 target_entry_hash,
593 kind: AmendmentKind::Redaction,
594 authority: None,
595 basis: basis.into(),
596 note: note.into(),
597 rationale: None,
598 redaction: Some(Redaction {
599 fields,
600 resulting_data_hash: data_hash(&redacted),
601 }),
602 },
603 redacted,
604 ))
605 }
606
607 pub fn validate(&self) -> Result<(), AmendmentError> {
610 if self.agora_governance_amendment != AMENDMENT_VERSION {
611 return Err(AmendmentError::UnsupportedVersion(
612 self.agora_governance_amendment,
613 ));
614 }
615 match (self.kind, &self.redaction) {
616 (AmendmentKind::Redaction, None) => {
617 Err(AmendmentError::MissingRedaction)
618 }
619 (k, Some(_)) if k != AmendmentKind::Redaction => {
620 Err(AmendmentError::UnexpectedRedaction)
621 }
622 _ => Ok(()),
623 }
624 }
625}
626
627pub fn kind_standing(kind: AmendmentKind) -> Option<Standing> {
630 match kind {
631 AmendmentKind::NonPrecedential => Some(Standing::NonPrecedential),
632 AmendmentKind::Overruled => Some(Standing::Overruled),
633 AmendmentKind::Superseded => Some(Standing::Superseded),
634 AmendmentKind::Reinstated => Some(Standing::InForce),
635 AmendmentKind::Correction
636 | AmendmentKind::Redaction
637 | AmendmentKind::Reattested => None,
638 }
639}
640
641pub fn standing(kinds: impl IntoIterator<Item = AmendmentKind>) -> Standing {
644 kinds
645 .into_iter()
646 .filter_map(kind_standing)
647 .last()
648 .unwrap_or_default()
649}
650
651#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
653pub enum RedactError {
654 #[error("pointer {0:?} does not resolve in the entry's data")]
655 Unresolved(String),
656 #[error("the empty pointer would redact the whole entry")]
657 WholeEntry,
658}
659
660pub fn redaction_marker(amendment_id: &GovernanceLogId) -> String {
662 format!("[redacted by {amendment_id}]")
663}
664
665pub fn redact_data(
673 data: &serde_json::Value,
674 fields: &[String],
675 amendment_id: &GovernanceLogId,
676) -> Result<serde_json::Value, RedactError> {
677 let marker = serde_json::Value::String(redaction_marker(amendment_id));
678 let mut out = data.clone();
679 for pointer in fields {
680 if pointer.is_empty() {
681 return Err(RedactError::WholeEntry);
682 }
683 let slot = out
684 .pointer_mut(pointer)
685 .ok_or_else(|| RedactError::Unresolved(pointer.clone()))?;
686 *slot = marker.clone();
687 }
688 Ok(out)
689}
690
691#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
693#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
694#[cfg_attr(feature = "schemars", schemars(inline))]
695pub struct AmendmentNotice {
696 pub id: GovernanceLogId,
697 pub kind: AmendmentKind,
698 #[serde(default)]
699 pub authority: Option<GovernanceLogId>,
700 pub basis: String,
701 pub note: String,
702 #[serde(default, skip_serializing_if = "Option::is_none")]
704 pub rationale: Option<String>,
705 pub created_at: DateTime<Utc>,
706}
707
708impl AmendmentNotice {
709 pub fn new(
711 id: GovernanceLogId,
712 created_at: DateTime<Utc>,
713 amendment: &Amendment,
714 ) -> Self {
715 Self {
716 id,
717 kind: amendment.kind,
718 authority: amendment.authority.clone(),
719 basis: amendment.basis.clone(),
720 note: amendment.note.clone(),
721 rationale: amendment.rationale.clone(),
722 created_at,
723 }
724 }
725}
726
727pub const KEY_ROTATION_VERSION: u32 = 1;
733
734pub const PUBLISHED_KEYS: &[&str] =
744 &["ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a"];
745
746#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
748#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
749#[cfg_attr(feature = "schemars", schemars(inline))]
750#[serde(rename_all = "snake_case")]
751pub enum RotationReason {
752 Routine,
754 Compromise,
757}
758
759#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
761#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
762#[cfg_attr(feature = "schemars", schemars(inline))]
763pub struct TrustedHead {
764 pub id: GovernanceLogId,
765 pub chain_seq: u64,
766 pub entry_hash: Sha256Hex,
767}
768
769#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
771pub enum RotationError {
772 #[error("agora_governance_key_rotation is {0}, not {KEY_ROTATION_VERSION}")]
773 UnsupportedVersion(u32),
774 #[error("new_key is not a valid Ed25519 public key")]
775 BadNewKey,
776 #[error(
777 "the proof of possession does not verify for this rotation at this position"
778 )]
779 BadProof,
780 #[error("a compromise rotation must name last_trusted")]
781 MissingLastTrusted,
782 #[error("a routine rotation must not name last_trusted")]
783 UnexpectedLastTrusted,
784 #[error("old_key is not the key that was in force")]
785 WrongOldKey,
786 #[error("last_trusted does not name an earlier entry of this chain")]
787 UnknownLastTrusted,
788 #[error(
789 "a compromise rotation to a key outside the trust anchor authenticates nothing"
790 )]
791 UnanchoredNewKey,
792 #[error("new_key has already held this chain; a key is never brought back")]
793 ReusedKey,
794 #[error("last_trusted names an entry an earlier compromise repudiated")]
795 RepudiatedLastTrusted,
796}
797
798#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
805#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
806#[cfg_attr(feature = "schemars", schemars(inline))]
807pub struct KeyRotation {
808 pub agora_governance_key_rotation: u32,
810 pub reason: RotationReason,
811 pub old_key: PublicKeyHex,
812 pub new_key: PublicKeyHex,
813 pub proof: SignatureHex,
817 pub proof_signed_at: i64,
819 #[serde(default, skip_serializing_if = "Option::is_none")]
821 pub last_trusted: Option<TrustedHead>,
822 pub note: String,
823}
824
825#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
832pub struct RotationStatement {
833 pub agora_governance_key_rotation: u32,
835 pub reason: RotationReason,
836 pub old_key: PublicKeyHex,
837 pub new_key: PublicKeyHex,
838 pub prev_hash: Option<Sha256Hex>,
839}
840
841impl RotationStatement {
842 pub fn new(
844 reason: RotationReason,
845 old_key: PublicKeyHex,
846 new_key: PublicKeyHex,
847 prev_hash: Option<Sha256Hex>,
848 ) -> Self {
849 Self {
850 agora_governance_key_rotation: KEY_ROTATION_VERSION,
851 reason,
852 old_key,
853 new_key,
854 prev_hash,
855 }
856 }
857
858 pub fn preimage(&self) -> Vec<u8> {
860 serde_json::to_vec(self).expect("a RotationStatement always serializes")
861 }
862
863 pub fn hash(&self) -> Sha256Hex {
865 Sha256Hex(Sha256::digest(self.preimage()).into())
866 }
867}
868
869impl KeyRotation {
870 pub fn routine(
875 old_key: PublicKeyHex,
876 new_signing_key: &SigningKey,
877 prev_hash: Option<Sha256Hex>,
878 now: DateTime<Utc>,
879 note: impl Into<String>,
880 ) -> Self {
881 Self::build(
882 RotationReason::Routine,
883 old_key,
884 new_signing_key,
885 None,
886 prev_hash,
887 now,
888 note,
889 )
890 }
891
892 pub fn compromise(
901 old_key: PublicKeyHex,
902 new_signing_key: &SigningKey,
903 last_trusted: TrustedHead,
904 prev_hash: Option<Sha256Hex>,
905 now: DateTime<Utc>,
906 note: impl Into<String>,
907 ) -> Self {
908 Self::build(
909 RotationReason::Compromise,
910 old_key,
911 new_signing_key,
912 Some(last_trusted),
913 prev_hash,
914 now,
915 note,
916 )
917 }
918
919 #[allow(clippy::too_many_arguments)]
920 fn build(
921 reason: RotationReason,
922 old_key: PublicKeyHex,
923 new_signing_key: &SigningKey,
924 last_trusted: Option<TrustedHead>,
925 prev_hash: Option<Sha256Hex>,
926 now: DateTime<Utc>,
927 note: impl Into<String>,
928 ) -> Self {
929 let new_key = PublicKeyHex::from(&new_signing_key.verifying_key());
930 let proof_signed_at = truncate_to_seconds(now).timestamp();
931 let statement =
932 RotationStatement::new(reason, old_key, new_key, prev_hash);
933 let proof = crypto::sign(
934 new_signing_key,
935 statement.hash().as_bytes(),
936 proof_signed_at,
937 );
938 Self {
939 agora_governance_key_rotation: KEY_ROTATION_VERSION,
940 reason,
941 old_key,
942 new_key,
943 proof: proof.into(),
944 proof_signed_at,
945 last_trusted,
946 note: note.into(),
947 }
948 }
949
950 pub fn statement(&self, prev_hash: Option<Sha256Hex>) -> RotationStatement {
952 RotationStatement::new(
953 self.reason,
954 self.old_key,
955 self.new_key,
956 prev_hash,
957 )
958 }
959
960 pub fn verify_proof(
964 &self,
965 prev_hash: Option<Sha256Hex>,
966 ) -> Result<(), RotationError> {
967 if self.agora_governance_key_rotation != KEY_ROTATION_VERSION {
968 return Err(RotationError::UnsupportedVersion(
969 self.agora_governance_key_rotation,
970 ));
971 }
972 match (self.reason, &self.last_trusted) {
973 (RotationReason::Compromise, None) => {
974 return Err(RotationError::MissingLastTrusted);
975 }
976 (RotationReason::Routine, Some(_)) => {
977 return Err(RotationError::UnexpectedLastTrusted);
978 }
979 _ => {}
980 }
981 let new_key = self
982 .new_key
983 .to_verifying_key()
984 .map_err(|_| RotationError::BadNewKey)?;
985 crypto::verify(
986 &new_key,
987 self.statement(prev_hash).hash().as_bytes(),
988 self.proof_signed_at,
989 &Signature::from(&self.proof),
990 )
991 .then_some(())
992 .ok_or(RotationError::BadProof)
993 }
994}
995
996#[derive(Debug, Clone, Default, PartialEq, Eq)]
1004pub struct KeyAnchor {
1005 keys: HashSet<PublicKeyHex>,
1006}
1007
1008impl KeyAnchor {
1009 pub fn published() -> Self {
1011 PUBLISHED_KEYS
1012 .iter()
1013 .map(|k| {
1014 k.parse()
1015 .expect("PUBLISHED_KEYS are valid 32-byte hex keys")
1016 })
1017 .collect()
1018 }
1019
1020 pub fn pinned(key: PublicKeyHex) -> Self {
1022 std::iter::once(key).collect()
1023 }
1024
1025 pub fn with(mut self, key: PublicKeyHex) -> Self {
1027 self.keys.insert(key);
1028 self
1029 }
1030
1031 pub fn contains(&self, key: &PublicKeyHex) -> bool {
1032 self.keys.contains(key)
1033 }
1034
1035 pub fn is_empty(&self) -> bool {
1036 self.keys.is_empty()
1037 }
1038
1039 pub fn keys(&self) -> impl Iterator<Item = &PublicKeyHex> {
1041 self.keys.iter()
1042 }
1043}
1044
1045impl FromIterator<PublicKeyHex> for KeyAnchor {
1046 fn from_iter<I: IntoIterator<Item = PublicKeyHex>>(iter: I) -> Self {
1047 Self {
1048 keys: iter.into_iter().collect(),
1049 }
1050 }
1051}
1052
1053#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1056#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1057#[cfg_attr(feature = "schemars", schemars(inline))]
1058pub struct GovernanceKeyRecord {
1059 pub public_key: PublicKeyHex,
1060 pub from_seq: u64,
1062 #[serde(default)]
1066 pub through_seq: Option<u64>,
1067 pub status: KeyStatus,
1068 #[serde(default)]
1071 pub introduced_by: Option<GovernanceLogId>,
1072 #[serde(default)]
1074 pub retired_by: Option<GovernanceLogId>,
1075}
1076
1077#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1082#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1083#[cfg_attr(feature = "schemars", schemars(inline))]
1084pub struct GovernanceSigningKeys {
1085 pub keys: Vec<GovernanceKeyRecord>,
1087}
1088
1089#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1091#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1092#[cfg_attr(feature = "schemars", schemars(inline))]
1093pub struct EntryVerdict {
1094 pub id: GovernanceLogId,
1095 pub chain_seq: u64,
1096 pub signature_valid: bool,
1099 pub link_valid: bool,
1102 #[serde(default)]
1108 pub content_matches: Option<bool>,
1109 pub retroactive: bool,
1111 pub out_of_order: bool,
1114 #[serde(default)]
1116 pub amended_by: Vec<GovernanceLogId>,
1117 #[serde(default, skip_serializing_if = "Option::is_none")]
1120 pub signed_by: Option<PublicKeyHex>,
1121 #[serde(default)]
1124 pub redacted: bool,
1125 #[serde(default, skip_serializing_if = "Option::is_none")]
1127 pub redacted_data_hash: Option<Sha256Hex>,
1128 #[serde(default)]
1131 pub repudiated: bool,
1132 #[serde(default)]
1135 pub reattested_by: Vec<GovernanceLogId>,
1136 #[serde(default, skip_serializing_if = "Option::is_none")]
1138 pub problem: Option<String>,
1139}
1140
1141#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1143#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1144#[cfg_attr(feature = "schemars", schemars(inline))]
1145pub struct GovernanceVerification {
1146 pub public_key: PublicKeyHex,
1148 pub ok: bool,
1154 #[serde(default)]
1156 pub head: Option<GovernanceLogId>,
1157 pub entries: Vec<EntryVerdict>,
1159 #[serde(default)]
1161 pub keys: Vec<GovernanceKeyRecord>,
1162 #[serde(default)]
1167 pub unanchored_keys: Vec<PublicKeyHex>,
1168 #[serde(default)]
1170 pub repudiated: Vec<GovernanceLogId>,
1171}
1172
1173impl GovernanceVerification {
1174 pub fn settle(mut self) -> Self {
1176 self.ok = self.entries.iter().all(|e| {
1177 e.signature_valid
1178 && e.link_valid
1179 && e.content_matches != Some(false)
1180 && e.problem.is_none()
1181 });
1182 self
1183 }
1184
1185 pub fn check_content(
1195 &mut self,
1196 link: &GovernanceChainLink,
1197 data: &serde_json::Value,
1198 ) -> bool {
1199 let hash = data_hash(data);
1200 let Some(entry) = self.entries.iter_mut().find(|e| e.id == link.id)
1201 else {
1202 return false;
1203 };
1204 let ok = hash == link.attestation.data_hash
1205 || entry.redacted_data_hash == Some(hash);
1206 entry.content_matches = Some(ok);
1207 ok
1208 }
1209}
1210
1211pub fn attest(
1222 key: &SigningKey,
1223 envelope: &Envelope,
1224 chain_seq: u64,
1225 signed_at: DateTime<Utc>,
1226) -> GovernanceAttestation {
1227 let signed_at = truncate_to_seconds(signed_at);
1228 let entry_hash = envelope.entry_hash();
1229 let signature =
1230 crypto::sign(key, entry_hash.as_bytes(), signed_at.timestamp());
1231 GovernanceAttestation {
1232 envelope_version: envelope.agora_governance_log,
1233 chain_seq,
1234 prev_hash: envelope.prev_hash,
1235 data_hash: envelope.data_hash,
1236 entry_hash,
1237 signature: signature.into(),
1238 signed_at,
1239 retroactive: is_retroactive(envelope.created_at(), signed_at),
1240 }
1241}
1242
1243#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1249pub enum LinkError {
1250 #[error(
1251 "envelope version {0} is not supported (this verifier knows {ENVELOPE_VERSION})"
1252 )]
1253 UnsupportedVersion(u32),
1254 #[error("entry_hash does not recompute from the envelope fields")]
1255 HashMismatch,
1256 #[error("signature does not verify under the published key")]
1257 BadSignature,
1258}
1259
1260pub fn recompute_entry_hash(link: &GovernanceChainLink) -> Sha256Hex {
1262 Envelope::new(
1263 link.id.clone(),
1264 link.entry_type,
1265 link.created_at,
1266 link.attestation.prev_hash,
1267 link.attestation.data_hash,
1268 )
1269 .entry_hash()
1270}
1271
1272pub fn verify_link(
1274 link: &GovernanceChainLink,
1275 key: &VerifyingKey,
1276) -> Result<(), LinkError> {
1277 let a = &link.attestation;
1278 if a.envelope_version != ENVELOPE_VERSION {
1279 return Err(LinkError::UnsupportedVersion(a.envelope_version));
1280 }
1281 if recompute_entry_hash(link) != a.entry_hash {
1282 return Err(LinkError::HashMismatch);
1283 }
1284 if !crypto::verify(
1285 key,
1286 a.entry_hash.as_bytes(),
1287 a.signed_at.timestamp(),
1288 &Signature::from(&a.signature),
1289 ) {
1290 return Err(LinkError::BadSignature);
1291 }
1292 Ok(())
1293}
1294
1295pub fn verify_data(
1297 link: &GovernanceChainLink,
1298 data: &serde_json::Value,
1299) -> bool {
1300 data_hash(data) == link.attestation.data_hash
1301}
1302
1303fn prefix_problem(link: &GovernanceChainLink) -> Option<String> {
1305 let prefix = link.id.prefix();
1306 let reserved =
1307 matches!(prefix, GovernanceLogPrefix::Amd | GovernanceLogPrefix::Key);
1308 let expected = match link.entry_type {
1309 GovernanceLogEntryType::Amendment => Some(GovernanceLogPrefix::Amd),
1310 GovernanceLogEntryType::KeyRotation => Some(GovernanceLogPrefix::Key),
1311 _ => None,
1312 };
1313 match expected {
1314 Some(want) if prefix != want => Some(format!(
1315 "the id of a {} entry must be in the {want}- series, not {}",
1316 link.entry_type, link.id
1317 )),
1318 None if reserved => Some(format!(
1319 "{prefix}- ids are reserved for amendment and key_rotation \
1320 entries, but {} is a {}",
1321 link.id, link.entry_type
1322 )),
1323 _ => None,
1324 }
1325}
1326
1327fn amendment_target(
1329 amendment: &Amendment,
1330 seq: u64,
1331 seq_of: &HashMap<&str, u64>,
1332 links: &[&GovernanceChainLink],
1333) -> Result<u64, AmendmentError> {
1334 amendment.validate()?;
1335 let target = *seq_of.get(amendment.target.as_str()).ok_or_else(|| {
1336 AmendmentError::UnknownTarget(amendment.target.clone())
1337 })?;
1338 if target >= seq {
1339 return Err(AmendmentError::ForwardReference(amendment.target.clone()));
1340 }
1341 if links[target as usize - 1].attestation.entry_hash
1342 != amendment.target_entry_hash
1343 {
1344 return Err(AmendmentError::WrongTargetHash(amendment.target.clone()));
1345 }
1346 Ok(target)
1347}
1348
1349struct KeyWalk {
1351 history: Vec<(GovernanceKeyRecord, VerifyingKey)>,
1353 unanchored: Vec<PublicKeyHex>,
1354 seen: HashSet<PublicKeyHex>,
1358 repudiated: HashSet<u64>,
1360}
1361
1362impl KeyWalk {
1363 fn new(genesis: &VerifyingKey, anchor: &KeyAnchor) -> Self {
1364 let public_key = PublicKeyHex::from(genesis);
1365 Self {
1366 history: vec![(
1367 GovernanceKeyRecord {
1368 public_key,
1369 from_seq: 1,
1370 through_seq: None,
1371 status: KeyStatus::Active,
1372 introduced_by: None,
1373 retired_by: None,
1374 },
1375 *genesis,
1376 )],
1377 unanchored: if anchor.contains(&public_key) {
1378 Vec::new()
1379 } else {
1380 vec![public_key]
1381 },
1382 seen: HashSet::from([public_key]),
1383 repudiated: HashSet::new(),
1384 }
1385 }
1386
1387 fn in_force(&self, seq: u64) -> (PublicKeyHex, VerifyingKey) {
1389 let (record, key) = self
1390 .history
1391 .iter()
1392 .rev()
1393 .find(|(r, _)| r.from_seq <= seq)
1394 .unwrap_or(&self.history[0]);
1395 (record.public_key, *key)
1396 }
1397
1398 fn active(&self) -> PublicKeyHex {
1400 self.history
1401 .last()
1402 .map(|(r, _)| r.public_key)
1403 .expect("the genesis key is always in the history")
1404 }
1405
1406 fn close(
1407 &mut self,
1408 through_seq: u64,
1409 status: KeyStatus,
1410 by: &GovernanceLogId,
1411 ) {
1412 if let Some((record, _)) = self.history.last_mut() {
1413 record.through_seq = Some(through_seq);
1414 record.status = status;
1415 record.retired_by = Some(by.clone());
1416 }
1417 }
1418
1419 fn open(
1420 &mut self,
1421 public_key: PublicKeyHex,
1422 key: VerifyingKey,
1423 from_seq: u64,
1424 by: &GovernanceLogId,
1425 ) {
1426 self.history.push((
1427 GovernanceKeyRecord {
1428 public_key,
1429 from_seq,
1430 through_seq: None,
1431 status: KeyStatus::Active,
1432 introduced_by: Some(by.clone()),
1433 retired_by: None,
1434 },
1435 key,
1436 ));
1437 }
1438
1439 fn apply(
1441 &mut self,
1442 rotation: &KeyRotation,
1443 link: &GovernanceChainLink,
1444 seq: u64,
1445 links: &[&GovernanceChainLink],
1446 anchor: &KeyAnchor,
1447 ) -> Result<(), RotationError> {
1448 rotation.verify_proof(link.attestation.prev_hash)?;
1449 let new_key = rotation
1450 .new_key
1451 .to_verifying_key()
1452 .map_err(|_| RotationError::BadNewKey)?;
1453 if self.seen.contains(&rotation.new_key) {
1454 return Err(RotationError::ReusedKey);
1455 }
1456 match rotation.reason {
1457 RotationReason::Routine => {
1458 if rotation.old_key != self.in_force(seq).0 {
1459 return Err(RotationError::WrongOldKey);
1460 }
1461 self.close(seq, KeyStatus::Retired, &link.id);
1462 self.open(rotation.new_key, new_key, seq + 1, &link.id);
1463 self.seen.insert(rotation.new_key);
1464 if !anchor.contains(&rotation.new_key) {
1465 self.unanchored.push(rotation.new_key);
1466 }
1467 Ok(())
1468 }
1469 RotationReason::Compromise => {
1470 let head = rotation
1471 .last_trusted
1472 .as_ref()
1473 .ok_or(RotationError::MissingLastTrusted)?;
1474 let trusted_seq = head.chain_seq;
1475 let names_an_earlier_entry = trusted_seq >= 1
1476 && trusted_seq < seq
1477 && links[trusted_seq as usize - 1].id == head.id
1478 && links[trusted_seq as usize - 1].attestation.entry_hash
1479 == head.entry_hash;
1480 if !names_an_earlier_entry {
1481 return Err(RotationError::UnknownLastTrusted);
1482 }
1483 if self.repudiated.contains(&trusted_seq) {
1486 return Err(RotationError::RepudiatedLastTrusted);
1487 }
1488 if rotation.old_key != self.in_force(trusted_seq).0 {
1491 return Err(RotationError::WrongOldKey);
1492 }
1493 if !anchor.contains(&rotation.new_key) {
1494 return Err(RotationError::UnanchoredNewKey);
1495 }
1496 self.history.retain(|(r, _)| r.from_seq <= trusted_seq);
1497 self.close(trusted_seq, KeyStatus::Compromised, &link.id);
1498 self.open(rotation.new_key, new_key, seq, &link.id);
1499 self.seen.insert(rotation.new_key);
1500 self.repudiated.extend(trusted_seq + 1..seq);
1501 Ok(())
1502 }
1503 }
1504 }
1505}
1506
1507pub fn verify_chain(
1530 links: &[GovernanceChainLink],
1531 genesis_key: &VerifyingKey,
1532 anchor: &KeyAnchor,
1533) -> GovernanceVerification {
1534 let mut links: Vec<&GovernanceChainLink> = links.iter().collect();
1535 links.sort_by_key(|l| l.attestation.chain_seq);
1536
1537 let mut seq_of: HashMap<&str, u64> = HashMap::new();
1538 let mut duplicates: HashSet<&str> = HashSet::new();
1539 for (i, link) in links.iter().enumerate() {
1540 if seq_of.insert(link.id.as_str(), i as u64 + 1).is_some() {
1541 duplicates.insert(link.id.as_str());
1542 }
1543 }
1544
1545 let mut walk = KeyWalk::new(genesis_key, anchor);
1546 let mut amendments: Vec<(u64, GovernanceLogId, Amendment, u64)> =
1548 Vec::new();
1549 let mut entries: Vec<EntryVerdict> = Vec::with_capacity(links.len());
1550 let mut prev: Option<&GovernanceChainLink> = None;
1551
1552 for (i, link) in links.iter().enumerate() {
1553 let a = &link.attestation;
1554 let expected_seq = i as u64 + 1;
1555 let mut problems: Vec<String> = Vec::new();
1556
1557 if let Some(problem) = prefix_problem(link) {
1558 problems.push(problem);
1559 }
1560 if duplicates.contains(link.id.as_str()) {
1561 problems.push(format!("{} appears more than once", link.id));
1562 }
1563
1564 let carries_meaning = matches!(
1568 link.entry_type,
1569 GovernanceLogEntryType::Amendment
1570 | GovernanceLogEntryType::KeyRotation
1571 );
1572 let mut amendment: Option<Amendment> = None;
1573 let mut rotation: Option<KeyRotation> = None;
1574 let mut content_matches: Option<bool> = None;
1575 match &link.data {
1576 Some(data) => {
1577 let matched = data_hash(data) == a.data_hash;
1578 content_matches = Some(matched);
1579 if !matched {
1580 if carries_meaning {
1581 problems.push(
1582 "`data` does not hash to the attested data_hash"
1583 .into(),
1584 );
1585 }
1586 } else {
1587 match link.entry_type {
1588 GovernanceLogEntryType::Amendment => {
1589 match serde_json::from_value(data.clone()) {
1590 Ok(v) => amendment = Some(v),
1591 Err(e) => problems.push(format!(
1592 "amendment `data` is malformed: {e}"
1593 )),
1594 }
1595 }
1596 GovernanceLogEntryType::KeyRotation => {
1597 match serde_json::from_value(data.clone()) {
1598 Ok(v) => rotation = Some(v),
1599 Err(e) => problems.push(format!(
1600 "key_rotation `data` is malformed: {e}"
1601 )),
1602 }
1603 }
1604 _ => {}
1605 }
1606 }
1607 }
1608 None if carries_meaning => problems.push(format!(
1609 "a {} entry must carry its `data`",
1610 link.entry_type
1611 )),
1612 None => {}
1613 }
1614
1615 let declared_key = rotation
1619 .as_ref()
1620 .filter(|r| {
1621 r.reason == RotationReason::Compromise
1622 && anchor.contains(&r.new_key)
1623 && !walk.seen.contains(&r.new_key)
1624 })
1625 .and_then(|r| r.new_key.to_verifying_key().ok());
1626 let (key_hex, key) = match declared_key {
1627 Some(k) => (PublicKeyHex::from(&k), k),
1628 None => walk.in_force(expected_seq),
1629 };
1630 if declared_key.is_none()
1633 && let Some(r) = rotation
1634 .as_ref()
1635 .filter(|r| r.reason == RotationReason::Compromise)
1636 {
1637 problems.push(
1638 if walk.seen.contains(&r.new_key) {
1639 RotationError::ReusedKey
1640 } else {
1641 RotationError::UnanchoredNewKey
1642 }
1643 .to_string(),
1644 );
1645 }
1646
1647 let (hash_ok, signature_valid) = match verify_link(link, &key) {
1648 Ok(()) => (true, true),
1649 Err(LinkError::BadSignature) => {
1650 problems.push(LinkError::BadSignature.to_string());
1651 (true, false)
1652 }
1653 Err(e) => {
1654 problems.push(e.to_string());
1655 (false, false)
1656 }
1657 };
1658
1659 let mut link_valid = hash_ok;
1660 if a.chain_seq != expected_seq {
1661 link_valid = false;
1662 problems.push(format!(
1663 "chain_seq {} where {expected_seq} was expected",
1664 a.chain_seq
1665 ));
1666 }
1667 let expected_prev = prev.map(|p| p.attestation.entry_hash);
1668 if a.prev_hash != expected_prev {
1669 link_valid = false;
1670 problems.push(match (a.prev_hash, expected_prev) {
1671 (Some(_), None) => "first entry names a predecessor".into(),
1672 (None, Some(_)) => "prev_hash is null mid-chain".into(),
1673 _ => "prev_hash is not the previous entry's entry_hash".into(),
1674 });
1675 }
1676 let out_of_order = prev.is_some_and(|p| link.created_at < p.created_at);
1677
1678 let authentic = signature_valid && link_valid;
1682 if let Some(rotation) = rotation.as_ref().filter(|_| authentic)
1683 && let Err(e) =
1684 walk.apply(rotation, link, expected_seq, &links, anchor)
1685 {
1686 problems.push(e.to_string());
1687 }
1688 if let Some(amendment) = amendment.filter(|_| authentic) {
1689 match amendment_target(&amendment, expected_seq, &seq_of, &links) {
1690 Ok(target) => amendments.push((
1691 expected_seq,
1692 link.id.clone(),
1693 amendment,
1694 target,
1695 )),
1696 Err(e) => problems.push(e.to_string()),
1697 }
1698 }
1699
1700 entries.push(EntryVerdict {
1701 id: link.id.clone(),
1702 chain_seq: a.chain_seq,
1703 signature_valid,
1704 link_valid,
1705 content_matches,
1706 retroactive: is_retroactive(link.created_at, a.signed_at),
1707 out_of_order,
1708 amended_by: Vec::new(),
1709 signed_by: Some(key_hex),
1710 redacted: false,
1711 redacted_data_hash: None,
1712 repudiated: false,
1713 reattested_by: Vec::new(),
1714 problem: (!problems.is_empty()).then(|| problems.join("; ")),
1715 });
1716 prev = Some(link);
1717 }
1718
1719 let mut applied = vec![false; amendments.len()];
1723 loop {
1724 let mut changed = false;
1725 for (i, (seq, id, amendment, target)) in amendments.iter().enumerate() {
1726 if applied[i]
1727 || amendment.kind != AmendmentKind::Reattested
1728 || walk.repudiated.contains(seq)
1729 {
1730 continue;
1731 }
1732 applied[i] = true;
1733 entries[*target as usize - 1].reattested_by.push(id.clone());
1734 walk.repudiated.remove(target);
1735 changed = true;
1736 }
1737 if !changed {
1738 break;
1739 }
1740 }
1741
1742 for (seq, id, amendment, target) in &amendments {
1743 if walk.repudiated.contains(seq) {
1744 continue;
1745 }
1746 let entry = &mut entries[*target as usize - 1];
1747 entry.amended_by.push(id.clone());
1748 if let Some(redaction) = &amendment.redaction {
1749 entry.redacted = true;
1750 entry.redacted_data_hash = Some(redaction.resulting_data_hash);
1751 }
1752 }
1753
1754 for (i, link) in links.iter().enumerate() {
1756 if entries[i].content_matches == Some(false)
1757 && let (Some(data), Some(hash)) =
1758 (&link.data, entries[i].redacted_data_hash)
1759 && data_hash(data) == hash
1760 {
1761 entries[i].content_matches = Some(true);
1762 }
1763 }
1764
1765 let mut seen = HashSet::new();
1766 walk.unanchored.retain(|key| seen.insert(*key));
1767
1768 let mut repudiated = Vec::new();
1769 for (i, entry) in entries.iter_mut().enumerate() {
1770 if walk.repudiated.contains(&(i as u64 + 1)) {
1771 entry.repudiated = true;
1772 repudiated.push(entry.id.clone());
1773 }
1774 }
1775
1776 GovernanceVerification {
1777 public_key: walk.active(),
1778 ok: false,
1779 head: prev.map(|p| p.id.clone()),
1780 entries,
1781 keys: walk.history.into_iter().map(|(record, _)| record).collect(),
1782 unanchored_keys: walk.unanchored,
1783 repudiated,
1784 }
1785 .settle()
1786}
1787
1788#[cfg(test)]
1789mod tests {
1790 use super::*;
1791 use crate::crypto::generate_keypair;
1792 use serde_json::json;
1793
1794 pub(super) fn gov(n: u32) -> GovernanceLogId {
1795 format!("GOV-2026-{n:04}").parse().unwrap()
1796 }
1797
1798 pub(super) fn amd(n: u32) -> GovernanceLogId {
1799 format!("AMD-2026-{n:04}").parse().unwrap()
1800 }
1801
1802 pub(super) fn key_id(n: u32) -> GovernanceLogId {
1803 format!("KEY-2026-{n:04}").parse().unwrap()
1804 }
1805
1806 pub(super) fn at(secs: i64) -> DateTime<Utc> {
1807 DateTime::from_timestamp(1_700_000_000 + secs, 123_456_789).unwrap()
1808 }
1809
1810 fn anchored(key: &VerifyingKey) -> KeyAnchor {
1812 KeyAnchor::pinned(key.into())
1813 }
1814
1815 pub(super) fn link(
1816 key: &SigningKey,
1817 n: u32,
1818 prev: Option<&GovernanceChainLink>,
1819 data: &serde_json::Value,
1820 signed_at: DateTime<Utc>,
1821 ) -> GovernanceChainLink {
1822 let created_at = truncate_to_micros(at(n as i64 * 10));
1823 let envelope = Envelope::new(
1824 gov(n),
1825 GovernanceLogEntryType::CouncilDecision,
1826 created_at,
1827 prev.map(|p| p.attestation.entry_hash),
1828 data_hash(data),
1829 );
1830 let attestation = attest(
1831 key,
1832 &envelope,
1833 prev.map_or(1, |p| p.attestation.chain_seq + 1),
1834 signed_at,
1835 );
1836 GovernanceChainLink {
1837 id: gov(n),
1838 entry_type: GovernanceLogEntryType::CouncilDecision,
1839 created_at,
1840 attestation,
1841 data: None,
1842 }
1843 }
1844
1845 pub(super) fn chain(key: &SigningKey, n: u32) -> Vec<GovernanceChainLink> {
1846 let mut out: Vec<GovernanceChainLink> = Vec::new();
1847 for i in 1..=n {
1848 let data = json!({"title": format!("Decision {i}"), "outcome": "approved"});
1849 let l = link(key, i, out.last(), &data, at(i as i64 * 10 + 1));
1850 out.push(l);
1851 }
1852 out
1853 }
1854
1855 pub(super) struct Chain {
1859 pub(super) links: Vec<GovernanceChainLink>,
1860 gov: u32,
1861 amd: u32,
1862 key: u32,
1863 }
1864
1865 impl Chain {
1866 pub(super) fn new() -> Self {
1867 Self {
1868 links: Vec::new(),
1869 gov: 0,
1870 amd: 0,
1871 key: 0,
1872 }
1873 }
1874
1875 pub(super) fn prev_hash(&self) -> Option<Sha256Hex> {
1876 self.links.last().map(|l| l.attestation.entry_hash)
1877 }
1878
1879 pub(super) fn hash_at(&self, seq: usize) -> Sha256Hex {
1881 self.links[seq - 1].attestation.entry_hash
1882 }
1883
1884 pub(super) fn next_amd(&self) -> GovernanceLogId {
1886 amd(self.amd + 1)
1887 }
1888
1889 pub(super) fn push(
1890 &mut self,
1891 signer: &SigningKey,
1892 id: GovernanceLogId,
1893 entry_type: GovernanceLogEntryType,
1894 data: serde_json::Value,
1895 carry: bool,
1896 ) -> GovernanceLogId {
1897 let n = self.links.len() as i64 + 1;
1898 let created_at = truncate_to_micros(at(n * 10));
1899 let envelope = Envelope::new(
1900 id.clone(),
1901 entry_type,
1902 created_at,
1903 self.prev_hash(),
1904 data_hash(&data),
1905 );
1906 let attestation =
1907 attest(signer, &envelope, n as u64, at(n * 10 + 1));
1908 self.links.push(GovernanceChainLink {
1909 id: id.clone(),
1910 entry_type,
1911 created_at,
1912 attestation,
1913 data: carry.then_some(data),
1914 });
1915 id
1916 }
1917
1918 pub(super) fn entry(
1921 &mut self,
1922 signer: &SigningKey,
1923 data: serde_json::Value,
1924 ) -> GovernanceLogId {
1925 self.gov += 1;
1926 let id = gov(self.gov);
1927 self.push(
1928 signer,
1929 id,
1930 GovernanceLogEntryType::CouncilDecision,
1931 data,
1932 false,
1933 )
1934 }
1935
1936 pub(super) fn decision(
1937 &mut self,
1938 signer: &SigningKey,
1939 ) -> GovernanceLogId {
1940 let data = json!({"title": format!("Decision {}", self.gov + 1)});
1941 self.entry(signer, data)
1942 }
1943
1944 pub(super) fn amend(
1945 &mut self,
1946 signer: &SigningKey,
1947 amendment: &Amendment,
1948 ) -> GovernanceLogId {
1949 self.amd += 1;
1950 let id = amd(self.amd);
1951 self.push(
1952 signer,
1953 id,
1954 GovernanceLogEntryType::Amendment,
1955 serde_json::to_value(amendment).unwrap(),
1956 true,
1957 )
1958 }
1959
1960 pub(super) fn rotate(
1961 &mut self,
1962 signer: &SigningKey,
1963 rotation: &KeyRotation,
1964 ) -> GovernanceLogId {
1965 self.key += 1;
1966 let id = key_id(self.key);
1967 self.push(
1968 signer,
1969 id,
1970 GovernanceLogEntryType::KeyRotation,
1971 serde_json::to_value(rotation).unwrap(),
1972 true,
1973 )
1974 }
1975 }
1976
1977 #[test]
1980 fn canonical_json_sorts_keys_at_every_level() {
1981 let v = json!({"b": {"z": 1, "a": [{"y": 2, "x": 3}]}, "a": null});
1982 assert_eq!(
1983 canonical_json(&v),
1984 br#"{"a":null,"b":{"a":[{"x":3,"y":2}],"z":1}}"#
1985 );
1986 }
1987
1988 #[test]
1989 fn canonical_json_is_compact_and_escapes_like_serde() {
1990 let v = json!({"s": "tab\there \"q\" ünïcode \u{1F600}", "n": [1, -2, 3.5, true, false]});
1991 let bytes = canonical_json(&v);
1992 let text = std::str::from_utf8(&bytes).unwrap();
1993 assert_eq!(
1994 text,
1995 r#"{"n":[1,-2,3.5,true,false],"s":"tab\there \"q\" ünïcode 😀"}"#
1996 );
1997 }
1998
1999 #[test]
2000 fn canonical_json_ignores_insertion_order() {
2001 let mut a = serde_json::Map::new();
2002 a.insert("z".into(), json!(1));
2003 a.insert("a".into(), json!(2));
2004 let mut b = serde_json::Map::new();
2005 b.insert("a".into(), json!(2));
2006 b.insert("z".into(), json!(1));
2007 assert_eq!(
2008 canonical_json(&serde_json::Value::Object(a)),
2009 canonical_json(&serde_json::Value::Object(b))
2010 );
2011 }
2012
2013 #[test]
2014 fn canonical_json_empty_containers() {
2015 assert_eq!(canonical_json(&json!({})), b"{}");
2016 assert_eq!(canonical_json(&json!([])), b"[]");
2017 assert_eq!(
2018 canonical_json(&json!({"a": {}, "b": []})),
2019 br#"{"a":{},"b":[]}"#
2020 );
2021 }
2022
2023 #[test]
2026 fn preimage_is_declaration_ordered_json() {
2027 let e = Envelope::new(
2028 gov(1),
2029 GovernanceLogEntryType::AppealsCourtDecision,
2030 at(0),
2031 None,
2032 data_hash(&json!({})),
2033 );
2034 let text = String::from_utf8(e.preimage()).unwrap();
2035 assert!(text.starts_with(r#"{"agora_governance_log":1,"id":"GOV-2026-0001","entry_type":"appeals_court_decision","created_at":1700000000123456,"prev_hash":null,"data_hash":""#), "{text}");
2036 }
2037
2038 #[test]
2039 fn every_envelope_field_changes_the_hash() {
2040 let base = Envelope::new(
2041 gov(1),
2042 GovernanceLogEntryType::CouncilDecision,
2043 at(0),
2044 None,
2045 data_hash(&json!({"a":1})),
2046 );
2047 let h = base.entry_hash();
2048 let mut e = base.clone();
2049 e.id = gov(2);
2050 assert_ne!(e.entry_hash(), h);
2051 let mut e = base.clone();
2052 e.entry_type = GovernanceLogEntryType::PolicyChange;
2053 assert_ne!(e.entry_hash(), h);
2054 let mut e = base.clone();
2055 e.created_at += 1;
2056 assert_ne!(e.entry_hash(), h);
2057 let mut e = base.clone();
2058 e.prev_hash = Some(h);
2059 assert_ne!(e.entry_hash(), h);
2060 let mut e = base.clone();
2061 e.data_hash = data_hash(&json!({"a":2}));
2062 assert_ne!(e.entry_hash(), h);
2063 assert_eq!(base.entry_hash(), h, "and it is deterministic");
2064 }
2065
2066 #[test]
2067 fn truncation_matches_what_the_envelope_carries() {
2068 let t = at(0);
2069 assert_eq!(truncate_to_micros(t).timestamp_subsec_nanos(), 123_456_000);
2070 assert_eq!(truncate_to_seconds(t).timestamp_subsec_nanos(), 0);
2071 assert_eq!(
2072 Envelope::new(
2073 gov(1),
2074 GovernanceLogEntryType::CouncilDecision,
2075 t,
2076 None,
2077 data_hash(&json!(null))
2078 )
2079 .created_at,
2080 truncate_to_micros(t).timestamp_micros()
2081 );
2082 }
2083
2084 #[test]
2087 fn hex_newtypes_round_trip_and_reject_wrong_lengths() {
2088 let h = data_hash(&json!(1));
2089 let s = serde_json::to_string(&h).unwrap();
2090 assert_eq!(s.len(), 66);
2091 let back: Sha256Hex = serde_json::from_str(&s).unwrap();
2092 assert_eq!(back, h);
2093 assert!(serde_json::from_str::<Sha256Hex>("\"abcd\"").is_err());
2094 assert!("zz".repeat(32).parse::<Sha256Hex>().is_err());
2095 assert!(Sha256Hex::try_from(vec![0u8; 31]).is_err());
2096 assert_eq!(format!("{h:?}"), format!("Sha256Hex({h})"));
2097 }
2098
2099 #[test]
2102 fn attest_then_verify_link() {
2103 let (key, pk) = generate_keypair();
2104 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
2105 assert_eq!(verify_link(&l, &pk), Ok(()));
2106 assert!(verify_data(&l, &json!({"a": 1})));
2107 assert!(!verify_data(&l, &json!({"a": 2})));
2108 assert!(!l.attestation.retroactive);
2109 }
2110
2111 #[test]
2112 fn wrong_key_fails_signature_only() {
2113 let (key, _) = generate_keypair();
2114 let (_, other) = generate_keypair();
2115 let l = link(&key, 1, None, &json!({}), at(5));
2116 assert_eq!(verify_link(&l, &other), Err(LinkError::BadSignature));
2117 }
2118
2119 #[test]
2120 fn tampering_with_any_attested_field_is_detected() {
2121 let (key, pk) = generate_keypair();
2122 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
2123
2124 let mut t = l.clone();
2125 t.created_at += chrono::Duration::microseconds(1);
2126 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
2127
2128 let mut t = l.clone();
2129 t.entry_type = GovernanceLogEntryType::StewardVeto;
2130 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
2131
2132 let mut t = l.clone();
2133 t.attestation.data_hash = data_hash(&json!({"a": 2}));
2134 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
2135
2136 let mut t = l.clone();
2139 t.attestation.signed_at -= chrono::Duration::seconds(1);
2140 assert_eq!(verify_link(&t, &pk), Err(LinkError::BadSignature));
2141
2142 let mut t = l.clone();
2143 t.attestation.envelope_version = 2;
2144 assert_eq!(verify_link(&t, &pk), Err(LinkError::UnsupportedVersion(2)));
2145 }
2146
2147 #[test]
2148 fn a_good_chain_verifies_in_any_input_order() {
2149 let (key, pk) = generate_keypair();
2150 let mut c = chain(&key, 4);
2151 c.reverse();
2152 let v = verify_chain(&c, &pk, &anchored(&pk));
2153 assert!(v.ok, "{v:#?}");
2154 assert_eq!(v.head, Some(gov(4)));
2155 assert_eq!(
2156 v.entries.iter().map(|e| e.chain_seq).collect::<Vec<_>>(),
2157 [1, 2, 3, 4]
2158 );
2159 assert!(v.entries.iter().all(|e| e.content_matches.is_none()
2160 && e.problem.is_none()
2161 && !e.out_of_order));
2162 assert_eq!(v.public_key, PublicKeyHex::from(&pk));
2163 }
2164
2165 #[test]
2166 fn empty_chain_is_ok_with_no_head() {
2167 let (_, pk) = generate_keypair();
2168 let v = verify_chain(&[], &pk, &anchored(&pk));
2169 assert!(v.ok);
2170 assert!(v.head.is_none());
2171 assert!(v.entries.is_empty());
2172 }
2173
2174 #[test]
2175 fn a_changed_entry_breaks_its_signature_and_the_next_link() {
2176 let (key, pk) = generate_keypair();
2177 let mut c = chain(&key, 3);
2178 let rewritten = link(
2181 &key,
2182 2,
2183 Some(&c[0]),
2184 &json!({"title": "Decision 2", "outcome": "REJECTED"}),
2185 at(21),
2186 );
2187 c[1] = rewritten;
2188 let v = verify_chain(&c, &pk, &anchored(&pk));
2189 assert!(!v.ok);
2190 assert!(
2191 v.entries[1].signature_valid && v.entries[1].link_valid,
2192 "the rewrite itself is well-formed: {:#?}",
2193 v.entries[1]
2194 );
2195 assert!(
2196 !v.entries[2].link_valid,
2197 "but entry 3 no longer points at it: {:#?}",
2198 v.entries[2]
2199 );
2200 assert!(
2201 v.entries[2]
2202 .problem
2203 .as_deref()
2204 .unwrap()
2205 .contains("prev_hash")
2206 );
2207 }
2208
2209 #[test]
2210 fn a_removed_entry_is_a_gap_and_a_broken_link() {
2211 let (key, pk) = generate_keypair();
2212 let mut c = chain(&key, 3);
2213 c.remove(1);
2214 let v = verify_chain(&c, &pk, &anchored(&pk));
2215 assert!(!v.ok);
2216 assert!(v.entries[0].link_valid);
2217 let p = v.entries[1].problem.as_deref().unwrap();
2218 assert!(p.contains("chain_seq 3 where 2 was expected"), "{p}");
2219 assert!(p.contains("prev_hash"), "{p}");
2220 }
2221
2222 #[test]
2223 fn a_second_genesis_is_rejected() {
2224 let (key, pk) = generate_keypair();
2225 let mut c = chain(&key, 2);
2226 let rogue = link(&key, 2, None, &json!({}), at(21));
2227 c[1] = rogue;
2228 let v = verify_chain(&c, &pk, &anchored(&pk));
2229 assert!(!v.ok);
2230 assert!(
2231 v.entries[1]
2232 .problem
2233 .as_deref()
2234 .unwrap()
2235 .contains("null mid-chain")
2236 );
2237 }
2238
2239 #[test]
2240 fn retroactive_and_out_of_order_are_recomputed_not_copied() {
2241 let (key, pk) = generate_keypair();
2242 let first = link(&key, 1, None, &json!({}), at(10 + 3600));
2243 let created = truncate_to_micros(at(5));
2246 let envelope = Envelope::new(
2247 gov(2),
2248 GovernanceLogEntryType::CouncilDecision,
2249 created,
2250 Some(first.attestation.entry_hash),
2251 data_hash(&json!({})),
2252 );
2253 let attestation = attest(&key, &envelope, 2, at(6));
2254 let mut second = GovernanceChainLink {
2255 id: gov(2),
2256 entry_type: GovernanceLogEntryType::CouncilDecision,
2257 created_at: created,
2258 attestation,
2259 data: None,
2260 };
2261 second.attestation.retroactive = true; let v = verify_chain(&[first, second], &pk, &anchored(&pk));
2263 assert!(v.ok, "{v:#?}");
2264 assert!(v.entries[0].retroactive);
2265 assert!(!v.entries[1].retroactive, "recomputed from timestamps");
2266 assert!(v.entries[1].out_of_order);
2267 }
2268
2269 #[test]
2270 fn content_mismatch_settles_to_not_ok() {
2271 let (key, pk) = generate_keypair();
2272 let c = chain(&key, 1);
2273 let mut v = verify_chain(&c, &pk, &anchored(&pk));
2274 v.entries[0].content_matches = Some(true);
2275 assert!(v.clone().settle().ok);
2276 v.entries[0].content_matches = Some(false);
2277 assert!(!v.settle().ok);
2278 }
2279
2280 #[test]
2283 fn an_amendment_fills_amended_by_on_its_target() {
2284 let (key, pk) = generate_keypair();
2285 let mut c = Chain::new();
2286 let target = c.decision(&key);
2287 c.decision(&key);
2288 let amendment = Amendment::new(
2289 target,
2290 c.hash_at(1),
2291 AmendmentKind::NonPrecedential,
2292 "§1 (Red Team Cases Recharacterized)",
2293 "diagnostic finding — not citable as moderation precedent",
2294 )
2295 .unwrap()
2296 .with_authority(gov(5))
2297 .with_rationale("§5 leaves the ruling itself standing");
2298 let id = c.amend(&key, &amendment);
2299
2300 let v = verify_chain(&c.links, &pk, &anchored(&pk));
2301 assert!(v.ok, "{v:#?}");
2302 assert_eq!(v.entries[0].amended_by, vec![id]);
2303 assert!(v.entries[1].amended_by.is_empty());
2304 assert!(!v.entries[0].redacted);
2305 assert_eq!(v.head, Some(amd(1)));
2306 assert_eq!(v.entries[2].content_matches, Some(true));
2308 assert_eq!(v.entries[0].content_matches, None);
2309 assert_eq!(standing([amendment.kind]), Standing::NonPrecedential);
2310 }
2311
2312 #[test]
2313 fn an_amendment_must_name_an_earlier_entry_by_its_exact_hash() {
2314 let (key, pk) = generate_keypair();
2315 let problem = |c: &Chain, at: usize| -> String {
2316 verify_chain(&c.links, &pk, &anchored(&pk)).entries[at]
2317 .problem
2318 .clone()
2319 .unwrap_or_default()
2320 };
2321
2322 let mut c = Chain::new();
2323 c.decision(&key);
2324 let unknown = Amendment::new(
2325 gov(99),
2326 c.hash_at(1),
2327 AmendmentKind::Overruled,
2328 "b",
2329 "n",
2330 )
2331 .unwrap();
2332 c.amend(&key, &unknown);
2333 assert!(
2334 problem(&c, 1).contains("is not an entry of this chain"),
2335 "{}",
2336 problem(&c, 1)
2337 );
2338 assert!(!verify_chain(&c.links, &pk, &anchored(&pk)).ok);
2339
2340 let mut c = Chain::new();
2342 c.decision(&key);
2343 let forward = Amendment::new(
2344 gov(2),
2345 c.hash_at(1),
2346 AmendmentKind::Overruled,
2347 "b",
2348 "n",
2349 )
2350 .unwrap();
2351 c.amend(&key, &forward);
2352 c.decision(&key);
2353 assert!(
2354 problem(&c, 1).contains("not an earlier entry"),
2355 "{}",
2356 problem(&c, 1)
2357 );
2358
2359 let mut c = Chain::new();
2361 let target = c.decision(&key);
2362 let wrong_hash = Amendment::new(
2363 target,
2364 data_hash(&json!("some other entry")),
2365 AmendmentKind::Overruled,
2366 "b",
2367 "n",
2368 )
2369 .unwrap();
2370 c.amend(&key, &wrong_hash);
2371 assert!(problem(&c, 1).contains("entry_hash"), "{}", problem(&c, 1));
2372 assert!(
2373 verify_chain(&c.links, &pk, &anchored(&pk)).entries[0]
2374 .amended_by
2375 .is_empty()
2376 );
2377 }
2378
2379 #[test]
2380 fn redaction_shape_violations_fail_verification() {
2381 let (key, pk) = generate_keypair();
2382 let amend_with = |mutate: &dyn Fn(&mut Amendment)| -> String {
2383 let mut c = Chain::new();
2384 let target = c.decision(&key);
2385 let mut amendment = Amendment::new(
2386 target,
2387 c.hash_at(1),
2388 AmendmentKind::Correction,
2389 "b",
2390 "n",
2391 )
2392 .unwrap();
2393 mutate(&mut amendment);
2394 c.amend(&key, &amendment);
2395 let v = verify_chain(&c.links, &pk, &anchored(&pk));
2396 assert!(!v.ok, "{v:#?}");
2397 v.entries[1].problem.clone().unwrap_or_default()
2398 };
2399
2400 assert_eq!(
2403 Amendment::new(
2404 gov(1),
2405 data_hash(&json!(null)),
2406 AmendmentKind::Redaction,
2407 "b",
2408 "n"
2409 ),
2410 Err(AmendmentError::MissingRedaction)
2411 );
2412 let p = amend_with(&|a| a.kind = AmendmentKind::Redaction);
2413 assert!(p.contains("requires a `redaction`"), "{p}");
2414
2415 let p = amend_with(&|a| {
2416 a.redaction = Some(Redaction {
2417 fields: vec!["/x".into()],
2418 resulting_data_hash: data_hash(&json!({})),
2419 })
2420 });
2421 assert!(p.contains("only valid on kind"), "{p}");
2422
2423 let p = amend_with(&|a| a.agora_governance_amendment = 2);
2424 assert!(p.contains("agora_governance_amendment is 2"), "{p}");
2425 }
2426
2427 #[test]
2428 fn standing_is_the_last_amendment_that_changes_it() {
2429 use AmendmentKind::*;
2430 assert_eq!(standing([]), Standing::InForce);
2431 assert_eq!(standing([Correction, Redaction]), Standing::InForce);
2432 assert_eq!(
2433 standing([Correction, NonPrecedential, Redaction]),
2434 Standing::NonPrecedential
2435 );
2436 assert_eq!(standing([Overruled, Reinstated]), Standing::InForce);
2437 assert_eq!(standing([Reinstated, Superseded]), Standing::Superseded);
2438 assert_eq!(kind_standing(Reattested), None);
2439 assert_eq!(kind_standing(Reinstated), Some(Standing::InForce));
2440 }
2441
2442 #[test]
2443 fn a_redaction_verifies_against_the_amendment_and_nothing_else() {
2444 let (key, pk) = generate_keypair();
2445 let data = json!({
2446 "finding": "upheld",
2447 "subject": {"handle": "someone", "detail": "personal"},
2448 });
2449 let mut c = Chain::new();
2450 let target = c.entry(&key, data.clone());
2451 let amendment_id = c.next_amd();
2452 let (amendment, redacted) = Amendment::redaction(
2453 &amendment_id,
2454 target,
2455 c.hash_at(1),
2456 "GDPR Art. 17(1)(a)",
2457 "personal data removed on request",
2458 vec!["/subject/handle".into(), "/subject/detail".into()],
2459 &data,
2460 )
2461 .unwrap();
2462 assert_eq!(c.amend(&key, &amendment), amendment_id);
2463
2464 let mut v = verify_chain(&c.links, &pk, &anchored(&pk));
2465 assert!(v.ok, "{v:#?}");
2466 assert!(v.entries[0].redacted);
2467 assert_eq!(v.entries[0].redacted_data_hash, Some(data_hash(&redacted)));
2468 assert_eq!(
2469 redacted["subject"]["handle"],
2470 json!(format!("[redacted by {amendment_id}]"))
2471 );
2472 assert_eq!(redacted["finding"], json!("upheld"), "and nothing else");
2473
2474 assert!(v.check_content(&c.links[0], &redacted));
2476 assert_eq!(v.entries[0].content_matches, Some(true));
2477 assert!(v.clone().settle().ok);
2478 assert!(v.check_content(&c.links[0], &data));
2480
2481 let mut tampered = redacted.clone();
2483 tampered["finding"] = json!("overturned");
2484 assert!(!v.check_content(&c.links[0], &tampered));
2485 assert_eq!(v.entries[0].content_matches, Some(false));
2486 assert!(!v.settle().ok);
2487 }
2488
2489 #[test]
2490 fn content_that_matches_nothing_fails_without_an_amendment() {
2491 let (key, pk) = generate_keypair();
2492 let mut c = Chain::new();
2493 c.entry(&key, json!({"a": 1}));
2494 let mut v = verify_chain(&c.links, &pk, &anchored(&pk));
2495 assert!(!v.check_content(&c.links[0], &json!({"a": 2})));
2496 assert!(!v.clone().settle().ok);
2497 let other = link(&key, 9, None, &json!({}), at(9));
2499 assert!(!v.check_content(&other, &json!({})));
2500 }
2501
2502 #[test]
2503 fn tampering_with_an_amendments_data_is_caught_by_the_data_hash() {
2504 let (key, pk) = generate_keypair();
2505 let mut c = Chain::new();
2506 let target = c.decision(&key);
2507 let amendment = Amendment::new(
2508 target,
2509 c.hash_at(1),
2510 AmendmentKind::Overruled,
2511 "b",
2512 "overruled by a later decision",
2513 )
2514 .unwrap();
2515 c.amend(&key, &amendment);
2516 c.links[1].data.as_mut().unwrap()["note"] =
2517 json!("reinstated, actually");
2518
2519 let v = verify_chain(&c.links, &pk, &anchored(&pk));
2520 assert!(!v.ok, "{v:#?}");
2521 let p = v.entries[1].problem.as_deref().unwrap();
2522 assert!(p.contains("does not hash to the attested data_hash"), "{p}");
2523 assert!(
2524 v.entries[0].amended_by.is_empty(),
2525 "an unreadable amendment has no effect"
2526 );
2527 assert!(
2528 v.entries[1].signature_valid && v.entries[1].link_valid,
2529 "the envelope is untouched — only the content is not what it \
2530 committed to"
2531 );
2532 }
2533
2534 #[test]
2535 fn an_amendment_or_rotation_must_carry_its_data() {
2536 let (key, pk) = generate_keypair();
2537 let mut c = Chain::new();
2538 let target = c.decision(&key);
2539 let amendment = Amendment::new(
2540 target,
2541 c.hash_at(1),
2542 AmendmentKind::Correction,
2543 "b",
2544 "n",
2545 )
2546 .unwrap();
2547 c.amend(&key, &amendment);
2548 let rotation = KeyRotation::routine(
2549 (&pk).into(),
2550 &key,
2551 c.prev_hash(),
2552 at(35),
2553 "n",
2554 );
2555 c.rotate(&key, &rotation);
2556 c.links[1].data = None;
2557 c.links[2].data = None;
2558
2559 let v = verify_chain(&c.links, &pk, &anchored(&pk));
2560 assert!(!v.ok, "{v:#?}");
2561 for (i, entry_type) in [(1, "amendment"), (2, "key_rotation")] {
2562 let p = v.entries[i].problem.as_deref().unwrap();
2563 assert!(p.contains(&format!("a {entry_type} entry")), "{p}");
2564 assert!(p.contains("must carry its `data`"), "{p}");
2565 }
2566 }
2567
2568 #[test]
2569 fn the_id_series_must_match_the_entry_type() {
2570 let (key, pk) = generate_keypair();
2571 let mut c = Chain::new();
2572 let target = c.decision(&key);
2573 let amendment = Amendment::new(
2574 target,
2575 c.hash_at(1),
2576 AmendmentKind::Correction,
2577 "b",
2578 "n",
2579 )
2580 .unwrap();
2581 c.push(
2582 &key,
2583 gov(7),
2584 GovernanceLogEntryType::Amendment,
2585 serde_json::to_value(&amendment).unwrap(),
2586 true,
2587 );
2588 let v = verify_chain(&c.links, &pk, &anchored(&pk));
2589 assert!(!v.ok, "{v:#?}");
2590 let p = v.entries[1].problem.as_deref().unwrap();
2591 assert!(p.contains("must be in the AMD- series"), "{p}");
2592
2593 let mut c = Chain::new();
2594 c.push(
2595 &key,
2596 key_id(1),
2597 GovernanceLogEntryType::CouncilDecision,
2598 json!({}),
2599 false,
2600 );
2601 let v = verify_chain(&c.links, &pk, &anchored(&pk));
2602 let p = v.entries[0].problem.as_deref().unwrap();
2603 assert!(p.contains("reserved"), "{p}");
2604 }
2605
2606 #[test]
2607 fn redact_data_replaces_whole_values_and_refuses_the_rest() {
2608 let id = amd(3);
2609 let data = json!({"a": {"b": [1, {"c": "secret"}]}, "d/e": "slash"});
2610 let out = redact_data(&data, &["/a/b/1/c".into(), "/d~1e".into()], &id)
2611 .unwrap();
2612 assert_eq!(out["a"]["b"][1]["c"], json!(redaction_marker(&id)));
2613 assert_eq!(out["d/e"], json!(redaction_marker(&id)));
2614 assert_eq!(out["a"]["b"][0], json!(1), "untouched");
2615
2616 assert_eq!(
2617 redact_data(&data, &["/a/nope".into()], &id),
2618 Err(RedactError::Unresolved("/a/nope".into()))
2619 );
2620 assert_eq!(
2621 redact_data(&data, &["".into()], &id),
2622 Err(RedactError::WholeEntry)
2623 );
2624 assert_eq!(redact_data(&data, &[], &id).unwrap(), data);
2625 }
2626
2627 #[test]
2630 fn a_routine_rotation_moves_the_chain_to_the_new_key() {
2631 let (old, old_pk) = generate_keypair();
2632 let (new, new_pk) = generate_keypair();
2633 let anchor = anchored(&old_pk).with((&new_pk).into());
2634 let mut c = Chain::new();
2635 c.decision(&old);
2636 let rotation = KeyRotation::routine(
2637 (&old_pk).into(),
2638 &new,
2639 c.prev_hash(),
2640 at(25),
2641 "scheduled rotation",
2642 );
2643 let rotation_id = c.rotate(&old, &rotation);
2644 c.decision(&new);
2645
2646 let v = verify_chain(&c.links, &old_pk, &anchor);
2647 assert!(v.ok, "{v:#?}");
2648 assert_eq!(v.public_key, (&new_pk).into());
2649 assert_eq!(
2650 v.entries[1].signed_by,
2651 Some((&old_pk).into()),
2652 "the rotation itself is signed by the old key"
2653 );
2654 assert_eq!(v.entries[2].signed_by, Some((&new_pk).into()));
2655 assert!(v.unanchored_keys.is_empty());
2656 assert!(v.repudiated.is_empty());
2657 assert_eq!(v.keys.len(), 2);
2658 assert_eq!(v.keys[0].public_key, (&old_pk).into());
2659 assert_eq!(v.keys[0].from_seq, 1);
2660 assert_eq!(v.keys[0].through_seq, Some(2));
2661 assert_eq!(v.keys[0].status, KeyStatus::Retired);
2662 assert_eq!(v.keys[0].introduced_by, None);
2663 assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
2664 assert_eq!(v.keys[1].from_seq, 3);
2665 assert_eq!(v.keys[1].through_seq, None);
2666 assert_eq!(v.keys[1].status, KeyStatus::Active);
2667 assert_eq!(v.keys[1].introduced_by.as_ref(), Some(&rotation_id));
2668 }
2669
2670 #[test]
2671 fn the_old_key_cannot_sign_after_a_routine_rotation() {
2672 let (old, old_pk) = generate_keypair();
2673 let (new, new_pk) = generate_keypair();
2674 let anchor = anchored(&old_pk).with((&new_pk).into());
2675 let mut c = Chain::new();
2676 c.decision(&old);
2677 let rotation = KeyRotation::routine(
2678 (&old_pk).into(),
2679 &new,
2680 c.prev_hash(),
2681 at(25),
2682 "scheduled",
2683 );
2684 c.rotate(&old, &rotation);
2685 c.decision(&old);
2686
2687 let v = verify_chain(&c.links, &old_pk, &anchor);
2688 assert!(!v.ok, "{v:#?}");
2689 assert!(!v.entries[2].signature_valid);
2690 assert!(
2691 v.entries[2].link_valid,
2692 "the linkage is fine; the key is not"
2693 );
2694 }
2695
2696 #[test]
2697 fn a_routine_rotation_to_an_unanchored_key_is_followed_and_reported() {
2698 let (old, old_pk) = generate_keypair();
2699 let (new, new_pk) = generate_keypair();
2700 let mut c = Chain::new();
2701 c.decision(&old);
2702 let rotation = KeyRotation::routine(
2703 (&old_pk).into(),
2704 &new,
2705 c.prev_hash(),
2706 at(25),
2707 "scheduled",
2708 );
2709 c.rotate(&old, &rotation);
2710 c.decision(&new);
2711
2712 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk));
2715 assert!(v.ok, "{v:#?}");
2716 assert_eq!(v.unanchored_keys, vec![PublicKeyHex::from(&new_pk)]);
2717 assert_eq!(v.public_key, (&new_pk).into());
2718 }
2719
2720 #[test]
2721 fn a_forged_proof_of_possession_is_rejected() {
2722 let (old, old_pk) = generate_keypair();
2723 let (_, new_pk) = generate_keypair();
2724 let (thief, _) = generate_keypair();
2725 let mut c = Chain::new();
2726 c.decision(&old);
2727 let mut rotation = KeyRotation::routine(
2730 (&old_pk).into(),
2731 &thief,
2732 c.prev_hash(),
2733 at(25),
2734 "scheduled",
2735 );
2736 rotation.new_key = (&new_pk).into();
2737 let statement = rotation.statement(c.prev_hash());
2738 rotation.proof = crypto::sign(
2739 &old,
2740 statement.hash().as_bytes(),
2741 rotation.proof_signed_at,
2742 )
2743 .into();
2744 c.rotate(&old, &rotation);
2745
2746 assert_eq!(
2747 rotation.verify_proof(c.links[1].attestation.prev_hash),
2748 Err(RotationError::BadProof)
2749 );
2750 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk));
2751 assert!(!v.ok, "{v:#?}");
2752 assert!(
2753 v.entries[1]
2754 .problem
2755 .as_deref()
2756 .unwrap()
2757 .contains("proof of possession")
2758 );
2759 assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
2760 }
2761
2762 #[test]
2763 fn a_proof_does_not_replay_at_another_position() {
2764 let (old, old_pk) = generate_keypair();
2765 let (new, new_pk) = generate_keypair();
2766 let anchor = anchored(&old_pk).with((&new_pk).into());
2767 let mut c = Chain::new();
2768 c.decision(&old);
2769 let rotation = KeyRotation::routine(
2771 (&old_pk).into(),
2772 &new,
2773 c.prev_hash(),
2774 at(25),
2775 "scheduled",
2776 );
2777 c.decision(&old);
2779 c.rotate(&old, &rotation);
2780
2781 let v = verify_chain(&c.links, &old_pk, &anchor);
2782 assert!(!v.ok, "{v:#?}");
2783 assert!(
2784 v.entries[2]
2785 .problem
2786 .as_deref()
2787 .unwrap()
2788 .contains("proof of possession")
2789 );
2790 assert_eq!(v.public_key, (&old_pk).into());
2791 }
2792
2793 #[test]
2794 fn a_compromise_repudiates_the_window_and_a_reattestation_restores_one() {
2795 let (old, old_pk) = generate_keypair();
2796 let (new, new_pk) = generate_keypair();
2797 let anchor = anchored(&old_pk).with((&new_pk).into());
2798 let mut c = Chain::new();
2799 c.decision(&old); c.decision(&old); let reattested = c.decision(&old); let rotation = KeyRotation::compromise(
2803 (&old_pk).into(),
2804 &new,
2805 TrustedHead {
2806 id: gov(1),
2807 chain_seq: 1,
2808 entry_hash: c.hash_at(1),
2809 },
2810 c.prev_hash(),
2811 at(45),
2812 "signing key exfiltrated",
2813 );
2814 let rotation_id = c.rotate(&new, &rotation); let vouch = Amendment::new(
2816 reattested,
2817 c.hash_at(3),
2818 AmendmentKind::Reattested,
2819 "Art. VII",
2820 "independently verified; the Steward vouches for it",
2821 )
2822 .unwrap();
2823 let vouch_id = c.amend(&new, &vouch); let v = verify_chain(&c.links, &old_pk, &anchor);
2826 assert!(
2827 v.ok,
2828 "repudiation is a declared state, not a defect: {v:#?}"
2829 );
2830 assert_eq!(v.repudiated, vec![gov(2)]);
2831 assert!(v.entries[1].repudiated);
2832 assert!(!v.entries[2].repudiated);
2833 assert_eq!(v.entries[2].reattested_by, vec![vouch_id.clone()]);
2834 assert_eq!(v.entries[2].amended_by, vec![vouch_id]);
2835 assert_eq!(v.entries[3].signed_by, Some((&new_pk).into()));
2836 assert_eq!(v.public_key, (&new_pk).into());
2837 assert_eq!(v.keys.len(), 2);
2838 assert_eq!(v.keys[0].status, KeyStatus::Compromised);
2839 assert_eq!(
2840 v.keys[0].through_seq,
2841 Some(1),
2842 "trusted through the last trusted entry, not through the \
2843 declaration"
2844 );
2845 assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
2846 assert_eq!(v.keys[1].from_seq, 4, "the declaration is its own first");
2847 }
2848
2849 #[test]
2850 fn a_stolen_key_cannot_be_rotated_back_in() {
2851 let (k1, k1_pk) = generate_keypair();
2855 let (k2, k2_pk) = generate_keypair();
2856 let anchor = anchored(&k1_pk).with((&k2_pk).into());
2857 let mut c = Chain::new();
2858 c.decision(&k1); let real = KeyRotation::compromise(
2860 (&k1_pk).into(),
2861 &k2,
2862 TrustedHead {
2863 id: gov(1),
2864 chain_seq: 1,
2865 entry_hash: c.hash_at(1),
2866 },
2867 c.prev_hash(),
2868 at(25),
2869 "signing key exfiltrated",
2870 );
2871 c.rotate(&k2, &real); c.decision(&k2); let honest = verify_chain(&c.links, &k1_pk, &anchor);
2874 assert!(honest.ok, "{honest:#?}");
2875
2876 let hijack = KeyRotation::compromise(
2877 (&k2_pk).into(),
2878 &k1,
2879 TrustedHead {
2880 id: gov(2),
2881 chain_seq: 3,
2882 entry_hash: c.hash_at(3),
2883 },
2884 c.prev_hash(),
2885 at(45),
2886 "the Steward's key is the compromised one, trust me",
2887 );
2888 c.rotate(&k1, &hijack); c.decision(&k1); let v = verify_chain(&c.links, &k1_pk, &anchor);
2892 assert!(!v.ok);
2893 assert_eq!(v.public_key, (&k2_pk).into(), "the chain stays with K2");
2894 assert!(!v.entries[3].signature_valid, "{:#?}", v.entries[3]);
2895 assert!(!v.entries[4].signature_valid, "K1 signs nothing again");
2896 assert_eq!(v.keys.len(), 2);
2897 assert_eq!(v.keys[1].status, KeyStatus::Active);
2898 }
2899
2900 #[test]
2901 fn a_routine_rotation_cannot_reuse_a_key_either() {
2902 let (k1, k1_pk) = generate_keypair();
2903 let (k2, k2_pk) = generate_keypair();
2904 let anchor = anchored(&k1_pk).with((&k2_pk).into());
2905 let mut c = Chain::new();
2906 c.decision(&k1);
2907 let out = KeyRotation::routine(
2908 (&k1_pk).into(),
2909 &k2,
2910 c.prev_hash(),
2911 at(15),
2912 "",
2913 );
2914 c.rotate(&k1, &out);
2915 let back = KeyRotation::routine(
2916 (&k2_pk).into(),
2917 &k1,
2918 c.prev_hash(),
2919 at(25),
2920 "",
2921 );
2922 c.rotate(&k2, &back);
2923 let v = verify_chain(&c.links, &k1_pk, &anchor);
2924 assert!(!v.ok);
2925 assert!(
2926 v.entries[2]
2927 .problem
2928 .as_deref()
2929 .unwrap()
2930 .contains("never brought back"),
2931 "{:#?}",
2932 v.entries[2]
2933 );
2934 assert_eq!(v.public_key, (&k2_pk).into());
2935 }
2936
2937 #[test]
2938 fn a_forged_entry_has_no_effects() {
2939 let (steward, steward_pk) = generate_keypair();
2940 let (forger, _) = generate_keypair();
2941 let anchor = anchored(&steward_pk);
2942 let mut c = Chain::new();
2943 let target = c.decision(&steward); let fake = Amendment::new(
2945 target,
2946 c.hash_at(1),
2947 AmendmentKind::Overruled,
2948 "none",
2949 "overruled, says nobody with the key",
2950 )
2951 .unwrap();
2952 c.amend(&forger, &fake); let grab = KeyRotation::routine(
2954 (&steward_pk).into(),
2955 &forger,
2956 c.prev_hash(),
2957 at(35),
2958 "",
2959 );
2960 c.rotate(&forger, &grab); let v = verify_chain(&c.links, &steward_pk, &anchor);
2963 assert!(!v.ok);
2964 assert!(v.entries[0].amended_by.is_empty(), "{:#?}", v.entries[0]);
2965 assert_eq!(v.public_key, (&steward_pk).into());
2966 assert_eq!(v.keys.len(), 1);
2967 assert!(v.unanchored_keys.is_empty());
2968 }
2969
2970 #[test]
2971 fn a_repeated_id_is_a_problem() {
2972 let (key, pk) = generate_keypair();
2973 let mut c = Chain::new();
2974 c.decision(&key);
2975 c.gov = 0;
2976 c.decision(&key); let v = verify_chain(&c.links, &pk, &anchored(&pk));
2978 assert!(!v.ok);
2979 assert!(v.entries.iter().all(|e| {
2980 e.problem
2981 .as_deref()
2982 .is_some_and(|p| p.contains("more than once"))
2983 }));
2984 }
2985
2986 #[test]
2987 fn a_second_compromise_cannot_anchor_inside_the_first_window() {
2988 let (k1, k1_pk) = generate_keypair();
2989 let (k2, k2_pk) = generate_keypair();
2990 let (k3, k3_pk) = generate_keypair();
2991 let anchor =
2992 anchored(&k1_pk).with((&k2_pk).into()).with((&k3_pk).into());
2993 let mut c = Chain::new();
2994 c.decision(&k1); c.decision(&k1); let first = KeyRotation::compromise(
2997 (&k1_pk).into(),
2998 &k2,
2999 TrustedHead {
3000 id: gov(1),
3001 chain_seq: 1,
3002 entry_hash: c.hash_at(1),
3003 },
3004 c.prev_hash(),
3005 at(35),
3006 "",
3007 );
3008 c.rotate(&k2, &first); let second = KeyRotation::compromise(
3010 (&k1_pk).into(),
3011 &k3,
3012 TrustedHead {
3013 id: gov(2),
3014 chain_seq: 2,
3015 entry_hash: c.hash_at(2),
3016 },
3017 c.prev_hash(),
3018 at(45),
3019 "",
3020 );
3021 c.rotate(&k3, &second); let v = verify_chain(&c.links, &k1_pk, &anchor);
3024 assert!(!v.ok);
3025 let p = v.entries[3].problem.as_deref().unwrap();
3026 assert!(p.contains("repudiated"), "{p}");
3027 assert_eq!(v.keys.len(), 2, "K1 and K2; K3 never took the chain");
3028 assert_eq!(v.keys[0].through_seq, Some(1));
3029 }
3030
3031 #[test]
3032 fn an_unanchored_compromise_fails_closed() {
3033 let (old, old_pk) = generate_keypair();
3034 let (new, _) = generate_keypair();
3035 let mut c = Chain::new();
3036 c.decision(&old);
3037 c.decision(&old);
3038 let rotation = KeyRotation::compromise(
3039 (&old_pk).into(),
3040 &new,
3041 TrustedHead {
3042 id: gov(1),
3043 chain_seq: 1,
3044 entry_hash: c.hash_at(1),
3045 },
3046 c.prev_hash(),
3047 at(45),
3048 "trust me",
3049 );
3050 c.rotate(&new, &rotation);
3051
3052 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk));
3055 assert!(!v.ok, "{v:#?}");
3056 let p = v.entries[2].problem.as_deref().unwrap();
3057 assert!(p.contains("outside the trust anchor"), "{p}");
3058 assert!(!v.entries[2].signature_valid, "checked under the old key");
3059 assert!(v.repudiated.is_empty(), "and nothing is repudiated");
3060 assert_eq!(v.public_key, (&old_pk).into());
3061 }
3062
3063 #[test]
3067 fn a_thiefs_rotation_is_void_once_a_compromise_names_an_earlier_head() {
3068 let (steward, steward_pk) = generate_keypair();
3069 let (thief, thief_pk) = generate_keypair();
3070 let (recovery, recovery_pk) = generate_keypair();
3071 let anchor = anchored(&steward_pk).with((&recovery_pk).into());
3072
3073 let mut c = Chain::new();
3074 c.decision(&steward); let stolen = KeyRotation::routine(
3076 (&steward_pk).into(),
3077 &thief,
3078 c.prev_hash(),
3079 at(25),
3080 "routine",
3081 );
3082 c.rotate(&steward, &stolen); c.decision(&thief); let declaration = KeyRotation::compromise(
3086 (&steward_pk).into(),
3087 &recovery,
3088 TrustedHead {
3089 id: gov(1),
3090 chain_seq: 1,
3091 entry_hash: c.hash_at(1),
3092 },
3093 c.prev_hash(),
3094 at(55),
3095 "key stolen; everything after GOV-2026-0001 is disclaimed",
3096 );
3097 c.rotate(&recovery, &declaration); let v = verify_chain(&c.links, &steward_pk, &anchor);
3100 assert!(v.ok, "{v:#?}");
3101 assert_eq!(v.repudiated, vec![key_id(1), gov(2)]);
3102 assert!(v.entries[1].repudiated && v.entries[2].repudiated);
3103 assert_eq!(
3104 v.unanchored_keys,
3105 vec![PublicKeyHex::from(&thief_pk)],
3106 "and the theft was visible as it happened"
3107 );
3108 assert_eq!(v.public_key, (&recovery_pk).into());
3109 assert_eq!(v.keys.len(), 2, "the thief's key is not part of history");
3110 assert_eq!(v.keys[0].public_key, (&steward_pk).into());
3111 assert_eq!(v.keys[0].status, KeyStatus::Compromised);
3112 assert_eq!(v.keys[1].public_key, (&recovery_pk).into());
3113 }
3114
3115 #[test]
3116 fn a_compromise_must_name_a_real_head_and_the_key_that_held_it() {
3117 let (old, old_pk) = generate_keypair();
3118 let (new, new_pk) = generate_keypair();
3119 let anchor = anchored(&old_pk).with((&new_pk).into());
3120 let head = |c: &Chain| TrustedHead {
3121 id: gov(1),
3122 chain_seq: 1,
3123 entry_hash: c.hash_at(1),
3124 };
3125
3126 let mut c = Chain::new();
3128 c.decision(&old);
3129 let mut trusted = head(&c);
3130 trusted.entry_hash = data_hash(&json!("nope"));
3131 let rotation = KeyRotation::compromise(
3132 (&old_pk).into(),
3133 &new,
3134 trusted,
3135 c.prev_hash(),
3136 at(45),
3137 "n",
3138 );
3139 c.rotate(&new, &rotation);
3140 let v = verify_chain(&c.links, &old_pk, &anchor);
3141 let p = v.entries[1].problem.as_deref().unwrap();
3142 assert!(p.contains("last_trusted"), "{p}");
3143
3144 let (other, _) = generate_keypair();
3146 let mut c = Chain::new();
3147 c.decision(&old);
3148 let rotation = KeyRotation::compromise(
3149 (&other.verifying_key()).into(),
3150 &new,
3151 head(&c),
3152 c.prev_hash(),
3153 at(45),
3154 "n",
3155 );
3156 c.rotate(&new, &rotation);
3157 let v = verify_chain(&c.links, &old_pk, &anchor);
3158 let p = v.entries[1].problem.as_deref().unwrap();
3159 assert!(p.contains("old_key is not the key"), "{p}");
3160
3161 let mut c = Chain::new();
3163 c.decision(&old);
3164 let mut rotation = KeyRotation::routine(
3165 (&old_pk).into(),
3166 &new,
3167 c.prev_hash(),
3168 at(45),
3169 "n",
3170 );
3171 rotation.last_trusted = Some(head(&c));
3172 c.rotate(&old, &rotation);
3173 let v = verify_chain(&c.links, &old_pk, &anchor);
3174 let p = v.entries[1].problem.as_deref().unwrap();
3175 assert!(p.contains("must not name last_trusted"), "{p}");
3176 }
3177
3178 #[test]
3179 fn a_genesis_key_outside_the_anchor_is_reported_not_rejected() {
3180 let (key, pk) = generate_keypair();
3181 let c = chain(&key, 2);
3182 let v = verify_chain(&c, &pk, &KeyAnchor::default());
3183 assert!(v.ok, "{v:#?}");
3184 assert_eq!(v.unanchored_keys, vec![PublicKeyHex::from(&pk)]);
3185 assert_eq!(v.keys.len(), 1);
3186 assert_eq!(v.keys[0].status, KeyStatus::Active);
3187 assert_eq!(v.keys[0].from_seq, 1);
3188 assert_eq!(v.keys[0].through_seq, None);
3189 }
3190
3191 #[test]
3192 fn published_keys_are_curve_points_and_make_an_anchor() {
3193 let anchor = KeyAnchor::published();
3194 assert!(!anchor.is_empty());
3195 assert_eq!(anchor.keys().count(), PUBLISHED_KEYS.len());
3196 for published in PUBLISHED_KEYS {
3197 let key: PublicKeyHex = published.parse().unwrap();
3198 assert!(anchor.contains(&key));
3199 assert!(
3200 key.to_verifying_key().is_ok(),
3201 "{published} is not a valid Ed25519 public key"
3202 );
3203 }
3204 let (_, other) = generate_keypair();
3205 assert!(!anchor.contains(&(&other).into()));
3206 assert!(
3207 anchor
3208 .clone()
3209 .with((&other).into())
3210 .contains(&(&other).into())
3211 );
3212 }
3213
3214 #[test]
3217 fn amendments_and_rotations_round_trip_as_entry_data() {
3218 let (key, pk) = generate_keypair();
3219 let amendment = Amendment::new(
3220 gov(1),
3221 data_hash(&json!("x")),
3222 AmendmentKind::Superseded,
3223 "Art. VI § 2",
3224 "superseded by GOV-2026-0009",
3225 )
3226 .unwrap()
3227 .with_authority(gov(9))
3228 .with_rationale("the later decision covers the same subject");
3229 let value = serde_json::to_value(&amendment).unwrap();
3230 assert_eq!(value["kind"], "superseded");
3231 assert_eq!(value["agora_governance_amendment"], 1);
3232 assert!(value.get("redaction").is_none(), "{value}");
3233 assert_eq!(
3234 serde_json::from_value::<Amendment>(value).unwrap(),
3235 amendment
3236 );
3237
3238 let rotation = KeyRotation::compromise(
3239 (&pk).into(),
3240 &key,
3241 TrustedHead {
3242 id: gov(1),
3243 chain_seq: 1,
3244 entry_hash: data_hash(&json!("x")),
3245 },
3246 Some(data_hash(&json!("prev"))),
3247 at(0),
3248 "note",
3249 );
3250 let value = serde_json::to_value(&rotation).unwrap();
3251 assert_eq!(value["reason"], "compromise");
3252 assert_eq!(value["last_trusted"]["chain_seq"], 1);
3253 assert_eq!(
3254 serde_json::from_value::<KeyRotation>(value).unwrap(),
3255 rotation
3256 );
3257
3258 let notice = AmendmentNotice::new(amd(1), at(0), &amendment);
3259 let value = serde_json::to_value(¬ice).unwrap();
3260 assert_eq!(value["id"], "AMD-2026-0001");
3261 assert_eq!(value["note"], "superseded by GOV-2026-0009");
3262 assert_eq!(
3263 serde_json::from_value::<AmendmentNotice>(value).unwrap(),
3264 notice
3265 );
3266 }
3267
3268 #[test]
3272 fn an_amendment_verifies_with_or_without_its_optional_fields() {
3273 let (key, pk) = generate_keypair();
3274 let mut c = Chain::new();
3275 let target = c.decision(&key);
3276 let bare = Amendment::new(
3277 target.clone(),
3278 c.hash_at(1),
3279 AmendmentKind::Correction,
3280 "clerical",
3281 "typo in the citation",
3282 )
3283 .unwrap();
3284 assert!(
3285 serde_json::to_value(&bare)
3286 .unwrap()
3287 .get("rationale")
3288 .is_none()
3289 );
3290 c.amend(&key, &bare);
3291 let full = bare.clone().with_rationale("at length: …");
3292 c.amend(&key, &full);
3293 let mut future = serde_json::to_value(&full).unwrap();
3295 future["superseded_by_something_new"] = json!(["later"]);
3296 c.push(
3297 &key,
3298 amd(3),
3299 GovernanceLogEntryType::Amendment,
3300 future,
3301 true,
3302 );
3303
3304 let v = verify_chain(&c.links, &pk, &anchored(&pk));
3305 assert!(v.ok, "{v:#?}");
3306 assert_eq!(
3307 v.entries[0].amended_by,
3308 vec![amd(1), amd(2), amd(3)],
3309 "all three name the target"
3310 );
3311 }
3312
3313 #[test]
3316 fn pre_0_26_wire_still_deserializes() {
3317 let link: GovernanceChainLink = serde_json::from_value(json!({
3318 "id": "GOV-2026-0001",
3319 "entry_type": "council_decision",
3320 "created_at": "2023-11-14T22:13:20.123456Z",
3321 "attestation": {
3322 "envelope_version": 1,
3323 "chain_seq": 1,
3324 "prev_hash": null,
3325 "data_hash": "00".repeat(32),
3326 "entry_hash": "11".repeat(32),
3327 "signature": "22".repeat(64),
3328 "signed_at": "2023-11-14T22:13:21Z",
3329 "retroactive": false,
3330 },
3331 }))
3332 .unwrap();
3333 assert!(link.data.is_none());
3334 assert!(
3335 serde_json::to_value(&link).unwrap().get("data").is_none(),
3336 "and a link without data does not grow a null field"
3337 );
3338
3339 let verdict: EntryVerdict = serde_json::from_value(json!({
3340 "id": "GOV-2026-0001",
3341 "chain_seq": 1,
3342 "signature_valid": true,
3343 "link_valid": true,
3344 "content_matches": null,
3345 "retroactive": false,
3346 "out_of_order": false,
3347 "amended_by": [],
3348 }))
3349 .unwrap();
3350 assert!(!verdict.repudiated && !verdict.redacted);
3351 assert!(verdict.signed_by.is_none());
3352 assert!(verdict.reattested_by.is_empty());
3353
3354 let verification: GovernanceVerification =
3355 serde_json::from_value(json!({
3356 "public_key": "33".repeat(32),
3357 "ok": true,
3358 "head": "GOV-2026-0001",
3359 "entries": [],
3360 }))
3361 .unwrap();
3362 assert!(verification.keys.is_empty());
3363 assert!(verification.unanchored_keys.is_empty());
3364 assert!(verification.repudiated.is_empty());
3365 }
3366
3367 #[test]
3368 fn the_report_round_trips() {
3369 let (key, pk) = generate_keypair();
3370 let mut c = Chain::new();
3371 let target = c.decision(&key);
3372 let amendment = Amendment::new(
3373 target,
3374 c.hash_at(1),
3375 AmendmentKind::Overruled,
3376 "b",
3377 "n",
3378 )
3379 .unwrap();
3380 c.amend(&key, &amendment);
3381 let v = verify_chain(&c.links, &pk, &anchored(&pk));
3382 let text = serde_json::to_string(&v).unwrap();
3383 assert_eq!(
3384 serde_json::from_str::<GovernanceVerification>(&text).unwrap(),
3385 v
3386 );
3387
3388 let keys = GovernanceSigningKeys { keys: v.keys };
3389 let value = serde_json::to_value(&keys).unwrap();
3390 assert!(value["keys"].is_array(), "an object, not a bare array");
3391 assert_eq!(value["keys"][0]["status"], "active");
3392 assert_eq!(
3393 serde_json::from_value::<GovernanceSigningKeys>(value).unwrap(),
3394 keys
3395 );
3396 }
3397
3398 #[test]
3402 fn envelope_v1_preimage_and_hash_are_pinned() {
3403 let envelope = Envelope::new(
3404 gov(6),
3405 GovernanceLogEntryType::CouncilDecision,
3406 at(0),
3407 Some(Sha256Hex::from([0x11; 32])),
3408 data_hash(&json!({"outcome": "approved", "title": "Ratification"})),
3409 );
3410 assert_eq!(
3411 String::from_utf8(envelope.preimage()).unwrap(),
3412 "{\"agora_governance_log\":1,\"id\":\"GOV-2026-0006\",\
3413 \"entry_type\":\"council_decision\",\
3414 \"created_at\":1700000000123456,\
3415 \"prev_hash\":\"1111111111111111111111111111111111111111111111111111111111111111\",\
3416 \"data_hash\":\"a4adf645ae3f60c56484d01aea87d6d490321d7fc66b1607df14b023fe567c7b\"}"
3417 );
3418 assert_eq!(
3419 envelope.entry_hash().to_hex(),
3420 "ba27577432f81e415f1c01cc4cfabab6070e3ac50fd468fffe195ef19c0e9464"
3421 );
3422 }
3423
3424 #[cfg(feature = "agora-client")]
3431 #[tokio::test]
3432 #[ignore = "networked: hits the live platform"]
3433 async fn the_published_key_is_the_one_the_platform_serves() {
3434 let client = crate::client::Client::new(
3435 url::Url::parse("https://subliminal.technology").unwrap(),
3436 )
3437 .unwrap();
3438 let served = client.get_governance_signing_key().await.unwrap();
3439 let newest: PublicKeyHex = PUBLISHED_KEYS
3440 .last()
3441 .expect("PUBLISHED_KEYS is never empty")
3442 .parse()
3443 .unwrap();
3444 assert_eq!(served.algorithm, "ed25519");
3445 assert_eq!(
3446 served.public_key, newest,
3447 "the platform serves {} but the newest key in PUBLISHED_KEYS is \
3448 {newest} — if this is a rotation, agentkit publishes the new \
3449 key FIRST and the rotation entry second",
3450 served.public_key
3451 );
3452 }
3453
3454 #[cfg(feature = "schemars")]
3455 #[test]
3456 fn wire_schemas_are_ref_free() {
3457 use crate::responses::inline_schema_for;
3458 for (name, schema) in [
3459 (
3460 "GovernanceAttestation",
3461 inline_schema_for::<GovernanceAttestation>(),
3462 ),
3463 (
3464 "GovernanceChainLink",
3465 inline_schema_for::<GovernanceChainLink>(),
3466 ),
3467 (
3468 "GovernanceSigningKey",
3469 inline_schema_for::<GovernanceSigningKey>(),
3470 ),
3471 (
3472 "GovernanceVerification",
3473 inline_schema_for::<GovernanceVerification>(),
3474 ),
3475 (
3476 "Vec<GovernanceChainLink>",
3477 inline_schema_for::<Vec<GovernanceChainLink>>(),
3478 ),
3479 ("Amendment", inline_schema_for::<Amendment>()),
3480 ("Redaction", inline_schema_for::<Redaction>()),
3481 ("AmendmentNotice", inline_schema_for::<AmendmentNotice>()),
3482 ("KeyRotation", inline_schema_for::<KeyRotation>()),
3483 ("TrustedHead", inline_schema_for::<TrustedHead>()),
3484 (
3485 "GovernanceKeyRecord",
3486 inline_schema_for::<GovernanceKeyRecord>(),
3487 ),
3488 (
3489 "GovernanceSigningKeys",
3490 inline_schema_for::<GovernanceSigningKeys>(),
3491 ),
3492 ("AmendmentKind", inline_schema_for::<AmendmentKind>()),
3493 ("Standing", inline_schema_for::<Standing>()),
3494 ("KeyStatus", inline_schema_for::<KeyStatus>()),
3495 ("RotationReason", inline_schema_for::<RotationReason>()),
3496 ] {
3497 let text = serde_json::to_string(&schema).unwrap();
3498 assert!(!text.contains("$ref"), "{name} must be $ref-free: {text}");
3499 assert!(
3500 !text.contains("$defs"),
3501 "{name} must be $defs-free: {text}"
3502 );
3503 }
3504 let text =
3505 serde_json::to_string(&inline_schema_for::<Sha256Hex>()).unwrap();
3506 assert!(text.contains("^[0-9a-f]{64}$"), "{text}");
3507 let text = serde_json::to_string(&inline_schema_for::<SignatureHex>())
3508 .unwrap();
3509 assert!(text.contains("^[0-9a-f]{128}$"), "{text}");
3510 }
3511}