1use crate::crypto::{self, Signature, SigningKey, VerifyingKey};
52use crate::enums::GovernanceLogEntryType;
53use crate::ids::{GovernanceLogId, GovernanceLogPrefix};
54use chrono::{DateTime, Utc};
55use serde::{Deserialize, Serialize};
56use sha2::{Digest, Sha256};
57use std::collections::{HashMap, HashSet};
58
59pub use crate::enums::{AmendmentKind, KeyStatus, Standing};
60
61mod texts;
62pub use texts::{
63 AmendmentText, AmendmentTextStatus, AmendmentTexts, CommittedText,
64 TextCommitment, TextStatus, WITHHELD_TEXT,
65};
66
67mod record;
68pub use record::{
69 RecordAttachment, RecordParticipant, STEWARD_RECORD_VERSION, StewardRecord,
70};
71
72mod root;
73pub use root::{
74 CertPurpose, CertificateError, KEY_CERT_VERSION, KeyCertStatement,
75 KeyCertificate, ROOT_DOMAIN, ROOT_KEYS, ROOT_THRESHOLD, RootSet,
76 RootSignature,
77};
78
79#[cfg(test)]
81mod vectors;
82
83pub const ENVELOPE_VERSION: u32 = 1;
85
86pub const RETROACTIVE_AFTER: chrono::Duration = chrono::Duration::seconds(60);
89
90#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
97#[error("{type_name}: expected {expected} bytes of hex, got {got:?}")]
98pub struct HexLengthError {
99 pub type_name: &'static str,
100 pub expected: usize,
101 pub got: String,
102}
103
104macro_rules! hex_bytes {
105 ($(#[$meta:meta])* $name:ident, $len:expr) => {
106 $(#[$meta])*
107 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
108 #[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
109 #[cfg_attr(feature = "sqlx", sqlx(transparent))]
110 pub struct $name([u8; $len]);
111
112 impl $name {
113 pub fn as_bytes(&self) -> &[u8; $len] {
115 &self.0
116 }
117
118 pub fn to_hex(&self) -> String {
120 hex::encode(self.0)
121 }
122 }
123
124 impl From<[u8; $len]> for $name {
125 fn from(bytes: [u8; $len]) -> Self {
126 Self(bytes)
127 }
128 }
129
130 impl TryFrom<&[u8]> for $name {
131 type Error = HexLengthError;
132
133 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
134 <[u8; $len]>::try_from(bytes).map(Self).map_err(|_| {
135 HexLengthError {
136 type_name: stringify!($name),
137 expected: $len,
138 got: hex::encode(bytes),
139 }
140 })
141 }
142 }
143
144 impl TryFrom<Vec<u8>> for $name {
145 type Error = HexLengthError;
146
147 fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
148 Self::try_from(bytes.as_slice())
149 }
150 }
151
152 impl std::str::FromStr for $name {
153 type Err = HexLengthError;
154
155 fn from_str(s: &str) -> Result<Self, Self::Err> {
156 let bytes = hex::decode(s.trim()).map_err(|_| HexLengthError {
157 type_name: stringify!($name),
158 expected: $len,
159 got: s.to_string(),
160 })?;
161 Self::try_from(bytes.as_slice())
162 }
163 }
164
165 impl std::fmt::Display for $name {
166 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
167 f.write_str(&self.to_hex())
168 }
169 }
170
171 impl std::fmt::Debug for $name {
172 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173 write!(f, "{}({})", stringify!($name), self.to_hex())
174 }
175 }
176
177 impl Serialize for $name {
178 fn serialize<S: serde::Serializer>(
179 &self,
180 s: S,
181 ) -> Result<S::Ok, S::Error> {
182 s.serialize_str(&self.to_hex())
183 }
184 }
185
186 impl<'de> Deserialize<'de> for $name {
187 fn deserialize<D: serde::Deserializer<'de>>(
188 d: D,
189 ) -> Result<Self, D::Error> {
190 let s = String::deserialize(d)?;
191 s.parse().map_err(serde::de::Error::custom)
192 }
193 }
194
195 #[cfg(feature = "schemars")]
199 impl schemars::JsonSchema for $name {
200 fn inline_schema() -> bool {
201 true
202 }
203
204 fn schema_name() -> std::borrow::Cow<'static, str> {
205 std::borrow::Cow::Borrowed(stringify!($name))
206 }
207
208 fn schema_id() -> std::borrow::Cow<'static, str> {
209 std::borrow::Cow::Borrowed(concat!(
210 module_path!(),
211 "::",
212 stringify!($name)
213 ))
214 }
215
216 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
217 schemars::json_schema!({
218 "type": "string",
219 "pattern": format!("^[0-9a-f]{{{}}}$", $len * 2),
220 "description": format!("{} bytes, lowercase hex", $len),
221 })
222 }
223 }
224 };
225}
226
227hex_bytes!(
228 Sha256Hex,
230 32
231);
232
233hex_bytes!(
234 SignatureHex,
236 64
237);
238
239hex_bytes!(
240 PublicKeyHex,
242 32
243);
244
245hex_bytes!(
246 Blind,
249 32
250);
251
252hex_bytes!(
253 TextSalt,
256 32
257);
258
259impl Blind {
260 pub fn random() -> Self {
262 use rand::RngCore;
263 let mut bytes = [0u8; 32];
264 rand::rngs::OsRng.fill_bytes(&mut bytes);
265 Self(bytes)
266 }
267}
268
269impl TextSalt {
270 pub fn random() -> Self {
272 Self(*Blind::random().as_bytes())
273 }
274}
275
276impl From<Signature> for SignatureHex {
277 fn from(sig: Signature) -> Self {
278 Self(sig.to_bytes())
279 }
280}
281
282impl From<&SignatureHex> for Signature {
283 fn from(sig: &SignatureHex) -> Self {
284 Signature::from_bytes(&sig.0)
285 }
286}
287
288impl From<&VerifyingKey> for PublicKeyHex {
289 fn from(key: &VerifyingKey) -> Self {
290 Self(key.to_bytes())
291 }
292}
293
294impl PublicKeyHex {
295 pub fn to_verifying_key(
297 &self,
298 ) -> Result<VerifyingKey, ed25519_dalek::SignatureError> {
299 VerifyingKey::from_bytes(&self.0)
300 }
301}
302
303pub fn canonical_json(value: &serde_json::Value) -> Vec<u8> {
316 let mut out = Vec::new();
317 write_canonical(value, &mut out);
318 out
319}
320
321fn write_canonical(value: &serde_json::Value, out: &mut Vec<u8>) {
322 use serde_json::Value;
323 match value {
324 Value::Null => out.extend_from_slice(b"null"),
325 Value::Bool(b) => {
326 out.extend_from_slice(if *b { b"true" } else { b"false" })
327 }
328 Value::Number(n) => serde_json::to_writer(&mut *out, n)
329 .expect("a number always serializes"),
330 Value::String(s) => serde_json::to_writer(&mut *out, s)
331 .expect("a string always serializes"),
332 Value::Array(items) => {
333 out.push(b'[');
334 for (i, item) in items.iter().enumerate() {
335 if i > 0 {
336 out.push(b',');
337 }
338 write_canonical(item, out);
339 }
340 out.push(b']');
341 }
342 Value::Object(map) => {
343 let mut keys: Vec<&String> = map.keys().collect();
344 keys.sort_unstable();
345 out.push(b'{');
346 for (i, key) in keys.into_iter().enumerate() {
347 if i > 0 {
348 out.push(b',');
349 }
350 serde_json::to_writer(&mut *out, key)
351 .expect("a string always serializes");
352 out.push(b':');
353 write_canonical(&map[key], out);
354 }
355 out.push(b'}');
356 }
357 }
358}
359
360pub fn non_integer_number(data: &serde_json::Value) -> Option<String> {
371 fn find(value: &serde_json::Value, path: &mut String) -> bool {
372 use serde_json::Value::{Array, Number, Object};
373 let mark = path.len();
374 match value {
375 Number(n) => return n.is_f64(),
376 Array(items) => {
377 for (i, item) in items.iter().enumerate() {
378 path.push_str(&format!("/{i}"));
379 if find(item, path) {
380 return true;
381 }
382 path.truncate(mark);
383 }
384 }
385 Object(map) => {
386 for (key, item) in map {
387 path.push('/');
388 path.push_str(&key.replace('~', "~0").replace('/', "~1"));
389 if find(item, path) {
390 return true;
391 }
392 path.truncate(mark);
393 }
394 }
395 _ => {}
396 }
397 false
398 }
399 let mut path = String::new();
400 find(data, &mut path).then_some(path)
401}
402
403pub fn data_hash(data: &serde_json::Value) -> Sha256Hex {
405 Sha256Hex(Sha256::digest(canonical_json(data)).into())
406}
407
408#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
413pub struct Envelope {
414 pub agora_governance_log: u32,
416 pub id: GovernanceLogId,
417 pub entry_type: GovernanceLogEntryType,
418 pub created_at: i64,
420 pub prev_hash: Option<Sha256Hex>,
421 pub data_hash: Sha256Hex,
422}
423
424impl Envelope {
425 pub fn new(
427 id: GovernanceLogId,
428 entry_type: GovernanceLogEntryType,
429 created_at: DateTime<Utc>,
430 prev_hash: Option<Sha256Hex>,
431 data_hash: Sha256Hex,
432 ) -> Self {
433 Self {
434 agora_governance_log: ENVELOPE_VERSION,
435 id,
436 entry_type,
437 created_at: created_at.timestamp_micros(),
438 prev_hash,
439 data_hash,
440 }
441 }
442
443 pub fn created_at(&self) -> DateTime<Utc> {
445 DateTime::from_timestamp_micros(self.created_at)
446 .expect("an Envelope only ever holds an in-range timestamp")
447 }
448
449 pub fn preimage(&self) -> Vec<u8> {
451 serde_json::to_vec(self).expect("an Envelope always serializes")
452 }
453
454 pub fn entry_hash(&self) -> Sha256Hex {
456 Sha256Hex(Sha256::digest(self.preimage()).into())
457 }
458}
459
460pub fn truncate_to_micros(t: DateTime<Utc>) -> DateTime<Utc> {
463 DateTime::from_timestamp_micros(t.timestamp_micros())
464 .expect("a timestamp that came from a DateTime is in range")
465}
466
467pub fn truncate_to_seconds(t: DateTime<Utc>) -> DateTime<Utc> {
470 DateTime::from_timestamp(t.timestamp(), 0)
471 .expect("a timestamp that came from a DateTime is in range")
472}
473
474pub fn is_retroactive(
478 created_at: DateTime<Utc>,
479 signed_at: DateTime<Utc>,
480) -> bool {
481 signed_at - created_at > RETROACTIVE_AFTER
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 GovernanceAttestation {
493 pub envelope_version: u32,
495 pub chain_seq: u64,
498 pub prev_hash: Option<Sha256Hex>,
500 pub data_hash: Sha256Hex,
502 pub entry_hash: Sha256Hex,
504 pub signature: SignatureHex,
507 pub signed_at: DateTime<Utc>,
510 pub retroactive: bool,
514}
515
516#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
520#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
521#[cfg_attr(feature = "schemars", schemars(inline))]
522pub struct GovernanceChainLink {
523 pub id: GovernanceLogId,
524 pub entry_type: GovernanceLogEntryType,
525 pub created_at: DateTime<Utc>,
526 pub attestation: GovernanceAttestation,
527 #[serde(default, skip_serializing_if = "Option::is_none")]
534 pub data: Option<serde_json::Value>,
535 #[serde(
539 default,
540 skip_serializing_if = "Option::is_none",
541 deserialize_with = "read_as_written"
542 )]
543 pub texts: Option<AmendmentTexts>,
544}
545
546#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
549#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
550#[cfg_attr(feature = "schemars", schemars(inline))]
551pub struct GovernanceSigningKey {
552 pub algorithm: String,
554 pub public_key: PublicKeyHex,
555 pub envelope_version: u32,
557}
558
559impl GovernanceSigningKey {
560 pub fn new(key: &VerifyingKey) -> Self {
562 Self {
563 algorithm: "ed25519".to_string(),
564 public_key: key.into(),
565 envelope_version: ENVELOPE_VERSION,
566 }
567 }
568}
569
570pub const AMENDMENT_VERSION: u32 = 2;
578
579#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
581pub enum AmendmentError {
582 #[error("agora_governance_amendment is {0}, not 1 or {AMENDMENT_VERSION}")]
583 UnsupportedVersion(u32),
584 #[error(
585 "a version 1 amendment carries its texts and a version \
586 {AMENDMENT_VERSION} one commits to them; this does neither \
587 consistently"
588 )]
589 TextShape,
590 #[error("`{0}` beside the entry is not the text the entry committed to")]
591 TextMismatch(&'static str),
592 #[error("texts beside an entry that commits to none")]
593 UncommittedText,
594 #[error("kind `redaction` requires a `redaction`")]
595 MissingRedaction,
596 #[error("`redaction` is only valid on kind `redaction`")]
597 UnexpectedRedaction,
598 #[error("amendment target {0} is not an entry of this chain")]
599 UnknownTarget(GovernanceLogId),
600 #[error("amendment target {0} is not an earlier entry")]
601 ForwardReference(GovernanceLogId),
602 #[error("target_entry_hash is not {0}'s entry_hash")]
603 WrongTargetHash(GovernanceLogId),
604}
605
606#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
612#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
613#[cfg_attr(feature = "schemars", schemars(inline))]
614pub struct Amendment {
615 pub agora_governance_amendment: u32,
617 pub target: GovernanceLogId,
618 pub target_entry_hash: Sha256Hex,
620 pub kind: AmendmentKind,
621 #[serde(default)]
624 pub authority: Option<GovernanceLogId>,
625 pub basis: AmendmentText,
629 pub note: AmendmentText,
631 #[serde(default, skip_serializing_if = "Option::is_none")]
633 pub rationale: Option<AmendmentText>,
634 #[serde(default, skip_serializing_if = "Option::is_none")]
636 pub redaction: Option<Redaction>,
637}
638
639#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
641#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
642#[cfg_attr(feature = "schemars", schemars(inline))]
643pub struct Redaction {
644 pub fields: Vec<String>,
647 pub resulting_data_hash: Sha256Hex,
650}
651
652#[derive(Debug, Clone, PartialEq, Eq)]
655pub struct AmendmentDraft {
656 pub amendment: Amendment,
657 pub texts: AmendmentTexts,
658}
659
660impl AmendmentDraft {
661 pub fn new(
668 target: GovernanceLogId,
669 target_entry_hash: Sha256Hex,
670 kind: AmendmentKind,
671 basis: impl Into<CommittedText>,
672 note: impl Into<CommittedText>,
673 ) -> Result<Self, AmendmentError> {
674 if kind == AmendmentKind::Redaction {
675 return Err(AmendmentError::MissingRedaction);
676 }
677 let (basis, note) = (basis.into(), note.into());
678 Ok(Self {
679 amendment: Amendment {
680 agora_governance_amendment: AMENDMENT_VERSION,
681 target,
682 target_entry_hash,
683 kind,
684 authority: None,
685 basis: AmendmentText::Committed(basis.commitment()),
686 note: AmendmentText::Committed(note.commitment()),
687 rationale: None,
688 redaction: None,
689 },
690 texts: AmendmentTexts {
691 basis: Some(basis),
692 note: Some(note),
693 rationale: None,
694 },
695 })
696 }
697
698 #[allow(clippy::too_many_arguments)]
708 pub fn redaction(
709 amendment_id: &GovernanceLogId,
710 target: GovernanceLogId,
711 target_entry_hash: Sha256Hex,
712 basis: impl Into<CommittedText>,
713 note: impl Into<CommittedText>,
714 fields: Vec<String>,
715 data: &serde_json::Value,
716 blind: Blind,
717 ) -> Result<(Self, serde_json::Value), RedactError> {
718 let redacted = redact_data(data, &fields, amendment_id, blind)?;
719 let (basis, note) = (basis.into(), note.into());
720 Ok((
721 Self {
722 amendment: Amendment {
723 agora_governance_amendment: AMENDMENT_VERSION,
724 target,
725 target_entry_hash,
726 kind: AmendmentKind::Redaction,
727 authority: None,
728 basis: AmendmentText::Committed(basis.commitment()),
729 note: AmendmentText::Committed(note.commitment()),
730 rationale: None,
731 redaction: Some(Redaction {
732 fields,
733 resulting_data_hash: data_hash(&redacted),
734 }),
735 },
736 texts: AmendmentTexts {
737 basis: Some(basis),
738 note: Some(note),
739 rationale: None,
740 },
741 },
742 redacted,
743 ))
744 }
745
746 pub fn with_authority(mut self, authority: GovernanceLogId) -> Self {
748 self.amendment.authority = Some(authority);
749 self
750 }
751
752 pub fn with_rationale(
754 mut self,
755 rationale: impl Into<CommittedText>,
756 ) -> Self {
757 let rationale = rationale.into();
758 self.amendment.rationale =
759 Some(AmendmentText::Committed(rationale.commitment()));
760 self.texts.rationale = Some(rationale);
761 self
762 }
763}
764
765impl Amendment {
766 pub fn validate(&self) -> Result<(), AmendmentError> {
770 let plain = match self.agora_governance_amendment {
771 1 => true,
772 AMENDMENT_VERSION => false,
773 other => return Err(AmendmentError::UnsupportedVersion(other)),
774 };
775 let texts =
776 [Some(&self.basis), Some(&self.note), self.rationale.as_ref()];
777 if texts.into_iter().flatten().any(|t| t.is_plain() != plain) {
778 return Err(AmendmentError::TextShape);
779 }
780 match (self.kind, &self.redaction) {
781 (AmendmentKind::Redaction, None) => {
782 Err(AmendmentError::MissingRedaction)
783 }
784 (k, Some(_)) if k != AmendmentKind::Redaction => {
785 Err(AmendmentError::UnexpectedRedaction)
786 }
787 _ => Ok(()),
788 }
789 }
790
791 pub fn text_status(
798 &self,
799 beside: Option<&AmendmentTexts>,
800 ) -> Result<Option<AmendmentTextStatus>, AmendmentError> {
801 let empty = AmendmentTexts::default();
802 let beside = beside.unwrap_or(&empty);
803 let (Some(basis), Some(note)) = (
804 self.basis.status(beside.basis.as_ref()),
805 self.note.status(beside.note.as_ref()),
806 ) else {
807 return if beside.is_empty() {
808 Ok(None)
809 } else {
810 Err(AmendmentError::UncommittedText)
811 };
812 };
813 let rationale = match (&self.rationale, &beside.rationale) {
814 (None, Some(_)) => return Err(AmendmentError::UncommittedText),
815 (None, None) => None,
816 (Some(text), beside) => text.status(beside.as_ref()),
817 };
818 for (name, status) in [
819 ("basis", Some(basis)),
820 ("note", Some(note)),
821 ("rationale", rationale),
822 ] {
823 if status == Some(TextStatus::Mismatch) {
824 return Err(AmendmentError::TextMismatch(name));
825 }
826 }
827 Ok(Some(AmendmentTextStatus {
828 basis,
829 note,
830 rationale,
831 }))
832 }
833}
834
835pub fn kind_standing(kind: AmendmentKind) -> Option<Standing> {
838 match kind {
839 AmendmentKind::NonPrecedential => Some(Standing::NonPrecedential),
840 AmendmentKind::Overruled => Some(Standing::Overruled),
841 AmendmentKind::Superseded => Some(Standing::Superseded),
842 AmendmentKind::Reinstated => Some(Standing::InForce),
843 AmendmentKind::Correction
844 | AmendmentKind::Redaction
845 | AmendmentKind::Reattested => None,
846 }
847}
848
849pub fn standing(kinds: impl IntoIterator<Item = AmendmentKind>) -> Standing {
852 kinds
853 .into_iter()
854 .filter_map(kind_standing)
855 .last()
856 .unwrap_or_default()
857}
858
859pub const BLIND_KEY: &str = "_blind";
862
863pub fn is_redactable(entry_type: GovernanceLogEntryType) -> bool {
867 !matches!(
868 entry_type,
869 GovernanceLogEntryType::Amendment | GovernanceLogEntryType::KeyRotation
870 )
871}
872
873#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
875pub enum BlindError {
876 #[error("a redactable entry's data must be a JSON object")]
877 NotAnObject,
878 #[error(
879 "data already has a {BLIND_KEY:?} key; the writer supplies it, not the caller"
880 )]
881 AlreadyBlinded,
882 #[error(
883 "{0:?} is a number that is not a 64-bit integer; governance data \
884 never contains one (put a fraction in a string)"
885 )]
886 NonIntegerNumber(String),
887}
888
889pub fn blind_data(
907 data: &serde_json::Value,
908 blind: Blind,
909) -> Result<serde_json::Value, BlindError> {
910 if let Some(pointer) = non_integer_number(data) {
911 return Err(BlindError::NonIntegerNumber(pointer));
912 }
913 let mut out = data.clone();
914 let object = out.as_object_mut().ok_or(BlindError::NotAnObject)?;
915 if object.contains_key(BLIND_KEY) {
916 return Err(BlindError::AlreadyBlinded);
917 }
918 object.insert(BLIND_KEY.to_string(), blind.to_hex().into());
919 Ok(out)
920}
921
922#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
924pub enum RedactError {
925 #[error("pointer {0:?} does not resolve in the entry's data")]
926 Unresolved(String),
927 #[error("the empty pointer would redact the whole entry")]
928 WholeEntry,
929 #[error("a redaction names at least one pointer")]
930 NoFields,
931 #[error("{0:?} is the entry's blind; every redaction replaces it already")]
932 BlindPointer(String),
933 #[error(
934 "{0:?} is a number that is not a 64-bit integer; governance data \
935 never contains one"
936 )]
937 NonIntegerNumber(String),
938}
939
940pub fn redaction_marker(amendment_id: &GovernanceLogId) -> String {
942 format!("[redacted by {amendment_id}]")
943}
944
945pub fn redact_data(
961 data: &serde_json::Value,
962 fields: &[String],
963 amendment_id: &GovernanceLogId,
964 blind: Blind,
965) -> Result<serde_json::Value, RedactError> {
966 if fields.is_empty() {
967 return Err(RedactError::NoFields);
968 }
969 if let Some(pointer) = non_integer_number(data) {
970 return Err(RedactError::NonIntegerNumber(pointer));
971 }
972 let blind_pointer = format!("/{BLIND_KEY}");
973 let marker = serde_json::Value::String(redaction_marker(amendment_id));
974 let mut out = data.clone();
975 for pointer in fields {
976 if pointer.is_empty() {
977 return Err(RedactError::WholeEntry);
978 }
979 if *pointer == blind_pointer {
980 return Err(RedactError::BlindPointer(pointer.clone()));
981 }
982 let slot = out
983 .pointer_mut(pointer)
984 .ok_or_else(|| RedactError::Unresolved(pointer.clone()))?;
985 *slot = marker.clone();
986 }
987 if let Some(object) = out.as_object_mut() {
990 object.insert(BLIND_KEY.to_string(), blind.to_hex().into());
991 }
992 Ok(out)
993}
994
995#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
997#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
998#[cfg_attr(feature = "schemars", schemars(inline))]
999pub struct AmendmentNotice {
1000 pub id: GovernanceLogId,
1001 pub kind: AmendmentKind,
1002 #[serde(default)]
1003 pub authority: Option<GovernanceLogId>,
1004 pub basis: String,
1005 pub note: String,
1006 #[serde(default, skip_serializing_if = "Option::is_none")]
1008 pub rationale: Option<String>,
1009 pub created_at: DateTime<Utc>,
1010}
1011
1012impl AmendmentNotice {
1013 pub fn new(
1016 id: GovernanceLogId,
1017 created_at: DateTime<Utc>,
1018 amendment: &Amendment,
1019 beside: Option<&AmendmentTexts>,
1020 ) -> Self {
1021 let beside = beside.cloned().unwrap_or_default();
1022 Self {
1023 id,
1024 kind: amendment.kind,
1025 authority: amendment.authority.clone(),
1026 basis: amendment.basis.resolve(beside.basis.as_ref()).into(),
1027 note: amendment.note.resolve(beside.note.as_ref()).into(),
1028 rationale: amendment
1029 .rationale
1030 .as_ref()
1031 .map(|r| r.resolve(beside.rationale.as_ref()).into()),
1032 created_at,
1033 }
1034 }
1035}
1036
1037pub const KEY_ROTATION_VERSION: u32 = 2;
1043
1044pub const PUBLISHED_KEYS: &[&str] =
1051 &["ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a"];
1052
1053#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1055#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1056#[cfg_attr(feature = "schemars", schemars(inline))]
1057#[serde(rename_all = "snake_case")]
1058pub enum RotationReason {
1059 Routine,
1061 Compromise,
1064}
1065
1066#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1068#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1069#[cfg_attr(feature = "schemars", schemars(inline))]
1070#[serde(deny_unknown_fields)]
1071pub struct TrustedHead {
1072 pub id: GovernanceLogId,
1073 pub chain_seq: u64,
1074 pub entry_hash: Sha256Hex,
1075}
1076
1077#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1079pub enum RotationError {
1080 #[error("agora_governance_key_rotation is {0}, not {KEY_ROTATION_VERSION}")]
1081 UnsupportedVersion(u32),
1082 #[error("new_key is not a valid Ed25519 public key")]
1083 BadNewKey,
1084 #[error(
1085 "the proof of possession does not verify for this rotation at this position"
1086 )]
1087 BadProof,
1088 #[error("a compromise certificate must name last_trusted")]
1089 MissingLastTrusted,
1090 #[error("certificate: {0}")]
1091 Certificate(#[from] CertificateError),
1092 #[error("outgoing_certificate: {0}")]
1093 OutgoingCertificate(CertificateError),
1094 #[error(
1095 "the chain's first rotation must carry the genesis key's \
1096 outgoing_certificate"
1097 )]
1098 MissingGenesisCertificate,
1099 #[error(
1100 "outgoing_certificate belongs on the chain's first rotation and \
1101 nowhere else"
1102 )]
1103 UnexpectedOutgoingCertificate,
1104 #[error("old_key is not the key that was in force")]
1105 WrongOldKey,
1106 #[error("last_trusted does not name an earlier entry of this chain")]
1107 UnknownLastTrusted,
1108 #[error("new_key has already held this chain; a key is never brought back")]
1109 ReusedKey,
1110 #[error("last_trusted names an entry an earlier compromise repudiated")]
1111 RepudiatedLastTrusted,
1112}
1113
1114#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1125#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1126#[cfg_attr(feature = "schemars", schemars(inline))]
1127#[serde(deny_unknown_fields)]
1128pub struct KeyRotation {
1129 pub agora_governance_key_rotation: u32,
1131 pub reason: RotationReason,
1132 pub old_key: PublicKeyHex,
1133 pub new_key: PublicKeyHex,
1134 pub proof: SignatureHex,
1138 pub proof_signed_at: i64,
1140 pub certificate: KeyCertificate,
1144 #[serde(default, skip_serializing_if = "Option::is_none")]
1148 pub outgoing_certificate: Option<KeyCertificate>,
1149}
1150
1151#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1158pub struct RotationStatement {
1159 pub agora_governance_key_rotation: u32,
1161 pub reason: RotationReason,
1162 pub old_key: PublicKeyHex,
1163 pub new_key: PublicKeyHex,
1164 pub prev_hash: Option<Sha256Hex>,
1165}
1166
1167impl RotationStatement {
1168 pub fn new(
1170 reason: RotationReason,
1171 old_key: PublicKeyHex,
1172 new_key: PublicKeyHex,
1173 prev_hash: Option<Sha256Hex>,
1174 ) -> Self {
1175 Self {
1176 agora_governance_key_rotation: KEY_ROTATION_VERSION,
1177 reason,
1178 old_key,
1179 new_key,
1180 prev_hash,
1181 }
1182 }
1183
1184 pub fn preimage(&self) -> Vec<u8> {
1186 serde_json::to_vec(self).expect("a RotationStatement always serializes")
1187 }
1188
1189 pub fn hash(&self) -> Sha256Hex {
1191 Sha256Hex(Sha256::digest(self.preimage()).into())
1192 }
1193}
1194
1195impl KeyRotation {
1196 pub fn routine(
1202 old_key: PublicKeyHex,
1203 new_signing_key: &SigningKey,
1204 prev_hash: Option<Sha256Hex>,
1205 now: DateTime<Utc>,
1206 certificate: KeyCertificate,
1207 ) -> Self {
1208 Self::build(
1209 RotationReason::Routine,
1210 old_key,
1211 new_signing_key,
1212 prev_hash,
1213 now,
1214 certificate,
1215 )
1216 }
1217
1218 pub fn compromise(
1226 old_key: PublicKeyHex,
1227 new_signing_key: &SigningKey,
1228 prev_hash: Option<Sha256Hex>,
1229 now: DateTime<Utc>,
1230 certificate: KeyCertificate,
1231 ) -> Self {
1232 Self::build(
1233 RotationReason::Compromise,
1234 old_key,
1235 new_signing_key,
1236 prev_hash,
1237 now,
1238 certificate,
1239 )
1240 }
1241
1242 fn build(
1243 reason: RotationReason,
1244 old_key: PublicKeyHex,
1245 new_signing_key: &SigningKey,
1246 prev_hash: Option<Sha256Hex>,
1247 now: DateTime<Utc>,
1248 certificate: KeyCertificate,
1249 ) -> Self {
1250 let new_key = PublicKeyHex::from(&new_signing_key.verifying_key());
1251 let proof_signed_at = truncate_to_seconds(now).timestamp();
1252 let statement =
1253 RotationStatement::new(reason, old_key, new_key, prev_hash);
1254 let proof = crypto::sign(
1255 new_signing_key,
1256 statement.hash().as_bytes(),
1257 proof_signed_at,
1258 );
1259 Self {
1260 agora_governance_key_rotation: KEY_ROTATION_VERSION,
1261 reason,
1262 old_key,
1263 new_key,
1264 proof: proof.into(),
1265 proof_signed_at,
1266 certificate,
1267 outgoing_certificate: None,
1268 }
1269 }
1270
1271 pub fn with_outgoing(mut self, certificate: KeyCertificate) -> Self {
1273 self.outgoing_certificate = Some(certificate);
1274 self
1275 }
1276
1277 pub fn statement(&self, prev_hash: Option<Sha256Hex>) -> RotationStatement {
1279 RotationStatement::new(
1280 self.reason,
1281 self.old_key,
1282 self.new_key,
1283 prev_hash,
1284 )
1285 }
1286
1287 pub fn last_trusted(&self) -> Option<&TrustedHead> {
1290 self.certificate.statement.last_trusted.as_ref()
1291 }
1292
1293 pub fn expected_statement(
1299 &self,
1300 seq: u64,
1301 prev_hash: Option<Sha256Hex>,
1302 ) -> Result<KeyCertStatement, RotationError> {
1303 match self.reason {
1304 RotationReason::Routine => {
1305 Ok(KeyCertStatement::routine(self.new_key, seq, prev_hash))
1306 }
1307 RotationReason::Compromise => Ok(KeyCertStatement::compromise(
1308 self.new_key,
1309 seq,
1310 prev_hash,
1311 self.last_trusted()
1312 .cloned()
1313 .ok_or(RotationError::MissingLastTrusted)?,
1314 )),
1315 }
1316 }
1317
1318 pub fn verify_proof(
1321 &self,
1322 prev_hash: Option<Sha256Hex>,
1323 ) -> Result<(), RotationError> {
1324 if self.agora_governance_key_rotation != KEY_ROTATION_VERSION {
1325 return Err(RotationError::UnsupportedVersion(
1326 self.agora_governance_key_rotation,
1327 ));
1328 }
1329 let new_key = self
1330 .new_key
1331 .to_verifying_key()
1332 .map_err(|_| RotationError::BadNewKey)?;
1333 crypto::verify(
1334 &new_key,
1335 self.statement(prev_hash).hash().as_bytes(),
1336 self.proof_signed_at,
1337 &Signature::from(&self.proof),
1338 )
1339 .then_some(())
1340 .ok_or(RotationError::BadProof)
1341 }
1342
1343 pub fn verify_certified(
1347 &self,
1348 seq: u64,
1349 prev_hash: Option<Sha256Hex>,
1350 roots: &RootSet,
1351 ) -> Result<(), RotationError> {
1352 self.verify_proof(prev_hash)?;
1353 let expected = self.expected_statement(seq, prev_hash)?;
1354 Ok(self.certificate.verify_for(&expected, roots)?)
1355 }
1356}
1357
1358#[derive(Debug, Clone, Default, PartialEq, Eq)]
1365pub struct KeyAnchor {
1366 keys: HashSet<PublicKeyHex>,
1367}
1368
1369impl KeyAnchor {
1370 pub fn published() -> Self {
1372 PUBLISHED_KEYS
1373 .iter()
1374 .map(|k| {
1375 k.parse()
1376 .expect("PUBLISHED_KEYS are valid 32-byte hex keys")
1377 })
1378 .collect()
1379 }
1380
1381 pub fn pinned(key: PublicKeyHex) -> Self {
1383 std::iter::once(key).collect()
1384 }
1385
1386 pub fn with(mut self, key: PublicKeyHex) -> Self {
1388 self.keys.insert(key);
1389 self
1390 }
1391
1392 pub fn contains(&self, key: &PublicKeyHex) -> bool {
1393 self.keys.contains(key)
1394 }
1395
1396 pub fn is_empty(&self) -> bool {
1397 self.keys.is_empty()
1398 }
1399
1400 pub fn keys(&self) -> impl Iterator<Item = &PublicKeyHex> {
1402 self.keys.iter()
1403 }
1404}
1405
1406impl FromIterator<PublicKeyHex> for KeyAnchor {
1407 fn from_iter<I: IntoIterator<Item = PublicKeyHex>>(iter: I) -> Self {
1408 Self {
1409 keys: iter.into_iter().collect(),
1410 }
1411 }
1412}
1413
1414#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1417#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1418#[cfg_attr(feature = "schemars", schemars(inline))]
1419pub struct GovernanceKeyRecord {
1420 pub public_key: PublicKeyHex,
1421 pub from_seq: u64,
1423 #[serde(default)]
1427 pub through_seq: Option<u64>,
1428 pub status: KeyStatus,
1429 #[serde(default)]
1432 pub introduced_by: Option<GovernanceLogId>,
1433 #[serde(default)]
1435 pub retired_by: Option<GovernanceLogId>,
1436 #[serde(default)]
1440 pub certified: bool,
1441}
1442
1443#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1448#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1449#[cfg_attr(feature = "schemars", schemars(inline))]
1450pub struct GovernanceSigningKeys {
1451 pub keys: Vec<GovernanceKeyRecord>,
1453}
1454
1455#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1457#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1458#[cfg_attr(feature = "schemars", schemars(inline))]
1459pub struct EntryVerdict {
1460 pub id: GovernanceLogId,
1461 pub chain_seq: u64,
1462 pub signature_valid: bool,
1465 pub link_valid: bool,
1468 #[serde(default)]
1474 pub content_matches: Option<bool>,
1475 pub retroactive: bool,
1477 pub out_of_order: bool,
1480 #[serde(default)]
1482 pub amended_by: Vec<GovernanceLogId>,
1483 #[serde(default, skip_serializing_if = "Option::is_none")]
1486 pub signed_by: Option<PublicKeyHex>,
1487 #[serde(default)]
1490 pub redacted: bool,
1491 #[serde(default, skip_serializing_if = "Option::is_none")]
1493 pub redacted_data_hash: Option<Sha256Hex>,
1494 #[serde(default)]
1497 pub repudiated: bool,
1498 #[serde(default)]
1501 pub reattested_by: Vec<GovernanceLogId>,
1502 #[serde(default, skip_serializing_if = "Option::is_none")]
1506 pub texts: Option<AmendmentTextStatus>,
1507 #[serde(default, skip_serializing_if = "Option::is_none")]
1509 pub problem: Option<String>,
1510}
1511
1512#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1514#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1515#[cfg_attr(feature = "schemars", schemars(inline))]
1516pub struct GovernanceVerification {
1517 pub public_key: PublicKeyHex,
1519 pub ok: bool,
1525 #[serde(default)]
1527 pub head: Option<GovernanceLogId>,
1528 pub entries: Vec<EntryVerdict>,
1530 #[serde(default)]
1532 pub keys: Vec<GovernanceKeyRecord>,
1533 #[serde(default)]
1538 pub unanchored_keys: Vec<PublicKeyHex>,
1539 #[serde(default)]
1541 pub repudiated: Vec<GovernanceLogId>,
1542}
1543
1544impl GovernanceVerification {
1545 pub fn settle(mut self) -> Self {
1547 self.ok = self.entries.iter().all(|e| {
1548 e.signature_valid
1549 && e.link_valid
1550 && e.content_matches != Some(false)
1551 && e.problem.is_none()
1552 });
1553 self
1554 }
1555
1556 pub fn check_content(
1566 &mut self,
1567 link: &GovernanceChainLink,
1568 data: &serde_json::Value,
1569 ) -> bool {
1570 let Some(entry) = self.entries.iter_mut().find(|e| e.id == link.id)
1571 else {
1572 return false;
1573 };
1574 let hash = non_integer_number(data).is_none().then(|| data_hash(data));
1576 let ok = hash.is_some_and(|hash| {
1577 hash == link.attestation.data_hash
1578 || entry.redacted_data_hash == Some(hash)
1579 });
1580 entry.content_matches = Some(ok);
1581 ok
1582 }
1583}
1584
1585pub fn attest(
1596 key: &SigningKey,
1597 envelope: &Envelope,
1598 chain_seq: u64,
1599 signed_at: DateTime<Utc>,
1600) -> GovernanceAttestation {
1601 let signed_at = truncate_to_seconds(signed_at);
1602 let entry_hash = envelope.entry_hash();
1603 let signature =
1604 crypto::sign(key, entry_hash.as_bytes(), signed_at.timestamp());
1605 GovernanceAttestation {
1606 envelope_version: envelope.agora_governance_log,
1607 chain_seq,
1608 prev_hash: envelope.prev_hash,
1609 data_hash: envelope.data_hash,
1610 entry_hash,
1611 signature: signature.into(),
1612 signed_at,
1613 retroactive: is_retroactive(envelope.created_at(), signed_at),
1614 }
1615}
1616
1617#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1623pub enum LinkError {
1624 #[error(
1625 "envelope version {0} is not supported (this verifier knows {ENVELOPE_VERSION})"
1626 )]
1627 UnsupportedVersion(u32),
1628 #[error("entry_hash does not recompute from the envelope fields")]
1629 HashMismatch,
1630 #[error("signature does not verify under the published key")]
1631 BadSignature,
1632}
1633
1634pub fn recompute_entry_hash(link: &GovernanceChainLink) -> Sha256Hex {
1636 Envelope::new(
1637 link.id.clone(),
1638 link.entry_type,
1639 link.created_at,
1640 link.attestation.prev_hash,
1641 link.attestation.data_hash,
1642 )
1643 .entry_hash()
1644}
1645
1646pub fn verify_link(
1648 link: &GovernanceChainLink,
1649 key: &VerifyingKey,
1650) -> Result<(), LinkError> {
1651 let a = &link.attestation;
1652 if a.envelope_version != ENVELOPE_VERSION {
1653 return Err(LinkError::UnsupportedVersion(a.envelope_version));
1654 }
1655 if recompute_entry_hash(link) != a.entry_hash {
1656 return Err(LinkError::HashMismatch);
1657 }
1658 if !crypto::verify(
1659 key,
1660 a.entry_hash.as_bytes(),
1661 a.signed_at.timestamp(),
1662 &Signature::from(&a.signature),
1663 ) {
1664 return Err(LinkError::BadSignature);
1665 }
1666 Ok(())
1667}
1668
1669pub fn verify_data(
1671 link: &GovernanceChainLink,
1672 data: &serde_json::Value,
1673) -> bool {
1674 data_hash(data) == link.attestation.data_hash
1675}
1676
1677fn same_shape(written: &serde_json::Value, read: &serde_json::Value) -> bool {
1687 use serde_json::Value::{Array, Null, Object};
1688 match (written, read) {
1689 (Object(w), Object(r)) => r.iter().all(|(k, r)| match w.get(k) {
1690 Some(w) => same_shape(w, r),
1691 None => r.is_null(),
1692 }),
1693 (Array(w), Array(r)) => {
1694 w.len() == r.len() && w.iter().zip(r).all(|(w, r)| same_shape(w, r))
1695 }
1696 (_, Object(_) | Array(_)) => false,
1697 (Object(_) | Array(_), Null) => false,
1698 _ => true,
1699 }
1700}
1701
1702fn read_strictly<T>(written: &serde_json::Value) -> Result<T, String>
1704where
1705 T: Serialize + serde::de::DeserializeOwned,
1706{
1707 let read: T =
1708 serde_json::from_value(written.clone()).map_err(|e| e.to_string())?;
1709 let again = serde_json::to_value(&read).map_err(|e| e.to_string())?;
1710 if same_shape(written, &again) {
1711 Ok(read)
1712 } else {
1713 Err("an array where an object belongs, or the reverse".into())
1714 }
1715}
1716
1717pub fn links_from_json(
1724 chain: &serde_json::Value,
1725) -> Result<Vec<GovernanceChainLink>, String> {
1726 chain
1727 .as_array()
1728 .ok_or("a chain is an array of links")?
1729 .iter()
1730 .map(read_strictly)
1731 .collect()
1732}
1733
1734fn read_as_written<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
1736where
1737 D: serde::Deserializer<'de>,
1738 T: Serialize + serde::de::DeserializeOwned,
1739{
1740 let written = serde_json::Value::deserialize(deserializer)?;
1741 if written.is_null() {
1742 return Ok(None);
1743 }
1744 read_strictly(&written)
1745 .map(Some)
1746 .map_err(serde::de::Error::custom)
1747}
1748
1749fn reserved_prefix(
1752 entry_type: GovernanceLogEntryType,
1753) -> Option<GovernanceLogPrefix> {
1754 match entry_type {
1755 GovernanceLogEntryType::Amendment => Some(GovernanceLogPrefix::Amd),
1756 GovernanceLogEntryType::KeyRotation => Some(GovernanceLogPrefix::Key),
1757 GovernanceLogEntryType::StewardRecord => Some(GovernanceLogPrefix::Rec),
1758 GovernanceLogEntryType::CouncilDecision
1759 | GovernanceLogEntryType::AppealsCourtDecision
1760 | GovernanceLogEntryType::EmergencyAction
1761 | GovernanceLogEntryType::PolicyChange
1762 | GovernanceLogEntryType::StewardVeto => None,
1763 }
1764}
1765
1766fn reserved_for(prefix: GovernanceLogPrefix) -> Option<GovernanceLogEntryType> {
1768 match prefix {
1769 GovernanceLogPrefix::Amd => Some(GovernanceLogEntryType::Amendment),
1770 GovernanceLogPrefix::Key => Some(GovernanceLogEntryType::KeyRotation),
1771 GovernanceLogPrefix::Rec => Some(GovernanceLogEntryType::StewardRecord),
1772 GovernanceLogPrefix::Gov | GovernanceLogPrefix::App => None,
1773 }
1774}
1775
1776fn prefix_problem(link: &GovernanceChainLink) -> Option<String> {
1778 let prefix = link.id.prefix();
1779 match (reserved_prefix(link.entry_type), reserved_for(prefix)) {
1780 (Some(want), _) if prefix != want => Some(format!(
1781 "the id of a {} entry must be in the {want}- series, not {}",
1782 link.entry_type, link.id
1783 )),
1784 (None, Some(owner)) => Some(format!(
1785 "{prefix}- ids are reserved for {owner} entries, but {} is a {}",
1786 link.id, link.entry_type
1787 )),
1788 _ => None,
1789 }
1790}
1791
1792fn amendment_target(
1794 amendment: &Amendment,
1795 seq: u64,
1796 seq_of: &HashMap<&str, u64>,
1797 links: &[&GovernanceChainLink],
1798) -> Result<u64, AmendmentError> {
1799 amendment.validate()?;
1800 let target = *seq_of.get(amendment.target.as_str()).ok_or_else(|| {
1801 AmendmentError::UnknownTarget(amendment.target.clone())
1802 })?;
1803 if target >= seq {
1804 return Err(AmendmentError::ForwardReference(amendment.target.clone()));
1805 }
1806 if links[target as usize - 1].attestation.entry_hash
1807 != amendment.target_entry_hash
1808 {
1809 return Err(AmendmentError::WrongTargetHash(amendment.target.clone()));
1810 }
1811 Ok(target)
1812}
1813
1814struct KeyWalk {
1816 history: Vec<(GovernanceKeyRecord, VerifyingKey)>,
1818 unanchored: Vec<PublicKeyHex>,
1819 seen: HashSet<PublicKeyHex>,
1823 repudiated: HashSet<u64>,
1825 genesis: PublicKeyHex,
1826 genesis_certified: bool,
1828}
1829
1830impl KeyWalk {
1831 fn new(genesis: &VerifyingKey, anchor: &KeyAnchor) -> Self {
1832 let public_key = PublicKeyHex::from(genesis);
1833 Self {
1834 genesis: public_key,
1835 genesis_certified: false,
1836 history: vec![(
1837 GovernanceKeyRecord {
1838 public_key,
1839 from_seq: 1,
1840 through_seq: None,
1841 status: KeyStatus::Active,
1842 introduced_by: None,
1843 retired_by: None,
1844 certified: false,
1845 },
1846 *genesis,
1847 )],
1848 unanchored: if anchor.contains(&public_key) {
1849 Vec::new()
1850 } else {
1851 vec![public_key]
1852 },
1853 seen: HashSet::from([public_key]),
1854 repudiated: HashSet::new(),
1855 }
1856 }
1857
1858 fn in_force(&self, seq: u64) -> (PublicKeyHex, VerifyingKey) {
1860 let (record, key) = self
1861 .history
1862 .iter()
1863 .rev()
1864 .find(|(r, _)| r.from_seq <= seq)
1865 .unwrap_or(&self.history[0]);
1866 (record.public_key, *key)
1867 }
1868
1869 fn active(&self) -> PublicKeyHex {
1871 self.history
1872 .last()
1873 .map(|(r, _)| r.public_key)
1874 .expect("the genesis key is always in the history")
1875 }
1876
1877 fn close(
1878 &mut self,
1879 through_seq: u64,
1880 status: KeyStatus,
1881 by: &GovernanceLogId,
1882 ) {
1883 if let Some((record, _)) = self.history.last_mut() {
1884 record.through_seq = Some(through_seq);
1885 record.status = status;
1886 record.retired_by = Some(by.clone());
1887 }
1888 }
1889
1890 fn open(
1891 &mut self,
1892 public_key: PublicKeyHex,
1893 key: VerifyingKey,
1894 from_seq: u64,
1895 by: &GovernanceLogId,
1896 ) {
1897 self.history.push((
1898 GovernanceKeyRecord {
1899 public_key,
1900 from_seq,
1901 through_seq: None,
1902 status: KeyStatus::Active,
1903 introduced_by: Some(by.clone()),
1904 retired_by: None,
1905 certified: true,
1906 },
1907 key,
1908 ));
1909 }
1910
1911 fn apply(
1913 &mut self,
1914 rotation: &KeyRotation,
1915 link: &GovernanceChainLink,
1916 seq: u64,
1917 links: &[&GovernanceChainLink],
1918 roots: &RootSet,
1919 ) -> Result<(), RotationError> {
1920 rotation.verify_certified(seq, link.attestation.prev_hash, roots)?;
1921 let new_key = rotation
1922 .new_key
1923 .to_verifying_key()
1924 .map_err(|_| RotationError::BadNewKey)?;
1925 if self.seen.contains(&rotation.new_key) {
1926 return Err(RotationError::ReusedKey);
1927 }
1928 match (&rotation.outgoing_certificate, self.genesis_certified) {
1933 (None, false) => {
1934 return Err(RotationError::MissingGenesisCertificate);
1935 }
1936 (Some(_), true) => {
1937 return Err(RotationError::UnexpectedOutgoingCertificate);
1938 }
1939 (Some(certificate), false) => certificate
1940 .verify_for(&KeyCertStatement::genesis(self.genesis), roots)
1941 .map_err(RotationError::OutgoingCertificate)?,
1942 (None, true) => {}
1943 }
1944 match rotation.reason {
1945 RotationReason::Routine => {
1946 if rotation.old_key != self.in_force(seq).0 {
1947 return Err(RotationError::WrongOldKey);
1948 }
1949 self.close(seq, KeyStatus::Retired, &link.id);
1950 self.open(rotation.new_key, new_key, seq + 1, &link.id);
1951 }
1952 RotationReason::Compromise => {
1953 let head = rotation
1954 .last_trusted()
1955 .ok_or(RotationError::MissingLastTrusted)?;
1956 let trusted_seq = head.chain_seq;
1957 let names_an_earlier_entry = trusted_seq >= 1
1958 && trusted_seq < seq
1959 && links[trusted_seq as usize - 1].id == head.id
1960 && links[trusted_seq as usize - 1].attestation.entry_hash
1961 == head.entry_hash;
1962 if !names_an_earlier_entry {
1963 return Err(RotationError::UnknownLastTrusted);
1964 }
1965 if self.repudiated.contains(&trusted_seq) {
1968 return Err(RotationError::RepudiatedLastTrusted);
1969 }
1970 if rotation.old_key != self.in_force(trusted_seq).0 {
1973 return Err(RotationError::WrongOldKey);
1974 }
1975 self.history.retain(|(r, _)| r.from_seq <= trusted_seq);
1976 self.close(trusted_seq, KeyStatus::Compromised, &link.id);
1977 self.open(rotation.new_key, new_key, seq, &link.id);
1978 self.repudiated.extend(trusted_seq + 1..seq);
1979 }
1980 }
1981 self.seen.insert(rotation.new_key);
1982 if !self.genesis_certified {
1983 self.genesis_certified = true;
1984 self.history[0].0.certified = true;
1985 self.unanchored.clear();
1986 }
1987 Ok(())
1988 }
1989}
1990
1991pub fn verify_chain(
2022 links: &[GovernanceChainLink],
2023 genesis_key: &VerifyingKey,
2024 anchor: &KeyAnchor,
2025 roots: &RootSet,
2026) -> GovernanceVerification {
2027 let mut links: Vec<&GovernanceChainLink> = links.iter().collect();
2028 links.sort_by_key(|l| l.attestation.chain_seq);
2029
2030 let mut seq_of: HashMap<&str, u64> = HashMap::new();
2031 let mut duplicates: HashSet<&str> = HashSet::new();
2032 for (i, link) in links.iter().enumerate() {
2033 if seq_of.insert(link.id.as_str(), i as u64 + 1).is_some() {
2034 duplicates.insert(link.id.as_str());
2035 }
2036 }
2037
2038 let mut walk = KeyWalk::new(genesis_key, anchor);
2039 let mut amendments: Vec<(u64, GovernanceLogId, Amendment, u64)> =
2041 Vec::new();
2042 let mut entries: Vec<EntryVerdict> = Vec::with_capacity(links.len());
2043 let mut prev: Option<&GovernanceChainLink> = None;
2044
2045 for (i, link) in links.iter().enumerate() {
2046 let a = &link.attestation;
2047 let expected_seq = i as u64 + 1;
2048 let mut problems: Vec<String> = Vec::new();
2049
2050 if let Some(problem) = prefix_problem(link) {
2051 problems.push(problem);
2052 }
2053 if duplicates.contains(link.id.as_str()) {
2054 problems.push(format!("{} appears more than once", link.id));
2055 }
2056
2057 let carries_meaning = matches!(
2061 link.entry_type,
2062 GovernanceLogEntryType::Amendment
2063 | GovernanceLogEntryType::KeyRotation
2064 );
2065 let mut amendment: Option<Amendment> = None;
2066 let mut rotation: Option<KeyRotation> = None;
2067 let mut content_matches: Option<bool> = None;
2068 match &link.data {
2069 Some(data) if non_integer_number(data).is_some() => {
2070 content_matches = Some(false);
2071 problems.push(format!(
2072 "`data` has a number that is not a 64-bit integer at {:?}; \
2073 governance data never contains one",
2074 non_integer_number(data).unwrap_or_default()
2075 ));
2076 }
2077 Some(data) => {
2078 let matched = data_hash(data) == a.data_hash;
2079 content_matches = Some(matched);
2080 if !matched {
2081 if carries_meaning {
2082 problems.push(
2083 "`data` does not hash to the attested data_hash"
2084 .into(),
2085 );
2086 }
2087 } else {
2088 match link.entry_type {
2089 GovernanceLogEntryType::Amendment => {
2090 match read_strictly(data) {
2091 Ok(v) => amendment = Some(v),
2092 Err(e) => problems.push(format!(
2093 "amendment `data` is malformed: {e}"
2094 )),
2095 }
2096 }
2097 GovernanceLogEntryType::KeyRotation => {
2098 match read_strictly(data) {
2099 Ok(v) => rotation = Some(v),
2100 Err(e) => problems.push(format!(
2101 "key_rotation `data` is malformed: {e}"
2102 )),
2103 }
2104 }
2105 _ => {}
2106 }
2107 }
2108 }
2109 None if carries_meaning => problems.push(format!(
2110 "a {} entry must carry its `data`",
2111 link.entry_type
2112 )),
2113 None => {}
2114 }
2115
2116 let declared = rotation
2120 .as_ref()
2121 .filter(|r| r.reason == RotationReason::Compromise)
2122 .map(|r| {
2123 if walk.seen.contains(&r.new_key) {
2124 return Err(RotationError::ReusedKey);
2125 }
2126 r.verify_certified(expected_seq, a.prev_hash, roots)?;
2127 r.new_key
2128 .to_verifying_key()
2129 .map_err(|_| RotationError::BadNewKey)
2130 });
2131 let (key_hex, key) = match &declared {
2132 Some(Ok(k)) => (PublicKeyHex::from(k), *k),
2133 _ => walk.in_force(expected_seq),
2134 };
2135 if let Some(Err(e)) = &declared {
2138 problems.push(e.to_string());
2139 }
2140
2141 let (hash_ok, signature_valid) = match verify_link(link, &key) {
2142 Ok(()) => (true, true),
2143 Err(LinkError::BadSignature) => {
2144 problems.push(LinkError::BadSignature.to_string());
2145 (true, false)
2146 }
2147 Err(e) => {
2148 problems.push(e.to_string());
2149 (false, false)
2150 }
2151 };
2152
2153 let mut link_valid = hash_ok;
2154 if a.chain_seq != expected_seq {
2155 link_valid = false;
2156 problems.push(format!(
2157 "chain_seq {} where {expected_seq} was expected",
2158 a.chain_seq
2159 ));
2160 }
2161 let expected_prev = prev.map(|p| p.attestation.entry_hash);
2162 if a.prev_hash != expected_prev {
2163 link_valid = false;
2164 problems.push(match (a.prev_hash, expected_prev) {
2165 (Some(_), None) => "first entry names a predecessor".into(),
2166 (None, Some(_)) => "prev_hash is null mid-chain".into(),
2167 _ => "prev_hash is not the previous entry's entry_hash".into(),
2168 });
2169 }
2170 let out_of_order = prev.is_some_and(|p| link.created_at < p.created_at);
2171
2172 let authentic = signature_valid && link_valid;
2176 if let Some(rotation) = rotation.as_ref().filter(|_| authentic)
2177 && let Err(e) =
2178 walk.apply(rotation, link, expected_seq, &links, roots)
2179 {
2180 let problem = e.to_string();
2181 if !problems.contains(&problem) {
2182 problems.push(problem);
2183 }
2184 }
2185 let mut texts = None;
2189 if let Some(amendment) = amendment
2190 .as_ref()
2191 .filter(|a| authentic && a.validate().is_ok())
2192 {
2193 match amendment.text_status(link.texts.as_ref()) {
2194 Ok(status) => texts = status,
2195 Err(e) => problems.push(e.to_string()),
2196 }
2197 } else if link.texts.as_ref().is_some_and(|t| !t.is_empty()) {
2198 problems.push(AmendmentError::UncommittedText.to_string());
2199 }
2200 if let Some(amendment) = amendment.filter(|_| authentic) {
2201 match amendment_target(&amendment, expected_seq, &seq_of, &links) {
2202 Ok(target) => amendments.push((
2203 expected_seq,
2204 link.id.clone(),
2205 amendment,
2206 target,
2207 )),
2208 Err(e) => problems.push(e.to_string()),
2209 }
2210 }
2211
2212 entries.push(EntryVerdict {
2213 id: link.id.clone(),
2214 chain_seq: a.chain_seq,
2215 signature_valid,
2216 link_valid,
2217 content_matches,
2218 retroactive: is_retroactive(link.created_at, a.signed_at),
2219 out_of_order,
2220 amended_by: Vec::new(),
2221 signed_by: Some(key_hex),
2222 redacted: false,
2223 redacted_data_hash: None,
2224 repudiated: false,
2225 reattested_by: Vec::new(),
2226 texts,
2227 problem: (!problems.is_empty()).then(|| problems.join("; ")),
2228 });
2229 prev = Some(link);
2230 }
2231
2232 let mut applied = vec![false; amendments.len()];
2236 loop {
2237 let mut changed = false;
2238 for (i, (seq, id, amendment, target)) in amendments.iter().enumerate() {
2239 if applied[i]
2240 || amendment.kind != AmendmentKind::Reattested
2241 || walk.repudiated.contains(seq)
2242 {
2243 continue;
2244 }
2245 applied[i] = true;
2246 entries[*target as usize - 1].reattested_by.push(id.clone());
2247 walk.repudiated.remove(target);
2248 changed = true;
2249 }
2250 if !changed {
2251 break;
2252 }
2253 }
2254
2255 for (seq, id, amendment, target) in &amendments {
2256 if walk.repudiated.contains(seq) {
2257 continue;
2258 }
2259 let entry = &mut entries[*target as usize - 1];
2260 entry.amended_by.push(id.clone());
2261 if let Some(redaction) = &amendment.redaction {
2262 entry.redacted = true;
2263 entry.redacted_data_hash = Some(redaction.resulting_data_hash);
2264 }
2265 }
2266
2267 for (i, link) in links.iter().enumerate() {
2269 if entries[i].content_matches == Some(false)
2270 && let (Some(data), Some(hash)) =
2271 (&link.data, entries[i].redacted_data_hash)
2272 && non_integer_number(data).is_none()
2273 && data_hash(data) == hash
2274 {
2275 entries[i].content_matches = Some(true);
2276 }
2277 }
2278
2279 let mut seen = HashSet::new();
2280 walk.unanchored.retain(|key| seen.insert(*key));
2281
2282 let mut repudiated = Vec::new();
2283 for (i, entry) in entries.iter_mut().enumerate() {
2284 if walk.repudiated.contains(&(i as u64 + 1)) {
2285 entry.repudiated = true;
2286 repudiated.push(entry.id.clone());
2287 }
2288 }
2289
2290 GovernanceVerification {
2291 public_key: walk.active(),
2292 ok: false,
2293 head: prev.map(|p| p.id.clone()),
2294 entries,
2295 keys: walk.history.into_iter().map(|(record, _)| record).collect(),
2296 unanchored_keys: walk.unanchored,
2297 repudiated,
2298 }
2299 .settle()
2300}
2301
2302#[cfg(test)]
2303mod tests {
2304 use super::*;
2305 use crate::crypto::generate_keypair;
2306 use serde_json::json;
2307
2308 pub(super) fn gov(n: u32) -> GovernanceLogId {
2309 format!("GOV-2026-{n:04}").parse().unwrap()
2310 }
2311
2312 pub(super) fn amd(n: u32) -> GovernanceLogId {
2313 format!("AMD-2026-{n:04}").parse().unwrap()
2314 }
2315
2316 pub(super) fn key_id(n: u32) -> GovernanceLogId {
2317 format!("KEY-2026-{n:04}").parse().unwrap()
2318 }
2319
2320 pub(super) fn rec(n: u32) -> GovernanceLogId {
2321 format!("REC-2026-{n:04}").parse().unwrap()
2322 }
2323
2324 pub(super) fn at(secs: i64) -> DateTime<Utc> {
2325 DateTime::from_timestamp(1_700_000_000 + secs, 123_456_789).unwrap()
2326 }
2327
2328 fn anchored(key: &VerifyingKey) -> KeyAnchor {
2330 KeyAnchor::pinned(key.into())
2331 }
2332
2333 pub(super) fn resalted(draft: &AmendmentDraft, seq: u64) -> AmendmentDraft {
2336 let fix = |field: &str, t: &Option<CommittedText>| {
2337 t.as_ref().map(|t| {
2338 let salt = Sha256::digest(format!("{seq}/{field}/{}", t.text));
2339 CommittedText::with_salt(
2340 TextSalt::from(<[u8; 32]>::from(salt)),
2341 t.text.clone(),
2342 )
2343 })
2344 };
2345 let texts = AmendmentTexts {
2346 basis: fix("basis", &draft.texts.basis),
2347 note: fix("note", &draft.texts.note),
2348 rationale: fix("rationale", &draft.texts.rationale),
2349 };
2350 let commit = |t: &Option<CommittedText>| {
2351 t.as_ref().map(|t| AmendmentText::Committed(t.commitment()))
2352 };
2353 AmendmentDraft {
2354 amendment: Amendment {
2355 basis: commit(&texts.basis).unwrap(),
2356 note: commit(&texts.note).unwrap(),
2357 rationale: commit(&texts.rationale),
2358 ..draft.amendment.clone()
2359 },
2360 texts,
2361 }
2362 }
2363
2364 pub(super) fn v1(draft: AmendmentDraft) -> Amendment {
2366 let plain =
2367 |t: Option<CommittedText>| t.map(|t| AmendmentText::Plain(t.text));
2368 Amendment {
2369 agora_governance_amendment: 1,
2370 basis: plain(draft.texts.basis).unwrap(),
2371 note: plain(draft.texts.note).unwrap(),
2372 rationale: plain(draft.texts.rationale),
2373 ..draft.amendment
2374 }
2375 }
2376
2377 pub(super) fn root(n: u8) -> SigningKey {
2380 SigningKey::from_bytes(&[0xA0 + n; 32])
2381 }
2382
2383 pub(super) fn roots() -> RootSet {
2385 RootSet::new(
2386 [1, 2].map(|n| PublicKeyHex::from(&root(n).verifying_key())),
2387 1,
2388 )
2389 }
2390
2391 pub(super) fn certify(
2393 signers: &[&SigningKey],
2394 statement: KeyCertStatement,
2395 ) -> KeyCertificate {
2396 use ed25519_dalek::Signer;
2397 let message = statement.signed_bytes();
2398 signers.iter().fold(
2399 KeyCertificate::unsigned(statement),
2400 |certificate, signer| {
2401 certificate.with(RootSignature {
2402 root_key: (&signer.verifying_key()).into(),
2403 signature: signer.sign(&message).into(),
2404 })
2405 },
2406 )
2407 }
2408
2409 fn for_new_at(c: &Chain, key: &VerifyingKey) -> KeyCertificate {
2411 certify(
2412 &[&root(1)],
2413 KeyCertStatement::routine(key.into(), c.next_seq(), c.prev_hash()),
2414 )
2415 }
2416
2417 pub(super) fn link(
2418 key: &SigningKey,
2419 n: u32,
2420 prev: Option<&GovernanceChainLink>,
2421 data: &serde_json::Value,
2422 signed_at: DateTime<Utc>,
2423 ) -> GovernanceChainLink {
2424 let created_at = truncate_to_micros(at(n as i64 * 10));
2425 let envelope = Envelope::new(
2426 gov(n),
2427 GovernanceLogEntryType::CouncilDecision,
2428 created_at,
2429 prev.map(|p| p.attestation.entry_hash),
2430 data_hash(data),
2431 );
2432 let attestation = attest(
2433 key,
2434 &envelope,
2435 prev.map_or(1, |p| p.attestation.chain_seq + 1),
2436 signed_at,
2437 );
2438 GovernanceChainLink {
2439 id: gov(n),
2440 entry_type: GovernanceLogEntryType::CouncilDecision,
2441 created_at,
2442 attestation,
2443 data: None,
2444 texts: None,
2445 }
2446 }
2447
2448 pub(super) fn chain(key: &SigningKey, n: u32) -> Vec<GovernanceChainLink> {
2449 let mut out: Vec<GovernanceChainLink> = Vec::new();
2450 for i in 1..=n {
2451 let data = json!({"title": format!("Decision {i}"), "outcome": "approved"});
2452 let l = link(key, i, out.last(), &data, at(i as i64 * 10 + 1));
2453 out.push(l);
2454 }
2455 out
2456 }
2457
2458 pub(super) struct Chain {
2462 pub(super) links: Vec<GovernanceChainLink>,
2463 gov: u32,
2464 amd: u32,
2465 key: u32,
2466 genesis: Option<PublicKeyHex>,
2468 }
2469
2470 impl Chain {
2471 pub(super) fn new() -> Self {
2472 Self {
2473 links: Vec::new(),
2474 gov: 0,
2475 amd: 0,
2476 key: 0,
2477 genesis: None,
2478 }
2479 }
2480
2481 pub(super) fn prev_hash(&self) -> Option<Sha256Hex> {
2482 self.links.last().map(|l| l.attestation.entry_hash)
2483 }
2484
2485 pub(super) fn hash_at(&self, seq: usize) -> Sha256Hex {
2487 self.links[seq - 1].attestation.entry_hash
2488 }
2489
2490 pub(super) fn next_seq(&self) -> u64 {
2492 self.links.len() as u64 + 1
2493 }
2494
2495 pub(super) fn head(&self, seq: usize) -> TrustedHead {
2497 TrustedHead {
2498 id: self.links[seq - 1].id.clone(),
2499 chain_seq: seq as u64,
2500 entry_hash: self.hash_at(seq),
2501 }
2502 }
2503
2504 fn outgoing(&self, rotation: KeyRotation) -> KeyRotation {
2506 match (self.key, self.genesis) {
2507 (0, Some(genesis)) => rotation.with_outgoing(certify(
2508 &[&root(1)],
2509 KeyCertStatement::genesis(genesis),
2510 )),
2511 _ => rotation,
2512 }
2513 }
2514
2515 pub(super) fn routine(
2518 &self,
2519 old: &VerifyingKey,
2520 new: &SigningKey,
2521 ) -> KeyRotation {
2522 let statement = KeyCertStatement::routine(
2523 (&new.verifying_key()).into(),
2524 self.next_seq(),
2525 self.prev_hash(),
2526 );
2527 self.outgoing(KeyRotation::routine(
2528 old.into(),
2529 new,
2530 self.prev_hash(),
2531 at(self.next_seq() as i64 * 10 + 5),
2532 certify(&[&root(1)], statement),
2533 ))
2534 }
2535
2536 pub(super) fn compromise(
2539 &self,
2540 old: &VerifyingKey,
2541 new: &SigningKey,
2542 trusted: usize,
2543 ) -> KeyRotation {
2544 let statement = KeyCertStatement::compromise(
2545 (&new.verifying_key()).into(),
2546 self.next_seq(),
2547 self.prev_hash(),
2548 self.head(trusted),
2549 );
2550 self.outgoing(KeyRotation::compromise(
2551 old.into(),
2552 new,
2553 self.prev_hash(),
2554 at(self.next_seq() as i64 * 10 + 5),
2555 certify(&[&root(1)], statement),
2556 ))
2557 }
2558
2559 pub(super) fn next_amd(&self) -> GovernanceLogId {
2561 amd(self.amd + 1)
2562 }
2563
2564 pub(super) fn push(
2565 &mut self,
2566 signer: &SigningKey,
2567 id: GovernanceLogId,
2568 entry_type: GovernanceLogEntryType,
2569 data: serde_json::Value,
2570 carry: bool,
2571 ) -> GovernanceLogId {
2572 self.genesis
2573 .get_or_insert_with(|| (&signer.verifying_key()).into());
2574 let n = self.links.len() as i64 + 1;
2575 let created_at = truncate_to_micros(at(n * 10));
2576 let envelope = Envelope::new(
2577 id.clone(),
2578 entry_type,
2579 created_at,
2580 self.prev_hash(),
2581 data_hash(&data),
2582 );
2583 let attestation =
2584 attest(signer, &envelope, n as u64, at(n * 10 + 1));
2585 self.links.push(GovernanceChainLink {
2586 id: id.clone(),
2587 entry_type,
2588 created_at,
2589 attestation,
2590 data: carry.then_some(data),
2591 texts: None,
2592 });
2593 id
2594 }
2595
2596 pub(super) fn entry(
2599 &mut self,
2600 signer: &SigningKey,
2601 data: serde_json::Value,
2602 ) -> GovernanceLogId {
2603 self.gov += 1;
2604 let id = gov(self.gov);
2605 self.push(
2606 signer,
2607 id,
2608 GovernanceLogEntryType::CouncilDecision,
2609 data,
2610 false,
2611 )
2612 }
2613
2614 pub(super) fn decision(
2615 &mut self,
2616 signer: &SigningKey,
2617 ) -> GovernanceLogId {
2618 let data = json!({"title": format!("Decision {}", self.gov + 1)});
2619 self.entry(signer, data)
2620 }
2621
2622 pub(super) fn amend(
2624 &mut self,
2625 signer: &SigningKey,
2626 draft: &AmendmentDraft,
2627 ) -> GovernanceLogId {
2628 let draft = resalted(draft, self.next_seq());
2629 let id = self.amend_v1(signer, &draft.amendment);
2630 let link = self.links.last_mut().unwrap();
2631 link.texts = Some(draft.texts);
2632 id
2633 }
2634
2635 pub(super) fn amend_v1(
2638 &mut self,
2639 signer: &SigningKey,
2640 amendment: &Amendment,
2641 ) -> GovernanceLogId {
2642 self.amd += 1;
2643 let id = amd(self.amd);
2644 self.push(
2645 signer,
2646 id,
2647 GovernanceLogEntryType::Amendment,
2648 serde_json::to_value(amendment).unwrap(),
2649 true,
2650 )
2651 }
2652
2653 pub(super) fn rotate(
2654 &mut self,
2655 signer: &SigningKey,
2656 rotation: &KeyRotation,
2657 ) -> GovernanceLogId {
2658 self.key += 1;
2659 let id = key_id(self.key);
2660 self.push(
2661 signer,
2662 id,
2663 GovernanceLogEntryType::KeyRotation,
2664 serde_json::to_value(rotation).unwrap(),
2665 true,
2666 )
2667 }
2668 }
2669
2670 #[test]
2673 fn canonical_json_sorts_keys_at_every_level() {
2674 let v = json!({"b": {"z": 1, "a": [{"y": 2, "x": 3}]}, "a": null});
2675 assert_eq!(
2676 canonical_json(&v),
2677 br#"{"a":null,"b":{"a":[{"x":3,"y":2}],"z":1}}"#
2678 );
2679 }
2680
2681 #[test]
2682 fn canonical_json_is_compact_and_escapes_like_serde() {
2683 let v = json!({"s": "tab\there \"q\" ünïcode \u{1F600}", "n": [1, -2, 3.5, true, false]});
2684 let bytes = canonical_json(&v);
2685 let text = std::str::from_utf8(&bytes).unwrap();
2686 assert_eq!(
2687 text,
2688 r#"{"n":[1,-2,3.5,true,false],"s":"tab\there \"q\" ünïcode 😀"}"#
2689 );
2690 }
2691
2692 #[test]
2693 fn canonical_json_ignores_insertion_order() {
2694 let mut a = serde_json::Map::new();
2695 a.insert("z".into(), json!(1));
2696 a.insert("a".into(), json!(2));
2697 let mut b = serde_json::Map::new();
2698 b.insert("a".into(), json!(2));
2699 b.insert("z".into(), json!(1));
2700 assert_eq!(
2701 canonical_json(&serde_json::Value::Object(a)),
2702 canonical_json(&serde_json::Value::Object(b))
2703 );
2704 }
2705
2706 #[test]
2707 fn canonical_json_empty_containers() {
2708 assert_eq!(canonical_json(&json!({})), b"{}");
2709 assert_eq!(canonical_json(&json!([])), b"[]");
2710 assert_eq!(
2711 canonical_json(&json!({"a": {}, "b": []})),
2712 br#"{"a":{},"b":[]}"#
2713 );
2714 }
2715
2716 #[test]
2719 fn preimage_is_declaration_ordered_json() {
2720 let e = Envelope::new(
2721 gov(1),
2722 GovernanceLogEntryType::AppealsCourtDecision,
2723 at(0),
2724 None,
2725 data_hash(&json!({})),
2726 );
2727 let text = String::from_utf8(e.preimage()).unwrap();
2728 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}");
2729 }
2730
2731 #[test]
2732 fn every_envelope_field_changes_the_hash() {
2733 let base = Envelope::new(
2734 gov(1),
2735 GovernanceLogEntryType::CouncilDecision,
2736 at(0),
2737 None,
2738 data_hash(&json!({"a":1})),
2739 );
2740 let h = base.entry_hash();
2741 let mut e = base.clone();
2742 e.id = gov(2);
2743 assert_ne!(e.entry_hash(), h);
2744 let mut e = base.clone();
2745 e.entry_type = GovernanceLogEntryType::PolicyChange;
2746 assert_ne!(e.entry_hash(), h);
2747 let mut e = base.clone();
2748 e.created_at += 1;
2749 assert_ne!(e.entry_hash(), h);
2750 let mut e = base.clone();
2751 e.prev_hash = Some(h);
2752 assert_ne!(e.entry_hash(), h);
2753 let mut e = base.clone();
2754 e.data_hash = data_hash(&json!({"a":2}));
2755 assert_ne!(e.entry_hash(), h);
2756 assert_eq!(base.entry_hash(), h, "and it is deterministic");
2757 }
2758
2759 #[test]
2760 fn truncation_matches_what_the_envelope_carries() {
2761 let t = at(0);
2762 assert_eq!(truncate_to_micros(t).timestamp_subsec_nanos(), 123_456_000);
2763 assert_eq!(truncate_to_seconds(t).timestamp_subsec_nanos(), 0);
2764 assert_eq!(
2765 Envelope::new(
2766 gov(1),
2767 GovernanceLogEntryType::CouncilDecision,
2768 t,
2769 None,
2770 data_hash(&json!(null))
2771 )
2772 .created_at,
2773 truncate_to_micros(t).timestamp_micros()
2774 );
2775 }
2776
2777 #[test]
2780 fn hex_newtypes_round_trip_and_reject_wrong_lengths() {
2781 let h = data_hash(&json!(1));
2782 let s = serde_json::to_string(&h).unwrap();
2783 assert_eq!(s.len(), 66);
2784 let back: Sha256Hex = serde_json::from_str(&s).unwrap();
2785 assert_eq!(back, h);
2786 assert!(serde_json::from_str::<Sha256Hex>("\"abcd\"").is_err());
2787 assert!("zz".repeat(32).parse::<Sha256Hex>().is_err());
2788 assert!(Sha256Hex::try_from(vec![0u8; 31]).is_err());
2789 assert_eq!(format!("{h:?}"), format!("Sha256Hex({h})"));
2790 }
2791
2792 #[test]
2795 fn attest_then_verify_link() {
2796 let (key, pk) = generate_keypair();
2797 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
2798 assert_eq!(verify_link(&l, &pk), Ok(()));
2799 assert!(verify_data(&l, &json!({"a": 1})));
2800 assert!(!verify_data(&l, &json!({"a": 2})));
2801 assert!(!l.attestation.retroactive);
2802 }
2803
2804 #[test]
2805 fn wrong_key_fails_signature_only() {
2806 let (key, _) = generate_keypair();
2807 let (_, other) = generate_keypair();
2808 let l = link(&key, 1, None, &json!({}), at(5));
2809 assert_eq!(verify_link(&l, &other), Err(LinkError::BadSignature));
2810 }
2811
2812 #[test]
2813 fn tampering_with_any_attested_field_is_detected() {
2814 let (key, pk) = generate_keypair();
2815 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
2816
2817 let mut t = l.clone();
2818 t.created_at += chrono::Duration::microseconds(1);
2819 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
2820
2821 let mut t = l.clone();
2822 t.entry_type = GovernanceLogEntryType::StewardVeto;
2823 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
2824
2825 let mut t = l.clone();
2826 t.attestation.data_hash = data_hash(&json!({"a": 2}));
2827 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
2828
2829 let mut t = l.clone();
2832 t.attestation.signed_at -= chrono::Duration::seconds(1);
2833 assert_eq!(verify_link(&t, &pk), Err(LinkError::BadSignature));
2834
2835 let mut t = l.clone();
2836 t.attestation.envelope_version = 2;
2837 assert_eq!(verify_link(&t, &pk), Err(LinkError::UnsupportedVersion(2)));
2838 }
2839
2840 #[test]
2841 fn a_good_chain_verifies_in_any_input_order() {
2842 let (key, pk) = generate_keypair();
2843 let mut c = chain(&key, 4);
2844 c.reverse();
2845 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
2846 assert!(v.ok, "{v:#?}");
2847 assert_eq!(v.head, Some(gov(4)));
2848 assert_eq!(
2849 v.entries.iter().map(|e| e.chain_seq).collect::<Vec<_>>(),
2850 [1, 2, 3, 4]
2851 );
2852 assert!(v.entries.iter().all(|e| e.content_matches.is_none()
2853 && e.problem.is_none()
2854 && !e.out_of_order));
2855 assert_eq!(v.public_key, PublicKeyHex::from(&pk));
2856 }
2857
2858 #[test]
2859 fn empty_chain_is_ok_with_no_head() {
2860 let (_, pk) = generate_keypair();
2861 let v = verify_chain(&[], &pk, &anchored(&pk), &roots());
2862 assert!(v.ok);
2863 assert!(v.head.is_none());
2864 assert!(v.entries.is_empty());
2865 }
2866
2867 #[test]
2868 fn a_changed_entry_breaks_its_signature_and_the_next_link() {
2869 let (key, pk) = generate_keypair();
2870 let mut c = chain(&key, 3);
2871 let rewritten = link(
2874 &key,
2875 2,
2876 Some(&c[0]),
2877 &json!({"title": "Decision 2", "outcome": "REJECTED"}),
2878 at(21),
2879 );
2880 c[1] = rewritten;
2881 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
2882 assert!(!v.ok);
2883 assert!(
2884 v.entries[1].signature_valid && v.entries[1].link_valid,
2885 "the rewrite itself is well-formed: {:#?}",
2886 v.entries[1]
2887 );
2888 assert!(
2889 !v.entries[2].link_valid,
2890 "but entry 3 no longer points at it: {:#?}",
2891 v.entries[2]
2892 );
2893 assert!(
2894 v.entries[2]
2895 .problem
2896 .as_deref()
2897 .unwrap()
2898 .contains("prev_hash")
2899 );
2900 }
2901
2902 #[test]
2903 fn a_removed_entry_is_a_gap_and_a_broken_link() {
2904 let (key, pk) = generate_keypair();
2905 let mut c = chain(&key, 3);
2906 c.remove(1);
2907 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
2908 assert!(!v.ok);
2909 assert!(v.entries[0].link_valid);
2910 let p = v.entries[1].problem.as_deref().unwrap();
2911 assert!(p.contains("chain_seq 3 where 2 was expected"), "{p}");
2912 assert!(p.contains("prev_hash"), "{p}");
2913 }
2914
2915 #[test]
2916 fn a_second_genesis_is_rejected() {
2917 let (key, pk) = generate_keypair();
2918 let mut c = chain(&key, 2);
2919 let rogue = link(&key, 2, None, &json!({}), at(21));
2920 c[1] = rogue;
2921 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
2922 assert!(!v.ok);
2923 assert!(
2924 v.entries[1]
2925 .problem
2926 .as_deref()
2927 .unwrap()
2928 .contains("null mid-chain")
2929 );
2930 }
2931
2932 #[test]
2933 fn retroactive_and_out_of_order_are_recomputed_not_copied() {
2934 let (key, pk) = generate_keypair();
2935 let first = link(&key, 1, None, &json!({}), at(10 + 3600));
2936 let created = truncate_to_micros(at(5));
2939 let envelope = Envelope::new(
2940 gov(2),
2941 GovernanceLogEntryType::CouncilDecision,
2942 created,
2943 Some(first.attestation.entry_hash),
2944 data_hash(&json!({})),
2945 );
2946 let attestation = attest(&key, &envelope, 2, at(6));
2947 let mut second = GovernanceChainLink {
2948 id: gov(2),
2949 entry_type: GovernanceLogEntryType::CouncilDecision,
2950 created_at: created,
2951 attestation,
2952 data: None,
2953 texts: None,
2954 };
2955 second.attestation.retroactive = true; let v = verify_chain(&[first, second], &pk, &anchored(&pk), &roots());
2957 assert!(v.ok, "{v:#?}");
2958 assert!(v.entries[0].retroactive);
2959 assert!(!v.entries[1].retroactive, "recomputed from timestamps");
2960 assert!(v.entries[1].out_of_order);
2961 }
2962
2963 #[test]
2964 fn content_mismatch_settles_to_not_ok() {
2965 let (key, pk) = generate_keypair();
2966 let c = chain(&key, 1);
2967 let mut v = verify_chain(&c, &pk, &anchored(&pk), &roots());
2968 v.entries[0].content_matches = Some(true);
2969 assert!(v.clone().settle().ok);
2970 v.entries[0].content_matches = Some(false);
2971 assert!(!v.settle().ok);
2972 }
2973
2974 #[test]
2977 fn an_amendment_fills_amended_by_on_its_target() {
2978 let (key, pk) = generate_keypair();
2979 let mut c = Chain::new();
2980 let target = c.decision(&key);
2981 c.decision(&key);
2982 let amendment = AmendmentDraft::new(
2983 target,
2984 c.hash_at(1),
2985 AmendmentKind::NonPrecedential,
2986 "§1 (Red Team Cases Recharacterized)",
2987 "diagnostic finding — not citable as moderation precedent",
2988 )
2989 .unwrap()
2990 .with_authority(gov(5))
2991 .with_rationale("§5 leaves the ruling itself standing");
2992 let id = c.amend(&key, &amendment);
2993
2994 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
2995 assert!(v.ok, "{v:#?}");
2996 assert_eq!(v.entries[0].amended_by, vec![id]);
2997 assert!(v.entries[1].amended_by.is_empty());
2998 assert!(!v.entries[0].redacted);
2999 assert_eq!(v.head, Some(amd(1)));
3000 assert_eq!(v.entries[2].content_matches, Some(true));
3002 assert_eq!(v.entries[0].content_matches, None);
3003 assert_eq!(
3004 standing([amendment.amendment.kind]),
3005 Standing::NonPrecedential
3006 );
3007 }
3008
3009 #[test]
3010 fn an_amendment_must_name_an_earlier_entry_by_its_exact_hash() {
3011 let (key, pk) = generate_keypair();
3012 let problem = |c: &Chain, at: usize| -> String {
3013 verify_chain(&c.links, &pk, &anchored(&pk), &roots()).entries[at]
3014 .problem
3015 .clone()
3016 .unwrap_or_default()
3017 };
3018
3019 let mut c = Chain::new();
3020 c.decision(&key);
3021 let unknown = AmendmentDraft::new(
3022 gov(99),
3023 c.hash_at(1),
3024 AmendmentKind::Overruled,
3025 "b",
3026 "n",
3027 )
3028 .unwrap();
3029 c.amend(&key, &unknown);
3030 assert!(
3031 problem(&c, 1).contains("is not an entry of this chain"),
3032 "{}",
3033 problem(&c, 1)
3034 );
3035 assert!(!verify_chain(&c.links, &pk, &anchored(&pk), &roots()).ok);
3036
3037 let mut c = Chain::new();
3039 c.decision(&key);
3040 let forward = AmendmentDraft::new(
3041 gov(2),
3042 c.hash_at(1),
3043 AmendmentKind::Overruled,
3044 "b",
3045 "n",
3046 )
3047 .unwrap();
3048 c.amend(&key, &forward);
3049 c.decision(&key);
3050 assert!(
3051 problem(&c, 1).contains("not an earlier entry"),
3052 "{}",
3053 problem(&c, 1)
3054 );
3055
3056 let mut c = Chain::new();
3058 let target = c.decision(&key);
3059 let wrong_hash = AmendmentDraft::new(
3060 target,
3061 data_hash(&json!("some other entry")),
3062 AmendmentKind::Overruled,
3063 "b",
3064 "n",
3065 )
3066 .unwrap();
3067 c.amend(&key, &wrong_hash);
3068 assert!(problem(&c, 1).contains("entry_hash"), "{}", problem(&c, 1));
3069 assert!(
3070 verify_chain(&c.links, &pk, &anchored(&pk), &roots()).entries[0]
3071 .amended_by
3072 .is_empty()
3073 );
3074 }
3075
3076 #[test]
3077 fn redaction_shape_violations_fail_verification() {
3078 let (key, pk) = generate_keypair();
3079 let amend_with = |mutate: &dyn Fn(&mut Amendment)| -> String {
3080 let mut c = Chain::new();
3081 let target = c.decision(&key);
3082 let mut amendment = AmendmentDraft::new(
3083 target,
3084 c.hash_at(1),
3085 AmendmentKind::Correction,
3086 "b",
3087 "n",
3088 )
3089 .unwrap();
3090 mutate(&mut amendment.amendment);
3091 c.amend_v1(&key, &amendment.amendment);
3092 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3093 assert!(!v.ok, "{v:#?}");
3094 v.entries[1].problem.clone().unwrap_or_default()
3095 };
3096
3097 assert_eq!(
3100 AmendmentDraft::new(
3101 gov(1),
3102 data_hash(&json!(null)),
3103 AmendmentKind::Redaction,
3104 "b",
3105 "n"
3106 ),
3107 Err(AmendmentError::MissingRedaction)
3108 );
3109 let p = amend_with(&|a| a.kind = AmendmentKind::Redaction);
3110 assert!(p.contains("requires a `redaction`"), "{p}");
3111
3112 let p = amend_with(&|a| {
3113 a.redaction = Some(Redaction {
3114 fields: vec!["/x".into()],
3115 resulting_data_hash: data_hash(&json!({})),
3116 })
3117 });
3118 assert!(p.contains("only valid on kind"), "{p}");
3119
3120 let p = amend_with(&|a| a.agora_governance_amendment = 3);
3121 assert!(p.contains("agora_governance_amendment is 3"), "{p}");
3122
3123 let p = amend_with(&|a| a.agora_governance_amendment = 1);
3126 assert!(p.contains("does neither consistently"), "{p}");
3127 let p = amend_with(&|a| a.note = AmendmentText::Plain("n".into()));
3128 assert!(p.contains("does neither consistently"), "{p}");
3129 }
3130
3131 #[test]
3132 fn governance_data_holds_no_number_that_is_not_a_64_bit_integer() {
3133 let fine = json!({"a": [1, -2, u64::MAX, i64::MIN], "b": {"c": "0.5"}});
3134 assert_eq!(non_integer_number(&fine), None);
3135 assert!(blind_data(&fine, Blind::random()).is_ok());
3136
3137 for (text, pointer) in [
3138 (r#"{"a": {"b/c": [1, 0.5]}}"#, "/a/b~1c/1"),
3139 (r#"{"n": 1.0}"#, "/n"),
3140 (r#"{"n": 1e3}"#, "/n"),
3141 (r#"{"n": 18446744073709551616}"#, "/n"),
3142 (r#"{"n": -9223372036854775809}"#, "/n"),
3143 ] {
3144 let data: serde_json::Value = serde_json::from_str(text).unwrap();
3145 assert_eq!(non_integer_number(&data).as_deref(), Some(pointer));
3146 assert_eq!(
3147 blind_data(&data, Blind::random()),
3148 Err(BlindError::NonIntegerNumber(pointer.into())),
3149 "the writer refuses it"
3150 );
3151 assert_eq!(
3152 redact_data(&data, &["/n".into()], &amd(1), Blind::random()),
3153 Err(RedactError::NonIntegerNumber(pointer.into()))
3154 );
3155 }
3156 }
3157
3158 #[test]
3159 fn standing_is_the_last_amendment_that_changes_it() {
3160 use AmendmentKind::*;
3161 assert_eq!(standing([]), Standing::InForce);
3162 assert_eq!(standing([Correction, Redaction]), Standing::InForce);
3163 assert_eq!(
3164 standing([Correction, NonPrecedential, Redaction]),
3165 Standing::NonPrecedential
3166 );
3167 assert_eq!(standing([Overruled, Reinstated]), Standing::InForce);
3168 assert_eq!(standing([Reinstated, Superseded]), Standing::Superseded);
3169 assert_eq!(kind_standing(Reattested), None);
3170 assert_eq!(kind_standing(Reinstated), Some(Standing::InForce));
3171 }
3172
3173 #[test]
3174 fn a_redaction_verifies_against_the_amendment_and_nothing_else() {
3175 let (key, pk) = generate_keypair();
3176 let data = json!({
3177 "finding": "upheld",
3178 "subject": {"handle": "someone", "detail": "personal"},
3179 });
3180 let mut c = Chain::new();
3181 let target = c.entry(&key, data.clone());
3182 let amendment_id = c.next_amd();
3183 let (amendment, redacted) = AmendmentDraft::redaction(
3184 &amendment_id,
3185 target,
3186 c.hash_at(1),
3187 "GDPR Art. 17(1)(a)",
3188 "personal data removed on request",
3189 vec!["/subject/handle".into(), "/subject/detail".into()],
3190 &data,
3191 Blind::from([7; 32]),
3192 )
3193 .unwrap();
3194 assert_eq!(c.amend(&key, &amendment), amendment_id);
3195 assert_eq!(redacted[BLIND_KEY], json!(Blind::from([7; 32])));
3196
3197 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3198 assert!(v.ok, "{v:#?}");
3199 assert!(v.entries[0].redacted);
3200 assert_eq!(v.entries[0].redacted_data_hash, Some(data_hash(&redacted)));
3201 assert_eq!(
3202 redacted["subject"]["handle"],
3203 json!(format!("[redacted by {amendment_id}]"))
3204 );
3205 assert_eq!(redacted["finding"], json!("upheld"), "and nothing else");
3206
3207 assert!(v.check_content(&c.links[0], &redacted));
3209 assert_eq!(v.entries[0].content_matches, Some(true));
3210 assert!(v.clone().settle().ok);
3211 assert!(v.check_content(&c.links[0], &data));
3213
3214 let mut tampered = redacted.clone();
3216 tampered["finding"] = json!("overturned");
3217 assert!(!v.check_content(&c.links[0], &tampered));
3218 assert_eq!(v.entries[0].content_matches, Some(false));
3219 assert!(!v.settle().ok);
3220 }
3221
3222 #[test]
3223 fn content_that_matches_nothing_fails_without_an_amendment() {
3224 let (key, pk) = generate_keypair();
3225 let mut c = Chain::new();
3226 c.entry(&key, json!({"a": 1}));
3227 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3228 assert!(!v.check_content(&c.links[0], &json!({"a": 2})));
3229 assert!(!v.clone().settle().ok);
3230 let other = link(&key, 9, None, &json!({}), at(9));
3232 assert!(!v.check_content(&other, &json!({})));
3233 }
3234
3235 #[test]
3236 fn tampering_with_an_amendments_data_is_caught_by_the_data_hash() {
3237 let (key, pk) = generate_keypair();
3238 let mut c = Chain::new();
3239 let target = c.decision(&key);
3240 let amendment = AmendmentDraft::new(
3241 target,
3242 c.hash_at(1),
3243 AmendmentKind::Overruled,
3244 "b",
3245 "overruled by a later decision",
3246 )
3247 .unwrap();
3248 c.amend(&key, &amendment);
3249 c.links[1].data.as_mut().unwrap()["note"] =
3250 json!("reinstated, actually");
3251
3252 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3253 assert!(!v.ok, "{v:#?}");
3254 let p = v.entries[1].problem.as_deref().unwrap();
3255 assert!(p.contains("does not hash to the attested data_hash"), "{p}");
3256 assert!(
3257 v.entries[0].amended_by.is_empty(),
3258 "an unreadable amendment has no effect"
3259 );
3260 assert!(
3261 v.entries[1].signature_valid && v.entries[1].link_valid,
3262 "the envelope is untouched — only the content is not what it \
3263 committed to"
3264 );
3265 }
3266
3267 #[test]
3268 fn an_amendment_or_rotation_must_carry_its_data() {
3269 let (key, pk) = generate_keypair();
3270 let mut c = Chain::new();
3271 let target = c.decision(&key);
3272 let amendment = AmendmentDraft::new(
3273 target,
3274 c.hash_at(1),
3275 AmendmentKind::Correction,
3276 "b",
3277 "n",
3278 )
3279 .unwrap();
3280 c.amend(&key, &amendment);
3281 let rotation = c.routine(&pk, &generate_keypair().0);
3282 c.rotate(&key, &rotation);
3283 c.links[1].data = None;
3284 c.links[2].data = None;
3285
3286 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3287 assert!(!v.ok, "{v:#?}");
3288 for (i, entry_type) in [(1, "amendment"), (2, "key_rotation")] {
3289 let p = v.entries[i].problem.as_deref().unwrap();
3290 assert!(p.contains(&format!("a {entry_type} entry")), "{p}");
3291 assert!(p.contains("must carry its `data`"), "{p}");
3292 }
3293 }
3294
3295 #[test]
3296 fn the_id_series_must_match_the_entry_type() {
3297 let (key, pk) = generate_keypair();
3298 let mut c = Chain::new();
3299 let target = c.decision(&key);
3300 let amendment = AmendmentDraft::new(
3301 target,
3302 c.hash_at(1),
3303 AmendmentKind::Correction,
3304 "b",
3305 "n",
3306 )
3307 .unwrap();
3308 c.push(
3309 &key,
3310 gov(7),
3311 GovernanceLogEntryType::Amendment,
3312 serde_json::to_value(&amendment.amendment).unwrap(),
3313 true,
3314 );
3315 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3316 assert!(!v.ok, "{v:#?}");
3317 let p = v.entries[1].problem.as_deref().unwrap();
3318 assert!(p.contains("must be in the AMD- series"), "{p}");
3319
3320 let mut c = Chain::new();
3321 c.push(
3322 &key,
3323 key_id(1),
3324 GovernanceLogEntryType::CouncilDecision,
3325 json!({}),
3326 false,
3327 );
3328 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3329 let p = v.entries[0].problem.as_deref().unwrap();
3330 assert!(p.contains("reserved"), "{p}");
3331 }
3332
3333 #[test]
3334 fn redact_data_replaces_whole_values_and_refuses_the_rest() {
3335 let id = amd(3);
3336 let blind = Blind::from([9; 32]);
3337 let data = json!({"a": {"b": [1, {"c": "secret"}]}, "d/e": "slash"});
3338 let out = redact_data(
3339 &data,
3340 &["/a/b/1/c".into(), "/d~1e".into()],
3341 &id,
3342 blind,
3343 )
3344 .unwrap();
3345 assert_eq!(out["a"]["b"][1]["c"], json!(redaction_marker(&id)));
3346 assert_eq!(out["d/e"], json!(redaction_marker(&id)));
3347 assert_eq!(out["a"]["b"][0], json!(1), "untouched");
3348 assert_eq!(out[BLIND_KEY], json!(blind), "a legacy entry gains one");
3349
3350 assert_eq!(
3351 redact_data(&data, &["/a/nope".into()], &id, blind),
3352 Err(RedactError::Unresolved("/a/nope".into()))
3353 );
3354 assert_eq!(
3355 redact_data(&data, &["".into()], &id, blind),
3356 Err(RedactError::WholeEntry)
3357 );
3358 assert_eq!(
3359 redact_data(&data, &[], &id, blind),
3360 Err(RedactError::NoFields)
3361 );
3362 assert_eq!(
3363 redact_data(&data, &["/_blind".into()], &id, blind),
3364 Err(RedactError::BlindPointer("/_blind".into()))
3365 );
3366 }
3367
3368 #[test]
3369 fn blind_data_is_for_objects_and_is_the_writers_to_supply() {
3370 let blind = Blind::from([1; 32]);
3371 let out = blind_data(&json!({"finding": "upheld"}), blind).unwrap();
3372 assert_eq!(out, json!({"finding": "upheld", "_blind": blind}));
3373 assert_eq!(
3374 blind_data(&json!([1]), blind),
3375 Err(BlindError::NotAnObject)
3376 );
3377 assert_eq!(blind_data(&out, blind), Err(BlindError::AlreadyBlinded));
3378 assert_ne!(Blind::random(), Blind::random());
3379
3380 use GovernanceLogEntryType::*;
3381 assert!(!is_redactable(Amendment) && !is_redactable(KeyRotation));
3382 assert!(
3383 is_redactable(CouncilDecision) && is_redactable(EmergencyAction)
3384 );
3385 }
3386
3387 #[test]
3391 fn a_removed_value_cannot_be_confirmed_by_guessing_it() {
3392 fn confirms(
3394 public: &serde_json::Value,
3395 pointer: &str,
3396 guess: &str,
3397 hash: Sha256Hex,
3398 ) -> bool {
3399 let mut attempt = public.clone();
3400 *attempt.pointer_mut(pointer).unwrap() = json!(guess);
3401 data_hash(&attempt) == hash
3402 }
3403 let legacy = json!({"finding": "upheld", "handle": "someone", "city": "Utrecht"});
3404
3405 let written = blind_data(&legacy, Blind::random()).unwrap();
3407 let first = redact_data(
3408 &written,
3409 &["/handle".into()],
3410 &amd(1),
3411 Blind::random(),
3412 )
3413 .unwrap();
3414 assert!(!confirms(&first, "/handle", "someone", data_hash(&written)));
3415
3416 let second =
3419 redact_data(&first, &["/city".into()], &amd(2), Blind::random())
3420 .unwrap();
3421 assert!(!confirms(&second, "/city", "Utrecht", data_hash(&first)));
3422
3423 let mut stripped =
3427 redact_data(&legacy, &["/handle".into()], &amd(3), Blind::random())
3428 .unwrap();
3429 let legacy_first = stripped.clone();
3430 stripped.as_object_mut().unwrap().remove(BLIND_KEY);
3431 assert!(confirms(
3432 &stripped,
3433 "/handle",
3434 "someone",
3435 data_hash(&legacy)
3436 ));
3437 let legacy_second = redact_data(
3439 &legacy_first,
3440 &["/city".into()],
3441 &amd(4),
3442 Blind::random(),
3443 )
3444 .unwrap();
3445 assert!(!confirms(
3446 &legacy_second,
3447 "/city",
3448 "Utrecht",
3449 data_hash(&legacy_first)
3450 ));
3451 }
3452
3453 #[test]
3456 fn a_routine_rotation_moves_the_chain_to_the_new_key() {
3457 let (old, old_pk) = generate_keypair();
3458 let (new, new_pk) = generate_keypair();
3459 let mut c = Chain::new();
3460 c.decision(&old);
3461 let rotation = c.routine(&old_pk, &new);
3462 let rotation_id = c.rotate(&old, &rotation);
3463 c.decision(&new);
3464
3465 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3468 assert!(v.ok, "{v:#?}");
3469 assert_eq!(v.public_key, (&new_pk).into());
3470 assert_eq!(
3471 v.entries[1].signed_by,
3472 Some((&old_pk).into()),
3473 "the rotation itself is signed by the old key"
3474 );
3475 assert_eq!(v.entries[2].signed_by, Some((&new_pk).into()));
3476 assert!(v.unanchored_keys.is_empty());
3477 assert!(v.repudiated.is_empty());
3478 assert_eq!(v.keys.len(), 2);
3479 assert_eq!(v.keys[0].public_key, (&old_pk).into());
3480 assert_eq!(v.keys[0].from_seq, 1);
3481 assert_eq!(v.keys[0].through_seq, Some(2));
3482 assert_eq!(v.keys[0].status, KeyStatus::Retired);
3483 assert_eq!(v.keys[0].introduced_by, None);
3484 assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
3485 assert!(v.keys[0].certified, "retroactively, by the rotation");
3486 assert_eq!(v.keys[1].from_seq, 3);
3487 assert_eq!(v.keys[1].through_seq, None);
3488 assert_eq!(v.keys[1].status, KeyStatus::Active);
3489 assert_eq!(v.keys[1].introduced_by.as_ref(), Some(&rotation_id));
3490 assert!(v.keys[1].certified);
3491 }
3492
3493 #[test]
3494 fn the_old_key_cannot_sign_after_a_routine_rotation() {
3495 let (old, old_pk) = generate_keypair();
3496 let (new, _) = generate_keypair();
3497 let mut c = Chain::new();
3498 c.decision(&old);
3499 let rotation = c.routine(&old_pk, &new);
3500 c.rotate(&old, &rotation);
3501 c.decision(&old);
3502
3503 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3504 assert!(!v.ok, "{v:#?}");
3505 assert!(!v.entries[2].signature_valid);
3506 assert!(
3507 v.entries[2].link_valid,
3508 "the linkage is fine; the key is not"
3509 );
3510 }
3511
3512 #[test]
3515 fn the_online_key_cannot_certify_its_own_successor() {
3516 let (old, old_pk) = generate_keypair();
3517 let (thief, _) = generate_keypair();
3518 let mut c = Chain::new();
3519 c.decision(&old);
3520 let mut rotation = c.routine(&old_pk, &thief);
3521 let statement = rotation.certificate.statement.clone();
3523 rotation.certificate = certify(&[&old], statement);
3524 c.rotate(&old, &rotation);
3525 c.decision(&thief);
3526
3527 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3528 assert!(!v.ok, "{v:#?}");
3529 let p = v.entries[1].problem.as_deref().unwrap();
3530 assert!(p.contains("0 valid root signature"), "{p}");
3531 assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
3532 assert!(!v.entries[2].signature_valid);
3533 assert_eq!(v.keys.len(), 1);
3534 }
3535
3536 #[test]
3537 fn a_certificate_counts_distinct_known_roots_only() {
3538 let (old, old_pk) = generate_keypair();
3539 let (new, _) = generate_keypair();
3540 let (stranger, _) = generate_keypair();
3541 let two_of_two = RootSet::new(
3542 [
3543 (&root(1).verifying_key()).into(),
3544 (&root(2).verifying_key()).into(),
3545 ],
3546 2,
3547 );
3548 let verdict = |signers: &[&SigningKey], roots: &RootSet| {
3549 let mut c = Chain::new();
3550 c.decision(&old);
3551 let mut rotation = c.routine(&old_pk, &new);
3552 let statement = rotation.certificate.statement.clone();
3553 rotation.certificate = certify(signers, statement);
3554 let genesis = KeyCertStatement::genesis((&old_pk).into());
3556 rotation.outgoing_certificate =
3557 Some(certify(&[&root(1), &root(2)], genesis));
3558 c.rotate(&old, &rotation);
3559 verify_chain(&c.links, &old_pk, &anchored(&old_pk), roots)
3560 };
3561
3562 assert!(verdict(&[&root(1), &root(2)], &two_of_two).ok);
3563 assert!(verdict(&[&root(2)], &roots()).ok, "either root, 1-of-2");
3564
3565 for (signers, why) in [
3566 (vec![&root(1)], "below the threshold"),
3567 (vec![&root(1), &root(1)], "one root twice is one root"),
3568 (vec![&root(1), &stranger], "an unknown root is nobody"),
3569 (vec![], "unsigned"),
3570 ] {
3571 let v = verdict(&signers, &two_of_two);
3572 assert!(!v.ok, "{why}: {v:#?}");
3573 let p = v.entries[1].problem.as_deref().unwrap();
3574 assert!(p.contains("where 2 are needed"), "{why}: {p}");
3575 }
3576
3577 assert!(verdict(&[&stranger, &root(1)], &roots()).ok);
3579 }
3580
3581 #[test]
3582 fn a_certificate_is_good_for_one_statement_at_one_position() {
3583 let (old, old_pk) = generate_keypair();
3584 let (new, new_pk) = generate_keypair();
3585 let (other, other_pk) = generate_keypair();
3586 let anchor = anchored(&old_pk);
3587
3588 let mut c = Chain::new();
3592 c.decision(&old);
3593 let early = c.routine(&old_pk, &new);
3594 c.decision(&old);
3595 let mut rotation = c.routine(&old_pk, &new);
3596 rotation.certificate = early.certificate;
3597 c.rotate(&old, &rotation);
3598 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
3599 assert!(!v.ok, "{v:#?}");
3600 let p = v.entries[2].problem.as_deref().unwrap();
3601 assert!(
3602 p.contains("different key, purpose or chain position"),
3603 "{p}"
3604 );
3605 assert_eq!(v.public_key, (&old_pk).into());
3606
3607 let mut c = Chain::new();
3609 c.decision(&old);
3610 let for_new = c.routine(&old_pk, &new);
3611 let mut rotation = c.routine(&old_pk, &other);
3612 rotation.certificate = for_new.certificate;
3613 c.rotate(&old, &rotation);
3614 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
3615 let p = v.entries[1].problem.as_deref().unwrap();
3616 assert!(
3617 p.contains("different key, purpose or chain position"),
3618 "{p}"
3619 );
3620
3621 let mut c = Chain::new();
3623 c.decision(&old);
3624 let mut rotation = c.routine(&old_pk, &other);
3625 rotation.certificate = for_new_at(&c, &new_pk);
3626 rotation.certificate.statement.key = (&other_pk).into();
3627 c.rotate(&old, &rotation);
3628 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
3629 let p = v.entries[1].problem.as_deref().unwrap();
3630 assert!(p.contains("0 valid root signature"), "{p}");
3631
3632 let mut c = Chain::new();
3634 c.decision(&old);
3635 c.decision(&old);
3636 let mut rotation = c.compromise(&old_pk, &new, 1);
3637 rotation.certificate = for_new_at(&c, &new_pk);
3638 c.rotate(&new, &rotation);
3639 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
3640 assert!(!v.ok);
3641 assert!(v.repudiated.is_empty(), "{v:#?}");
3642 assert_eq!(v.public_key, (&old_pk).into());
3643 }
3644
3645 #[test]
3648 fn a_root_signature_without_the_domain_prefix_certifies_nothing() {
3649 use ed25519_dalek::Signer;
3650 let (old, old_pk) = generate_keypair();
3651 let (new, _) = generate_keypair();
3652 let mut c = Chain::new();
3653 c.decision(&old);
3654 let mut rotation = c.routine(&old_pk, &new);
3655 let statement = rotation.certificate.statement.clone();
3656 let bare = canonical_json(&serde_json::to_value(&statement).unwrap());
3657 assert_eq!(
3658 statement.signed_bytes(),
3659 [ROOT_DOMAIN, bare.as_slice()].concat()
3660 );
3661 rotation.certificate =
3662 KeyCertificate::unsigned(statement).with(RootSignature {
3663 root_key: (&root(1).verifying_key()).into(),
3664 signature: root(1).sign(&bare).into(),
3665 });
3666 c.rotate(&old, &rotation);
3667 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3668 assert!(!v.ok, "{v:#?}");
3669 assert_eq!(v.public_key, (&old_pk).into());
3670 }
3671
3672 #[test]
3673 fn the_first_rotation_carries_the_genesis_certificate_and_only_it_does() {
3674 let (k1, k1_pk) = generate_keypair();
3675 let (k2, k2_pk) = generate_keypair();
3676 let (k3, _) = generate_keypair();
3677 let genesis =
3678 || certify(&[&root(1)], KeyCertStatement::genesis((&k1_pk).into()));
3679
3680 let mut c = Chain::new();
3682 c.decision(&k1);
3683 let mut rotation = c.routine(&k1_pk, &k2);
3684 rotation.outgoing_certificate = None;
3685 c.rotate(&k1, &rotation);
3686 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3687 assert!(!v.ok);
3688 let p = v.entries[1].problem.as_deref().unwrap();
3689 assert!(p.contains("genesis key's outgoing_certificate"), "{p}");
3690 assert_eq!(v.public_key, (&k1_pk).into());
3691
3692 let mut c = Chain::new();
3694 c.decision(&k1);
3695 let rotation = c.routine(&k1_pk, &k2).with_outgoing(certify(
3696 &[&root(1)],
3697 KeyCertStatement::genesis((&k2_pk).into()),
3698 ));
3699 c.rotate(&k1, &rotation);
3700 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3701 let p = v.entries[1].problem.as_deref().unwrap();
3702 assert!(p.starts_with("outgoing_certificate:"), "{p}");
3703
3704 let mut c = Chain::new();
3707 c.decision(&k1);
3708 let rotation = c.routine(&k1_pk, &k2);
3709 assert_eq!(rotation.outgoing_certificate, Some(genesis()));
3710 c.rotate(&k1, &rotation);
3711 let v = verify_chain(&c.links, &k1_pk, &KeyAnchor::default(), &roots());
3712 assert!(v.ok, "{v:#?}");
3713 assert!(v.unanchored_keys.is_empty(), "{v:#?}");
3714
3715 let again = c.routine(&k2_pk, &k3).with_outgoing(genesis());
3717 c.rotate(&k2, &again);
3718 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3719 assert!(!v.ok);
3720 let p = v.entries[2].problem.as_deref().unwrap();
3721 assert!(p.contains("nowhere else"), "{p}");
3722 }
3723
3724 #[test]
3725 fn a_forged_proof_of_possession_is_rejected() {
3726 let (old, old_pk) = generate_keypair();
3727 let (_, new_pk) = generate_keypair();
3728 let (thief, _) = generate_keypair();
3729 let mut c = Chain::new();
3730 c.decision(&old);
3731 let mut rotation = c.routine(&old_pk, &thief);
3734 rotation.new_key = (&new_pk).into();
3735 rotation.certificate = for_new_at(&c, &new_pk);
3736 let statement = rotation.statement(c.prev_hash());
3737 rotation.proof = crypto::sign(
3738 &old,
3739 statement.hash().as_bytes(),
3740 rotation.proof_signed_at,
3741 )
3742 .into();
3743 c.rotate(&old, &rotation);
3744
3745 assert_eq!(
3746 rotation.verify_proof(c.links[1].attestation.prev_hash),
3747 Err(RotationError::BadProof)
3748 );
3749 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3750 assert!(!v.ok, "{v:#?}");
3751 assert!(
3752 v.entries[1]
3753 .problem
3754 .as_deref()
3755 .unwrap()
3756 .contains("proof of possession")
3757 );
3758 assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
3759 }
3760
3761 #[test]
3762 fn a_compromise_repudiates_the_window_and_a_reattestation_restores_one() {
3763 let (old, old_pk) = generate_keypair();
3764 let (new, new_pk) = generate_keypair();
3765 let mut c = Chain::new();
3766 c.decision(&old); c.decision(&old); let reattested = c.decision(&old); let rotation = c.compromise(&old_pk, &new, 1);
3770 let rotation_id = c.rotate(&new, &rotation); let vouch = AmendmentDraft::new(
3772 reattested,
3773 c.hash_at(3),
3774 AmendmentKind::Reattested,
3775 "Art. VII",
3776 "independently verified; the Steward vouches for it",
3777 )
3778 .unwrap();
3779 let vouch_id = c.amend(&new, &vouch); let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3782 assert!(
3783 v.ok,
3784 "repudiation is a declared state, not a defect: {v:#?}"
3785 );
3786 assert_eq!(v.repudiated, vec![gov(2)]);
3787 assert!(v.entries[1].repudiated);
3788 assert!(!v.entries[2].repudiated);
3789 assert_eq!(v.entries[2].reattested_by, vec![vouch_id.clone()]);
3790 assert_eq!(v.entries[2].amended_by, vec![vouch_id]);
3791 assert_eq!(v.entries[3].signed_by, Some((&new_pk).into()));
3792 assert_eq!(v.public_key, (&new_pk).into());
3793 assert_eq!(v.keys.len(), 2);
3794 assert_eq!(v.keys[0].status, KeyStatus::Compromised);
3795 assert_eq!(
3796 v.keys[0].through_seq,
3797 Some(1),
3798 "trusted through the last trusted entry, not through the \
3799 declaration"
3800 );
3801 assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
3802 assert_eq!(v.keys[1].from_seq, 4, "the declaration is its own first");
3803 }
3804
3805 #[test]
3808 fn last_trusted_is_the_roots_to_say() {
3809 let (old, old_pk) = generate_keypair();
3810 let (new, _) = generate_keypair();
3811 let mut c = Chain::new();
3812 c.decision(&old); c.decision(&old); let mut rotation = c.compromise(&old_pk, &new, 1);
3815 rotation.certificate.statement.last_trusted = Some(c.head(2));
3816 c.rotate(&new, &rotation);
3817 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3818 assert!(!v.ok, "{v:#?}");
3819 assert!(v.repudiated.is_empty());
3820 assert_eq!(v.public_key, (&old_pk).into());
3821 }
3822
3823 #[test]
3824 fn a_stolen_key_cannot_be_rotated_back_in() {
3825 let (k1, k1_pk) = generate_keypair();
3829 let (k2, k2_pk) = generate_keypair();
3830 let mut c = Chain::new();
3831 c.decision(&k1); let real = c.compromise(&k1_pk, &k2, 1);
3833 c.rotate(&k2, &real); c.decision(&k2); let honest =
3836 verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3837 assert!(honest.ok, "{honest:#?}");
3838
3839 let hijack = c.compromise(&k2_pk, &k1, 3);
3840 c.rotate(&k1, &hijack); c.decision(&k1); let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3844 assert!(!v.ok);
3845 assert_eq!(v.public_key, (&k2_pk).into(), "the chain stays with K2");
3846 let p = v.entries[3].problem.as_deref().unwrap();
3847 assert!(p.contains("never brought back"), "{p}");
3848 assert!(!v.entries[3].signature_valid, "{:#?}", v.entries[3]);
3849 assert!(!v.entries[4].signature_valid, "K1 signs nothing again");
3850 assert_eq!(v.keys.len(), 2);
3851 assert_eq!(v.keys[1].status, KeyStatus::Active);
3852 }
3853
3854 #[test]
3855 fn a_routine_rotation_cannot_reuse_a_key_either() {
3856 let (k1, k1_pk) = generate_keypair();
3857 let (k2, k2_pk) = generate_keypair();
3858 let mut c = Chain::new();
3859 c.decision(&k1);
3860 let out = c.routine(&k1_pk, &k2);
3861 c.rotate(&k1, &out);
3862 let back = c.routine(&k2_pk, &k1);
3863 c.rotate(&k2, &back);
3864 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3865 assert!(!v.ok);
3866 assert!(
3867 v.entries[2]
3868 .problem
3869 .as_deref()
3870 .unwrap()
3871 .contains("never brought back"),
3872 "{:#?}",
3873 v.entries[2]
3874 );
3875 assert_eq!(v.public_key, (&k2_pk).into());
3876 }
3877
3878 #[test]
3879 fn a_forged_entry_has_no_effects() {
3880 let (steward, steward_pk) = generate_keypair();
3881 let (forger, _) = generate_keypair();
3882 let anchor = anchored(&steward_pk);
3883 let mut c = Chain::new();
3884 let target = c.decision(&steward); let fake = AmendmentDraft::new(
3886 target,
3887 c.hash_at(1),
3888 AmendmentKind::Overruled,
3889 "none",
3890 "overruled, says nobody with the key",
3891 )
3892 .unwrap();
3893 c.amend(&forger, &fake); let grab = c.routine(&steward_pk, &forger);
3897 c.rotate(&forger, &grab); let v = verify_chain(&c.links, &steward_pk, &anchor, &roots());
3900 assert!(!v.ok);
3901 assert!(v.entries[0].amended_by.is_empty(), "{:#?}", v.entries[0]);
3902 assert_eq!(v.public_key, (&steward_pk).into());
3903 assert_eq!(v.keys.len(), 1);
3904 assert!(v.unanchored_keys.is_empty());
3905 }
3906
3907 #[test]
3908 fn a_repeated_id_is_a_problem() {
3909 let (key, pk) = generate_keypair();
3910 let mut c = Chain::new();
3911 c.decision(&key);
3912 c.gov = 0;
3913 c.decision(&key); let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3915 assert!(!v.ok);
3916 assert!(v.entries.iter().all(|e| {
3917 e.problem
3918 .as_deref()
3919 .is_some_and(|p| p.contains("more than once"))
3920 }));
3921 }
3922
3923 #[test]
3924 fn a_second_compromise_cannot_anchor_inside_the_first_window() {
3925 let (k1, k1_pk) = generate_keypair();
3926 let (k2, _) = generate_keypair();
3927 let (k3, _) = generate_keypair();
3928 let mut c = Chain::new();
3929 c.decision(&k1); c.decision(&k1); let first = c.compromise(&k1_pk, &k2, 1);
3932 c.rotate(&k2, &first); let second = c.compromise(&k1_pk, &k3, 2);
3934 c.rotate(&k3, &second); let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3937 assert!(!v.ok);
3938 let p = v.entries[3].problem.as_deref().unwrap();
3939 assert!(p.contains("repudiated"), "{p}");
3940 assert_eq!(v.keys.len(), 2, "K1 and K2; K3 never took the chain");
3941 assert_eq!(v.keys[0].through_seq, Some(1));
3942 }
3943
3944 #[test]
3945 fn an_uncertified_compromise_fails_closed() {
3946 let (old, old_pk) = generate_keypair();
3947 let (new, new_pk) = generate_keypair();
3948 let mut c = Chain::new();
3949 c.decision(&old);
3950 c.decision(&old);
3951 let mut rotation = c.compromise(&old_pk, &new, 1);
3952 let statement = rotation.certificate.statement.clone();
3953 rotation.certificate = certify(&[&new], statement);
3954 c.rotate(&new, &rotation);
3955
3956 let anchor = anchored(&old_pk).with((&new_pk).into());
3958 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
3959 assert!(!v.ok, "{v:#?}");
3960 let p = v.entries[2].problem.as_deref().unwrap();
3961 assert!(p.contains("0 valid root signature"), "{p}");
3962 assert!(!v.entries[2].signature_valid, "checked under the old key");
3963 assert!(v.repudiated.is_empty(), "and nothing is repudiated");
3964 assert_eq!(v.public_key, (&old_pk).into());
3965 }
3966
3967 #[test]
3972 fn a_rotation_inside_the_window_is_void_with_it() {
3973 let (steward, steward_pk) = generate_keypair();
3974 let (successor, _) = generate_keypair();
3975 let (recovery, recovery_pk) = generate_keypair();
3976
3977 let mut c = Chain::new();
3978 c.decision(&steward); c.decision(&steward); let routine = c.routine(&steward_pk, &successor);
3981 c.rotate(&steward, &routine); c.decision(&successor); let declaration = c.compromise(&steward_pk, &recovery, 1);
3985 c.rotate(&recovery, &declaration); let v = verify_chain(
3988 &c.links,
3989 &steward_pk,
3990 &anchored(&steward_pk),
3991 &roots(),
3992 );
3993 assert!(v.ok, "{v:#?}");
3994 assert_eq!(v.repudiated, vec![gov(2), key_id(1), gov(3)]);
3995 assert_eq!(v.public_key, (&recovery_pk).into());
3996 assert_eq!(v.keys.len(), 2, "the successor is not part of history");
3997 assert_eq!(v.keys[0].public_key, (&steward_pk).into());
3998 assert_eq!(v.keys[0].status, KeyStatus::Compromised);
3999 assert!(
4000 v.keys[0].certified,
4001 "the genesis certificate rode in on the voided rotation and \
4002 is the root's word all the same"
4003 );
4004 assert_eq!(v.keys[1].public_key, (&recovery_pk).into());
4005 }
4006
4007 #[test]
4008 fn a_compromise_must_name_a_real_head_and_the_key_that_held_it() {
4009 let (old, old_pk) = generate_keypair();
4010 let (new, new_pk) = generate_keypair();
4011 let anchor = anchored(&old_pk);
4012
4013 let mut c = Chain::new();
4016 c.decision(&old);
4017 let mut trusted = c.head(1);
4018 trusted.entry_hash = data_hash(&json!("nope"));
4019 let statement = KeyCertStatement::compromise(
4020 (&new_pk).into(),
4021 2,
4022 c.prev_hash(),
4023 trusted,
4024 );
4025 let mut rotation = c.compromise(&old_pk, &new, 1);
4026 rotation.certificate = certify(&[&root(1)], statement);
4027 c.rotate(&new, &rotation);
4028 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
4029 let p = v.entries[1].problem.as_deref().unwrap();
4030 assert!(p.contains("last_trusted"), "{p}");
4031
4032 let (_, other_pk) = generate_keypair();
4034 let mut c = Chain::new();
4035 c.decision(&old);
4036 let rotation = c.compromise(&other_pk, &new, 1);
4037 c.rotate(&new, &rotation);
4038 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
4039 let p = v.entries[1].problem.as_deref().unwrap();
4040 assert!(p.contains("old_key is not the key"), "{p}");
4041
4042 let mut c = Chain::new();
4044 c.decision(&old);
4045 let mut rotation = c.compromise(&old_pk, &new, 1);
4046 rotation.certificate.statement.last_trusted = None;
4047 c.rotate(&new, &rotation);
4048 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
4049 let p = v.entries[1].problem.as_deref().unwrap();
4050 assert!(p.contains("must name last_trusted"), "{p}");
4051 }
4052
4053 #[test]
4054 fn a_rotation_carries_no_free_text() {
4055 let (old, old_pk) = generate_keypair();
4056 let (new, _) = generate_keypair();
4057 let mut c = Chain::new();
4058 c.decision(&old);
4059 let rotation = c.routine(&old_pk, &new);
4060 let mut value = serde_json::to_value(&rotation).unwrap();
4061 assert!(serde_json::from_value::<KeyRotation>(value.clone()).is_ok());
4062 value["note"] = json!("at the request of …");
4063 assert!(serde_json::from_value::<KeyRotation>(value).is_err());
4064
4065 let mut head = serde_json::to_value(c.head(1)).unwrap();
4066 head["comment"] = json!("the last one I remember signing");
4067 assert!(serde_json::from_value::<TrustedHead>(head).is_err());
4068
4069 let mut statement =
4070 serde_json::to_value(&rotation.certificate.statement).unwrap();
4071 statement["comment"] = json!("signed in the kitchen");
4072 assert!(serde_json::from_value::<KeyCertStatement>(statement).is_err());
4073 }
4074
4075 #[test]
4076 fn the_root_statement_bytes_are_pinned() {
4077 let key: PublicKeyHex = PUBLISHED_KEYS[0].parse().unwrap();
4078 assert_eq!(
4079 String::from_utf8(KeyCertStatement::genesis(key).signed_bytes())
4080 .unwrap(),
4081 "agora-governance-root-v1\n\
4082 {\"agora_governance_key_cert\":1,\"from_seq\":1,\
4083 \"key\":\"ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a\",\
4084 \"last_trusted\":null,\"prev_hash\":null,\"purpose\":\"genesis\"}"
4085 );
4086
4087 let statement = KeyCertStatement::compromise(
4091 Sha256Hex::from([0xab; 32]).to_string().parse().unwrap(),
4092 15,
4093 Some([0xcd; 32].into()),
4094 TrustedHead {
4095 id: gov(10),
4096 chain_seq: 11,
4097 entry_hash: [0xef; 32].into(),
4098 },
4099 );
4100 assert_eq!(
4101 Sha256Hex::from(<[u8; 32]>::from(Sha256::digest(
4102 statement.signed_bytes()
4103 )))
4104 .to_string(),
4105 "633685771e08be126fd12ca4eb98c77120e5868236ff541ecba85ce6a21e6b68"
4106 );
4107 }
4108
4109 #[test]
4110 fn root_keys_are_curve_points_and_make_a_root_set() {
4111 let roots = RootSet::published();
4112 assert_eq!(roots.keys().count(), ROOT_KEYS.len());
4113 assert_eq!(roots.threshold(), ROOT_THRESHOLD);
4114 assert!(ROOT_THRESHOLD >= 1 && ROOT_THRESHOLD <= ROOT_KEYS.len());
4115 for root in ROOT_KEYS {
4116 let key: PublicKeyHex = root.parse().unwrap();
4117 assert!(roots.contains(&key));
4118 assert!(
4119 key.to_verifying_key().is_ok(),
4120 "{root} is not a valid Ed25519 public key"
4121 );
4122 assert!(
4123 !PUBLISHED_KEYS.contains(root),
4124 "a root key never signs entries"
4125 );
4126 }
4127 assert_eq!(RootSet::new([], 0).threshold(), 1);
4128 }
4129
4130 #[test]
4131 fn a_genesis_key_outside_the_anchor_is_reported_not_rejected() {
4132 let (key, pk) = generate_keypair();
4133 let c = chain(&key, 2);
4134 let v = verify_chain(&c, &pk, &KeyAnchor::default(), &roots());
4135 assert!(v.ok, "{v:#?}");
4136 assert_eq!(v.unanchored_keys, vec![PublicKeyHex::from(&pk)]);
4137 assert_eq!(v.keys.len(), 1);
4138 assert_eq!(v.keys[0].status, KeyStatus::Active);
4139 assert_eq!(v.keys[0].from_seq, 1);
4140 assert_eq!(v.keys[0].through_seq, None);
4141 }
4142
4143 #[test]
4144 fn published_keys_are_curve_points_and_make_an_anchor() {
4145 let anchor = KeyAnchor::published();
4146 assert!(!anchor.is_empty());
4147 assert_eq!(anchor.keys().count(), PUBLISHED_KEYS.len());
4148 for published in PUBLISHED_KEYS {
4149 let key: PublicKeyHex = published.parse().unwrap();
4150 assert!(anchor.contains(&key));
4151 assert!(
4152 key.to_verifying_key().is_ok(),
4153 "{published} is not a valid Ed25519 public key"
4154 );
4155 }
4156 let (_, other) = generate_keypair();
4157 assert!(!anchor.contains(&(&other).into()));
4158 assert!(
4159 anchor
4160 .clone()
4161 .with((&other).into())
4162 .contains(&(&other).into())
4163 );
4164 }
4165
4166 #[test]
4169 fn amendments_and_rotations_round_trip_as_entry_data() {
4170 let (key, pk) = generate_keypair();
4171 let amendment = AmendmentDraft::new(
4172 gov(1),
4173 data_hash(&json!("x")),
4174 AmendmentKind::Superseded,
4175 "Art. VI § 2",
4176 "superseded by GOV-2026-0009",
4177 )
4178 .unwrap()
4179 .with_authority(gov(9))
4180 .with_rationale("the later decision covers the same subject");
4181 let AmendmentDraft { amendment, texts } = amendment;
4182 let value = serde_json::to_value(&amendment).unwrap();
4183 assert_eq!(value["kind"], "superseded");
4184 assert_eq!(value["agora_governance_amendment"], 2);
4185 assert!(value.get("redaction").is_none(), "{value}");
4186 let text = value.to_string();
4187 assert!(!text.contains("Art. VI"), "no text in signed data: {text}");
4188 assert!(!text.contains("salt"), "and no salt: {text}");
4189 assert_eq!(
4190 value["note"]["commitment"],
4191 texts
4192 .note
4193 .as_ref()
4194 .unwrap()
4195 .commitment()
4196 .commitment
4197 .to_string()
4198 );
4199 assert_eq!(
4200 serde_json::from_value::<Amendment>(value).unwrap(),
4201 amendment
4202 );
4203 let beside = serde_json::to_value(&texts).unwrap();
4204 assert_eq!(beside["basis"]["text"], "Art. VI § 2");
4205 assert_eq!(
4206 serde_json::from_value::<AmendmentTexts>(beside).unwrap(),
4207 texts
4208 );
4209
4210 let legacy = json!({
4212 "agora_governance_amendment": 1,
4213 "target": "GOV-2026-0001",
4214 "target_entry_hash": data_hash(&json!("x")),
4215 "kind": "non_precedential",
4216 "authority": "GOV-2026-0005",
4217 "basis": "§1",
4218 "note": "diagnostic finding",
4219 });
4220 let legacy: Amendment = serde_json::from_value(legacy).unwrap();
4221 assert_eq!(legacy.validate(), Ok(()));
4222 assert_eq!(legacy.basis, AmendmentText::Plain("§1".into()));
4223
4224 let mut c = Chain::new();
4225 c.decision(&key);
4226 let rotation = c.compromise(&pk, &generate_keypair().0, 1);
4227 let value = serde_json::to_value(&rotation).unwrap();
4228 assert_eq!(value["agora_governance_key_rotation"], 2);
4229 assert_eq!(value["reason"], "compromise");
4230 let statement = &value["certificate"]["statement"];
4231 assert_eq!(statement["purpose"], "compromise");
4232 assert_eq!(statement["last_trusted"]["chain_seq"], 1);
4233 assert_eq!(
4234 value["outgoing_certificate"]["statement"]["purpose"],
4235 "genesis"
4236 );
4237 assert!(value.get("note").is_none(), "{value}");
4238 assert_eq!(
4239 serde_json::from_value::<KeyRotation>(value).unwrap(),
4240 rotation
4241 );
4242
4243 let notice =
4244 AmendmentNotice::new(amd(1), at(0), &amendment, Some(&texts));
4245 let value = serde_json::to_value(¬ice).unwrap();
4246 assert_eq!(value["id"], "AMD-2026-0001");
4247 assert_eq!(value["note"], "superseded by GOV-2026-0009");
4248 assert_eq!(
4249 serde_json::from_value::<AmendmentNotice>(value).unwrap(),
4250 notice
4251 );
4252
4253 let erased = AmendmentTexts {
4255 rationale: None,
4256 ..texts.clone()
4257 };
4258 let notice =
4259 AmendmentNotice::new(amd(1), at(0), &amendment, Some(&erased));
4260 assert_eq!(notice.note, "superseded by GOV-2026-0009");
4261 assert_eq!(notice.rationale.as_deref(), Some(WITHHELD_TEXT));
4262 let notice = AmendmentNotice::new(amd(1), at(0), &amendment, None);
4263 assert_eq!(notice.basis, WITHHELD_TEXT);
4264 let notice = AmendmentNotice::new(amd(1), at(0), &legacy, None);
4265 assert_eq!(notice.note, "diagnostic finding");
4266 }
4267
4268 #[test]
4272 fn an_amendment_verifies_with_or_without_its_optional_fields() {
4273 let (key, pk) = generate_keypair();
4274 let mut c = Chain::new();
4275 let target = c.decision(&key);
4276 let bare = AmendmentDraft::new(
4277 target.clone(),
4278 c.hash_at(1),
4279 AmendmentKind::Correction,
4280 "clerical",
4281 "typo in the citation",
4282 )
4283 .unwrap();
4284 assert!(
4285 serde_json::to_value(&bare.amendment)
4286 .unwrap()
4287 .get("rationale")
4288 .is_none()
4289 );
4290 c.amend(&key, &bare);
4291 let full = bare.clone().with_rationale("at length: …");
4292 c.amend(&key, &full);
4293 let mut future = serde_json::to_value(&full.amendment).unwrap();
4295 future["superseded_by_something_new"] = json!(["later"]);
4296 c.push(
4297 &key,
4298 amd(3),
4299 GovernanceLogEntryType::Amendment,
4300 future,
4301 true,
4302 );
4303
4304 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4305 assert!(v.ok, "{v:#?}");
4306 assert_eq!(
4307 v.entries[0].amended_by,
4308 vec![amd(1), amd(2), amd(3)],
4309 "all three name the target"
4310 );
4311 }
4312
4313 #[test]
4316 fn pre_0_26_wire_still_deserializes() {
4317 let link: GovernanceChainLink = serde_json::from_value(json!({
4318 "id": "GOV-2026-0001",
4319 "entry_type": "council_decision",
4320 "created_at": "2023-11-14T22:13:20.123456Z",
4321 "attestation": {
4322 "envelope_version": 1,
4323 "chain_seq": 1,
4324 "prev_hash": null,
4325 "data_hash": "00".repeat(32),
4326 "entry_hash": "11".repeat(32),
4327 "signature": "22".repeat(64),
4328 "signed_at": "2023-11-14T22:13:21Z",
4329 "retroactive": false,
4330 },
4331 }))
4332 .unwrap();
4333 assert!(link.data.is_none());
4334 assert!(
4335 serde_json::to_value(&link).unwrap().get("data").is_none(),
4336 "and a link without data does not grow a null field"
4337 );
4338
4339 let verdict: EntryVerdict = serde_json::from_value(json!({
4340 "id": "GOV-2026-0001",
4341 "chain_seq": 1,
4342 "signature_valid": true,
4343 "link_valid": true,
4344 "content_matches": null,
4345 "retroactive": false,
4346 "out_of_order": false,
4347 "amended_by": [],
4348 }))
4349 .unwrap();
4350 assert!(!verdict.repudiated && !verdict.redacted);
4351 assert!(verdict.signed_by.is_none());
4352 assert!(verdict.reattested_by.is_empty());
4353
4354 let verification: GovernanceVerification =
4355 serde_json::from_value(json!({
4356 "public_key": "33".repeat(32),
4357 "ok": true,
4358 "head": "GOV-2026-0001",
4359 "entries": [],
4360 }))
4361 .unwrap();
4362 assert!(verification.keys.is_empty());
4363 assert!(verification.unanchored_keys.is_empty());
4364 assert!(verification.repudiated.is_empty());
4365 }
4366
4367 #[test]
4368 fn the_report_round_trips() {
4369 let (key, pk) = generate_keypair();
4370 let mut c = Chain::new();
4371 let target = c.decision(&key);
4372 let amendment = AmendmentDraft::new(
4373 target,
4374 c.hash_at(1),
4375 AmendmentKind::Overruled,
4376 "b",
4377 "n",
4378 )
4379 .unwrap();
4380 c.amend(&key, &amendment);
4381 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4382 let text = serde_json::to_string(&v).unwrap();
4383 assert_eq!(
4384 serde_json::from_str::<GovernanceVerification>(&text).unwrap(),
4385 v
4386 );
4387
4388 let keys = GovernanceSigningKeys { keys: v.keys };
4389 let value = serde_json::to_value(&keys).unwrap();
4390 assert!(value["keys"].is_array(), "an object, not a bare array");
4391 assert_eq!(value["keys"][0]["status"], "active");
4392 assert_eq!(
4393 serde_json::from_value::<GovernanceSigningKeys>(value).unwrap(),
4394 keys
4395 );
4396 }
4397
4398 #[test]
4402 fn envelope_v1_preimage_and_hash_are_pinned() {
4403 let envelope = Envelope::new(
4404 gov(6),
4405 GovernanceLogEntryType::CouncilDecision,
4406 at(0),
4407 Some(Sha256Hex::from([0x11; 32])),
4408 data_hash(&json!({"outcome": "approved", "title": "Ratification"})),
4409 );
4410 assert_eq!(
4411 String::from_utf8(envelope.preimage()).unwrap(),
4412 "{\"agora_governance_log\":1,\"id\":\"GOV-2026-0006\",\
4413 \"entry_type\":\"council_decision\",\
4414 \"created_at\":1700000000123456,\
4415 \"prev_hash\":\"1111111111111111111111111111111111111111111111111111111111111111\",\
4416 \"data_hash\":\"a4adf645ae3f60c56484d01aea87d6d490321d7fc66b1607df14b023fe567c7b\"}"
4417 );
4418 assert_eq!(
4419 envelope.entry_hash().to_hex(),
4420 "ba27577432f81e415f1c01cc4cfabab6070e3ac50fd468fffe195ef19c0e9464"
4421 );
4422 }
4423
4424 #[cfg(feature = "agora-client")]
4438 #[tokio::test]
4439 #[ignore = "networked: hits the live platform"]
4440 async fn the_published_key_is_the_one_the_platform_serves() {
4441 let client = crate::client::Client::new(
4442 url::Url::parse("https://subliminal.technology").unwrap(),
4443 )
4444 .unwrap();
4445 let served = client.get_governance_signing_key().await.unwrap();
4446 assert_eq!(served.algorithm, "ed25519");
4447
4448 let genesis: PublicKeyHex = PUBLISHED_KEYS
4449 .first()
4450 .expect("PUBLISHED_KEYS is never empty")
4451 .parse()
4452 .unwrap();
4453 let links = client.get_governance_chain().await.unwrap();
4454 let report = verify_chain(
4455 &links,
4456 &genesis.to_verifying_key().unwrap(),
4457 &KeyAnchor::published(),
4458 &RootSet::published(),
4459 );
4460 let problems: Vec<_> = report
4461 .entries
4462 .iter()
4463 .filter_map(|e| {
4464 e.problem.as_ref().map(|p| (e.id.clone(), p.clone()))
4465 })
4466 .collect();
4467 assert!(
4468 report.ok,
4469 "the live chain does not verify under this build's genesis key \
4470 and roots: {problems:#?}"
4471 );
4472 assert!(report.unanchored_keys.is_empty(), "{report:#?}");
4473 assert_eq!(
4474 served.public_key, report.public_key,
4475 "the platform serves {} but the chain, followed under the \
4476 published roots, is held by {}",
4477 served.public_key, report.public_key
4478 );
4479 }
4480
4481 #[cfg(feature = "schemars")]
4482 #[test]
4483 fn wire_schemas_are_ref_free() {
4484 use crate::responses::inline_schema_for;
4485 for (name, schema) in [
4486 (
4487 "GovernanceAttestation",
4488 inline_schema_for::<GovernanceAttestation>(),
4489 ),
4490 (
4491 "GovernanceChainLink",
4492 inline_schema_for::<GovernanceChainLink>(),
4493 ),
4494 (
4495 "GovernanceSigningKey",
4496 inline_schema_for::<GovernanceSigningKey>(),
4497 ),
4498 (
4499 "GovernanceVerification",
4500 inline_schema_for::<GovernanceVerification>(),
4501 ),
4502 (
4503 "Vec<GovernanceChainLink>",
4504 inline_schema_for::<Vec<GovernanceChainLink>>(),
4505 ),
4506 ("Amendment", inline_schema_for::<Amendment>()),
4507 ("Redaction", inline_schema_for::<Redaction>()),
4508 ("AmendmentNotice", inline_schema_for::<AmendmentNotice>()),
4509 ("KeyRotation", inline_schema_for::<KeyRotation>()),
4510 ("TrustedHead", inline_schema_for::<TrustedHead>()),
4511 (
4512 "GovernanceKeyRecord",
4513 inline_schema_for::<GovernanceKeyRecord>(),
4514 ),
4515 (
4516 "GovernanceSigningKeys",
4517 inline_schema_for::<GovernanceSigningKeys>(),
4518 ),
4519 ("AmendmentKind", inline_schema_for::<AmendmentKind>()),
4520 ("Standing", inline_schema_for::<Standing>()),
4521 ("KeyStatus", inline_schema_for::<KeyStatus>()),
4522 ("RotationReason", inline_schema_for::<RotationReason>()),
4523 ] {
4524 let text = serde_json::to_string(&schema).unwrap();
4525 assert!(!text.contains("$ref"), "{name} must be $ref-free: {text}");
4526 assert!(
4527 !text.contains("$defs"),
4528 "{name} must be $defs-free: {text}"
4529 );
4530 }
4531 let text =
4532 serde_json::to_string(&inline_schema_for::<Sha256Hex>()).unwrap();
4533 assert!(text.contains("^[0-9a-f]{64}$"), "{text}");
4534 let text = serde_json::to_string(&inline_schema_for::<SignatureHex>())
4535 .unwrap();
4536 assert!(text.contains("^[0-9a-f]{128}$"), "{text}");
4537 }
4538}