1#![doc = include_str!("../README.md")]
2
3extern crate alloc;
30
31use alloc::{string::String, vec::Vec};
32
33use blake2::{Blake2s256, Digest};
34
35pub mod cbor;
36
37use cbor::Value;
38
39pub const AUM_HASH_LEN: usize = 32;
41
42const MAX_SIG_NESTING_DEPTH: usize = 16;
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
54pub struct AumHash(pub [u8; AUM_HASH_LEN]);
55
56impl AumHash {
57 pub fn from_base32(text: &str) -> Option<AumHash> {
60 let decoded = base32_decode_nopad(text)?;
61 if decoded.len() != AUM_HASH_LEN {
62 return None;
63 }
64 let mut h = [0u8; AUM_HASH_LEN];
65 h.copy_from_slice(&decoded);
66 Some(AumHash(h))
67 }
68
69 pub fn to_base32(&self) -> String {
71 base32_encode_nopad(&self.0)
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
78#[repr(u8)]
79pub enum AumKind {
80 Invalid = 0,
82 AddKey = 1,
84 RemoveKey = 2,
86 NoOp = 3,
88 UpdateKey = 4,
90 Checkpoint = 5,
92}
93
94impl AumKind {
95 pub fn from_u8(n: u8) -> Option<AumKind> {
99 Some(match n {
100 0 => AumKind::Invalid,
101 1 => AumKind::AddKey,
102 2 => AumKind::RemoveKey,
103 3 => AumKind::NoOp,
104 4 => AumKind::UpdateKey,
105 5 => AumKind::Checkpoint,
106 _ => return None,
107 })
108 }
109}
110
111#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum KeyKind {
114 Ed25519,
116}
117
118#[derive(Debug, Clone, PartialEq, Eq)]
121pub struct Key {
122 pub kind: KeyKind,
124 pub votes: u32,
126 pub public: Vec<u8>,
128}
129
130impl Key {
131 pub fn id(&self) -> &[u8] {
133 &self.public
134 }
135}
136
137#[derive(Debug, Clone, Copy, PartialEq, Eq)]
139#[repr(u8)]
140pub enum SigKind {
141 Invalid = 0,
143 Direct = 1,
145 Rotation = 2,
147 Credential = 3,
149}
150
151impl SigKind {
152 fn from_u8(n: u8) -> Option<SigKind> {
153 Some(match n {
154 0 => SigKind::Invalid,
155 1 => SigKind::Direct,
156 2 => SigKind::Rotation,
157 3 => SigKind::Credential,
158 _ => return None,
159 })
160 }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq)]
166pub struct NodeKeySignature {
167 pub sig_kind: SigKind,
169 pub pubkey: Vec<u8>,
171 pub key_id: Vec<u8>,
173 pub signature: Vec<u8>,
175 pub nested: Option<alloc::boxed::Box<NodeKeySignature>>,
177 pub wrapping_pubkey: Vec<u8>,
179}
180
181#[derive(thiserror::Error, Debug, Clone, PartialEq, Eq)]
183pub enum TkaError {
184 #[error("TKA decode error: {0}")]
186 Decode(&'static str),
187 #[error("TKA signature verification failed")]
189 BadSignature,
190 #[error("TKA authorizing key is not trusted")]
192 UntrustedKey,
193 #[error("a credential signature cannot authorize a node")]
195 CredentialCannotAuthorize,
196 #[error("signature does not cover this node key")]
198 NodeKeyMismatch,
199 #[error("AUM parent hash does not match the current chain head")]
202 BadParent,
203 #[error("AUM key-state update is invalid (key already exists, or no such key)")]
206 BadKeyState,
207 #[error("AUM chain is empty or has no valid genesis")]
210 BadChain,
211}
212
213impl NodeKeySignature {
214 fn sig_hash(&self) -> [u8; AUM_HASH_LEN] {
218 let v = self.to_cbor(false);
219 blake2s_256(&v.to_vec())
220 }
221
222 fn to_cbor(&self, include_signature: bool) -> Value {
225 cbor::int_map([
226 (1, Some(Value::Uint(self.sig_kind as u8 as u64))),
227 (2, nonempty_bytes(&self.pubkey)),
228 (3, nonempty_bytes(&self.key_id)),
229 (
230 4,
231 if include_signature {
232 nonempty_bytes(&self.signature)
233 } else {
234 None
235 },
236 ),
237 (5, self.nested.as_ref().map(|n| n.to_cbor(true))),
238 (6, nonempty_bytes(&self.wrapping_pubkey)),
239 ])
240 }
241
242 fn authorizing_key_id(&self) -> Result<&[u8], TkaError> {
245 match self.sig_kind {
246 SigKind::Rotation => self
247 .nested
248 .as_ref()
249 .ok_or(TkaError::Decode("rotation signature missing nested"))?
250 .authorizing_key_id(),
251 SigKind::Direct | SigKind::Credential => Ok(&self.key_id),
252 SigKind::Invalid => Err(TkaError::Decode("invalid signature kind")),
253 }
254 }
255
256 fn verify_signature(&self, node_key: &[u8], verification_key: &Key) -> Result<(), TkaError> {
259 if self.sig_kind != SigKind::Credential && self.pubkey != node_key {
261 return Err(TkaError::NodeKeyMismatch);
262 }
263
264 let sig_hash = self.sig_hash();
265
266 match self.sig_kind {
267 SigKind::Rotation => {
268 let nested = self
269 .nested
270 .as_ref()
271 .ok_or(TkaError::Decode("rotation signature missing nested"))?;
272 let verify_pub = &nested.wrapping_pubkey;
275 if verify_pub.len() != 32 {
276 return Err(TkaError::Decode("wrapping pubkey wrong length"));
277 }
278 verify_ed25519_std(verify_pub, &sig_hash, &self.signature)?;
279 if nested.sig_kind == SigKind::Credential && nested.pubkey != *verify_pub {
285 return Err(TkaError::NodeKeyMismatch);
286 }
287 nested.verify_signature(verify_pub, verification_key)
289 }
290 SigKind::Direct | SigKind::Credential => {
291 if self.nested.is_some() {
292 return Err(TkaError::Decode("direct/credential signature has nested"));
293 }
294 if verification_key.kind != KeyKind::Ed25519 || verification_key.public.len() != 32
295 {
296 return Err(TkaError::Decode("verification key not ed25519"));
297 }
298 verify_ed25519_zip215(&verification_key.public, &sig_hash, &self.signature)
301 }
302 SigKind::Invalid => Err(TkaError::Decode("invalid signature kind")),
303 }
304 }
305}
306
307#[derive(Debug, Clone, Default, PartialEq, Eq)]
310pub struct State {
311 pub keys: Vec<Key>,
313}
314
315impl State {
316 pub fn get_key(&self, key_id: &[u8]) -> Option<&Key> {
318 self.keys.iter().find(|k| k.id() == key_id)
319 }
320}
321
322#[derive(Debug, Clone)]
326pub struct Authority {
327 head: AumHash,
328 state: State,
329}
330
331impl Authority {
332 pub fn from_state(head: AumHash, state: State) -> Authority {
335 Authority { head, state }
336 }
337
338 pub fn head(&self) -> AumHash {
340 self.head
341 }
342
343 pub fn state(&self) -> &State {
345 &self.state
346 }
347
348 pub fn head_matches(&self, head: &AumHash) -> bool {
351 &self.head == head
352 }
353
354 pub fn node_key_authorized(
368 &self,
369 node_key: &[u8],
370 signature_cbor: &[u8],
371 ) -> Result<(), TkaError> {
372 let sig = decode_node_key_signature(signature_cbor)?;
373 if sig.sig_kind == SigKind::Credential {
375 return Err(TkaError::CredentialCannotAuthorize);
376 }
377 let key_id = sig.authorizing_key_id()?;
378 let key = self.state.get_key(key_id).ok_or(TkaError::UntrustedKey)?;
379 sig.verify_signature(node_key, key)
380 }
381}
382
383pub fn aum_hash(canonical_cbor: &[u8]) -> AumHash {
386 AumHash(blake2s_256(canonical_cbor))
387}
388
389#[derive(Debug, Clone, PartialEq, Eq)]
395pub struct AumKey {
396 pub kind: KeyKind,
398 pub votes: u32,
400 pub public: Vec<u8>,
402 pub meta: Vec<(alloc::string::String, alloc::string::String)>,
404}
405
406impl AumKey {
407 pub fn id(&self) -> &[u8] {
409 &self.public
410 }
411
412 pub fn to_key(&self) -> Key {
415 Key {
416 kind: self.kind,
417 votes: self.votes,
418 public: self.public.clone(),
419 }
420 }
421
422 fn kind_u8(&self) -> u8 {
423 match self.kind {
424 KeyKind::Ed25519 => 1,
425 }
426 }
427
428 fn to_cbor(&self) -> Value {
429 cbor::int_map([
430 (1, Some(Value::Uint(self.kind_u8() as u64))),
431 (2, Some(Value::Uint(self.votes as u64))),
432 (3, Some(Value::Bytes(self.public.clone()))),
433 (12, meta_to_cbor(&self.meta)),
434 ])
435 }
436}
437
438#[derive(Debug, Clone, PartialEq, Eq, Default)]
448pub struct AumState {
449 pub last_aum_hash: Option<AumHash>,
451 pub disablement_values: Option<Vec<Vec<u8>>>,
454 pub keys: Option<Vec<AumKey>>,
457 pub state_id1: u64,
459 pub state_id2: u64,
461}
462
463impl AumState {
464 fn to_cbor(&self) -> Value {
465 cbor::int_map([
466 (
467 1,
468 Some(match &self.last_aum_hash {
469 Some(h) => Value::Bytes(h.0.to_vec()),
470 None => Value::Null,
471 }),
472 ),
473 (
474 2,
475 Some(match &self.disablement_values {
476 None => Value::Null,
478 Some(vals) => {
479 Value::Array(vals.iter().map(|d| Value::Bytes(d.clone())).collect())
480 }
481 }),
482 ),
483 (
484 3,
485 Some(match &self.keys {
486 None => Value::Null,
487 Some(keys) => Value::Array(keys.iter().map(AumKey::to_cbor).collect()),
488 }),
489 ),
490 (
491 4,
492 (self.state_id1 != 0).then_some(Value::Uint(self.state_id1)),
493 ),
494 (
495 5,
496 (self.state_id2 != 0).then_some(Value::Uint(self.state_id2)),
497 ),
498 ])
499 }
500}
501
502#[derive(Debug, Clone, PartialEq, Eq)]
505pub struct AumSignature {
506 pub key_id: Vec<u8>,
508 pub signature: Vec<u8>,
510}
511
512impl AumSignature {
513 fn to_cbor(&self) -> Value {
514 cbor::int_map([
518 (1, Some(bytes_or_null(&self.key_id))),
519 (2, Some(bytes_or_null(&self.signature))),
520 ])
521 }
522}
523
524#[derive(Debug, Clone, PartialEq, Eq)]
534pub struct Aum {
535 pub message_kind: AumKind,
537 pub prev_aum_hash: Option<AumHash>,
539 pub key: Option<AumKey>,
541 pub key_id: Vec<u8>,
543 pub state: Option<AumState>,
545 pub votes: Option<u32>,
547 pub meta: Vec<(alloc::string::String, alloc::string::String)>,
549 pub signatures: Vec<AumSignature>,
551}
552
553impl Aum {
554 fn message_kind_u8(&self) -> u8 {
555 self.message_kind as u8
556 }
557
558 fn to_cbor(&self, include_signatures: bool) -> Value {
561 let signatures = if include_signatures && !self.signatures.is_empty() {
562 Some(Value::Array(
563 self.signatures.iter().map(AumSignature::to_cbor).collect(),
564 ))
565 } else {
566 None
567 };
568 cbor::int_map([
569 (1, Some(Value::Uint(self.message_kind_u8() as u64))),
571 (
573 2,
574 Some(match &self.prev_aum_hash {
575 Some(h) => Value::Bytes(h.0.to_vec()),
576 None => Value::Null,
577 }),
578 ),
579 (3, self.key.as_ref().map(AumKey::to_cbor)),
580 (4, nonempty_bytes(&self.key_id)),
581 (5, self.state.as_ref().map(AumState::to_cbor)),
582 (6, self.votes.map(|v| Value::Uint(v as u64))),
583 (7, meta_to_cbor(&self.meta)),
584 (23, signatures),
585 ])
586 }
587
588 pub fn serialize(&self) -> Vec<u8> {
590 self.to_cbor(true).to_vec()
591 }
592
593 pub fn hash(&self) -> AumHash {
595 AumHash(blake2s_256(&self.serialize()))
596 }
597
598 pub fn sig_hash(&self) -> [u8; AUM_HASH_LEN] {
601 blake2s_256(&self.to_cbor(false).to_vec())
602 }
603}
604
605fn meta_to_cbor(meta: &[(alloc::string::String, alloc::string::String)]) -> Option<Value> {
608 if meta.is_empty() {
609 return None;
610 }
611 Some(Value::TextMap(
612 meta.iter()
613 .map(|(k, v)| (k.as_bytes().to_vec(), Value::Text(v.as_bytes().to_vec())))
614 .collect(),
615 ))
616}
617
618fn blake2s_256(data: &[u8]) -> [u8; AUM_HASH_LEN] {
619 let mut hasher = Blake2s256::new();
620 hasher.update(data);
621 let out = hasher.finalize();
622 let mut h = [0u8; AUM_HASH_LEN];
623 h.copy_from_slice(&out);
624 h
625}
626
627fn nonempty_bytes(b: &[u8]) -> Option<Value> {
629 if b.is_empty() {
630 None
631 } else {
632 Some(Value::Bytes(b.to_vec()))
633 }
634}
635
636fn bytes_or_null(b: &[u8]) -> Value {
640 if b.is_empty() {
641 Value::Null
642 } else {
643 Value::Bytes(b.to_vec())
644 }
645}
646
647#[derive(Debug, Clone, Default)]
657struct ReplayState {
658 keys: Vec<AumKey>,
659 last_aum_hash: Option<AumHash>,
660 state_id: Option<(u64, u64)>,
664}
665
666impl ReplayState {
667 fn get_key(&self, key_id: &[u8]) -> Option<&AumKey> {
668 self.keys.iter().find(|k| k.id() == key_id)
669 }
670
671 fn find_key_index(&self, key_id: &[u8]) -> Option<usize> {
672 self.keys.iter().position(|k| k.id() == key_id)
673 }
674
675 fn weight(&self, aum: &Aum) -> u64 {
679 let mut seen: Vec<&[u8]> = Vec::new();
680 let mut weight: u64 = 0;
681 for sig in &aum.signatures {
682 let id = sig.key_id.as_slice();
683 if seen.contains(&id) {
684 continue;
685 }
686 if let Some(key) = self.get_key(id) {
687 weight += key.votes as u64;
688 seen.push(id);
689 }
690 }
691 weight
692 }
693
694 fn apply_verified_aum(&mut self, aum: &Aum) -> Result<(), TkaError> {
705 match &self.last_aum_hash {
706 Some(head) => match &aum.prev_aum_hash {
708 Some(prev) if prev == head => {}
709 _ => return Err(TkaError::BadParent),
710 },
711 None => {
713 if aum.prev_aum_hash.is_some() {
714 return Err(TkaError::BadParent);
715 }
716 if !matches!(
717 aum.message_kind,
718 AumKind::NoOp | AumKind::AddKey | AumKind::Checkpoint
719 ) {
720 return Err(TkaError::BadChain);
721 }
722 }
723 }
724
725 match aum.message_kind {
726 AumKind::NoOp | AumKind::Invalid => {
727 }
730 AumKind::Checkpoint => {
731 let state = aum
736 .state
737 .as_ref()
738 .ok_or(TkaError::Decode("checkpoint AUM missing state"))?;
739 let incoming = (state.state_id1, state.state_id2);
740 match self.state_id {
741 Some(existing) if existing != incoming => {
742 return Err(TkaError::BadKeyState);
743 }
744 _ => self.state_id = Some(incoming),
745 }
746 self.keys = state.keys.clone().unwrap_or_default();
749 }
750 AumKind::AddKey => {
751 let key = aum
752 .key
753 .as_ref()
754 .ok_or(TkaError::Decode("AddKey AUM missing key"))?;
755 if self.get_key(key.id()).is_some() {
756 return Err(TkaError::BadKeyState);
757 }
758 self.keys.push(key.clone());
759 }
760 AumKind::UpdateKey => {
761 let idx = self
762 .find_key_index(&aum.key_id)
763 .ok_or(TkaError::BadKeyState)?;
764 if let Some(votes) = aum.votes {
774 self.keys[idx].votes = votes;
775 }
776 if !aum.meta.is_empty() {
777 self.keys[idx].meta = aum.meta.clone();
778 }
779 }
780 AumKind::RemoveKey => {
781 let idx = self
782 .find_key_index(&aum.key_id)
783 .ok_or(TkaError::BadKeyState)?;
784 self.keys.remove(idx);
785 }
786 }
787
788 self.last_aum_hash = Some(aum.hash());
789 Ok(())
790 }
791
792 fn to_state(&self) -> State {
794 State {
795 keys: self.keys.iter().map(AumKey::to_key).collect(),
796 }
797 }
798}
799
800fn pick_next_aum<'a>(state: &ReplayState, candidates: &'a [Aum]) -> &'a Aum {
811 debug_assert!(!candidates.is_empty(), "pick_next_aum needs candidates");
812 let mut best = &candidates[0];
813 let mut best_weight = state.weight(best);
814 let mut best_hash = best.hash();
815 for cand in &candidates[1..] {
816 let w = state.weight(cand);
817 let h = cand.hash();
818 let better = if w != best_weight {
820 w > best_weight
821 } else if (cand.message_kind == AumKind::RemoveKey)
822 != (best.message_kind == AumKind::RemoveKey)
823 {
824 cand.message_kind == AumKind::RemoveKey
826 } else {
827 h.0 < best_hash.0
829 };
830 if better {
831 best = cand;
832 best_weight = w;
833 best_hash = h;
834 }
835 }
836 best
837}
838
839impl Authority {
840 pub fn from_chain(aums: &[Aum]) -> Result<Authority, TkaError> {
859 let last = aums.last().ok_or(TkaError::BadChain)?;
860 let head = last.hash();
861 let mut state = ReplayState::default();
862 for aum in aums {
863 state.apply_verified_aum(aum)?;
864 }
865 Ok(Authority {
866 head,
867 state: state.to_state(),
868 })
869 }
870
871 pub fn from_forked_chain(prefix: &[Aum], branches: &[&[Aum]]) -> Result<Authority, TkaError> {
892 if branches.is_empty() || branches.iter().any(|b| b.len() != 1) {
895 return Err(TkaError::BadChain);
896 }
897 let mut state = ReplayState::default();
898 for aum in prefix {
899 state.apply_verified_aum(aum)?;
900 }
901 let heads: Vec<Aum> = branches.iter().map(|b| b[0].clone()).collect();
904 let winner_head = pick_next_aum(&state, &heads).hash();
905 let winner = branches
906 .iter()
907 .find(|b| b[0].hash() == winner_head)
908 .ok_or(TkaError::BadChain)?;
909 state.apply_verified_aum(&winner[0])?;
910 Ok(Authority {
911 head: winner[0].hash(),
912 state: state.to_state(),
913 })
914 }
915}
916
917fn verify_ed25519_std(public: &[u8], msg: &[u8], sig: &[u8]) -> Result<(), TkaError> {
919 use ed25519_dalek::{Signature, Verifier, VerifyingKey};
920 let pk: [u8; 32] = public
921 .try_into()
922 .map_err(|_| TkaError::Decode("bad pubkey len"))?;
923 let vk = VerifyingKey::from_bytes(&pk).map_err(|_| TkaError::Decode("bad ed25519 pubkey"))?;
924 let sig: [u8; 64] = sig
925 .try_into()
926 .map_err(|_| TkaError::Decode("bad sig len"))?;
927 vk.verify(msg, &Signature::from_bytes(&sig))
928 .map_err(|_| TkaError::BadSignature)
929}
930
931fn verify_ed25519_zip215(public: &[u8], msg: &[u8], sig: &[u8]) -> Result<(), TkaError> {
933 let pk: [u8; 32] = public
934 .try_into()
935 .map_err(|_| TkaError::Decode("bad pubkey len"))?;
936 let vk = ed25519_zebra::VerificationKey::try_from(pk)
937 .map_err(|_| TkaError::Decode("bad ed25519 pubkey"))?;
938 let sig_bytes: [u8; 64] = sig
939 .try_into()
940 .map_err(|_| TkaError::Decode("bad sig len"))?;
941 let sig = ed25519_zebra::Signature::from(sig_bytes);
942 vk.verify(&sig, msg).map_err(|_| TkaError::BadSignature)
943}
944
945fn decode_node_key_signature(buf: &[u8]) -> Result<NodeKeySignature, TkaError> {
948 let (val, rest) = decode_value(buf, 0)?;
949 if !rest.is_empty() {
950 return Err(TkaError::Decode("trailing bytes after signature"));
951 }
952 node_key_signature_from_value(val, 0)
953}
954
955fn node_key_signature_from_value(val: Value, depth: usize) -> Result<NodeKeySignature, TkaError> {
956 if depth > MAX_SIG_NESTING_DEPTH {
957 return Err(TkaError::Decode("nested signature too deep"));
958 }
959 let Value::IntMap(entries) = val else {
960 return Err(TkaError::Decode("signature is not an int-keyed map"));
961 };
962 let mut sig_kind = None;
963 let mut pubkey = Vec::new();
964 let mut key_id = Vec::new();
965 let mut signature = Vec::new();
966 let mut nested = None;
967 let mut wrapping_pubkey = Vec::new();
968
969 for (k, v) in entries {
970 match k {
971 1 => {
972 let Value::Uint(n) = v else {
973 return Err(TkaError::Decode("sig kind not uint"));
974 };
975 sig_kind = Some(
976 SigKind::from_u8(
977 u8::try_from(n).map_err(|_| TkaError::Decode("sig kind range"))?,
978 )
979 .ok_or(TkaError::Decode("unknown sig kind"))?,
980 );
981 }
982 2 => pubkey = expect_bytes(v)?,
983 3 => key_id = expect_bytes(v)?,
984 4 => signature = expect_bytes(v)?,
985 5 => {
986 nested = Some(alloc::boxed::Box::new(node_key_signature_from_value(
987 v,
988 depth + 1,
989 )?))
990 }
991 6 => wrapping_pubkey = expect_bytes(v)?,
992 _ => return Err(TkaError::Decode("unknown signature field")),
993 }
994 }
995
996 Ok(NodeKeySignature {
997 sig_kind: sig_kind.ok_or(TkaError::Decode("signature missing kind"))?,
998 pubkey,
999 key_id,
1000 signature,
1001 nested,
1002 wrapping_pubkey,
1003 })
1004}
1005
1006fn expect_bytes(v: Value) -> Result<Vec<u8>, TkaError> {
1007 match v {
1008 Value::Bytes(b) => Ok(b),
1009 _ => Err(TkaError::Decode("expected byte string")),
1010 }
1011}
1012
1013fn decode_value(buf: &[u8], depth: usize) -> Result<(Value, &[u8]), TkaError> {
1016 if depth > MAX_SIG_NESTING_DEPTH {
1019 return Err(TkaError::Decode("nested signature too deep"));
1020 }
1021 let (major, arg, rest) = decode_head(buf)?;
1022 match major {
1023 0 => Ok((Value::Uint(arg), rest)),
1024 2 => {
1025 let len = arg as usize;
1026 if rest.len() < len {
1027 return Err(TkaError::Decode("byte string truncated"));
1028 }
1029 Ok((Value::Bytes(rest[..len].to_vec()), &rest[len..]))
1030 }
1031 3 => {
1032 let len = arg as usize;
1033 if rest.len() < len {
1034 return Err(TkaError::Decode("text string truncated"));
1035 }
1036 Ok((Value::Text(rest[..len].to_vec()), &rest[len..]))
1037 }
1038 4 => {
1039 let mut items = Vec::new();
1040 let mut cur = rest;
1041 for _ in 0..arg {
1042 let (v, next) = decode_value(cur, depth + 1)?;
1043 items.push(v);
1044 cur = next;
1045 }
1046 Ok((Value::Array(items), cur))
1047 }
1048 5 => {
1049 let mut entries: Vec<(u64, Value)> = Vec::new();
1050 let mut cur = rest;
1051 for _ in 0..arg {
1052 let (k, next) = decode_head(cur).and_then(|(m, a, r)| {
1053 if m == 0 {
1054 Ok((a, r))
1055 } else {
1056 Err(TkaError::Decode("map key not uint"))
1057 }
1058 })?;
1059 if entries.iter().any(|(existing, _)| *existing == k) {
1062 return Err(TkaError::Decode("duplicate map key"));
1063 }
1064 let (v, next2) = decode_value(next, depth + 1)?;
1065 entries.push((k, v));
1066 cur = next2;
1067 }
1068 Ok((Value::IntMap(entries), cur))
1069 }
1070 _ => Err(TkaError::Decode("unsupported CBOR major type")),
1078 }
1079}
1080
1081fn decode_head(buf: &[u8]) -> Result<(u8, u64, &[u8]), TkaError> {
1083 let first = *buf.first().ok_or(TkaError::Decode("empty CBOR"))?;
1084 let major = first >> 5;
1085 let info = first & 0x1f;
1086 let rest = &buf[1..];
1087 let (arg, rest) = match info {
1088 n @ 0..=23 => (n as u64, rest),
1089 24 => {
1090 let b = *rest.first().ok_or(TkaError::Decode("truncated u8"))?;
1091 (b as u64, &rest[1..])
1092 }
1093 25 => {
1094 if rest.len() < 2 {
1095 return Err(TkaError::Decode("truncated u16"));
1096 }
1097 (u16::from_be_bytes([rest[0], rest[1]]) as u64, &rest[2..])
1098 }
1099 26 => {
1100 if rest.len() < 4 {
1101 return Err(TkaError::Decode("truncated u32"));
1102 }
1103 (
1104 u32::from_be_bytes([rest[0], rest[1], rest[2], rest[3]]) as u64,
1105 &rest[4..],
1106 )
1107 }
1108 27 => {
1109 if rest.len() < 8 {
1110 return Err(TkaError::Decode("truncated u64"));
1111 }
1112 let mut b = [0u8; 8];
1113 b.copy_from_slice(&rest[..8]);
1114 (u64::from_be_bytes(b), &rest[8..])
1115 }
1116 _ => return Err(TkaError::Decode("indefinite/reserved CBOR length")),
1117 };
1118 Ok((major, arg, rest))
1119}
1120
1121const BASE32_ALPHABET: &[u8; 32] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
1124
1125fn base32_encode_nopad(data: &[u8]) -> String {
1126 let mut out = String::new();
1127 let mut buffer: u32 = 0;
1128 let mut bits: u32 = 0;
1129 for &b in data {
1130 buffer = (buffer << 8) | b as u32;
1131 bits += 8;
1132 while bits >= 5 {
1133 bits -= 5;
1134 let idx = ((buffer >> bits) & 0x1f) as usize;
1135 out.push(BASE32_ALPHABET[idx] as char);
1136 }
1137 }
1138 if bits > 0 {
1139 let idx = ((buffer << (5 - bits)) & 0x1f) as usize;
1140 out.push(BASE32_ALPHABET[idx] as char);
1141 }
1142 out
1143}
1144
1145fn base32_decode_nopad(text: &str) -> Option<Vec<u8>> {
1146 let mut buffer: u32 = 0;
1147 let mut bits: u32 = 0;
1148 let mut out = Vec::new();
1149 for c in text.chars() {
1150 let val = match c {
1151 'A'..='Z' => c as u32 - 'A' as u32,
1152 '2'..='7' => c as u32 - '2' as u32 + 26,
1153 _ => return None,
1154 };
1155 buffer = (buffer << 5) | val;
1156 bits += 5;
1157 if bits >= 8 {
1158 bits -= 8;
1159 out.push((buffer >> bits) as u8);
1160 }
1161 }
1162 Some(out)
1163}
1164
1165#[cfg(test)]
1166mod tests {
1167 use super::*;
1168
1169 #[test]
1170 fn base32_roundtrip_32_bytes() {
1171 let h = AumHash([0xABu8; 32]);
1172 let text = h.to_base32();
1173 let back = AumHash::from_base32(&text).unwrap();
1174 assert_eq!(h, back);
1175 }
1176
1177 #[test]
1178 fn base32_rejects_wrong_length() {
1179 assert!(AumHash::from_base32("AAAA").is_none());
1181 assert!(AumHash::from_base32("aaaa").is_none());
1183 }
1184
1185 #[test]
1186 fn base32_matches_known_vector() {
1187 assert_eq!(base32_encode_nopad(b"foobar"), "MZXW6YTBOI");
1189 assert_eq!(base32_decode_nopad("MZXW6YTBOI").unwrap(), b"foobar");
1190 }
1191
1192 #[test]
1193 fn credential_signature_cannot_authorize() {
1194 let auth = Authority::from_state(AumHash([0; 32]), State::default());
1195 let sig = NodeKeySignature {
1196 sig_kind: SigKind::Credential,
1197 pubkey: alloc::vec![1, 2, 3],
1198 key_id: alloc::vec![4, 5, 6],
1199 signature: alloc::vec![0; 64],
1200 nested: None,
1201 wrapping_pubkey: Vec::new(),
1202 };
1203 let cbor = sig.to_cbor(true).to_vec();
1204 let err = auth.node_key_authorized(&[1, 2, 3], &cbor).unwrap_err();
1205 assert_eq!(err, TkaError::CredentialCannotAuthorize);
1206 }
1207
1208 #[test]
1209 fn untrusted_key_denied() {
1210 let auth = Authority::from_state(AumHash([0; 32]), State::default());
1212 let sig = NodeKeySignature {
1213 sig_kind: SigKind::Direct,
1214 pubkey: alloc::vec![9; 32],
1215 key_id: alloc::vec![7; 32],
1216 signature: alloc::vec![0; 64],
1217 nested: None,
1218 wrapping_pubkey: Vec::new(),
1219 };
1220 let cbor = sig.to_cbor(true).to_vec();
1221 let err = auth.node_key_authorized(&[9; 32], &cbor).unwrap_err();
1222 assert_eq!(err, TkaError::UntrustedKey);
1223 }
1224
1225 #[test]
1226 fn direct_signature_verifies_end_to_end() {
1227 use ed25519_dalek::{Signer, SigningKey};
1228
1229 let signing = SigningKey::from_bytes(&[42u8; 32]);
1231 let trusted_pub = signing.verifying_key().to_bytes().to_vec();
1232 let node_key = alloc::vec![7u8; 32];
1233
1234 let trusted = Key {
1235 kind: KeyKind::Ed25519,
1236 votes: 1,
1237 public: trusted_pub.clone(),
1238 };
1239 let auth = Authority::from_state(
1240 AumHash([0; 32]),
1241 State {
1242 keys: alloc::vec![trusted],
1243 },
1244 );
1245
1246 let mut sig = NodeKeySignature {
1248 sig_kind: SigKind::Direct,
1249 pubkey: node_key.clone(),
1250 key_id: trusted_pub.clone(),
1251 signature: Vec::new(),
1252 nested: None,
1253 wrapping_pubkey: Vec::new(),
1254 };
1255 let sig_hash = sig.sig_hash();
1256 sig.signature = signing.sign(&sig_hash).to_bytes().to_vec();
1259
1260 let cbor = sig.to_cbor(true).to_vec();
1261 assert!(auth.node_key_authorized(&node_key, &cbor).is_ok());
1262
1263 let other = alloc::vec![8u8; 32];
1265 assert_eq!(
1266 auth.node_key_authorized(&other, &cbor).unwrap_err(),
1267 TkaError::NodeKeyMismatch
1268 );
1269 }
1270
1271 #[test]
1272 fn tampered_signature_denied() {
1273 use ed25519_dalek::{Signer, SigningKey};
1274
1275 let signing = SigningKey::from_bytes(&[42u8; 32]);
1276 let trusted_pub = signing.verifying_key().to_bytes().to_vec();
1277 let node_key = alloc::vec![7u8; 32];
1278 let auth = Authority::from_state(
1279 AumHash([0; 32]),
1280 State {
1281 keys: alloc::vec![Key {
1282 kind: KeyKind::Ed25519,
1283 votes: 1,
1284 public: trusted_pub.clone(),
1285 }],
1286 },
1287 );
1288 let mut sig = NodeKeySignature {
1289 sig_kind: SigKind::Direct,
1290 pubkey: node_key.clone(),
1291 key_id: trusted_pub,
1292 signature: Vec::new(),
1293 nested: None,
1294 wrapping_pubkey: Vec::new(),
1295 };
1296 let sig_hash = sig.sig_hash();
1297 let mut sigbytes = signing.sign(&sig_hash).to_bytes();
1298 sigbytes[0] ^= 0xff; sig.signature = sigbytes.to_vec();
1300
1301 let cbor = sig.to_cbor(true).to_vec();
1302 assert_eq!(
1303 auth.node_key_authorized(&node_key, &cbor).unwrap_err(),
1304 TkaError::BadSignature
1305 );
1306 }
1307
1308 #[test]
1309 fn head_matches_check() {
1310 let h = AumHash([5u8; 32]);
1311 let auth = Authority::from_state(h, State::default());
1312 assert!(auth.head_matches(&h));
1313 assert!(!auth.head_matches(&AumHash([6u8; 32])));
1314 }
1315
1316 #[test]
1319 fn deeply_nested_signature_rejected_without_overflow() {
1320 let mut sig = NodeKeySignature {
1324 sig_kind: SigKind::Direct,
1325 pubkey: alloc::vec![1u8; 32],
1326 key_id: alloc::vec![2u8; 32],
1327 signature: alloc::vec![3u8; 64],
1328 nested: None,
1329 wrapping_pubkey: Vec::new(),
1330 };
1331 for _ in 0..(MAX_SIG_NESTING_DEPTH + 8) {
1332 sig = NodeKeySignature {
1333 sig_kind: SigKind::Rotation,
1334 pubkey: alloc::vec![1u8; 32],
1335 key_id: Vec::new(),
1336 signature: alloc::vec![3u8; 64],
1337 nested: Some(alloc::boxed::Box::new(sig)),
1338 wrapping_pubkey: alloc::vec![1u8; 32],
1339 };
1340 }
1341 let cbor = sig.to_cbor(true).to_vec();
1342 let err = decode_node_key_signature(&cbor).unwrap_err();
1343 assert_eq!(err, TkaError::Decode("nested signature too deep"));
1344 }
1345
1346 #[test]
1349 fn duplicate_map_key_rejected() {
1350 let blob = [0xa2u8, 0x01, 0x00, 0x01, 0x01];
1352 let err = decode_node_key_signature(&blob).unwrap_err();
1353 assert_eq!(err, TkaError::Decode("duplicate map key"));
1354 }
1355
1356 #[test]
1370 fn rotation_chain_verifies_end_to_end() {
1371 use ed25519_dalek::{Signer, SigningKey};
1372
1373 let trusted = SigningKey::from_bytes(&[7u8; 32]);
1375 let trusted_pub = trusted.verifying_key().to_bytes().to_vec();
1376
1377 let wrapping = SigningKey::from_bytes(&[9u8; 32]);
1380 let wrapping_pub = wrapping.verifying_key().to_bytes().to_vec();
1381
1382 let node_key = alloc::vec![5u8; 32];
1383
1384 let auth = Authority::from_state(
1385 AumHash([0; 32]),
1386 State {
1387 keys: alloc::vec![Key {
1388 kind: KeyKind::Ed25519,
1389 votes: 1,
1390 public: trusted_pub.clone(),
1391 }],
1392 },
1393 );
1394
1395 let mut inner = NodeKeySignature {
1398 sig_kind: SigKind::Direct,
1399 pubkey: wrapping_pub.clone(),
1400 key_id: trusted_pub.clone(),
1401 signature: Vec::new(),
1402 nested: None,
1403 wrapping_pubkey: wrapping_pub.clone(),
1404 };
1405 let inner_hash = inner.sig_hash();
1406 inner.signature = trusted.sign(&inner_hash).to_bytes().to_vec();
1407
1408 let mut outer = NodeKeySignature {
1410 sig_kind: SigKind::Rotation,
1411 pubkey: node_key.clone(),
1412 key_id: Vec::new(),
1413 signature: Vec::new(),
1414 nested: Some(alloc::boxed::Box::new(inner)),
1415 wrapping_pubkey: Vec::new(),
1416 };
1417 let outer_hash = outer.sig_hash();
1418 outer.signature = wrapping.sign(&outer_hash).to_bytes().to_vec();
1419
1420 let cbor = outer.to_cbor(true).to_vec();
1421 assert!(auth.node_key_authorized(&node_key, &cbor).is_ok());
1422
1423 let mut tampered = outer.clone();
1425 let mut sb = tampered.signature.clone();
1426 sb[0] ^= 0xff;
1427 tampered.signature = sb;
1428 let cbor_bad = tampered.to_cbor(true).to_vec();
1429 assert_eq!(
1430 auth.node_key_authorized(&node_key, &cbor_bad).unwrap_err(),
1431 TkaError::BadSignature
1432 );
1433 }
1434
1435 #[test]
1438 fn rotation_nested_credential_pubkey_bind() {
1439 use ed25519_dalek::{Signer, SigningKey};
1440
1441 let trusted = SigningKey::from_bytes(&[11u8; 32]);
1442 let trusted_pub = trusted.verifying_key().to_bytes().to_vec();
1443 let wrapping = SigningKey::from_bytes(&[13u8; 32]);
1444 let wrapping_pub = wrapping.verifying_key().to_bytes().to_vec();
1445 let node_key = alloc::vec![6u8; 32];
1446
1447 let auth = Authority::from_state(
1448 AumHash([0; 32]),
1449 State {
1450 keys: alloc::vec![Key {
1451 kind: KeyKind::Ed25519,
1452 votes: 1,
1453 public: trusted_pub.clone(),
1454 }],
1455 },
1456 );
1457
1458 let build = |cred_pubkey: Vec<u8>| -> Vec<u8> {
1460 let mut inner = NodeKeySignature {
1461 sig_kind: SigKind::Credential,
1462 pubkey: cred_pubkey,
1463 key_id: trusted_pub.clone(),
1464 signature: Vec::new(),
1465 nested: None,
1466 wrapping_pubkey: wrapping_pub.clone(),
1467 };
1468 let inner_hash = inner.sig_hash();
1469 inner.signature = trusted.sign(&inner_hash).to_bytes().to_vec();
1470
1471 let mut outer = NodeKeySignature {
1472 sig_kind: SigKind::Rotation,
1473 pubkey: node_key.clone(),
1474 key_id: Vec::new(),
1475 signature: Vec::new(),
1476 nested: Some(alloc::boxed::Box::new(inner)),
1477 wrapping_pubkey: Vec::new(),
1478 };
1479 let outer_hash = outer.sig_hash();
1480 outer.signature = wrapping.sign(&outer_hash).to_bytes().to_vec();
1481 outer.to_cbor(true).to_vec()
1482 };
1483
1484 let cbor_ok = build(wrapping_pub.clone());
1486 assert!(auth.node_key_authorized(&node_key, &cbor_ok).is_ok());
1487
1488 let cbor_bad = build(alloc::vec![0xaau8; 32]);
1491 assert_eq!(
1492 auth.node_key_authorized(&node_key, &cbor_bad).unwrap_err(),
1493 TkaError::NodeKeyMismatch
1494 );
1495 }
1496
1497 fn hex(bytes: &[u8]) -> String {
1501 let mut s = String::new();
1502 for b in bytes {
1503 s.push_str(&alloc::format!("{b:02x}"));
1504 }
1505 s
1506 }
1507
1508 #[test]
1526 fn node_key_signature_cbor_frozen_vector() {
1527 let pubkey: Vec<u8> = (0u8..32).collect();
1529 let key_id: Vec<u8> = (32u8..64).collect();
1530 let signature: Vec<u8> = (64u8..128).collect();
1531
1532 let sig = NodeKeySignature {
1533 sig_kind: SigKind::Direct,
1534 pubkey,
1535 key_id,
1536 signature,
1537 nested: None,
1538 wrapping_pubkey: Vec::new(), };
1540
1541 let full = sig.to_cbor(true).to_vec();
1543 const EXPECTED_FULL: &[u8] = &[
1544 0xa4, 0x01, 0x01, 0x02, 0x58, 0x20, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
1545 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15,
1546 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x03, 0x58, 0x20, 0x20,
1547 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e,
1548 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c,
1549 0x3d, 0x3e, 0x3f, 0x04, 0x58, 0x40, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,
1550 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55,
1551 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63,
1552 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71,
1553 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f,
1554 ];
1555 assert_eq!(
1556 full,
1557 EXPECTED_FULL,
1558 "full CBOR serialization changed (canonical-CBOR encoding drift). actual: {}",
1559 hex(&full)
1560 );
1561
1562 let preimage = sig.to_cbor(false).to_vec();
1564 const EXPECTED_PREIMAGE: &[u8] = &[
1565 0xa3, 0x01, 0x01, 0x02, 0x58, 0x20, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
1566 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15,
1567 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x03, 0x58, 0x20, 0x20,
1568 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e,
1569 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c,
1570 0x3d, 0x3e, 0x3f,
1571 ];
1572 assert_eq!(
1573 preimage,
1574 EXPECTED_PREIMAGE,
1575 "SigHash preimage CBOR changed. actual: {}",
1576 hex(&preimage)
1577 );
1578
1579 let sig_hash = sig.sig_hash();
1581 const EXPECTED_SIG_HASH: [u8; AUM_HASH_LEN] = [
1582 0x22, 0x6f, 0x9c, 0xbc, 0x63, 0x73, 0x92, 0x75, 0x2e, 0x0e, 0xb1, 0x32, 0x9c, 0xc4,
1583 0x99, 0x07, 0x01, 0x4a, 0xb6, 0x4f, 0x8e, 0x5d, 0x82, 0x85, 0xc2, 0x91, 0x42, 0x62,
1584 0xf6, 0xa6, 0xa8, 0x33,
1585 ];
1586 assert_eq!(
1587 sig_hash,
1588 EXPECTED_SIG_HASH,
1589 "sig_hash (BLAKE2s-256 of preimage) changed. actual: {}",
1590 hex(&sig_hash)
1591 );
1592
1593 let aum = aum_hash(&full);
1596 const EXPECTED_AUM_HASH: [u8; AUM_HASH_LEN] = [
1597 0xa4, 0x40, 0x71, 0xa3, 0x7a, 0xbf, 0x80, 0x92, 0xd6, 0xff, 0x23, 0x84, 0xb2, 0xb0,
1598 0xa3, 0x50, 0xc7, 0xcb, 0x48, 0x41, 0xed, 0x68, 0x99, 0x62, 0x41, 0x7c, 0xd4, 0x23,
1599 0x68, 0xdc, 0x72, 0x49,
1600 ];
1601 assert_eq!(
1602 aum.0,
1603 EXPECTED_AUM_HASH,
1604 "aum_hash over full serialization changed. actual: {}",
1605 hex(&aum.0)
1606 );
1607 }
1608
1609 fn unhex(s: &str) -> Vec<u8> {
1613 assert!(s.len().is_multiple_of(2), "odd hex length");
1614 let nib = |c: u8| -> u8 {
1615 match c {
1616 b'0'..=b'9' => c - b'0',
1617 b'a'..=b'f' => c - b'a' + 10,
1618 b'A'..=b'F' => c - b'A' + 10,
1619 _ => panic!("bad hex nibble"),
1620 }
1621 };
1622 let b = s.as_bytes();
1623 let mut out = Vec::with_capacity(s.len() / 2);
1624 let mut i = 0;
1625 while i < b.len() {
1626 out.push((nib(b[i]) << 4) | nib(b[i + 1]));
1627 i += 2;
1628 }
1629 out
1630 }
1631
1632 const SPECCHECK_VECTORS: [(&str, &str, &str); 12] = [
1642 (
1644 "8c93255d71dcab10e8f379c26200f3c7bd5f09d9bc3068d3ef4edeb4853022b6",
1645 "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa",
1646 "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a0000000000000000000000000000000000000000000000000000000000000000",
1647 ),
1648 (
1650 "9bd9f44f4dcc75bd531b56b2cd280b0bb38fc1cd6d1230e14861d861de092e79",
1651 "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa",
1652 "f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43a5bb704786be79fc476f91d3f3f89b03984d8068dcf1bb7dfc6637b45450ac04",
1653 ),
1654 (
1656 "aebf3f2601a0c8c5d39cc7d8911642f740b78168218da8471772b35f9d35b9ab",
1657 "f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43",
1658 "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa8c4bd45aecaca5b24fb97bc10ac27ac8751a7dfe1baff8b953ec9f5833ca260e",
1659 ),
1660 (
1662 "9bd9f44f4dcc75bd531b56b2cd280b0bb38fc1cd6d1230e14861d861de092e79",
1663 "cdb267ce40c5cd45306fa5d2f29731459387dbf9eb933b7bd5aed9a765b88d4d",
1664 "9046a64750444938de19f227bb80485e92b83fdb4b6506c160484c016cc1852f87909e14428a7a1d62e9f22f3d3ad7802db02eb2e688b6c52fcd6648a98bd009",
1665 ),
1666 (
1668 "e47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec4011eaccd55b53f56c",
1669 "cdb267ce40c5cd45306fa5d2f29731459387dbf9eb933b7bd5aed9a765b88d4d",
1670 "160a1cb0dc9c0258cd0a7d23e94d8fa878bcb1925f2c64246b2dee1796bed5125ec6bc982a269b723e0668e540911a9a6a58921d6925e434ab10aa7940551a09",
1671 ),
1672 (
1674 "e47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec4011eaccd55b53f56c",
1675 "cdb267ce40c5cd45306fa5d2f29731459387dbf9eb933b7bd5aed9a765b88d4d",
1676 "21122a84e0b5fca4052f5b1235c80a537878b38f3142356b2c2384ebad4668b7e40bc836dac0f71076f9abe3a53f9c03c1ceeeddb658d0030494ace586687405",
1677 ),
1678 (
1680 "85e241a07d148b41e47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec40",
1681 "442aad9f089ad9e14647b1ef9099a1ff4798d78589e66f28eca69c11f582a623",
1682 "e96f66be976d82e60150baecff9906684aebb1ef181f67a7189ac78ea23b6c0e547f7690a0e2ddcd04d87dbc3490dc19b3b3052f7ff0538cb68afb369ba3a514",
1683 ),
1684 (
1686 "85e241a07d148b41e47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec40",
1687 "442aad9f089ad9e14647b1ef9099a1ff4798d78589e66f28eca69c11f582a623",
1688 "8ce5b96c8f26d0ab6c47958c9e68b937104cd36e13c33566acd2fe8d38aa19427e71f98a473474f2f13f06f97c20d58cc3f54b8bd0d272f42b695dd7e89a8c22",
1689 ),
1690 (
1692 "9bedc267423725d473888631ebf45988bad3db83851ee85c85e241a07d148b41",
1693 "f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43",
1694 "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03be9678ac102edcd92b0210bb34d7428d12ffc5df5f37e359941266a4e35f0f",
1695 ),
1696 (
1698 "9bedc267423725d473888631ebf45988bad3db83851ee85c85e241a07d148b41",
1699 "f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43",
1700 "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffca8c5b64cd208982aa38d4936621a4775aa233aa0505711d8fdcfdaa943d4908",
1701 ),
1702 (
1704 "e96b7021eb39c1a163b6da4e3093dcd3f21387da4cc4572be588fafae23c155b",
1705 "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
1706 "a9d55260f765261eb9b84e106f665e00b867287a761990d7135963ee0a7d59dca5bb704786be79fc476f91d3f3f89b03984d8068dcf1bb7dfc6637b45450ac04",
1707 ),
1708 (
1710 "39a591f5321bbe07fd5a23dc2f39d025d74526615746727ceefd6e82ae65c06f",
1711 "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
1712 "a9d55260f765261eb9b84e106f665e00b867287a761990d7135963ee0a7d59dca5bb704786be79fc476f91d3f3f89b03984d8068dcf1bb7dfc6637b45450ac04",
1713 ),
1714 ];
1715
1716 #[test]
1732 fn ed25519_speccheck_dual_verifier_kat() {
1733 const STD_EXPECT: [bool; 12] = [
1739 true, true, true, true, false, false, false, false, false, false, false, true,
1740 ];
1741 const ZIP215_EXPECT: [bool; 12] = [
1742 true, true, true, true, true, true, false, false, false, true, true, true,
1743 ];
1744
1745 for (i, (msg_hex, pk_hex, sig_hex)) in SPECCHECK_VECTORS.iter().enumerate() {
1746 let msg = unhex(msg_hex);
1747 let pk = unhex(pk_hex);
1748 let sig = unhex(sig_hex);
1749 assert_eq!(pk.len(), 32, "vector {i}: pubkey not 32 bytes");
1750 assert_eq!(sig.len(), 64, "vector {i}: signature not 64 bytes");
1751
1752 let std_ok = verify_ed25519_std(&pk, &msg, &sig).is_ok();
1753 let zip_ok = verify_ed25519_zip215(&pk, &msg, &sig).is_ok();
1754
1755 assert_eq!(
1756 std_ok, STD_EXPECT[i],
1757 "speccheck vector {i}: verify_ed25519_std accept={std_ok}, expected {}",
1758 STD_EXPECT[i]
1759 );
1760 assert_eq!(
1761 zip_ok, ZIP215_EXPECT[i],
1762 "speccheck vector {i}: verify_ed25519_zip215 accept={zip_ok}, expected {}",
1763 ZIP215_EXPECT[i]
1764 );
1765 }
1766
1767 for &i in &[6usize, 7usize] {
1771 let (msg_hex, pk_hex, sig_hex) = SPECCHECK_VECTORS[i];
1772 let (msg, pk, sig) = (unhex(msg_hex), unhex(pk_hex), unhex(sig_hex));
1773 assert!(
1774 verify_ed25519_std(&pk, &msg, &sig).is_err(),
1775 "SECURITY: verify_ed25519_std ACCEPTED S>=L malleability vector {i}"
1776 );
1777 }
1778
1779 {
1783 let (msg_hex, pk_hex, sig_hex) = SPECCHECK_VECTORS[4];
1784 let (msg, pk, sig) = (unhex(msg_hex), unhex(pk_hex), unhex(sig_hex));
1785 assert!(
1786 verify_ed25519_zip215(&pk, &msg, &sig).is_ok(),
1787 "vector 4: ZIP-215 (zebra) should ACCEPT the cofactored discriminator"
1788 );
1789 assert!(
1790 verify_ed25519_std(&pk, &msg, &sig).is_err(),
1791 "vector 4: standard (dalek) should REJECT the cofactored discriminator"
1792 );
1793 }
1794 }
1795
1796 #[test]
1815 fn tka_cbor_matches_go_golden() {
1816 let pubkey32 = unhex("a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf");
1818 let key_id32 = unhex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
1819 let sig64 = unhex(
1820 "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeffe0e1e2e3e4e5e6e7e8e9eaebecedeeefd0d1d2d3d4d5d6d7d8d9dadbdcdddedfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf",
1821 );
1822 let wrap32 = unhex("101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f");
1823 let rot_sig64 = unhex(
1824 "55565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f9091929394",
1825 );
1826
1827 {
1829 let sig = NodeKeySignature {
1830 sig_kind: SigKind::Direct,
1831 pubkey: pubkey32.clone(),
1832 key_id: key_id32.clone(),
1833 signature: sig64.clone(),
1834 nested: None,
1835 wrapping_pubkey: Vec::new(),
1836 };
1837 let full = sig.to_cbor(true).to_vec();
1838 let expected_full = unhex(
1839 "a40101025820a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf035820000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f045840f0f1f2f3f4f5f6f7f8f9fafbfcfdfeffe0e1e2e3e4e5e6e7e8e9eaebecedeeefd0d1d2d3d4d5d6d7d8d9dadbdcdddedfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf",
1840 );
1841 assert_eq!(
1842 full,
1843 expected_full,
1844 "GOLDEN 1 (Direct) full CBOR diverged from Go tka v1.100.0. actual: {}",
1845 hex(&full)
1846 );
1847 let expected_hash =
1848 unhex("7e9653c97d35485b37b9bf942b1861cd2f3cb0663b5bb154f1178cca72101e74");
1849 assert_eq!(
1850 sig.sig_hash().as_slice(),
1851 expected_hash.as_slice(),
1852 "GOLDEN 1 (Direct) sig_hash diverged from Go tka v1.100.0. actual: {}",
1853 hex(&sig.sig_hash())
1854 );
1855 }
1856
1857 {
1859 let sig = NodeKeySignature {
1860 sig_kind: SigKind::Credential,
1861 pubkey: pubkey32.clone(),
1862 key_id: key_id32.clone(),
1863 signature: sig64.clone(),
1864 nested: None,
1865 wrapping_pubkey: Vec::new(),
1866 };
1867 let full = sig.to_cbor(true).to_vec();
1868 let expected_full = unhex(
1869 "a40103025820a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf035820000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f045840f0f1f2f3f4f5f6f7f8f9fafbfcfdfeffe0e1e2e3e4e5e6e7e8e9eaebecedeeefd0d1d2d3d4d5d6d7d8d9dadbdcdddedfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf",
1870 );
1871 assert_eq!(
1872 full,
1873 expected_full,
1874 "GOLDEN 2 (Credential) full CBOR diverged from Go tka v1.100.0. actual: {}",
1875 hex(&full)
1876 );
1877 let expected_hash =
1878 unhex("b6070ea8bc7ae8989ef4293f5031bedaa4a499803ade99f9e2f34dc2898ac03f");
1879 assert_eq!(
1880 sig.sig_hash().as_slice(),
1881 expected_hash.as_slice(),
1882 "GOLDEN 2 (Credential) sig_hash diverged from Go tka v1.100.0. actual: {}",
1883 hex(&sig.sig_hash())
1884 );
1885 }
1886
1887 {
1897 let nested = NodeKeySignature {
1898 sig_kind: SigKind::Direct,
1899 pubkey: pubkey32.clone(),
1900 key_id: key_id32.clone(),
1901 signature: sig64.clone(),
1902 nested: None,
1903 wrapping_pubkey: wrap32.clone(),
1904 };
1905 let sig = NodeKeySignature {
1906 sig_kind: SigKind::Rotation,
1907 pubkey: wrap32.clone(),
1908 key_id: Vec::new(),
1909 signature: rot_sig64.clone(),
1910 nested: Some(alloc::boxed::Box::new(nested)),
1911 wrapping_pubkey: Vec::new(),
1912 };
1913 let full = sig.to_cbor(true).to_vec();
1914 let expected_full = unhex(
1915 "a40102025820101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f04584055565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939405a50101025820a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf035820000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f045840f0f1f2f3f4f5f6f7f8f9fafbfcfdfeffe0e1e2e3e4e5e6e7e8e9eaebecedeeefd0d1d2d3d4d5d6d7d8d9dadbdcdddedfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf065820101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f",
1916 );
1917 assert_eq!(
1918 full,
1919 expected_full,
1920 "GOLDEN 3 (Rotation) full CBOR diverged from Go tka v1.100.0. actual: {}",
1921 hex(&full)
1922 );
1923 let expected_hash =
1924 unhex("fac0a5a6781bb945369c28a0b3d3eea04e1648b60ec1a990a1ff68a9a566e6a7");
1925 assert_eq!(
1926 sig.sig_hash().as_slice(),
1927 expected_hash.as_slice(),
1928 "GOLDEN 3 (Rotation) sig_hash diverged from Go tka v1.100.0. actual: {}",
1929 hex(&sig.sig_hash())
1930 );
1931 }
1932 }
1933
1934 #[test]
1950 fn ed25519_dual_verifier_matches_go_verdicts() {
1951 const GO_STD_ACCEPT: [bool; 12] = [
1953 true, true, true, true, false, false, false, false, false, false, false, true,
1954 ];
1955 const GO_ZIP215_ACCEPT: [bool; 12] = [
1956 true, true, true, true, true, true, false, false, false, true, true, true,
1957 ];
1958
1959 for (i, (msg_hex, pk_hex, sig_hex)) in SPECCHECK_VECTORS.iter().enumerate() {
1960 let msg = unhex(msg_hex);
1961 let pk = unhex(pk_hex);
1962 let sig = unhex(sig_hex);
1963
1964 let std_ok = verify_ed25519_std(&pk, &msg, &sig).is_ok();
1965 let zip_ok = verify_ed25519_zip215(&pk, &msg, &sig).is_ok();
1966
1967 assert_eq!(
1968 std_ok, GO_STD_ACCEPT[i],
1969 "vector {i}: Rust verify_ed25519_std accept={std_ok} disagrees with Go \
1970 crypto/ed25519.Verify={}",
1971 GO_STD_ACCEPT[i]
1972 );
1973 assert_eq!(
1974 zip_ok, GO_ZIP215_ACCEPT[i],
1975 "vector {i}: Rust verify_ed25519_zip215 accept={zip_ok} disagrees with Go \
1976 ed25519consensus.Verify={}",
1977 GO_ZIP215_ACCEPT[i]
1978 );
1979 }
1980 }
1981
1982 #[test]
1988 fn aum_serialize_matches_go_test_serialization_vectors() {
1989 let add_key_inner_zero_key = cbor::Value::IntMap(alloc::vec![
1995 (1, cbor::Value::Uint(0)), (2, cbor::Value::Uint(0)), (3, cbor::Value::Null), ]);
1999 assert_eq!(
2000 add_key_inner_zero_key.to_vec(),
2001 alloc::vec![0xa3, 0x01, 0x00, 0x02, 0x00, 0x03, 0xf6],
2002 "Go's zero Key{{}} encodes as map(3){{kind=0, votes=0, public=null}}"
2003 );
2004
2005 let remove_key = Aum {
2007 message_kind: AumKind::RemoveKey,
2008 prev_aum_hash: None,
2009 key: None,
2010 key_id: alloc::vec![1, 2],
2011 state: None,
2012 votes: None,
2013 meta: Vec::new(),
2014 signatures: Vec::new(),
2015 };
2016 assert_eq!(
2017 remove_key.serialize(),
2018 alloc::vec![0xa3, 0x01, 0x02, 0x02, 0xf6, 0x04, 0x42, 0x01, 0x02],
2020 "RemoveKey AUM serialization must match Go TestSerialization byte-for-byte"
2021 );
2022
2023 let update_key = Aum {
2026 message_kind: AumKind::UpdateKey,
2027 prev_aum_hash: None,
2028 key: None,
2029 key_id: alloc::vec![1, 2],
2030 state: None,
2031 votes: Some(2),
2032 meta: alloc::vec![("a".into(), "b".into())],
2033 signatures: Vec::new(),
2034 };
2035 assert_eq!(
2036 update_key.serialize(),
2037 alloc::vec![
2040 0xa5, 0x01, 0x04, 0x02, 0xf6, 0x04, 0x42, 0x01, 0x02, 0x06, 0x02, 0x07, 0xa1, 0x61,
2041 0x61, 0x61, 0x62
2042 ],
2043 "UpdateKey AUM serialization must match Go TestSerialization byte-for-byte"
2044 );
2045
2046 let with_sig = Aum {
2048 message_kind: AumKind::AddKey,
2049 prev_aum_hash: None,
2050 key: None,
2051 key_id: Vec::new(),
2052 state: None,
2053 votes: None,
2054 meta: Vec::new(),
2055 signatures: alloc::vec![AumSignature {
2056 key_id: alloc::vec![1],
2057 signature: Vec::new(),
2058 }],
2059 };
2060 assert_eq!(
2061 with_sig.serialize(),
2062 alloc::vec![
2065 0xa3, 0x01, 0x01, 0x02, 0xf6, 0x17, 0x81, 0xa2, 0x01, 0x41, 0x01, 0x02, 0xf6
2066 ],
2067 "Signature AUM serialization must match Go TestSerialization (key 23 + nil sig = null)"
2068 );
2069
2070 let no_sig = Aum {
2073 signatures: Vec::new(),
2074 ..with_sig.clone()
2075 };
2076 assert_eq!(
2077 with_sig.sig_hash(),
2078 blake2s_256(&no_sig.serialize()),
2079 "SigHash preimage must omit key 23 (Signatures), matching Go AUM.SigHash"
2080 );
2081 assert_ne!(
2083 with_sig.hash().0,
2084 with_sig.sig_hash(),
2085 "Hash (incl. signatures) must differ from SigHash (excl.) when signatures are present"
2086 );
2087 }
2088
2089 #[test]
2093 fn aum_checkpoint_state_serialization() {
2094 let checkpoint = Aum {
2095 message_kind: AumKind::Checkpoint,
2096 prev_aum_hash: Some(AumHash([0u8; AUM_HASH_LEN])),
2097 key: None,
2098 key_id: Vec::new(),
2099 state: Some(AumState {
2100 last_aum_hash: Some(AumHash([0u8; AUM_HASH_LEN])),
2101 disablement_values: Some(Vec::new()),
2102 keys: Some(alloc::vec![AumKey {
2103 kind: KeyKind::Ed25519,
2104 votes: 1,
2105 public: alloc::vec![5, 6],
2106 meta: Vec::new(),
2107 }]),
2108 state_id1: 0,
2109 state_id2: 0,
2110 }),
2111 votes: None,
2112 meta: Vec::new(),
2113 signatures: Vec::new(),
2114 };
2115 let bytes = checkpoint.serialize();
2116 assert_eq!(
2120 &bytes[0..3],
2121 &[0xa3, 0x01, 0x05],
2122 "map(3), MessageKind=Checkpoint(5)"
2123 );
2124 assert_eq!(
2125 &bytes[3..6],
2126 &[0x02, 0x58, 0x20],
2127 "key2 prev = 32-byte byte string head"
2128 );
2129 assert_eq!(bytes[38], 0x05, "key 5 = State");
2133 assert_eq!(
2135 &bytes[39..42],
2136 &[0xa3, 0x01, 0x58],
2137 "State map(3), key1 LastAUMHash bytes"
2138 );
2139 let tail = &bytes[bytes.len() - 4..];
2141 assert_eq!(
2142 tail,
2143 &[0x03, 0x42, 0x05, 0x06],
2144 "Key.Public (key 3) = bytes{{5,6}}"
2145 );
2146 assert_eq!(checkpoint.hash(), checkpoint.hash());
2148 }
2149
2150 fn test_aum_key(seed: u8, votes: u32) -> AumKey {
2154 use ed25519_dalek::SigningKey;
2155 let pubk = SigningKey::from_bytes(&[seed; 32])
2156 .verifying_key()
2157 .to_bytes()
2158 .to_vec();
2159 AumKey {
2160 kind: KeyKind::Ed25519,
2161 votes,
2162 public: pubk,
2163 meta: Vec::new(),
2164 }
2165 }
2166
2167 fn genesis_add(key: AumKey) -> Aum {
2169 Aum {
2170 message_kind: AumKind::AddKey,
2171 prev_aum_hash: None,
2172 key: Some(key),
2173 key_id: Vec::new(),
2174 state: None,
2175 votes: None,
2176 meta: Vec::new(),
2177 signatures: Vec::new(),
2178 }
2179 }
2180
2181 fn child(parent: &Aum, kind: AumKind, key: Option<AumKey>, key_id: Vec<u8>) -> Aum {
2183 Aum {
2184 message_kind: kind,
2185 prev_aum_hash: Some(parent.hash()),
2186 key,
2187 key_id,
2188 state: None,
2189 votes: None,
2190 meta: Vec::new(),
2191 signatures: Vec::new(),
2192 }
2193 }
2194
2195 #[test]
2198 fn replay_linear_chain_folds_all_kinds() {
2199 let k0 = test_aum_key(1, 1);
2200 let k1 = test_aum_key(2, 1);
2201
2202 let a0 = genesis_add(k0.clone());
2203 let a1 = child(&a0, AumKind::AddKey, Some(k1.clone()), Vec::new());
2204 let mut a2 = child(&a1, AumKind::UpdateKey, None, k1.public.clone());
2205 a2.votes = Some(5);
2206 let a3 = child(&a2, AumKind::RemoveKey, None, k0.public.clone());
2207
2208 let auth = Authority::from_chain(&[a0, a1, a2, a3.clone()]).unwrap();
2209
2210 assert_eq!(auth.state().keys.len(), 1, "k0 removed, k1 remains");
2212 let remaining = &auth.state().keys[0];
2213 assert_eq!(remaining.public, k1.public, "k1 is the surviving key");
2214 assert_eq!(remaining.votes, 5, "UpdateKey raised k1's votes to 5");
2215 assert_eq!(auth.head(), a3.hash(), "head = last AUM hash");
2217 }
2218
2219 #[test]
2221 fn replay_rejects_broken_parent_link() {
2222 let k0 = test_aum_key(1, 1);
2223 let k1 = test_aum_key(2, 1);
2224 let a0 = genesis_add(k0);
2225 let mut a1 = child(&a0, AumKind::AddKey, Some(k1), Vec::new());
2227 a1.prev_aum_hash = Some(AumHash([0xab; 32]));
2228 assert_eq!(
2229 Authority::from_chain(&[a0, a1]).unwrap_err(),
2230 TkaError::BadParent
2231 );
2232 }
2233
2234 #[test]
2236 fn replay_rejects_bad_key_state() {
2237 let k0 = test_aum_key(1, 1);
2238 let a0 = genesis_add(k0.clone());
2239 let dup = child(&a0, AumKind::AddKey, Some(k0.clone()), Vec::new());
2241 assert_eq!(
2242 Authority::from_chain(&[a0.clone(), dup]).unwrap_err(),
2243 TkaError::BadKeyState
2244 );
2245 let absent = test_aum_key(9, 1);
2247 let rm = child(&a0, AumKind::RemoveKey, None, absent.public.clone());
2248 assert_eq!(
2249 Authority::from_chain(&[a0, rm]).unwrap_err(),
2250 TkaError::BadKeyState
2251 );
2252 }
2253
2254 #[test]
2256 fn replay_empty_chain_is_bad_chain() {
2257 assert_eq!(Authority::from_chain(&[]).unwrap_err(), TkaError::BadChain);
2258 }
2259
2260 #[test]
2263 fn replay_weight_dedups_and_ignores_unknown() {
2264 let k0 = test_aum_key(1, 2);
2265 let k1 = test_aum_key(2, 3);
2266 let state = ReplayState {
2267 keys: alloc::vec![k0.clone(), k1.clone()],
2268 last_aum_hash: None,
2269 state_id: None,
2270 };
2271
2272 let mut aum = genesis_add(test_aum_key(5, 1));
2274 assert_eq!(state.weight(&aum), 0);
2275
2276 aum.signatures = alloc::vec![AumSignature {
2278 key_id: k0.public.clone(),
2279 signature: Vec::new()
2280 }];
2281 assert_eq!(state.weight(&aum), 2);
2282
2283 aum.signatures = alloc::vec![
2285 AumSignature {
2286 key_id: k0.public.clone(),
2287 signature: Vec::new()
2288 },
2289 AumSignature {
2290 key_id: k1.public.clone(),
2291 signature: Vec::new()
2292 },
2293 ];
2294 assert_eq!(state.weight(&aum), 5);
2295
2296 aum.signatures = alloc::vec![
2298 AumSignature {
2299 key_id: k0.public.clone(),
2300 signature: Vec::new()
2301 },
2302 AumSignature {
2303 key_id: k0.public.clone(),
2304 signature: Vec::new()
2305 },
2306 ];
2307 assert_eq!(state.weight(&aum), 2, "a key signing twice counts once");
2308
2309 aum.signatures = alloc::vec![AumSignature {
2311 key_id: alloc::vec![0xff; 32],
2312 signature: Vec::new()
2313 }];
2314 assert_eq!(
2315 state.weight(&aum),
2316 0,
2317 "an untrusted signing key contributes no weight"
2318 );
2319 }
2320
2321 #[test]
2325 fn pick_next_aum_lowest_hash_tiebreak_is_order_independent() {
2326 let k = test_aum_key(1, 1);
2327 let a0 = genesis_add(k);
2328 let c1 = child(&a0, AumKind::NoOp, None, alloc::vec![1]);
2330 let c2 = child(&a0, AumKind::NoOp, None, alloc::vec![2]);
2331 let state = ReplayState::default();
2332
2333 let lower = if c1.hash().0 < c2.hash().0 {
2334 c1.hash()
2335 } else {
2336 c2.hash()
2337 };
2338 let ab = [c1.clone(), c2.clone()];
2339 let ba = [c2, c1];
2340 let pick_ab = pick_next_aum(&state, &ab).hash();
2341 let pick_ba = pick_next_aum(&state, &ba).hash();
2342 assert_eq!(pick_ab, lower, "lowest hash wins");
2343 assert_eq!(
2344 pick_ab, pick_ba,
2345 "selection is independent of candidate order"
2346 );
2347 }
2348
2349 #[test]
2352 fn pick_next_aum_weight_beats_hash() {
2353 use ed25519_dalek::SigningKey;
2354 let signer_seed = 3u8;
2355 let signer_pub = SigningKey::from_bytes(&[signer_seed; 32])
2356 .verifying_key()
2357 .to_bytes()
2358 .to_vec();
2359 let state = ReplayState {
2360 keys: alloc::vec![AumKey {
2361 kind: KeyKind::Ed25519,
2362 votes: 4,
2363 public: signer_pub.clone(),
2364 meta: Vec::new(),
2365 }],
2366 last_aum_hash: None,
2367 state_id: None,
2368 };
2369
2370 let a0 = genesis_add(test_aum_key(1, 1));
2371 let unsigned = child(&a0, AumKind::NoOp, None, alloc::vec![1]);
2372 let mut signed = child(&a0, AumKind::NoOp, None, alloc::vec![2]);
2373 signed.signatures = alloc::vec![AumSignature {
2374 key_id: signer_pub,
2375 signature: Vec::new(),
2376 }];
2377
2378 let candidates = [unsigned.clone(), signed.clone()];
2380 let winner = pick_next_aum(&state, &candidates);
2381 assert_eq!(
2382 winner.hash(),
2383 signed.hash(),
2384 "higher weight wins over lower hash"
2385 );
2386 }
2387
2388 #[test]
2391 fn forked_chain_prefers_removekey_branch() {
2392 let k0 = test_aum_key(1, 1);
2393 let k1 = test_aum_key(2, 1);
2394 let a0 = genesis_add(k0.clone());
2396 let a1 = child(&a0, AumKind::AddKey, Some(k1.clone()), Vec::new());
2397 let branch_remove = child(&a1, AumKind::RemoveKey, None, k0.public.clone());
2399 let branch_noop = child(&a1, AumKind::NoOp, None, alloc::vec![9]);
2400
2401 let noop_branch = [branch_noop.clone()];
2402 let remove_branch = [branch_remove.clone()];
2403 let auth = Authority::from_forked_chain(&[a0, a1], &[&noop_branch[..], &remove_branch[..]])
2404 .unwrap();
2405
2406 assert_eq!(auth.state().keys.len(), 1);
2408 assert_eq!(auth.state().keys[0].public, k1.public);
2409 assert_eq!(
2410 auth.head(),
2411 branch_remove.hash(),
2412 "active head = RemoveKey branch"
2413 );
2414 }
2415
2416 #[test]
2420 fn replayed_authority_authorizes_node_end_to_end() {
2421 use ed25519_dalek::{Signer, SigningKey};
2422
2423 let signing = SigningKey::from_bytes(&[77u8; 32]);
2424 let trusted_pub = signing.verifying_key().to_bytes().to_vec();
2425 let trusted = AumKey {
2426 kind: KeyKind::Ed25519,
2427 votes: 1,
2428 public: trusted_pub.clone(),
2429 meta: Vec::new(),
2430 };
2431 let revoked_signing = SigningKey::from_bytes(&[88u8; 32]);
2433 let revoked_pub = revoked_signing.verifying_key().to_bytes().to_vec();
2434 let revoked = AumKey {
2435 kind: KeyKind::Ed25519,
2436 votes: 1,
2437 public: revoked_pub.clone(),
2438 meta: Vec::new(),
2439 };
2440
2441 let a0 = genesis_add(trusted);
2442 let a1 = child(&a0, AumKind::AddKey, Some(revoked), Vec::new());
2443 let a2 = child(&a1, AumKind::RemoveKey, None, revoked_pub.clone());
2444 let auth = Authority::from_chain(&[a0, a1, a2]).unwrap();
2445
2446 let node_key = alloc::vec![7u8; 32];
2447 let mut sig = NodeKeySignature {
2449 sig_kind: SigKind::Direct,
2450 pubkey: node_key.clone(),
2451 key_id: trusted_pub.clone(),
2452 signature: Vec::new(),
2453 nested: None,
2454 wrapping_pubkey: Vec::new(),
2455 };
2456 sig.signature = signing.sign(&sig.sig_hash()).to_bytes().to_vec();
2457 assert!(
2458 auth.node_key_authorized(&node_key, &sig.to_cbor(true).to_vec())
2459 .is_ok(),
2460 "the replayed authority must authorize a node signed by a still-trusted key"
2461 );
2462
2463 let mut bad = NodeKeySignature {
2465 sig_kind: SigKind::Direct,
2466 pubkey: node_key.clone(),
2467 key_id: revoked_pub.clone(),
2468 signature: Vec::new(),
2469 nested: None,
2470 wrapping_pubkey: Vec::new(),
2471 };
2472 bad.signature = revoked_signing.sign(&bad.sig_hash()).to_bytes().to_vec();
2473 assert_eq!(
2474 auth.node_key_authorized(&node_key, &bad.to_cbor(true).to_vec())
2475 .unwrap_err(),
2476 TkaError::UntrustedKey,
2477 "a key the chain removed must not authorize"
2478 );
2479 }
2480
2481 #[test]
2484 fn replay_rejects_invalid_genesis_kind() {
2485 let mut g = genesis_add(test_aum_key(1, 1));
2488 g.message_kind = AumKind::UpdateKey;
2489 g.key = None;
2490 g.key_id = test_aum_key(1, 1).public.clone();
2491 assert_eq!(
2492 Authority::from_chain(&[g]).unwrap_err(),
2493 TkaError::BadChain,
2494 "an UpdateKey cannot be a genesis AUM"
2495 );
2496 }
2497
2498 #[test]
2501 fn replay_rejects_genesis_with_parent() {
2502 let mut g = genesis_add(test_aum_key(1, 1));
2503 g.prev_aum_hash = Some(AumHash([0x11; 32])); assert_eq!(
2505 Authority::from_chain(&[g]).unwrap_err(),
2506 TkaError::BadParent,
2507 "a genesis AUM that names a parent must be rejected (not treated as genesis)"
2508 );
2509 }
2510
2511 #[test]
2514 fn replay_rejects_checkpoint_stateid_mismatch() {
2515 let k = test_aum_key(1, 1);
2516 let genesis = Aum {
2518 message_kind: AumKind::Checkpoint,
2519 prev_aum_hash: None,
2520 key: None,
2521 key_id: Vec::new(),
2522 state: Some(AumState {
2523 last_aum_hash: None,
2524 disablement_values: Some(Vec::new()),
2525 keys: Some(alloc::vec![k.clone()]),
2526 state_id1: 7,
2527 state_id2: 0,
2528 }),
2529 votes: None,
2530 meta: Vec::new(),
2531 signatures: Vec::new(),
2532 };
2533 let bad = Aum {
2535 message_kind: AumKind::Checkpoint,
2536 prev_aum_hash: Some(genesis.hash()),
2537 key: None,
2538 key_id: Vec::new(),
2539 state: Some(AumState {
2540 last_aum_hash: Some(genesis.hash()),
2541 disablement_values: Some(Vec::new()),
2542 keys: Some(alloc::vec![k.clone()]),
2543 state_id1: 8, state_id2: 0,
2545 }),
2546 votes: None,
2547 meta: Vec::new(),
2548 signatures: Vec::new(),
2549 };
2550 assert_eq!(
2551 Authority::from_chain(&[genesis.clone(), bad]).unwrap_err(),
2552 TkaError::BadKeyState,
2553 "a checkpoint with a foreign StateID belongs to another authority and must be rejected"
2554 );
2555 let ok = Aum {
2557 message_kind: AumKind::Checkpoint,
2558 prev_aum_hash: Some(genesis.hash()),
2559 key: None,
2560 key_id: Vec::new(),
2561 state: Some(AumState {
2562 last_aum_hash: Some(genesis.hash()),
2563 disablement_values: Some(Vec::new()),
2564 keys: Some(alloc::vec![k]),
2565 state_id1: 7,
2566 state_id2: 0,
2567 }),
2568 votes: None,
2569 meta: Vec::new(),
2570 signatures: Vec::new(),
2571 };
2572 assert!(Authority::from_chain(&[genesis, ok]).is_ok());
2573 }
2574
2575 #[test]
2578 fn forked_chain_rejects_multistep_branch() {
2579 let k0 = test_aum_key(1, 1);
2580 let a0 = genesis_add(k0.clone());
2581 let b1 = child(&a0, AumKind::NoOp, None, alloc::vec![1]);
2582 let b2 = child(&b1, AumKind::NoOp, None, alloc::vec![2]);
2584 let single = [child(&a0, AumKind::NoOp, None, alloc::vec![3])];
2585 let multi = [b1, b2];
2586 assert_eq!(
2587 Authority::from_forked_chain(&[a0], &[&single[..], &multi[..]]).unwrap_err(),
2588 TkaError::BadChain,
2589 "a multi-step branch must be rejected, not judged by its first AUM"
2590 );
2591 }
2592
2593 #[test]
2613 fn aum_hash_sighash_matches_go_golden() {
2614 let prev = AumHash({
2616 let mut a = [0u8; AUM_HASH_LEN];
2617 let mut i = 0;
2618 while i < AUM_HASH_LEN {
2619 a[i] = 0x20u8.wrapping_add(i as u8);
2620 i += 1;
2621 }
2622 a
2623 });
2624 let key_pub: Vec<u8> = (0..32u16).map(|i| 0x40u8.wrapping_add(i as u8)).collect();
2625 let key_pub2: Vec<u8> = (0..32u16).map(|i| 0x60u8.wrapping_add(i as u8)).collect();
2626 let sig_bytes: Vec<u8> = (0..64u16).map(|i| 0x80u8.wrapping_add(i as u8)).collect();
2627
2628 let check = |label: &str, aum: &Aum, ser_hex: &str, hash_hex: &str, sig_hash_hex: &str| {
2630 assert_eq!(
2631 hex(&aum.serialize()),
2632 ser_hex,
2633 "{label}: Aum::serialize diverged from Go tka v1.100.0"
2634 );
2635 assert_eq!(
2636 hex(&aum.hash().0),
2637 hash_hex,
2638 "{label}: Aum::hash (Go AUM.Hash) diverged from Go tka v1.100.0"
2639 );
2640 assert_eq!(
2641 hex(&aum.sig_hash()),
2642 sig_hash_hex,
2643 "{label}: Aum::sig_hash (Go AUM.SigHash) diverged from Go tka v1.100.0"
2644 );
2645 };
2646
2647 let add_key = Aum {
2649 message_kind: AumKind::AddKey,
2650 prev_aum_hash: None,
2651 key: Some(AumKey {
2652 kind: KeyKind::Ed25519,
2653 votes: 7,
2654 public: key_pub.clone(),
2655 meta: alloc::vec![("name".into(), "alpha".into())],
2656 }),
2657 key_id: Vec::new(),
2658 state: None,
2659 votes: None,
2660 meta: Vec::new(),
2661 signatures: Vec::new(),
2662 };
2663 check(
2664 "AddKey",
2665 &add_key,
2666 "a3010102f603a401010207035820404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f0ca1646e616d6565616c706861",
2667 "921ca301077ae2b892ca8c40b3315e5f2a9ccc9ac99eec784e93b323577e1e14",
2668 "921ca301077ae2b892ca8c40b3315e5f2a9ccc9ac99eec784e93b323577e1e14",
2669 );
2670
2671 let remove_key = Aum {
2673 message_kind: AumKind::RemoveKey,
2674 prev_aum_hash: Some(prev),
2675 key: None,
2676 key_id: key_pub.clone(),
2677 state: None,
2678 votes: None,
2679 meta: Vec::new(),
2680 signatures: Vec::new(),
2681 };
2682 check(
2683 "RemoveKey",
2684 &remove_key,
2685 "a30102025820202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f045820404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f",
2686 "46be17c398760d2e649147b06a68e8576ee37c59cf0558046182f48ba20aa912",
2687 "46be17c398760d2e649147b06a68e8576ee37c59cf0558046182f48ba20aa912",
2688 );
2689
2690 let update_key = Aum {
2692 message_kind: AumKind::UpdateKey,
2693 prev_aum_hash: Some(prev),
2694 key: None,
2695 key_id: key_pub.clone(),
2696 state: None,
2697 votes: Some(2),
2698 meta: alloc::vec![("role".into(), "ci".into())],
2699 signatures: Vec::new(),
2700 };
2701 check(
2702 "UpdateKey",
2703 &update_key,
2704 "a50104025820202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f045820404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f060207a164726f6c65626369",
2705 "42d160d81a0922511b4ce60050dba76569f79423b6e721c5040faf414c250e43",
2706 "42d160d81a0922511b4ce60050dba76569f79423b6e721c5040faf414c250e43",
2707 );
2708
2709 let signed = Aum {
2712 message_kind: AumKind::AddKey,
2713 prev_aum_hash: Some(prev),
2714 key: Some(AumKey {
2715 kind: KeyKind::Ed25519,
2716 votes: 1,
2717 public: key_pub.clone(),
2718 meta: Vec::new(),
2719 }),
2720 key_id: Vec::new(),
2721 state: None,
2722 votes: None,
2723 meta: Vec::new(),
2724 signatures: alloc::vec![AumSignature {
2725 key_id: key_pub2.clone(),
2726 signature: sig_bytes.clone(),
2727 }],
2728 };
2729 check(
2730 "AddKey+Signature",
2731 &signed,
2732 "a40101025820202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f03a301010201035820404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f1781a2015820606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f025840808182838485868788898a8b8c8d8e8f909192939495969798999a9b9c9d9e9fa0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf",
2733 "e70332d9a03b205577204f1896bb8dcb7c8f8894cc87a5b5c4d5dabcdf6ef135",
2734 "0a7a0ecdf854ad99e8728a1de89ac23c1f08457132a537a3add9594749a7f536",
2735 );
2736 assert_ne!(
2737 hex(&signed.hash().0),
2738 hex(&signed.sig_hash()),
2739 "Hash() must cover Signatures while SigHash() excludes them (Go AUM.SigHash nils them)"
2740 );
2741
2742 let checkpoint = Aum {
2745 message_kind: AumKind::Checkpoint,
2746 prev_aum_hash: Some(prev),
2747 key: None,
2748 key_id: Vec::new(),
2749 state: Some(AumState {
2750 last_aum_hash: Some(prev),
2751 disablement_values: Some(alloc::vec![alloc::vec![0xaa, 0xbb]]),
2752 keys: Some(alloc::vec![AumKey {
2753 kind: KeyKind::Ed25519,
2754 votes: 1,
2755 public: key_pub.clone(),
2756 meta: Vec::new(),
2757 }]),
2758 state_id1: 0,
2759 state_id2: 0,
2760 }),
2761 votes: None,
2762 meta: Vec::new(),
2763 signatures: Vec::new(),
2764 };
2765 check(
2766 "Checkpoint(populated DisablementValues)",
2767 &checkpoint,
2768 "a30105025820202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f05a3015820202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f028142aabb0381a301010201035820404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5f",
2769 "38c35c51580dcb75212d79c07695c8ec8b399b59ba552ea93f080b126fdaa0ae",
2770 "38c35c51580dcb75212d79c07695c8ec8b399b59ba552ea93f080b126fdaa0ae",
2771 );
2772 }
2773
2774 #[test]
2784 fn aum_checkpoint_nil_disablement_matches_go() {
2785 let prev = AumHash({
2786 let mut a = [0u8; AUM_HASH_LEN];
2787 let mut i = 0;
2788 while i < AUM_HASH_LEN {
2789 a[i] = 0x20u8.wrapping_add(i as u8);
2790 i += 1;
2791 }
2792 a
2793 });
2794 let key_pub: Vec<u8> = (0..32u16).map(|i| 0x40u8.wrapping_add(i as u8)).collect();
2795 let key_pub2: Vec<u8> = (0..32u16).map(|i| 0x60u8.wrapping_add(i as u8)).collect();
2796
2797 let checkpoint = Aum {
2799 message_kind: AumKind::Checkpoint,
2800 prev_aum_hash: Some(prev),
2801 key: None,
2802 key_id: Vec::new(),
2803 state: Some(AumState {
2804 last_aum_hash: Some(prev),
2805 disablement_values: Some(Vec::new()),
2806 keys: Some(alloc::vec![
2807 AumKey {
2808 kind: KeyKind::Ed25519,
2809 votes: 1,
2810 public: key_pub.clone(),
2811 meta: Vec::new(),
2812 },
2813 AumKey {
2814 kind: KeyKind::Ed25519,
2815 votes: 3,
2816 public: key_pub2.clone(),
2817 meta: alloc::vec![("k".into(), "v".into())],
2818 },
2819 ]),
2820 state_id1: 0,
2821 state_id2: 0,
2822 }),
2823 votes: None,
2824 meta: Vec::new(),
2825 signatures: Vec::new(),
2826 };
2827
2828 const GO_SERIALIZE: &str = "a30105025820202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f05a3015820202122232425262728292a2b2c2d2e2f303132333435363738393a3b3c3d3e3f02f60382a301010201035820404142434445464748494a4b4c4d4e4f505152535455565758595a5b5c5d5e5fa401010203035820606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f0ca1616b6176";
2834 const GO_HASH: &str = "cae17cc938c5a954cd4389d83c6afe4d3487edac38b94824bec3312b82f35710";
2835
2836 let checkpoint = {
2839 let mut c = checkpoint;
2840 if let Some(state) = c.state.as_mut() {
2841 state.disablement_values = None;
2842 }
2843 c
2844 };
2845
2846 assert_eq!(
2847 hex(&checkpoint.serialize()),
2848 GO_SERIALIZE,
2849 "nil DisablementValues must encode as CBOR null (0xf6), byte-matching Go"
2850 );
2851 assert_eq!(
2852 hex(&checkpoint.hash().0),
2853 GO_HASH,
2854 "with the nil-vs-empty fix, the checkpoint chain-link Hash matches Go"
2855 );
2856 }
2857}