umsh_crypto/
lib.rs

1#![allow(async_fn_in_trait)]
2#![cfg_attr(not(feature = "std"), no_std)]
3
4//! Cryptographic traits and UMSH-specific key/packet operations.
5//!
6//! This crate separates algorithm providers from protocol logic. The low-level
7//! traits such as [`AesProvider`] and [`Sha256Provider`] can be backed either by
8//! software implementations or hardware accelerators, while [`CryptoEngine`]
9//! implements the UMSH-specific derivation and packet-authentication rules.
10//!
11//! # Example
12//!
13//! ```rust
14//! use umsh_crypto::software::{SoftwareAes, SoftwareIdentity, SoftwareSha256};
15//! use umsh_crypto::{CryptoEngine, NodeIdentity};
16//!
17//! let alice = SoftwareIdentity::from_secret_bytes(&[0x11; 32]);
18//! let bob = SoftwareIdentity::from_secret_bytes(&[0x22; 32]);
19//! let shared = alice.shared_secret_with(bob.public_key()).unwrap();
20//! let engine = CryptoEngine::new(SoftwareAes, SoftwareSha256);
21//! let keys = engine.derive_pairwise_keys(&shared);
22//!
23//! assert_ne!(keys.k_enc, [0u8; 32]);
24//! assert_ne!(keys.k_mic, [0u8; 32]);
25//! ```
26
27use core::ops::Range;
28
29use umsh_core::{
30    ChannelId, ChannelKey, ChannelTag, PacketHeader, PacketType, PublicKey, SourceAddrRef,
31    UnsealedPacket, feed_aad,
32};
33use zeroize::{Zeroize, ZeroizeOnDrop};
34
35pub mod pool;
36pub mod replay;
37
38/// AES block-cipher instance used by the protocol engine.
39pub trait AesCipher {
40    /// Encrypt one 16-byte block in place.
41    fn encrypt_block(&self, block: &mut [u8; 16]);
42    /// Decrypt one 16-byte block in place.
43    fn decrypt_block(&self, block: &mut [u8; 16]);
44}
45
46impl<C: AesCipher + ?Sized> AesCipher for &C {
47    fn encrypt_block(&self, block: &mut [u8; 16]) {
48        (**self).encrypt_block(block);
49    }
50
51    fn decrypt_block(&self, block: &mut [u8; 16]) {
52        (**self).decrypt_block(block);
53    }
54}
55
56/// Factory for keyed AES cipher instances.
57pub trait AesProvider {
58    /// Concrete cipher type returned by [`new_cipher`](Self::new_cipher).
59    type Cipher: AesCipher;
60
61    /// Create a new AES-256 cipher using `key`.
62    fn new_cipher(&self, key: &[u8; 32]) -> Self::Cipher;
63}
64
65/// SHA-256 and HMAC-SHA-256 provider.
66pub trait Sha256Provider {
67    /// Hash a list of borrowed byte slices as one concatenated message.
68    fn hash(&self, data: &[&[u8]]) -> [u8; 32];
69    /// Compute HMAC-SHA-256 over a list of borrowed byte slices.
70    fn hmac(&self, key: &[u8], data: &[&[u8]]) -> [u8; 32];
71}
72
73/// Raw X25519 shared secret.
74#[derive(Clone, Zeroize, ZeroizeOnDrop)]
75pub struct SharedSecret(pub [u8; 32]);
76
77/// Node identity capable of signing and key agreement.
78pub trait NodeIdentity {
79    type Error;
80
81    /// Return the long-term Ed25519 public key for this identity.
82    fn public_key(&self) -> &PublicKey;
83
84    /// Return the three-byte node hint derived from [`public_key`](Self::public_key).
85    fn hint(&self) -> umsh_core::NodeHint {
86        self.public_key().hint()
87    }
88
89    /// Sign an arbitrary message.
90    async fn sign(&self, message: &[u8]) -> Result<[u8; 64], Self::Error>;
91    /// Perform X25519-style key agreement with a peer public key.
92    async fn agree(&self, peer: &PublicKey) -> Result<SharedSecret, Self::Error>;
93}
94
95/// Protocol-level crypto failures.
96#[derive(Clone, Debug, PartialEq, Eq)]
97pub enum CryptoError {
98    InvalidPublicKey,
99    InvalidSharedSecret,
100    InvalidPacket,
101    AuthenticationFailed,
102}
103
104/// Maximum channel-name length accepted by
105/// [`CryptoEngine::derive_named_channel_key`].
106///
107/// This is an implementation limit of the no-alloc canonicalization buffer,
108/// not a protocol constant; the spec places no length bound on named
109/// channels.
110pub const MAX_CHANNEL_NAME_LEN: usize = 64;
111
112/// Error returned when a channel name cannot be canonicalized for key
113/// derivation (see multicast-channels.md § Named Channels).
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub enum ChannelNameError {
116    /// The name contains non-ASCII characters. UTF-8 names are deferred by
117    /// the spec because Unicode case-folding is non-trivial.
118    NotAscii,
119    /// The name exceeds [`MAX_CHANNEL_NAME_LEN`].
120    TooLong,
121}
122
123/// Derived pairwise transport keys.
124#[derive(Clone, Zeroize, ZeroizeOnDrop)]
125pub struct PairwiseKeys {
126    pub k_enc: [u8; 32],
127    pub k_mic: [u8; 32],
128}
129
130/// Derived multicast or channel transport keys.
131#[derive(Clone)]
132pub struct DerivedChannelKeys {
133    pub k_enc: [u8; 32],
134    pub k_mic: [u8; 32],
135    pub channel_id: ChannelId,
136}
137
138impl Zeroize for DerivedChannelKeys {
139    fn zeroize(&mut self) {
140        self.k_enc.zeroize();
141        self.k_mic.zeroize();
142    }
143}
144
145impl Drop for DerivedChannelKeys {
146    fn drop(&mut self) {
147        self.zeroize();
148    }
149}
150
151/// Incremental AES-CMAC state.
152pub struct CmacState<C: AesCipher> {
153    cipher: C,
154    state: [u8; 16],
155    buffer: [u8; 16],
156    pos: usize,
157    k1: [u8; 16],
158    k2: [u8; 16],
159}
160
161impl<C: AesCipher> CmacState<C> {
162    /// Initialize a new incremental CMAC state.
163    pub fn new(cipher: C) -> Self {
164        let mut l = [0u8; 16];
165        cipher.encrypt_block(&mut l);
166        let k1 = dbl(&l);
167        let k2 = dbl(&k1);
168        Self {
169            cipher,
170            state: [0u8; 16],
171            buffer: [0u8; 16],
172            pos: 0,
173            k1,
174            k2,
175        }
176    }
177
178    /// Feed additional bytes into the MAC state.
179    pub fn update(&mut self, mut data: &[u8]) {
180        while !data.is_empty() {
181            let space = 16 - self.pos;
182            let take = space.min(data.len());
183            self.buffer[self.pos..self.pos + take].copy_from_slice(&data[..take]);
184            self.pos += take;
185            data = &data[take..];
186            if self.pos == 16 && !data.is_empty() {
187                self.process_buffer();
188            }
189        }
190    }
191
192    /// Finalize and return the full 16-byte CMAC value.
193    pub fn finalize(self) -> [u8; 16] {
194        let this = self;
195        let mut last = [0u8; 16];
196        if this.pos == 16 {
197            last.copy_from_slice(&this.buffer);
198            xor_in_place(&mut last, &this.k1);
199        } else {
200            last[..this.pos].copy_from_slice(&this.buffer[..this.pos]);
201            last[this.pos] = 0x80;
202            xor_in_place(&mut last, &this.k2);
203        }
204
205        xor_in_place(&mut last, &this.state);
206        this.cipher.encrypt_block(&mut last);
207        last
208    }
209
210    fn process_buffer(&mut self) {
211        let mut block = self.buffer;
212        xor_in_place(&mut block, &self.state);
213        self.cipher.encrypt_block(&mut block);
214        self.state = block;
215        self.buffer = [0u8; 16];
216        self.pos = 0;
217    }
218}
219
220/// Incremental RFC 5297 §2.4 S2V state.
221///
222/// Associated-data components are absorbed one at a time with
223/// [`push_component`](Self::push_component); the final vector component (the
224/// plaintext) is supplied to [`finalize`](Self::finalize), which returns the
225/// synthetic IV `V`. The state borrows a single keyed cipher, so one key
226/// schedule serves every CMAC in the computation.
227pub struct S2vState<'a, C: AesCipher> {
228    cipher: &'a C,
229    d: [u8; 16],
230}
231
232impl<'a, C: AesCipher> S2vState<'a, C> {
233    /// Initialize the running value: `D = CMAC(K, <zero>)`.
234    pub fn new(cipher: &'a C) -> Self {
235        let mut cmac = CmacState::new(cipher);
236        cmac.update(&[0u8; 16]);
237        Self {
238            cipher,
239            d: cmac.finalize(),
240        }
241    }
242
243    /// Absorb one associated-data component `S_i`, streamed through `feed`:
244    /// `D = dbl(D) xor CMAC(K, S_i)`.
245    pub fn push_component(&mut self, feed: impl FnOnce(&mut CmacState<&'a C>)) {
246        let mut cmac = CmacState::new(self.cipher);
247        feed(&mut cmac);
248        let mac = cmac.finalize();
249        self.d = dbl(&self.d);
250        xor_in_place(&mut self.d, &mac);
251    }
252
253    /// Finish with the final component `S_n` and return `V`.
254    ///
255    /// Per RFC 5297: `V = CMAC(K, S_n xorend D)` when `S_n` fills a block,
256    /// otherwise `V = CMAC(K, dbl(D) xor pad(S_n))`.
257    pub fn finalize(self, last: &[u8]) -> [u8; 16] {
258        let mut cmac = CmacState::new(self.cipher);
259        if last.len() >= 16 {
260            let split = last.len() - 16;
261            cmac.update(&last[..split]);
262            let mut tail = [0u8; 16];
263            tail.copy_from_slice(&last[split..]);
264            xor_in_place(&mut tail, &self.d);
265            cmac.update(&tail);
266        } else {
267            let mut block = dbl(&self.d);
268            for (dst, byte) in block.iter_mut().zip(last) {
269                *dst ^= *byte;
270            }
271            block[last.len()] ^= 0x80;
272            cmac.update(&block);
273        }
274        cmac.finalize()
275    }
276}
277
278/// UMSH protocol crypto engine.
279pub struct CryptoEngine<A: AesProvider, S: Sha256Provider> {
280    aes: A,
281    sha: S,
282}
283
284impl<A: AesProvider, S: Sha256Provider> CryptoEngine<A, S> {
285    /// Create a new engine from algorithm providers.
286    pub fn new(aes: A, sha: S) -> Self {
287        Self { aes, sha }
288    }
289
290    /// Derive stable pairwise encryption and MIC keys from a shared secret.
291    ///
292    /// The 64-byte HKDF output is the RFC 5297 AES-SIV key `K = K1 || K2`:
293    /// `k_mic` is the leftmost half (the S2V key `K1`) and `k_enc` is the
294    /// rightmost half (the CTR key `K2`).
295    pub fn derive_pairwise_keys(&self, shared_secret: &SharedSecret) -> PairwiseKeys {
296        let mut okm = [0u8; 64];
297        self.hkdf(
298            &shared_secret.0,
299            b"UMSH-PAIRWISE-SALT",
300            b"UMSH-UNICAST-V2",
301            &mut okm,
302        );
303        let mut keys = PairwiseKeys {
304            k_enc: [0u8; 32],
305            k_mic: [0u8; 32],
306        };
307        keys.k_mic.copy_from_slice(&okm[..32]);
308        keys.k_enc.copy_from_slice(&okm[32..64]);
309        okm.zeroize();
310        keys
311    }
312
313    /// Derive the channel identifier from a raw channel key.
314    pub fn derive_channel_id(&self, channel_key: &ChannelKey) -> ChannelId {
315        let mut out = [0u8; 2];
316        self.hkdf(&channel_key.0, b"UMSH-CHAN-ID", b"", &mut out);
317        ChannelId(out)
318    }
319
320    /// Derive three bytes for presentation — a deterministic color a user
321    /// interface can give a channel.
322    ///
323    /// This is the channel-identifier derivation run one byte longer, so the
324    /// first two bytes are the channel identifier itself: HKDF-Expand emits a
325    /// prefix of the same block either way. Nothing on the wire carries the
326    /// third byte, and nothing depends on it.
327    pub fn derive_channel_tint(&self, channel_key: &ChannelKey) -> [u8; 3] {
328        let mut out = [0u8; 3];
329        self.hkdf(&channel_key.0, b"UMSH-CHAN-ID", b"", &mut out);
330        out
331    }
332
333    /// Derive the channel tag — a local identity wide enough to tell apart two
334    /// channels whose keys derive the same channel identifier.
335    ///
336    /// Same derivation as the channel identifier, run to sixteen bytes, so the
337    /// identifier and the presentation tint are prefixes of it.
338    pub fn derive_channel_tag(&self, channel_key: &ChannelKey) -> ChannelTag {
339        let mut out = [0u8; 16];
340        self.hkdf(&channel_key.0, b"UMSH-CHAN-ID", b"", &mut out);
341        ChannelTag(out)
342    }
343
344    /// Derive multicast transport keys and the channel identifier.
345    ///
346    /// As with [`derive_pairwise_keys`](Self::derive_pairwise_keys), the
347    /// 64-byte HKDF output is the RFC 5297 AES-SIV key: S2V key first, CTR
348    /// key second.
349    pub fn derive_channel_keys(&self, channel_key: &ChannelKey) -> DerivedChannelKeys {
350        let channel_id = self.derive_channel_id(channel_key);
351        let mut info = [0u8; 15];
352        info[..13].copy_from_slice(b"UMSH-MCAST-V2");
353        info[13..15].copy_from_slice(&channel_id.0);
354        let mut okm = [0u8; 64];
355        self.hkdf(&channel_key.0, b"UMSH-MCAST-SALT", &info, &mut okm);
356        let mut derived = DerivedChannelKeys {
357            k_enc: [0u8; 32],
358            k_mic: [0u8; 32],
359            channel_id,
360        };
361        derived.k_mic.copy_from_slice(&okm[..32]);
362        derived.k_enc.copy_from_slice(&okm[32..64]);
363        okm.zeroize();
364        derived
365    }
366
367    /// Combine pairwise and channel keys for blind-unicast payload protection.
368    pub fn derive_blind_keys(
369        &self,
370        pairwise: &PairwiseKeys,
371        channel: &DerivedChannelKeys,
372    ) -> PairwiseKeys {
373        let mut keys = PairwiseKeys {
374            k_enc: [0u8; 32],
375            k_mic: [0u8; 32],
376        };
377        for (dst, (left, right)) in keys
378            .k_enc
379            .iter_mut()
380            .zip(pairwise.k_enc.iter().zip(channel.k_enc.iter()))
381        {
382            *dst = left ^ right;
383        }
384        for (dst, (left, right)) in keys
385            .k_mic
386            .iter_mut()
387            .zip(pairwise.k_mic.iter().zip(channel.k_mic.iter()))
388        {
389            *dst = left ^ right;
390        }
391        keys
392    }
393
394    /// Derive a channel key from a human-readable channel name.
395    ///
396    /// The name is canonicalized first, per the spec (multicast-channels.md
397    /// § Named Channels): it must be ASCII, and ASCII letters are folded to
398    /// lowercase before derivation — so `Public`, `public`, and `PUBLIC` all
399    /// derive the same key. Non-ASCII names are rejected (UTF-8 case-folding
400    /// is deferred by the spec). Names longer than
401    /// [`MAX_CHANNEL_NAME_LEN`] are rejected as an implementation limit of
402    /// the no-alloc canonicalization buffer.
403    pub fn derive_named_channel_key(&self, name: &str) -> Result<ChannelKey, ChannelNameError> {
404        let bytes = name.as_bytes();
405        if bytes.len() > MAX_CHANNEL_NAME_LEN {
406            return Err(ChannelNameError::TooLong);
407        }
408        if !name.is_ascii() {
409            return Err(ChannelNameError::NotAscii);
410        }
411        let mut canonical = [0u8; MAX_CHANNEL_NAME_LEN];
412        for (dst, byte) in canonical.iter_mut().zip(bytes) {
413            *dst = byte.to_ascii_lowercase();
414        }
415        Ok(ChannelKey(
416            self.sha
417                .hmac(b"UMSH-CHANNEL-V1", &[&canonical[..bytes.len()]]),
418        ))
419    }
420
421    /// Seal a unicast or multicast packet in place.
422    pub fn seal_packet(
423        &self,
424        packet: &mut UnsealedPacket<'_>,
425        keys: &PairwiseKeys,
426    ) -> Result<usize, CryptoError> {
427        let header = packet.header().map_err(|_| CryptoError::InvalidPacket)?;
428        let sec_info = header.sec_info.ok_or(CryptoError::InvalidPacket)?;
429        let full_mac = {
430            let bytes = packet.as_bytes();
431            self.s2v_tag(
432                &keys.k_mic,
433                |cmac| feed_aad(&header, bytes, |chunk| cmac.update(chunk)),
434                packet.body(),
435            )
436        };
437
438        let mic_len = sec_info
439            .scf
440            .mic_size()
441            .map_err(|_| CryptoError::InvalidPacket)?
442            .byte_len();
443        packet.mic_slot()[..mic_len].copy_from_slice(&full_mac[..mic_len]);
444
445        if sec_info.scf.encrypted() {
446            let iv = self.build_ctr_iv(
447                &full_mac[..mic_len],
448                &packet.as_bytes()[packet.sec_info_range()],
449            );
450            self.aes_ctr(&keys.k_enc, &iv, packet.body_mut());
451        }
452
453        Ok(mic_len)
454    }
455
456    /// Seal a blind-unicast packet, including its hidden address block.
457    pub fn seal_blind_packet(
458        &self,
459        packet: &mut UnsealedPacket<'_>,
460        blind_keys: &PairwiseKeys,
461        channel_keys: &DerivedChannelKeys,
462    ) -> Result<usize, CryptoError> {
463        let header = packet.header().map_err(|_| CryptoError::InvalidPacket)?;
464        match header.packet_type() {
465            PacketType::BlindUnicast | PacketType::BlindUnicastAckReq => {}
466            _ => return Err(CryptoError::InvalidPacket),
467        }
468
469        let sec_info = header.sec_info.ok_or(CryptoError::InvalidPacket)?;
470        let blind_addr_range = packet
471            .blind_addr_range()
472            .ok_or(CryptoError::InvalidPacket)?;
473        let full_mac = {
474            let bytes = packet.as_bytes();
475            self.s2v_tag(
476                &blind_keys.k_mic,
477                |cmac| feed_aad(&header, bytes, |chunk| cmac.update(chunk)),
478                packet.body(),
479            )
480        };
481
482        let mic_len = sec_info
483            .scf
484            .mic_size()
485            .map_err(|_| CryptoError::InvalidPacket)?
486            .byte_len();
487        let iv = self.build_ctr_iv(
488            &full_mac[..mic_len],
489            &packet.as_bytes()[packet.sec_info_range()],
490        );
491        packet.mic_slot()[..mic_len].copy_from_slice(&full_mac[..mic_len]);
492        if sec_info.scf.encrypted() {
493            self.aes_ctr(&blind_keys.k_enc, &iv, packet.body_mut());
494            self.aes_ctr(
495                &channel_keys.k_enc,
496                &iv,
497                &mut packet.as_bytes_mut()[blind_addr_range],
498            );
499        }
500        Ok(mic_len)
501    }
502
503    /// Verify and, if needed, decrypt a received secure packet in place.
504    pub fn open_packet(
505        &self,
506        buf: &mut [u8],
507        header: &PacketHeader,
508        keys: &PairwiseKeys,
509    ) -> Result<Range<usize>, CryptoError> {
510        let sec_info = header.sec_info.ok_or(CryptoError::InvalidPacket)?;
511        let mut mic = [0u8; 16];
512        let mic_len = header.mic_range.end - header.mic_range.start;
513        mic[..mic_len].copy_from_slice(&buf[header.mic_range.clone()]);
514        if sec_info.scf.encrypted() {
515            let iv = self.build_ctr_iv(&mic[..mic_len], &buf[sec_info_bytes_range(header)?]);
516            self.aes_ctr(&keys.k_enc, &iv, &mut buf[header.body_range.clone()]);
517        }
518
519        let full_mac = self.s2v_tag(
520            &keys.k_mic,
521            |cmac| feed_aad(header, buf, |chunk| cmac.update(chunk)),
522            &buf[header.body_range.clone()],
523        );
524        if !constant_time_eq(&mic[..mic_len], &full_mac[..mic_len]) {
525            return Err(CryptoError::AuthenticationFailed);
526        }
527
528        let body_range = match (header.packet_type(), header.source) {
529            (PacketType::Multicast, SourceAddrRef::Encrypted { len, .. }) => {
530                (header.body_range.start + len)..header.body_range.end
531            }
532            _ => header.body_range.clone(),
533        };
534        Ok(body_range)
535    }
536
537    /// Decrypt the blinded destination/source address block of a blind unicast.
538    pub fn decrypt_blind_addr(
539        &self,
540        buf: &mut [u8],
541        header: &PacketHeader,
542        channel_keys: &DerivedChannelKeys,
543    ) -> Result<(umsh_core::NodeHint, SourceAddrRef), CryptoError> {
544        match header.source {
545            SourceAddrRef::Encrypted { offset, len } => {
546                let addr_start = offset.checked_sub(3).ok_or(CryptoError::InvalidPacket)?;
547                let addr_end = addr_start + 3 + len;
548                let iv = self.build_ctr_iv(
549                    &buf[header.mic_range.clone()],
550                    &buf[sec_info_bytes_range(header)?],
551                );
552                self.aes_ctr(&channel_keys.k_enc, &iv, &mut buf[addr_start..addr_end]);
553                let dst = umsh_core::NodeHint([
554                    buf[addr_start],
555                    buf[addr_start + 1],
556                    buf[addr_start + 2],
557                ]);
558                let source = if len == 3 {
559                    SourceAddrRef::Hint(umsh_core::NodeHint([
560                        buf[addr_start + 3],
561                        buf[addr_start + 4],
562                        buf[addr_start + 5],
563                    ]))
564                } else if len == 32 {
565                    SourceAddrRef::FullKeyAt {
566                        offset: addr_start + 3,
567                    }
568                } else {
569                    return Err(CryptoError::InvalidPacket);
570                };
571                Ok((dst, source))
572            }
573            SourceAddrRef::Hint(hint) => {
574                let dst = header.dst.ok_or(CryptoError::InvalidPacket)?;
575                Ok((dst, SourceAddrRef::Hint(hint)))
576            }
577            SourceAddrRef::FullKeyAt { offset } => {
578                let dst = header.dst.ok_or(CryptoError::InvalidPacket)?;
579                let _ = buf
580                    .get(offset..offset + 32)
581                    .ok_or(CryptoError::InvalidPacket)?;
582                Ok((dst, SourceAddrRef::FullKeyAt { offset }))
583            }
584            SourceAddrRef::None => Err(CryptoError::InvalidPacket),
585        }
586    }
587
588    /// Compute the packet tag `V = S2V(k_mic, S1, S2)` per RFC 5297 §2.4,
589    /// with the canonical AAD (streamed through `aad`) as the single
590    /// associated-data component `S1` and `plaintext` as the final
591    /// component `S2`.
592    pub fn s2v_tag(
593        &self,
594        k_mic: &[u8; 32],
595        aad: impl FnOnce(&mut CmacState<&A::Cipher>),
596        plaintext: &[u8],
597    ) -> [u8; 16] {
598        let cipher = self.aes.new_cipher(k_mic);
599        let mut s2v = S2vState::new(&cipher);
600        s2v.push_component(aad);
601        s2v.finalize(plaintext)
602    }
603
604    /// Compute the 8-byte MAC ack trailer (`ack_mic || ack_tag`) from a full
605    /// S2V tag and `k_enc`.
606    ///
607    /// The trailer splits into two 4-byte halves:
608    ///
609    /// - **`ack_mic`** = the first 4 bytes of `full_tag`. Because the on-wire
610    ///   MIC is a prefix-truncation of the full tag, this equals the first
611    ///   4 bytes of the acknowledged packet's on-wire MIC — a *public*
612    ///   correlation handle any node that received the original packet
613    ///   (including forwarding repeaters) can compute.
614    /// - **`ack_tag`** = `truncate_4( AES-256-ECB(k_enc, full_tag) )`. A keyed
615    ///   value only the original sender and final destination can produce; it
616    ///   authenticates the standalone ack.
617    ///
618    /// See the spec's *Ack Tag Construction* section.
619    pub fn compute_ack_trailer(&self, full_tag: &[u8; 16], k_enc: &[u8; 32]) -> [u8; 8] {
620        let cipher = self.aes.new_cipher(k_enc);
621        let mut block = *full_tag;
622        cipher.encrypt_block(&mut block);
623        let mut trailer = [0u8; 8];
624        trailer[..4].copy_from_slice(&full_tag[..4]); // ack_mic (public)
625        trailer[4..].copy_from_slice(&block[..4]); // ack_tag (keyed)
626        trailer
627    }
628
629    /// Create a reusable incremental CMAC state.
630    pub fn cmac_state(&self, key: &[u8; 32]) -> CmacState<A::Cipher> {
631        CmacState::new(self.aes.new_cipher(key))
632    }
633
634    /// Convenience wrapper for AES-CMAC over concatenated slices.
635    pub fn aes_cmac(&self, key: &[u8; 32], data: &[&[u8]]) -> [u8; 16] {
636        let mut state = self.cmac_state(key);
637        for chunk in data {
638            state.update(chunk);
639        }
640        state.finalize()
641    }
642
643    /// Apply AES-CTR using `iv` as the initial counter block.
644    pub fn aes_ctr(&self, key: &[u8; 32], iv: &[u8; 16], data: &mut [u8]) {
645        aes_ctr_with_cipher(&self.aes.new_cipher(key), iv, data);
646    }
647
648    /// Construct the CTR IV from MIC bytes and SECINFO bytes.
649    ///
650    /// With a 16-byte MIC the IV is the masked MIC alone — exactly the
651    /// initial counter `Q` from RFC 5297 §2.6; shorter MICs are padded
652    /// from the SECINFO bytes, so the IV construction of a received
653    /// packet must locate SECINFO exactly (see [`sec_info_bytes_range`]).
654    /// The RFC's bit-clearing of the top bit of bytes 8 and 12 is applied
655    /// unconditionally, whatever bytes occupy those positions.
656    pub fn build_ctr_iv(&self, mic: &[u8], sec_info_bytes: &[u8]) -> [u8; 16] {
657        let mut iv = [0u8; 16];
658        let mut written = 0usize;
659        for byte in mic.iter().chain(sec_info_bytes.iter()).take(16) {
660            iv[written] = *byte;
661            written += 1;
662        }
663        iv[8] &= 0x7F;
664        iv[12] &= 0x7F;
665        iv
666    }
667
668    /// Run HKDF-SHA256 and write the output into `okm`.
669    pub fn hkdf(&self, ikm: &[u8], salt: &[u8], info: &[u8], okm: &mut [u8]) {
670        hkdf_sha256(&self.sha, ikm, salt, info, okm);
671    }
672}
673
674/// HKDF-SHA256 over any [`Sha256Provider`], for callers that have no
675/// [`CryptoEngine`] — the entropy pool derives keys before any AES
676/// provider exists.
677pub fn hkdf_sha256<S: Sha256Provider>(
678    sha: &S,
679    ikm: &[u8],
680    salt: &[u8],
681    info: &[u8],
682    okm: &mut [u8],
683) {
684    let prk = sha.hmac(salt, &[ikm]);
685    let mut previous = [0u8; 32];
686    let mut previous_len = 0usize;
687    let mut written = 0usize;
688    let mut counter = 1u8;
689    while written < okm.len() {
690        let next = sha.hmac(&prk, &[&previous[..previous_len], info, &[counter]]);
691        let take = (okm.len() - written).min(next.len());
692        okm[written..written + take].copy_from_slice(&next[..take]);
693        previous = next;
694        previous_len = 32;
695        written += take;
696        counter = counter.wrapping_add(1);
697    }
698}
699
700fn aes_ctr_with_cipher<C: AesCipher>(cipher: &C, iv: &[u8; 16], data: &mut [u8]) {
701    let mut counter = *iv;
702    for chunk in data.chunks_mut(16) {
703        let mut stream = counter;
704        cipher.encrypt_block(&mut stream);
705        for (dst, src) in chunk.iter_mut().zip(stream.iter()) {
706            *dst ^= *src;
707        }
708        increment_counter(&mut counter);
709    }
710}
711
712fn xor_in_place(dst: &mut [u8; 16], rhs: &[u8; 16]) {
713    for (left, right) in dst.iter_mut().zip(rhs.iter()) {
714        *left ^= *right;
715    }
716}
717
718fn dbl(block: &[u8; 16]) -> [u8; 16] {
719    let mut out = [0u8; 16];
720    let mut carry = 0u8;
721    for (index, byte) in block.iter().enumerate().rev() {
722        out[index] = (byte << 1) | carry;
723        carry = byte >> 7;
724    }
725    if block[0] & 0x80 != 0 {
726        out[15] ^= 0x87;
727    }
728    out
729}
730
731/// The on-wire SECINFO byte range of a parsed secure packet. In every
732/// secure layout (unicast, multicast, blind unicast) SECINFO
733/// immediately precedes the options block — not the body, which the
734/// options end marker or a blind address block may separate from it.
735fn sec_info_bytes_range(header: &PacketHeader) -> Result<Range<usize>, CryptoError> {
736    let sec_info = header.sec_info.ok_or(CryptoError::InvalidPacket)?;
737    header
738        .options_range
739        .start
740        .checked_sub(sec_info.wire_len())
741        .map(|start| start..header.options_range.start)
742        .ok_or(CryptoError::InvalidPacket)
743}
744
745fn increment_counter(counter: &mut [u8; 16]) {
746    for byte in counter.iter_mut().rev() {
747        let (next, carry) = byte.overflowing_add(1);
748        *byte = next;
749        if !carry {
750            break;
751        }
752    }
753}
754
755fn constant_time_eq(left: &[u8], right: &[u8]) -> bool {
756    let mut diff = left.len() ^ right.len();
757    let max_len = left.len().max(right.len());
758    for index in 0..max_len {
759        let lhs = left.get(index).copied().unwrap_or(0);
760        let rhs = right.get(index).copied().unwrap_or(0);
761        diff |= usize::from(lhs ^ rhs);
762    }
763    diff == 0
764}
765
766#[cfg(feature = "software-crypto")]
767pub mod software {
768    use aes::cipher::{BlockDecrypt, BlockEncrypt, KeyInit, generic_array::GenericArray};
769    use curve25519_dalek::{edwards::CompressedEdwardsY, montgomery::MontgomeryPoint};
770    use ed25519_dalek::{Signer, SigningKey};
771    use rand_core::CryptoRngCore;
772    use sha2::{Digest, Sha256, Sha512};
773
774    use super::*;
775
776    pub struct SoftwareAes;
777
778    pub struct SoftwareAesCipher(aes::Aes256);
779
780    impl AesCipher for SoftwareAesCipher {
781        fn encrypt_block(&self, block: &mut [u8; 16]) {
782            self.0.encrypt_block(GenericArray::from_mut_slice(block));
783        }
784
785        fn decrypt_block(&self, block: &mut [u8; 16]) {
786            self.0.decrypt_block(GenericArray::from_mut_slice(block));
787        }
788    }
789
790    impl AesProvider for SoftwareAes {
791        type Cipher = SoftwareAesCipher;
792
793        fn new_cipher(&self, key: &[u8; 32]) -> Self::Cipher {
794            SoftwareAesCipher(aes::Aes256::new(GenericArray::from_slice(key)))
795        }
796    }
797
798    pub struct SoftwareSha256;
799
800    impl Sha256Provider for SoftwareSha256 {
801        fn hash(&self, data: &[&[u8]]) -> [u8; 32] {
802            let mut hasher = Sha256::new();
803            for chunk in data {
804                hasher.update(chunk);
805            }
806            hasher.finalize().into()
807        }
808
809        fn hmac(&self, key: &[u8], data: &[&[u8]]) -> [u8; 32] {
810            const BLOCK_LEN: usize = 64;
811            let mut key_block = [0u8; BLOCK_LEN];
812            if key.len() > BLOCK_LEN {
813                key_block[..32].copy_from_slice(&self.hash(&[key]));
814            } else {
815                key_block[..key.len()].copy_from_slice(key);
816            }
817
818            let mut ipad = [0x36u8; BLOCK_LEN];
819            let mut opad = [0x5Cu8; BLOCK_LEN];
820            for index in 0..BLOCK_LEN {
821                ipad[index] ^= key_block[index];
822                opad[index] ^= key_block[index];
823            }
824
825            let mut inner = Sha256::new();
826            inner.update(ipad);
827            for chunk in data {
828                inner.update(chunk);
829            }
830            let inner_hash = inner.finalize();
831
832            let mut outer = Sha256::new();
833            outer.update(opad);
834            outer.update(inner_hash);
835            outer.finalize().into()
836        }
837    }
838
839    /// Cloning is deliberate: a holder that must both hand the identity to
840    /// the MAC and retain signing ability (e.g. standalone node-identity
841    /// bundles) keeps a clone rather than round-tripping secret bytes.
842    #[derive(Clone)]
843    pub struct SoftwareIdentity {
844        secret: SigningKey,
845        public: PublicKey,
846    }
847
848    impl SoftwareIdentity {
849        pub fn generate(rng: &mut impl CryptoRngCore) -> Self {
850            let secret = SigningKey::generate(rng);
851            let public = PublicKey(secret.verifying_key().to_bytes());
852            Self { secret, public }
853        }
854
855        pub fn from_secret_bytes(bytes: &[u8; 32]) -> Self {
856            let secret = SigningKey::from_bytes(bytes);
857            let public = PublicKey(secret.verifying_key().to_bytes());
858            Self { secret, public }
859        }
860
861        pub fn shared_secret_with(&self, peer: &PublicKey) -> Result<SharedSecret, CryptoError> {
862            let local = signing_key_to_x25519(&self.secret);
863            let remote = public_key_to_x25519(peer)?;
864            let shared = local.diffie_hellman(&remote);
865            let bytes = shared.to_bytes();
866            if bytes.iter().all(|byte| *byte == 0) {
867                return Err(CryptoError::InvalidSharedSecret);
868            }
869            Ok(SharedSecret(bytes))
870        }
871    }
872
873    impl NodeIdentity for SoftwareIdentity {
874        type Error = CryptoError;
875
876        fn public_key(&self) -> &PublicKey {
877            &self.public
878        }
879
880        async fn sign(&self, message: &[u8]) -> Result<[u8; 64], Self::Error> {
881            Ok(self.secret.sign(message).to_bytes())
882        }
883
884        async fn agree(&self, peer: &PublicKey) -> Result<SharedSecret, Self::Error> {
885            self.shared_secret_with(peer)
886        }
887    }
888
889    pub type SoftwareCryptoEngine = CryptoEngine<SoftwareAes, SoftwareSha256>;
890
891    fn signing_key_to_x25519(secret: &SigningKey) -> x25519_dalek::StaticSecret {
892        let digest = Sha512::digest(secret.to_bytes());
893        let mut scalar = [0u8; 32];
894        scalar.copy_from_slice(&digest[..32]);
895        scalar[0] &= 248;
896        scalar[31] &= 127;
897        scalar[31] |= 64;
898        x25519_dalek::StaticSecret::from(scalar)
899    }
900
901    fn public_key_to_x25519(public: &PublicKey) -> Result<x25519_dalek::PublicKey, CryptoError> {
902        let compressed = CompressedEdwardsY(public.0);
903        let edwards = compressed
904            .decompress()
905            .ok_or(CryptoError::InvalidPublicKey)?;
906        let mont: MontgomeryPoint = edwards.to_montgomery();
907        Ok(x25519_dalek::PublicKey::from(mont.to_bytes()))
908    }
909
910    /// Return `true` if `pk`'s bytes decode to a valid Ed25519 compressed
911    /// public-key point on the curve. Used by the MAC layer and CLI to reject
912    /// malformed peer keys before they can poison the peer registry.
913    pub fn is_valid_ed25519_public_key(pk: &PublicKey) -> bool {
914        CompressedEdwardsY(pk.0).decompress().is_some()
915    }
916
917    /// Verify a detached Ed25519 signature against `pk`. Used for standalone
918    /// node-identity bundles (QR codes, broadcasts) whose signed range must
919    /// verify before the bundle's claims are presented as authenticated.
920    pub fn verify_ed25519_signature(pk: &PublicKey, message: &[u8], signature: &[u8; 64]) -> bool {
921        let Ok(key) = ed25519_dalek::VerifyingKey::from_bytes(&pk.0) else {
922            return false;
923        };
924        key.verify_strict(message, &ed25519_dalek::Signature::from_bytes(signature))
925            .is_ok()
926    }
927}
928
929#[cfg(feature = "software-crypto")]
930pub use software::*;
931
932#[cfg(test)]
933mod tests {
934    use crate::{constant_time_eq, dbl};
935
936    #[cfg(feature = "software-crypto")]
937    use super::{software::*, *};
938    #[cfg(feature = "software-crypto")]
939    use umsh_core::{MicSize, NodeHint, PacketBuilder, PacketHeader, PublicKey};
940
941    #[cfg(feature = "software-crypto")]
942    #[test]
943    fn pairwise_hkdf_is_stable() {
944        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
945        let keys = engine.derive_pairwise_keys(&SharedSecret([7u8; 32]));
946        assert_ne!(keys.k_enc, [0u8; 32]);
947        assert_ne!(keys.k_mic, [0u8; 32]);
948    }
949
950    /// The pairwise HKDF output is the RFC 5297 AES-SIV key: S2V key (K1)
951    /// first, CTR key (K2) second.
952    #[cfg(feature = "software-crypto")]
953    #[test]
954    fn pairwise_okm_is_the_rfc5297_key() {
955        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
956        let shared = SharedSecret([9u8; 32]);
957        let keys = engine.derive_pairwise_keys(&shared);
958        let mut okm = [0u8; 64];
959        engine.hkdf(
960            &shared.0,
961            b"UMSH-PAIRWISE-SALT",
962            b"UMSH-UNICAST-V2",
963            &mut okm,
964        );
965        assert_eq!(keys.k_mic, okm[..32]);
966        assert_eq!(keys.k_enc, okm[32..]);
967    }
968
969    /// NIST SP 800-38B CMAC-AES256 example key.
970    #[cfg(feature = "software-crypto")]
971    const NIST_AES256_KEY: &str =
972        "603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4";
973
974    #[cfg(feature = "software-crypto")]
975    #[test]
976    fn aes_cmac_matches_nist_aes256_example_2() {
977        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
978        let key = hex_32(NIST_AES256_KEY);
979        let msg = hex_vec("6bc1bee22e409f96e93d7e117393172a");
980        let expected = hex_16("28a7023f452e8f82bd4bf28d8c37c35c");
981        assert_eq!(engine.aes_cmac(&key, &[&msg]), expected);
982    }
983
984    #[cfg(feature = "software-crypto")]
985    #[test]
986    fn aes_cmac_matches_nist_aes256_example_1_empty() {
987        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
988        let key = hex_32(NIST_AES256_KEY);
989        let expected = hex_16("028962f61b7bf89efc6b551f4667d983");
990        assert_eq!(engine.aes_cmac(&key, &[&[]]), expected);
991    }
992
993    #[cfg(feature = "software-crypto")]
994    #[test]
995    fn aes_cmac_matches_nist_aes256_example_4_64b() {
996        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
997        let key = hex_32(NIST_AES256_KEY);
998        let msg = hex_vec(
999            "6bc1bee22e409f96e93d7e117393172a\
1000             ae2d8a571e03ac9c9eb76fac45af8e51\
1001             30c81c46a35ce411e5fbc1191a0a52ef\
1002             f69f2445df4f9b17ad2b417be66c3710",
1003        );
1004        let expected = hex_16("e1992190549f6ed5696a2c056c315410");
1005        assert_eq!(engine.aes_cmac(&key, &[&msg]), expected);
1006    }
1007
1008    #[cfg(feature = "software-crypto")]
1009    #[test]
1010    fn aes_cmac_matches_nist_aes256_example_3_40b() {
1011        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1012        let key = hex_32(NIST_AES256_KEY);
1013        let msg = hex_vec(
1014            "6bc1bee22e409f96e93d7e117393172a\
1015             ae2d8a571e03ac9c9eb76fac45af8e51\
1016             30c81c46a35ce411",
1017        );
1018        let expected = hex_16("aaf3d8f1de5640c232f5b169b9c911e6");
1019        assert_eq!(engine.aes_cmac(&key, &[&msg]), expected);
1020    }
1021
1022    /// NIST SP 800-38A Section F.5.5 — AES-256 CTR mode, single block.
1023    #[cfg(feature = "software-crypto")]
1024    #[test]
1025    fn aes_ctr_matches_nist_sp800_38a_block_1() {
1026        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1027        let key = hex_32(NIST_AES256_KEY);
1028        let iv = hex_16("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff");
1029        let mut data = hex_vec("6bc1bee22e409f96e93d7e117393172a");
1030        engine.aes_ctr(&key, &iv, &mut data);
1031        assert_eq!(data, hex_vec("601ec313775789a5b7a7f504bbf3d228"));
1032    }
1033
1034    /// NIST SP 800-38A Section F.5.5 — AES-256 CTR mode, 4 blocks.
1035    /// Verifies counter increment across multiple blocks.
1036    #[cfg(feature = "software-crypto")]
1037    #[test]
1038    fn aes_ctr_matches_nist_sp800_38a_4_blocks() {
1039        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1040        let key = hex_32(NIST_AES256_KEY);
1041        let iv = hex_16("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff");
1042        let mut data = hex_vec(
1043            "6bc1bee22e409f96e93d7e117393172a\
1044             ae2d8a571e03ac9c9eb76fac45af8e51\
1045             30c81c46a35ce411e5fbc1191a0a52ef\
1046             f69f2445df4f9b17ad2b417be66c3710",
1047        );
1048        let expected = hex_vec(
1049            "601ec313775789a5b7a7f504bbf3d228\
1050             f443e3ca4d62b59aca84e990cacaf5c5\
1051             2b0930daa23de94ce87017ba2d84988d\
1052             dfc9c58db67aada613c2dd08457941a6",
1053        );
1054        engine.aes_ctr(&key, &iv, &mut data);
1055        assert_eq!(data, expected);
1056    }
1057
1058    /// NIST SP 800-38A Section F.5.6 — CTR decrypt (symmetric operation).
1059    #[cfg(feature = "software-crypto")]
1060    #[test]
1061    fn aes_ctr_decrypt_matches_nist_sp800_38a() {
1062        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1063        let key = hex_32(NIST_AES256_KEY);
1064        let iv = hex_16("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff");
1065        let mut data = hex_vec(
1066            "601ec313775789a5b7a7f504bbf3d228\
1067             f443e3ca4d62b59aca84e990cacaf5c5\
1068             2b0930daa23de94ce87017ba2d84988d\
1069             dfc9c58db67aada613c2dd08457941a6",
1070        );
1071        let expected = hex_vec(
1072            "6bc1bee22e409f96e93d7e117393172a\
1073             ae2d8a571e03ac9c9eb76fac45af8e51\
1074             30c81c46a35ce411e5fbc1191a0a52ef\
1075             f69f2445df4f9b17ad2b417be66c3710",
1076        );
1077        engine.aes_ctr(&key, &iv, &mut data);
1078        assert_eq!(data, expected);
1079    }
1080
1081    /// AES-CTR with partial final block (non-aligned length).
1082    #[cfg(feature = "software-crypto")]
1083    #[test]
1084    fn aes_ctr_partial_block() {
1085        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1086        let key = hex_32(NIST_AES256_KEY);
1087        let iv = hex_16("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff");
1088        // Encrypt 5 bytes (less than one block)
1089        let mut data = hex_vec("6bc1bee22e");
1090        engine.aes_ctr(&key, &iv, &mut data);
1091        // Should match first 5 bytes of full block 1 ciphertext
1092        assert_eq!(data, hex_vec("601ec31377"));
1093        // Decrypt back
1094        engine.aes_ctr(&key, &iv, &mut data);
1095        assert_eq!(data, hex_vec("6bc1bee22e"));
1096    }
1097
1098    /// AES-128 cipher for the RFC 5297 appendix vectors, which use
1099    /// AES-SIV-CMAC-256 (two 128-bit subkeys). The S2V and CTR cores are
1100    /// block-level and key-size-free, so the RFC's own vectors remain
1101    /// runnable even though the protocol uses AES-256.
1102    #[cfg(feature = "software-crypto")]
1103    struct Aes128TestCipher(aes::Aes128);
1104
1105    #[cfg(feature = "software-crypto")]
1106    impl Aes128TestCipher {
1107        fn new(key: &[u8; 16]) -> Self {
1108            use aes::cipher::KeyInit;
1109            Self(aes::Aes128::new(
1110                aes::cipher::generic_array::GenericArray::from_slice(key),
1111            ))
1112        }
1113    }
1114
1115    #[cfg(feature = "software-crypto")]
1116    impl AesCipher for Aes128TestCipher {
1117        fn encrypt_block(&self, block: &mut [u8; 16]) {
1118            use aes::cipher::BlockEncrypt;
1119            self.0
1120                .encrypt_block(aes::cipher::generic_array::GenericArray::from_mut_slice(
1121                    block,
1122                ));
1123        }
1124
1125        fn decrypt_block(&self, block: &mut [u8; 16]) {
1126            use aes::cipher::BlockDecrypt;
1127            self.0
1128                .decrypt_block(aes::cipher::generic_array::GenericArray::from_mut_slice(
1129                    block,
1130                ));
1131        }
1132    }
1133
1134    /// RFC 5297 Appendix A.1 — deterministic authenticated encryption.
1135    /// Exercises the single-AD path and S2V's dbl-and-pad branch (the
1136    /// 14-byte plaintext is shorter than a block).
1137    #[cfg(feature = "software-crypto")]
1138    #[test]
1139    fn rfc5297_a1_deterministic_aes_siv() {
1140        let k1 = hex_16("fffefdfcfbfaf9f8f7f6f5f4f3f2f1f0");
1141        let k2 = hex_16("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff");
1142        let ad = hex_vec("101112131415161718191a1b1c1d1e1f2021222324252627");
1143        let plaintext = hex_vec("112233445566778899aabbccddee");
1144
1145        let s2v_cipher = Aes128TestCipher::new(&k1);
1146        let mut s2v = S2vState::new(&s2v_cipher);
1147        s2v.push_component(|cmac| cmac.update(&ad));
1148        let v = s2v.finalize(&plaintext);
1149        assert_eq!(v, hex_16("85632d07c6e8f37f950acd320a2ecc93"));
1150
1151        let mut q = v;
1152        q[8] &= 0x7F;
1153        q[12] &= 0x7F;
1154        let ctr_cipher = Aes128TestCipher::new(&k2);
1155        let mut data = plaintext.clone();
1156        aes_ctr_with_cipher(&ctr_cipher, &q, &mut data);
1157        assert_eq!(data, hex_vec("40c02b9690c4dc04daef7f6afe5c"));
1158    }
1159
1160    /// RFC 5297 Appendix A.2 — nonce-based authenticated encryption.
1161    /// Exercises multiple S2V components and the xorend branch (the
1162    /// 47-byte plaintext spans multiple blocks).
1163    #[cfg(feature = "software-crypto")]
1164    #[test]
1165    fn rfc5297_a2_nonce_based_aes_siv() {
1166        let k1 = hex_16("7f7e7d7c7b7a79787776757473727170");
1167        let k2 = hex_16("404142434445464748494a4b4c4d4e4f");
1168        let ad1 = hex_vec(
1169            "00112233445566778899aabbccddeeff\
1170             deaddadadeaddadaffeeddccbbaa9988\
1171             7766554433221100",
1172        );
1173        let ad2 = hex_vec("102030405060708090a0");
1174        let nonce = hex_vec("09f911029d74e35bd84156c5635688c0");
1175        let plaintext = hex_vec(
1176            "7468697320697320736f6d6520706c61\
1177             696e7465787420746f20656e63727970\
1178             74207573696e67205349562d414553",
1179        );
1180
1181        let s2v_cipher = Aes128TestCipher::new(&k1);
1182        let mut s2v = S2vState::new(&s2v_cipher);
1183        s2v.push_component(|cmac| cmac.update(&ad1));
1184        s2v.push_component(|cmac| cmac.update(&ad2));
1185        s2v.push_component(|cmac| cmac.update(&nonce));
1186        let v = s2v.finalize(&plaintext);
1187        assert_eq!(v, hex_16("7bdb6e3b432667eb06f4d14bff2fbd0f"));
1188
1189        let mut q = v;
1190        q[8] &= 0x7F;
1191        q[12] &= 0x7F;
1192        let ctr_cipher = Aes128TestCipher::new(&k2);
1193        let mut data = plaintext.clone();
1194        aes_ctr_with_cipher(&ctr_cipher, &q, &mut data);
1195        assert_eq!(
1196            data,
1197            hex_vec(
1198                "cb900f2fddbe404326601965c889bf17\
1199                 dba77ceb094fa663b7a3f748ba8af829\
1200                 ea64ad544a272e9c485b62a3fd5c0d"
1201            )
1202        );
1203    }
1204
1205    /// The RFC 5297 bit-clearing applies at every MIC length, whatever
1206    /// bytes occupy positions 8 and 12.
1207    #[cfg(feature = "software-crypto")]
1208    #[test]
1209    fn build_ctr_iv_masks_rfc5297_bits() {
1210        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1211        let mic = [0xFFu8; 16];
1212        let sec_info = [0xFFu8; 7];
1213        for mic_len in [4usize, 8, 12, 16] {
1214            let iv = engine.build_ctr_iv(&mic[..mic_len], &sec_info);
1215            assert_eq!(iv[8] & 0x80, 0, "mic_len {mic_len}");
1216            assert_eq!(iv[12] & 0x80, 0, "mic_len {mic_len}");
1217        }
1218    }
1219
1220    #[cfg(feature = "software-crypto")]
1221    #[test]
1222    fn hmac_sha256_matches_rfc4231_case_1() {
1223        let sha = SoftwareSha256;
1224        let key = [0x0bu8; 20];
1225        let expected = hex_32("b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7");
1226        assert_eq!(sha.hmac(&key, &[b"Hi There"]), expected);
1227    }
1228
1229    #[cfg(feature = "software-crypto")]
1230    #[test]
1231    fn hkdf_matches_rfc5869_case_1() {
1232        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1233        let ikm = [0x0bu8; 22];
1234        let salt = hex_vec("000102030405060708090a0b0c");
1235        let info = hex_vec("f0f1f2f3f4f5f6f7f8f9");
1236        let mut okm = [0u8; 42];
1237        engine.hkdf(&ikm, &salt, &info, &mut okm);
1238        assert_eq!(
1239            okm.to_vec(),
1240            hex_vec(
1241                "3cb25f25faacd57a90434f64d0362f2a2d2d0a90cf1a5a4c5db02d56ecc4c5bf34007208d5b887185865"
1242            )
1243        );
1244    }
1245
1246    #[test]
1247    fn cmac_doubling_matches_rfc4493_subkey_generation() {
1248        let l = hex_16("7df76b0c1ab899b33e42f047b91b546f");
1249        let expected_k1 = hex_16("fbeed618357133667c85e08f7236a8de");
1250        let expected_k2 = hex_16("f7ddac306ae266ccf90bc11ee46d513b");
1251        let k1 = dbl(&l);
1252        let k2 = dbl(&k1);
1253        assert_eq!(k1, expected_k1);
1254        assert_eq!(k2, expected_k2);
1255    }
1256
1257    #[test]
1258    fn constant_time_eq_rejects_mismatch() {
1259        assert!(constant_time_eq(&[1, 2, 3], &[1, 2, 3]));
1260        assert!(!constant_time_eq(&[1, 2, 3], &[1, 2, 4]));
1261        assert!(!constant_time_eq(&[1, 2, 3], &[1, 2, 3, 4]));
1262    }
1263
1264    #[cfg(feature = "software-crypto")]
1265    #[test]
1266    fn unicast_seal_and_open_round_trip() {
1267        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1268        let keys = engine.derive_pairwise_keys(&SharedSecret([9u8; 32]));
1269        let src = PublicKey([0xA1; 32]);
1270        let dst = NodeHint([0xC3, 0xD4, 0x25]);
1271        let mut buf = [0u8; 128];
1272        let mut packet = PacketBuilder::new(&mut buf)
1273            .unicast(dst)
1274            .source_full(&src)
1275            .frame_counter(1)
1276            .encrypted()
1277            .mic_size(MicSize::Mic16)
1278            .payload(b"hello")
1279            .build()
1280            .unwrap();
1281
1282        engine.seal_packet(&mut packet, &keys).unwrap();
1283        let header = PacketHeader::parse(packet.as_bytes()).unwrap();
1284        let mut wire = packet.as_bytes().to_vec();
1285        let range = engine.open_packet(&mut wire, &header, &keys).unwrap();
1286        assert_eq!(&wire[range], b"hello");
1287    }
1288
1289    /// A sealed Mic16 packet is byte-for-byte RFC 5297 AEAD_AES_SIV_CMAC_512:
1290    /// the RustCrypto `aes-siv` crate, keyed with `k_mic || k_enc` and given
1291    /// the canonical AAD as its single header component, must reproduce the
1292    /// on-wire MIC and ciphertext exactly.
1293    #[cfg(feature = "software-crypto")]
1294    #[test]
1295    fn mic16_packet_matches_rfc5297_aes_siv_crate() {
1296        use aes_siv::{KeyInit, siv::Aes256Siv};
1297
1298        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1299        let keys = engine.derive_pairwise_keys(&SharedSecret([9u8; 32]));
1300        let src = PublicKey([0xA1; 32]);
1301        let dst = NodeHint([0xC3, 0xD4, 0x25]);
1302        for payload in [&b"hello, RFC 5297 - a plaintext spanning blocks"[..], b""] {
1303            let mut buf = [0u8; 128];
1304            let mut packet = PacketBuilder::new(&mut buf)
1305                .unicast(dst)
1306                .source_full(&src)
1307                .frame_counter(7)
1308                .encrypted()
1309                .mic_size(MicSize::Mic16)
1310                .payload(payload)
1311                .build()
1312                .unwrap();
1313            engine.seal_packet(&mut packet, &keys).unwrap();
1314
1315            let wire = packet.as_bytes().to_vec();
1316            let header = PacketHeader::parse(&wire).unwrap();
1317            let mut aad = std::vec::Vec::new();
1318            feed_aad(&header, &wire, |chunk| aad.extend_from_slice(chunk));
1319
1320            let mut siv_key = [0u8; 64];
1321            siv_key[..32].copy_from_slice(&keys.k_mic);
1322            siv_key[32..].copy_from_slice(&keys.k_enc);
1323            let mut siv = Aes256Siv::new((&siv_key).into());
1324            let sealed = siv.encrypt([&aad], payload).unwrap();
1325
1326            // RFC 5297 output is V || C; UMSH carries C in the body and V at
1327            // the end of the packet.
1328            assert_eq!(sealed[..16], wire[header.mic_range.clone()]);
1329            assert_eq!(sealed[16..], wire[header.body_range.clone()]);
1330        }
1331    }
1332
1333    /// For truncated MICs only the IV construction differs: the full S2V tag
1334    /// still matches the `aes-siv` crate's tag for the same inputs.
1335    #[cfg(feature = "software-crypto")]
1336    #[test]
1337    fn short_mic_tag_matches_rfc5297_s2v() {
1338        use aes_siv::{KeyInit, siv::Aes256Siv};
1339
1340        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1341        let keys = engine.derive_pairwise_keys(&SharedSecret([11u8; 32]));
1342        let payload = b"short-tag payload";
1343        let src = PublicKey([0xB2; 32]);
1344        let dst = NodeHint([0x10, 0x20, 0x30]);
1345        let mut buf = [0u8; 128];
1346        let mut packet = PacketBuilder::new(&mut buf)
1347            .unicast(dst)
1348            .source_full(&src)
1349            .frame_counter(3)
1350            .encrypted()
1351            .mic_size(MicSize::Mic4)
1352            .payload(payload)
1353            .build()
1354            .unwrap();
1355
1356        let full_tag = {
1357            let header = packet.header().unwrap();
1358            let bytes = packet.as_bytes();
1359            engine.s2v_tag(
1360                &keys.k_mic,
1361                |cmac| feed_aad(&header, bytes, |chunk| cmac.update(chunk)),
1362                packet.body(),
1363            )
1364        };
1365        let mut aad = std::vec::Vec::new();
1366        {
1367            let header = packet.header().unwrap();
1368            feed_aad(&header, packet.as_bytes(), |chunk| {
1369                aad.extend_from_slice(chunk)
1370            });
1371        }
1372        engine.seal_packet(&mut packet, &keys).unwrap();
1373
1374        let mut siv_key = [0u8; 64];
1375        siv_key[..32].copy_from_slice(&keys.k_mic);
1376        siv_key[32..].copy_from_slice(&keys.k_enc);
1377        let mut siv = Aes256Siv::new((&siv_key).into());
1378        let sealed = siv.encrypt([&aad], payload).unwrap();
1379        assert_eq!(sealed[..16], full_tag);
1380
1381        // The on-wire MIC is the prefix truncation of that tag.
1382        let header = PacketHeader::parse(packet.as_bytes()).unwrap();
1383        assert_eq!(packet.as_bytes()[header.mic_range.clone()], full_tag[..4]);
1384    }
1385
1386    /// Regression: with a MIC shorter than 16 bytes the CTR IV includes
1387    /// SECINFO bytes, and SECINFO precedes the options block — not the
1388    /// body, which the options end marker separates from it. The IV of
1389    /// a received packet must be built from the true SECINFO position
1390    /// or decryption diverges from sealing.
1391    #[cfg(feature = "software-crypto")]
1392    #[test]
1393    fn short_mic_encrypted_unicast_round_trip() {
1394        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1395        let keys = engine.derive_pairwise_keys(&SharedSecret([9u8; 32]));
1396        let mut buf = [0u8; 128];
1397        let mut packet = PacketBuilder::new(&mut buf)
1398            .unicast(NodeHint([0xC3, 0xD4, 0x25]))
1399            .source_hint(NodeHint([0xA1, 0xA1, 0xA1]))
1400            .frame_counter(7)
1401            .encrypted()
1402            .mic_size(MicSize::Mic8)
1403            .payload(b"short mic")
1404            .build()
1405            .unwrap();
1406        engine.seal_packet(&mut packet, &keys).unwrap();
1407        let header = PacketHeader::parse(packet.as_bytes()).unwrap();
1408        let mut wire = packet.as_bytes().to_vec();
1409        let range = engine.open_packet(&mut wire, &header, &keys).unwrap();
1410        assert_eq!(&wire[range], b"short mic");
1411    }
1412
1413    /// Regression companion to the above for blind unicast, where the
1414    /// concealed address block also sits between SECINFO and the body:
1415    /// address decryption and payload opening must both locate SECINFO
1416    /// correctly with a short MIC.
1417    #[cfg(feature = "software-crypto")]
1418    #[test]
1419    fn short_mic_blind_unicast_round_trip() {
1420        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1421        let channel_key = ChannelKey([0x5A; 32]);
1422        let channel_keys = engine.derive_channel_keys(&channel_key);
1423        let pairwise = engine.derive_pairwise_keys(&SharedSecret([9u8; 32]));
1424        let blind_keys = engine.derive_blind_keys(&pairwise, &channel_keys);
1425        let dst = NodeHint([0xC3, 0xD4, 0x25]);
1426        let src = NodeHint([0xA1, 0xA1, 0xA1]);
1427
1428        let mut buf = [0u8; 128];
1429        let mut packet = PacketBuilder::new(&mut buf)
1430            .blind_unicast(channel_keys.channel_id, dst)
1431            .source_hint(src)
1432            .frame_counter(7)
1433            .ack_requested()
1434            .encrypted()
1435            .mic_size(MicSize::Mic8)
1436            .payload(b"blind")
1437            .build()
1438            .unwrap();
1439        engine
1440            .seal_blind_packet(&mut packet, &blind_keys, &channel_keys)
1441            .unwrap();
1442
1443        let header = PacketHeader::parse(packet.as_bytes()).unwrap();
1444        let mut wire = packet.as_bytes().to_vec();
1445        let (decrypted_dst, decrypted_src) = engine
1446            .decrypt_blind_addr(&mut wire, &header, &channel_keys)
1447            .unwrap();
1448        assert_eq!(decrypted_dst, dst);
1449        assert_eq!(decrypted_src, SourceAddrRef::Hint(src));
1450        let range = engine.open_packet(&mut wire, &header, &blind_keys).unwrap();
1451        assert_eq!(&wire[range], b"blind");
1452    }
1453
1454    /// Verify compute_ack_trailer produces a deterministic 8-byte value and
1455    /// that its two halves are the public `ack_mic` (CMAC prefix) and the
1456    /// keyed `ack_tag`.
1457    #[cfg(feature = "software-crypto")]
1458    #[test]
1459    fn compute_ack_trailer_is_deterministic() {
1460        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1461        let key = hex_32(NIST_AES256_KEY);
1462        let tag = hex_16("28a7023f452e8f82bd4bf28d8c37c35c");
1463        let t1 = engine.compute_ack_trailer(&tag, &key);
1464        let t2 = engine.compute_ack_trailer(&tag, &key);
1465        assert_eq!(t1, t2);
1466        assert_eq!(t1.len(), 8);
1467        // ack_mic half is the first 4 bytes of the full tag (public).
1468        assert_eq!(t1[..4], tag[..4]);
1469        // ack_tag half is the first 4 bytes of AES-ECB(key, tag) (keyed).
1470        let cipher = SoftwareAes.new_cipher(&key);
1471        let mut block = tag;
1472        cipher.encrypt_block(&mut block);
1473        assert_eq!(t1[4..], block[..4]);
1474    }
1475
1476    /// Verify that compute_ack_trailer with different tags produces different
1477    /// trailers in both the mic and tag halves.
1478    #[cfg(feature = "software-crypto")]
1479    #[test]
1480    fn compute_ack_trailer_differs_for_different_tags() {
1481        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1482        let key = hex_32(NIST_AES256_KEY);
1483        let tag_a = hex_16("28a7023f452e8f82bd4bf28d8c37c35c");
1484        let tag_b = hex_16("aaf3d8f1de5640c232f5b169b9c911e6");
1485        let t_a = engine.compute_ack_trailer(&tag_a, &key);
1486        let t_b = engine.compute_ack_trailer(&tag_b, &key);
1487        assert_ne!(t_a, t_b);
1488        assert_ne!(t_a, [0u8; 8]);
1489        assert_ne!(t_b, [0u8; 8]);
1490    }
1491
1492    #[cfg(feature = "software-crypto")]
1493    #[test]
1494    fn encrypted_multicast_round_trip_preserves_source_prefix() {
1495        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1496        let channel_key = ChannelKey([0x5Au8; 32]);
1497        let derived = engine.derive_channel_keys(&channel_key);
1498        let src = PublicKey([0xA1; 32]);
1499        let mut buf = [0u8; 160];
1500        let mut packet = PacketBuilder::new(&mut buf)
1501            .multicast(derived.channel_id)
1502            .source_hint(src.hint())
1503            .frame_counter(5)
1504            .encrypted()
1505            .mic_size(MicSize::Mic16)
1506            .payload(b"hello")
1507            .build()
1508            .unwrap();
1509
1510        let multicast_keys = PairwiseKeys {
1511            k_enc: derived.k_enc,
1512            k_mic: derived.k_mic,
1513        };
1514        engine.seal_packet(&mut packet, &multicast_keys).unwrap();
1515        let header = PacketHeader::parse(packet.as_bytes()).unwrap();
1516        let mut wire = packet.as_bytes().to_vec();
1517        let range = engine
1518            .open_packet(&mut wire, &header, &multicast_keys)
1519            .unwrap();
1520        assert_eq!(
1521            &wire[header.body_range.start..header.body_range.start + 3],
1522            &src.hint().0
1523        );
1524        assert_eq!(&wire[range], b"hello");
1525    }
1526
1527    #[cfg(feature = "software-crypto")]
1528    #[test]
1529    fn blind_unicast_round_trip_recovers_addresses_and_payload() {
1530        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1531        let shared = SharedSecret([0x33u8; 32]);
1532        let pairwise = engine.derive_pairwise_keys(&shared);
1533        let channel_key = ChannelKey([0x5Au8; 32]);
1534        let channel = engine.derive_channel_keys(&channel_key);
1535        let blind_keys = engine.derive_blind_keys(&pairwise, &channel);
1536        let src = PublicKey([
1537            0xA1, 0xB2, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E,
1538            0x0F, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C,
1539            0x1D, 0x1E, 0x1F, 0x20,
1540        ]);
1541        let dst = NodeHint([0xC3, 0xD4, 0x25]);
1542        let mut buf = [0u8; 160];
1543        let mut packet = PacketBuilder::new(&mut buf)
1544            .blind_unicast(channel.channel_id, dst)
1545            .source_full(&src)
1546            .frame_counter(5)
1547            .mic_size(MicSize::Mic16)
1548            .payload(b"hello")
1549            .build()
1550            .unwrap();
1551
1552        engine
1553            .seal_blind_packet(&mut packet, &blind_keys, &channel)
1554            .unwrap();
1555        let header = PacketHeader::parse(packet.as_bytes()).unwrap();
1556        let mut wire = packet.as_bytes().to_vec();
1557        let (decoded_dst, decoded_src) = engine
1558            .decrypt_blind_addr(&mut wire, &header, &channel)
1559            .unwrap();
1560        let range = engine.open_packet(&mut wire, &header, &blind_keys).unwrap();
1561
1562        assert_eq!(decoded_dst, dst);
1563        assert_eq!(
1564            decoded_src,
1565            SourceAddrRef::FullKeyAt {
1566                offset: header.body_range.start - 32
1567            }
1568        );
1569        assert_eq!(
1570            &wire[header.body_range.start - 32..header.body_range.start],
1571            &src.0
1572        );
1573        assert_eq!(&wire[range], b"hello");
1574    }
1575
1576    #[cfg(feature = "software-crypto")]
1577    #[test]
1578    fn software_identity_agreement_is_symmetric() {
1579        let alice = SoftwareIdentity::from_secret_bytes(&[
1580            0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x1E,
1581            0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2A, 0x2B, 0x2C,
1582            0x2D, 0x2E, 0x2F, 0x30,
1583        ]);
1584        let bob = SoftwareIdentity::from_secret_bytes(&[
1585            0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3A, 0x3B, 0x3C, 0x3D, 0x3E,
1586            0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, 0x46, 0x47, 0x48, 0x49, 0x4A, 0x4B, 0x4C,
1587            0x4D, 0x4E, 0x4F, 0x50,
1588        ]);
1589
1590        let ab = alice.shared_secret_with(bob.public_key()).unwrap();
1591        let ba = bob.shared_secret_with(alice.public_key()).unwrap();
1592        assert_eq!(ab.0, ba.0);
1593    }
1594
1595    #[cfg(feature = "software-crypto")]
1596    #[test]
1597    fn channel_tint_extends_the_channel_identifier() {
1598        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1599        let key = ChannelKey([0x5Au8; 32]);
1600        let tint = engine.derive_channel_tint(&key);
1601        // The presentation color is the identifier derivation run one byte
1602        // longer, so a caller holding either can recognize the other.
1603        assert_eq!(&tint[..2], &engine.derive_channel_id(&key).0[..]);
1604        assert_eq!(tint, engine.derive_channel_tint(&key));
1605        assert_ne!(tint, engine.derive_channel_tint(&ChannelKey([0x5Bu8; 32])));
1606    }
1607
1608    #[cfg(feature = "software-crypto")]
1609    #[test]
1610    fn channel_tag_extends_the_channel_identifier_and_tint() {
1611        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1612        let key = ChannelKey([0x5Au8; 32]);
1613        let tag = engine.derive_channel_tag(&key);
1614        // One derivation, three lengths: whoever holds the tag can recover the
1615        // identifier and the tint without the key.
1616        assert_eq!(tag.channel_id(), engine.derive_channel_id(&key));
1617        assert_eq!(&tag.0[..3], &engine.derive_channel_tint(&key)[..]);
1618        assert_eq!(tag, engine.derive_channel_tag(&key));
1619        assert_ne!(tag, engine.derive_channel_tag(&ChannelKey([0x5Bu8; 32])));
1620    }
1621
1622    // ── derive_named_channel_key ──────────────────────────────────────────────
1623
1624    #[cfg(feature = "software-crypto")]
1625    #[test]
1626    fn derive_named_channel_key_is_deterministic() {
1627        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1628        let k1 = engine.derive_named_channel_key("lobby").unwrap();
1629        let k2 = engine.derive_named_channel_key("lobby").unwrap();
1630        let k3 = engine.derive_named_channel_key("other").unwrap();
1631        assert_eq!(k1, k2);
1632        assert_ne!(k1, k3);
1633        assert_ne!(k1, ChannelKey([0u8; 32]));
1634    }
1635
1636    #[cfg(feature = "software-crypto")]
1637    #[test]
1638    fn derive_named_channel_key_folds_ascii_case() {
1639        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1640        let lower = engine.derive_named_channel_key("public").unwrap();
1641        assert_eq!(engine.derive_named_channel_key("Public").unwrap(), lower);
1642        assert_eq!(engine.derive_named_channel_key("PUBLIC").unwrap(), lower);
1643        // Digits and symbols are untouched by the fold.
1644        assert_eq!(
1645            engine.derive_named_channel_key("Mesh-42!").unwrap(),
1646            engine.derive_named_channel_key("mesh-42!").unwrap()
1647        );
1648    }
1649
1650    #[cfg(feature = "software-crypto")]
1651    #[test]
1652    fn derive_named_channel_key_rejects_invalid_names() {
1653        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1654        assert_eq!(
1655            engine.derive_named_channel_key("caf\u{e9}"),
1656            Err(ChannelNameError::NotAscii)
1657        );
1658        let too_long = "x".repeat(MAX_CHANNEL_NAME_LEN + 1);
1659        assert_eq!(
1660            engine.derive_named_channel_key(&too_long),
1661            Err(ChannelNameError::TooLong)
1662        );
1663        // Exactly at the limit is fine.
1664        let at_limit = "x".repeat(MAX_CHANNEL_NAME_LEN);
1665        assert!(engine.derive_named_channel_key(&at_limit).is_ok());
1666    }
1667
1668    // ── SoftwareIdentity::generate ────────────────────────────────────────────
1669
1670    #[cfg(feature = "software-crypto")]
1671    #[test]
1672    fn software_identity_generate_produces_valid_key() {
1673        use rand_core::{CryptoRng, RngCore};
1674
1675        // Minimal deterministic RNG for testing — counter-based, not cryptographically
1676        // strong, but sufficient to exercise the generate() code path.
1677        struct CounterRng(u64);
1678        impl RngCore for CounterRng {
1679            fn next_u32(&mut self) -> u32 {
1680                self.next_u64() as u32
1681            }
1682            fn next_u64(&mut self) -> u64 {
1683                self.0 = self.0.wrapping_add(1);
1684                self.0
1685            }
1686            fn fill_bytes(&mut self, dest: &mut [u8]) {
1687                for chunk in dest.chunks_mut(8) {
1688                    let bytes = self.next_u64().to_le_bytes();
1689                    chunk.copy_from_slice(&bytes[..chunk.len()]);
1690                }
1691            }
1692            fn try_fill_bytes(&mut self, dest: &mut [u8]) -> Result<(), rand_core::Error> {
1693                self.fill_bytes(dest);
1694                Ok(())
1695            }
1696        }
1697        impl CryptoRng for CounterRng {}
1698
1699        let mut rng1 = CounterRng(0);
1700        let mut rng2 = CounterRng(1000);
1701        let id1 = SoftwareIdentity::generate(&mut rng1);
1702        let id2 = SoftwareIdentity::generate(&mut rng2);
1703        assert_ne!(id1.public_key(), id2.public_key());
1704        let ss1 = id1.shared_secret_with(id2.public_key()).unwrap();
1705        let ss2 = id2.shared_secret_with(id1.public_key()).unwrap();
1706        assert_eq!(ss1.0, ss2.0);
1707    }
1708
1709    // ── seal_blind_packet wrong packet type ───────────────────────────────────
1710
1711    #[cfg(feature = "software-crypto")]
1712    #[test]
1713    fn seal_blind_packet_rejects_non_blind_packet() {
1714        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1715        let shared = SharedSecret([0x11u8; 32]);
1716        let pairwise = engine.derive_pairwise_keys(&shared);
1717        let channel_key = ChannelKey([0x5Au8; 32]);
1718        let channel = engine.derive_channel_keys(&channel_key);
1719        let blind_keys = engine.derive_blind_keys(&pairwise, &channel);
1720        let src = PublicKey([0xA1u8; 32]);
1721        let dst = NodeHint([0xC3, 0xD4, 0x25]);
1722        let mut buf = [0u8; 128];
1723        // Build a regular unicast packet (not a blind unicast).
1724        let mut packet = PacketBuilder::new(&mut buf)
1725            .unicast(dst)
1726            .source_full(&src)
1727            .frame_counter(1)
1728            .encrypted()
1729            .mic_size(MicSize::Mic16)
1730            .payload(b"test")
1731            .build()
1732            .unwrap();
1733        assert!(matches!(
1734            engine.seal_blind_packet(&mut packet, &blind_keys, &channel),
1735            Err(CryptoError::InvalidPacket)
1736        ));
1737    }
1738
1739    // ── open_packet authentication failure ────────────────────────────────────
1740
1741    #[cfg(feature = "software-crypto")]
1742    #[test]
1743    fn open_packet_fails_on_tampered_ciphertext() {
1744        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1745        let keys = engine.derive_pairwise_keys(&SharedSecret([9u8; 32]));
1746        let src = PublicKey([0xA1; 32]);
1747        let dst = NodeHint([0xC3, 0xD4, 0x25]);
1748        let mut buf = [0u8; 128];
1749        let mut packet = PacketBuilder::new(&mut buf)
1750            .unicast(dst)
1751            .source_full(&src)
1752            .frame_counter(1)
1753            .encrypted()
1754            .mic_size(MicSize::Mic16)
1755            .payload(b"hello")
1756            .build()
1757            .unwrap();
1758        engine.seal_packet(&mut packet, &keys).unwrap();
1759
1760        // Flip a bit in the sealed packet body.
1761        let mut wire = packet.as_bytes().to_vec();
1762        let header = PacketHeader::parse(&wire).unwrap();
1763        wire[header.body_range.start] ^= 0x01;
1764
1765        assert!(matches!(
1766            engine.open_packet(&mut wire, &header, &keys),
1767            Err(CryptoError::AuthenticationFailed)
1768        ));
1769    }
1770
1771    // ── decrypt_blind_addr: encrypted hint source (len == 3) ─────────────────
1772
1773    #[cfg(feature = "software-crypto")]
1774    #[test]
1775    fn decrypt_blind_addr_encrypted_hint_source() {
1776        let engine = SoftwareCryptoEngine::new(SoftwareAes, SoftwareSha256);
1777        let shared = SharedSecret([0x33u8; 32]);
1778        let pairwise = engine.derive_pairwise_keys(&shared);
1779        let channel_key = ChannelKey([0x5Au8; 32]);
1780        let channel = engine.derive_channel_keys(&channel_key);
1781        let blind_keys = engine.derive_blind_keys(&pairwise, &channel);
1782        let src = PublicKey([0xA1u8; 32]);
1783        let dst = NodeHint([0xC3, 0xD4, 0x25]);
1784        let mut buf = [0u8; 128];
1785        // Use source_hint → produces Encrypted { len: 3 } in blind unicast.
1786        let mut packet = PacketBuilder::new(&mut buf)
1787            .blind_unicast(channel.channel_id, dst)
1788            .source_hint(src.hint())
1789            .frame_counter(5)
1790            .encrypted()
1791            .mic_size(MicSize::Mic16)
1792            .payload(b"hi")
1793            .build()
1794            .unwrap();
1795        engine
1796            .seal_blind_packet(&mut packet, &blind_keys, &channel)
1797            .unwrap();
1798        let mut wire = packet.as_bytes().to_vec();
1799        let header = PacketHeader::parse(&wire).unwrap();
1800        let (decoded_dst, decoded_src) = engine
1801            .decrypt_blind_addr(&mut wire, &header, &channel)
1802            .unwrap();
1803        assert_eq!(decoded_dst, dst);
1804        assert_eq!(decoded_src, SourceAddrRef::Hint(src.hint()));
1805    }
1806
1807    // ── SoftwareAes::decrypt_block ────────────────────────────────────────────
1808
1809    #[cfg(feature = "software-crypto")]
1810    #[test]
1811    fn aes_decrypt_block_inverts_encrypt_block() {
1812        let key = hex_32(NIST_AES256_KEY);
1813        let cipher = SoftwareAes.new_cipher(&key);
1814        let original = hex_16("6bc1bee22e409f96e93d7e117393172a");
1815        let mut block = original;
1816        cipher.encrypt_block(&mut block);
1817        assert_ne!(block, original);
1818        cipher.decrypt_block(&mut block);
1819        assert_eq!(block, original);
1820    }
1821
1822    // ── NodeIdentity trait: sign and agree ────────────────────────────────────
1823
1824    #[cfg(feature = "software-crypto")]
1825    #[test]
1826    fn sign_and_agree_via_trait() {
1827        use core::future::Future;
1828        use core::pin::pin;
1829        use core::task::{Context, Poll, RawWaker, RawWakerVTable, Waker};
1830
1831        fn block_on<F: Future>(future: F) -> F::Output {
1832            let raw = RawWaker::new(
1833                core::ptr::null(),
1834                &RawWakerVTable::new(|_| panic!(), |_| {}, |_| {}, |_| {}),
1835            );
1836            let waker = unsafe { Waker::from_raw(raw) };
1837            let mut ctx = Context::from_waker(&waker);
1838            match pin!(future).poll(&mut ctx) {
1839                Poll::Ready(v) => v,
1840                Poll::Pending => panic!("future unexpectedly pending"),
1841            }
1842        }
1843
1844        let alice = SoftwareIdentity::from_secret_bytes(&[0x11u8; 32]);
1845        let bob = SoftwareIdentity::from_secret_bytes(&[0x22u8; 32]);
1846
1847        let sig = block_on(alice.sign(b"test message")).unwrap();
1848        assert_eq!(sig.len(), 64);
1849
1850        let shared = block_on(alice.agree(bob.public_key())).unwrap();
1851        assert_ne!(shared.0, [0u8; 32]);
1852    }
1853
1854    // ── NodeIdentity::hint default impl ───────────────────────────────────────
1855
1856    #[cfg(feature = "software-crypto")]
1857    #[test]
1858    fn node_identity_hint_matches_public_key_hint() {
1859        let id = SoftwareIdentity::from_secret_bytes(&[0x11u8; 32]);
1860        // Calls the NodeIdentity::hint() default impl, which derives from public_key().
1861        assert_eq!(NodeIdentity::hint(&id), id.public_key().hint());
1862    }
1863
1864    // ── uppercase hex in decode_hex ───────────────────────────────────────────
1865
1866    #[test]
1867    fn hex_vec_accepts_uppercase() {
1868        assert_eq!(hex_vec("DEADBEEF"), hex_vec("deadbeef"));
1869        assert_eq!(hex_vec("0A1B2C3D"), hex_vec("0a1b2c3d"));
1870    }
1871
1872    fn hex_vec(input: &str) -> std::vec::Vec<u8> {
1873        assert_eq!(input.len() % 2, 0);
1874        let mut out = std::vec::Vec::with_capacity(input.len() / 2);
1875        let bytes = input.as_bytes();
1876        for index in (0..bytes.len()).step_by(2) {
1877            out.push((decode_hex(bytes[index]) << 4) | decode_hex(bytes[index + 1]));
1878        }
1879        out
1880    }
1881
1882    fn hex_16(input: &str) -> [u8; 16] {
1883        let bytes = hex_vec(input);
1884        let mut out = [0u8; 16];
1885        out.copy_from_slice(&bytes);
1886        out
1887    }
1888
1889    #[cfg(feature = "software-crypto")]
1890    fn hex_32(input: &str) -> [u8; 32] {
1891        let bytes = hex_vec(input);
1892        let mut out = [0u8; 32];
1893        out.copy_from_slice(&bytes);
1894        out
1895    }
1896
1897    fn decode_hex(byte: u8) -> u8 {
1898        match byte {
1899            b'0'..=b'9' => byte - b'0',
1900            b'a'..=b'f' => byte - b'a' + 10,
1901            b'A'..=b'F' => byte - b'A' + 10,
1902            _ => panic!("invalid hex"),
1903        }
1904    }
1905
1906    #[cfg(feature = "software-crypto")]
1907    #[test]
1908    fn is_valid_ed25519_public_key_accepts_real_key() {
1909        // Any verifying key produced by a SigningKey is by construction a
1910        // valid Ed25519 point — the validator must accept it.
1911        let identity = SoftwareIdentity::from_secret_bytes(&[0x11; 32]);
1912        assert!(is_valid_ed25519_public_key(identity.public_key()));
1913    }
1914
1915    #[cfg(feature = "software-crypto")]
1916    #[test]
1917    fn is_valid_ed25519_public_key_rejects_non_curve_bytes() {
1918        // Y = 2 (little-endian) is not on the Ed25519 curve: the
1919        // recovered x^2 from the curve equation has no square root in
1920        // GF(2^255 - 19). Confirm the validator rejects it — this is
1921        // the exact failure mode a user typo trips when they paste a
1922        // mistyped hex pubkey on the CLI.
1923        let mut bytes = [0u8; 32];
1924        bytes[0] = 2;
1925        let bogus = PublicKey(bytes);
1926        assert!(!is_valid_ed25519_public_key(&bogus));
1927    }
1928
1929    #[cfg(feature = "software-crypto")]
1930    #[test]
1931    fn is_valid_ed25519_public_key_handles_all_zero() {
1932        // The all-zero compressed Y value decompresses to the curve
1933        // identity point. Whatever the library's actual behavior is,
1934        // pin it so a curve25519-dalek update can't silently flip the
1935        // contract. (Today: accepted as a valid point.)
1936        let zero = PublicKey([0u8; 32]);
1937        assert!(is_valid_ed25519_public_key(&zero));
1938    }
1939}