1use crate::crypto::{self, Signature, SigningKey, VerifyingKey};
46use crate::enums::GovernanceLogEntryType;
47use crate::ids::{GovernanceLogId, GovernanceLogPrefix};
48use chrono::{DateTime, Utc};
49use serde::{Deserialize, Serialize};
50use sha2::{Digest, Sha256};
51use std::collections::{HashMap, HashSet};
52
53pub use crate::enums::{AmendmentKind, KeyStatus, Standing};
54
55mod texts;
56pub use texts::{
57 AmendmentText, AmendmentTextStatus, AmendmentTexts, CommittedText,
58 TextCommitment, TextStatus, WITHHELD_TEXT,
59};
60
61mod root;
62pub use root::{
63 CertPurpose, CertificateError, KEY_CERT_VERSION, KeyCertStatement,
64 KeyCertificate, ROOT_DOMAIN, ROOT_KEYS, ROOT_THRESHOLD, RootSet,
65 RootSignature,
66};
67
68#[cfg(test)]
70mod vectors;
71
72pub const ENVELOPE_VERSION: u32 = 1;
74
75pub const RETROACTIVE_AFTER: chrono::Duration = chrono::Duration::seconds(60);
78
79#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
86#[error("{type_name}: expected {expected} bytes of hex, got {got:?}")]
87pub struct HexLengthError {
88 pub type_name: &'static str,
89 pub expected: usize,
90 pub got: String,
91}
92
93macro_rules! hex_bytes {
94 ($(#[$meta:meta])* $name:ident, $len:expr) => {
95 $(#[$meta])*
96 #[derive(Clone, Copy, PartialEq, Eq, Hash)]
97 #[cfg_attr(feature = "sqlx", derive(sqlx::Type))]
98 #[cfg_attr(feature = "sqlx", sqlx(transparent))]
99 pub struct $name([u8; $len]);
100
101 impl $name {
102 pub fn as_bytes(&self) -> &[u8; $len] {
104 &self.0
105 }
106
107 pub fn to_hex(&self) -> String {
109 hex::encode(self.0)
110 }
111 }
112
113 impl From<[u8; $len]> for $name {
114 fn from(bytes: [u8; $len]) -> Self {
115 Self(bytes)
116 }
117 }
118
119 impl TryFrom<&[u8]> for $name {
120 type Error = HexLengthError;
121
122 fn try_from(bytes: &[u8]) -> Result<Self, Self::Error> {
123 <[u8; $len]>::try_from(bytes).map(Self).map_err(|_| {
124 HexLengthError {
125 type_name: stringify!($name),
126 expected: $len,
127 got: hex::encode(bytes),
128 }
129 })
130 }
131 }
132
133 impl TryFrom<Vec<u8>> for $name {
134 type Error = HexLengthError;
135
136 fn try_from(bytes: Vec<u8>) -> Result<Self, Self::Error> {
137 Self::try_from(bytes.as_slice())
138 }
139 }
140
141 impl std::str::FromStr for $name {
142 type Err = HexLengthError;
143
144 fn from_str(s: &str) -> Result<Self, Self::Err> {
145 let bytes = hex::decode(s.trim()).map_err(|_| HexLengthError {
146 type_name: stringify!($name),
147 expected: $len,
148 got: s.to_string(),
149 })?;
150 Self::try_from(bytes.as_slice())
151 }
152 }
153
154 impl std::fmt::Display for $name {
155 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
156 f.write_str(&self.to_hex())
157 }
158 }
159
160 impl std::fmt::Debug for $name {
161 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162 write!(f, "{}({})", stringify!($name), self.to_hex())
163 }
164 }
165
166 impl Serialize for $name {
167 fn serialize<S: serde::Serializer>(
168 &self,
169 s: S,
170 ) -> Result<S::Ok, S::Error> {
171 s.serialize_str(&self.to_hex())
172 }
173 }
174
175 impl<'de> Deserialize<'de> for $name {
176 fn deserialize<D: serde::Deserializer<'de>>(
177 d: D,
178 ) -> Result<Self, D::Error> {
179 let s = String::deserialize(d)?;
180 s.parse().map_err(serde::de::Error::custom)
181 }
182 }
183
184 #[cfg(feature = "schemars")]
188 impl schemars::JsonSchema for $name {
189 fn inline_schema() -> bool {
190 true
191 }
192
193 fn schema_name() -> std::borrow::Cow<'static, str> {
194 std::borrow::Cow::Borrowed(stringify!($name))
195 }
196
197 fn schema_id() -> std::borrow::Cow<'static, str> {
198 std::borrow::Cow::Borrowed(concat!(
199 module_path!(),
200 "::",
201 stringify!($name)
202 ))
203 }
204
205 fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
206 schemars::json_schema!({
207 "type": "string",
208 "pattern": format!("^[0-9a-f]{{{}}}$", $len * 2),
209 "description": format!("{} bytes, lowercase hex", $len),
210 })
211 }
212 }
213 };
214}
215
216hex_bytes!(
217 Sha256Hex,
219 32
220);
221
222hex_bytes!(
223 SignatureHex,
225 64
226);
227
228hex_bytes!(
229 PublicKeyHex,
231 32
232);
233
234hex_bytes!(
235 Blind,
238 32
239);
240
241hex_bytes!(
242 TextSalt,
245 32
246);
247
248impl Blind {
249 pub fn random() -> Self {
251 use rand::RngCore;
252 let mut bytes = [0u8; 32];
253 rand::rngs::OsRng.fill_bytes(&mut bytes);
254 Self(bytes)
255 }
256}
257
258impl TextSalt {
259 pub fn random() -> Self {
261 Self(*Blind::random().as_bytes())
262 }
263}
264
265impl From<Signature> for SignatureHex {
266 fn from(sig: Signature) -> Self {
267 Self(sig.to_bytes())
268 }
269}
270
271impl From<&SignatureHex> for Signature {
272 fn from(sig: &SignatureHex) -> Self {
273 Signature::from_bytes(&sig.0)
274 }
275}
276
277impl From<&VerifyingKey> for PublicKeyHex {
278 fn from(key: &VerifyingKey) -> Self {
279 Self(key.to_bytes())
280 }
281}
282
283impl PublicKeyHex {
284 pub fn to_verifying_key(
286 &self,
287 ) -> Result<VerifyingKey, ed25519_dalek::SignatureError> {
288 VerifyingKey::from_bytes(&self.0)
289 }
290}
291
292pub fn canonical_json(value: &serde_json::Value) -> Vec<u8> {
305 let mut out = Vec::new();
306 write_canonical(value, &mut out);
307 out
308}
309
310fn write_canonical(value: &serde_json::Value, out: &mut Vec<u8>) {
311 use serde_json::Value;
312 match value {
313 Value::Null => out.extend_from_slice(b"null"),
314 Value::Bool(b) => {
315 out.extend_from_slice(if *b { b"true" } else { b"false" })
316 }
317 Value::Number(n) => serde_json::to_writer(&mut *out, n)
318 .expect("a number always serializes"),
319 Value::String(s) => serde_json::to_writer(&mut *out, s)
320 .expect("a string always serializes"),
321 Value::Array(items) => {
322 out.push(b'[');
323 for (i, item) in items.iter().enumerate() {
324 if i > 0 {
325 out.push(b',');
326 }
327 write_canonical(item, out);
328 }
329 out.push(b']');
330 }
331 Value::Object(map) => {
332 let mut keys: Vec<&String> = map.keys().collect();
333 keys.sort_unstable();
334 out.push(b'{');
335 for (i, key) in keys.into_iter().enumerate() {
336 if i > 0 {
337 out.push(b',');
338 }
339 serde_json::to_writer(&mut *out, key)
340 .expect("a string always serializes");
341 out.push(b':');
342 write_canonical(&map[key], out);
343 }
344 out.push(b'}');
345 }
346 }
347}
348
349pub fn non_integer_number(data: &serde_json::Value) -> Option<String> {
360 fn find(value: &serde_json::Value, path: &mut String) -> bool {
361 use serde_json::Value::{Array, Number, Object};
362 let mark = path.len();
363 match value {
364 Number(n) => return n.is_f64(),
365 Array(items) => {
366 for (i, item) in items.iter().enumerate() {
367 path.push_str(&format!("/{i}"));
368 if find(item, path) {
369 return true;
370 }
371 path.truncate(mark);
372 }
373 }
374 Object(map) => {
375 for (key, item) in map {
376 path.push('/');
377 path.push_str(&key.replace('~', "~0").replace('/', "~1"));
378 if find(item, path) {
379 return true;
380 }
381 path.truncate(mark);
382 }
383 }
384 _ => {}
385 }
386 false
387 }
388 let mut path = String::new();
389 find(data, &mut path).then_some(path)
390}
391
392pub fn data_hash(data: &serde_json::Value) -> Sha256Hex {
394 Sha256Hex(Sha256::digest(canonical_json(data)).into())
395}
396
397#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
402pub struct Envelope {
403 pub agora_governance_log: u32,
405 pub id: GovernanceLogId,
406 pub entry_type: GovernanceLogEntryType,
407 pub created_at: i64,
409 pub prev_hash: Option<Sha256Hex>,
410 pub data_hash: Sha256Hex,
411}
412
413impl Envelope {
414 pub fn new(
416 id: GovernanceLogId,
417 entry_type: GovernanceLogEntryType,
418 created_at: DateTime<Utc>,
419 prev_hash: Option<Sha256Hex>,
420 data_hash: Sha256Hex,
421 ) -> Self {
422 Self {
423 agora_governance_log: ENVELOPE_VERSION,
424 id,
425 entry_type,
426 created_at: created_at.timestamp_micros(),
427 prev_hash,
428 data_hash,
429 }
430 }
431
432 pub fn created_at(&self) -> DateTime<Utc> {
434 DateTime::from_timestamp_micros(self.created_at)
435 .expect("an Envelope only ever holds an in-range timestamp")
436 }
437
438 pub fn preimage(&self) -> Vec<u8> {
440 serde_json::to_vec(self).expect("an Envelope always serializes")
441 }
442
443 pub fn entry_hash(&self) -> Sha256Hex {
445 Sha256Hex(Sha256::digest(self.preimage()).into())
446 }
447}
448
449pub fn truncate_to_micros(t: DateTime<Utc>) -> DateTime<Utc> {
452 DateTime::from_timestamp_micros(t.timestamp_micros())
453 .expect("a timestamp that came from a DateTime is in range")
454}
455
456pub fn truncate_to_seconds(t: DateTime<Utc>) -> DateTime<Utc> {
459 DateTime::from_timestamp(t.timestamp(), 0)
460 .expect("a timestamp that came from a DateTime is in range")
461}
462
463pub fn is_retroactive(
467 created_at: DateTime<Utc>,
468 signed_at: DateTime<Utc>,
469) -> bool {
470 signed_at - created_at > RETROACTIVE_AFTER
471}
472
473#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
479#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
480#[cfg_attr(feature = "schemars", schemars(inline))]
481pub struct GovernanceAttestation {
482 pub envelope_version: u32,
484 pub chain_seq: u64,
487 pub prev_hash: Option<Sha256Hex>,
489 pub data_hash: Sha256Hex,
491 pub entry_hash: Sha256Hex,
493 pub signature: SignatureHex,
496 pub signed_at: DateTime<Utc>,
499 pub retroactive: bool,
503}
504
505#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
509#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
510#[cfg_attr(feature = "schemars", schemars(inline))]
511pub struct GovernanceChainLink {
512 pub id: GovernanceLogId,
513 pub entry_type: GovernanceLogEntryType,
514 pub created_at: DateTime<Utc>,
515 pub attestation: GovernanceAttestation,
516 #[serde(default, skip_serializing_if = "Option::is_none")]
523 pub data: Option<serde_json::Value>,
524 #[serde(
528 default,
529 skip_serializing_if = "Option::is_none",
530 deserialize_with = "read_as_written"
531 )]
532 pub texts: Option<AmendmentTexts>,
533}
534
535#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
538#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
539#[cfg_attr(feature = "schemars", schemars(inline))]
540pub struct GovernanceSigningKey {
541 pub algorithm: String,
543 pub public_key: PublicKeyHex,
544 pub envelope_version: u32,
546}
547
548impl GovernanceSigningKey {
549 pub fn new(key: &VerifyingKey) -> Self {
551 Self {
552 algorithm: "ed25519".to_string(),
553 public_key: key.into(),
554 envelope_version: ENVELOPE_VERSION,
555 }
556 }
557}
558
559pub const AMENDMENT_VERSION: u32 = 2;
567
568#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
570pub enum AmendmentError {
571 #[error("agora_governance_amendment is {0}, not 1 or {AMENDMENT_VERSION}")]
572 UnsupportedVersion(u32),
573 #[error(
574 "a version 1 amendment carries its texts and a version \
575 {AMENDMENT_VERSION} one commits to them; this does neither \
576 consistently"
577 )]
578 TextShape,
579 #[error("`{0}` beside the entry is not the text the entry committed to")]
580 TextMismatch(&'static str),
581 #[error("texts beside an entry that commits to none")]
582 UncommittedText,
583 #[error("kind `redaction` requires a `redaction`")]
584 MissingRedaction,
585 #[error("`redaction` is only valid on kind `redaction`")]
586 UnexpectedRedaction,
587 #[error("amendment target {0} is not an entry of this chain")]
588 UnknownTarget(GovernanceLogId),
589 #[error("amendment target {0} is not an earlier entry")]
590 ForwardReference(GovernanceLogId),
591 #[error("target_entry_hash is not {0}'s entry_hash")]
592 WrongTargetHash(GovernanceLogId),
593}
594
595#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
601#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
602#[cfg_attr(feature = "schemars", schemars(inline))]
603pub struct Amendment {
604 pub agora_governance_amendment: u32,
606 pub target: GovernanceLogId,
607 pub target_entry_hash: Sha256Hex,
609 pub kind: AmendmentKind,
610 #[serde(default)]
613 pub authority: Option<GovernanceLogId>,
614 pub basis: AmendmentText,
618 pub note: AmendmentText,
620 #[serde(default, skip_serializing_if = "Option::is_none")]
622 pub rationale: Option<AmendmentText>,
623 #[serde(default, skip_serializing_if = "Option::is_none")]
625 pub redaction: Option<Redaction>,
626}
627
628#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
630#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
631#[cfg_attr(feature = "schemars", schemars(inline))]
632pub struct Redaction {
633 pub fields: Vec<String>,
636 pub resulting_data_hash: Sha256Hex,
639}
640
641#[derive(Debug, Clone, PartialEq, Eq)]
644pub struct AmendmentDraft {
645 pub amendment: Amendment,
646 pub texts: AmendmentTexts,
647}
648
649impl AmendmentDraft {
650 pub fn new(
657 target: GovernanceLogId,
658 target_entry_hash: Sha256Hex,
659 kind: AmendmentKind,
660 basis: impl Into<CommittedText>,
661 note: impl Into<CommittedText>,
662 ) -> Result<Self, AmendmentError> {
663 if kind == AmendmentKind::Redaction {
664 return Err(AmendmentError::MissingRedaction);
665 }
666 let (basis, note) = (basis.into(), note.into());
667 Ok(Self {
668 amendment: Amendment {
669 agora_governance_amendment: AMENDMENT_VERSION,
670 target,
671 target_entry_hash,
672 kind,
673 authority: None,
674 basis: AmendmentText::Committed(basis.commitment()),
675 note: AmendmentText::Committed(note.commitment()),
676 rationale: None,
677 redaction: None,
678 },
679 texts: AmendmentTexts {
680 basis: Some(basis),
681 note: Some(note),
682 rationale: None,
683 },
684 })
685 }
686
687 #[allow(clippy::too_many_arguments)]
697 pub fn redaction(
698 amendment_id: &GovernanceLogId,
699 target: GovernanceLogId,
700 target_entry_hash: Sha256Hex,
701 basis: impl Into<CommittedText>,
702 note: impl Into<CommittedText>,
703 fields: Vec<String>,
704 data: &serde_json::Value,
705 blind: Blind,
706 ) -> Result<(Self, serde_json::Value), RedactError> {
707 let redacted = redact_data(data, &fields, amendment_id, blind)?;
708 let (basis, note) = (basis.into(), note.into());
709 Ok((
710 Self {
711 amendment: Amendment {
712 agora_governance_amendment: AMENDMENT_VERSION,
713 target,
714 target_entry_hash,
715 kind: AmendmentKind::Redaction,
716 authority: None,
717 basis: AmendmentText::Committed(basis.commitment()),
718 note: AmendmentText::Committed(note.commitment()),
719 rationale: None,
720 redaction: Some(Redaction {
721 fields,
722 resulting_data_hash: data_hash(&redacted),
723 }),
724 },
725 texts: AmendmentTexts {
726 basis: Some(basis),
727 note: Some(note),
728 rationale: None,
729 },
730 },
731 redacted,
732 ))
733 }
734
735 pub fn with_authority(mut self, authority: GovernanceLogId) -> Self {
737 self.amendment.authority = Some(authority);
738 self
739 }
740
741 pub fn with_rationale(
743 mut self,
744 rationale: impl Into<CommittedText>,
745 ) -> Self {
746 let rationale = rationale.into();
747 self.amendment.rationale =
748 Some(AmendmentText::Committed(rationale.commitment()));
749 self.texts.rationale = Some(rationale);
750 self
751 }
752}
753
754impl Amendment {
755 pub fn validate(&self) -> Result<(), AmendmentError> {
759 let plain = match self.agora_governance_amendment {
760 1 => true,
761 AMENDMENT_VERSION => false,
762 other => return Err(AmendmentError::UnsupportedVersion(other)),
763 };
764 let texts =
765 [Some(&self.basis), Some(&self.note), self.rationale.as_ref()];
766 if texts.into_iter().flatten().any(|t| t.is_plain() != plain) {
767 return Err(AmendmentError::TextShape);
768 }
769 match (self.kind, &self.redaction) {
770 (AmendmentKind::Redaction, None) => {
771 Err(AmendmentError::MissingRedaction)
772 }
773 (k, Some(_)) if k != AmendmentKind::Redaction => {
774 Err(AmendmentError::UnexpectedRedaction)
775 }
776 _ => Ok(()),
777 }
778 }
779
780 pub fn text_status(
787 &self,
788 beside: Option<&AmendmentTexts>,
789 ) -> Result<Option<AmendmentTextStatus>, AmendmentError> {
790 let empty = AmendmentTexts::default();
791 let beside = beside.unwrap_or(&empty);
792 let (Some(basis), Some(note)) = (
793 self.basis.status(beside.basis.as_ref()),
794 self.note.status(beside.note.as_ref()),
795 ) else {
796 return if beside.is_empty() {
797 Ok(None)
798 } else {
799 Err(AmendmentError::UncommittedText)
800 };
801 };
802 let rationale = match (&self.rationale, &beside.rationale) {
803 (None, Some(_)) => return Err(AmendmentError::UncommittedText),
804 (None, None) => None,
805 (Some(text), beside) => text.status(beside.as_ref()),
806 };
807 for (name, status) in [
808 ("basis", Some(basis)),
809 ("note", Some(note)),
810 ("rationale", rationale),
811 ] {
812 if status == Some(TextStatus::Mismatch) {
813 return Err(AmendmentError::TextMismatch(name));
814 }
815 }
816 Ok(Some(AmendmentTextStatus {
817 basis,
818 note,
819 rationale,
820 }))
821 }
822}
823
824pub fn kind_standing(kind: AmendmentKind) -> Option<Standing> {
827 match kind {
828 AmendmentKind::NonPrecedential => Some(Standing::NonPrecedential),
829 AmendmentKind::Overruled => Some(Standing::Overruled),
830 AmendmentKind::Superseded => Some(Standing::Superseded),
831 AmendmentKind::Reinstated => Some(Standing::InForce),
832 AmendmentKind::Correction
833 | AmendmentKind::Redaction
834 | AmendmentKind::Reattested => None,
835 }
836}
837
838pub fn standing(kinds: impl IntoIterator<Item = AmendmentKind>) -> Standing {
841 kinds
842 .into_iter()
843 .filter_map(kind_standing)
844 .last()
845 .unwrap_or_default()
846}
847
848pub const BLIND_KEY: &str = "_blind";
851
852pub fn is_redactable(entry_type: GovernanceLogEntryType) -> bool {
856 !matches!(
857 entry_type,
858 GovernanceLogEntryType::Amendment | GovernanceLogEntryType::KeyRotation
859 )
860}
861
862#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
864pub enum BlindError {
865 #[error("a redactable entry's data must be a JSON object")]
866 NotAnObject,
867 #[error(
868 "data already has a {BLIND_KEY:?} key; the writer supplies it, not the caller"
869 )]
870 AlreadyBlinded,
871 #[error(
872 "{0:?} is a number that is not a 64-bit integer; governance data \
873 never contains one (put a fraction in a string)"
874 )]
875 NonIntegerNumber(String),
876}
877
878pub fn blind_data(
896 data: &serde_json::Value,
897 blind: Blind,
898) -> Result<serde_json::Value, BlindError> {
899 if let Some(pointer) = non_integer_number(data) {
900 return Err(BlindError::NonIntegerNumber(pointer));
901 }
902 let mut out = data.clone();
903 let object = out.as_object_mut().ok_or(BlindError::NotAnObject)?;
904 if object.contains_key(BLIND_KEY) {
905 return Err(BlindError::AlreadyBlinded);
906 }
907 object.insert(BLIND_KEY.to_string(), blind.to_hex().into());
908 Ok(out)
909}
910
911#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
913pub enum RedactError {
914 #[error("pointer {0:?} does not resolve in the entry's data")]
915 Unresolved(String),
916 #[error("the empty pointer would redact the whole entry")]
917 WholeEntry,
918 #[error("a redaction names at least one pointer")]
919 NoFields,
920 #[error("{0:?} is the entry's blind; every redaction replaces it already")]
921 BlindPointer(String),
922 #[error(
923 "{0:?} is a number that is not a 64-bit integer; governance data \
924 never contains one"
925 )]
926 NonIntegerNumber(String),
927}
928
929pub fn redaction_marker(amendment_id: &GovernanceLogId) -> String {
931 format!("[redacted by {amendment_id}]")
932}
933
934pub fn redact_data(
950 data: &serde_json::Value,
951 fields: &[String],
952 amendment_id: &GovernanceLogId,
953 blind: Blind,
954) -> Result<serde_json::Value, RedactError> {
955 if fields.is_empty() {
956 return Err(RedactError::NoFields);
957 }
958 if let Some(pointer) = non_integer_number(data) {
959 return Err(RedactError::NonIntegerNumber(pointer));
960 }
961 let blind_pointer = format!("/{BLIND_KEY}");
962 let marker = serde_json::Value::String(redaction_marker(amendment_id));
963 let mut out = data.clone();
964 for pointer in fields {
965 if pointer.is_empty() {
966 return Err(RedactError::WholeEntry);
967 }
968 if *pointer == blind_pointer {
969 return Err(RedactError::BlindPointer(pointer.clone()));
970 }
971 let slot = out
972 .pointer_mut(pointer)
973 .ok_or_else(|| RedactError::Unresolved(pointer.clone()))?;
974 *slot = marker.clone();
975 }
976 if let Some(object) = out.as_object_mut() {
979 object.insert(BLIND_KEY.to_string(), blind.to_hex().into());
980 }
981 Ok(out)
982}
983
984#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
986#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
987#[cfg_attr(feature = "schemars", schemars(inline))]
988pub struct AmendmentNotice {
989 pub id: GovernanceLogId,
990 pub kind: AmendmentKind,
991 #[serde(default)]
992 pub authority: Option<GovernanceLogId>,
993 pub basis: String,
994 pub note: String,
995 #[serde(default, skip_serializing_if = "Option::is_none")]
997 pub rationale: Option<String>,
998 pub created_at: DateTime<Utc>,
999}
1000
1001impl AmendmentNotice {
1002 pub fn new(
1005 id: GovernanceLogId,
1006 created_at: DateTime<Utc>,
1007 amendment: &Amendment,
1008 beside: Option<&AmendmentTexts>,
1009 ) -> Self {
1010 let beside = beside.cloned().unwrap_or_default();
1011 Self {
1012 id,
1013 kind: amendment.kind,
1014 authority: amendment.authority.clone(),
1015 basis: amendment.basis.resolve(beside.basis.as_ref()).into(),
1016 note: amendment.note.resolve(beside.note.as_ref()).into(),
1017 rationale: amendment
1018 .rationale
1019 .as_ref()
1020 .map(|r| r.resolve(beside.rationale.as_ref()).into()),
1021 created_at,
1022 }
1023 }
1024}
1025
1026pub const KEY_ROTATION_VERSION: u32 = 2;
1032
1033pub const PUBLISHED_KEYS: &[&str] =
1040 &["ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a"];
1041
1042#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1044#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1045#[cfg_attr(feature = "schemars", schemars(inline))]
1046#[serde(rename_all = "snake_case")]
1047pub enum RotationReason {
1048 Routine,
1050 Compromise,
1053}
1054
1055#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1057#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1058#[cfg_attr(feature = "schemars", schemars(inline))]
1059#[serde(deny_unknown_fields)]
1060pub struct TrustedHead {
1061 pub id: GovernanceLogId,
1062 pub chain_seq: u64,
1063 pub entry_hash: Sha256Hex,
1064}
1065
1066#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1068pub enum RotationError {
1069 #[error("agora_governance_key_rotation is {0}, not {KEY_ROTATION_VERSION}")]
1070 UnsupportedVersion(u32),
1071 #[error("new_key is not a valid Ed25519 public key")]
1072 BadNewKey,
1073 #[error(
1074 "the proof of possession does not verify for this rotation at this position"
1075 )]
1076 BadProof,
1077 #[error("a compromise certificate must name last_trusted")]
1078 MissingLastTrusted,
1079 #[error("certificate: {0}")]
1080 Certificate(#[from] CertificateError),
1081 #[error("outgoing_certificate: {0}")]
1082 OutgoingCertificate(CertificateError),
1083 #[error(
1084 "the chain's first rotation must carry the genesis key's \
1085 outgoing_certificate"
1086 )]
1087 MissingGenesisCertificate,
1088 #[error(
1089 "outgoing_certificate belongs on the chain's first rotation and \
1090 nowhere else"
1091 )]
1092 UnexpectedOutgoingCertificate,
1093 #[error("old_key is not the key that was in force")]
1094 WrongOldKey,
1095 #[error("last_trusted does not name an earlier entry of this chain")]
1096 UnknownLastTrusted,
1097 #[error("new_key has already held this chain; a key is never brought back")]
1098 ReusedKey,
1099 #[error("last_trusted names an entry an earlier compromise repudiated")]
1100 RepudiatedLastTrusted,
1101}
1102
1103#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1114#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1115#[cfg_attr(feature = "schemars", schemars(inline))]
1116#[serde(deny_unknown_fields)]
1117pub struct KeyRotation {
1118 pub agora_governance_key_rotation: u32,
1120 pub reason: RotationReason,
1121 pub old_key: PublicKeyHex,
1122 pub new_key: PublicKeyHex,
1123 pub proof: SignatureHex,
1127 pub proof_signed_at: i64,
1129 pub certificate: KeyCertificate,
1133 #[serde(default, skip_serializing_if = "Option::is_none")]
1137 pub outgoing_certificate: Option<KeyCertificate>,
1138}
1139
1140#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
1147pub struct RotationStatement {
1148 pub agora_governance_key_rotation: u32,
1150 pub reason: RotationReason,
1151 pub old_key: PublicKeyHex,
1152 pub new_key: PublicKeyHex,
1153 pub prev_hash: Option<Sha256Hex>,
1154}
1155
1156impl RotationStatement {
1157 pub fn new(
1159 reason: RotationReason,
1160 old_key: PublicKeyHex,
1161 new_key: PublicKeyHex,
1162 prev_hash: Option<Sha256Hex>,
1163 ) -> Self {
1164 Self {
1165 agora_governance_key_rotation: KEY_ROTATION_VERSION,
1166 reason,
1167 old_key,
1168 new_key,
1169 prev_hash,
1170 }
1171 }
1172
1173 pub fn preimage(&self) -> Vec<u8> {
1175 serde_json::to_vec(self).expect("a RotationStatement always serializes")
1176 }
1177
1178 pub fn hash(&self) -> Sha256Hex {
1180 Sha256Hex(Sha256::digest(self.preimage()).into())
1181 }
1182}
1183
1184impl KeyRotation {
1185 pub fn routine(
1191 old_key: PublicKeyHex,
1192 new_signing_key: &SigningKey,
1193 prev_hash: Option<Sha256Hex>,
1194 now: DateTime<Utc>,
1195 certificate: KeyCertificate,
1196 ) -> Self {
1197 Self::build(
1198 RotationReason::Routine,
1199 old_key,
1200 new_signing_key,
1201 prev_hash,
1202 now,
1203 certificate,
1204 )
1205 }
1206
1207 pub fn compromise(
1215 old_key: PublicKeyHex,
1216 new_signing_key: &SigningKey,
1217 prev_hash: Option<Sha256Hex>,
1218 now: DateTime<Utc>,
1219 certificate: KeyCertificate,
1220 ) -> Self {
1221 Self::build(
1222 RotationReason::Compromise,
1223 old_key,
1224 new_signing_key,
1225 prev_hash,
1226 now,
1227 certificate,
1228 )
1229 }
1230
1231 fn build(
1232 reason: RotationReason,
1233 old_key: PublicKeyHex,
1234 new_signing_key: &SigningKey,
1235 prev_hash: Option<Sha256Hex>,
1236 now: DateTime<Utc>,
1237 certificate: KeyCertificate,
1238 ) -> Self {
1239 let new_key = PublicKeyHex::from(&new_signing_key.verifying_key());
1240 let proof_signed_at = truncate_to_seconds(now).timestamp();
1241 let statement =
1242 RotationStatement::new(reason, old_key, new_key, prev_hash);
1243 let proof = crypto::sign(
1244 new_signing_key,
1245 statement.hash().as_bytes(),
1246 proof_signed_at,
1247 );
1248 Self {
1249 agora_governance_key_rotation: KEY_ROTATION_VERSION,
1250 reason,
1251 old_key,
1252 new_key,
1253 proof: proof.into(),
1254 proof_signed_at,
1255 certificate,
1256 outgoing_certificate: None,
1257 }
1258 }
1259
1260 pub fn with_outgoing(mut self, certificate: KeyCertificate) -> Self {
1262 self.outgoing_certificate = Some(certificate);
1263 self
1264 }
1265
1266 pub fn statement(&self, prev_hash: Option<Sha256Hex>) -> RotationStatement {
1268 RotationStatement::new(
1269 self.reason,
1270 self.old_key,
1271 self.new_key,
1272 prev_hash,
1273 )
1274 }
1275
1276 pub fn last_trusted(&self) -> Option<&TrustedHead> {
1279 self.certificate.statement.last_trusted.as_ref()
1280 }
1281
1282 pub fn expected_statement(
1288 &self,
1289 seq: u64,
1290 prev_hash: Option<Sha256Hex>,
1291 ) -> Result<KeyCertStatement, RotationError> {
1292 match self.reason {
1293 RotationReason::Routine => {
1294 Ok(KeyCertStatement::routine(self.new_key, seq, prev_hash))
1295 }
1296 RotationReason::Compromise => Ok(KeyCertStatement::compromise(
1297 self.new_key,
1298 seq,
1299 prev_hash,
1300 self.last_trusted()
1301 .cloned()
1302 .ok_or(RotationError::MissingLastTrusted)?,
1303 )),
1304 }
1305 }
1306
1307 pub fn verify_proof(
1310 &self,
1311 prev_hash: Option<Sha256Hex>,
1312 ) -> Result<(), RotationError> {
1313 if self.agora_governance_key_rotation != KEY_ROTATION_VERSION {
1314 return Err(RotationError::UnsupportedVersion(
1315 self.agora_governance_key_rotation,
1316 ));
1317 }
1318 let new_key = self
1319 .new_key
1320 .to_verifying_key()
1321 .map_err(|_| RotationError::BadNewKey)?;
1322 crypto::verify(
1323 &new_key,
1324 self.statement(prev_hash).hash().as_bytes(),
1325 self.proof_signed_at,
1326 &Signature::from(&self.proof),
1327 )
1328 .then_some(())
1329 .ok_or(RotationError::BadProof)
1330 }
1331
1332 pub fn verify_certified(
1336 &self,
1337 seq: u64,
1338 prev_hash: Option<Sha256Hex>,
1339 roots: &RootSet,
1340 ) -> Result<(), RotationError> {
1341 self.verify_proof(prev_hash)?;
1342 let expected = self.expected_statement(seq, prev_hash)?;
1343 Ok(self.certificate.verify_for(&expected, roots)?)
1344 }
1345}
1346
1347#[derive(Debug, Clone, Default, PartialEq, Eq)]
1354pub struct KeyAnchor {
1355 keys: HashSet<PublicKeyHex>,
1356}
1357
1358impl KeyAnchor {
1359 pub fn published() -> Self {
1361 PUBLISHED_KEYS
1362 .iter()
1363 .map(|k| {
1364 k.parse()
1365 .expect("PUBLISHED_KEYS are valid 32-byte hex keys")
1366 })
1367 .collect()
1368 }
1369
1370 pub fn pinned(key: PublicKeyHex) -> Self {
1372 std::iter::once(key).collect()
1373 }
1374
1375 pub fn with(mut self, key: PublicKeyHex) -> Self {
1377 self.keys.insert(key);
1378 self
1379 }
1380
1381 pub fn contains(&self, key: &PublicKeyHex) -> bool {
1382 self.keys.contains(key)
1383 }
1384
1385 pub fn is_empty(&self) -> bool {
1386 self.keys.is_empty()
1387 }
1388
1389 pub fn keys(&self) -> impl Iterator<Item = &PublicKeyHex> {
1391 self.keys.iter()
1392 }
1393}
1394
1395impl FromIterator<PublicKeyHex> for KeyAnchor {
1396 fn from_iter<I: IntoIterator<Item = PublicKeyHex>>(iter: I) -> Self {
1397 Self {
1398 keys: iter.into_iter().collect(),
1399 }
1400 }
1401}
1402
1403#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1406#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1407#[cfg_attr(feature = "schemars", schemars(inline))]
1408pub struct GovernanceKeyRecord {
1409 pub public_key: PublicKeyHex,
1410 pub from_seq: u64,
1412 #[serde(default)]
1416 pub through_seq: Option<u64>,
1417 pub status: KeyStatus,
1418 #[serde(default)]
1421 pub introduced_by: Option<GovernanceLogId>,
1422 #[serde(default)]
1424 pub retired_by: Option<GovernanceLogId>,
1425 #[serde(default)]
1429 pub certified: bool,
1430}
1431
1432#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1437#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1438#[cfg_attr(feature = "schemars", schemars(inline))]
1439pub struct GovernanceSigningKeys {
1440 pub keys: Vec<GovernanceKeyRecord>,
1442}
1443
1444#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1446#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1447#[cfg_attr(feature = "schemars", schemars(inline))]
1448pub struct EntryVerdict {
1449 pub id: GovernanceLogId,
1450 pub chain_seq: u64,
1451 pub signature_valid: bool,
1454 pub link_valid: bool,
1457 #[serde(default)]
1463 pub content_matches: Option<bool>,
1464 pub retroactive: bool,
1466 pub out_of_order: bool,
1469 #[serde(default)]
1471 pub amended_by: Vec<GovernanceLogId>,
1472 #[serde(default, skip_serializing_if = "Option::is_none")]
1475 pub signed_by: Option<PublicKeyHex>,
1476 #[serde(default)]
1479 pub redacted: bool,
1480 #[serde(default, skip_serializing_if = "Option::is_none")]
1482 pub redacted_data_hash: Option<Sha256Hex>,
1483 #[serde(default)]
1486 pub repudiated: bool,
1487 #[serde(default)]
1490 pub reattested_by: Vec<GovernanceLogId>,
1491 #[serde(default, skip_serializing_if = "Option::is_none")]
1495 pub texts: Option<AmendmentTextStatus>,
1496 #[serde(default, skip_serializing_if = "Option::is_none")]
1498 pub problem: Option<String>,
1499}
1500
1501#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1503#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
1504#[cfg_attr(feature = "schemars", schemars(inline))]
1505pub struct GovernanceVerification {
1506 pub public_key: PublicKeyHex,
1508 pub ok: bool,
1514 #[serde(default)]
1516 pub head: Option<GovernanceLogId>,
1517 pub entries: Vec<EntryVerdict>,
1519 #[serde(default)]
1521 pub keys: Vec<GovernanceKeyRecord>,
1522 #[serde(default)]
1527 pub unanchored_keys: Vec<PublicKeyHex>,
1528 #[serde(default)]
1530 pub repudiated: Vec<GovernanceLogId>,
1531}
1532
1533impl GovernanceVerification {
1534 pub fn settle(mut self) -> Self {
1536 self.ok = self.entries.iter().all(|e| {
1537 e.signature_valid
1538 && e.link_valid
1539 && e.content_matches != Some(false)
1540 && e.problem.is_none()
1541 });
1542 self
1543 }
1544
1545 pub fn check_content(
1555 &mut self,
1556 link: &GovernanceChainLink,
1557 data: &serde_json::Value,
1558 ) -> bool {
1559 let Some(entry) = self.entries.iter_mut().find(|e| e.id == link.id)
1560 else {
1561 return false;
1562 };
1563 let hash = non_integer_number(data).is_none().then(|| data_hash(data));
1565 let ok = hash.is_some_and(|hash| {
1566 hash == link.attestation.data_hash
1567 || entry.redacted_data_hash == Some(hash)
1568 });
1569 entry.content_matches = Some(ok);
1570 ok
1571 }
1572}
1573
1574pub fn attest(
1585 key: &SigningKey,
1586 envelope: &Envelope,
1587 chain_seq: u64,
1588 signed_at: DateTime<Utc>,
1589) -> GovernanceAttestation {
1590 let signed_at = truncate_to_seconds(signed_at);
1591 let entry_hash = envelope.entry_hash();
1592 let signature =
1593 crypto::sign(key, entry_hash.as_bytes(), signed_at.timestamp());
1594 GovernanceAttestation {
1595 envelope_version: envelope.agora_governance_log,
1596 chain_seq,
1597 prev_hash: envelope.prev_hash,
1598 data_hash: envelope.data_hash,
1599 entry_hash,
1600 signature: signature.into(),
1601 signed_at,
1602 retroactive: is_retroactive(envelope.created_at(), signed_at),
1603 }
1604}
1605
1606#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
1612pub enum LinkError {
1613 #[error(
1614 "envelope version {0} is not supported (this verifier knows {ENVELOPE_VERSION})"
1615 )]
1616 UnsupportedVersion(u32),
1617 #[error("entry_hash does not recompute from the envelope fields")]
1618 HashMismatch,
1619 #[error("signature does not verify under the published key")]
1620 BadSignature,
1621}
1622
1623pub fn recompute_entry_hash(link: &GovernanceChainLink) -> Sha256Hex {
1625 Envelope::new(
1626 link.id.clone(),
1627 link.entry_type,
1628 link.created_at,
1629 link.attestation.prev_hash,
1630 link.attestation.data_hash,
1631 )
1632 .entry_hash()
1633}
1634
1635pub fn verify_link(
1637 link: &GovernanceChainLink,
1638 key: &VerifyingKey,
1639) -> Result<(), LinkError> {
1640 let a = &link.attestation;
1641 if a.envelope_version != ENVELOPE_VERSION {
1642 return Err(LinkError::UnsupportedVersion(a.envelope_version));
1643 }
1644 if recompute_entry_hash(link) != a.entry_hash {
1645 return Err(LinkError::HashMismatch);
1646 }
1647 if !crypto::verify(
1648 key,
1649 a.entry_hash.as_bytes(),
1650 a.signed_at.timestamp(),
1651 &Signature::from(&a.signature),
1652 ) {
1653 return Err(LinkError::BadSignature);
1654 }
1655 Ok(())
1656}
1657
1658pub fn verify_data(
1660 link: &GovernanceChainLink,
1661 data: &serde_json::Value,
1662) -> bool {
1663 data_hash(data) == link.attestation.data_hash
1664}
1665
1666fn same_shape(written: &serde_json::Value, read: &serde_json::Value) -> bool {
1676 use serde_json::Value::{Array, Null, Object};
1677 match (written, read) {
1678 (Object(w), Object(r)) => r.iter().all(|(k, r)| match w.get(k) {
1679 Some(w) => same_shape(w, r),
1680 None => r.is_null(),
1681 }),
1682 (Array(w), Array(r)) => {
1683 w.len() == r.len() && w.iter().zip(r).all(|(w, r)| same_shape(w, r))
1684 }
1685 (_, Object(_) | Array(_)) => false,
1686 (Object(_) | Array(_), Null) => false,
1687 _ => true,
1688 }
1689}
1690
1691fn read_strictly<T>(written: &serde_json::Value) -> Result<T, String>
1693where
1694 T: Serialize + serde::de::DeserializeOwned,
1695{
1696 let read: T =
1697 serde_json::from_value(written.clone()).map_err(|e| e.to_string())?;
1698 let again = serde_json::to_value(&read).map_err(|e| e.to_string())?;
1699 if same_shape(written, &again) {
1700 Ok(read)
1701 } else {
1702 Err("an array where an object belongs, or the reverse".into())
1703 }
1704}
1705
1706pub fn links_from_json(
1713 chain: &serde_json::Value,
1714) -> Result<Vec<GovernanceChainLink>, String> {
1715 chain
1716 .as_array()
1717 .ok_or("a chain is an array of links")?
1718 .iter()
1719 .map(read_strictly)
1720 .collect()
1721}
1722
1723fn read_as_written<'de, D, T>(deserializer: D) -> Result<Option<T>, D::Error>
1725where
1726 D: serde::Deserializer<'de>,
1727 T: Serialize + serde::de::DeserializeOwned,
1728{
1729 let written = serde_json::Value::deserialize(deserializer)?;
1730 if written.is_null() {
1731 return Ok(None);
1732 }
1733 read_strictly(&written)
1734 .map(Some)
1735 .map_err(serde::de::Error::custom)
1736}
1737
1738fn prefix_problem(link: &GovernanceChainLink) -> Option<String> {
1740 let prefix = link.id.prefix();
1741 let reserved =
1742 matches!(prefix, GovernanceLogPrefix::Amd | GovernanceLogPrefix::Key);
1743 let expected = match link.entry_type {
1744 GovernanceLogEntryType::Amendment => Some(GovernanceLogPrefix::Amd),
1745 GovernanceLogEntryType::KeyRotation => Some(GovernanceLogPrefix::Key),
1746 _ => None,
1747 };
1748 match expected {
1749 Some(want) if prefix != want => Some(format!(
1750 "the id of a {} entry must be in the {want}- series, not {}",
1751 link.entry_type, link.id
1752 )),
1753 None if reserved => Some(format!(
1754 "{prefix}- ids are reserved for amendment and key_rotation \
1755 entries, but {} is a {}",
1756 link.id, link.entry_type
1757 )),
1758 _ => None,
1759 }
1760}
1761
1762fn amendment_target(
1764 amendment: &Amendment,
1765 seq: u64,
1766 seq_of: &HashMap<&str, u64>,
1767 links: &[&GovernanceChainLink],
1768) -> Result<u64, AmendmentError> {
1769 amendment.validate()?;
1770 let target = *seq_of.get(amendment.target.as_str()).ok_or_else(|| {
1771 AmendmentError::UnknownTarget(amendment.target.clone())
1772 })?;
1773 if target >= seq {
1774 return Err(AmendmentError::ForwardReference(amendment.target.clone()));
1775 }
1776 if links[target as usize - 1].attestation.entry_hash
1777 != amendment.target_entry_hash
1778 {
1779 return Err(AmendmentError::WrongTargetHash(amendment.target.clone()));
1780 }
1781 Ok(target)
1782}
1783
1784struct KeyWalk {
1786 history: Vec<(GovernanceKeyRecord, VerifyingKey)>,
1788 unanchored: Vec<PublicKeyHex>,
1789 seen: HashSet<PublicKeyHex>,
1793 repudiated: HashSet<u64>,
1795 genesis: PublicKeyHex,
1796 genesis_certified: bool,
1798}
1799
1800impl KeyWalk {
1801 fn new(genesis: &VerifyingKey, anchor: &KeyAnchor) -> Self {
1802 let public_key = PublicKeyHex::from(genesis);
1803 Self {
1804 genesis: public_key,
1805 genesis_certified: false,
1806 history: vec![(
1807 GovernanceKeyRecord {
1808 public_key,
1809 from_seq: 1,
1810 through_seq: None,
1811 status: KeyStatus::Active,
1812 introduced_by: None,
1813 retired_by: None,
1814 certified: false,
1815 },
1816 *genesis,
1817 )],
1818 unanchored: if anchor.contains(&public_key) {
1819 Vec::new()
1820 } else {
1821 vec![public_key]
1822 },
1823 seen: HashSet::from([public_key]),
1824 repudiated: HashSet::new(),
1825 }
1826 }
1827
1828 fn in_force(&self, seq: u64) -> (PublicKeyHex, VerifyingKey) {
1830 let (record, key) = self
1831 .history
1832 .iter()
1833 .rev()
1834 .find(|(r, _)| r.from_seq <= seq)
1835 .unwrap_or(&self.history[0]);
1836 (record.public_key, *key)
1837 }
1838
1839 fn active(&self) -> PublicKeyHex {
1841 self.history
1842 .last()
1843 .map(|(r, _)| r.public_key)
1844 .expect("the genesis key is always in the history")
1845 }
1846
1847 fn close(
1848 &mut self,
1849 through_seq: u64,
1850 status: KeyStatus,
1851 by: &GovernanceLogId,
1852 ) {
1853 if let Some((record, _)) = self.history.last_mut() {
1854 record.through_seq = Some(through_seq);
1855 record.status = status;
1856 record.retired_by = Some(by.clone());
1857 }
1858 }
1859
1860 fn open(
1861 &mut self,
1862 public_key: PublicKeyHex,
1863 key: VerifyingKey,
1864 from_seq: u64,
1865 by: &GovernanceLogId,
1866 ) {
1867 self.history.push((
1868 GovernanceKeyRecord {
1869 public_key,
1870 from_seq,
1871 through_seq: None,
1872 status: KeyStatus::Active,
1873 introduced_by: Some(by.clone()),
1874 retired_by: None,
1875 certified: true,
1876 },
1877 key,
1878 ));
1879 }
1880
1881 fn apply(
1883 &mut self,
1884 rotation: &KeyRotation,
1885 link: &GovernanceChainLink,
1886 seq: u64,
1887 links: &[&GovernanceChainLink],
1888 roots: &RootSet,
1889 ) -> Result<(), RotationError> {
1890 rotation.verify_certified(seq, link.attestation.prev_hash, roots)?;
1891 let new_key = rotation
1892 .new_key
1893 .to_verifying_key()
1894 .map_err(|_| RotationError::BadNewKey)?;
1895 if self.seen.contains(&rotation.new_key) {
1896 return Err(RotationError::ReusedKey);
1897 }
1898 match (&rotation.outgoing_certificate, self.genesis_certified) {
1903 (None, false) => {
1904 return Err(RotationError::MissingGenesisCertificate);
1905 }
1906 (Some(_), true) => {
1907 return Err(RotationError::UnexpectedOutgoingCertificate);
1908 }
1909 (Some(certificate), false) => certificate
1910 .verify_for(&KeyCertStatement::genesis(self.genesis), roots)
1911 .map_err(RotationError::OutgoingCertificate)?,
1912 (None, true) => {}
1913 }
1914 match rotation.reason {
1915 RotationReason::Routine => {
1916 if rotation.old_key != self.in_force(seq).0 {
1917 return Err(RotationError::WrongOldKey);
1918 }
1919 self.close(seq, KeyStatus::Retired, &link.id);
1920 self.open(rotation.new_key, new_key, seq + 1, &link.id);
1921 }
1922 RotationReason::Compromise => {
1923 let head = rotation
1924 .last_trusted()
1925 .ok_or(RotationError::MissingLastTrusted)?;
1926 let trusted_seq = head.chain_seq;
1927 let names_an_earlier_entry = trusted_seq >= 1
1928 && trusted_seq < seq
1929 && links[trusted_seq as usize - 1].id == head.id
1930 && links[trusted_seq as usize - 1].attestation.entry_hash
1931 == head.entry_hash;
1932 if !names_an_earlier_entry {
1933 return Err(RotationError::UnknownLastTrusted);
1934 }
1935 if self.repudiated.contains(&trusted_seq) {
1938 return Err(RotationError::RepudiatedLastTrusted);
1939 }
1940 if rotation.old_key != self.in_force(trusted_seq).0 {
1943 return Err(RotationError::WrongOldKey);
1944 }
1945 self.history.retain(|(r, _)| r.from_seq <= trusted_seq);
1946 self.close(trusted_seq, KeyStatus::Compromised, &link.id);
1947 self.open(rotation.new_key, new_key, seq, &link.id);
1948 self.repudiated.extend(trusted_seq + 1..seq);
1949 }
1950 }
1951 self.seen.insert(rotation.new_key);
1952 if !self.genesis_certified {
1953 self.genesis_certified = true;
1954 self.history[0].0.certified = true;
1955 self.unanchored.clear();
1956 }
1957 Ok(())
1958 }
1959}
1960
1961pub fn verify_chain(
1992 links: &[GovernanceChainLink],
1993 genesis_key: &VerifyingKey,
1994 anchor: &KeyAnchor,
1995 roots: &RootSet,
1996) -> GovernanceVerification {
1997 let mut links: Vec<&GovernanceChainLink> = links.iter().collect();
1998 links.sort_by_key(|l| l.attestation.chain_seq);
1999
2000 let mut seq_of: HashMap<&str, u64> = HashMap::new();
2001 let mut duplicates: HashSet<&str> = HashSet::new();
2002 for (i, link) in links.iter().enumerate() {
2003 if seq_of.insert(link.id.as_str(), i as u64 + 1).is_some() {
2004 duplicates.insert(link.id.as_str());
2005 }
2006 }
2007
2008 let mut walk = KeyWalk::new(genesis_key, anchor);
2009 let mut amendments: Vec<(u64, GovernanceLogId, Amendment, u64)> =
2011 Vec::new();
2012 let mut entries: Vec<EntryVerdict> = Vec::with_capacity(links.len());
2013 let mut prev: Option<&GovernanceChainLink> = None;
2014
2015 for (i, link) in links.iter().enumerate() {
2016 let a = &link.attestation;
2017 let expected_seq = i as u64 + 1;
2018 let mut problems: Vec<String> = Vec::new();
2019
2020 if let Some(problem) = prefix_problem(link) {
2021 problems.push(problem);
2022 }
2023 if duplicates.contains(link.id.as_str()) {
2024 problems.push(format!("{} appears more than once", link.id));
2025 }
2026
2027 let carries_meaning = matches!(
2031 link.entry_type,
2032 GovernanceLogEntryType::Amendment
2033 | GovernanceLogEntryType::KeyRotation
2034 );
2035 let mut amendment: Option<Amendment> = None;
2036 let mut rotation: Option<KeyRotation> = None;
2037 let mut content_matches: Option<bool> = None;
2038 match &link.data {
2039 Some(data) if non_integer_number(data).is_some() => {
2040 content_matches = Some(false);
2041 problems.push(format!(
2042 "`data` has a number that is not a 64-bit integer at {:?}; \
2043 governance data never contains one",
2044 non_integer_number(data).unwrap_or_default()
2045 ));
2046 }
2047 Some(data) => {
2048 let matched = data_hash(data) == a.data_hash;
2049 content_matches = Some(matched);
2050 if !matched {
2051 if carries_meaning {
2052 problems.push(
2053 "`data` does not hash to the attested data_hash"
2054 .into(),
2055 );
2056 }
2057 } else {
2058 match link.entry_type {
2059 GovernanceLogEntryType::Amendment => {
2060 match read_strictly(data) {
2061 Ok(v) => amendment = Some(v),
2062 Err(e) => problems.push(format!(
2063 "amendment `data` is malformed: {e}"
2064 )),
2065 }
2066 }
2067 GovernanceLogEntryType::KeyRotation => {
2068 match read_strictly(data) {
2069 Ok(v) => rotation = Some(v),
2070 Err(e) => problems.push(format!(
2071 "key_rotation `data` is malformed: {e}"
2072 )),
2073 }
2074 }
2075 _ => {}
2076 }
2077 }
2078 }
2079 None if carries_meaning => problems.push(format!(
2080 "a {} entry must carry its `data`",
2081 link.entry_type
2082 )),
2083 None => {}
2084 }
2085
2086 let declared = rotation
2090 .as_ref()
2091 .filter(|r| r.reason == RotationReason::Compromise)
2092 .map(|r| {
2093 if walk.seen.contains(&r.new_key) {
2094 return Err(RotationError::ReusedKey);
2095 }
2096 r.verify_certified(expected_seq, a.prev_hash, roots)?;
2097 r.new_key
2098 .to_verifying_key()
2099 .map_err(|_| RotationError::BadNewKey)
2100 });
2101 let (key_hex, key) = match &declared {
2102 Some(Ok(k)) => (PublicKeyHex::from(k), *k),
2103 _ => walk.in_force(expected_seq),
2104 };
2105 if let Some(Err(e)) = &declared {
2108 problems.push(e.to_string());
2109 }
2110
2111 let (hash_ok, signature_valid) = match verify_link(link, &key) {
2112 Ok(()) => (true, true),
2113 Err(LinkError::BadSignature) => {
2114 problems.push(LinkError::BadSignature.to_string());
2115 (true, false)
2116 }
2117 Err(e) => {
2118 problems.push(e.to_string());
2119 (false, false)
2120 }
2121 };
2122
2123 let mut link_valid = hash_ok;
2124 if a.chain_seq != expected_seq {
2125 link_valid = false;
2126 problems.push(format!(
2127 "chain_seq {} where {expected_seq} was expected",
2128 a.chain_seq
2129 ));
2130 }
2131 let expected_prev = prev.map(|p| p.attestation.entry_hash);
2132 if a.prev_hash != expected_prev {
2133 link_valid = false;
2134 problems.push(match (a.prev_hash, expected_prev) {
2135 (Some(_), None) => "first entry names a predecessor".into(),
2136 (None, Some(_)) => "prev_hash is null mid-chain".into(),
2137 _ => "prev_hash is not the previous entry's entry_hash".into(),
2138 });
2139 }
2140 let out_of_order = prev.is_some_and(|p| link.created_at < p.created_at);
2141
2142 let authentic = signature_valid && link_valid;
2146 if let Some(rotation) = rotation.as_ref().filter(|_| authentic)
2147 && let Err(e) =
2148 walk.apply(rotation, link, expected_seq, &links, roots)
2149 {
2150 let problem = e.to_string();
2151 if !problems.contains(&problem) {
2152 problems.push(problem);
2153 }
2154 }
2155 let mut texts = None;
2159 if let Some(amendment) = amendment
2160 .as_ref()
2161 .filter(|a| authentic && a.validate().is_ok())
2162 {
2163 match amendment.text_status(link.texts.as_ref()) {
2164 Ok(status) => texts = status,
2165 Err(e) => problems.push(e.to_string()),
2166 }
2167 } else if link.texts.as_ref().is_some_and(|t| !t.is_empty()) {
2168 problems.push(AmendmentError::UncommittedText.to_string());
2169 }
2170 if let Some(amendment) = amendment.filter(|_| authentic) {
2171 match amendment_target(&amendment, expected_seq, &seq_of, &links) {
2172 Ok(target) => amendments.push((
2173 expected_seq,
2174 link.id.clone(),
2175 amendment,
2176 target,
2177 )),
2178 Err(e) => problems.push(e.to_string()),
2179 }
2180 }
2181
2182 entries.push(EntryVerdict {
2183 id: link.id.clone(),
2184 chain_seq: a.chain_seq,
2185 signature_valid,
2186 link_valid,
2187 content_matches,
2188 retroactive: is_retroactive(link.created_at, a.signed_at),
2189 out_of_order,
2190 amended_by: Vec::new(),
2191 signed_by: Some(key_hex),
2192 redacted: false,
2193 redacted_data_hash: None,
2194 repudiated: false,
2195 reattested_by: Vec::new(),
2196 texts,
2197 problem: (!problems.is_empty()).then(|| problems.join("; ")),
2198 });
2199 prev = Some(link);
2200 }
2201
2202 let mut applied = vec![false; amendments.len()];
2206 loop {
2207 let mut changed = false;
2208 for (i, (seq, id, amendment, target)) in amendments.iter().enumerate() {
2209 if applied[i]
2210 || amendment.kind != AmendmentKind::Reattested
2211 || walk.repudiated.contains(seq)
2212 {
2213 continue;
2214 }
2215 applied[i] = true;
2216 entries[*target as usize - 1].reattested_by.push(id.clone());
2217 walk.repudiated.remove(target);
2218 changed = true;
2219 }
2220 if !changed {
2221 break;
2222 }
2223 }
2224
2225 for (seq, id, amendment, target) in &amendments {
2226 if walk.repudiated.contains(seq) {
2227 continue;
2228 }
2229 let entry = &mut entries[*target as usize - 1];
2230 entry.amended_by.push(id.clone());
2231 if let Some(redaction) = &amendment.redaction {
2232 entry.redacted = true;
2233 entry.redacted_data_hash = Some(redaction.resulting_data_hash);
2234 }
2235 }
2236
2237 for (i, link) in links.iter().enumerate() {
2239 if entries[i].content_matches == Some(false)
2240 && let (Some(data), Some(hash)) =
2241 (&link.data, entries[i].redacted_data_hash)
2242 && non_integer_number(data).is_none()
2243 && data_hash(data) == hash
2244 {
2245 entries[i].content_matches = Some(true);
2246 }
2247 }
2248
2249 let mut seen = HashSet::new();
2250 walk.unanchored.retain(|key| seen.insert(*key));
2251
2252 let mut repudiated = Vec::new();
2253 for (i, entry) in entries.iter_mut().enumerate() {
2254 if walk.repudiated.contains(&(i as u64 + 1)) {
2255 entry.repudiated = true;
2256 repudiated.push(entry.id.clone());
2257 }
2258 }
2259
2260 GovernanceVerification {
2261 public_key: walk.active(),
2262 ok: false,
2263 head: prev.map(|p| p.id.clone()),
2264 entries,
2265 keys: walk.history.into_iter().map(|(record, _)| record).collect(),
2266 unanchored_keys: walk.unanchored,
2267 repudiated,
2268 }
2269 .settle()
2270}
2271
2272#[cfg(test)]
2273mod tests {
2274 use super::*;
2275 use crate::crypto::generate_keypair;
2276 use serde_json::json;
2277
2278 pub(super) fn gov(n: u32) -> GovernanceLogId {
2279 format!("GOV-2026-{n:04}").parse().unwrap()
2280 }
2281
2282 pub(super) fn amd(n: u32) -> GovernanceLogId {
2283 format!("AMD-2026-{n:04}").parse().unwrap()
2284 }
2285
2286 pub(super) fn key_id(n: u32) -> GovernanceLogId {
2287 format!("KEY-2026-{n:04}").parse().unwrap()
2288 }
2289
2290 pub(super) fn at(secs: i64) -> DateTime<Utc> {
2291 DateTime::from_timestamp(1_700_000_000 + secs, 123_456_789).unwrap()
2292 }
2293
2294 fn anchored(key: &VerifyingKey) -> KeyAnchor {
2296 KeyAnchor::pinned(key.into())
2297 }
2298
2299 pub(super) fn resalted(draft: &AmendmentDraft, seq: u64) -> AmendmentDraft {
2302 let fix = |field: &str, t: &Option<CommittedText>| {
2303 t.as_ref().map(|t| {
2304 let salt = Sha256::digest(format!("{seq}/{field}/{}", t.text));
2305 CommittedText::with_salt(
2306 TextSalt::from(<[u8; 32]>::from(salt)),
2307 t.text.clone(),
2308 )
2309 })
2310 };
2311 let texts = AmendmentTexts {
2312 basis: fix("basis", &draft.texts.basis),
2313 note: fix("note", &draft.texts.note),
2314 rationale: fix("rationale", &draft.texts.rationale),
2315 };
2316 let commit = |t: &Option<CommittedText>| {
2317 t.as_ref().map(|t| AmendmentText::Committed(t.commitment()))
2318 };
2319 AmendmentDraft {
2320 amendment: Amendment {
2321 basis: commit(&texts.basis).unwrap(),
2322 note: commit(&texts.note).unwrap(),
2323 rationale: commit(&texts.rationale),
2324 ..draft.amendment.clone()
2325 },
2326 texts,
2327 }
2328 }
2329
2330 pub(super) fn v1(draft: AmendmentDraft) -> Amendment {
2332 let plain =
2333 |t: Option<CommittedText>| t.map(|t| AmendmentText::Plain(t.text));
2334 Amendment {
2335 agora_governance_amendment: 1,
2336 basis: plain(draft.texts.basis).unwrap(),
2337 note: plain(draft.texts.note).unwrap(),
2338 rationale: plain(draft.texts.rationale),
2339 ..draft.amendment
2340 }
2341 }
2342
2343 pub(super) fn root(n: u8) -> SigningKey {
2346 SigningKey::from_bytes(&[0xA0 + n; 32])
2347 }
2348
2349 pub(super) fn roots() -> RootSet {
2351 RootSet::new(
2352 [1, 2].map(|n| PublicKeyHex::from(&root(n).verifying_key())),
2353 1,
2354 )
2355 }
2356
2357 pub(super) fn certify(
2359 signers: &[&SigningKey],
2360 statement: KeyCertStatement,
2361 ) -> KeyCertificate {
2362 use ed25519_dalek::Signer;
2363 let message = statement.signed_bytes();
2364 signers.iter().fold(
2365 KeyCertificate::unsigned(statement),
2366 |certificate, signer| {
2367 certificate.with(RootSignature {
2368 root_key: (&signer.verifying_key()).into(),
2369 signature: signer.sign(&message).into(),
2370 })
2371 },
2372 )
2373 }
2374
2375 fn for_new_at(c: &Chain, key: &VerifyingKey) -> KeyCertificate {
2377 certify(
2378 &[&root(1)],
2379 KeyCertStatement::routine(key.into(), c.next_seq(), c.prev_hash()),
2380 )
2381 }
2382
2383 pub(super) fn link(
2384 key: &SigningKey,
2385 n: u32,
2386 prev: Option<&GovernanceChainLink>,
2387 data: &serde_json::Value,
2388 signed_at: DateTime<Utc>,
2389 ) -> GovernanceChainLink {
2390 let created_at = truncate_to_micros(at(n as i64 * 10));
2391 let envelope = Envelope::new(
2392 gov(n),
2393 GovernanceLogEntryType::CouncilDecision,
2394 created_at,
2395 prev.map(|p| p.attestation.entry_hash),
2396 data_hash(data),
2397 );
2398 let attestation = attest(
2399 key,
2400 &envelope,
2401 prev.map_or(1, |p| p.attestation.chain_seq + 1),
2402 signed_at,
2403 );
2404 GovernanceChainLink {
2405 id: gov(n),
2406 entry_type: GovernanceLogEntryType::CouncilDecision,
2407 created_at,
2408 attestation,
2409 data: None,
2410 texts: None,
2411 }
2412 }
2413
2414 pub(super) fn chain(key: &SigningKey, n: u32) -> Vec<GovernanceChainLink> {
2415 let mut out: Vec<GovernanceChainLink> = Vec::new();
2416 for i in 1..=n {
2417 let data = json!({"title": format!("Decision {i}"), "outcome": "approved"});
2418 let l = link(key, i, out.last(), &data, at(i as i64 * 10 + 1));
2419 out.push(l);
2420 }
2421 out
2422 }
2423
2424 pub(super) struct Chain {
2428 pub(super) links: Vec<GovernanceChainLink>,
2429 gov: u32,
2430 amd: u32,
2431 key: u32,
2432 genesis: Option<PublicKeyHex>,
2434 }
2435
2436 impl Chain {
2437 pub(super) fn new() -> Self {
2438 Self {
2439 links: Vec::new(),
2440 gov: 0,
2441 amd: 0,
2442 key: 0,
2443 genesis: None,
2444 }
2445 }
2446
2447 pub(super) fn prev_hash(&self) -> Option<Sha256Hex> {
2448 self.links.last().map(|l| l.attestation.entry_hash)
2449 }
2450
2451 pub(super) fn hash_at(&self, seq: usize) -> Sha256Hex {
2453 self.links[seq - 1].attestation.entry_hash
2454 }
2455
2456 pub(super) fn next_seq(&self) -> u64 {
2458 self.links.len() as u64 + 1
2459 }
2460
2461 pub(super) fn head(&self, seq: usize) -> TrustedHead {
2463 TrustedHead {
2464 id: self.links[seq - 1].id.clone(),
2465 chain_seq: seq as u64,
2466 entry_hash: self.hash_at(seq),
2467 }
2468 }
2469
2470 fn outgoing(&self, rotation: KeyRotation) -> KeyRotation {
2472 match (self.key, self.genesis) {
2473 (0, Some(genesis)) => rotation.with_outgoing(certify(
2474 &[&root(1)],
2475 KeyCertStatement::genesis(genesis),
2476 )),
2477 _ => rotation,
2478 }
2479 }
2480
2481 pub(super) fn routine(
2484 &self,
2485 old: &VerifyingKey,
2486 new: &SigningKey,
2487 ) -> KeyRotation {
2488 let statement = KeyCertStatement::routine(
2489 (&new.verifying_key()).into(),
2490 self.next_seq(),
2491 self.prev_hash(),
2492 );
2493 self.outgoing(KeyRotation::routine(
2494 old.into(),
2495 new,
2496 self.prev_hash(),
2497 at(self.next_seq() as i64 * 10 + 5),
2498 certify(&[&root(1)], statement),
2499 ))
2500 }
2501
2502 pub(super) fn compromise(
2505 &self,
2506 old: &VerifyingKey,
2507 new: &SigningKey,
2508 trusted: usize,
2509 ) -> KeyRotation {
2510 let statement = KeyCertStatement::compromise(
2511 (&new.verifying_key()).into(),
2512 self.next_seq(),
2513 self.prev_hash(),
2514 self.head(trusted),
2515 );
2516 self.outgoing(KeyRotation::compromise(
2517 old.into(),
2518 new,
2519 self.prev_hash(),
2520 at(self.next_seq() as i64 * 10 + 5),
2521 certify(&[&root(1)], statement),
2522 ))
2523 }
2524
2525 pub(super) fn next_amd(&self) -> GovernanceLogId {
2527 amd(self.amd + 1)
2528 }
2529
2530 pub(super) fn push(
2531 &mut self,
2532 signer: &SigningKey,
2533 id: GovernanceLogId,
2534 entry_type: GovernanceLogEntryType,
2535 data: serde_json::Value,
2536 carry: bool,
2537 ) -> GovernanceLogId {
2538 self.genesis
2539 .get_or_insert_with(|| (&signer.verifying_key()).into());
2540 let n = self.links.len() as i64 + 1;
2541 let created_at = truncate_to_micros(at(n * 10));
2542 let envelope = Envelope::new(
2543 id.clone(),
2544 entry_type,
2545 created_at,
2546 self.prev_hash(),
2547 data_hash(&data),
2548 );
2549 let attestation =
2550 attest(signer, &envelope, n as u64, at(n * 10 + 1));
2551 self.links.push(GovernanceChainLink {
2552 id: id.clone(),
2553 entry_type,
2554 created_at,
2555 attestation,
2556 data: carry.then_some(data),
2557 texts: None,
2558 });
2559 id
2560 }
2561
2562 pub(super) fn entry(
2565 &mut self,
2566 signer: &SigningKey,
2567 data: serde_json::Value,
2568 ) -> GovernanceLogId {
2569 self.gov += 1;
2570 let id = gov(self.gov);
2571 self.push(
2572 signer,
2573 id,
2574 GovernanceLogEntryType::CouncilDecision,
2575 data,
2576 false,
2577 )
2578 }
2579
2580 pub(super) fn decision(
2581 &mut self,
2582 signer: &SigningKey,
2583 ) -> GovernanceLogId {
2584 let data = json!({"title": format!("Decision {}", self.gov + 1)});
2585 self.entry(signer, data)
2586 }
2587
2588 pub(super) fn amend(
2590 &mut self,
2591 signer: &SigningKey,
2592 draft: &AmendmentDraft,
2593 ) -> GovernanceLogId {
2594 let draft = resalted(draft, self.next_seq());
2595 let id = self.amend_v1(signer, &draft.amendment);
2596 let link = self.links.last_mut().unwrap();
2597 link.texts = Some(draft.texts);
2598 id
2599 }
2600
2601 pub(super) fn amend_v1(
2604 &mut self,
2605 signer: &SigningKey,
2606 amendment: &Amendment,
2607 ) -> GovernanceLogId {
2608 self.amd += 1;
2609 let id = amd(self.amd);
2610 self.push(
2611 signer,
2612 id,
2613 GovernanceLogEntryType::Amendment,
2614 serde_json::to_value(amendment).unwrap(),
2615 true,
2616 )
2617 }
2618
2619 pub(super) fn rotate(
2620 &mut self,
2621 signer: &SigningKey,
2622 rotation: &KeyRotation,
2623 ) -> GovernanceLogId {
2624 self.key += 1;
2625 let id = key_id(self.key);
2626 self.push(
2627 signer,
2628 id,
2629 GovernanceLogEntryType::KeyRotation,
2630 serde_json::to_value(rotation).unwrap(),
2631 true,
2632 )
2633 }
2634 }
2635
2636 #[test]
2639 fn canonical_json_sorts_keys_at_every_level() {
2640 let v = json!({"b": {"z": 1, "a": [{"y": 2, "x": 3}]}, "a": null});
2641 assert_eq!(
2642 canonical_json(&v),
2643 br#"{"a":null,"b":{"a":[{"x":3,"y":2}],"z":1}}"#
2644 );
2645 }
2646
2647 #[test]
2648 fn canonical_json_is_compact_and_escapes_like_serde() {
2649 let v = json!({"s": "tab\there \"q\" ünïcode \u{1F600}", "n": [1, -2, 3.5, true, false]});
2650 let bytes = canonical_json(&v);
2651 let text = std::str::from_utf8(&bytes).unwrap();
2652 assert_eq!(
2653 text,
2654 r#"{"n":[1,-2,3.5,true,false],"s":"tab\there \"q\" ünïcode 😀"}"#
2655 );
2656 }
2657
2658 #[test]
2659 fn canonical_json_ignores_insertion_order() {
2660 let mut a = serde_json::Map::new();
2661 a.insert("z".into(), json!(1));
2662 a.insert("a".into(), json!(2));
2663 let mut b = serde_json::Map::new();
2664 b.insert("a".into(), json!(2));
2665 b.insert("z".into(), json!(1));
2666 assert_eq!(
2667 canonical_json(&serde_json::Value::Object(a)),
2668 canonical_json(&serde_json::Value::Object(b))
2669 );
2670 }
2671
2672 #[test]
2673 fn canonical_json_empty_containers() {
2674 assert_eq!(canonical_json(&json!({})), b"{}");
2675 assert_eq!(canonical_json(&json!([])), b"[]");
2676 assert_eq!(
2677 canonical_json(&json!({"a": {}, "b": []})),
2678 br#"{"a":{},"b":[]}"#
2679 );
2680 }
2681
2682 #[test]
2685 fn preimage_is_declaration_ordered_json() {
2686 let e = Envelope::new(
2687 gov(1),
2688 GovernanceLogEntryType::AppealsCourtDecision,
2689 at(0),
2690 None,
2691 data_hash(&json!({})),
2692 );
2693 let text = String::from_utf8(e.preimage()).unwrap();
2694 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}");
2695 }
2696
2697 #[test]
2698 fn every_envelope_field_changes_the_hash() {
2699 let base = Envelope::new(
2700 gov(1),
2701 GovernanceLogEntryType::CouncilDecision,
2702 at(0),
2703 None,
2704 data_hash(&json!({"a":1})),
2705 );
2706 let h = base.entry_hash();
2707 let mut e = base.clone();
2708 e.id = gov(2);
2709 assert_ne!(e.entry_hash(), h);
2710 let mut e = base.clone();
2711 e.entry_type = GovernanceLogEntryType::PolicyChange;
2712 assert_ne!(e.entry_hash(), h);
2713 let mut e = base.clone();
2714 e.created_at += 1;
2715 assert_ne!(e.entry_hash(), h);
2716 let mut e = base.clone();
2717 e.prev_hash = Some(h);
2718 assert_ne!(e.entry_hash(), h);
2719 let mut e = base.clone();
2720 e.data_hash = data_hash(&json!({"a":2}));
2721 assert_ne!(e.entry_hash(), h);
2722 assert_eq!(base.entry_hash(), h, "and it is deterministic");
2723 }
2724
2725 #[test]
2726 fn truncation_matches_what_the_envelope_carries() {
2727 let t = at(0);
2728 assert_eq!(truncate_to_micros(t).timestamp_subsec_nanos(), 123_456_000);
2729 assert_eq!(truncate_to_seconds(t).timestamp_subsec_nanos(), 0);
2730 assert_eq!(
2731 Envelope::new(
2732 gov(1),
2733 GovernanceLogEntryType::CouncilDecision,
2734 t,
2735 None,
2736 data_hash(&json!(null))
2737 )
2738 .created_at,
2739 truncate_to_micros(t).timestamp_micros()
2740 );
2741 }
2742
2743 #[test]
2746 fn hex_newtypes_round_trip_and_reject_wrong_lengths() {
2747 let h = data_hash(&json!(1));
2748 let s = serde_json::to_string(&h).unwrap();
2749 assert_eq!(s.len(), 66);
2750 let back: Sha256Hex = serde_json::from_str(&s).unwrap();
2751 assert_eq!(back, h);
2752 assert!(serde_json::from_str::<Sha256Hex>("\"abcd\"").is_err());
2753 assert!("zz".repeat(32).parse::<Sha256Hex>().is_err());
2754 assert!(Sha256Hex::try_from(vec![0u8; 31]).is_err());
2755 assert_eq!(format!("{h:?}"), format!("Sha256Hex({h})"));
2756 }
2757
2758 #[test]
2761 fn attest_then_verify_link() {
2762 let (key, pk) = generate_keypair();
2763 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
2764 assert_eq!(verify_link(&l, &pk), Ok(()));
2765 assert!(verify_data(&l, &json!({"a": 1})));
2766 assert!(!verify_data(&l, &json!({"a": 2})));
2767 assert!(!l.attestation.retroactive);
2768 }
2769
2770 #[test]
2771 fn wrong_key_fails_signature_only() {
2772 let (key, _) = generate_keypair();
2773 let (_, other) = generate_keypair();
2774 let l = link(&key, 1, None, &json!({}), at(5));
2775 assert_eq!(verify_link(&l, &other), Err(LinkError::BadSignature));
2776 }
2777
2778 #[test]
2779 fn tampering_with_any_attested_field_is_detected() {
2780 let (key, pk) = generate_keypair();
2781 let l = link(&key, 1, None, &json!({"a": 1}), at(5));
2782
2783 let mut t = l.clone();
2784 t.created_at += chrono::Duration::microseconds(1);
2785 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
2786
2787 let mut t = l.clone();
2788 t.entry_type = GovernanceLogEntryType::StewardVeto;
2789 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
2790
2791 let mut t = l.clone();
2792 t.attestation.data_hash = data_hash(&json!({"a": 2}));
2793 assert_eq!(verify_link(&t, &pk), Err(LinkError::HashMismatch));
2794
2795 let mut t = l.clone();
2798 t.attestation.signed_at -= chrono::Duration::seconds(1);
2799 assert_eq!(verify_link(&t, &pk), Err(LinkError::BadSignature));
2800
2801 let mut t = l.clone();
2802 t.attestation.envelope_version = 2;
2803 assert_eq!(verify_link(&t, &pk), Err(LinkError::UnsupportedVersion(2)));
2804 }
2805
2806 #[test]
2807 fn a_good_chain_verifies_in_any_input_order() {
2808 let (key, pk) = generate_keypair();
2809 let mut c = chain(&key, 4);
2810 c.reverse();
2811 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
2812 assert!(v.ok, "{v:#?}");
2813 assert_eq!(v.head, Some(gov(4)));
2814 assert_eq!(
2815 v.entries.iter().map(|e| e.chain_seq).collect::<Vec<_>>(),
2816 [1, 2, 3, 4]
2817 );
2818 assert!(v.entries.iter().all(|e| e.content_matches.is_none()
2819 && e.problem.is_none()
2820 && !e.out_of_order));
2821 assert_eq!(v.public_key, PublicKeyHex::from(&pk));
2822 }
2823
2824 #[test]
2825 fn empty_chain_is_ok_with_no_head() {
2826 let (_, pk) = generate_keypair();
2827 let v = verify_chain(&[], &pk, &anchored(&pk), &roots());
2828 assert!(v.ok);
2829 assert!(v.head.is_none());
2830 assert!(v.entries.is_empty());
2831 }
2832
2833 #[test]
2834 fn a_changed_entry_breaks_its_signature_and_the_next_link() {
2835 let (key, pk) = generate_keypair();
2836 let mut c = chain(&key, 3);
2837 let rewritten = link(
2840 &key,
2841 2,
2842 Some(&c[0]),
2843 &json!({"title": "Decision 2", "outcome": "REJECTED"}),
2844 at(21),
2845 );
2846 c[1] = rewritten;
2847 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
2848 assert!(!v.ok);
2849 assert!(
2850 v.entries[1].signature_valid && v.entries[1].link_valid,
2851 "the rewrite itself is well-formed: {:#?}",
2852 v.entries[1]
2853 );
2854 assert!(
2855 !v.entries[2].link_valid,
2856 "but entry 3 no longer points at it: {:#?}",
2857 v.entries[2]
2858 );
2859 assert!(
2860 v.entries[2]
2861 .problem
2862 .as_deref()
2863 .unwrap()
2864 .contains("prev_hash")
2865 );
2866 }
2867
2868 #[test]
2869 fn a_removed_entry_is_a_gap_and_a_broken_link() {
2870 let (key, pk) = generate_keypair();
2871 let mut c = chain(&key, 3);
2872 c.remove(1);
2873 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
2874 assert!(!v.ok);
2875 assert!(v.entries[0].link_valid);
2876 let p = v.entries[1].problem.as_deref().unwrap();
2877 assert!(p.contains("chain_seq 3 where 2 was expected"), "{p}");
2878 assert!(p.contains("prev_hash"), "{p}");
2879 }
2880
2881 #[test]
2882 fn a_second_genesis_is_rejected() {
2883 let (key, pk) = generate_keypair();
2884 let mut c = chain(&key, 2);
2885 let rogue = link(&key, 2, None, &json!({}), at(21));
2886 c[1] = rogue;
2887 let v = verify_chain(&c, &pk, &anchored(&pk), &roots());
2888 assert!(!v.ok);
2889 assert!(
2890 v.entries[1]
2891 .problem
2892 .as_deref()
2893 .unwrap()
2894 .contains("null mid-chain")
2895 );
2896 }
2897
2898 #[test]
2899 fn retroactive_and_out_of_order_are_recomputed_not_copied() {
2900 let (key, pk) = generate_keypair();
2901 let first = link(&key, 1, None, &json!({}), at(10 + 3600));
2902 let created = truncate_to_micros(at(5));
2905 let envelope = Envelope::new(
2906 gov(2),
2907 GovernanceLogEntryType::CouncilDecision,
2908 created,
2909 Some(first.attestation.entry_hash),
2910 data_hash(&json!({})),
2911 );
2912 let attestation = attest(&key, &envelope, 2, at(6));
2913 let mut second = GovernanceChainLink {
2914 id: gov(2),
2915 entry_type: GovernanceLogEntryType::CouncilDecision,
2916 created_at: created,
2917 attestation,
2918 data: None,
2919 texts: None,
2920 };
2921 second.attestation.retroactive = true; let v = verify_chain(&[first, second], &pk, &anchored(&pk), &roots());
2923 assert!(v.ok, "{v:#?}");
2924 assert!(v.entries[0].retroactive);
2925 assert!(!v.entries[1].retroactive, "recomputed from timestamps");
2926 assert!(v.entries[1].out_of_order);
2927 }
2928
2929 #[test]
2930 fn content_mismatch_settles_to_not_ok() {
2931 let (key, pk) = generate_keypair();
2932 let c = chain(&key, 1);
2933 let mut v = verify_chain(&c, &pk, &anchored(&pk), &roots());
2934 v.entries[0].content_matches = Some(true);
2935 assert!(v.clone().settle().ok);
2936 v.entries[0].content_matches = Some(false);
2937 assert!(!v.settle().ok);
2938 }
2939
2940 #[test]
2943 fn an_amendment_fills_amended_by_on_its_target() {
2944 let (key, pk) = generate_keypair();
2945 let mut c = Chain::new();
2946 let target = c.decision(&key);
2947 c.decision(&key);
2948 let amendment = AmendmentDraft::new(
2949 target,
2950 c.hash_at(1),
2951 AmendmentKind::NonPrecedential,
2952 "§1 (Red Team Cases Recharacterized)",
2953 "diagnostic finding — not citable as moderation precedent",
2954 )
2955 .unwrap()
2956 .with_authority(gov(5))
2957 .with_rationale("§5 leaves the ruling itself standing");
2958 let id = c.amend(&key, &amendment);
2959
2960 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
2961 assert!(v.ok, "{v:#?}");
2962 assert_eq!(v.entries[0].amended_by, vec![id]);
2963 assert!(v.entries[1].amended_by.is_empty());
2964 assert!(!v.entries[0].redacted);
2965 assert_eq!(v.head, Some(amd(1)));
2966 assert_eq!(v.entries[2].content_matches, Some(true));
2968 assert_eq!(v.entries[0].content_matches, None);
2969 assert_eq!(
2970 standing([amendment.amendment.kind]),
2971 Standing::NonPrecedential
2972 );
2973 }
2974
2975 #[test]
2976 fn an_amendment_must_name_an_earlier_entry_by_its_exact_hash() {
2977 let (key, pk) = generate_keypair();
2978 let problem = |c: &Chain, at: usize| -> String {
2979 verify_chain(&c.links, &pk, &anchored(&pk), &roots()).entries[at]
2980 .problem
2981 .clone()
2982 .unwrap_or_default()
2983 };
2984
2985 let mut c = Chain::new();
2986 c.decision(&key);
2987 let unknown = AmendmentDraft::new(
2988 gov(99),
2989 c.hash_at(1),
2990 AmendmentKind::Overruled,
2991 "b",
2992 "n",
2993 )
2994 .unwrap();
2995 c.amend(&key, &unknown);
2996 assert!(
2997 problem(&c, 1).contains("is not an entry of this chain"),
2998 "{}",
2999 problem(&c, 1)
3000 );
3001 assert!(!verify_chain(&c.links, &pk, &anchored(&pk), &roots()).ok);
3002
3003 let mut c = Chain::new();
3005 c.decision(&key);
3006 let forward = AmendmentDraft::new(
3007 gov(2),
3008 c.hash_at(1),
3009 AmendmentKind::Overruled,
3010 "b",
3011 "n",
3012 )
3013 .unwrap();
3014 c.amend(&key, &forward);
3015 c.decision(&key);
3016 assert!(
3017 problem(&c, 1).contains("not an earlier entry"),
3018 "{}",
3019 problem(&c, 1)
3020 );
3021
3022 let mut c = Chain::new();
3024 let target = c.decision(&key);
3025 let wrong_hash = AmendmentDraft::new(
3026 target,
3027 data_hash(&json!("some other entry")),
3028 AmendmentKind::Overruled,
3029 "b",
3030 "n",
3031 )
3032 .unwrap();
3033 c.amend(&key, &wrong_hash);
3034 assert!(problem(&c, 1).contains("entry_hash"), "{}", problem(&c, 1));
3035 assert!(
3036 verify_chain(&c.links, &pk, &anchored(&pk), &roots()).entries[0]
3037 .amended_by
3038 .is_empty()
3039 );
3040 }
3041
3042 #[test]
3043 fn redaction_shape_violations_fail_verification() {
3044 let (key, pk) = generate_keypair();
3045 let amend_with = |mutate: &dyn Fn(&mut Amendment)| -> String {
3046 let mut c = Chain::new();
3047 let target = c.decision(&key);
3048 let mut amendment = AmendmentDraft::new(
3049 target,
3050 c.hash_at(1),
3051 AmendmentKind::Correction,
3052 "b",
3053 "n",
3054 )
3055 .unwrap();
3056 mutate(&mut amendment.amendment);
3057 c.amend_v1(&key, &amendment.amendment);
3058 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3059 assert!(!v.ok, "{v:#?}");
3060 v.entries[1].problem.clone().unwrap_or_default()
3061 };
3062
3063 assert_eq!(
3066 AmendmentDraft::new(
3067 gov(1),
3068 data_hash(&json!(null)),
3069 AmendmentKind::Redaction,
3070 "b",
3071 "n"
3072 ),
3073 Err(AmendmentError::MissingRedaction)
3074 );
3075 let p = amend_with(&|a| a.kind = AmendmentKind::Redaction);
3076 assert!(p.contains("requires a `redaction`"), "{p}");
3077
3078 let p = amend_with(&|a| {
3079 a.redaction = Some(Redaction {
3080 fields: vec!["/x".into()],
3081 resulting_data_hash: data_hash(&json!({})),
3082 })
3083 });
3084 assert!(p.contains("only valid on kind"), "{p}");
3085
3086 let p = amend_with(&|a| a.agora_governance_amendment = 3);
3087 assert!(p.contains("agora_governance_amendment is 3"), "{p}");
3088
3089 let p = amend_with(&|a| a.agora_governance_amendment = 1);
3092 assert!(p.contains("does neither consistently"), "{p}");
3093 let p = amend_with(&|a| a.note = AmendmentText::Plain("n".into()));
3094 assert!(p.contains("does neither consistently"), "{p}");
3095 }
3096
3097 #[test]
3098 fn governance_data_holds_no_number_that_is_not_a_64_bit_integer() {
3099 let fine = json!({"a": [1, -2, u64::MAX, i64::MIN], "b": {"c": "0.5"}});
3100 assert_eq!(non_integer_number(&fine), None);
3101 assert!(blind_data(&fine, Blind::random()).is_ok());
3102
3103 for (text, pointer) in [
3104 (r#"{"a": {"b/c": [1, 0.5]}}"#, "/a/b~1c/1"),
3105 (r#"{"n": 1.0}"#, "/n"),
3106 (r#"{"n": 1e3}"#, "/n"),
3107 (r#"{"n": 18446744073709551616}"#, "/n"),
3108 (r#"{"n": -9223372036854775809}"#, "/n"),
3109 ] {
3110 let data: serde_json::Value = serde_json::from_str(text).unwrap();
3111 assert_eq!(non_integer_number(&data).as_deref(), Some(pointer));
3112 assert_eq!(
3113 blind_data(&data, Blind::random()),
3114 Err(BlindError::NonIntegerNumber(pointer.into())),
3115 "the writer refuses it"
3116 );
3117 assert_eq!(
3118 redact_data(&data, &["/n".into()], &amd(1), Blind::random()),
3119 Err(RedactError::NonIntegerNumber(pointer.into()))
3120 );
3121 }
3122 }
3123
3124 #[test]
3125 fn standing_is_the_last_amendment_that_changes_it() {
3126 use AmendmentKind::*;
3127 assert_eq!(standing([]), Standing::InForce);
3128 assert_eq!(standing([Correction, Redaction]), Standing::InForce);
3129 assert_eq!(
3130 standing([Correction, NonPrecedential, Redaction]),
3131 Standing::NonPrecedential
3132 );
3133 assert_eq!(standing([Overruled, Reinstated]), Standing::InForce);
3134 assert_eq!(standing([Reinstated, Superseded]), Standing::Superseded);
3135 assert_eq!(kind_standing(Reattested), None);
3136 assert_eq!(kind_standing(Reinstated), Some(Standing::InForce));
3137 }
3138
3139 #[test]
3140 fn a_redaction_verifies_against_the_amendment_and_nothing_else() {
3141 let (key, pk) = generate_keypair();
3142 let data = json!({
3143 "finding": "upheld",
3144 "subject": {"handle": "someone", "detail": "personal"},
3145 });
3146 let mut c = Chain::new();
3147 let target = c.entry(&key, data.clone());
3148 let amendment_id = c.next_amd();
3149 let (amendment, redacted) = AmendmentDraft::redaction(
3150 &amendment_id,
3151 target,
3152 c.hash_at(1),
3153 "GDPR Art. 17(1)(a)",
3154 "personal data removed on request",
3155 vec!["/subject/handle".into(), "/subject/detail".into()],
3156 &data,
3157 Blind::from([7; 32]),
3158 )
3159 .unwrap();
3160 assert_eq!(c.amend(&key, &amendment), amendment_id);
3161 assert_eq!(redacted[BLIND_KEY], json!(Blind::from([7; 32])));
3162
3163 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3164 assert!(v.ok, "{v:#?}");
3165 assert!(v.entries[0].redacted);
3166 assert_eq!(v.entries[0].redacted_data_hash, Some(data_hash(&redacted)));
3167 assert_eq!(
3168 redacted["subject"]["handle"],
3169 json!(format!("[redacted by {amendment_id}]"))
3170 );
3171 assert_eq!(redacted["finding"], json!("upheld"), "and nothing else");
3172
3173 assert!(v.check_content(&c.links[0], &redacted));
3175 assert_eq!(v.entries[0].content_matches, Some(true));
3176 assert!(v.clone().settle().ok);
3177 assert!(v.check_content(&c.links[0], &data));
3179
3180 let mut tampered = redacted.clone();
3182 tampered["finding"] = json!("overturned");
3183 assert!(!v.check_content(&c.links[0], &tampered));
3184 assert_eq!(v.entries[0].content_matches, Some(false));
3185 assert!(!v.settle().ok);
3186 }
3187
3188 #[test]
3189 fn content_that_matches_nothing_fails_without_an_amendment() {
3190 let (key, pk) = generate_keypair();
3191 let mut c = Chain::new();
3192 c.entry(&key, json!({"a": 1}));
3193 let mut v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3194 assert!(!v.check_content(&c.links[0], &json!({"a": 2})));
3195 assert!(!v.clone().settle().ok);
3196 let other = link(&key, 9, None, &json!({}), at(9));
3198 assert!(!v.check_content(&other, &json!({})));
3199 }
3200
3201 #[test]
3202 fn tampering_with_an_amendments_data_is_caught_by_the_data_hash() {
3203 let (key, pk) = generate_keypair();
3204 let mut c = Chain::new();
3205 let target = c.decision(&key);
3206 let amendment = AmendmentDraft::new(
3207 target,
3208 c.hash_at(1),
3209 AmendmentKind::Overruled,
3210 "b",
3211 "overruled by a later decision",
3212 )
3213 .unwrap();
3214 c.amend(&key, &amendment);
3215 c.links[1].data.as_mut().unwrap()["note"] =
3216 json!("reinstated, actually");
3217
3218 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3219 assert!(!v.ok, "{v:#?}");
3220 let p = v.entries[1].problem.as_deref().unwrap();
3221 assert!(p.contains("does not hash to the attested data_hash"), "{p}");
3222 assert!(
3223 v.entries[0].amended_by.is_empty(),
3224 "an unreadable amendment has no effect"
3225 );
3226 assert!(
3227 v.entries[1].signature_valid && v.entries[1].link_valid,
3228 "the envelope is untouched — only the content is not what it \
3229 committed to"
3230 );
3231 }
3232
3233 #[test]
3234 fn an_amendment_or_rotation_must_carry_its_data() {
3235 let (key, pk) = generate_keypair();
3236 let mut c = Chain::new();
3237 let target = c.decision(&key);
3238 let amendment = AmendmentDraft::new(
3239 target,
3240 c.hash_at(1),
3241 AmendmentKind::Correction,
3242 "b",
3243 "n",
3244 )
3245 .unwrap();
3246 c.amend(&key, &amendment);
3247 let rotation = c.routine(&pk, &generate_keypair().0);
3248 c.rotate(&key, &rotation);
3249 c.links[1].data = None;
3250 c.links[2].data = None;
3251
3252 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3253 assert!(!v.ok, "{v:#?}");
3254 for (i, entry_type) in [(1, "amendment"), (2, "key_rotation")] {
3255 let p = v.entries[i].problem.as_deref().unwrap();
3256 assert!(p.contains(&format!("a {entry_type} entry")), "{p}");
3257 assert!(p.contains("must carry its `data`"), "{p}");
3258 }
3259 }
3260
3261 #[test]
3262 fn the_id_series_must_match_the_entry_type() {
3263 let (key, pk) = generate_keypair();
3264 let mut c = Chain::new();
3265 let target = c.decision(&key);
3266 let amendment = AmendmentDraft::new(
3267 target,
3268 c.hash_at(1),
3269 AmendmentKind::Correction,
3270 "b",
3271 "n",
3272 )
3273 .unwrap();
3274 c.push(
3275 &key,
3276 gov(7),
3277 GovernanceLogEntryType::Amendment,
3278 serde_json::to_value(&amendment.amendment).unwrap(),
3279 true,
3280 );
3281 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3282 assert!(!v.ok, "{v:#?}");
3283 let p = v.entries[1].problem.as_deref().unwrap();
3284 assert!(p.contains("must be in the AMD- series"), "{p}");
3285
3286 let mut c = Chain::new();
3287 c.push(
3288 &key,
3289 key_id(1),
3290 GovernanceLogEntryType::CouncilDecision,
3291 json!({}),
3292 false,
3293 );
3294 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3295 let p = v.entries[0].problem.as_deref().unwrap();
3296 assert!(p.contains("reserved"), "{p}");
3297 }
3298
3299 #[test]
3300 fn redact_data_replaces_whole_values_and_refuses_the_rest() {
3301 let id = amd(3);
3302 let blind = Blind::from([9; 32]);
3303 let data = json!({"a": {"b": [1, {"c": "secret"}]}, "d/e": "slash"});
3304 let out = redact_data(
3305 &data,
3306 &["/a/b/1/c".into(), "/d~1e".into()],
3307 &id,
3308 blind,
3309 )
3310 .unwrap();
3311 assert_eq!(out["a"]["b"][1]["c"], json!(redaction_marker(&id)));
3312 assert_eq!(out["d/e"], json!(redaction_marker(&id)));
3313 assert_eq!(out["a"]["b"][0], json!(1), "untouched");
3314 assert_eq!(out[BLIND_KEY], json!(blind), "a legacy entry gains one");
3315
3316 assert_eq!(
3317 redact_data(&data, &["/a/nope".into()], &id, blind),
3318 Err(RedactError::Unresolved("/a/nope".into()))
3319 );
3320 assert_eq!(
3321 redact_data(&data, &["".into()], &id, blind),
3322 Err(RedactError::WholeEntry)
3323 );
3324 assert_eq!(
3325 redact_data(&data, &[], &id, blind),
3326 Err(RedactError::NoFields)
3327 );
3328 assert_eq!(
3329 redact_data(&data, &["/_blind".into()], &id, blind),
3330 Err(RedactError::BlindPointer("/_blind".into()))
3331 );
3332 }
3333
3334 #[test]
3335 fn blind_data_is_for_objects_and_is_the_writers_to_supply() {
3336 let blind = Blind::from([1; 32]);
3337 let out = blind_data(&json!({"finding": "upheld"}), blind).unwrap();
3338 assert_eq!(out, json!({"finding": "upheld", "_blind": blind}));
3339 assert_eq!(
3340 blind_data(&json!([1]), blind),
3341 Err(BlindError::NotAnObject)
3342 );
3343 assert_eq!(blind_data(&out, blind), Err(BlindError::AlreadyBlinded));
3344 assert_ne!(Blind::random(), Blind::random());
3345
3346 use GovernanceLogEntryType::*;
3347 assert!(!is_redactable(Amendment) && !is_redactable(KeyRotation));
3348 assert!(
3349 is_redactable(CouncilDecision) && is_redactable(EmergencyAction)
3350 );
3351 }
3352
3353 #[test]
3357 fn a_removed_value_cannot_be_confirmed_by_guessing_it() {
3358 fn confirms(
3360 public: &serde_json::Value,
3361 pointer: &str,
3362 guess: &str,
3363 hash: Sha256Hex,
3364 ) -> bool {
3365 let mut attempt = public.clone();
3366 *attempt.pointer_mut(pointer).unwrap() = json!(guess);
3367 data_hash(&attempt) == hash
3368 }
3369 let legacy = json!({"finding": "upheld", "handle": "someone", "city": "Utrecht"});
3370
3371 let written = blind_data(&legacy, Blind::random()).unwrap();
3373 let first = redact_data(
3374 &written,
3375 &["/handle".into()],
3376 &amd(1),
3377 Blind::random(),
3378 )
3379 .unwrap();
3380 assert!(!confirms(&first, "/handle", "someone", data_hash(&written)));
3381
3382 let second =
3385 redact_data(&first, &["/city".into()], &amd(2), Blind::random())
3386 .unwrap();
3387 assert!(!confirms(&second, "/city", "Utrecht", data_hash(&first)));
3388
3389 let mut stripped =
3393 redact_data(&legacy, &["/handle".into()], &amd(3), Blind::random())
3394 .unwrap();
3395 let legacy_first = stripped.clone();
3396 stripped.as_object_mut().unwrap().remove(BLIND_KEY);
3397 assert!(confirms(
3398 &stripped,
3399 "/handle",
3400 "someone",
3401 data_hash(&legacy)
3402 ));
3403 let legacy_second = redact_data(
3405 &legacy_first,
3406 &["/city".into()],
3407 &amd(4),
3408 Blind::random(),
3409 )
3410 .unwrap();
3411 assert!(!confirms(
3412 &legacy_second,
3413 "/city",
3414 "Utrecht",
3415 data_hash(&legacy_first)
3416 ));
3417 }
3418
3419 #[test]
3422 fn a_routine_rotation_moves_the_chain_to_the_new_key() {
3423 let (old, old_pk) = generate_keypair();
3424 let (new, new_pk) = generate_keypair();
3425 let mut c = Chain::new();
3426 c.decision(&old);
3427 let rotation = c.routine(&old_pk, &new);
3428 let rotation_id = c.rotate(&old, &rotation);
3429 c.decision(&new);
3430
3431 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3434 assert!(v.ok, "{v:#?}");
3435 assert_eq!(v.public_key, (&new_pk).into());
3436 assert_eq!(
3437 v.entries[1].signed_by,
3438 Some((&old_pk).into()),
3439 "the rotation itself is signed by the old key"
3440 );
3441 assert_eq!(v.entries[2].signed_by, Some((&new_pk).into()));
3442 assert!(v.unanchored_keys.is_empty());
3443 assert!(v.repudiated.is_empty());
3444 assert_eq!(v.keys.len(), 2);
3445 assert_eq!(v.keys[0].public_key, (&old_pk).into());
3446 assert_eq!(v.keys[0].from_seq, 1);
3447 assert_eq!(v.keys[0].through_seq, Some(2));
3448 assert_eq!(v.keys[0].status, KeyStatus::Retired);
3449 assert_eq!(v.keys[0].introduced_by, None);
3450 assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
3451 assert!(v.keys[0].certified, "retroactively, by the rotation");
3452 assert_eq!(v.keys[1].from_seq, 3);
3453 assert_eq!(v.keys[1].through_seq, None);
3454 assert_eq!(v.keys[1].status, KeyStatus::Active);
3455 assert_eq!(v.keys[1].introduced_by.as_ref(), Some(&rotation_id));
3456 assert!(v.keys[1].certified);
3457 }
3458
3459 #[test]
3460 fn the_old_key_cannot_sign_after_a_routine_rotation() {
3461 let (old, old_pk) = generate_keypair();
3462 let (new, _) = generate_keypair();
3463 let mut c = Chain::new();
3464 c.decision(&old);
3465 let rotation = c.routine(&old_pk, &new);
3466 c.rotate(&old, &rotation);
3467 c.decision(&old);
3468
3469 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3470 assert!(!v.ok, "{v:#?}");
3471 assert!(!v.entries[2].signature_valid);
3472 assert!(
3473 v.entries[2].link_valid,
3474 "the linkage is fine; the key is not"
3475 );
3476 }
3477
3478 #[test]
3481 fn the_online_key_cannot_certify_its_own_successor() {
3482 let (old, old_pk) = generate_keypair();
3483 let (thief, _) = generate_keypair();
3484 let mut c = Chain::new();
3485 c.decision(&old);
3486 let mut rotation = c.routine(&old_pk, &thief);
3487 let statement = rotation.certificate.statement.clone();
3489 rotation.certificate = certify(&[&old], statement);
3490 c.rotate(&old, &rotation);
3491 c.decision(&thief);
3492
3493 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3494 assert!(!v.ok, "{v:#?}");
3495 let p = v.entries[1].problem.as_deref().unwrap();
3496 assert!(p.contains("0 valid root signature"), "{p}");
3497 assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
3498 assert!(!v.entries[2].signature_valid);
3499 assert_eq!(v.keys.len(), 1);
3500 }
3501
3502 #[test]
3503 fn a_certificate_counts_distinct_known_roots_only() {
3504 let (old, old_pk) = generate_keypair();
3505 let (new, _) = generate_keypair();
3506 let (stranger, _) = generate_keypair();
3507 let two_of_two = RootSet::new(
3508 [
3509 (&root(1).verifying_key()).into(),
3510 (&root(2).verifying_key()).into(),
3511 ],
3512 2,
3513 );
3514 let verdict = |signers: &[&SigningKey], roots: &RootSet| {
3515 let mut c = Chain::new();
3516 c.decision(&old);
3517 let mut rotation = c.routine(&old_pk, &new);
3518 let statement = rotation.certificate.statement.clone();
3519 rotation.certificate = certify(signers, statement);
3520 let genesis = KeyCertStatement::genesis((&old_pk).into());
3522 rotation.outgoing_certificate =
3523 Some(certify(&[&root(1), &root(2)], genesis));
3524 c.rotate(&old, &rotation);
3525 verify_chain(&c.links, &old_pk, &anchored(&old_pk), roots)
3526 };
3527
3528 assert!(verdict(&[&root(1), &root(2)], &two_of_two).ok);
3529 assert!(verdict(&[&root(2)], &roots()).ok, "either root, 1-of-2");
3530
3531 for (signers, why) in [
3532 (vec![&root(1)], "below the threshold"),
3533 (vec![&root(1), &root(1)], "one root twice is one root"),
3534 (vec![&root(1), &stranger], "an unknown root is nobody"),
3535 (vec![], "unsigned"),
3536 ] {
3537 let v = verdict(&signers, &two_of_two);
3538 assert!(!v.ok, "{why}: {v:#?}");
3539 let p = v.entries[1].problem.as_deref().unwrap();
3540 assert!(p.contains("where 2 are needed"), "{why}: {p}");
3541 }
3542
3543 assert!(verdict(&[&stranger, &root(1)], &roots()).ok);
3545 }
3546
3547 #[test]
3548 fn a_certificate_is_good_for_one_statement_at_one_position() {
3549 let (old, old_pk) = generate_keypair();
3550 let (new, new_pk) = generate_keypair();
3551 let (other, other_pk) = generate_keypair();
3552 let anchor = anchored(&old_pk);
3553
3554 let mut c = Chain::new();
3558 c.decision(&old);
3559 let early = c.routine(&old_pk, &new);
3560 c.decision(&old);
3561 let mut rotation = c.routine(&old_pk, &new);
3562 rotation.certificate = early.certificate;
3563 c.rotate(&old, &rotation);
3564 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
3565 assert!(!v.ok, "{v:#?}");
3566 let p = v.entries[2].problem.as_deref().unwrap();
3567 assert!(
3568 p.contains("different key, purpose or chain position"),
3569 "{p}"
3570 );
3571 assert_eq!(v.public_key, (&old_pk).into());
3572
3573 let mut c = Chain::new();
3575 c.decision(&old);
3576 let for_new = c.routine(&old_pk, &new);
3577 let mut rotation = c.routine(&old_pk, &other);
3578 rotation.certificate = for_new.certificate;
3579 c.rotate(&old, &rotation);
3580 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
3581 let p = v.entries[1].problem.as_deref().unwrap();
3582 assert!(
3583 p.contains("different key, purpose or chain position"),
3584 "{p}"
3585 );
3586
3587 let mut c = Chain::new();
3589 c.decision(&old);
3590 let mut rotation = c.routine(&old_pk, &other);
3591 rotation.certificate = for_new_at(&c, &new_pk);
3592 rotation.certificate.statement.key = (&other_pk).into();
3593 c.rotate(&old, &rotation);
3594 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
3595 let p = v.entries[1].problem.as_deref().unwrap();
3596 assert!(p.contains("0 valid root signature"), "{p}");
3597
3598 let mut c = Chain::new();
3600 c.decision(&old);
3601 c.decision(&old);
3602 let mut rotation = c.compromise(&old_pk, &new, 1);
3603 rotation.certificate = for_new_at(&c, &new_pk);
3604 c.rotate(&new, &rotation);
3605 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
3606 assert!(!v.ok);
3607 assert!(v.repudiated.is_empty(), "{v:#?}");
3608 assert_eq!(v.public_key, (&old_pk).into());
3609 }
3610
3611 #[test]
3614 fn a_root_signature_without_the_domain_prefix_certifies_nothing() {
3615 use ed25519_dalek::Signer;
3616 let (old, old_pk) = generate_keypair();
3617 let (new, _) = generate_keypair();
3618 let mut c = Chain::new();
3619 c.decision(&old);
3620 let mut rotation = c.routine(&old_pk, &new);
3621 let statement = rotation.certificate.statement.clone();
3622 let bare = canonical_json(&serde_json::to_value(&statement).unwrap());
3623 assert_eq!(
3624 statement.signed_bytes(),
3625 [ROOT_DOMAIN, bare.as_slice()].concat()
3626 );
3627 rotation.certificate =
3628 KeyCertificate::unsigned(statement).with(RootSignature {
3629 root_key: (&root(1).verifying_key()).into(),
3630 signature: root(1).sign(&bare).into(),
3631 });
3632 c.rotate(&old, &rotation);
3633 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3634 assert!(!v.ok, "{v:#?}");
3635 assert_eq!(v.public_key, (&old_pk).into());
3636 }
3637
3638 #[test]
3639 fn the_first_rotation_carries_the_genesis_certificate_and_only_it_does() {
3640 let (k1, k1_pk) = generate_keypair();
3641 let (k2, k2_pk) = generate_keypair();
3642 let (k3, _) = generate_keypair();
3643 let genesis =
3644 || certify(&[&root(1)], KeyCertStatement::genesis((&k1_pk).into()));
3645
3646 let mut c = Chain::new();
3648 c.decision(&k1);
3649 let mut rotation = c.routine(&k1_pk, &k2);
3650 rotation.outgoing_certificate = None;
3651 c.rotate(&k1, &rotation);
3652 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3653 assert!(!v.ok);
3654 let p = v.entries[1].problem.as_deref().unwrap();
3655 assert!(p.contains("genesis key's outgoing_certificate"), "{p}");
3656 assert_eq!(v.public_key, (&k1_pk).into());
3657
3658 let mut c = Chain::new();
3660 c.decision(&k1);
3661 let rotation = c.routine(&k1_pk, &k2).with_outgoing(certify(
3662 &[&root(1)],
3663 KeyCertStatement::genesis((&k2_pk).into()),
3664 ));
3665 c.rotate(&k1, &rotation);
3666 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3667 let p = v.entries[1].problem.as_deref().unwrap();
3668 assert!(p.starts_with("outgoing_certificate:"), "{p}");
3669
3670 let mut c = Chain::new();
3673 c.decision(&k1);
3674 let rotation = c.routine(&k1_pk, &k2);
3675 assert_eq!(rotation.outgoing_certificate, Some(genesis()));
3676 c.rotate(&k1, &rotation);
3677 let v = verify_chain(&c.links, &k1_pk, &KeyAnchor::default(), &roots());
3678 assert!(v.ok, "{v:#?}");
3679 assert!(v.unanchored_keys.is_empty(), "{v:#?}");
3680
3681 let again = c.routine(&k2_pk, &k3).with_outgoing(genesis());
3683 c.rotate(&k2, &again);
3684 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3685 assert!(!v.ok);
3686 let p = v.entries[2].problem.as_deref().unwrap();
3687 assert!(p.contains("nowhere else"), "{p}");
3688 }
3689
3690 #[test]
3691 fn a_forged_proof_of_possession_is_rejected() {
3692 let (old, old_pk) = generate_keypair();
3693 let (_, new_pk) = generate_keypair();
3694 let (thief, _) = generate_keypair();
3695 let mut c = Chain::new();
3696 c.decision(&old);
3697 let mut rotation = c.routine(&old_pk, &thief);
3700 rotation.new_key = (&new_pk).into();
3701 rotation.certificate = for_new_at(&c, &new_pk);
3702 let statement = rotation.statement(c.prev_hash());
3703 rotation.proof = crypto::sign(
3704 &old,
3705 statement.hash().as_bytes(),
3706 rotation.proof_signed_at,
3707 )
3708 .into();
3709 c.rotate(&old, &rotation);
3710
3711 assert_eq!(
3712 rotation.verify_proof(c.links[1].attestation.prev_hash),
3713 Err(RotationError::BadProof)
3714 );
3715 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3716 assert!(!v.ok, "{v:#?}");
3717 assert!(
3718 v.entries[1]
3719 .problem
3720 .as_deref()
3721 .unwrap()
3722 .contains("proof of possession")
3723 );
3724 assert_eq!(v.public_key, (&old_pk).into(), "the chain does not move");
3725 }
3726
3727 #[test]
3728 fn a_compromise_repudiates_the_window_and_a_reattestation_restores_one() {
3729 let (old, old_pk) = generate_keypair();
3730 let (new, new_pk) = generate_keypair();
3731 let mut c = Chain::new();
3732 c.decision(&old); c.decision(&old); let reattested = c.decision(&old); let rotation = c.compromise(&old_pk, &new, 1);
3736 let rotation_id = c.rotate(&new, &rotation); let vouch = AmendmentDraft::new(
3738 reattested,
3739 c.hash_at(3),
3740 AmendmentKind::Reattested,
3741 "Art. VII",
3742 "independently verified; the Steward vouches for it",
3743 )
3744 .unwrap();
3745 let vouch_id = c.amend(&new, &vouch); let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3748 assert!(
3749 v.ok,
3750 "repudiation is a declared state, not a defect: {v:#?}"
3751 );
3752 assert_eq!(v.repudiated, vec![gov(2)]);
3753 assert!(v.entries[1].repudiated);
3754 assert!(!v.entries[2].repudiated);
3755 assert_eq!(v.entries[2].reattested_by, vec![vouch_id.clone()]);
3756 assert_eq!(v.entries[2].amended_by, vec![vouch_id]);
3757 assert_eq!(v.entries[3].signed_by, Some((&new_pk).into()));
3758 assert_eq!(v.public_key, (&new_pk).into());
3759 assert_eq!(v.keys.len(), 2);
3760 assert_eq!(v.keys[0].status, KeyStatus::Compromised);
3761 assert_eq!(
3762 v.keys[0].through_seq,
3763 Some(1),
3764 "trusted through the last trusted entry, not through the \
3765 declaration"
3766 );
3767 assert_eq!(v.keys[0].retired_by.as_ref(), Some(&rotation_id));
3768 assert_eq!(v.keys[1].from_seq, 4, "the declaration is its own first");
3769 }
3770
3771 #[test]
3774 fn last_trusted_is_the_roots_to_say() {
3775 let (old, old_pk) = generate_keypair();
3776 let (new, _) = generate_keypair();
3777 let mut c = Chain::new();
3778 c.decision(&old); c.decision(&old); let mut rotation = c.compromise(&old_pk, &new, 1);
3781 rotation.certificate.statement.last_trusted = Some(c.head(2));
3782 c.rotate(&new, &rotation);
3783 let v = verify_chain(&c.links, &old_pk, &anchored(&old_pk), &roots());
3784 assert!(!v.ok, "{v:#?}");
3785 assert!(v.repudiated.is_empty());
3786 assert_eq!(v.public_key, (&old_pk).into());
3787 }
3788
3789 #[test]
3790 fn a_stolen_key_cannot_be_rotated_back_in() {
3791 let (k1, k1_pk) = generate_keypair();
3795 let (k2, k2_pk) = generate_keypair();
3796 let mut c = Chain::new();
3797 c.decision(&k1); let real = c.compromise(&k1_pk, &k2, 1);
3799 c.rotate(&k2, &real); c.decision(&k2); let honest =
3802 verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3803 assert!(honest.ok, "{honest:#?}");
3804
3805 let hijack = c.compromise(&k2_pk, &k1, 3);
3806 c.rotate(&k1, &hijack); c.decision(&k1); let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3810 assert!(!v.ok);
3811 assert_eq!(v.public_key, (&k2_pk).into(), "the chain stays with K2");
3812 let p = v.entries[3].problem.as_deref().unwrap();
3813 assert!(p.contains("never brought back"), "{p}");
3814 assert!(!v.entries[3].signature_valid, "{:#?}", v.entries[3]);
3815 assert!(!v.entries[4].signature_valid, "K1 signs nothing again");
3816 assert_eq!(v.keys.len(), 2);
3817 assert_eq!(v.keys[1].status, KeyStatus::Active);
3818 }
3819
3820 #[test]
3821 fn a_routine_rotation_cannot_reuse_a_key_either() {
3822 let (k1, k1_pk) = generate_keypair();
3823 let (k2, k2_pk) = generate_keypair();
3824 let mut c = Chain::new();
3825 c.decision(&k1);
3826 let out = c.routine(&k1_pk, &k2);
3827 c.rotate(&k1, &out);
3828 let back = c.routine(&k2_pk, &k1);
3829 c.rotate(&k2, &back);
3830 let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3831 assert!(!v.ok);
3832 assert!(
3833 v.entries[2]
3834 .problem
3835 .as_deref()
3836 .unwrap()
3837 .contains("never brought back"),
3838 "{:#?}",
3839 v.entries[2]
3840 );
3841 assert_eq!(v.public_key, (&k2_pk).into());
3842 }
3843
3844 #[test]
3845 fn a_forged_entry_has_no_effects() {
3846 let (steward, steward_pk) = generate_keypair();
3847 let (forger, _) = generate_keypair();
3848 let anchor = anchored(&steward_pk);
3849 let mut c = Chain::new();
3850 let target = c.decision(&steward); let fake = AmendmentDraft::new(
3852 target,
3853 c.hash_at(1),
3854 AmendmentKind::Overruled,
3855 "none",
3856 "overruled, says nobody with the key",
3857 )
3858 .unwrap();
3859 c.amend(&forger, &fake); let grab = c.routine(&steward_pk, &forger);
3863 c.rotate(&forger, &grab); let v = verify_chain(&c.links, &steward_pk, &anchor, &roots());
3866 assert!(!v.ok);
3867 assert!(v.entries[0].amended_by.is_empty(), "{:#?}", v.entries[0]);
3868 assert_eq!(v.public_key, (&steward_pk).into());
3869 assert_eq!(v.keys.len(), 1);
3870 assert!(v.unanchored_keys.is_empty());
3871 }
3872
3873 #[test]
3874 fn a_repeated_id_is_a_problem() {
3875 let (key, pk) = generate_keypair();
3876 let mut c = Chain::new();
3877 c.decision(&key);
3878 c.gov = 0;
3879 c.decision(&key); let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
3881 assert!(!v.ok);
3882 assert!(v.entries.iter().all(|e| {
3883 e.problem
3884 .as_deref()
3885 .is_some_and(|p| p.contains("more than once"))
3886 }));
3887 }
3888
3889 #[test]
3890 fn a_second_compromise_cannot_anchor_inside_the_first_window() {
3891 let (k1, k1_pk) = generate_keypair();
3892 let (k2, _) = generate_keypair();
3893 let (k3, _) = generate_keypair();
3894 let mut c = Chain::new();
3895 c.decision(&k1); c.decision(&k1); let first = c.compromise(&k1_pk, &k2, 1);
3898 c.rotate(&k2, &first); let second = c.compromise(&k1_pk, &k3, 2);
3900 c.rotate(&k3, &second); let v = verify_chain(&c.links, &k1_pk, &anchored(&k1_pk), &roots());
3903 assert!(!v.ok);
3904 let p = v.entries[3].problem.as_deref().unwrap();
3905 assert!(p.contains("repudiated"), "{p}");
3906 assert_eq!(v.keys.len(), 2, "K1 and K2; K3 never took the chain");
3907 assert_eq!(v.keys[0].through_seq, Some(1));
3908 }
3909
3910 #[test]
3911 fn an_uncertified_compromise_fails_closed() {
3912 let (old, old_pk) = generate_keypair();
3913 let (new, new_pk) = generate_keypair();
3914 let mut c = Chain::new();
3915 c.decision(&old);
3916 c.decision(&old);
3917 let mut rotation = c.compromise(&old_pk, &new, 1);
3918 let statement = rotation.certificate.statement.clone();
3919 rotation.certificate = certify(&[&new], statement);
3920 c.rotate(&new, &rotation);
3921
3922 let anchor = anchored(&old_pk).with((&new_pk).into());
3924 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
3925 assert!(!v.ok, "{v:#?}");
3926 let p = v.entries[2].problem.as_deref().unwrap();
3927 assert!(p.contains("0 valid root signature"), "{p}");
3928 assert!(!v.entries[2].signature_valid, "checked under the old key");
3929 assert!(v.repudiated.is_empty(), "and nothing is repudiated");
3930 assert_eq!(v.public_key, (&old_pk).into());
3931 }
3932
3933 #[test]
3938 fn a_rotation_inside_the_window_is_void_with_it() {
3939 let (steward, steward_pk) = generate_keypair();
3940 let (successor, _) = generate_keypair();
3941 let (recovery, recovery_pk) = generate_keypair();
3942
3943 let mut c = Chain::new();
3944 c.decision(&steward); c.decision(&steward); let routine = c.routine(&steward_pk, &successor);
3947 c.rotate(&steward, &routine); c.decision(&successor); let declaration = c.compromise(&steward_pk, &recovery, 1);
3951 c.rotate(&recovery, &declaration); let v = verify_chain(
3954 &c.links,
3955 &steward_pk,
3956 &anchored(&steward_pk),
3957 &roots(),
3958 );
3959 assert!(v.ok, "{v:#?}");
3960 assert_eq!(v.repudiated, vec![gov(2), key_id(1), gov(3)]);
3961 assert_eq!(v.public_key, (&recovery_pk).into());
3962 assert_eq!(v.keys.len(), 2, "the successor is not part of history");
3963 assert_eq!(v.keys[0].public_key, (&steward_pk).into());
3964 assert_eq!(v.keys[0].status, KeyStatus::Compromised);
3965 assert!(
3966 v.keys[0].certified,
3967 "the genesis certificate rode in on the voided rotation and \
3968 is the root's word all the same"
3969 );
3970 assert_eq!(v.keys[1].public_key, (&recovery_pk).into());
3971 }
3972
3973 #[test]
3974 fn a_compromise_must_name_a_real_head_and_the_key_that_held_it() {
3975 let (old, old_pk) = generate_keypair();
3976 let (new, new_pk) = generate_keypair();
3977 let anchor = anchored(&old_pk);
3978
3979 let mut c = Chain::new();
3982 c.decision(&old);
3983 let mut trusted = c.head(1);
3984 trusted.entry_hash = data_hash(&json!("nope"));
3985 let statement = KeyCertStatement::compromise(
3986 (&new_pk).into(),
3987 2,
3988 c.prev_hash(),
3989 trusted,
3990 );
3991 let mut rotation = c.compromise(&old_pk, &new, 1);
3992 rotation.certificate = certify(&[&root(1)], statement);
3993 c.rotate(&new, &rotation);
3994 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
3995 let p = v.entries[1].problem.as_deref().unwrap();
3996 assert!(p.contains("last_trusted"), "{p}");
3997
3998 let (_, other_pk) = generate_keypair();
4000 let mut c = Chain::new();
4001 c.decision(&old);
4002 let rotation = c.compromise(&other_pk, &new, 1);
4003 c.rotate(&new, &rotation);
4004 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
4005 let p = v.entries[1].problem.as_deref().unwrap();
4006 assert!(p.contains("old_key is not the key"), "{p}");
4007
4008 let mut c = Chain::new();
4010 c.decision(&old);
4011 let mut rotation = c.compromise(&old_pk, &new, 1);
4012 rotation.certificate.statement.last_trusted = None;
4013 c.rotate(&new, &rotation);
4014 let v = verify_chain(&c.links, &old_pk, &anchor, &roots());
4015 let p = v.entries[1].problem.as_deref().unwrap();
4016 assert!(p.contains("must name last_trusted"), "{p}");
4017 }
4018
4019 #[test]
4020 fn a_rotation_carries_no_free_text() {
4021 let (old, old_pk) = generate_keypair();
4022 let (new, _) = generate_keypair();
4023 let mut c = Chain::new();
4024 c.decision(&old);
4025 let rotation = c.routine(&old_pk, &new);
4026 let mut value = serde_json::to_value(&rotation).unwrap();
4027 assert!(serde_json::from_value::<KeyRotation>(value.clone()).is_ok());
4028 value["note"] = json!("at the request of …");
4029 assert!(serde_json::from_value::<KeyRotation>(value).is_err());
4030
4031 let mut head = serde_json::to_value(c.head(1)).unwrap();
4032 head["comment"] = json!("the last one I remember signing");
4033 assert!(serde_json::from_value::<TrustedHead>(head).is_err());
4034
4035 let mut statement =
4036 serde_json::to_value(&rotation.certificate.statement).unwrap();
4037 statement["comment"] = json!("signed in the kitchen");
4038 assert!(serde_json::from_value::<KeyCertStatement>(statement).is_err());
4039 }
4040
4041 #[test]
4042 fn the_root_statement_bytes_are_pinned() {
4043 let key: PublicKeyHex = PUBLISHED_KEYS[0].parse().unwrap();
4044 assert_eq!(
4045 String::from_utf8(KeyCertStatement::genesis(key).signed_bytes())
4046 .unwrap(),
4047 "agora-governance-root-v1\n\
4048 {\"agora_governance_key_cert\":1,\"from_seq\":1,\
4049 \"key\":\"ebb3091dd328f1463362c171121921b2fe14628e3fc4c145deaccefb85c0e78a\",\
4050 \"last_trusted\":null,\"prev_hash\":null,\"purpose\":\"genesis\"}"
4051 );
4052
4053 let statement = KeyCertStatement::compromise(
4057 Sha256Hex::from([0xab; 32]).to_string().parse().unwrap(),
4058 15,
4059 Some([0xcd; 32].into()),
4060 TrustedHead {
4061 id: gov(10),
4062 chain_seq: 11,
4063 entry_hash: [0xef; 32].into(),
4064 },
4065 );
4066 assert_eq!(
4067 Sha256Hex::from(<[u8; 32]>::from(Sha256::digest(
4068 statement.signed_bytes()
4069 )))
4070 .to_string(),
4071 "633685771e08be126fd12ca4eb98c77120e5868236ff541ecba85ce6a21e6b68"
4072 );
4073 }
4074
4075 #[test]
4076 fn root_keys_are_curve_points_and_make_a_root_set() {
4077 let roots = RootSet::published();
4078 assert_eq!(roots.keys().count(), ROOT_KEYS.len());
4079 assert_eq!(roots.threshold(), ROOT_THRESHOLD);
4080 assert!(ROOT_THRESHOLD >= 1 && ROOT_THRESHOLD <= ROOT_KEYS.len());
4081 for root in ROOT_KEYS {
4082 let key: PublicKeyHex = root.parse().unwrap();
4083 assert!(roots.contains(&key));
4084 assert!(
4085 key.to_verifying_key().is_ok(),
4086 "{root} is not a valid Ed25519 public key"
4087 );
4088 assert!(
4089 !PUBLISHED_KEYS.contains(root),
4090 "a root key never signs entries"
4091 );
4092 }
4093 assert_eq!(RootSet::new([], 0).threshold(), 1);
4094 }
4095
4096 #[test]
4097 fn a_genesis_key_outside_the_anchor_is_reported_not_rejected() {
4098 let (key, pk) = generate_keypair();
4099 let c = chain(&key, 2);
4100 let v = verify_chain(&c, &pk, &KeyAnchor::default(), &roots());
4101 assert!(v.ok, "{v:#?}");
4102 assert_eq!(v.unanchored_keys, vec![PublicKeyHex::from(&pk)]);
4103 assert_eq!(v.keys.len(), 1);
4104 assert_eq!(v.keys[0].status, KeyStatus::Active);
4105 assert_eq!(v.keys[0].from_seq, 1);
4106 assert_eq!(v.keys[0].through_seq, None);
4107 }
4108
4109 #[test]
4110 fn published_keys_are_curve_points_and_make_an_anchor() {
4111 let anchor = KeyAnchor::published();
4112 assert!(!anchor.is_empty());
4113 assert_eq!(anchor.keys().count(), PUBLISHED_KEYS.len());
4114 for published in PUBLISHED_KEYS {
4115 let key: PublicKeyHex = published.parse().unwrap();
4116 assert!(anchor.contains(&key));
4117 assert!(
4118 key.to_verifying_key().is_ok(),
4119 "{published} is not a valid Ed25519 public key"
4120 );
4121 }
4122 let (_, other) = generate_keypair();
4123 assert!(!anchor.contains(&(&other).into()));
4124 assert!(
4125 anchor
4126 .clone()
4127 .with((&other).into())
4128 .contains(&(&other).into())
4129 );
4130 }
4131
4132 #[test]
4135 fn amendments_and_rotations_round_trip_as_entry_data() {
4136 let (key, pk) = generate_keypair();
4137 let amendment = AmendmentDraft::new(
4138 gov(1),
4139 data_hash(&json!("x")),
4140 AmendmentKind::Superseded,
4141 "Art. VI § 2",
4142 "superseded by GOV-2026-0009",
4143 )
4144 .unwrap()
4145 .with_authority(gov(9))
4146 .with_rationale("the later decision covers the same subject");
4147 let AmendmentDraft { amendment, texts } = amendment;
4148 let value = serde_json::to_value(&amendment).unwrap();
4149 assert_eq!(value["kind"], "superseded");
4150 assert_eq!(value["agora_governance_amendment"], 2);
4151 assert!(value.get("redaction").is_none(), "{value}");
4152 let text = value.to_string();
4153 assert!(!text.contains("Art. VI"), "no text in signed data: {text}");
4154 assert!(!text.contains("salt"), "and no salt: {text}");
4155 assert_eq!(
4156 value["note"]["commitment"],
4157 texts
4158 .note
4159 .as_ref()
4160 .unwrap()
4161 .commitment()
4162 .commitment
4163 .to_string()
4164 );
4165 assert_eq!(
4166 serde_json::from_value::<Amendment>(value).unwrap(),
4167 amendment
4168 );
4169 let beside = serde_json::to_value(&texts).unwrap();
4170 assert_eq!(beside["basis"]["text"], "Art. VI § 2");
4171 assert_eq!(
4172 serde_json::from_value::<AmendmentTexts>(beside).unwrap(),
4173 texts
4174 );
4175
4176 let legacy = json!({
4178 "agora_governance_amendment": 1,
4179 "target": "GOV-2026-0001",
4180 "target_entry_hash": data_hash(&json!("x")),
4181 "kind": "non_precedential",
4182 "authority": "GOV-2026-0005",
4183 "basis": "§1",
4184 "note": "diagnostic finding",
4185 });
4186 let legacy: Amendment = serde_json::from_value(legacy).unwrap();
4187 assert_eq!(legacy.validate(), Ok(()));
4188 assert_eq!(legacy.basis, AmendmentText::Plain("§1".into()));
4189
4190 let mut c = Chain::new();
4191 c.decision(&key);
4192 let rotation = c.compromise(&pk, &generate_keypair().0, 1);
4193 let value = serde_json::to_value(&rotation).unwrap();
4194 assert_eq!(value["agora_governance_key_rotation"], 2);
4195 assert_eq!(value["reason"], "compromise");
4196 let statement = &value["certificate"]["statement"];
4197 assert_eq!(statement["purpose"], "compromise");
4198 assert_eq!(statement["last_trusted"]["chain_seq"], 1);
4199 assert_eq!(
4200 value["outgoing_certificate"]["statement"]["purpose"],
4201 "genesis"
4202 );
4203 assert!(value.get("note").is_none(), "{value}");
4204 assert_eq!(
4205 serde_json::from_value::<KeyRotation>(value).unwrap(),
4206 rotation
4207 );
4208
4209 let notice =
4210 AmendmentNotice::new(amd(1), at(0), &amendment, Some(&texts));
4211 let value = serde_json::to_value(¬ice).unwrap();
4212 assert_eq!(value["id"], "AMD-2026-0001");
4213 assert_eq!(value["note"], "superseded by GOV-2026-0009");
4214 assert_eq!(
4215 serde_json::from_value::<AmendmentNotice>(value).unwrap(),
4216 notice
4217 );
4218
4219 let erased = AmendmentTexts {
4221 rationale: None,
4222 ..texts.clone()
4223 };
4224 let notice =
4225 AmendmentNotice::new(amd(1), at(0), &amendment, Some(&erased));
4226 assert_eq!(notice.note, "superseded by GOV-2026-0009");
4227 assert_eq!(notice.rationale.as_deref(), Some(WITHHELD_TEXT));
4228 let notice = AmendmentNotice::new(amd(1), at(0), &amendment, None);
4229 assert_eq!(notice.basis, WITHHELD_TEXT);
4230 let notice = AmendmentNotice::new(amd(1), at(0), &legacy, None);
4231 assert_eq!(notice.note, "diagnostic finding");
4232 }
4233
4234 #[test]
4238 fn an_amendment_verifies_with_or_without_its_optional_fields() {
4239 let (key, pk) = generate_keypair();
4240 let mut c = Chain::new();
4241 let target = c.decision(&key);
4242 let bare = AmendmentDraft::new(
4243 target.clone(),
4244 c.hash_at(1),
4245 AmendmentKind::Correction,
4246 "clerical",
4247 "typo in the citation",
4248 )
4249 .unwrap();
4250 assert!(
4251 serde_json::to_value(&bare.amendment)
4252 .unwrap()
4253 .get("rationale")
4254 .is_none()
4255 );
4256 c.amend(&key, &bare);
4257 let full = bare.clone().with_rationale("at length: …");
4258 c.amend(&key, &full);
4259 let mut future = serde_json::to_value(&full.amendment).unwrap();
4261 future["superseded_by_something_new"] = json!(["later"]);
4262 c.push(
4263 &key,
4264 amd(3),
4265 GovernanceLogEntryType::Amendment,
4266 future,
4267 true,
4268 );
4269
4270 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4271 assert!(v.ok, "{v:#?}");
4272 assert_eq!(
4273 v.entries[0].amended_by,
4274 vec![amd(1), amd(2), amd(3)],
4275 "all three name the target"
4276 );
4277 }
4278
4279 #[test]
4282 fn pre_0_26_wire_still_deserializes() {
4283 let link: GovernanceChainLink = serde_json::from_value(json!({
4284 "id": "GOV-2026-0001",
4285 "entry_type": "council_decision",
4286 "created_at": "2023-11-14T22:13:20.123456Z",
4287 "attestation": {
4288 "envelope_version": 1,
4289 "chain_seq": 1,
4290 "prev_hash": null,
4291 "data_hash": "00".repeat(32),
4292 "entry_hash": "11".repeat(32),
4293 "signature": "22".repeat(64),
4294 "signed_at": "2023-11-14T22:13:21Z",
4295 "retroactive": false,
4296 },
4297 }))
4298 .unwrap();
4299 assert!(link.data.is_none());
4300 assert!(
4301 serde_json::to_value(&link).unwrap().get("data").is_none(),
4302 "and a link without data does not grow a null field"
4303 );
4304
4305 let verdict: EntryVerdict = serde_json::from_value(json!({
4306 "id": "GOV-2026-0001",
4307 "chain_seq": 1,
4308 "signature_valid": true,
4309 "link_valid": true,
4310 "content_matches": null,
4311 "retroactive": false,
4312 "out_of_order": false,
4313 "amended_by": [],
4314 }))
4315 .unwrap();
4316 assert!(!verdict.repudiated && !verdict.redacted);
4317 assert!(verdict.signed_by.is_none());
4318 assert!(verdict.reattested_by.is_empty());
4319
4320 let verification: GovernanceVerification =
4321 serde_json::from_value(json!({
4322 "public_key": "33".repeat(32),
4323 "ok": true,
4324 "head": "GOV-2026-0001",
4325 "entries": [],
4326 }))
4327 .unwrap();
4328 assert!(verification.keys.is_empty());
4329 assert!(verification.unanchored_keys.is_empty());
4330 assert!(verification.repudiated.is_empty());
4331 }
4332
4333 #[test]
4334 fn the_report_round_trips() {
4335 let (key, pk) = generate_keypair();
4336 let mut c = Chain::new();
4337 let target = c.decision(&key);
4338 let amendment = AmendmentDraft::new(
4339 target,
4340 c.hash_at(1),
4341 AmendmentKind::Overruled,
4342 "b",
4343 "n",
4344 )
4345 .unwrap();
4346 c.amend(&key, &amendment);
4347 let v = verify_chain(&c.links, &pk, &anchored(&pk), &roots());
4348 let text = serde_json::to_string(&v).unwrap();
4349 assert_eq!(
4350 serde_json::from_str::<GovernanceVerification>(&text).unwrap(),
4351 v
4352 );
4353
4354 let keys = GovernanceSigningKeys { keys: v.keys };
4355 let value = serde_json::to_value(&keys).unwrap();
4356 assert!(value["keys"].is_array(), "an object, not a bare array");
4357 assert_eq!(value["keys"][0]["status"], "active");
4358 assert_eq!(
4359 serde_json::from_value::<GovernanceSigningKeys>(value).unwrap(),
4360 keys
4361 );
4362 }
4363
4364 #[test]
4368 fn envelope_v1_preimage_and_hash_are_pinned() {
4369 let envelope = Envelope::new(
4370 gov(6),
4371 GovernanceLogEntryType::CouncilDecision,
4372 at(0),
4373 Some(Sha256Hex::from([0x11; 32])),
4374 data_hash(&json!({"outcome": "approved", "title": "Ratification"})),
4375 );
4376 assert_eq!(
4377 String::from_utf8(envelope.preimage()).unwrap(),
4378 "{\"agora_governance_log\":1,\"id\":\"GOV-2026-0006\",\
4379 \"entry_type\":\"council_decision\",\
4380 \"created_at\":1700000000123456,\
4381 \"prev_hash\":\"1111111111111111111111111111111111111111111111111111111111111111\",\
4382 \"data_hash\":\"a4adf645ae3f60c56484d01aea87d6d490321d7fc66b1607df14b023fe567c7b\"}"
4383 );
4384 assert_eq!(
4385 envelope.entry_hash().to_hex(),
4386 "ba27577432f81e415f1c01cc4cfabab6070e3ac50fd468fffe195ef19c0e9464"
4387 );
4388 }
4389
4390 #[cfg(feature = "agora-client")]
4404 #[tokio::test]
4405 #[ignore = "networked: hits the live platform"]
4406 async fn the_published_key_is_the_one_the_platform_serves() {
4407 let client = crate::client::Client::new(
4408 url::Url::parse("https://subliminal.technology").unwrap(),
4409 )
4410 .unwrap();
4411 let served = client.get_governance_signing_key().await.unwrap();
4412 assert_eq!(served.algorithm, "ed25519");
4413
4414 let genesis: PublicKeyHex = PUBLISHED_KEYS
4415 .first()
4416 .expect("PUBLISHED_KEYS is never empty")
4417 .parse()
4418 .unwrap();
4419 let links = client.get_governance_chain().await.unwrap();
4420 let report = verify_chain(
4421 &links,
4422 &genesis.to_verifying_key().unwrap(),
4423 &KeyAnchor::published(),
4424 &RootSet::published(),
4425 );
4426 let problems: Vec<_> = report
4427 .entries
4428 .iter()
4429 .filter_map(|e| {
4430 e.problem.as_ref().map(|p| (e.id.clone(), p.clone()))
4431 })
4432 .collect();
4433 assert!(
4434 report.ok,
4435 "the live chain does not verify under this build's genesis key \
4436 and roots: {problems:#?}"
4437 );
4438 assert!(report.unanchored_keys.is_empty(), "{report:#?}");
4439 assert_eq!(
4440 served.public_key, report.public_key,
4441 "the platform serves {} but the chain, followed under the \
4442 published roots, is held by {}",
4443 served.public_key, report.public_key
4444 );
4445 }
4446
4447 #[cfg(feature = "schemars")]
4448 #[test]
4449 fn wire_schemas_are_ref_free() {
4450 use crate::responses::inline_schema_for;
4451 for (name, schema) in [
4452 (
4453 "GovernanceAttestation",
4454 inline_schema_for::<GovernanceAttestation>(),
4455 ),
4456 (
4457 "GovernanceChainLink",
4458 inline_schema_for::<GovernanceChainLink>(),
4459 ),
4460 (
4461 "GovernanceSigningKey",
4462 inline_schema_for::<GovernanceSigningKey>(),
4463 ),
4464 (
4465 "GovernanceVerification",
4466 inline_schema_for::<GovernanceVerification>(),
4467 ),
4468 (
4469 "Vec<GovernanceChainLink>",
4470 inline_schema_for::<Vec<GovernanceChainLink>>(),
4471 ),
4472 ("Amendment", inline_schema_for::<Amendment>()),
4473 ("Redaction", inline_schema_for::<Redaction>()),
4474 ("AmendmentNotice", inline_schema_for::<AmendmentNotice>()),
4475 ("KeyRotation", inline_schema_for::<KeyRotation>()),
4476 ("TrustedHead", inline_schema_for::<TrustedHead>()),
4477 (
4478 "GovernanceKeyRecord",
4479 inline_schema_for::<GovernanceKeyRecord>(),
4480 ),
4481 (
4482 "GovernanceSigningKeys",
4483 inline_schema_for::<GovernanceSigningKeys>(),
4484 ),
4485 ("AmendmentKind", inline_schema_for::<AmendmentKind>()),
4486 ("Standing", inline_schema_for::<Standing>()),
4487 ("KeyStatus", inline_schema_for::<KeyStatus>()),
4488 ("RotationReason", inline_schema_for::<RotationReason>()),
4489 ] {
4490 let text = serde_json::to_string(&schema).unwrap();
4491 assert!(!text.contains("$ref"), "{name} must be $ref-free: {text}");
4492 assert!(
4493 !text.contains("$defs"),
4494 "{name} must be $defs-free: {text}"
4495 );
4496 }
4497 let text =
4498 serde_json::to_string(&inline_schema_for::<Sha256Hex>()).unwrap();
4499 assert!(text.contains("^[0-9a-f]{64}$"), "{text}");
4500 let text = serde_json::to_string(&inline_schema_for::<SignatureHex>())
4501 .unwrap();
4502 assert!(text.contains("^[0-9a-f]{128}$"), "{text}");
4503 }
4504}