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