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