Skip to main content

zerodds_security_pki/
plugin.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! `AuthenticationPlugin`-Impl auf Basis von X.509 / rustls-webpki.
5//!
6//! Spec: OMG DDS-Security 1.2 §10.3.2.6-8 + §10.3.4. Tokens werden ueber
7//! das `DataHolder`-Wire-Format aus [`crate::handshake_token`] kodiert.
8//!
9//! zerodds-lint: allow no_dyn_in_safe
10//! (`webpki::EndEntityCert::verify_signature` nimmt
11//! `&dyn SignatureVerificationAlgorithm` — das ist ein 3rd-party-API,
12//! nicht ZeroDDS-Eigenkonstruktion. Wir koennen das nicht zu konkreten
13//! Generics konvertieren ohne webpki-Fork.)
14
15use alloc::collections::{BTreeMap, BTreeSet};
16use alloc::vec::Vec;
17use core::sync::atomic::{AtomicU64, Ordering};
18
19use ring::rand::{SecureRandom, SystemRandom};
20use ring::signature;
21use rustls_pki_types::CertificateDer;
22use zerodds_security::authentication::{
23    AuthenticationPlugin, HandshakeHandle, HandshakeStepOutcome, IdentityHandle,
24    SharedSecretHandle, SharedSecretProvider,
25};
26use zerodds_security::error::{SecurityError, SecurityErrorKind, SecurityResult};
27use zerodds_security::properties::PropertyList;
28use zerodds_security_keyexchange::KeyExchange;
29
30use crate::handshake_token::{
31    self as ht, FinalBuildInput, ReplyBuildInput, RequestBuildInput, ct_eq, signing_bytes,
32};
33use crate::identity::{CertKeyAlgo, IdentityConfig, ParsedIdentity, PkiError};
34
35/// Property-Keys (Spec §8.3.2.7 / implementation-defined). Wir folgen
36/// der Fast-DDS-Konvention, damit Nutzer vorhandene XML-Configs
37/// uebernehmen koennen.
38mod keys {
39    /// PEM-kodiertes Identity-Zertifikat (im Property-Value direkt,
40    /// **nicht** als Dateipfad — so kann der Plugin-Caller entscheiden,
41    /// ob er vom Filesystem laedt oder aus einem Secret-Manager).
42    pub const IDENTITY_CERT: &str = "dds.sec.auth.identity_certificate";
43    /// PEM-kodiertes CA-Bundle.
44    pub const IDENTITY_CA: &str = "dds.sec.auth.identity_ca";
45    /// PEM-kodierter PKCS8-Private-Key (passend zum Identity-Cert).
46    pub const IDENTITY_KEY: &str = "dds.sec.auth.private_key";
47}
48
49/// Maximaler Speicher fuer "kuerzlich gesehene" Initiator-Challenges
50/// (Replay-Detection-Cache pro Replier). 1024 entries × 32 byte = 32 KiB.
51const REPLAY_CACHE_CAP: usize = 1024;
52
53/// PKI/X.509-basierter `AuthenticationPlugin`. Verifiziert Identity-
54/// Certs gegen einen vorgegebenen Trust-Anchor und fuehrt einen
55/// 3-Round PKI-DH-Handshake (Spec §10.3.2.6-8 Tab.56/57/58) zum Peer.
56///
57/// Wire (C3.1): drei `DataHolder`-Tokens
58/// `DDS:Auth:PKI-DH:1.2+AuthReq` / `+AuthReply` / `+AuthFinal`. Beide
59/// Seiten echo'n `cert_der` + `dh*` + `challenge*` + Hash-Bindings.
60/// Replier signiert (kagree || ch1 || dh1 || ch2 || dh2) mit
61/// Identity-Private-Key; Initiator signiert (kagree || ch2 || dh2 ||
62/// ch1 || dh1).
63pub struct PkiAuthenticationPlugin {
64    next_handle: AtomicU64,
65    identities: BTreeMap<IdentityHandle, ParsedIdentity>,
66    /// Initiator-Side: noch nicht abgeschlossene Handshakes.
67    pending_initiator: BTreeMap<HandshakeHandle, InitiatorState>,
68    /// Replier-Side: zwischen Reply-Send und Final-Empfang.
69    pending_replier: BTreeMap<HandshakeHandle, ReplierState>,
70    /// Abgeschlossene Handshakes → SharedSecret-Handle.
71    handshake_to_secret: BTreeMap<HandshakeHandle, SharedSecretHandle>,
72    /// Materialisierte SharedSecrets (32 byte HKDF-SHA256-Output).
73    secrets: BTreeMap<SharedSecretHandle, Vec<u8>>,
74    /// Pro lokalem Identity-Handle: Set aller bereits gesehenen
75    /// Initiator-Challenges (Replay-Detection). Wird gecappt.
76    replay_cache: BTreeMap<IdentityHandle, BTreeSet<[u8; 32]>>,
77    /// FIFO-Order der replay-cache Entries fuer Cap-Eviction.
78    replay_order: BTreeMap<IdentityHandle, Vec<[u8; 32]>>,
79}
80
81struct InitiatorState {
82    /// Lokaler Identity-Handle (fuer Cert/Key-Lookup).
83    local: IdentityHandle,
84    /// Ephemerales DH-Keypair (verbraucht beim REPLY).
85    kx: Option<KeyExchange>,
86    /// Bytes des lokalen DH-Public.
87    dh1: Vec<u8>,
88    /// Lokal generierte Challenge.
89    challenge1: [u8; 32],
90    /// Hash-Bindung des Initiator-Tupels.
91    hash_c1: [u8; 32],
92    /// Permissions-Document (echo). Wird vom AccessControl-Plugin im
93    /// Permissions-Bind-Schritt konsumiert; hier nur fuer Symmetrie
94    /// zum Replier-State gehalten.
95    #[allow(dead_code)]
96    permissions: Vec<u8>,
97    /// Pdata (echo).
98    #[allow(dead_code)]
99    pdata: Vec<u8>,
100    /// Eigenes `c.kagree_algo` (was wir im REQUEST geschickt haben).
101    kagree_algo: alloc::string::String,
102    /// Eigenes `c.dsign_algo`. Wird vom Replier echoed und beim
103    /// Reply-Decode auf Plausibilitaet geprueft (kein Match-Vergleich
104    /// hier, weil der Initiator den Replier-Algo schickt).
105    #[allow(dead_code)]
106    dsign_algo: alloc::string::String,
107}
108
109struct ReplierState {
110    /// Lokaler Identity-Handle.
111    local: IdentityHandle,
112    /// Vom Replier akzeptierter `c.kagree_algo` (vom Initiator uebernommen).
113    kagree_algo: alloc::string::String,
114    /// Initiator-Challenge.
115    challenge1: [u8; 32],
116    /// Replier-Challenge.
117    challenge2: [u8; 32],
118    /// Initiator-DH (echo).
119    dh1: Vec<u8>,
120    /// Replier-DH.
121    dh2: Vec<u8>,
122    /// hash_c1 (echo).
123    hash_c1: [u8; 32],
124    /// hash_c2 (replier-Tupel).
125    hash_c2: [u8; 32],
126    /// Bereits abgeleitetes SharedSecret.
127    secret_handle: SharedSecretHandle,
128    /// Initiator-Cert-DER (zur Final-Signature-Pruefung).
129    initiator_cert_der: Vec<u8>,
130    /// Detektierter Initiator-Cert-Algo.
131    initiator_key_algo: CertKeyAlgo,
132}
133
134impl Default for PkiAuthenticationPlugin {
135    fn default() -> Self {
136        Self::new()
137    }
138}
139
140impl PkiAuthenticationPlugin {
141    /// Konstruktor.
142    #[must_use]
143    pub fn new() -> Self {
144        Self {
145            next_handle: AtomicU64::new(0),
146            identities: BTreeMap::new(),
147            pending_initiator: BTreeMap::new(),
148            pending_replier: BTreeMap::new(),
149            handshake_to_secret: BTreeMap::new(),
150            secrets: BTreeMap::new(),
151            replay_cache: BTreeMap::new(),
152            replay_order: BTreeMap::new(),
153        }
154    }
155
156    fn next_id(&self) -> u64 {
157        self.next_handle.fetch_add(1, Ordering::Relaxed) + 1
158    }
159
160    /// Programmatische Variante: direkt `IdentityConfig` statt
161    /// `PropertyList` uebergeben. Nuetzlich fuer Tests + native
162    /// Rust-Caller.
163    ///
164    /// # Errors
165    /// Siehe [`PkiError`].
166    pub fn validate_with_config(
167        &mut self,
168        cfg: IdentityConfig,
169        _participant_guid: [u8; 16],
170    ) -> SecurityResult<IdentityHandle> {
171        let parsed = ParsedIdentity::from_config(&cfg).map_err(pki_to_security)?;
172        let handle = IdentityHandle(self.next_id());
173        self.identities.insert(handle, parsed);
174        Ok(handle)
175    }
176
177    /// Liest die rohen 32-byte SharedSecret-Bytes. Primär fuer Tests;
178    /// im Produktions-Pfad wird der `SharedSecretHandle` an den
179    /// CryptoPlugin durchgereicht, ohne dass der Caller die Bytes
180    /// sieht.
181    #[must_use]
182    pub fn secret_bytes(&self, handle: SharedSecretHandle) -> Option<&[u8]> {
183        self.secrets.get(&handle).map(Vec::as_slice)
184    }
185
186    fn store_secret(&mut self, _h: HandshakeHandle, bytes: Vec<u8>) -> SharedSecretHandle {
187        let handle = SharedSecretHandle(self.next_id());
188        self.secrets.insert(handle, bytes);
189        handle
190    }
191
192    fn record_challenge(&mut self, local: IdentityHandle, c: [u8; 32]) -> SecurityResult<()> {
193        let cache = self.replay_cache.entry(local).or_default();
194        if cache.contains(&c) {
195            return Err(SecurityError::new(
196                SecurityErrorKind::AuthenticationFailed,
197                "pki: replayed challenge1 detected",
198            ));
199        }
200        cache.insert(c);
201        let order = self.replay_order.entry(local).or_default();
202        order.push(c);
203        if order.len() > REPLAY_CACHE_CAP {
204            let dropped = order.remove(0);
205            cache.remove(&dropped);
206        }
207        Ok(())
208    }
209}
210
211impl SharedSecretProvider for PkiAuthenticationPlugin {
212    fn get_shared_secret(&self, handle: SharedSecretHandle) -> Option<Vec<u8>> {
213        self.secrets.get(&handle).cloned()
214    }
215}
216
217fn pki_to_security(e: PkiError) -> SecurityError {
218    let kind = match &e {
219        PkiError::InvalidPem(_) | PkiError::NoCertInPem => SecurityErrorKind::BadArgument,
220        PkiError::CertInvalid(_) => SecurityErrorKind::AuthenticationFailed,
221        PkiError::EmptyTrustAnchors => SecurityErrorKind::InvalidConfiguration,
222    };
223    SecurityError::new(kind, alloc::format!("pki: {e}"))
224}
225
226fn random_challenge() -> SecurityResult<[u8; 32]> {
227    let rng = SystemRandom::new();
228    let mut buf = [0u8; 32];
229    rng.fill(&mut buf).map_err(|_| {
230        SecurityError::new(
231            SecurityErrorKind::CryptoFailed,
232            "pki: SystemRandom not available",
233        )
234    })?;
235    Ok(buf)
236}
237
238fn algo_for(key_algo: CertKeyAlgo) -> SecurityResult<&'static str> {
239    match key_algo {
240        CertKeyAlgo::EcdsaP256Sha256 => Ok(ht::algo::ECDSA_SHA256),
241        CertKeyAlgo::RsaPssSha256 => Ok(ht::algo::RSASSA_PSS_SHA256),
242        CertKeyAlgo::Unknown => Err(SecurityError::new(
243            SecurityErrorKind::InvalidConfiguration,
244            "pki: cert public-key algo unsupported",
245        )),
246    }
247}
248
249fn check_dsign_matches(declared: &str, detected: CertKeyAlgo) -> SecurityResult<()> {
250    let expected = algo_for(detected)?;
251    if !declared.eq_ignore_ascii_case(expected) {
252        return Err(SecurityError::new(
253            SecurityErrorKind::InvalidConfiguration,
254            alloc::format!("pki: c.dsign_algo {declared} doesn't match cert (expected {expected})"),
255        ));
256    }
257    Ok(())
258}
259
260fn sign_with(key_algo: CertKeyAlgo, pkcs8: &[u8], msg: &[u8]) -> SecurityResult<Vec<u8>> {
261    let rng = SystemRandom::new();
262    match key_algo {
263        CertKeyAlgo::EcdsaP256Sha256 => {
264            let key = signature::EcdsaKeyPair::from_pkcs8(
265                &signature::ECDSA_P256_SHA256_ASN1_SIGNING,
266                pkcs8,
267                &rng,
268            )
269            .map_err(|e| {
270                SecurityError::new(
271                    SecurityErrorKind::CryptoFailed,
272                    alloc::format!("pki: ecdsa key-parse failed: {e}"),
273                )
274            })?;
275            let sig = key.sign(&rng, msg).map_err(|e| {
276                SecurityError::new(
277                    SecurityErrorKind::CryptoFailed,
278                    alloc::format!("pki: ecdsa sign failed: {e}"),
279                )
280            })?;
281            Ok(sig.as_ref().to_vec())
282        }
283        CertKeyAlgo::RsaPssSha256 => {
284            let key = signature::RsaKeyPair::from_pkcs8(pkcs8).map_err(|e| {
285                SecurityError::new(
286                    SecurityErrorKind::CryptoFailed,
287                    alloc::format!("pki: rsa key-parse failed: {e}"),
288                )
289            })?;
290            let mut out = alloc::vec![0u8; key.public().modulus_len()];
291            key.sign(&signature::RSA_PSS_SHA256, &rng, msg, &mut out)
292                .map_err(|e| {
293                    SecurityError::new(
294                        SecurityErrorKind::CryptoFailed,
295                        alloc::format!("pki: rsa sign failed: {e}"),
296                    )
297                })?;
298            Ok(out)
299        }
300        CertKeyAlgo::Unknown => Err(SecurityError::new(
301            SecurityErrorKind::InvalidConfiguration,
302            "pki: cannot sign — unsupported key algo",
303        )),
304    }
305}
306
307fn verify_signature_with_cert(
308    cert_der: &[u8],
309    key_algo: CertKeyAlgo,
310    msg: &[u8],
311    sig: &[u8],
312) -> SecurityResult<()> {
313    let cert = CertificateDer::from_slice(cert_der);
314    let ee = webpki::EndEntityCert::try_from(&cert).map_err(|e| {
315        SecurityError::new(
316            SecurityErrorKind::AuthenticationFailed,
317            alloc::format!("pki: peer cert parse failed: {e:?}"),
318        )
319    })?;
320    let alg: &dyn rustls_pki_types::SignatureVerificationAlgorithm = match key_algo {
321        CertKeyAlgo::EcdsaP256Sha256 => webpki::ring::ECDSA_P256_SHA256,
322        CertKeyAlgo::RsaPssSha256 => webpki::ring::RSA_PSS_2048_8192_SHA256_LEGACY_KEY,
323        CertKeyAlgo::Unknown => {
324            return Err(SecurityError::new(
325                SecurityErrorKind::InvalidConfiguration,
326                "pki: peer cert algo unsupported",
327            ));
328        }
329    };
330    ee.verify_signature(alg, msg, sig).map_err(|e| {
331        SecurityError::new(
332            SecurityErrorKind::AuthenticationFailed,
333            alloc::format!("pki: signature verify failed: {e:?}"),
334        )
335    })
336}
337
338/// Detect peer cert algo from DER (re-uses identity helper logic via
339/// trial-parse; pragmatic — we duplicate the SPKI scan locally to avoid
340/// re-exporting an internal helper).
341fn detect_peer_algo(cert_der: &[u8]) -> CertKeyAlgo {
342    crate::identity::detect_cert_algo_pub(cert_der)
343}
344
345/// Leitet 32-byte SharedSecret aus DH + Challenges via HKDF-SHA256 ab.
346/// Salt = (challenge1 || challenge2). Info = ZeroDDS-Domain-Separator.
347fn derive_shared_secret(
348    raw_dh: &[u8],
349    challenge1: &[u8; 32],
350    challenge2: &[u8; 32],
351) -> SecurityResult<Vec<u8>> {
352    use ring::hkdf;
353    let mut salt = [0u8; 64];
354    salt[..32].copy_from_slice(challenge1);
355    salt[32..].copy_from_slice(challenge2);
356    let salt_obj = hkdf::Salt::new(hkdf::HKDF_SHA256, &salt);
357    let prk = salt_obj.extract(raw_dh);
358    let info = [b"DDS-Security-1.2-SharedSecret".as_slice()];
359    let okm = prk.expand(&info, hkdf::HKDF_SHA256).map_err(|_| {
360        SecurityError::new(SecurityErrorKind::CryptoFailed, "pki: HKDF expand failed")
361    })?;
362    let mut out = [0u8; 32];
363    okm.fill(&mut out).map_err(|_| {
364        SecurityError::new(SecurityErrorKind::CryptoFailed, "pki: HKDF fill failed")
365    })?;
366    Ok(out.to_vec())
367}
368
369impl AuthenticationPlugin for PkiAuthenticationPlugin {
370    fn validate_local_identity(
371        &mut self,
372        props: &PropertyList,
373        participant_guid: [u8; 16],
374    ) -> SecurityResult<IdentityHandle> {
375        let cert = props.get(keys::IDENTITY_CERT).ok_or_else(|| {
376            SecurityError::new(
377                SecurityErrorKind::InvalidConfiguration,
378                "pki: fehlt dds.sec.auth.identity_certificate",
379            )
380        })?;
381        let ca = props.get(keys::IDENTITY_CA).ok_or_else(|| {
382            SecurityError::new(
383                SecurityErrorKind::InvalidConfiguration,
384                "pki: fehlt dds.sec.auth.identity_ca",
385            )
386        })?;
387        let cfg = IdentityConfig {
388            identity_cert_pem: cert.as_bytes().to_vec(),
389            identity_ca_pem: ca.as_bytes().to_vec(),
390            identity_key_pem: props.get(keys::IDENTITY_KEY).map(|s| s.as_bytes().to_vec()),
391        };
392        self.validate_with_config(cfg, participant_guid)
393    }
394
395    fn validate_remote_identity(
396        &mut self,
397        local: IdentityHandle,
398        _remote_participant_guid: [u8; 16],
399        remote_auth_token: &[u8],
400    ) -> SecurityResult<IdentityHandle> {
401        let parsed = self.identities.get(&local).ok_or_else(|| {
402            SecurityError::new(
403                SecurityErrorKind::BadArgument,
404                "pki: unbekannter lokaler IdentityHandle",
405            )
406        })?;
407        parsed
408            .verify_remote_der(remote_auth_token)
409            .map_err(pki_to_security)?;
410        let handle = IdentityHandle(self.next_id());
411        Ok(handle)
412    }
413
414    fn begin_handshake_request(
415        &mut self,
416        initiator: IdentityHandle,
417        _replier: IdentityHandle,
418    ) -> SecurityResult<(HandshakeHandle, HandshakeStepOutcome)> {
419        let parsed = self.identities.get(&initiator).ok_or_else(|| {
420            SecurityError::new(
421                SecurityErrorKind::BadArgument,
422                "pki: unbekannter Initiator-IdentityHandle",
423            )
424        })?;
425        let cert_der = parsed.cert_der.clone();
426        let key_algo = parsed.key_algo;
427        let dsign_algo = algo_for(key_algo)?.to_owned();
428        let kagree_algo = ht::algo::X25519.to_owned();
429
430        let kx = KeyExchange::new()?;
431        let dh1 = kx.public_key().to_vec();
432        let challenge1 = random_challenge()?;
433        let permissions: Vec<u8> = Vec::new();
434        let pdata: Vec<u8> = Vec::new();
435
436        let bytes = ht::build_request_token(&RequestBuildInput {
437            cert_der: &cert_der,
438            permissions: &permissions,
439            pdata: &pdata,
440            dsign_algo: &dsign_algo,
441            kagree_algo: &kagree_algo,
442            dh1: &dh1,
443            challenge1: &challenge1,
444            ocsp_status: &[],
445        })?;
446
447        let hash_c1 =
448            ht::compute_hash_c(&cert_der, &permissions, &pdata, &dsign_algo, &kagree_algo);
449
450        let handle = HandshakeHandle(self.next_id());
451        self.pending_initiator.insert(
452            handle,
453            InitiatorState {
454                local: initiator,
455                kx: Some(kx),
456                dh1,
457                challenge1,
458                hash_c1,
459                permissions,
460                pdata,
461                kagree_algo,
462                dsign_algo,
463            },
464        );
465        Ok((handle, HandshakeStepOutcome::SendMessage { token: bytes }))
466    }
467
468    fn begin_handshake_reply(
469        &mut self,
470        replier: IdentityHandle,
471        _initiator: IdentityHandle,
472        request_token: &[u8],
473    ) -> SecurityResult<(HandshakeHandle, HandshakeStepOutcome)> {
474        // 1) Parse + Hash-Re-Validation (passiert in parse_request_token).
475        let req = ht::parse_request_token(request_token)?;
476
477        // 2) Initiator-Cert gegen unseren Trust-Store validieren.
478        let parsed = self.identities.get(&replier).ok_or_else(|| {
479            SecurityError::new(
480                SecurityErrorKind::BadArgument,
481                "pki: unbekannter Replier-IdentityHandle",
482            )
483        })?;
484        parsed
485            .verify_remote_der(&req.cert_der)
486            .map_err(pki_to_security)?;
487
488        // 3) Replay-Detection.
489        self.record_challenge(replier, req.challenge1)?;
490
491        // 4) Initiator-`c.dsign_algo` muss zu seinem Cert-Public-Key passen.
492        let initiator_key_algo = detect_peer_algo(&req.cert_der);
493        check_dsign_matches(&req.dsign_algo, initiator_key_algo)?;
494
495        // 5) Eigenes DH-Pair generieren + challenge2.
496        let kx = KeyExchange::new()?;
497        let dh2 = kx.public_key().to_vec();
498        let challenge2 = random_challenge()?;
499
500        // 6) DH-Agreement → SharedSecret.
501        let parsed = self.identities.get(&replier).ok_or_else(|| {
502            SecurityError::new(SecurityErrorKind::Internal, "pki: replier identity gone")
503        })?;
504        // Sign with Replier-Key
505        let priv_key = parsed.private_key_pkcs8_der.clone().ok_or_else(|| {
506            SecurityError::new(
507                SecurityErrorKind::InvalidConfiguration,
508                "pki: replier hat keinen private-key konfiguriert (kann nicht signieren)",
509            )
510        })?;
511        let replier_cert_der = parsed.cert_der.clone();
512        let replier_dsign = algo_for(parsed.key_algo)?.to_owned();
513        let replier_key_algo = parsed.key_algo;
514
515        // raw DH output (KeyExchange consumes self).
516        let secret_bytes = kx.derive_shared_secret(&req.dh1)?;
517        // HKDF-Re-Derive with challenges as salt for spec-aligned key.
518        let final_secret = derive_shared_secret(&secret_bytes, &req.challenge1, &challenge2)?;
519
520        // 7) Signatur ueber (kagree || ch1 || dh1 || ch2 || dh2).
521        let to_sign = signing_bytes(
522            &req.kagree_algo,
523            &req.challenge1,
524            &req.dh1,
525            &challenge2,
526            &dh2,
527        );
528        let signature = sign_with(replier_key_algo, &priv_key, &to_sign)?;
529
530        // 8) Replier-hash_c2 = ueber Replier-Tupel (echo kagree, eigenes
531        //    cert/perm/pdata/dsign).
532        let permissions: Vec<u8> = Vec::new();
533        let pdata: Vec<u8> = Vec::new();
534        let hash_c2 = ht::compute_hash_c(
535            &replier_cert_der,
536            &permissions,
537            &pdata,
538            &replier_dsign,
539            &req.kagree_algo,
540        );
541
542        // 9) Reply-Token bauen.
543        let reply_bytes = ht::build_reply_token(&ReplyBuildInput {
544            cert_der: &replier_cert_der,
545            permissions: &permissions,
546            pdata: &pdata,
547            dsign_algo: &replier_dsign,
548            kagree_algo: &req.kagree_algo,
549            dh2: &dh2,
550            challenge2: &challenge2,
551            hash_c1: &req.hash_c1,
552            dh1: &req.dh1,
553            challenge1: &req.challenge1,
554            ocsp_status: &[],
555            signature: &signature,
556        })?;
557
558        // 10) State persistieren — Final-Empfang braucht das.
559        let handle = HandshakeHandle(self.next_id());
560        let secret_handle = self.store_secret(handle, final_secret);
561        self.handshake_to_secret.insert(handle, secret_handle);
562        self.pending_replier.insert(
563            handle,
564            ReplierState {
565                local: replier,
566                kagree_algo: req.kagree_algo,
567                challenge1: req.challenge1,
568                challenge2,
569                dh1: req.dh1,
570                dh2,
571                hash_c1: req.hash_c1,
572                hash_c2,
573                secret_handle,
574                initiator_cert_der: req.cert_der,
575                initiator_key_algo,
576            },
577        );
578
579        Ok((
580            handle,
581            HandshakeStepOutcome::SendMessage { token: reply_bytes },
582        ))
583    }
584
585    fn process_handshake(
586        &mut self,
587        handshake: HandshakeHandle,
588        token: &[u8],
589    ) -> SecurityResult<HandshakeStepOutcome> {
590        // Initiator-Seite hat einen pending_initiator-Eintrag → Token = REPLY.
591        if self.pending_initiator.contains_key(&handshake) {
592            return self.process_reply_on_initiator(handshake, token);
593        }
594        // Replier-Seite hat einen pending_replier-Eintrag → Token = FINAL.
595        if self.pending_replier.contains_key(&handshake) {
596            return self.process_final_on_replier(handshake, token);
597        }
598        Err(SecurityError::new(
599            SecurityErrorKind::BadArgument,
600            "pki: unbekannter HandshakeHandle",
601        ))
602    }
603
604    fn shared_secret(&self, handshake: HandshakeHandle) -> SecurityResult<SharedSecretHandle> {
605        self.handshake_to_secret
606            .get(&handshake)
607            .copied()
608            .ok_or_else(|| {
609                SecurityError::new(
610                    SecurityErrorKind::BadArgument,
611                    "pki: handshake-handle unbekannt oder noch nicht completed",
612                )
613            })
614    }
615
616    fn plugin_class_id(&self) -> &str {
617        "DDS:Auth:PKI-DH:1.2"
618    }
619}
620
621impl PkiAuthenticationPlugin {
622    fn process_reply_on_initiator(
623        &mut self,
624        handshake: HandshakeHandle,
625        token: &[u8],
626    ) -> SecurityResult<HandshakeStepOutcome> {
627        let reply = ht::parse_reply_token(token)?;
628
629        let st = self.pending_initiator.remove(&handshake).ok_or_else(|| {
630            SecurityError::new(SecurityErrorKind::BadArgument, "pki: initiator state gone")
631        })?;
632
633        // a) Echo-Konsistenz: hash_c1, dh1, challenge1 muessen 1:1 stimmen.
634        if !ct_eq(&reply.hash_c1, &st.hash_c1) {
635            return Err(SecurityError::new(
636                SecurityErrorKind::AuthenticationFailed,
637                "reply: hash_c1 echo mismatch (cert-bind broken)",
638            ));
639        }
640        if !ct_eq(&reply.dh1, &st.dh1) {
641            return Err(SecurityError::new(
642                SecurityErrorKind::AuthenticationFailed,
643                "reply: dh1 echo mismatch",
644            ));
645        }
646        if !ct_eq(&reply.challenge1, &st.challenge1) {
647            return Err(SecurityError::new(
648                SecurityErrorKind::AuthenticationFailed,
649                "reply: challenge1 echo mismatch",
650            ));
651        }
652        if reply.kagree_algo != st.kagree_algo {
653            return Err(SecurityError::new(
654                SecurityErrorKind::AuthenticationFailed,
655                "reply: kagree_algo mismatch",
656            ));
657        }
658
659        // b) Replier-Cert validieren.
660        // Snapshot der lokalen Identity, damit wir spaeter mutable
661        // borrowen koennen (store_secret).
662        let (priv_key, initiator_key_algo) = {
663            let parsed = self.identities.get(&st.local).ok_or_else(|| {
664                SecurityError::new(SecurityErrorKind::Internal, "pki: initiator identity gone")
665            })?;
666            parsed
667                .verify_remote_der(&reply.cert_der)
668                .map_err(pki_to_security)?;
669            let pk = parsed.private_key_pkcs8_der.clone().ok_or_else(|| {
670                SecurityError::new(
671                    SecurityErrorKind::InvalidConfiguration,
672                    "pki: initiator hat keinen private-key (final-sign nicht moeglich)",
673                )
674            })?;
675            (pk, parsed.key_algo)
676        };
677        let replier_key_algo = detect_peer_algo(&reply.cert_der);
678        check_dsign_matches(&reply.dsign_algo, replier_key_algo)?;
679
680        // c) Replier-Signature pruefen.
681        let to_verify = signing_bytes(
682            &reply.kagree_algo,
683            &reply.challenge1,
684            &reply.dh1,
685            &reply.challenge2,
686            &reply.dh2,
687        );
688        verify_signature_with_cert(
689            &reply.cert_der,
690            replier_key_algo,
691            &to_verify,
692            &reply.signature,
693        )?;
694
695        // d) DH-Agreement.
696        let kx = st.kx.ok_or_else(|| {
697            SecurityError::new(SecurityErrorKind::Internal, "pki: ephemeral kx gone")
698        })?;
699        let raw = kx.derive_shared_secret(&reply.dh2)?;
700        let final_secret = derive_shared_secret(&raw, &st.challenge1, &reply.challenge2)?;
701
702        let secret_handle = self.store_secret(handshake, final_secret);
703        self.handshake_to_secret.insert(handshake, secret_handle);
704
705        // e) Final-Token bauen + signieren.
706        let to_sign = signing_bytes(
707            &reply.kagree_algo,
708            &reply.challenge2,
709            &reply.dh2,
710            &reply.challenge1,
711            &reply.dh1,
712        );
713        let signature = sign_with(initiator_key_algo, &priv_key, &to_sign)?;
714        let final_token = ht::build_final_token(&FinalBuildInput {
715            hash_c1: &reply.hash_c1,
716            hash_c2: &reply.hash_c2,
717            dh1: &reply.dh1,
718            dh2: &reply.dh2,
719            challenge1: &reply.challenge1,
720            challenge2: &reply.challenge2,
721            ocsp_status: &[],
722            signature: &signature,
723        })?;
724
725        // Spec §10.3.2.10: Initiator schickt `final_token` UND ist
726        // **complete**. Wir tunneln beides via `SendMessage` + Lookup
727        // ueber `shared_secret()`. Damit die Wire-Schicht den Token
728        // sieht, returnen wir SendMessage; die DCPS-Runtime ruft danach
729        // `shared_secret()`.
730        Ok(HandshakeStepOutcome::SendMessage { token: final_token })
731    }
732
733    fn process_final_on_replier(
734        &mut self,
735        handshake: HandshakeHandle,
736        token: &[u8],
737    ) -> SecurityResult<HandshakeStepOutcome> {
738        let final_tok = ht::parse_final_token(token)?;
739        let st = self.pending_replier.remove(&handshake).ok_or_else(|| {
740            SecurityError::new(SecurityErrorKind::BadArgument, "pki: replier state gone")
741        })?;
742
743        // Echo-Konsistenz.
744        if !ct_eq(&final_tok.hash_c1, &st.hash_c1)
745            || !ct_eq(&final_tok.hash_c2, &st.hash_c2)
746            || !ct_eq(&final_tok.dh1, &st.dh1)
747            || !ct_eq(&final_tok.dh2, &st.dh2)
748            || !ct_eq(&final_tok.challenge1, &st.challenge1)
749            || !ct_eq(&final_tok.challenge2, &st.challenge2)
750        {
751            return Err(SecurityError::new(
752                SecurityErrorKind::AuthenticationFailed,
753                "final: echo mismatch",
754            ));
755        }
756
757        // Initiator-Signature ueber (kagree || ch2 || dh2 || ch1 || dh1).
758        let to_verify = signing_bytes(
759            &st.kagree_algo,
760            &st.challenge2,
761            &st.dh2,
762            &st.challenge1,
763            &st.dh1,
764        );
765        verify_signature_with_cert(
766            &st.initiator_cert_der,
767            st.initiator_key_algo,
768            &to_verify,
769            &final_tok.signature,
770        )?;
771
772        // Wir nehmen den Local-Handle nur wegen `local`-Field, damit der
773        // Lint nicht meckert; produktiv koennten wir es fuer
774        // Audit-Logging nutzen.
775        let _ = st.local;
776        Ok(HandshakeStepOutcome::Complete {
777            secret: st.secret_handle,
778        })
779    }
780}
781
782#[cfg(test)]
783#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
784mod tests {
785    use super::*;
786    use zerodds_security::properties::Property;
787
788    /// Erzeugt ein CA + End-Entity-Cert + dazugehoerigen Private-Key
789    /// (PKCS8-PEM) — alles fuer rcgen-Default ECDSA-P256.
790    fn make_signed_cert_ca_key() -> (Vec<u8>, Vec<u8>, Vec<u8>) {
791        use rcgen::{CertificateParams, KeyPair};
792
793        let mut ca_params = CertificateParams::new(vec!["ZeroDDS Test CA".into()]).unwrap();
794        ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
795        let ca_key = KeyPair::generate().unwrap();
796        let ca_cert = ca_params.self_signed(&ca_key).unwrap();
797        let ca_pem = ca_cert.pem();
798
799        let mut ee_params = CertificateParams::new(vec!["zerodds-node".into()]).unwrap();
800        ee_params.is_ca = rcgen::IsCa::NoCa;
801        let ee_key = KeyPair::generate().unwrap();
802        let ee_cert = ee_params.signed_by(&ee_key, &ca_cert, &ca_key).unwrap();
803        let ee_pem = ee_cert.pem();
804        let ee_key_pem = ee_key.serialize_pem();
805
806        (
807            ee_pem.into_bytes(),
808            ca_pem.into_bytes(),
809            ee_key_pem.into_bytes(),
810        )
811    }
812
813    fn make_cert_with_wrong_ca() -> (Vec<u8>, Vec<u8>, Vec<u8>) {
814        use rcgen::{CertificateParams, KeyPair};
815
816        let mut trusted_ca_params = CertificateParams::new(vec!["Trusted CA".into()]).unwrap();
817        trusted_ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
818        let trusted_ca_key = KeyPair::generate().unwrap();
819        let trusted_ca_cert = trusted_ca_params.self_signed(&trusted_ca_key).unwrap();
820
821        let mut rogue_ca_params = CertificateParams::new(vec!["Rogue CA".into()]).unwrap();
822        rogue_ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
823        let rogue_ca_key = KeyPair::generate().unwrap();
824        let rogue_ca_cert = rogue_ca_params.self_signed(&rogue_ca_key).unwrap();
825
826        let mut ee_params = CertificateParams::new(vec!["impersonator".into()]).unwrap();
827        ee_params.is_ca = rcgen::IsCa::NoCa;
828        let ee_key = KeyPair::generate().unwrap();
829        let ee_cert = ee_params
830            .signed_by(&ee_key, &rogue_ca_cert, &rogue_ca_key)
831            .unwrap();
832
833        (
834            ee_cert.pem().into_bytes(),
835            trusted_ca_cert.pem().into_bytes(),
836            ee_key.serialize_pem().into_bytes(),
837        )
838    }
839
840    fn alice_bob() -> (
841        PkiAuthenticationPlugin,
842        PkiAuthenticationPlugin,
843        IdentityHandle,
844        IdentityHandle,
845        IdentityHandle,
846        IdentityHandle,
847    ) {
848        let (a_cert, ca, a_key) = make_signed_cert_ca_key();
849        let (b_cert, _ca2, b_key) = make_signed_cert_ca_key();
850        // Beide nutzen ihre eigenen CAs, aber wir muessen beide in das
851        // Trust-Bundle der Gegenseite reinpacken, damit die Cert-Chain
852        // valide ist. Stattdessen: beide nehmen die Alice-CA als
853        // Trust-Anchor und Bob's Cert wird unter dieser CA gefaked.
854        // Einfacher: beide regenerieren mit gemeinsamer CA.
855        let _ = (b_cert, b_key);
856
857        // Cleaner approach: gemeinsame CA.
858        use rcgen::{CertificateParams, KeyPair};
859        let mut ca_params = CertificateParams::new(vec!["Common CA".into()]).unwrap();
860        ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
861        let ca_key = KeyPair::generate().unwrap();
862        let ca_cert = ca_params.self_signed(&ca_key).unwrap();
863        let ca_pem = ca_cert.pem().into_bytes();
864
865        let mut alice_params = CertificateParams::new(vec!["alice".into()]).unwrap();
866        alice_params.is_ca = rcgen::IsCa::NoCa;
867        let alice_key_pair = KeyPair::generate().unwrap();
868        let alice_cert = alice_params
869            .signed_by(&alice_key_pair, &ca_cert, &ca_key)
870            .unwrap();
871        let alice_cert_pem = alice_cert.pem().into_bytes();
872        let alice_key_pem = alice_key_pair.serialize_pem().into_bytes();
873
874        let mut bob_params = CertificateParams::new(vec!["bob".into()]).unwrap();
875        bob_params.is_ca = rcgen::IsCa::NoCa;
876        let bob_key_pair = KeyPair::generate().unwrap();
877        let bob_cert = bob_params
878            .signed_by(&bob_key_pair, &ca_cert, &ca_key)
879            .unwrap();
880        let bob_cert_pem = bob_cert.pem().into_bytes();
881        let bob_key_pem = bob_key_pair.serialize_pem().into_bytes();
882
883        let mut alice = PkiAuthenticationPlugin::new();
884        let mut bob = PkiAuthenticationPlugin::new();
885        let alice_h = alice
886            .validate_with_config(
887                IdentityConfig {
888                    identity_cert_pem: alice_cert_pem.clone(),
889                    identity_ca_pem: ca_pem.clone(),
890                    identity_key_pem: Some(alice_key_pem),
891                },
892                [0xAA; 16],
893            )
894            .unwrap();
895        let alice_remote_for_bob = alice
896            .validate_remote_identity(alice_h, [0xBB; 16], &cert_der_from_pem(&bob_cert_pem))
897            .unwrap();
898        let bob_h = bob
899            .validate_with_config(
900                IdentityConfig {
901                    identity_cert_pem: bob_cert_pem.clone(),
902                    identity_ca_pem: ca_pem,
903                    identity_key_pem: Some(bob_key_pem),
904                },
905                [0xBB; 16],
906            )
907            .unwrap();
908        let bob_remote_for_alice = bob
909            .validate_remote_identity(bob_h, [0xAA; 16], &cert_der_from_pem(&alice_cert_pem))
910            .unwrap();
911        let _ = (a_cert, ca, a_key);
912        (
913            alice,
914            bob,
915            alice_h,
916            alice_remote_for_bob,
917            bob_h,
918            bob_remote_for_alice,
919        )
920    }
921
922    fn cert_der_from_pem(pem: &[u8]) -> Vec<u8> {
923        use rustls_pki_types::CertificateDer;
924        use rustls_pki_types::pem::PemObject;
925        CertificateDer::pem_slice_iter(pem)
926            .next()
927            .unwrap()
928            .unwrap()
929            .as_ref()
930            .to_vec()
931    }
932
933    #[test]
934    fn plugin_class_id_matches_spec() {
935        let p = PkiAuthenticationPlugin::new();
936        assert_eq!(p.plugin_class_id(), "DDS:Auth:PKI-DH:1.2");
937    }
938
939    #[test]
940    fn validate_local_identity_accepts_ca_signed_cert() {
941        let (cert_pem, ca_pem, key_pem) = make_signed_cert_ca_key();
942        let mut plugin = PkiAuthenticationPlugin::new();
943        let cfg = IdentityConfig {
944            identity_cert_pem: cert_pem,
945            identity_ca_pem: ca_pem,
946            identity_key_pem: Some(key_pem),
947        };
948        let handle = plugin
949            .validate_with_config(cfg, [0xAA; 16])
950            .expect("signed cert must validate");
951        assert_eq!(handle, IdentityHandle(1));
952    }
953
954    #[test]
955    fn validate_local_identity_rejects_wrong_ca() {
956        let (cert_pem, trusted_ca_pem, key_pem) = make_cert_with_wrong_ca();
957        let mut plugin = PkiAuthenticationPlugin::new();
958        let cfg = IdentityConfig {
959            identity_cert_pem: cert_pem,
960            identity_ca_pem: trusted_ca_pem,
961            identity_key_pem: Some(key_pem),
962        };
963        let err = plugin
964            .validate_with_config(cfg, [0xAA; 16])
965            .expect_err("rogue-CA cert must not validate");
966        assert_eq!(err.kind, SecurityErrorKind::AuthenticationFailed);
967    }
968
969    #[test]
970    fn validate_local_identity_rejects_empty_trust_anchors() {
971        let (cert_pem, _, _) = make_signed_cert_ca_key();
972        let mut plugin = PkiAuthenticationPlugin::new();
973        let cfg = IdentityConfig {
974            identity_cert_pem: cert_pem,
975            identity_ca_pem: b"".to_vec(),
976            identity_key_pem: None,
977        };
978        let err = plugin.validate_with_config(cfg, [0xAA; 16]).unwrap_err();
979        assert_eq!(err.kind, SecurityErrorKind::InvalidConfiguration);
980    }
981
982    #[test]
983    fn validate_local_identity_via_property_list() {
984        let (cert_pem, ca_pem, key_pem) = make_signed_cert_ca_key();
985        let mut plugin = PkiAuthenticationPlugin::new();
986        let cert_str = std::str::from_utf8(&cert_pem).unwrap().to_owned();
987        let ca_str = std::str::from_utf8(&ca_pem).unwrap().to_owned();
988        let key_str = std::str::from_utf8(&key_pem).unwrap().to_owned();
989        let props = PropertyList::new()
990            .with(Property::local(
991                "dds.sec.auth.identity_certificate",
992                cert_str,
993            ))
994            .with(Property::local("dds.sec.auth.identity_ca", ca_str))
995            .with(Property::local("dds.sec.auth.private_key", key_str));
996        let handle = plugin
997            .validate_local_identity(&props, [0xAA; 16])
998            .expect("validate via props");
999        assert!(handle.0 >= 1);
1000    }
1001
1002    // -------------------------------------------------------------
1003    // C3.1 — Spec-konformer Handshake (Tab.56/57/58)
1004    // -------------------------------------------------------------
1005
1006    #[test]
1007    fn full_three_round_handshake_alice_bob() {
1008        let (mut alice, mut bob, alice_h, alice_remote_bob, bob_h, bob_remote_alice) = alice_bob();
1009
1010        // 1) Alice → REQUEST.
1011        let (alice_hs, out1) = alice
1012            .begin_handshake_request(alice_h, alice_remote_bob)
1013            .unwrap();
1014        let req_token = match out1 {
1015            HandshakeStepOutcome::SendMessage { token } => token,
1016            _ => panic!("expected SendMessage"),
1017        };
1018        assert!(req_token.len() > 100, "request token enthaelt cert + DH");
1019
1020        // 2) Bob → REPLY.
1021        let (bob_hs, out2) = bob
1022            .begin_handshake_reply(bob_h, bob_remote_alice, &req_token)
1023            .unwrap();
1024        let reply_token = match out2 {
1025            HandshakeStepOutcome::SendMessage { token } => token,
1026            _ => panic!("expected SendMessage"),
1027        };
1028
1029        // 3) Alice → FINAL.
1030        let out3 = alice.process_handshake(alice_hs, &reply_token).unwrap();
1031        let final_token = match out3 {
1032            HandshakeStepOutcome::SendMessage { token } => token,
1033            _ => panic!("alice expected to send final token"),
1034        };
1035
1036        // 4) Bob processes FINAL → Complete.
1037        let out4 = bob.process_handshake(bob_hs, &final_token).unwrap();
1038        let bob_secret = match out4 {
1039            HandshakeStepOutcome::Complete { secret } => secret,
1040            _ => panic!("expected Complete"),
1041        };
1042
1043        let alice_secret = alice.shared_secret(alice_hs).unwrap();
1044        let a_bytes = alice.secret_bytes(alice_secret).unwrap();
1045        let b_bytes = bob.secret_bytes(bob_secret).unwrap();
1046        assert_eq!(a_bytes.len(), 32);
1047        assert_eq!(
1048            a_bytes, b_bytes,
1049            "alice + bob muessen identisches secret ableiten"
1050        );
1051    }
1052
1053    #[test]
1054    fn request_token_has_spec_class_id_and_properties() {
1055        let (mut alice, _bob, alice_h, alice_remote_bob, _, _) = alice_bob();
1056        let (_, out) = alice
1057            .begin_handshake_request(alice_h, alice_remote_bob)
1058            .unwrap();
1059        let token = match out {
1060            HandshakeStepOutcome::SendMessage { token } => token,
1061            _ => panic!(),
1062        };
1063        let parsed = ht::DataHolder::from_cdr_le(&token).unwrap();
1064        assert_eq!(parsed.class_id, "DDS:Auth:PKI-DH:1.2+AuthReq");
1065        assert!(parsed.property("c.dsign_algo").is_some());
1066        assert!(parsed.property("c.kagree_algo").is_some());
1067        for k in [
1068            "c.id",
1069            "c.perm",
1070            "c.pdata",
1071            "hash_c1",
1072            "dh1",
1073            "challenge1",
1074            "ocsp_status",
1075        ] {
1076            assert!(
1077                parsed.binary_property(k).is_some(),
1078                "missing binary prop: {k}"
1079            );
1080        }
1081    }
1082
1083    #[test]
1084    fn cert_bind_replier_modifies_initiator_cert_in_reply_rejected() {
1085        let (mut alice, mut bob, alice_h, alice_remote_bob, bob_h, bob_remote_alice) = alice_bob();
1086        let (alice_hs, out1) = alice
1087            .begin_handshake_request(alice_h, alice_remote_bob)
1088            .unwrap();
1089        let req = match out1 {
1090            HandshakeStepOutcome::SendMessage { token } => token,
1091            _ => panic!(),
1092        };
1093        let (_bob_hs, out2) = bob
1094            .begin_handshake_reply(bob_h, bob_remote_alice, &req)
1095            .unwrap();
1096        let mut reply_token = match out2 {
1097            HandshakeStepOutcome::SendMessage { token } => token,
1098            _ => panic!(),
1099        };
1100
1101        // Tamper: kippe ein bit in hash_c1 → Echo-Mismatch beim Initiator.
1102        let mut h = ht::DataHolder::from_cdr_le(&reply_token).unwrap();
1103        let mut hash_c1 = h.binary_property("hash_c1").unwrap().to_vec();
1104        hash_c1[0] ^= 0x01;
1105        h.set_binary_property("hash_c1", hash_c1);
1106        reply_token = h.to_cdr_le();
1107
1108        let err = alice.process_handshake(alice_hs, &reply_token).unwrap_err();
1109        assert_eq!(err.kind, SecurityErrorKind::AuthenticationFailed);
1110    }
1111
1112    #[test]
1113    fn signature_tamper_rejected_by_initiator() {
1114        let (mut alice, mut bob, alice_h, alice_remote_bob, bob_h, bob_remote_alice) = alice_bob();
1115        let (alice_hs, out1) = alice
1116            .begin_handshake_request(alice_h, alice_remote_bob)
1117            .unwrap();
1118        let req = match out1 {
1119            HandshakeStepOutcome::SendMessage { token } => token,
1120            _ => panic!(),
1121        };
1122        let (_, out2) = bob
1123            .begin_handshake_reply(bob_h, bob_remote_alice, &req)
1124            .unwrap();
1125        let mut reply = match out2 {
1126            HandshakeStepOutcome::SendMessage { token } => token,
1127            _ => panic!(),
1128        };
1129
1130        let mut h = ht::DataHolder::from_cdr_le(&reply).unwrap();
1131        let mut sig = h.binary_property("signature").unwrap().to_vec();
1132        sig[0] ^= 0x01;
1133        h.set_binary_property("signature", sig);
1134        reply = h.to_cdr_le();
1135
1136        let err = alice.process_handshake(alice_hs, &reply).unwrap_err();
1137        assert_eq!(err.kind, SecurityErrorKind::AuthenticationFailed);
1138    }
1139
1140    #[test]
1141    fn dh_tamper_in_reply_breaks_final_signature() {
1142        let (mut alice, mut bob, alice_h, alice_remote_bob, bob_h, bob_remote_alice) = alice_bob();
1143        let (alice_hs, out1) = alice
1144            .begin_handshake_request(alice_h, alice_remote_bob)
1145            .unwrap();
1146        let req = match out1 {
1147            HandshakeStepOutcome::SendMessage { token } => token,
1148            _ => panic!(),
1149        };
1150        let (bob_hs, out2) = bob
1151            .begin_handshake_reply(bob_h, bob_remote_alice, &req)
1152            .unwrap();
1153        let mut reply = match out2 {
1154            HandshakeStepOutcome::SendMessage { token } => token,
1155            _ => panic!(),
1156        };
1157
1158        // Kippt dh2 → Initiator-Sig-Verify schlaegt fehl (signature
1159        // covers dh2). Wenn das durchrutschen wuerde, wuerde die Final-
1160        // Sig divergieren.
1161        let mut h = ht::DataHolder::from_cdr_le(&reply).unwrap();
1162        let mut dh2 = h.binary_property("dh2").unwrap().to_vec();
1163        dh2[0] ^= 0x01;
1164        h.set_binary_property("dh2", dh2);
1165        reply = h.to_cdr_le();
1166
1167        let err = alice.process_handshake(alice_hs, &reply).unwrap_err();
1168        assert_eq!(err.kind, SecurityErrorKind::AuthenticationFailed);
1169        let _ = bob_hs;
1170    }
1171
1172    #[test]
1173    fn wrong_ca_initiator_rejected_by_replier() {
1174        let (rogue_cert, _trusted_ca, rogue_key) = make_cert_with_wrong_ca();
1175        // Bob hat die Trusted-CA, aber Alice's cert ist von Rogue-CA.
1176        // Wir mounten Bob mit der Trusted-CA, Alice mit ihrer Rogue-CA.
1177
1178        // Alice braucht eine CA die ihr Cert akzeptiert — also nehmen
1179        // wir den Pfad: Alice valide-with-config laed mit der Rogue-CA
1180        // (selbe rogue-ca aus der Helper). Aber make_cert_with_wrong_ca
1181        // gibt nur das EE-Cert + die Trusted-CA zurück. Wir muessen die
1182        // Rogue-CA selbst rebuilden — pragmatischer: Alice+Bob mit
1183        // derselben gemeinsamen CA, aber Bob setzt seinen Trust-Anchor
1184        // auf eine *andere* CA → Initiator-Cert wird beim Reply-Schritt
1185        // rejected.
1186
1187        use rcgen::{CertificateParams, KeyPair};
1188        // Common CA fuer Alice's identity:
1189        let mut alice_ca_params = CertificateParams::new(vec!["AliceCA".into()]).unwrap();
1190        alice_ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
1191        let alice_ca_key = KeyPair::generate().unwrap();
1192        let alice_ca_cert = alice_ca_params.self_signed(&alice_ca_key).unwrap();
1193        let alice_ca_pem = alice_ca_cert.pem();
1194
1195        let mut alice_params = CertificateParams::new(vec!["alice".into()]).unwrap();
1196        alice_params.is_ca = rcgen::IsCa::NoCa;
1197        let alice_key = KeyPair::generate().unwrap();
1198        let alice_cert = alice_params
1199            .signed_by(&alice_key, &alice_ca_cert, &alice_ca_key)
1200            .unwrap();
1201        let alice_cert_pem = alice_cert.pem().into_bytes();
1202        let alice_key_pem = alice_key.serialize_pem().into_bytes();
1203
1204        // Bob mit completely different CA (and own cert):
1205        let mut bob_ca_params = CertificateParams::new(vec!["BobCA".into()]).unwrap();
1206        bob_ca_params.is_ca = rcgen::IsCa::Ca(rcgen::BasicConstraints::Unconstrained);
1207        let bob_ca_key = KeyPair::generate().unwrap();
1208        let bob_ca_cert = bob_ca_params.self_signed(&bob_ca_key).unwrap();
1209        let bob_ca_pem = bob_ca_cert.pem();
1210        let mut bob_params = CertificateParams::new(vec!["bob".into()]).unwrap();
1211        bob_params.is_ca = rcgen::IsCa::NoCa;
1212        let bob_key = KeyPair::generate().unwrap();
1213        let bob_cert = bob_params
1214            .signed_by(&bob_key, &bob_ca_cert, &bob_ca_key)
1215            .unwrap();
1216        let bob_cert_pem = bob_cert.pem().into_bytes();
1217        let bob_key_pem = bob_key.serialize_pem().into_bytes();
1218
1219        let mut alice = PkiAuthenticationPlugin::new();
1220        let mut bob = PkiAuthenticationPlugin::new();
1221        let alice_h = alice
1222            .validate_with_config(
1223                IdentityConfig {
1224                    identity_cert_pem: alice_cert_pem.clone(),
1225                    identity_ca_pem: alice_ca_pem.into_bytes(),
1226                    identity_key_pem: Some(alice_key_pem),
1227                },
1228                [0xAA; 16],
1229            )
1230            .unwrap();
1231        let bob_h = bob
1232            .validate_with_config(
1233                IdentityConfig {
1234                    identity_cert_pem: bob_cert_pem.clone(),
1235                    identity_ca_pem: bob_ca_pem.into_bytes(),
1236                    identity_key_pem: Some(bob_key_pem),
1237                },
1238                [0xBB; 16],
1239            )
1240            .unwrap();
1241
1242        // Alice kennt Bob nicht (anderes CA), aber sie versucht trotzdem
1243        // einen Handshake zu starten. Wir koennen alice_remote_bob nicht
1244        // ueber validate_remote_identity holen — also faken wir mit
1245        // einem gueltigen Stub-Handle. Der Test prueft nur den
1246        // Replier-Path.
1247        let (_alice_hs, out1) = alice
1248            .begin_handshake_request(alice_h, IdentityHandle(99))
1249            .unwrap();
1250        let req = match out1 {
1251            HandshakeStepOutcome::SendMessage { token } => token,
1252            _ => panic!(),
1253        };
1254        // Bob versucht zu replyen → Cert von Alice ist nicht von BobCA → reject.
1255        let err = bob
1256            .begin_handshake_reply(bob_h, IdentityHandle(99), &req)
1257            .unwrap_err();
1258        assert_eq!(err.kind, SecurityErrorKind::AuthenticationFailed);
1259        let _ = (rogue_cert, rogue_key);
1260    }
1261
1262    #[test]
1263    fn replay_initiator_request_rejected_second_time() {
1264        let (mut alice, mut bob, alice_h, alice_remote_bob, bob_h, bob_remote_alice) = alice_bob();
1265        let (_alice_hs, out1) = alice
1266            .begin_handshake_request(alice_h, alice_remote_bob)
1267            .unwrap();
1268        let req = match out1 {
1269            HandshakeStepOutcome::SendMessage { token } => token,
1270            _ => panic!(),
1271        };
1272        // Erstes Reply: ok.
1273        let _ = bob
1274            .begin_handshake_reply(bob_h, bob_remote_alice, &req)
1275            .unwrap();
1276        // Zweites Reply mit *gleichem* request → replay → reject.
1277        let err = bob
1278            .begin_handshake_reply(bob_h, bob_remote_alice, &req)
1279            .unwrap_err();
1280        assert_eq!(err.kind, SecurityErrorKind::AuthenticationFailed);
1281    }
1282
1283    #[test]
1284    fn truncated_request_token_rejected() {
1285        let (_alice, mut bob, _alice_h, _, bob_h, bob_remote_alice) = alice_bob();
1286        let err = bob
1287            .begin_handshake_reply(bob_h, bob_remote_alice, &[0u8, 1u8, 2u8, 3u8, 4u8])
1288            .unwrap_err();
1289        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
1290    }
1291
1292    #[test]
1293    fn hash_c1_mismatch_in_request_rejected() {
1294        let (mut alice, mut bob, alice_h, alice_remote_bob, bob_h, bob_remote_alice) = alice_bob();
1295        let (_, out1) = alice
1296            .begin_handshake_request(alice_h, alice_remote_bob)
1297            .unwrap();
1298        let req = match out1 {
1299            HandshakeStepOutcome::SendMessage { token } => token,
1300            _ => panic!(),
1301        };
1302        // Manipuliere hash_c1 → parse_request_token rejected.
1303        let mut h = ht::DataHolder::from_cdr_le(&req).unwrap();
1304        let mut hc = h.binary_property("hash_c1").unwrap().to_vec();
1305        hc[5] ^= 0xFF;
1306        h.set_binary_property("hash_c1", hc);
1307        let bad = h.to_cdr_le();
1308        let err = bob
1309            .begin_handshake_reply(bob_h, bob_remote_alice, &bad)
1310            .unwrap_err();
1311        assert_eq!(err.kind, SecurityErrorKind::AuthenticationFailed);
1312    }
1313
1314    #[test]
1315    fn cross_algorithm_dsign_mismatch_rejected() {
1316        let (mut alice, mut bob, alice_h, alice_remote_bob, bob_h, bob_remote_alice) = alice_bob();
1317        let (_alice_hs, out1) = alice
1318            .begin_handshake_request(alice_h, alice_remote_bob)
1319            .unwrap();
1320        let mut req = match out1 {
1321            HandshakeStepOutcome::SendMessage { token } => token,
1322            _ => panic!(),
1323        };
1324        // Der Token sagt "RSASSA-PSS-SHA256" obwohl das Cert ECDSA ist.
1325        // Aber: hash_c1 enthaelt das ALTE algo → wenn wir nur dsign_algo
1326        // tauschen, wird hash_c1 mismatchen. Wir muessen also auch
1327        // hash_c1 neu berechnen und das `c.dsign_algo` aendern, dann
1328        // sollte die Algo-Cross-Check beim Replier-Step greifen.
1329        let mut h = ht::DataHolder::from_cdr_le(&req).unwrap();
1330        h.set_property("c.dsign_algo", "RSASSA-PSS-SHA256");
1331        let cert_der = h.binary_property("c.id").unwrap().to_vec();
1332        let perm = h.binary_property("c.perm").unwrap().to_vec();
1333        let pdata = h.binary_property("c.pdata").unwrap().to_vec();
1334        let kagree = h.property("c.kagree_algo").unwrap().to_owned();
1335        let new_hash = ht::compute_hash_c(&cert_der, &perm, &pdata, "RSASSA-PSS-SHA256", &kagree);
1336        h.set_binary_property("hash_c1", new_hash.to_vec());
1337        req = h.to_cdr_le();
1338        let err = bob
1339            .begin_handshake_reply(bob_h, bob_remote_alice, &req)
1340            .unwrap_err();
1341        assert_eq!(err.kind, SecurityErrorKind::InvalidConfiguration);
1342    }
1343
1344    #[test]
1345    fn extra_unknown_properties_in_reply_accepted_forward_compat() {
1346        let (mut alice, mut bob, alice_h, alice_remote_bob, bob_h, bob_remote_alice) = alice_bob();
1347        let (alice_hs, out1) = alice
1348            .begin_handshake_request(alice_h, alice_remote_bob)
1349            .unwrap();
1350        let req = match out1 {
1351            HandshakeStepOutcome::SendMessage { token } => token,
1352            _ => panic!(),
1353        };
1354        let (_, out2) = bob
1355            .begin_handshake_reply(bob_h, bob_remote_alice, &req)
1356            .unwrap();
1357        let reply = match out2 {
1358            HandshakeStepOutcome::SendMessage { token } => token,
1359            _ => panic!(),
1360        };
1361        // Fuegt ein unbekanntes Property hinzu.
1362        let mut h = ht::DataHolder::from_cdr_le(&reply).unwrap();
1363        h.set_property("zerodds.future.feature", "yes");
1364        h.set_binary_property("zerodds.future.opaque", alloc::vec![0xFF; 8]);
1365        // hash_c2 ist ueber die ORIGINAL-Properties. Da wir nur EXTRA-
1366        // Felder hinzufuegen, bleiben die hash-Inputs gleich → ok.
1367        let new_reply = h.to_cdr_le();
1368        let res = alice.process_handshake(alice_hs, &new_reply);
1369        assert!(
1370            res.is_ok(),
1371            "forward-compat extra props muessen akzeptiert werden"
1372        );
1373    }
1374
1375    #[test]
1376    fn empty_permissions_accepted_in_phase3_mvp() {
1377        let (mut alice, mut bob, alice_h, alice_remote_bob, bob_h, bob_remote_alice) = alice_bob();
1378        let (alice_hs, out1) = alice
1379            .begin_handshake_request(alice_h, alice_remote_bob)
1380            .unwrap();
1381        let req = match out1 {
1382            HandshakeStepOutcome::SendMessage { token } => token,
1383            _ => panic!(),
1384        };
1385        // alice setzt keine permissions in diesem Test (kein
1386        // AccessControl-Bind im Authentication-only-Roundtrip).
1387        let h = ht::DataHolder::from_cdr_le(&req).unwrap();
1388        assert_eq!(h.binary_property("c.perm").unwrap().len(), 0);
1389        let _ = bob
1390            .begin_handshake_reply(bob_h, bob_remote_alice, &req)
1391            .unwrap();
1392        let _ = alice_hs;
1393    }
1394
1395    #[test]
1396    fn shared_secret_returns_bad_argument_for_unknown_handle() {
1397        let plugin = PkiAuthenticationPlugin::new();
1398        let err = plugin.shared_secret(HandshakeHandle(42)).unwrap_err();
1399        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
1400    }
1401
1402    #[test]
1403    fn validate_remote_identity_accepts_trusted_cert_der() {
1404        let (cert_pem, ca_pem, key_pem) = make_signed_cert_ca_key();
1405        let mut plugin = PkiAuthenticationPlugin::new();
1406        let local = plugin
1407            .validate_with_config(
1408                IdentityConfig {
1409                    identity_cert_pem: cert_pem.clone(),
1410                    identity_ca_pem: ca_pem,
1411                    identity_key_pem: Some(key_pem),
1412                },
1413                [0xAA; 16],
1414            )
1415            .unwrap();
1416
1417        let remote_der = cert_der_from_pem(&cert_pem);
1418        let remote = plugin
1419            .validate_remote_identity(local, [0xBB; 16], &remote_der)
1420            .expect("trusted remote must be accepted");
1421        assert_ne!(remote, local);
1422    }
1423
1424    // -------------------------------------------------------------
1425    // Mutation-Killer (2026-05-01)
1426    // -------------------------------------------------------------
1427
1428    /// Faengt Mutation `>` -> `>=` und `>` -> `==` auf der replay-cache-
1429    /// Eviction-Boundary. Der Cache MUSS GENAU REPLAY_CACHE_CAP
1430    /// Eintraege halten koennen — Eviction tritt erst beim CAP+1-ten ein.
1431    ///
1432    /// Test-Strategie: nach EXAKT CAP unique Inserts pruefen, ob der
1433    /// erste noch im Cache ist (= Replay-Reject).
1434    /// * Original `>`: 1024 > 1024 false → keine Eviction → 0 noch drin → Reject.
1435    /// * Mutation `==`: 1024 == 1024 true → 0 evicted → kein Reject.
1436    /// * Mutation `>=`: gleichfalls evicted bei 1024 → kein Reject.
1437    #[test]
1438    fn replay_cache_holds_exactly_cap_entries_before_eviction() {
1439        let (mut alice, _bob, alice_h, _, _, _) = alice_bob();
1440        for i in 0..REPLAY_CACHE_CAP {
1441            let mut c = [0u8; 32];
1442            c[0..8].copy_from_slice(&(i as u64).to_le_bytes());
1443            alice.record_challenge(alice_h, c).unwrap();
1444        }
1445        // Genau CAP unique. Der erste (i=0) MUSS noch drin sein → Replay-Reject.
1446        let mut c_first = [0u8; 32];
1447        c_first[0..8].copy_from_slice(&0u64.to_le_bytes());
1448        let err = alice.record_challenge(alice_h, c_first).unwrap_err();
1449        assert_eq!(err.kind, SecurityErrorKind::AuthenticationFailed);
1450    }
1451
1452    /// Eviction tritt beim CAP+1-ten Eintrag ein — Test des
1453    /// Eviction-Pfads selbst (FIFO: aelteste Eintragung wird evicted).
1454    #[test]
1455    fn replay_cache_evicts_oldest_at_cap_plus_one() {
1456        let (mut alice, _bob, alice_h, _, _, _) = alice_bob();
1457        for i in 0..REPLAY_CACHE_CAP {
1458            let mut c = [0u8; 32];
1459            c[0..8].copy_from_slice(&(i as u64).to_le_bytes());
1460            alice.record_challenge(alice_h, c).unwrap();
1461        }
1462        // CAP+1: Eviction triggert auf dem aeltesten (i=0).
1463        let mut c_extra = [0u8; 32];
1464        c_extra[0..8].copy_from_slice(&(REPLAY_CACHE_CAP as u64).to_le_bytes());
1465        alice.record_challenge(alice_h, c_extra).unwrap();
1466
1467        // Der erste (i=0) sollte jetzt evicted sein → re-insert erfolgreich.
1468        let mut c_first = [0u8; 32];
1469        c_first[0..8].copy_from_slice(&0u64.to_le_bytes());
1470        alice
1471            .record_challenge(alice_h, c_first)
1472            .expect("after CAP+1 inserts, oldest must be evicted");
1473
1474        // Ein junger Eintrag (z.B. der CAP-te) muss noch drin sein.
1475        // Nach insert von 0 wurde 1 evicted (FIFO), also testen wir
1476        // einen sicher noch-vorhandenen Eintrag — der CAP-te (=1024).
1477        let mut c_recent = [0u8; 32];
1478        c_recent[0..8].copy_from_slice(&(REPLAY_CACHE_CAP as u64).to_le_bytes());
1479        let err = alice.record_challenge(alice_h, c_recent).unwrap_err();
1480        assert_eq!(err.kind, SecurityErrorKind::AuthenticationFailed);
1481    }
1482
1483    /// Faengt Mutation `get_shared_secret -> None / Some(vec![]) / Some(vec![0]) / Some(vec![1])`.
1484    /// Nach erfolgreichem Handshake muss `get_shared_secret` das
1485    /// gespeicherte 32-Byte-Secret zurueckgeben, nicht None oder einen
1486    /// pauschalen Wert.
1487    #[test]
1488    fn get_shared_secret_returns_stored_value() {
1489        let (mut alice, mut bob, alice_h, alice_remote_bob, bob_h, bob_remote_alice) = alice_bob();
1490
1491        let (alice_hs, out1) = alice
1492            .begin_handshake_request(alice_h, alice_remote_bob)
1493            .unwrap();
1494        let req = match out1 {
1495            HandshakeStepOutcome::SendMessage { token } => token,
1496            _ => panic!(),
1497        };
1498        let (bob_hs, out2) = bob
1499            .begin_handshake_reply(bob_h, bob_remote_alice, &req)
1500            .unwrap();
1501        let reply = match out2 {
1502            HandshakeStepOutcome::SendMessage { token } => token,
1503            _ => panic!(),
1504        };
1505        let out3 = alice.process_handshake(alice_hs, &reply).unwrap();
1506        let final_token = match out3 {
1507            HandshakeStepOutcome::SendMessage { token } => token,
1508            _ => panic!(),
1509        };
1510        let out4 = bob.process_handshake(bob_hs, &final_token).unwrap();
1511        let bob_handle = match out4 {
1512            HandshakeStepOutcome::Complete { secret } => secret,
1513            _ => panic!(),
1514        };
1515        let alice_handle = alice.shared_secret(alice_hs).unwrap();
1516
1517        // get_shared_secret muss konkrete 32 Byte zurueckgeben:
1518        let alice_bytes = SharedSecretProvider::get_shared_secret(&alice, alice_handle).unwrap();
1519        let bob_bytes = SharedSecretProvider::get_shared_secret(&bob, bob_handle).unwrap();
1520        // Mutation `None`: unwrap panicked → test fails.
1521        // Mutation `Some(vec![])`: len=0 != 32.
1522        // Mutation `Some(vec![0])` / `Some(vec![1])`: len=1 != 32.
1523        assert_eq!(alice_bytes.len(), 32);
1524        assert_eq!(bob_bytes.len(), 32);
1525        assert_eq!(
1526            alice_bytes, bob_bytes,
1527            "shared secrets muessen identisch + nicht trivial sein"
1528        );
1529        assert!(alice_bytes.iter().any(|&b| b != 0));
1530        assert!(alice_bytes.iter().any(|&b| b != 1));
1531
1532        // get_shared_secret mit unbekanntem Handle muss None liefern.
1533        let bogus_handle = zerodds_security::authentication::SharedSecretHandle(0xDEAD_BEEF);
1534        assert!(SharedSecretProvider::get_shared_secret(&alice, bogus_handle).is_none());
1535    }
1536
1537    /// Faengt Mutationen `||` -> `&&` auf jedem der 6 Echo-Checks in
1538    /// `process_final_on_replier`. Pro Field je ein Test — wenn EIN
1539    /// Field nicht stimmt, muss bob den Final-Token rejecten.
1540    /// Mit `&&`: nur ALLE 6 Mismatches wuerden rejecten.
1541    fn run_handshake_tampered_final<M>(mutate: M)
1542    where
1543        M: FnOnce(&mut ht::DataHolder),
1544    {
1545        let (mut alice, mut bob, alice_h, alice_remote_bob, bob_h, bob_remote_alice) = alice_bob();
1546        let (alice_hs, out1) = alice
1547            .begin_handshake_request(alice_h, alice_remote_bob)
1548            .unwrap();
1549        let req = match out1 {
1550            HandshakeStepOutcome::SendMessage { token } => token,
1551            _ => panic!(),
1552        };
1553        let (bob_hs, out2) = bob
1554            .begin_handshake_reply(bob_h, bob_remote_alice, &req)
1555            .unwrap();
1556        let reply = match out2 {
1557            HandshakeStepOutcome::SendMessage { token } => token,
1558            _ => panic!(),
1559        };
1560        let out3 = alice.process_handshake(alice_hs, &reply).unwrap();
1561        let final_token = match out3 {
1562            HandshakeStepOutcome::SendMessage { token } => token,
1563            _ => panic!(),
1564        };
1565
1566        let mut h = ht::DataHolder::from_cdr_le(&final_token).unwrap();
1567        mutate(&mut h);
1568        let tampered = h.to_cdr_le();
1569        let err = bob.process_handshake(bob_hs, &tampered).unwrap_err();
1570        assert_eq!(
1571            err.kind,
1572            SecurityErrorKind::AuthenticationFailed,
1573            "tampered final-token muss AuthFailed liefern"
1574        );
1575    }
1576
1577    fn flip_first_byte(h: &mut ht::DataHolder, prop: &str) {
1578        let mut v = h.binary_property(prop).unwrap().to_vec();
1579        v[0] ^= 0x01;
1580        h.set_binary_property(prop, v);
1581    }
1582
1583    #[test]
1584    fn final_token_hash_c1_tamper_rejected() {
1585        run_handshake_tampered_final(|h| flip_first_byte(h, "hash_c1"));
1586    }
1587    #[test]
1588    fn final_token_hash_c2_tamper_rejected() {
1589        run_handshake_tampered_final(|h| flip_first_byte(h, "hash_c2"));
1590    }
1591    #[test]
1592    fn final_token_dh1_tamper_rejected() {
1593        run_handshake_tampered_final(|h| flip_first_byte(h, "dh1"));
1594    }
1595    #[test]
1596    fn final_token_dh2_tamper_rejected() {
1597        run_handshake_tampered_final(|h| flip_first_byte(h, "dh2"));
1598    }
1599    #[test]
1600    fn final_token_challenge1_tamper_rejected() {
1601        run_handshake_tampered_final(|h| flip_first_byte(h, "challenge1"));
1602    }
1603    #[test]
1604    fn final_token_challenge2_tamper_rejected() {
1605        run_handshake_tampered_final(|h| flip_first_byte(h, "challenge2"));
1606    }
1607}