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}
200
201impl NodeKeySignature {
202 fn sig_hash(&self) -> [u8; AUM_HASH_LEN] {
206 let v = self.to_cbor(false);
207 blake2s_256(&v.to_vec())
208 }
209
210 fn to_cbor(&self, include_signature: bool) -> Value {
213 cbor::int_map([
214 (1, Some(Value::Uint(self.sig_kind as u8 as u64))),
215 (2, nonempty_bytes(&self.pubkey)),
216 (3, nonempty_bytes(&self.key_id)),
217 (
218 4,
219 if include_signature {
220 nonempty_bytes(&self.signature)
221 } else {
222 None
223 },
224 ),
225 (5, self.nested.as_ref().map(|n| n.to_cbor(true))),
226 (6, nonempty_bytes(&self.wrapping_pubkey)),
227 ])
228 }
229
230 fn authorizing_key_id(&self) -> Result<&[u8], TkaError> {
233 match self.sig_kind {
234 SigKind::Rotation => self
235 .nested
236 .as_ref()
237 .ok_or(TkaError::Decode("rotation signature missing nested"))?
238 .authorizing_key_id(),
239 SigKind::Direct | SigKind::Credential => Ok(&self.key_id),
240 SigKind::Invalid => Err(TkaError::Decode("invalid signature kind")),
241 }
242 }
243
244 fn verify_signature(&self, node_key: &[u8], verification_key: &Key) -> Result<(), TkaError> {
247 if self.sig_kind != SigKind::Credential && self.pubkey != node_key {
249 return Err(TkaError::NodeKeyMismatch);
250 }
251
252 let sig_hash = self.sig_hash();
253
254 match self.sig_kind {
255 SigKind::Rotation => {
256 let nested = self
257 .nested
258 .as_ref()
259 .ok_or(TkaError::Decode("rotation signature missing nested"))?;
260 let verify_pub = &nested.wrapping_pubkey;
263 if verify_pub.len() != 32 {
264 return Err(TkaError::Decode("wrapping pubkey wrong length"));
265 }
266 verify_ed25519_std(verify_pub, &sig_hash, &self.signature)?;
267 if nested.sig_kind == SigKind::Credential && nested.pubkey != *verify_pub {
273 return Err(TkaError::NodeKeyMismatch);
274 }
275 nested.verify_signature(verify_pub, verification_key)
277 }
278 SigKind::Direct | SigKind::Credential => {
279 if self.nested.is_some() {
280 return Err(TkaError::Decode("direct/credential signature has nested"));
281 }
282 if verification_key.kind != KeyKind::Ed25519 || verification_key.public.len() != 32
283 {
284 return Err(TkaError::Decode("verification key not ed25519"));
285 }
286 verify_ed25519_zip215(&verification_key.public, &sig_hash, &self.signature)
289 }
290 SigKind::Invalid => Err(TkaError::Decode("invalid signature kind")),
291 }
292 }
293}
294
295#[derive(Debug, Clone, Default, PartialEq, Eq)]
298pub struct State {
299 pub keys: Vec<Key>,
301}
302
303impl State {
304 pub fn get_key(&self, key_id: &[u8]) -> Option<&Key> {
306 self.keys.iter().find(|k| k.id() == key_id)
307 }
308}
309
310#[derive(Debug, Clone)]
314pub struct Authority {
315 head: AumHash,
316 state: State,
317}
318
319impl Authority {
320 pub fn from_state(head: AumHash, state: State) -> Authority {
323 Authority { head, state }
324 }
325
326 pub fn head(&self) -> AumHash {
328 self.head
329 }
330
331 pub fn state(&self) -> &State {
333 &self.state
334 }
335
336 pub fn head_matches(&self, head: &AumHash) -> bool {
339 &self.head == head
340 }
341
342 pub fn node_key_authorized(
356 &self,
357 node_key: &[u8],
358 signature_cbor: &[u8],
359 ) -> Result<(), TkaError> {
360 let sig = decode_node_key_signature(signature_cbor)?;
361 if sig.sig_kind == SigKind::Credential {
363 return Err(TkaError::CredentialCannotAuthorize);
364 }
365 let key_id = sig.authorizing_key_id()?;
366 let key = self.state.get_key(key_id).ok_or(TkaError::UntrustedKey)?;
367 sig.verify_signature(node_key, key)
368 }
369}
370
371pub fn aum_hash(canonical_cbor: &[u8]) -> AumHash {
374 AumHash(blake2s_256(canonical_cbor))
375}
376
377#[derive(Debug, Clone, PartialEq, Eq)]
383pub struct AumKey {
384 pub kind: KeyKind,
386 pub votes: u32,
388 pub public: Vec<u8>,
390 pub meta: Vec<(alloc::string::String, alloc::string::String)>,
392}
393
394impl AumKey {
395 pub fn id(&self) -> &[u8] {
397 &self.public
398 }
399
400 fn kind_u8(&self) -> u8 {
401 match self.kind {
402 KeyKind::Ed25519 => 1,
403 }
404 }
405
406 fn to_cbor(&self) -> Value {
407 cbor::int_map([
408 (1, Some(Value::Uint(self.kind_u8() as u64))),
409 (2, Some(Value::Uint(self.votes as u64))),
410 (3, Some(Value::Bytes(self.public.clone()))),
411 (12, meta_to_cbor(&self.meta)),
412 ])
413 }
414}
415
416#[derive(Debug, Clone, PartialEq, Eq, Default)]
422pub struct AumState {
423 pub last_aum_hash: Option<AumHash>,
425 pub disablement_values: Vec<Vec<u8>>,
427 pub keys: Vec<AumKey>,
429 pub state_id1: u64,
431 pub state_id2: u64,
433}
434
435impl AumState {
436 fn to_cbor(&self) -> Value {
437 cbor::int_map([
438 (
439 1,
440 Some(match &self.last_aum_hash {
441 Some(h) => Value::Bytes(h.0.to_vec()),
442 None => Value::Null,
443 }),
444 ),
445 (
446 2,
447 Some(Value::Array(
448 self.disablement_values
449 .iter()
450 .map(|d| Value::Bytes(d.clone()))
451 .collect(),
452 )),
453 ),
454 (
455 3,
456 Some(Value::Array(
457 self.keys.iter().map(AumKey::to_cbor).collect(),
458 )),
459 ),
460 (
461 4,
462 (self.state_id1 != 0).then_some(Value::Uint(self.state_id1)),
463 ),
464 (
465 5,
466 (self.state_id2 != 0).then_some(Value::Uint(self.state_id2)),
467 ),
468 ])
469 }
470}
471
472#[derive(Debug, Clone, PartialEq, Eq)]
475pub struct AumSignature {
476 pub key_id: Vec<u8>,
478 pub signature: Vec<u8>,
480}
481
482impl AumSignature {
483 fn to_cbor(&self) -> Value {
484 cbor::int_map([
488 (1, Some(bytes_or_null(&self.key_id))),
489 (2, Some(bytes_or_null(&self.signature))),
490 ])
491 }
492}
493
494#[derive(Debug, Clone, PartialEq, Eq)]
504pub struct Aum {
505 pub message_kind: AumKind,
507 pub prev_aum_hash: Option<AumHash>,
509 pub key: Option<AumKey>,
511 pub key_id: Vec<u8>,
513 pub state: Option<AumState>,
515 pub votes: Option<u32>,
517 pub meta: Vec<(alloc::string::String, alloc::string::String)>,
519 pub signatures: Vec<AumSignature>,
521}
522
523impl Aum {
524 fn message_kind_u8(&self) -> u8 {
525 self.message_kind as u8
526 }
527
528 fn to_cbor(&self, include_signatures: bool) -> Value {
531 let signatures = if include_signatures && !self.signatures.is_empty() {
532 Some(Value::Array(
533 self.signatures.iter().map(AumSignature::to_cbor).collect(),
534 ))
535 } else {
536 None
537 };
538 cbor::int_map([
539 (1, Some(Value::Uint(self.message_kind_u8() as u64))),
541 (
543 2,
544 Some(match &self.prev_aum_hash {
545 Some(h) => Value::Bytes(h.0.to_vec()),
546 None => Value::Null,
547 }),
548 ),
549 (3, self.key.as_ref().map(AumKey::to_cbor)),
550 (4, nonempty_bytes(&self.key_id)),
551 (5, self.state.as_ref().map(AumState::to_cbor)),
552 (6, self.votes.map(|v| Value::Uint(v as u64))),
553 (7, meta_to_cbor(&self.meta)),
554 (23, signatures),
555 ])
556 }
557
558 pub fn serialize(&self) -> Vec<u8> {
560 self.to_cbor(true).to_vec()
561 }
562
563 pub fn hash(&self) -> AumHash {
565 AumHash(blake2s_256(&self.serialize()))
566 }
567
568 pub fn sig_hash(&self) -> [u8; AUM_HASH_LEN] {
571 blake2s_256(&self.to_cbor(false).to_vec())
572 }
573}
574
575fn meta_to_cbor(meta: &[(alloc::string::String, alloc::string::String)]) -> Option<Value> {
578 if meta.is_empty() {
579 return None;
580 }
581 Some(Value::TextMap(
582 meta.iter()
583 .map(|(k, v)| (k.as_bytes().to_vec(), Value::Text(v.as_bytes().to_vec())))
584 .collect(),
585 ))
586}
587
588fn blake2s_256(data: &[u8]) -> [u8; AUM_HASH_LEN] {
589 let mut hasher = Blake2s256::new();
590 hasher.update(data);
591 let out = hasher.finalize();
592 let mut h = [0u8; AUM_HASH_LEN];
593 h.copy_from_slice(&out);
594 h
595}
596
597fn nonempty_bytes(b: &[u8]) -> Option<Value> {
599 if b.is_empty() {
600 None
601 } else {
602 Some(Value::Bytes(b.to_vec()))
603 }
604}
605
606fn bytes_or_null(b: &[u8]) -> Value {
610 if b.is_empty() {
611 Value::Null
612 } else {
613 Value::Bytes(b.to_vec())
614 }
615}
616
617fn verify_ed25519_std(public: &[u8], msg: &[u8], sig: &[u8]) -> Result<(), TkaError> {
619 use ed25519_dalek::{Signature, Verifier, VerifyingKey};
620 let pk: [u8; 32] = public
621 .try_into()
622 .map_err(|_| TkaError::Decode("bad pubkey len"))?;
623 let vk = VerifyingKey::from_bytes(&pk).map_err(|_| TkaError::Decode("bad ed25519 pubkey"))?;
624 let sig: [u8; 64] = sig
625 .try_into()
626 .map_err(|_| TkaError::Decode("bad sig len"))?;
627 vk.verify(msg, &Signature::from_bytes(&sig))
628 .map_err(|_| TkaError::BadSignature)
629}
630
631fn verify_ed25519_zip215(public: &[u8], msg: &[u8], sig: &[u8]) -> Result<(), TkaError> {
633 let pk: [u8; 32] = public
634 .try_into()
635 .map_err(|_| TkaError::Decode("bad pubkey len"))?;
636 let vk = ed25519_zebra::VerificationKey::try_from(pk)
637 .map_err(|_| TkaError::Decode("bad ed25519 pubkey"))?;
638 let sig_bytes: [u8; 64] = sig
639 .try_into()
640 .map_err(|_| TkaError::Decode("bad sig len"))?;
641 let sig = ed25519_zebra::Signature::from(sig_bytes);
642 vk.verify(&sig, msg).map_err(|_| TkaError::BadSignature)
643}
644
645fn decode_node_key_signature(buf: &[u8]) -> Result<NodeKeySignature, TkaError> {
648 let (val, rest) = decode_value(buf, 0)?;
649 if !rest.is_empty() {
650 return Err(TkaError::Decode("trailing bytes after signature"));
651 }
652 node_key_signature_from_value(val, 0)
653}
654
655fn node_key_signature_from_value(val: Value, depth: usize) -> Result<NodeKeySignature, TkaError> {
656 if depth > MAX_SIG_NESTING_DEPTH {
657 return Err(TkaError::Decode("nested signature too deep"));
658 }
659 let Value::IntMap(entries) = val else {
660 return Err(TkaError::Decode("signature is not an int-keyed map"));
661 };
662 let mut sig_kind = None;
663 let mut pubkey = Vec::new();
664 let mut key_id = Vec::new();
665 let mut signature = Vec::new();
666 let mut nested = None;
667 let mut wrapping_pubkey = Vec::new();
668
669 for (k, v) in entries {
670 match k {
671 1 => {
672 let Value::Uint(n) = v else {
673 return Err(TkaError::Decode("sig kind not uint"));
674 };
675 sig_kind = Some(
676 SigKind::from_u8(
677 u8::try_from(n).map_err(|_| TkaError::Decode("sig kind range"))?,
678 )
679 .ok_or(TkaError::Decode("unknown sig kind"))?,
680 );
681 }
682 2 => pubkey = expect_bytes(v)?,
683 3 => key_id = expect_bytes(v)?,
684 4 => signature = expect_bytes(v)?,
685 5 => {
686 nested = Some(alloc::boxed::Box::new(node_key_signature_from_value(
687 v,
688 depth + 1,
689 )?))
690 }
691 6 => wrapping_pubkey = expect_bytes(v)?,
692 _ => return Err(TkaError::Decode("unknown signature field")),
693 }
694 }
695
696 Ok(NodeKeySignature {
697 sig_kind: sig_kind.ok_or(TkaError::Decode("signature missing kind"))?,
698 pubkey,
699 key_id,
700 signature,
701 nested,
702 wrapping_pubkey,
703 })
704}
705
706fn expect_bytes(v: Value) -> Result<Vec<u8>, TkaError> {
707 match v {
708 Value::Bytes(b) => Ok(b),
709 _ => Err(TkaError::Decode("expected byte string")),
710 }
711}
712
713fn decode_value(buf: &[u8], depth: usize) -> Result<(Value, &[u8]), TkaError> {
716 if depth > MAX_SIG_NESTING_DEPTH {
719 return Err(TkaError::Decode("nested signature too deep"));
720 }
721 let (major, arg, rest) = decode_head(buf)?;
722 match major {
723 0 => Ok((Value::Uint(arg), rest)),
724 2 => {
725 let len = arg as usize;
726 if rest.len() < len {
727 return Err(TkaError::Decode("byte string truncated"));
728 }
729 Ok((Value::Bytes(rest[..len].to_vec()), &rest[len..]))
730 }
731 3 => {
732 let len = arg as usize;
733 if rest.len() < len {
734 return Err(TkaError::Decode("text string truncated"));
735 }
736 Ok((Value::Text(rest[..len].to_vec()), &rest[len..]))
737 }
738 4 => {
739 let mut items = Vec::new();
740 let mut cur = rest;
741 for _ in 0..arg {
742 let (v, next) = decode_value(cur, depth + 1)?;
743 items.push(v);
744 cur = next;
745 }
746 Ok((Value::Array(items), cur))
747 }
748 5 => {
749 let mut entries: Vec<(u64, Value)> = Vec::new();
750 let mut cur = rest;
751 for _ in 0..arg {
752 let (k, next) = decode_head(cur).and_then(|(m, a, r)| {
753 if m == 0 {
754 Ok((a, r))
755 } else {
756 Err(TkaError::Decode("map key not uint"))
757 }
758 })?;
759 if entries.iter().any(|(existing, _)| *existing == k) {
762 return Err(TkaError::Decode("duplicate map key"));
763 }
764 let (v, next2) = decode_value(next, depth + 1)?;
765 entries.push((k, v));
766 cur = next2;
767 }
768 Ok((Value::IntMap(entries), cur))
769 }
770 _ => Err(TkaError::Decode("unsupported CBOR major type")),
771 }
772}
773
774fn decode_head(buf: &[u8]) -> Result<(u8, u64, &[u8]), TkaError> {
776 let first = *buf.first().ok_or(TkaError::Decode("empty CBOR"))?;
777 let major = first >> 5;
778 let info = first & 0x1f;
779 let rest = &buf[1..];
780 let (arg, rest) = match info {
781 n @ 0..=23 => (n as u64, rest),
782 24 => {
783 let b = *rest.first().ok_or(TkaError::Decode("truncated u8"))?;
784 (b as u64, &rest[1..])
785 }
786 25 => {
787 if rest.len() < 2 {
788 return Err(TkaError::Decode("truncated u16"));
789 }
790 (u16::from_be_bytes([rest[0], rest[1]]) as u64, &rest[2..])
791 }
792 26 => {
793 if rest.len() < 4 {
794 return Err(TkaError::Decode("truncated u32"));
795 }
796 (
797 u32::from_be_bytes([rest[0], rest[1], rest[2], rest[3]]) as u64,
798 &rest[4..],
799 )
800 }
801 27 => {
802 if rest.len() < 8 {
803 return Err(TkaError::Decode("truncated u64"));
804 }
805 let mut b = [0u8; 8];
806 b.copy_from_slice(&rest[..8]);
807 (u64::from_be_bytes(b), &rest[8..])
808 }
809 _ => return Err(TkaError::Decode("indefinite/reserved CBOR length")),
810 };
811 Ok((major, arg, rest))
812}
813
814const BASE32_ALPHABET: &[u8; 32] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
817
818fn base32_encode_nopad(data: &[u8]) -> String {
819 let mut out = String::new();
820 let mut buffer: u32 = 0;
821 let mut bits: u32 = 0;
822 for &b in data {
823 buffer = (buffer << 8) | b as u32;
824 bits += 8;
825 while bits >= 5 {
826 bits -= 5;
827 let idx = ((buffer >> bits) & 0x1f) as usize;
828 out.push(BASE32_ALPHABET[idx] as char);
829 }
830 }
831 if bits > 0 {
832 let idx = ((buffer << (5 - bits)) & 0x1f) as usize;
833 out.push(BASE32_ALPHABET[idx] as char);
834 }
835 out
836}
837
838fn base32_decode_nopad(text: &str) -> Option<Vec<u8>> {
839 let mut buffer: u32 = 0;
840 let mut bits: u32 = 0;
841 let mut out = Vec::new();
842 for c in text.chars() {
843 let val = match c {
844 'A'..='Z' => c as u32 - 'A' as u32,
845 '2'..='7' => c as u32 - '2' as u32 + 26,
846 _ => return None,
847 };
848 buffer = (buffer << 5) | val;
849 bits += 5;
850 if bits >= 8 {
851 bits -= 8;
852 out.push((buffer >> bits) as u8);
853 }
854 }
855 Some(out)
856}
857
858#[cfg(test)]
859mod tests {
860 use super::*;
861
862 #[test]
863 fn base32_roundtrip_32_bytes() {
864 let h = AumHash([0xABu8; 32]);
865 let text = h.to_base32();
866 let back = AumHash::from_base32(&text).unwrap();
867 assert_eq!(h, back);
868 }
869
870 #[test]
871 fn base32_rejects_wrong_length() {
872 assert!(AumHash::from_base32("AAAA").is_none());
874 assert!(AumHash::from_base32("aaaa").is_none());
876 }
877
878 #[test]
879 fn base32_matches_known_vector() {
880 assert_eq!(base32_encode_nopad(b"foobar"), "MZXW6YTBOI");
882 assert_eq!(base32_decode_nopad("MZXW6YTBOI").unwrap(), b"foobar");
883 }
884
885 #[test]
886 fn credential_signature_cannot_authorize() {
887 let auth = Authority::from_state(AumHash([0; 32]), State::default());
888 let sig = NodeKeySignature {
889 sig_kind: SigKind::Credential,
890 pubkey: alloc::vec![1, 2, 3],
891 key_id: alloc::vec![4, 5, 6],
892 signature: alloc::vec![0; 64],
893 nested: None,
894 wrapping_pubkey: Vec::new(),
895 };
896 let cbor = sig.to_cbor(true).to_vec();
897 let err = auth.node_key_authorized(&[1, 2, 3], &cbor).unwrap_err();
898 assert_eq!(err, TkaError::CredentialCannotAuthorize);
899 }
900
901 #[test]
902 fn untrusted_key_denied() {
903 let auth = Authority::from_state(AumHash([0; 32]), State::default());
905 let sig = NodeKeySignature {
906 sig_kind: SigKind::Direct,
907 pubkey: alloc::vec![9; 32],
908 key_id: alloc::vec![7; 32],
909 signature: alloc::vec![0; 64],
910 nested: None,
911 wrapping_pubkey: Vec::new(),
912 };
913 let cbor = sig.to_cbor(true).to_vec();
914 let err = auth.node_key_authorized(&[9; 32], &cbor).unwrap_err();
915 assert_eq!(err, TkaError::UntrustedKey);
916 }
917
918 #[test]
919 fn direct_signature_verifies_end_to_end() {
920 use ed25519_dalek::{Signer, SigningKey};
921
922 let signing = SigningKey::from_bytes(&[42u8; 32]);
924 let trusted_pub = signing.verifying_key().to_bytes().to_vec();
925 let node_key = alloc::vec![7u8; 32];
926
927 let trusted = Key {
928 kind: KeyKind::Ed25519,
929 votes: 1,
930 public: trusted_pub.clone(),
931 };
932 let auth = Authority::from_state(
933 AumHash([0; 32]),
934 State {
935 keys: alloc::vec![trusted],
936 },
937 );
938
939 let mut sig = NodeKeySignature {
941 sig_kind: SigKind::Direct,
942 pubkey: node_key.clone(),
943 key_id: trusted_pub.clone(),
944 signature: Vec::new(),
945 nested: None,
946 wrapping_pubkey: Vec::new(),
947 };
948 let sig_hash = sig.sig_hash();
949 sig.signature = signing.sign(&sig_hash).to_bytes().to_vec();
952
953 let cbor = sig.to_cbor(true).to_vec();
954 assert!(auth.node_key_authorized(&node_key, &cbor).is_ok());
955
956 let other = alloc::vec![8u8; 32];
958 assert_eq!(
959 auth.node_key_authorized(&other, &cbor).unwrap_err(),
960 TkaError::NodeKeyMismatch
961 );
962 }
963
964 #[test]
965 fn tampered_signature_denied() {
966 use ed25519_dalek::{Signer, SigningKey};
967
968 let signing = SigningKey::from_bytes(&[42u8; 32]);
969 let trusted_pub = signing.verifying_key().to_bytes().to_vec();
970 let node_key = alloc::vec![7u8; 32];
971 let auth = Authority::from_state(
972 AumHash([0; 32]),
973 State {
974 keys: alloc::vec![Key {
975 kind: KeyKind::Ed25519,
976 votes: 1,
977 public: trusted_pub.clone(),
978 }],
979 },
980 );
981 let mut sig = NodeKeySignature {
982 sig_kind: SigKind::Direct,
983 pubkey: node_key.clone(),
984 key_id: trusted_pub,
985 signature: Vec::new(),
986 nested: None,
987 wrapping_pubkey: Vec::new(),
988 };
989 let sig_hash = sig.sig_hash();
990 let mut sigbytes = signing.sign(&sig_hash).to_bytes();
991 sigbytes[0] ^= 0xff; sig.signature = sigbytes.to_vec();
993
994 let cbor = sig.to_cbor(true).to_vec();
995 assert_eq!(
996 auth.node_key_authorized(&node_key, &cbor).unwrap_err(),
997 TkaError::BadSignature
998 );
999 }
1000
1001 #[test]
1002 fn head_matches_check() {
1003 let h = AumHash([5u8; 32]);
1004 let auth = Authority::from_state(h, State::default());
1005 assert!(auth.head_matches(&h));
1006 assert!(!auth.head_matches(&AumHash([6u8; 32])));
1007 }
1008
1009 #[test]
1012 fn deeply_nested_signature_rejected_without_overflow() {
1013 let mut sig = NodeKeySignature {
1017 sig_kind: SigKind::Direct,
1018 pubkey: alloc::vec![1u8; 32],
1019 key_id: alloc::vec![2u8; 32],
1020 signature: alloc::vec![3u8; 64],
1021 nested: None,
1022 wrapping_pubkey: Vec::new(),
1023 };
1024 for _ in 0..(MAX_SIG_NESTING_DEPTH + 8) {
1025 sig = NodeKeySignature {
1026 sig_kind: SigKind::Rotation,
1027 pubkey: alloc::vec![1u8; 32],
1028 key_id: Vec::new(),
1029 signature: alloc::vec![3u8; 64],
1030 nested: Some(alloc::boxed::Box::new(sig)),
1031 wrapping_pubkey: alloc::vec![1u8; 32],
1032 };
1033 }
1034 let cbor = sig.to_cbor(true).to_vec();
1035 let err = decode_node_key_signature(&cbor).unwrap_err();
1036 assert_eq!(err, TkaError::Decode("nested signature too deep"));
1037 }
1038
1039 #[test]
1042 fn duplicate_map_key_rejected() {
1043 let blob = [0xa2u8, 0x01, 0x00, 0x01, 0x01];
1045 let err = decode_node_key_signature(&blob).unwrap_err();
1046 assert_eq!(err, TkaError::Decode("duplicate map key"));
1047 }
1048
1049 #[test]
1063 fn rotation_chain_verifies_end_to_end() {
1064 use ed25519_dalek::{Signer, SigningKey};
1065
1066 let trusted = SigningKey::from_bytes(&[7u8; 32]);
1068 let trusted_pub = trusted.verifying_key().to_bytes().to_vec();
1069
1070 let wrapping = SigningKey::from_bytes(&[9u8; 32]);
1073 let wrapping_pub = wrapping.verifying_key().to_bytes().to_vec();
1074
1075 let node_key = alloc::vec![5u8; 32];
1076
1077 let auth = Authority::from_state(
1078 AumHash([0; 32]),
1079 State {
1080 keys: alloc::vec![Key {
1081 kind: KeyKind::Ed25519,
1082 votes: 1,
1083 public: trusted_pub.clone(),
1084 }],
1085 },
1086 );
1087
1088 let mut inner = NodeKeySignature {
1091 sig_kind: SigKind::Direct,
1092 pubkey: wrapping_pub.clone(),
1093 key_id: trusted_pub.clone(),
1094 signature: Vec::new(),
1095 nested: None,
1096 wrapping_pubkey: wrapping_pub.clone(),
1097 };
1098 let inner_hash = inner.sig_hash();
1099 inner.signature = trusted.sign(&inner_hash).to_bytes().to_vec();
1100
1101 let mut outer = NodeKeySignature {
1103 sig_kind: SigKind::Rotation,
1104 pubkey: node_key.clone(),
1105 key_id: Vec::new(),
1106 signature: Vec::new(),
1107 nested: Some(alloc::boxed::Box::new(inner)),
1108 wrapping_pubkey: Vec::new(),
1109 };
1110 let outer_hash = outer.sig_hash();
1111 outer.signature = wrapping.sign(&outer_hash).to_bytes().to_vec();
1112
1113 let cbor = outer.to_cbor(true).to_vec();
1114 assert!(auth.node_key_authorized(&node_key, &cbor).is_ok());
1115
1116 let mut tampered = outer.clone();
1118 let mut sb = tampered.signature.clone();
1119 sb[0] ^= 0xff;
1120 tampered.signature = sb;
1121 let cbor_bad = tampered.to_cbor(true).to_vec();
1122 assert_eq!(
1123 auth.node_key_authorized(&node_key, &cbor_bad).unwrap_err(),
1124 TkaError::BadSignature
1125 );
1126 }
1127
1128 #[test]
1131 fn rotation_nested_credential_pubkey_bind() {
1132 use ed25519_dalek::{Signer, SigningKey};
1133
1134 let trusted = SigningKey::from_bytes(&[11u8; 32]);
1135 let trusted_pub = trusted.verifying_key().to_bytes().to_vec();
1136 let wrapping = SigningKey::from_bytes(&[13u8; 32]);
1137 let wrapping_pub = wrapping.verifying_key().to_bytes().to_vec();
1138 let node_key = alloc::vec![6u8; 32];
1139
1140 let auth = Authority::from_state(
1141 AumHash([0; 32]),
1142 State {
1143 keys: alloc::vec![Key {
1144 kind: KeyKind::Ed25519,
1145 votes: 1,
1146 public: trusted_pub.clone(),
1147 }],
1148 },
1149 );
1150
1151 let build = |cred_pubkey: Vec<u8>| -> Vec<u8> {
1153 let mut inner = NodeKeySignature {
1154 sig_kind: SigKind::Credential,
1155 pubkey: cred_pubkey,
1156 key_id: trusted_pub.clone(),
1157 signature: Vec::new(),
1158 nested: None,
1159 wrapping_pubkey: wrapping_pub.clone(),
1160 };
1161 let inner_hash = inner.sig_hash();
1162 inner.signature = trusted.sign(&inner_hash).to_bytes().to_vec();
1163
1164 let mut outer = NodeKeySignature {
1165 sig_kind: SigKind::Rotation,
1166 pubkey: node_key.clone(),
1167 key_id: Vec::new(),
1168 signature: Vec::new(),
1169 nested: Some(alloc::boxed::Box::new(inner)),
1170 wrapping_pubkey: Vec::new(),
1171 };
1172 let outer_hash = outer.sig_hash();
1173 outer.signature = wrapping.sign(&outer_hash).to_bytes().to_vec();
1174 outer.to_cbor(true).to_vec()
1175 };
1176
1177 let cbor_ok = build(wrapping_pub.clone());
1179 assert!(auth.node_key_authorized(&node_key, &cbor_ok).is_ok());
1180
1181 let cbor_bad = build(alloc::vec![0xaau8; 32]);
1184 assert_eq!(
1185 auth.node_key_authorized(&node_key, &cbor_bad).unwrap_err(),
1186 TkaError::NodeKeyMismatch
1187 );
1188 }
1189
1190 fn hex(bytes: &[u8]) -> String {
1194 let mut s = String::new();
1195 for b in bytes {
1196 s.push_str(&alloc::format!("{b:02x}"));
1197 }
1198 s
1199 }
1200
1201 #[test]
1219 fn node_key_signature_cbor_frozen_vector() {
1220 let pubkey: Vec<u8> = (0u8..32).collect();
1222 let key_id: Vec<u8> = (32u8..64).collect();
1223 let signature: Vec<u8> = (64u8..128).collect();
1224
1225 let sig = NodeKeySignature {
1226 sig_kind: SigKind::Direct,
1227 pubkey,
1228 key_id,
1229 signature,
1230 nested: None,
1231 wrapping_pubkey: Vec::new(), };
1233
1234 let full = sig.to_cbor(true).to_vec();
1236 const EXPECTED_FULL: &[u8] = &[
1237 0xa4, 0x01, 0x01, 0x02, 0x58, 0x20, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
1238 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15,
1239 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x03, 0x58, 0x20, 0x20,
1240 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e,
1241 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c,
1242 0x3d, 0x3e, 0x3f, 0x04, 0x58, 0x40, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47,
1243 0x48, 0x49, 0x4a, 0x4b, 0x4c, 0x4d, 0x4e, 0x4f, 0x50, 0x51, 0x52, 0x53, 0x54, 0x55,
1244 0x56, 0x57, 0x58, 0x59, 0x5a, 0x5b, 0x5c, 0x5d, 0x5e, 0x5f, 0x60, 0x61, 0x62, 0x63,
1245 0x64, 0x65, 0x66, 0x67, 0x68, 0x69, 0x6a, 0x6b, 0x6c, 0x6d, 0x6e, 0x6f, 0x70, 0x71,
1246 0x72, 0x73, 0x74, 0x75, 0x76, 0x77, 0x78, 0x79, 0x7a, 0x7b, 0x7c, 0x7d, 0x7e, 0x7f,
1247 ];
1248 assert_eq!(
1249 full,
1250 EXPECTED_FULL,
1251 "full CBOR serialization changed (canonical-CBOR encoding drift). actual: {}",
1252 hex(&full)
1253 );
1254
1255 let preimage = sig.to_cbor(false).to_vec();
1257 const EXPECTED_PREIMAGE: &[u8] = &[
1258 0xa3, 0x01, 0x01, 0x02, 0x58, 0x20, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07,
1259 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15,
1260 0x16, 0x17, 0x18, 0x19, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x03, 0x58, 0x20, 0x20,
1261 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, 0x2b, 0x2c, 0x2d, 0x2e,
1262 0x2f, 0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c,
1263 0x3d, 0x3e, 0x3f,
1264 ];
1265 assert_eq!(
1266 preimage,
1267 EXPECTED_PREIMAGE,
1268 "SigHash preimage CBOR changed. actual: {}",
1269 hex(&preimage)
1270 );
1271
1272 let sig_hash = sig.sig_hash();
1274 const EXPECTED_SIG_HASH: [u8; AUM_HASH_LEN] = [
1275 0x22, 0x6f, 0x9c, 0xbc, 0x63, 0x73, 0x92, 0x75, 0x2e, 0x0e, 0xb1, 0x32, 0x9c, 0xc4,
1276 0x99, 0x07, 0x01, 0x4a, 0xb6, 0x4f, 0x8e, 0x5d, 0x82, 0x85, 0xc2, 0x91, 0x42, 0x62,
1277 0xf6, 0xa6, 0xa8, 0x33,
1278 ];
1279 assert_eq!(
1280 sig_hash,
1281 EXPECTED_SIG_HASH,
1282 "sig_hash (BLAKE2s-256 of preimage) changed. actual: {}",
1283 hex(&sig_hash)
1284 );
1285
1286 let aum = aum_hash(&full);
1289 const EXPECTED_AUM_HASH: [u8; AUM_HASH_LEN] = [
1290 0xa4, 0x40, 0x71, 0xa3, 0x7a, 0xbf, 0x80, 0x92, 0xd6, 0xff, 0x23, 0x84, 0xb2, 0xb0,
1291 0xa3, 0x50, 0xc7, 0xcb, 0x48, 0x41, 0xed, 0x68, 0x99, 0x62, 0x41, 0x7c, 0xd4, 0x23,
1292 0x68, 0xdc, 0x72, 0x49,
1293 ];
1294 assert_eq!(
1295 aum.0,
1296 EXPECTED_AUM_HASH,
1297 "aum_hash over full serialization changed. actual: {}",
1298 hex(&aum.0)
1299 );
1300 }
1301
1302 fn unhex(s: &str) -> Vec<u8> {
1306 assert!(s.len().is_multiple_of(2), "odd hex length");
1307 let nib = |c: u8| -> u8 {
1308 match c {
1309 b'0'..=b'9' => c - b'0',
1310 b'a'..=b'f' => c - b'a' + 10,
1311 b'A'..=b'F' => c - b'A' + 10,
1312 _ => panic!("bad hex nibble"),
1313 }
1314 };
1315 let b = s.as_bytes();
1316 let mut out = Vec::with_capacity(s.len() / 2);
1317 let mut i = 0;
1318 while i < b.len() {
1319 out.push((nib(b[i]) << 4) | nib(b[i + 1]));
1320 i += 2;
1321 }
1322 out
1323 }
1324
1325 const SPECCHECK_VECTORS: [(&str, &str, &str); 12] = [
1335 (
1337 "8c93255d71dcab10e8f379c26200f3c7bd5f09d9bc3068d3ef4edeb4853022b6",
1338 "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa",
1339 "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac037a0000000000000000000000000000000000000000000000000000000000000000",
1340 ),
1341 (
1343 "9bd9f44f4dcc75bd531b56b2cd280b0bb38fc1cd6d1230e14861d861de092e79",
1344 "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa",
1345 "f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43a5bb704786be79fc476f91d3f3f89b03984d8068dcf1bb7dfc6637b45450ac04",
1346 ),
1347 (
1349 "aebf3f2601a0c8c5d39cc7d8911642f740b78168218da8471772b35f9d35b9ab",
1350 "f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43",
1351 "c7176a703d4dd84fba3c0b760d10670f2a2053fa2c39ccc64ec7fd7792ac03fa8c4bd45aecaca5b24fb97bc10ac27ac8751a7dfe1baff8b953ec9f5833ca260e",
1352 ),
1353 (
1355 "9bd9f44f4dcc75bd531b56b2cd280b0bb38fc1cd6d1230e14861d861de092e79",
1356 "cdb267ce40c5cd45306fa5d2f29731459387dbf9eb933b7bd5aed9a765b88d4d",
1357 "9046a64750444938de19f227bb80485e92b83fdb4b6506c160484c016cc1852f87909e14428a7a1d62e9f22f3d3ad7802db02eb2e688b6c52fcd6648a98bd009",
1358 ),
1359 (
1361 "e47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec4011eaccd55b53f56c",
1362 "cdb267ce40c5cd45306fa5d2f29731459387dbf9eb933b7bd5aed9a765b88d4d",
1363 "160a1cb0dc9c0258cd0a7d23e94d8fa878bcb1925f2c64246b2dee1796bed5125ec6bc982a269b723e0668e540911a9a6a58921d6925e434ab10aa7940551a09",
1364 ),
1365 (
1367 "e47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec4011eaccd55b53f56c",
1368 "cdb267ce40c5cd45306fa5d2f29731459387dbf9eb933b7bd5aed9a765b88d4d",
1369 "21122a84e0b5fca4052f5b1235c80a537878b38f3142356b2c2384ebad4668b7e40bc836dac0f71076f9abe3a53f9c03c1ceeeddb658d0030494ace586687405",
1370 ),
1371 (
1373 "85e241a07d148b41e47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec40",
1374 "442aad9f089ad9e14647b1ef9099a1ff4798d78589e66f28eca69c11f582a623",
1375 "e96f66be976d82e60150baecff9906684aebb1ef181f67a7189ac78ea23b6c0e547f7690a0e2ddcd04d87dbc3490dc19b3b3052f7ff0538cb68afb369ba3a514",
1376 ),
1377 (
1379 "85e241a07d148b41e47d62c63f830dc7a6851a0b1f33ae4bb2f507fb6cffec40",
1380 "442aad9f089ad9e14647b1ef9099a1ff4798d78589e66f28eca69c11f582a623",
1381 "8ce5b96c8f26d0ab6c47958c9e68b937104cd36e13c33566acd2fe8d38aa19427e71f98a473474f2f13f06f97c20d58cc3f54b8bd0d272f42b695dd7e89a8c22",
1382 ),
1383 (
1385 "9bedc267423725d473888631ebf45988bad3db83851ee85c85e241a07d148b41",
1386 "f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43",
1387 "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff03be9678ac102edcd92b0210bb34d7428d12ffc5df5f37e359941266a4e35f0f",
1388 ),
1389 (
1391 "9bedc267423725d473888631ebf45988bad3db83851ee85c85e241a07d148b41",
1392 "f7badec5b8abeaf699583992219b7b223f1df3fbbea919844e3f7c554a43dd43",
1393 "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffca8c5b64cd208982aa38d4936621a4775aa233aa0505711d8fdcfdaa943d4908",
1394 ),
1395 (
1397 "e96b7021eb39c1a163b6da4e3093dcd3f21387da4cc4572be588fafae23c155b",
1398 "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
1399 "a9d55260f765261eb9b84e106f665e00b867287a761990d7135963ee0a7d59dca5bb704786be79fc476f91d3f3f89b03984d8068dcf1bb7dfc6637b45450ac04",
1400 ),
1401 (
1403 "39a591f5321bbe07fd5a23dc2f39d025d74526615746727ceefd6e82ae65c06f",
1404 "ecffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
1405 "a9d55260f765261eb9b84e106f665e00b867287a761990d7135963ee0a7d59dca5bb704786be79fc476f91d3f3f89b03984d8068dcf1bb7dfc6637b45450ac04",
1406 ),
1407 ];
1408
1409 #[test]
1425 fn ed25519_speccheck_dual_verifier_kat() {
1426 const STD_EXPECT: [bool; 12] = [
1432 true, true, true, true, false, false, false, false, false, false, false, true,
1433 ];
1434 const ZIP215_EXPECT: [bool; 12] = [
1435 true, true, true, true, true, true, false, false, false, true, true, true,
1436 ];
1437
1438 for (i, (msg_hex, pk_hex, sig_hex)) in SPECCHECK_VECTORS.iter().enumerate() {
1439 let msg = unhex(msg_hex);
1440 let pk = unhex(pk_hex);
1441 let sig = unhex(sig_hex);
1442 assert_eq!(pk.len(), 32, "vector {i}: pubkey not 32 bytes");
1443 assert_eq!(sig.len(), 64, "vector {i}: signature not 64 bytes");
1444
1445 let std_ok = verify_ed25519_std(&pk, &msg, &sig).is_ok();
1446 let zip_ok = verify_ed25519_zip215(&pk, &msg, &sig).is_ok();
1447
1448 assert_eq!(
1449 std_ok, STD_EXPECT[i],
1450 "speccheck vector {i}: verify_ed25519_std accept={std_ok}, expected {}",
1451 STD_EXPECT[i]
1452 );
1453 assert_eq!(
1454 zip_ok, ZIP215_EXPECT[i],
1455 "speccheck vector {i}: verify_ed25519_zip215 accept={zip_ok}, expected {}",
1456 ZIP215_EXPECT[i]
1457 );
1458 }
1459
1460 for &i in &[6usize, 7usize] {
1464 let (msg_hex, pk_hex, sig_hex) = SPECCHECK_VECTORS[i];
1465 let (msg, pk, sig) = (unhex(msg_hex), unhex(pk_hex), unhex(sig_hex));
1466 assert!(
1467 verify_ed25519_std(&pk, &msg, &sig).is_err(),
1468 "SECURITY: verify_ed25519_std ACCEPTED S>=L malleability vector {i}"
1469 );
1470 }
1471
1472 {
1476 let (msg_hex, pk_hex, sig_hex) = SPECCHECK_VECTORS[4];
1477 let (msg, pk, sig) = (unhex(msg_hex), unhex(pk_hex), unhex(sig_hex));
1478 assert!(
1479 verify_ed25519_zip215(&pk, &msg, &sig).is_ok(),
1480 "vector 4: ZIP-215 (zebra) should ACCEPT the cofactored discriminator"
1481 );
1482 assert!(
1483 verify_ed25519_std(&pk, &msg, &sig).is_err(),
1484 "vector 4: standard (dalek) should REJECT the cofactored discriminator"
1485 );
1486 }
1487 }
1488
1489 #[test]
1508 fn tka_cbor_matches_go_golden() {
1509 let pubkey32 = unhex("a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf");
1511 let key_id32 = unhex("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
1512 let sig64 = unhex(
1513 "f0f1f2f3f4f5f6f7f8f9fafbfcfdfeffe0e1e2e3e4e5e6e7e8e9eaebecedeeefd0d1d2d3d4d5d6d7d8d9dadbdcdddedfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf",
1514 );
1515 let wrap32 = unhex("101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f");
1516 let rot_sig64 = unhex(
1517 "55565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f9091929394",
1518 );
1519
1520 {
1522 let sig = NodeKeySignature {
1523 sig_kind: SigKind::Direct,
1524 pubkey: pubkey32.clone(),
1525 key_id: key_id32.clone(),
1526 signature: sig64.clone(),
1527 nested: None,
1528 wrapping_pubkey: Vec::new(),
1529 };
1530 let full = sig.to_cbor(true).to_vec();
1531 let expected_full = unhex(
1532 "a40101025820a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf035820000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f045840f0f1f2f3f4f5f6f7f8f9fafbfcfdfeffe0e1e2e3e4e5e6e7e8e9eaebecedeeefd0d1d2d3d4d5d6d7d8d9dadbdcdddedfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf",
1533 );
1534 assert_eq!(
1535 full,
1536 expected_full,
1537 "GOLDEN 1 (Direct) full CBOR diverged from Go tka v1.100.0. actual: {}",
1538 hex(&full)
1539 );
1540 let expected_hash =
1541 unhex("7e9653c97d35485b37b9bf942b1861cd2f3cb0663b5bb154f1178cca72101e74");
1542 assert_eq!(
1543 sig.sig_hash().as_slice(),
1544 expected_hash.as_slice(),
1545 "GOLDEN 1 (Direct) sig_hash diverged from Go tka v1.100.0. actual: {}",
1546 hex(&sig.sig_hash())
1547 );
1548 }
1549
1550 {
1552 let sig = NodeKeySignature {
1553 sig_kind: SigKind::Credential,
1554 pubkey: pubkey32.clone(),
1555 key_id: key_id32.clone(),
1556 signature: sig64.clone(),
1557 nested: None,
1558 wrapping_pubkey: Vec::new(),
1559 };
1560 let full = sig.to_cbor(true).to_vec();
1561 let expected_full = unhex(
1562 "a40103025820a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf035820000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f045840f0f1f2f3f4f5f6f7f8f9fafbfcfdfeffe0e1e2e3e4e5e6e7e8e9eaebecedeeefd0d1d2d3d4d5d6d7d8d9dadbdcdddedfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf",
1563 );
1564 assert_eq!(
1565 full,
1566 expected_full,
1567 "GOLDEN 2 (Credential) full CBOR diverged from Go tka v1.100.0. actual: {}",
1568 hex(&full)
1569 );
1570 let expected_hash =
1571 unhex("b6070ea8bc7ae8989ef4293f5031bedaa4a499803ade99f9e2f34dc2898ac03f");
1572 assert_eq!(
1573 sig.sig_hash().as_slice(),
1574 expected_hash.as_slice(),
1575 "GOLDEN 2 (Credential) sig_hash diverged from Go tka v1.100.0. actual: {}",
1576 hex(&sig.sig_hash())
1577 );
1578 }
1579
1580 {
1590 let nested = NodeKeySignature {
1591 sig_kind: SigKind::Direct,
1592 pubkey: pubkey32.clone(),
1593 key_id: key_id32.clone(),
1594 signature: sig64.clone(),
1595 nested: None,
1596 wrapping_pubkey: wrap32.clone(),
1597 };
1598 let sig = NodeKeySignature {
1599 sig_kind: SigKind::Rotation,
1600 pubkey: wrap32.clone(),
1601 key_id: Vec::new(),
1602 signature: rot_sig64.clone(),
1603 nested: Some(alloc::boxed::Box::new(nested)),
1604 wrapping_pubkey: Vec::new(),
1605 };
1606 let full = sig.to_cbor(true).to_vec();
1607 let expected_full = unhex(
1608 "a40102025820101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f04584055565758595a5b5c5d5e5f606162636465666768696a6b6c6d6e6f707172737475767778797a7b7c7d7e7f808182838485868788898a8b8c8d8e8f909192939405a50101025820a0a1a2a3a4a5a6a7a8a9aaabacadaeafb0b1b2b3b4b5b6b7b8b9babbbcbdbebf035820000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f045840f0f1f2f3f4f5f6f7f8f9fafbfcfdfeffe0e1e2e3e4e5e6e7e8e9eaebecedeeefd0d1d2d3d4d5d6d7d8d9dadbdcdddedfc0c1c2c3c4c5c6c7c8c9cacbcccdcecf065820101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f",
1609 );
1610 assert_eq!(
1611 full,
1612 expected_full,
1613 "GOLDEN 3 (Rotation) full CBOR diverged from Go tka v1.100.0. actual: {}",
1614 hex(&full)
1615 );
1616 let expected_hash =
1617 unhex("fac0a5a6781bb945369c28a0b3d3eea04e1648b60ec1a990a1ff68a9a566e6a7");
1618 assert_eq!(
1619 sig.sig_hash().as_slice(),
1620 expected_hash.as_slice(),
1621 "GOLDEN 3 (Rotation) sig_hash diverged from Go tka v1.100.0. actual: {}",
1622 hex(&sig.sig_hash())
1623 );
1624 }
1625 }
1626
1627 #[test]
1643 fn ed25519_dual_verifier_matches_go_verdicts() {
1644 const GO_STD_ACCEPT: [bool; 12] = [
1646 true, true, true, true, false, false, false, false, false, false, false, true,
1647 ];
1648 const GO_ZIP215_ACCEPT: [bool; 12] = [
1649 true, true, true, true, true, true, false, false, false, true, true, true,
1650 ];
1651
1652 for (i, (msg_hex, pk_hex, sig_hex)) in SPECCHECK_VECTORS.iter().enumerate() {
1653 let msg = unhex(msg_hex);
1654 let pk = unhex(pk_hex);
1655 let sig = unhex(sig_hex);
1656
1657 let std_ok = verify_ed25519_std(&pk, &msg, &sig).is_ok();
1658 let zip_ok = verify_ed25519_zip215(&pk, &msg, &sig).is_ok();
1659
1660 assert_eq!(
1661 std_ok, GO_STD_ACCEPT[i],
1662 "vector {i}: Rust verify_ed25519_std accept={std_ok} disagrees with Go \
1663 crypto/ed25519.Verify={}",
1664 GO_STD_ACCEPT[i]
1665 );
1666 assert_eq!(
1667 zip_ok, GO_ZIP215_ACCEPT[i],
1668 "vector {i}: Rust verify_ed25519_zip215 accept={zip_ok} disagrees with Go \
1669 ed25519consensus.Verify={}",
1670 GO_ZIP215_ACCEPT[i]
1671 );
1672 }
1673 }
1674
1675 #[test]
1681 fn aum_serialize_matches_go_test_serialization_vectors() {
1682 let add_key_inner_zero_key = cbor::Value::IntMap(alloc::vec![
1688 (1, cbor::Value::Uint(0)), (2, cbor::Value::Uint(0)), (3, cbor::Value::Null), ]);
1692 assert_eq!(
1693 add_key_inner_zero_key.to_vec(),
1694 alloc::vec![0xa3, 0x01, 0x00, 0x02, 0x00, 0x03, 0xf6],
1695 "Go's zero Key{{}} encodes as map(3){{kind=0, votes=0, public=null}}"
1696 );
1697
1698 let remove_key = Aum {
1700 message_kind: AumKind::RemoveKey,
1701 prev_aum_hash: None,
1702 key: None,
1703 key_id: alloc::vec![1, 2],
1704 state: None,
1705 votes: None,
1706 meta: Vec::new(),
1707 signatures: Vec::new(),
1708 };
1709 assert_eq!(
1710 remove_key.serialize(),
1711 alloc::vec![0xa3, 0x01, 0x02, 0x02, 0xf6, 0x04, 0x42, 0x01, 0x02],
1713 "RemoveKey AUM serialization must match Go TestSerialization byte-for-byte"
1714 );
1715
1716 let update_key = Aum {
1719 message_kind: AumKind::UpdateKey,
1720 prev_aum_hash: None,
1721 key: None,
1722 key_id: alloc::vec![1, 2],
1723 state: None,
1724 votes: Some(2),
1725 meta: alloc::vec![("a".into(), "b".into())],
1726 signatures: Vec::new(),
1727 };
1728 assert_eq!(
1729 update_key.serialize(),
1730 alloc::vec![
1733 0xa5, 0x01, 0x04, 0x02, 0xf6, 0x04, 0x42, 0x01, 0x02, 0x06, 0x02, 0x07, 0xa1, 0x61,
1734 0x61, 0x61, 0x62
1735 ],
1736 "UpdateKey AUM serialization must match Go TestSerialization byte-for-byte"
1737 );
1738
1739 let with_sig = Aum {
1741 message_kind: AumKind::AddKey,
1742 prev_aum_hash: None,
1743 key: None,
1744 key_id: Vec::new(),
1745 state: None,
1746 votes: None,
1747 meta: Vec::new(),
1748 signatures: alloc::vec![AumSignature {
1749 key_id: alloc::vec![1],
1750 signature: Vec::new(),
1751 }],
1752 };
1753 assert_eq!(
1754 with_sig.serialize(),
1755 alloc::vec![
1758 0xa3, 0x01, 0x01, 0x02, 0xf6, 0x17, 0x81, 0xa2, 0x01, 0x41, 0x01, 0x02, 0xf6
1759 ],
1760 "Signature AUM serialization must match Go TestSerialization (key 23 + nil sig = null)"
1761 );
1762
1763 let no_sig = Aum {
1766 signatures: Vec::new(),
1767 ..with_sig.clone()
1768 };
1769 assert_eq!(
1770 with_sig.sig_hash(),
1771 blake2s_256(&no_sig.serialize()),
1772 "SigHash preimage must omit key 23 (Signatures), matching Go AUM.SigHash"
1773 );
1774 assert_ne!(
1776 with_sig.hash().0,
1777 with_sig.sig_hash(),
1778 "Hash (incl. signatures) must differ from SigHash (excl.) when signatures are present"
1779 );
1780 }
1781
1782 #[test]
1786 fn aum_checkpoint_state_serialization() {
1787 let checkpoint = Aum {
1788 message_kind: AumKind::Checkpoint,
1789 prev_aum_hash: Some(AumHash([0u8; AUM_HASH_LEN])),
1790 key: None,
1791 key_id: Vec::new(),
1792 state: Some(AumState {
1793 last_aum_hash: Some(AumHash([0u8; AUM_HASH_LEN])),
1794 disablement_values: Vec::new(),
1795 keys: alloc::vec![AumKey {
1796 kind: KeyKind::Ed25519,
1797 votes: 1,
1798 public: alloc::vec![5, 6],
1799 meta: Vec::new(),
1800 }],
1801 state_id1: 0,
1802 state_id2: 0,
1803 }),
1804 votes: None,
1805 meta: Vec::new(),
1806 signatures: Vec::new(),
1807 };
1808 let bytes = checkpoint.serialize();
1809 assert_eq!(
1813 &bytes[0..3],
1814 &[0xa3, 0x01, 0x05],
1815 "map(3), MessageKind=Checkpoint(5)"
1816 );
1817 assert_eq!(
1818 &bytes[3..6],
1819 &[0x02, 0x58, 0x20],
1820 "key2 prev = 32-byte byte string head"
1821 );
1822 assert_eq!(bytes[38], 0x05, "key 5 = State");
1826 assert_eq!(
1828 &bytes[39..42],
1829 &[0xa3, 0x01, 0x58],
1830 "State map(3), key1 LastAUMHash bytes"
1831 );
1832 let tail = &bytes[bytes.len() - 4..];
1834 assert_eq!(
1835 tail,
1836 &[0x03, 0x42, 0x05, 0x06],
1837 "Key.Public (key 3) = bytes{{5,6}}"
1838 );
1839 assert_eq!(checkpoint.hash(), checkpoint.hash());
1841 }
1842}