Skip to main content

zerodds_security_pki/
psk.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Builtin Pre-Shared-Key Authentication-Plugin (Spec §10.7).
5//!
6//! zerodds-lint: allow no_dyn_in_safe
7//! (`SharedSecretProvider` ist Plugin-SPI; `&dyn`-Test-Bridge demonstriert
8//! Substitution-Pfad zum Crypto-Plugin.)
9//!
10//! Spec-Class-Id `"DDS:Auth:PSK:1.2"`. Alternative zum X.509-PKI-Pfad
11//! aus `crate::plugin` — die Identitaet wird ueber einen pre-shared
12//! symmetrischen Schluessel (typisch 256 bit) statt ueber einen
13//! Cert-Chain etabliert. Anwendungsfall: Embedded-Systeme ohne X.509-
14//! Toolchain (Industrial / Defense / Mesh-Networks).
15//!
16//! # Wire-Layout (Spec §10.7.2)
17//!
18//! Drei Tokens, alle als `DataHolder` auf der Wire:
19//!
20//! | Token  | class_id                       | Properties / Binary-Properties |
21//! |--------|--------------------------------|--------------------------------|
22//! | Req    | `DDS:Auth:PSK:1.2+AuthReq`     | `psk.id`, `challenge1`, `c.kagree_algo="PSK"` |
23//! | Reply  | `DDS:Auth:PSK:1.2+AuthReply`   | `psk.id`, `challenge1`, `challenge2`, `hmac` |
24//! | Final  | `DDS:Auth:PSK:1.2+AuthFinal`   | `challenge1`, `challenge2`, `hmac` |
25//!
26//! Der HMAC ist `HMAC-SHA256(pre_shared_key, length_prefixed(psk.id ||
27//! challenge1 || challenge2))`. Der `SharedSecret`-Output ist 32 byte
28//! HKDF-SHA256(pre_shared_key, salt=challenge1||challenge2,
29//! info="DDS-Security-1.2-PSK").
30//!
31//! # Spec-Items, die hier nicht abgedeckt sind
32//!
33//! * Token-spezifische `properties` aus Spec §10.7 Tab.61 — die Spec
34//!   listet ein paar optionale Properties (z.B. `c.id` als Identitaet,
35//!   wir bilden das auf `psk.id` ab). Wir verfolgen den Cyclone-DDS-
36//!   Konventions-Pfad statt der Spec-Letterung.
37//! * Cross-Vendor Live-Interop ist ungetestet — die Cyclone-PSK-Suite
38//!   ist nicht offen verfuegbar.
39
40use alloc::collections::{BTreeMap, BTreeSet};
41use alloc::string::{String, ToString};
42use alloc::vec::Vec;
43use core::sync::atomic::{AtomicU64, Ordering};
44
45use ring::hkdf;
46use ring::hmac;
47use ring::rand::{SecureRandom, SystemRandom};
48use zerodds_security::authentication::{
49    AuthenticationPlugin, HandshakeHandle, HandshakeStepOutcome, IdentityHandle,
50    SharedSecretHandle, SharedSecretProvider,
51};
52use zerodds_security::error::{SecurityError, SecurityErrorKind, SecurityResult};
53use zerodds_security::properties::PropertyList;
54use zerodds_security::token::DataHolder;
55
56/// Spec-konforme Class-Id-Strings (§10.7).
57pub mod class_id {
58    /// Plugin-Class-Id der PSK-Authentication.
59    pub const PSK: &str = "DDS:Auth:PSK:1.2";
60    /// `HandshakeRequestMessageToken` PSK.
61    pub const REQUEST: &str = "DDS:Auth:PSK:1.2+AuthReq";
62    /// `HandshakeReplyMessageToken` PSK.
63    pub const REPLY: &str = "DDS:Auth:PSK:1.2+AuthReply";
64    /// `HandshakeFinalMessageToken` PSK.
65    pub const FINAL: &str = "DDS:Auth:PSK:1.2+AuthFinal";
66}
67
68/// Property-Keys im Handshake-Token (Spec §10.7.2).
69pub mod prop {
70    /// PSK-Identitaet (UTF-8-String).
71    pub const PSK_ID: &str = "psk.id";
72    /// Initiator-Challenge (32 byte binary).
73    pub const CHALLENGE1: &str = "challenge1";
74    /// Replier-Challenge (32 byte binary).
75    pub const CHALLENGE2: &str = "challenge2";
76    /// HMAC-SHA256-Tag (32 byte binary).
77    pub const HMAC: &str = "hmac";
78    /// Key-Agreement-Algorithmus (immer `"PSK"` fuer dieses Plugin).
79    pub const KAGREE_ALGO: &str = "c.kagree_algo";
80}
81
82/// PropertyList-Key fuer den lokalen PSK-Identifier.
83pub const PROP_PSK_ID: &str = "dds.psk.identity_id";
84/// PropertyList-Key fuer den lokalen PSK-Material (hex-encoded).
85pub const PROP_PSK_KEY_HEX: &str = "dds.psk.pre_shared_key_hex";
86
87/// Replay-Cache pro lokaler Identity (DoS-Cap analog zu PKI).
88const REPLAY_CACHE_CAP: usize = 1024;
89
90/// HKDF-Info-String fuer SharedSecret-Derivation. Spec-Domain-
91/// Separator (§10.7.3).
92pub const HKDF_INFO_SHARED_SECRET: &[u8] = b"DDS-Security-1.2-PSK";
93
94/// Builtin PSK-Authentication-Plugin (Spec §10.7).
95pub struct PskAuthenticationPlugin {
96    next_handle: AtomicU64,
97    /// Konfigurierte PSKs: Identity-String → Pre-Shared-Key-Bytes.
98    psks: BTreeMap<String, Vec<u8>>,
99    /// Lokal-validierte Identitaeten: Handle → Identity-String.
100    identities: BTreeMap<IdentityHandle, String>,
101    /// Initiator-State zwischen Request und Reply.
102    pending_initiator: BTreeMap<HandshakeHandle, InitiatorState>,
103    /// Replier-State zwischen Reply und Final.
104    pending_replier: BTreeMap<HandshakeHandle, ReplierState>,
105    /// Abgeschlossene Handshakes → SharedSecret-Handle.
106    handshake_to_secret: BTreeMap<HandshakeHandle, SharedSecretHandle>,
107    /// Materialisierte SharedSecrets (32 byte HKDF-Output).
108    secrets: BTreeMap<SharedSecretHandle, Vec<u8>>,
109    /// Replay-Cache: pro lokaler Identity die bereits gesehenen
110    /// `challenge1`-Werte (Replier-Sicht).
111    replay_cache: BTreeMap<IdentityHandle, BTreeSet<[u8; 32]>>,
112    /// FIFO-Order der replay-cache Entries fuer Cap-Eviction.
113    replay_order: BTreeMap<IdentityHandle, Vec<[u8; 32]>>,
114}
115
116struct InitiatorState {
117    local: IdentityHandle,
118    psk_id: String,
119    challenge1: [u8; 32],
120}
121
122struct ReplierState {
123    local: IdentityHandle,
124    psk_id: String,
125    challenge1: [u8; 32],
126    challenge2: [u8; 32],
127    secret_handle: SharedSecretHandle,
128}
129
130impl Default for PskAuthenticationPlugin {
131    fn default() -> Self {
132        Self::new()
133    }
134}
135
136impl PskAuthenticationPlugin {
137    /// Konstruktor.
138    #[must_use]
139    pub fn new() -> Self {
140        Self {
141            next_handle: AtomicU64::new(0),
142            psks: BTreeMap::new(),
143            identities: BTreeMap::new(),
144            pending_initiator: BTreeMap::new(),
145            pending_replier: BTreeMap::new(),
146            handshake_to_secret: BTreeMap::new(),
147            secrets: BTreeMap::new(),
148            replay_cache: BTreeMap::new(),
149            replay_order: BTreeMap::new(),
150        }
151    }
152
153    fn next_id(&self) -> u64 {
154        self.next_handle.fetch_add(1, Ordering::Relaxed) + 1
155    }
156
157    /// Registriert einen Pre-Shared-Key fuer eine Identity. Replace-
158    /// Semantik: zweite Registrierung mit gleicher ID ueberschreibt.
159    ///
160    /// # Errors
161    /// `BadArgument` wenn `id` leer oder `key` leer ist.
162    pub fn register_psk(&mut self, id: String, key: Vec<u8>) -> SecurityResult<()> {
163        if id.is_empty() {
164            return Err(SecurityError::new(
165                SecurityErrorKind::BadArgument,
166                "psk: identity-id leer",
167            ));
168        }
169        if key.is_empty() {
170            return Err(SecurityError::new(
171                SecurityErrorKind::BadArgument,
172                "psk: pre-shared-key leer",
173            ));
174        }
175        self.psks.insert(id, key);
176        Ok(())
177    }
178
179    /// Validiert eine lokale PSK-Identity und liefert einen Handle.
180    ///
181    /// # Errors
182    /// `BadArgument` wenn `identity_id` nicht registriert ist.
183    pub fn validate_local_psk_identity(
184        &mut self,
185        identity_id: &str,
186    ) -> SecurityResult<IdentityHandle> {
187        if !self.psks.contains_key(identity_id) {
188            return Err(SecurityError::new(
189                SecurityErrorKind::BadArgument,
190                alloc::format!("psk: unbekannte identity-id '{identity_id}'"),
191            ));
192        }
193        let handle = IdentityHandle(self.next_id());
194        self.identities.insert(handle, identity_id.to_string());
195        Ok(handle)
196    }
197
198    /// Validiert eine Remote-PSK-Identity (aus dem propagierten
199    /// IdentityToken). Pruefung: die behauptete `psk.id` muss in
200    /// unserer lokalen PSK-Map vorhanden sein.
201    ///
202    /// # Errors
203    /// `BadArgument` wenn der Token-Format-Fehler hat oder die
204    /// `psk.id` nicht in der lokalen Map ist.
205    pub fn validate_remote_psk_identity(
206        &mut self,
207        remote_token: &[u8],
208    ) -> SecurityResult<IdentityHandle> {
209        let dh = DataHolder::from_cdr_le(remote_token)?;
210        if dh.class_id != class_id::PSK {
211            return Err(SecurityError::new(
212                SecurityErrorKind::AuthenticationFailed,
213                alloc::format!(
214                    "psk: remote-IdentityToken hat falsche class_id '{}'",
215                    dh.class_id
216                ),
217            ));
218        }
219        let id = dh.property(PROP_PSK_ID).ok_or_else(|| {
220            SecurityError::new(
221                SecurityErrorKind::AuthenticationFailed,
222                "psk: IdentityToken ohne psk.id",
223            )
224        })?;
225        if !self.psks.contains_key(id) {
226            return Err(SecurityError::new(
227                SecurityErrorKind::AuthenticationFailed,
228                alloc::format!("psk: remote psk.id '{id}' nicht im lokalen Trust-Store"),
229            ));
230        }
231        let handle = IdentityHandle(self.next_id());
232        self.identities.insert(handle, id.to_string());
233        Ok(handle)
234    }
235
236    /// Erzeugt das Wire-IdentityToken (`DDS:Auth:PSK:1.2`) fuer eine
237    /// lokale Identity.
238    ///
239    /// # Errors
240    /// `BadArgument` wenn der Handle unbekannt ist.
241    pub fn build_identity_token(&self, local: IdentityHandle) -> SecurityResult<Vec<u8>> {
242        let id = self.identities.get(&local).ok_or_else(|| {
243            SecurityError::new(SecurityErrorKind::BadArgument, "psk: unbekannter Handle")
244        })?;
245        let dh = DataHolder::new(class_id::PSK).with_property(PROP_PSK_ID, id.clone());
246        Ok(dh.to_cdr_le())
247    }
248
249    /// Liefert das rohe SharedSecret (32 byte) — primaer fuer Tests.
250    #[must_use]
251    pub fn secret_bytes(&self, handle: SharedSecretHandle) -> Option<&[u8]> {
252        self.secrets.get(&handle).map(Vec::as_slice)
253    }
254
255    fn store_secret(&mut self, bytes: Vec<u8>) -> SharedSecretHandle {
256        let handle = SharedSecretHandle(self.next_id());
257        self.secrets.insert(handle, bytes);
258        handle
259    }
260
261    fn record_challenge(&mut self, local: IdentityHandle, c: [u8; 32]) -> SecurityResult<()> {
262        let cache = self.replay_cache.entry(local).or_default();
263        if cache.contains(&c) {
264            return Err(SecurityError::new(
265                SecurityErrorKind::AuthenticationFailed,
266                "psk: replayed challenge1 detected",
267            ));
268        }
269        cache.insert(c);
270        let order = self.replay_order.entry(local).or_default();
271        order.push(c);
272        if order.len() > REPLAY_CACHE_CAP {
273            let dropped = order.remove(0);
274            cache.remove(&dropped);
275        }
276        Ok(())
277    }
278
279    fn lookup_psk(&self, id: &str) -> SecurityResult<&[u8]> {
280        self.psks.get(id).map(Vec::as_slice).ok_or_else(|| {
281            SecurityError::new(
282                SecurityErrorKind::AuthenticationFailed,
283                alloc::format!("psk: unbekannte identity-id '{id}'"),
284            )
285        })
286    }
287}
288
289impl SharedSecretProvider for PskAuthenticationPlugin {
290    fn get_shared_secret(&self, handle: SharedSecretHandle) -> Option<Vec<u8>> {
291        self.secrets.get(&handle).cloned()
292    }
293}
294
295/// Zufaellige 32-byte Challenge.
296fn random_challenge() -> SecurityResult<[u8; 32]> {
297    let rng = SystemRandom::new();
298    let mut buf = [0u8; 32];
299    rng.fill(&mut buf).map_err(|_| {
300        SecurityError::new(
301            SecurityErrorKind::CryptoFailed,
302            "psk: SystemRandom not available",
303        )
304    })?;
305    Ok(buf)
306}
307
308/// HMAC-Input nach Spec §10.7.2. Length-prefixed Concatenation
309/// (`u32-LE len || bytes`) verhindert Cross-Field-Tampering.
310fn hmac_input(psk_id: &str, ch1: &[u8; 32], ch2: &[u8; 32]) -> Vec<u8> {
311    let mut out = Vec::with_capacity(4 + psk_id.len() + 4 + 32 + 4 + 32);
312    out.extend_from_slice(&(psk_id.len() as u32).to_le_bytes());
313    out.extend_from_slice(psk_id.as_bytes());
314    out.extend_from_slice(&(32u32).to_le_bytes());
315    out.extend_from_slice(ch1);
316    out.extend_from_slice(&(32u32).to_le_bytes());
317    out.extend_from_slice(ch2);
318    out
319}
320
321fn hmac_sign(psk: &[u8], psk_id: &str, ch1: &[u8; 32], ch2: &[u8; 32]) -> [u8; 32] {
322    let key = hmac::Key::new(hmac::HMAC_SHA256, psk);
323    let tag = hmac::sign(&key, &hmac_input(psk_id, ch1, ch2));
324    let mut out = [0u8; 32];
325    out.copy_from_slice(tag.as_ref());
326    out
327}
328
329fn hmac_verify(
330    psk: &[u8],
331    psk_id: &str,
332    ch1: &[u8; 32],
333    ch2: &[u8; 32],
334    tag: &[u8],
335) -> SecurityResult<()> {
336    let key = hmac::Key::new(hmac::HMAC_SHA256, psk);
337    hmac::verify(&key, &hmac_input(psk_id, ch1, ch2), tag).map_err(|_| {
338        SecurityError::new(
339            SecurityErrorKind::AuthenticationFailed,
340            "psk: hmac verify failed",
341        )
342    })
343}
344
345/// Spec §10.7.3 — SharedSecret = HKDF-SHA256(psk, salt=ch1||ch2,
346/// info="DDS-Security-1.2-PSK"). Output: 32 byte.
347pub fn derive_psk_shared_secret(
348    psk: &[u8],
349    ch1: &[u8; 32],
350    ch2: &[u8; 32],
351) -> SecurityResult<[u8; 32]> {
352    let mut salt = [0u8; 64];
353    salt[..32].copy_from_slice(ch1);
354    salt[32..].copy_from_slice(ch2);
355    let salt_obj = hkdf::Salt::new(hkdf::HKDF_SHA256, &salt);
356    let prk = salt_obj.extract(psk);
357    let info = [HKDF_INFO_SHARED_SECRET];
358    let okm = prk.expand(&info, hkdf::HKDF_SHA256).map_err(|_| {
359        SecurityError::new(SecurityErrorKind::CryptoFailed, "psk: HKDF expand failed")
360    })?;
361    let mut out = [0u8; 32];
362    okm.fill(&mut out).map_err(|_| {
363        SecurityError::new(SecurityErrorKind::CryptoFailed, "psk: HKDF fill failed")
364    })?;
365    Ok(out)
366}
367
368fn read_32(dh: &DataHolder, name: &str) -> SecurityResult<[u8; 32]> {
369    let bytes = dh.binary_property(name).ok_or_else(|| {
370        SecurityError::new(
371            SecurityErrorKind::AuthenticationFailed,
372            alloc::format!("psk: missing binary property '{name}'"),
373        )
374    })?;
375    if bytes.len() != 32 {
376        return Err(SecurityError::new(
377            SecurityErrorKind::AuthenticationFailed,
378            alloc::format!("psk: '{name}' must be 32 bytes"),
379        ));
380    }
381    let mut out = [0u8; 32];
382    out.copy_from_slice(bytes);
383    Ok(out)
384}
385
386impl AuthenticationPlugin for PskAuthenticationPlugin {
387    fn validate_local_identity(
388        &mut self,
389        props: &PropertyList,
390        _participant_guid: [u8; 16],
391    ) -> SecurityResult<IdentityHandle> {
392        let id = props.get(PROP_PSK_ID).ok_or_else(|| {
393            SecurityError::new(
394                SecurityErrorKind::InvalidConfiguration,
395                "psk: fehlt dds.psk.identity_id",
396            )
397        })?;
398        // Optional: PSK-Material direkt aus Properties laden (hex).
399        if let Some(hex) = props.get(PROP_PSK_KEY_HEX) {
400            let bytes = hex_decode(hex)?;
401            self.register_psk(id.to_string(), bytes)?;
402        }
403        self.validate_local_psk_identity(id)
404    }
405
406    fn validate_remote_identity(
407        &mut self,
408        _local: IdentityHandle,
409        _remote_participant_guid: [u8; 16],
410        remote_auth_token: &[u8],
411    ) -> SecurityResult<IdentityHandle> {
412        self.validate_remote_psk_identity(remote_auth_token)
413    }
414
415    fn begin_handshake_request(
416        &mut self,
417        initiator: IdentityHandle,
418        _replier: IdentityHandle,
419    ) -> SecurityResult<(HandshakeHandle, HandshakeStepOutcome)> {
420        let psk_id = self
421            .identities
422            .get(&initiator)
423            .ok_or_else(|| {
424                SecurityError::new(
425                    SecurityErrorKind::BadArgument,
426                    "psk: unbekannter Initiator-IdentityHandle",
427                )
428            })?
429            .clone();
430        let challenge1 = random_challenge()?;
431
432        let token = DataHolder::new(class_id::REQUEST)
433            .with_property(prop::PSK_ID, psk_id.clone())
434            .with_property(prop::KAGREE_ALGO, "PSK")
435            .with_binary_property(prop::CHALLENGE1, challenge1.to_vec())
436            .to_cdr_le();
437
438        let handle = HandshakeHandle(self.next_id());
439        self.pending_initiator.insert(
440            handle,
441            InitiatorState {
442                local: initiator,
443                psk_id,
444                challenge1,
445            },
446        );
447        Ok((handle, HandshakeStepOutcome::SendMessage { token }))
448    }
449
450    fn begin_handshake_reply(
451        &mut self,
452        replier: IdentityHandle,
453        _initiator: IdentityHandle,
454        request_token: &[u8],
455    ) -> SecurityResult<(HandshakeHandle, HandshakeStepOutcome)> {
456        let dh = DataHolder::from_cdr_le(request_token)?;
457        if dh.class_id != class_id::REQUEST {
458            return Err(SecurityError::new(
459                SecurityErrorKind::AuthenticationFailed,
460                alloc::format!(
461                    "psk: reply expected request, got class_id '{}'",
462                    dh.class_id
463                ),
464            ));
465        }
466        let psk_id = dh
467            .property(prop::PSK_ID)
468            .ok_or_else(|| {
469                SecurityError::new(
470                    SecurityErrorKind::AuthenticationFailed,
471                    "psk: request missing psk.id",
472                )
473            })?
474            .to_string();
475        let challenge1 = read_32(&dh, prop::CHALLENGE1)?;
476
477        // Replay-Detection (Replier-Seite).
478        self.record_challenge(replier, challenge1)?;
479
480        // PSK-Lookup gegen lokalen Trust-Store.
481        let psk = self.lookup_psk(&psk_id)?.to_vec();
482
483        let challenge2 = random_challenge()?;
484        let hmac = hmac_sign(&psk, &psk_id, &challenge1, &challenge2);
485        let secret = derive_psk_shared_secret(&psk, &challenge1, &challenge2)?;
486
487        let token = DataHolder::new(class_id::REPLY)
488            .with_property(prop::PSK_ID, psk_id.clone())
489            .with_property(prop::KAGREE_ALGO, "PSK")
490            .with_binary_property(prop::CHALLENGE1, challenge1.to_vec())
491            .with_binary_property(prop::CHALLENGE2, challenge2.to_vec())
492            .with_binary_property(prop::HMAC, hmac.to_vec())
493            .to_cdr_le();
494
495        let secret_handle = self.store_secret(secret.to_vec());
496        let handle = HandshakeHandle(self.next_id());
497        self.handshake_to_secret.insert(handle, secret_handle);
498        self.pending_replier.insert(
499            handle,
500            ReplierState {
501                local: replier,
502                psk_id,
503                challenge1,
504                challenge2,
505                secret_handle,
506            },
507        );
508        Ok((handle, HandshakeStepOutcome::SendMessage { token }))
509    }
510
511    fn process_handshake(
512        &mut self,
513        handshake: HandshakeHandle,
514        token: &[u8],
515    ) -> SecurityResult<HandshakeStepOutcome> {
516        if self.pending_initiator.contains_key(&handshake) {
517            return self.process_reply_on_initiator(handshake, token);
518        }
519        if self.pending_replier.contains_key(&handshake) {
520            return self.process_final_on_replier(handshake, token);
521        }
522        Err(SecurityError::new(
523            SecurityErrorKind::BadArgument,
524            "psk: unbekannter HandshakeHandle",
525        ))
526    }
527
528    fn shared_secret(&self, handshake: HandshakeHandle) -> SecurityResult<SharedSecretHandle> {
529        self.handshake_to_secret
530            .get(&handshake)
531            .copied()
532            .ok_or_else(|| {
533                SecurityError::new(
534                    SecurityErrorKind::BadArgument,
535                    "psk: handshake-handle unbekannt oder noch nicht completed",
536                )
537            })
538    }
539
540    fn plugin_class_id(&self) -> &str {
541        class_id::PSK
542    }
543}
544
545impl PskAuthenticationPlugin {
546    fn process_reply_on_initiator(
547        &mut self,
548        handshake: HandshakeHandle,
549        token: &[u8],
550    ) -> SecurityResult<HandshakeStepOutcome> {
551        let dh = DataHolder::from_cdr_le(token)?;
552        if dh.class_id != class_id::REPLY {
553            return Err(SecurityError::new(
554                SecurityErrorKind::AuthenticationFailed,
555                alloc::format!("psk: process expected reply, got '{}'", dh.class_id),
556            ));
557        }
558        let st = self.pending_initiator.remove(&handshake).ok_or_else(|| {
559            SecurityError::new(SecurityErrorKind::BadArgument, "psk: initiator state gone")
560        })?;
561        let psk_id_in = dh
562            .property(prop::PSK_ID)
563            .ok_or_else(|| {
564                SecurityError::new(
565                    SecurityErrorKind::AuthenticationFailed,
566                    "psk: reply missing psk.id",
567                )
568            })?
569            .to_string();
570        if psk_id_in != st.psk_id {
571            return Err(SecurityError::new(
572                SecurityErrorKind::AuthenticationFailed,
573                "psk: psk.id echo mismatch in reply",
574            ));
575        }
576        let ch1 = read_32(&dh, prop::CHALLENGE1)?;
577        if ch1 != st.challenge1 {
578            return Err(SecurityError::new(
579                SecurityErrorKind::AuthenticationFailed,
580                "psk: challenge1 echo mismatch",
581            ));
582        }
583        let ch2 = read_32(&dh, prop::CHALLENGE2)?;
584        let hmac = dh.binary_property(prop::HMAC).ok_or_else(|| {
585            SecurityError::new(
586                SecurityErrorKind::AuthenticationFailed,
587                "psk: reply missing hmac",
588            )
589        })?;
590
591        let psk = self.lookup_psk(&st.psk_id)?.to_vec();
592        hmac_verify(&psk, &st.psk_id, &ch1, &ch2, hmac)?;
593
594        // Eigenes HMAC fuer Final-Token.
595        let final_hmac = hmac_sign(&psk, &st.psk_id, &ch1, &ch2);
596        let secret = derive_psk_shared_secret(&psk, &ch1, &ch2)?;
597        let secret_handle = self.store_secret(secret.to_vec());
598        self.handshake_to_secret.insert(handshake, secret_handle);
599
600        let final_token = DataHolder::new(class_id::FINAL)
601            .with_binary_property(prop::CHALLENGE1, ch1.to_vec())
602            .with_binary_property(prop::CHALLENGE2, ch2.to_vec())
603            .with_binary_property(prop::HMAC, final_hmac.to_vec())
604            .to_cdr_le();
605
606        let _ = st.local;
607        Ok(HandshakeStepOutcome::SendMessage { token: final_token })
608    }
609
610    fn process_final_on_replier(
611        &mut self,
612        handshake: HandshakeHandle,
613        token: &[u8],
614    ) -> SecurityResult<HandshakeStepOutcome> {
615        let dh = DataHolder::from_cdr_le(token)?;
616        if dh.class_id != class_id::FINAL {
617            return Err(SecurityError::new(
618                SecurityErrorKind::AuthenticationFailed,
619                alloc::format!("psk: process expected final, got '{}'", dh.class_id),
620            ));
621        }
622        let st = self.pending_replier.remove(&handshake).ok_or_else(|| {
623            SecurityError::new(SecurityErrorKind::BadArgument, "psk: replier state gone")
624        })?;
625        let ch1 = read_32(&dh, prop::CHALLENGE1)?;
626        let ch2 = read_32(&dh, prop::CHALLENGE2)?;
627        if ch1 != st.challenge1 || ch2 != st.challenge2 {
628            return Err(SecurityError::new(
629                SecurityErrorKind::AuthenticationFailed,
630                "psk: final challenge echo mismatch",
631            ));
632        }
633        let hmac = dh.binary_property(prop::HMAC).ok_or_else(|| {
634            SecurityError::new(
635                SecurityErrorKind::AuthenticationFailed,
636                "psk: final missing hmac",
637            )
638        })?;
639        let psk = self.lookup_psk(&st.psk_id)?.to_vec();
640        hmac_verify(&psk, &st.psk_id, &ch1, &ch2, hmac)?;
641
642        let _ = st.local;
643        Ok(HandshakeStepOutcome::Complete {
644            secret: st.secret_handle,
645        })
646    }
647}
648
649fn hex_decode(s: &str) -> SecurityResult<Vec<u8>> {
650    if s.len() % 2 != 0 {
651        return Err(SecurityError::new(
652            SecurityErrorKind::BadArgument,
653            "psk: hex string with odd length",
654        ));
655    }
656    let mut out = Vec::with_capacity(s.len() / 2);
657    let bytes = s.as_bytes();
658    for chunk in bytes.chunks(2) {
659        let hi = hex_nibble(chunk[0])?;
660        let lo = hex_nibble(chunk[1])?;
661        // Arithmetic form statt `(hi << 4) | lo`: bei nibble-Werten
662        // (0..=15) ueberlappen die Bits nicht, also identisch.
663        // mutation-detection-freundlich: `*` und `+` sind nicht
664        // aequivalent zueinander.
665        out.push(hi * 16 + lo);
666    }
667    Ok(out)
668}
669
670fn hex_nibble(c: u8) -> SecurityResult<u8> {
671    match c {
672        b'0'..=b'9' => Ok(c - b'0'),
673        b'a'..=b'f' => Ok(c - b'a' + 10),
674        b'A'..=b'F' => Ok(c - b'A' + 10),
675        _ => Err(SecurityError::new(
676            SecurityErrorKind::BadArgument,
677            "psk: invalid hex nibble",
678        )),
679    }
680}
681
682#[cfg(test)]
683#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
684mod tests {
685    use super::*;
686    use zerodds_security::properties::Property;
687
688    fn alice_bob_with_shared_psk() -> (
689        PskAuthenticationPlugin,
690        PskAuthenticationPlugin,
691        IdentityHandle,
692        IdentityHandle,
693    ) {
694        let psk = alloc::vec![0xA5u8; 32];
695        let mut alice = PskAuthenticationPlugin::new();
696        let mut bob = PskAuthenticationPlugin::new();
697        alice.register_psk("alice-bob".into(), psk.clone()).unwrap();
698        bob.register_psk("alice-bob".into(), psk).unwrap();
699        let alice_h = alice.validate_local_psk_identity("alice-bob").unwrap();
700        let bob_h = bob.validate_local_psk_identity("alice-bob").unwrap();
701        (alice, bob, alice_h, bob_h)
702    }
703
704    #[test]
705    fn plugin_class_id_matches_spec() {
706        let p = PskAuthenticationPlugin::new();
707        assert_eq!(p.plugin_class_id(), "DDS:Auth:PSK:1.2");
708    }
709
710    #[test]
711    fn token_class_ids_match_spec() {
712        assert_eq!(class_id::PSK, "DDS:Auth:PSK:1.2");
713        assert_eq!(class_id::REQUEST, "DDS:Auth:PSK:1.2+AuthReq");
714        assert_eq!(class_id::REPLY, "DDS:Auth:PSK:1.2+AuthReply");
715        assert_eq!(class_id::FINAL, "DDS:Auth:PSK:1.2+AuthFinal");
716    }
717
718    #[test]
719    fn register_psk_then_validate_local_happy_path() {
720        let mut p = PskAuthenticationPlugin::new();
721        p.register_psk("client-1".into(), alloc::vec![0x11; 32])
722            .unwrap();
723        let h = p.validate_local_psk_identity("client-1").unwrap();
724        assert!(h.0 >= 1);
725    }
726
727    #[test]
728    fn validate_local_unknown_id_rejected() {
729        let mut p = PskAuthenticationPlugin::new();
730        let err = p.validate_local_psk_identity("ghost").unwrap_err();
731        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
732    }
733
734    #[test]
735    fn register_psk_rejects_empty_key() {
736        let mut p = PskAuthenticationPlugin::new();
737        let err = p.register_psk("x".into(), Vec::new()).unwrap_err();
738        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
739    }
740
741    #[test]
742    fn register_psk_rejects_empty_id() {
743        let mut p = PskAuthenticationPlugin::new();
744        let err = p
745            .register_psk(String::new(), alloc::vec![1, 2, 3])
746            .unwrap_err();
747        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
748    }
749
750    #[test]
751    fn register_psk_replace_semantics_last_wins() {
752        let mut p = PskAuthenticationPlugin::new();
753        p.register_psk("k".into(), alloc::vec![1; 32]).unwrap();
754        p.register_psk("k".into(), alloc::vec![2; 32]).unwrap();
755        let key = p.psks.get("k").unwrap();
756        assert_eq!(key, &alloc::vec![2u8; 32]);
757    }
758
759    #[test]
760    fn full_three_round_handshake_alice_bob() {
761        let (mut alice, mut bob, alice_h, bob_h) = alice_bob_with_shared_psk();
762
763        let (alice_hs, out1) = alice.begin_handshake_request(alice_h, bob_h).unwrap();
764        let req = match out1 {
765            HandshakeStepOutcome::SendMessage { token } => token,
766            _ => panic!("expected SendMessage"),
767        };
768
769        let (bob_hs, out2) = bob.begin_handshake_reply(bob_h, alice_h, &req).unwrap();
770        let reply = match out2 {
771            HandshakeStepOutcome::SendMessage { token } => token,
772            _ => panic!("expected SendMessage"),
773        };
774
775        let out3 = alice.process_handshake(alice_hs, &reply).unwrap();
776        let final_tok = match out3 {
777            HandshakeStepOutcome::SendMessage { token } => token,
778            _ => panic!("expected SendMessage"),
779        };
780
781        let out4 = bob.process_handshake(bob_hs, &final_tok).unwrap();
782        let bob_secret = match out4 {
783            HandshakeStepOutcome::Complete { secret } => secret,
784            _ => panic!("expected Complete"),
785        };
786
787        let alice_secret = alice.shared_secret(alice_hs).unwrap();
788        let a_bytes = alice.secret_bytes(alice_secret).unwrap();
789        let b_bytes = bob.secret_bytes(bob_secret).unwrap();
790        assert_eq!(a_bytes.len(), 32);
791        assert_eq!(
792            a_bytes, b_bytes,
793            "alice + bob muessen gleiches secret haben"
794        );
795    }
796
797    #[test]
798    fn tampered_reply_hmac_rejected_by_initiator() {
799        let (mut alice, mut bob, alice_h, bob_h) = alice_bob_with_shared_psk();
800        let (alice_hs, out1) = alice.begin_handshake_request(alice_h, bob_h).unwrap();
801        let req = match out1 {
802            HandshakeStepOutcome::SendMessage { token } => token,
803            _ => panic!(),
804        };
805        let (_, out2) = bob.begin_handshake_reply(bob_h, alice_h, &req).unwrap();
806        let mut reply = match out2 {
807            HandshakeStepOutcome::SendMessage { token } => token,
808            _ => panic!(),
809        };
810        let mut h = DataHolder::from_cdr_le(&reply).unwrap();
811        let mut hmac = h.binary_property(prop::HMAC).unwrap().to_vec();
812        hmac[0] ^= 0x01;
813        h.set_binary_property(prop::HMAC, hmac);
814        reply = h.to_cdr_le();
815        let err = alice.process_handshake(alice_hs, &reply).unwrap_err();
816        assert_eq!(err.kind, SecurityErrorKind::AuthenticationFailed);
817    }
818
819    #[test]
820    fn replay_initiator_request_rejected_second_time() {
821        let (mut alice, mut bob, alice_h, bob_h) = alice_bob_with_shared_psk();
822        let (_alice_hs, out1) = alice.begin_handshake_request(alice_h, bob_h).unwrap();
823        let req = match out1 {
824            HandshakeStepOutcome::SendMessage { token } => token,
825            _ => panic!(),
826        };
827        bob.begin_handshake_reply(bob_h, alice_h, &req).unwrap();
828        let err = bob.begin_handshake_reply(bob_h, alice_h, &req).unwrap_err();
829        assert_eq!(err.kind, SecurityErrorKind::AuthenticationFailed);
830    }
831
832    #[test]
833    fn wrong_psk_on_replier_breaks_hmac_on_initiator() {
834        let mut alice = PskAuthenticationPlugin::new();
835        let mut bob = PskAuthenticationPlugin::new();
836        alice
837            .register_psk("k".into(), alloc::vec![0xAAu8; 32])
838            .unwrap();
839        // Bob hat anderen Key fuer dieselbe ID.
840        bob.register_psk("k".into(), alloc::vec![0xBBu8; 32])
841            .unwrap();
842        let alice_h = alice.validate_local_psk_identity("k").unwrap();
843        let bob_h = bob.validate_local_psk_identity("k").unwrap();
844
845        let (alice_hs, out1) = alice.begin_handshake_request(alice_h, bob_h).unwrap();
846        let req = match out1 {
847            HandshakeStepOutcome::SendMessage { token } => token,
848            _ => panic!(),
849        };
850        let (_, out2) = bob.begin_handshake_reply(bob_h, alice_h, &req).unwrap();
851        let reply = match out2 {
852            HandshakeStepOutcome::SendMessage { token } => token,
853            _ => panic!(),
854        };
855        let err = alice.process_handshake(alice_hs, &reply).unwrap_err();
856        assert_eq!(err.kind, SecurityErrorKind::AuthenticationFailed);
857    }
858
859    #[test]
860    fn unknown_psk_id_in_request_rejected_by_replier() {
861        let mut bob = PskAuthenticationPlugin::new();
862        bob.register_psk("known".into(), alloc::vec![0x11; 32])
863            .unwrap();
864        let bob_h = bob.validate_local_psk_identity("known").unwrap();
865        // Alice schickt request mit "unknown" id (handgebaut).
866        let req = DataHolder::new(class_id::REQUEST)
867            .with_property(prop::PSK_ID, "unknown")
868            .with_property(prop::KAGREE_ALGO, "PSK")
869            .with_binary_property(prop::CHALLENGE1, alloc::vec![0u8; 32])
870            .to_cdr_le();
871        let err = bob
872            .begin_handshake_reply(bob_h, IdentityHandle(99), &req)
873            .unwrap_err();
874        assert_eq!(err.kind, SecurityErrorKind::AuthenticationFailed);
875    }
876
877    #[test]
878    fn truncated_request_rejected() {
879        let mut bob = PskAuthenticationPlugin::new();
880        bob.register_psk("k".into(), alloc::vec![0x11; 32]).unwrap();
881        let bob_h = bob.validate_local_psk_identity("k").unwrap();
882        let err = bob
883            .begin_handshake_reply(bob_h, IdentityHandle(99), &[0u8, 1, 2])
884            .unwrap_err();
885        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
886    }
887
888    #[test]
889    fn validate_remote_token_happy_path() {
890        let mut p = PskAuthenticationPlugin::new();
891        p.register_psk("peer-1".into(), alloc::vec![0xCCu8; 32])
892            .unwrap();
893        let local = p.validate_local_psk_identity("peer-1").unwrap();
894        let token = p.build_identity_token(local).unwrap();
895        let remote = p.validate_remote_psk_identity(&token).unwrap();
896        assert_ne!(remote, local);
897    }
898
899    #[test]
900    fn validate_remote_token_rejects_unknown_id() {
901        let mut p = PskAuthenticationPlugin::new();
902        p.register_psk("known".into(), alloc::vec![0xCCu8; 32])
903            .unwrap();
904        let token = DataHolder::new(class_id::PSK)
905            .with_property(PROP_PSK_ID, "stranger")
906            .to_cdr_le();
907        let err = p.validate_remote_psk_identity(&token).unwrap_err();
908        assert_eq!(err.kind, SecurityErrorKind::AuthenticationFailed);
909    }
910
911    #[test]
912    fn validate_remote_token_rejects_wrong_class_id() {
913        let mut p = PskAuthenticationPlugin::new();
914        p.register_psk("k".into(), alloc::vec![0x1u8; 32]).unwrap();
915        let token = DataHolder::new("DDS:Auth:PKI-DH:1.2")
916            .with_property(PROP_PSK_ID, "k")
917            .to_cdr_le();
918        let err = p.validate_remote_psk_identity(&token).unwrap_err();
919        assert_eq!(err.kind, SecurityErrorKind::AuthenticationFailed);
920    }
921
922    #[test]
923    fn cross_plugin_psk_vs_pki_mismatch_class_id() {
924        // Token von PSK-Plugin → von PKI-IdentityToken-Decoder als
925        // unterschiedlich klassifiziert (verschiedene class_ids).
926        let mut psk = PskAuthenticationPlugin::new();
927        psk.register_psk("k".into(), alloc::vec![0xAA; 32]).unwrap();
928        let h = psk.validate_local_psk_identity("k").unwrap();
929        let psk_token = psk.build_identity_token(h).unwrap();
930        let dh = DataHolder::from_cdr_le(&psk_token).unwrap();
931        assert_eq!(dh.class_id, "DDS:Auth:PSK:1.2");
932        assert_ne!(dh.class_id, "DDS:Auth:PKI-DH:1.2");
933    }
934
935    #[test]
936    fn token_roundtrip_via_data_holder_codec() {
937        let mut p = PskAuthenticationPlugin::new();
938        p.register_psk("alpha".into(), alloc::vec![0xBE; 32])
939            .unwrap();
940        let h = p.validate_local_psk_identity("alpha").unwrap();
941        let token = p.build_identity_token(h).unwrap();
942        let dh = DataHolder::from_cdr_le(&token).unwrap();
943        assert_eq!(dh.class_id, class_id::PSK);
944        assert_eq!(dh.property(PROP_PSK_ID), Some("alpha"));
945    }
946
947    #[test]
948    fn validate_local_via_property_list_with_inline_hex_key() {
949        let mut p = PskAuthenticationPlugin::new();
950        let key_hex: String = (0..32).map(|_| "ab").collect();
951        let props = PropertyList::new()
952            .with(Property::local(PROP_PSK_ID, "node-1"))
953            .with(Property::local(PROP_PSK_KEY_HEX, key_hex));
954        let h = p.validate_local_identity(&props, [0xAA; 16]).unwrap();
955        assert!(h.0 >= 1);
956    }
957
958    #[test]
959    fn validate_local_via_property_list_missing_id_rejected() {
960        let mut p = PskAuthenticationPlugin::new();
961        let props = PropertyList::new();
962        let err = p.validate_local_identity(&props, [0xAA; 16]).unwrap_err();
963        assert_eq!(err.kind, SecurityErrorKind::InvalidConfiguration);
964    }
965
966    #[test]
967    fn shared_secret_returns_bad_argument_for_unknown_handle() {
968        let p = PskAuthenticationPlugin::new();
969        let err = p.shared_secret(HandshakeHandle(42)).unwrap_err();
970        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
971    }
972
973    #[test]
974    fn hkdf_test_vector_rfc5869_ish_is_deterministic() {
975        // Cross-Validation: gleicher PSK + gleiche Challenges → bit-identisches Secret.
976        let psk = alloc::vec![0x0bu8; 22];
977        let ch1 = [0x01u8; 32];
978        let ch2 = [0x02u8; 32];
979        let s1 = derive_psk_shared_secret(&psk, &ch1, &ch2).unwrap();
980        let s2 = derive_psk_shared_secret(&psk, &ch1, &ch2).unwrap();
981        assert_eq!(s1, s2);
982        // Andere Challenges → anderer Secret.
983        let s3 = derive_psk_shared_secret(&psk, &ch2, &ch1).unwrap();
984        assert_ne!(s1, s3);
985    }
986
987    #[test]
988    fn shared_secret_is_32_bytes() {
989        let psk = alloc::vec![0xFFu8; 16];
990        let s = derive_psk_shared_secret(&psk, &[0u8; 32], &[1u8; 32]).unwrap();
991        assert_eq!(s.len(), 32);
992    }
993
994    #[test]
995    fn process_handshake_unknown_handle_rejected() {
996        let mut p = PskAuthenticationPlugin::new();
997        let err = p.process_handshake(HandshakeHandle(999), &[]).unwrap_err();
998        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
999    }
1000
1001    #[test]
1002    fn final_token_validates_initiator_hmac_on_replier() {
1003        // Wenn der Initiator-HMAC im Final-Token kaputt ist, schlaegt
1004        // process_final_on_replier mit AuthenticationFailed fehl.
1005        let (mut alice, mut bob, alice_h, bob_h) = alice_bob_with_shared_psk();
1006        let (alice_hs, req_out) = alice.begin_handshake_request(alice_h, bob_h).unwrap();
1007        let req = match req_out {
1008            HandshakeStepOutcome::SendMessage { token } => token,
1009            _ => panic!(),
1010        };
1011        let (bob_hs, reply_out) = bob.begin_handshake_reply(bob_h, alice_h, &req).unwrap();
1012        let reply = match reply_out {
1013            HandshakeStepOutcome::SendMessage { token } => token,
1014            _ => panic!(),
1015        };
1016        let final_out = alice.process_handshake(alice_hs, &reply).unwrap();
1017        let mut final_tok = match final_out {
1018            HandshakeStepOutcome::SendMessage { token } => token,
1019            _ => panic!(),
1020        };
1021        let mut h = DataHolder::from_cdr_le(&final_tok).unwrap();
1022        let mut hm = h.binary_property(prop::HMAC).unwrap().to_vec();
1023        hm[5] ^= 0xFF;
1024        h.set_binary_property(prop::HMAC, hm);
1025        final_tok = h.to_cdr_le();
1026        let err = bob.process_handshake(bob_hs, &final_tok).unwrap_err();
1027        assert_eq!(err.kind, SecurityErrorKind::AuthenticationFailed);
1028    }
1029
1030    #[test]
1031    fn request_token_carries_kagree_psk() {
1032        let (mut alice, _bob, alice_h, _bob_h) = alice_bob_with_shared_psk();
1033        let (_, out) = alice
1034            .begin_handshake_request(alice_h, IdentityHandle(99))
1035            .unwrap();
1036        let token = match out {
1037            HandshakeStepOutcome::SendMessage { token } => token,
1038            _ => panic!(),
1039        };
1040        let dh = DataHolder::from_cdr_le(&token).unwrap();
1041        assert_eq!(dh.class_id, class_id::REQUEST);
1042        assert_eq!(dh.property(prop::KAGREE_ALGO), Some("PSK"));
1043    }
1044
1045    #[test]
1046    fn shared_secret_provider_returns_bytes_after_handshake() {
1047        let (mut alice, mut bob, alice_h, bob_h) = alice_bob_with_shared_psk();
1048        let (alice_hs, out1) = alice.begin_handshake_request(alice_h, bob_h).unwrap();
1049        let req = match out1 {
1050            HandshakeStepOutcome::SendMessage { token } => token,
1051            _ => panic!(),
1052        };
1053        let (bob_hs, out2) = bob.begin_handshake_reply(bob_h, alice_h, &req).unwrap();
1054        let reply = match out2 {
1055            HandshakeStepOutcome::SendMessage { token } => token,
1056            _ => panic!(),
1057        };
1058        let out3 = alice.process_handshake(alice_hs, &reply).unwrap();
1059        let final_tok = match out3 {
1060            HandshakeStepOutcome::SendMessage { token } => token,
1061            _ => panic!(),
1062        };
1063        bob.process_handshake(bob_hs, &final_tok).unwrap();
1064
1065        let alice_secret = alice.shared_secret(alice_hs).unwrap();
1066        let provider: &dyn SharedSecretProvider = &alice;
1067        let bytes = provider.get_shared_secret(alice_secret).unwrap();
1068        assert_eq!(bytes.len(), 32);
1069    }
1070
1071    #[test]
1072    fn hex_decode_round_trips_simple_input() {
1073        let v = hex_decode("0a0b").unwrap();
1074        assert_eq!(v, alloc::vec![0x0a, 0x0b]);
1075    }
1076
1077    #[test]
1078    fn hex_decode_rejects_odd_len() {
1079        let err = hex_decode("abc").unwrap_err();
1080        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
1081    }
1082
1083    #[test]
1084    fn hex_decode_rejects_non_hex() {
1085        let err = hex_decode("zz").unwrap_err();
1086        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
1087    }
1088
1089    // -------------------------------------------------------------
1090    // Mutation-Killer (2026-05-01)
1091    // -------------------------------------------------------------
1092
1093    /// Replay-cache CAP-Boundary (analog plugin.rs).
1094    /// Faengt `>` -> `==`/`>=` auf record_challenge eviction.
1095    #[test]
1096    fn psk_replay_cache_holds_exactly_cap() {
1097        let (mut alice, _, alice_h, _) = alice_bob_with_shared_psk();
1098        for i in 0..REPLAY_CACHE_CAP {
1099            let mut c = [0u8; 32];
1100            c[0..8].copy_from_slice(&(i as u64).to_le_bytes());
1101            alice.record_challenge(alice_h, c).unwrap();
1102        }
1103        let mut c_first = [0u8; 32];
1104        c_first[0..8].copy_from_slice(&0u64.to_le_bytes());
1105        let err = alice.record_challenge(alice_h, c_first).unwrap_err();
1106        assert_eq!(err.kind, SecurityErrorKind::AuthenticationFailed);
1107    }
1108
1109    #[test]
1110    fn psk_replay_cache_evicts_at_cap_plus_one() {
1111        let (mut alice, _, alice_h, _) = alice_bob_with_shared_psk();
1112        for i in 0..REPLAY_CACHE_CAP {
1113            let mut c = [0u8; 32];
1114            c[0..8].copy_from_slice(&(i as u64).to_le_bytes());
1115            alice.record_challenge(alice_h, c).unwrap();
1116        }
1117        let mut c_extra = [0u8; 32];
1118        c_extra[0..8].copy_from_slice(&(REPLAY_CACHE_CAP as u64).to_le_bytes());
1119        alice.record_challenge(alice_h, c_extra).unwrap();
1120        let mut c0 = [0u8; 32];
1121        c0[0..8].copy_from_slice(&0u64.to_le_bytes());
1122        alice
1123            .record_challenge(alice_h, c0)
1124            .expect("oldest should be evicted");
1125    }
1126
1127    /// hmac_input liefert eine spezifische length-prefixed Konkatenation.
1128    /// Faengt Mutationen `vec![]`/`vec![0]`/`vec![1]`.
1129    #[test]
1130    fn hmac_input_length_prefixed_concatenation() {
1131        let psk_id = "abc";
1132        let ch1 = [0x11u8; 32];
1133        let ch2 = [0x22u8; 32];
1134        let out = hmac_input(psk_id, &ch1, &ch2);
1135        // Layout: u32-LE psk_id_len + psk_id + u32-LE 32 + ch1 + u32-LE 32 + ch2
1136        // = 4 + 3 + 4 + 32 + 4 + 32 = 79
1137        assert_eq!(out.len(), 4 + 3 + 4 + 32 + 4 + 32);
1138        // Erste 4 bytes: 3 als u32-LE
1139        assert_eq!(&out[0..4], &3u32.to_le_bytes());
1140        // psk_id ascii
1141        assert_eq!(&out[4..7], b"abc");
1142        // ch1-len-prefix
1143        assert_eq!(&out[7..11], &32u32.to_le_bytes());
1144        // ch1 bytes
1145        assert_eq!(&out[11..43], &ch1[..]);
1146        // ch2-len-prefix
1147        assert_eq!(&out[43..47], &32u32.to_le_bytes());
1148        // ch2 bytes
1149        assert_eq!(&out[47..79], &ch2[..]);
1150
1151        // Verschiedene Inputs => verschiedene Outputs
1152        let out_b = hmac_input("xyz", &ch1, &ch2);
1153        assert_ne!(out, out_b);
1154    }
1155
1156    /// hex_nibble: a..f, A..F, 0..9 alle drei Branches abdecken.
1157    /// Faengt `delete arm` und `+ -> -` Mutationen.
1158    #[test]
1159    fn hex_nibble_all_three_ranges() {
1160        for (c, expected) in [
1161            (b'0', 0u8),
1162            (b'5', 5),
1163            (b'9', 9),
1164            (b'a', 10),
1165            (b'c', 12),
1166            (b'f', 15),
1167            (b'A', 10),
1168            (b'C', 12),
1169            (b'F', 15),
1170        ] {
1171            assert_eq!(hex_nibble(c).unwrap(), expected, "char {c:#x}");
1172        }
1173        assert!(hex_nibble(b'g').is_err());
1174        assert!(hex_nibble(b'G').is_err());
1175        assert!(hex_nibble(b'@').is_err());
1176    }
1177
1178    /// hex_decode mit konkretem Wert — faengt `*` -> `+` und arithm.
1179    /// Mutationen auf der nibble-Akkumulation.
1180    #[test]
1181    fn hex_decode_specific_byte_values() {
1182        // "ab" = 0xAB; mit `*` mutated waere result 10+11=21 != 171.
1183        assert_eq!(hex_decode("ab").unwrap(), vec![0xAB]);
1184        // "F0" = 0xF0; mutated: 15+0=15 vs 15*16=240.
1185        assert_eq!(hex_decode("F0").unwrap(), vec![0xF0]);
1186        assert_eq!(hex_decode("0F").unwrap(), vec![0x0F]);
1187        assert_eq!(
1188            hex_decode("DEADBEEF").unwrap(),
1189            vec![0xDE, 0xAD, 0xBE, 0xEF]
1190        );
1191    }
1192
1193    /// Final-Token Tampering: einzelnes Field-Mismatch muss rejecten.
1194    /// Faengt `||` -> `&&` (Zeile 624).
1195    fn run_psk_final_tampered<M>(mutate: M)
1196    where
1197        M: FnOnce(&mut DataHolder),
1198    {
1199        let (mut alice, mut bob, alice_h, bob_h) = alice_bob_with_shared_psk();
1200        let (alice_hs, out1) = alice.begin_handshake_request(alice_h, bob_h).unwrap();
1201        let req = match out1 {
1202            HandshakeStepOutcome::SendMessage { token } => token,
1203            _ => panic!(),
1204        };
1205        let (bob_hs, out2) = bob.begin_handshake_reply(bob_h, alice_h, &req).unwrap();
1206        let reply = match out2 {
1207            HandshakeStepOutcome::SendMessage { token } => token,
1208            _ => panic!(),
1209        };
1210        let out3 = alice.process_handshake(alice_hs, &reply).unwrap();
1211        let final_tok = match out3 {
1212            HandshakeStepOutcome::SendMessage { token } => token,
1213            _ => panic!(),
1214        };
1215        let mut h = DataHolder::from_cdr_le(&final_tok).unwrap();
1216        mutate(&mut h);
1217        let tampered = h.to_cdr_le();
1218        let err = bob.process_handshake(bob_hs, &tampered).unwrap_err();
1219        assert_eq!(err.kind, SecurityErrorKind::AuthenticationFailed);
1220    }
1221
1222    #[test]
1223    fn psk_final_challenge1_tamper_rejected() {
1224        run_psk_final_tampered(|h| {
1225            let mut v = h.binary_property("challenge1").unwrap().to_vec();
1226            v[0] ^= 0x01;
1227            h.set_binary_property("challenge1", v);
1228        });
1229    }
1230    #[test]
1231    fn psk_final_challenge2_tamper_rejected() {
1232        run_psk_final_tampered(|h| {
1233            let mut v = h.binary_property("challenge2").unwrap().to_vec();
1234            v[0] ^= 0x01;
1235            h.set_binary_property("challenge2", v);
1236        });
1237    }
1238
1239    /// Faengt `||` -> `&&` (Zeile 624) in process_final_on_replier
1240    /// Echo-Check: ch1 != local UND ch2 == local muss rejecten.
1241    ///
1242    /// Naive Tamper-Tests catchen das nicht, weil hmac-Verify den
1243    /// Mismatch ALSO catcht. Hier wird die HMAC ueber die getamperten
1244    /// Werte neu berechnet, sodass die Mutation an `&&` zur Replier-
1245    /// Akzeptanz fuehrt waehrend die Original `||` zur Echo-Reject
1246    /// fuehrt.
1247    #[test]
1248    fn psk_final_only_ch1_tamper_with_recomputed_hmac_rejected() {
1249        let psk_bytes = alloc::vec![0xA5u8; 32];
1250        let psk_id = "alice-bob";
1251
1252        let (mut alice, mut bob, alice_h, bob_h) = alice_bob_with_shared_psk();
1253        let (alice_hs, out1) = alice.begin_handshake_request(alice_h, bob_h).unwrap();
1254        let req = match out1 {
1255            HandshakeStepOutcome::SendMessage { token } => token,
1256            _ => panic!(),
1257        };
1258        let (bob_hs, out2) = bob.begin_handshake_reply(bob_h, alice_h, &req).unwrap();
1259        let reply = match out2 {
1260            HandshakeStepOutcome::SendMessage { token } => token,
1261            _ => panic!(),
1262        };
1263        let out3 = alice.process_handshake(alice_hs, &reply).unwrap();
1264        let final_tok = match out3 {
1265            HandshakeStepOutcome::SendMessage { token } => token,
1266            _ => panic!(),
1267        };
1268
1269        // Tamper challenge1 + recompute HMAC ueber die getamperten Werte.
1270        let mut h = DataHolder::from_cdr_le(&final_tok).unwrap();
1271        let mut new_ch1 = h.binary_property("challenge1").unwrap().to_vec();
1272        new_ch1[0] ^= 0x01;
1273        let ch2_vec = h.binary_property("challenge2").unwrap().to_vec();
1274
1275        let mut ch1_arr = [0u8; 32];
1276        ch1_arr.copy_from_slice(&new_ch1);
1277        let mut ch2_arr = [0u8; 32];
1278        ch2_arr.copy_from_slice(&ch2_vec);
1279        let new_hmac = hmac_sign(&psk_bytes, psk_id, &ch1_arr, &ch2_arr);
1280
1281        h.set_binary_property("challenge1", new_ch1);
1282        h.set_binary_property("hmac", new_hmac.to_vec());
1283        let tampered = h.to_cdr_le();
1284
1285        // Original (`||`): Echo-Mismatch von ch1 => AuthFailed.
1286        // Mutation (`&&`): ch1 mismatched aber ch2 ok => Echo-Check
1287        // passt durch, hmac-Verify passt (neu berechnet) => Success.
1288        let err = bob.process_handshake(bob_hs, &tampered).unwrap_err();
1289        assert_eq!(err.kind, SecurityErrorKind::AuthenticationFailed);
1290    }
1291}