Skip to main content

zerodds_security_pki/
handshake_token.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! DDS-Security 1.2 §10.3.2.6-8 — `HandshakeMessageToken` Codec.
5//!
6//! Spec-konformer Wire-Layout fuer die drei Handshake-Tokens
7//! (`HandshakeRequestMessageToken` / `HandshakeReplyMessageToken` /
8//! `HandshakeFinalMessageToken`). Beide Seiten transportieren cert-DER,
9//! ephemerals, challenges und die Signature ueber
10//! (`c.kagree_algo` || challengeI || dhI || challengeR || dhR).
11//!
12//! # Wire-Layout
13//!
14//! Wir nutzen den shared `DataHolder`-Codec aus `zerodds_security::token`
15//! (Spec §7.2.7 Tab.7) — XCDR1-LE mit length-prefix-Strings (NUL-
16//! terminiert) und 4-byte-Alignment. Der gleiche Codec transportiert
17//! die SPDP-`PID_IDENTITY_TOKEN`/`PID_PERMISSIONS_TOKEN` (C3.5), so
18//! dass es nur **einen** Wire-Layout-Pfad gibt.
19
20use alloc::string::String;
21use alloc::vec::Vec;
22
23use zerodds_security::error::{SecurityError, SecurityErrorKind, SecurityResult};
24// Re-Export, damit Caller `ht::DataHolder` weiter benutzen koennen.
25use ring::digest;
26pub use zerodds_security::token::DataHolder;
27
28// ----------------------------------------------------------------------
29// Spec-Konstanten — §10.3.2.6
30// ----------------------------------------------------------------------
31
32/// Spec class_id-Strings fuer die drei Handshake-Tokens.
33pub mod class_id {
34    /// `HandshakeRequestMessageToken` — Spec §10.3.2.6 Tab.56.
35    pub const REQUEST: &str = "DDS:Auth:PKI-DH:1.2+AuthReq";
36    /// `HandshakeReplyMessageToken` — Spec §10.3.2.7 Tab.57.
37    pub const REPLY: &str = "DDS:Auth:PKI-DH:1.2+AuthReply";
38    /// `HandshakeFinalMessageToken` — Spec §10.3.2.8 Tab.58.
39    pub const FINAL: &str = "DDS:Auth:PKI-DH:1.2+AuthFinal";
40}
41
42/// Spec-Algorithmus-Strings (`c.dsign_algo`, `c.kagree_algo`).
43pub mod algo {
44    /// ECDSA über P-256 mit SHA-256 (Default fuer rcgen-Certs).
45    pub const ECDSA_SHA256: &str = "ECDSA-SHA256";
46    /// RSASSA-PSS mit SHA-256 (alternative fuer RSA-Certs).
47    pub const RSASSA_PSS_SHA256: &str = "RSASSA-PSS-SHA256";
48    /// ECDHE über P-256 mit Cofactor-EUM-Modus — Spec-Default.
49    pub const ECDHE_CEUM_P256: &str = "ECDHE-CEUM-P256";
50    /// DH mit MODP-2048-Group (Spec-Fallback).
51    pub const DH_MODP_2048: &str = "DH+MODP-2048-256";
52    /// X25519 — ZeroDDS-Erweiterung; nicht Spec, aber moderner Curve.
53    pub const X25519: &str = "X25519";
54}
55
56/// Property-Keys laut Spec §10.3.2.6 Tab.56.
57pub mod prop {
58    /// Identity-Cert (DER).
59    pub const C_ID: &str = "c.id";
60    /// Permissions-Document (kann leer sein, wenn kein
61    /// AccessControl-Plugin am Participant haengt).
62    pub const C_PERM: &str = "c.perm";
63    /// ParticipantBuiltinTopicData (XCDR), opaque hier.
64    pub const C_PDATA: &str = "c.pdata";
65    /// Digital-Signature-Algorithmus.
66    pub const C_DSIGN_ALGO: &str = "c.dsign_algo";
67    /// Key-Agreement-Algorithmus.
68    pub const C_KAGREE_ALGO: &str = "c.kagree_algo";
69    /// SHA-256 ueber das Tupel (c.id, c.perm, c.pdata, c.dsign_algo, c.kagree_algo).
70    pub const HASH_C1: &str = "hash_c1";
71    /// SHA-256 ueber das Replier-Tupel.
72    pub const HASH_C2: &str = "hash_c2";
73    /// Initiator-DH-Public-Key.
74    pub const DH1: &str = "dh1";
75    /// Replier-DH-Public-Key.
76    pub const DH2: &str = "dh2";
77    /// Initiator-Random-Challenge (32 byte).
78    pub const CHALLENGE1: &str = "challenge1";
79    /// Replier-Random-Challenge (32 byte).
80    pub const CHALLENGE2: &str = "challenge2";
81    /// OCSP-Response-Bytes (optional, leer = none).
82    pub const OCSP_STATUS: &str = "ocsp_status";
83    /// Replier/Initiator-Signature.
84    pub const SIGNATURE: &str = "signature";
85}
86
87// ----------------------------------------------------------------------
88// DoS-Caps — Spec §8.2.2 + ZeroDDS-Hardening
89// ----------------------------------------------------------------------
90
91/// Max Token-Bytes (DoS-Cap). Spec MTU-Heuristic: 64 KiB.
92pub const MAX_TOKEN_BYTES: usize = 65_536;
93/// Max Cert-DER-Bytes (16 KiB).
94pub const MAX_CERT_DER: usize = 16_384;
95/// Max Challenge-Bytes (Spec ist 32, aber wir cappen <= 64).
96pub const MAX_CHALLENGE: usize = 64;
97/// Max DH-Public-Key-Bytes (uncompressed P-256 = 65 byte, X25519 = 32).
98pub const MAX_DH_PUB: usize = 256;
99/// Max Property-Wert-String.
100pub const MAX_PROP_VALUE: usize = 4096;
101/// Max Property-Anzahl pro DataHolder.
102pub const MAX_PROPS: usize = 64;
103/// Max binary-Property-Anzahl pro DataHolder.
104pub const MAX_BIN_PROPS: usize = 64;
105/// Max Signature-Bytes (RSA-PSS-2048 = 256, ECDSA-P256-ASN.1 ≤ 72).
106pub const MAX_SIGNATURE: usize = 1024;
107
108// ----------------------------------------------------------------------
109// Hash-Helper
110// ----------------------------------------------------------------------
111
112/// SHA-256 ueber `(c.id || c.perm || c.pdata || c.dsign_algo || c.kagree_algo)`.
113///
114/// Eingaben werden length-prefixed (u32-LE) verkettet, damit das Hash
115/// kollisionsfest gegen unterschiedliche Splittings der gleichen Bytes
116/// ist.
117#[must_use]
118pub fn compute_hash_c(
119    c_id: &[u8],
120    c_perm: &[u8],
121    c_pdata: &[u8],
122    c_dsign_algo: &str,
123    c_kagree_algo: &str,
124) -> [u8; 32] {
125    let mut ctx = digest::Context::new(&digest::SHA256);
126    fn put(ctx: &mut digest::Context, bytes: &[u8]) {
127        let len = u32::try_from(bytes.len()).unwrap_or(u32::MAX);
128        ctx.update(&len.to_le_bytes());
129        ctx.update(bytes);
130    }
131    put(&mut ctx, c_id);
132    put(&mut ctx, c_perm);
133    put(&mut ctx, c_pdata);
134    put(&mut ctx, c_dsign_algo.as_bytes());
135    put(&mut ctx, c_kagree_algo.as_bytes());
136    let d = ctx.finish();
137    let mut out = [0u8; 32];
138    out.copy_from_slice(d.as_ref());
139    out
140}
141
142/// Deterministische Bytes ueber die der Replier signiert (und der
143/// Initiator beim Final).
144///
145/// Spec-Layout: `(c.kagree_algo || challengeI || dhI || challengeR || dhR)`.
146/// Wir length-prefixen jedes Element (u32-LE), damit kein
147/// Concatenation-Ambiguity-Angriff moeglich ist.
148#[must_use]
149pub fn signing_bytes(
150    c_kagree_algo: &str,
151    challenge1: &[u8],
152    dh1: &[u8],
153    challenge2: &[u8],
154    dh2: &[u8],
155) -> Vec<u8> {
156    let mut out = Vec::with_capacity(c_kagree_algo.len() + 32 + 64 + 32 + 64 + 4 * 5);
157    fn put(out: &mut Vec<u8>, bytes: &[u8]) {
158        let len = u32::try_from(bytes.len()).unwrap_or(u32::MAX);
159        out.extend_from_slice(&len.to_le_bytes());
160        out.extend_from_slice(bytes);
161    }
162    put(&mut out, c_kagree_algo.as_bytes());
163    put(&mut out, challenge1);
164    put(&mut out, dh1);
165    put(&mut out, challenge2);
166    put(&mut out, dh2);
167    out
168}
169
170// ----------------------------------------------------------------------
171// Token-Builder
172// ----------------------------------------------------------------------
173
174/// Parsed-View eines Request-Tokens (Initiator → Replier).
175#[derive(Debug, Clone)]
176pub struct RequestTokenView {
177    /// Initiator-Identity-Cert (DER).
178    pub cert_der: Vec<u8>,
179    /// Permissions-Document (kann leer sein).
180    pub permissions: Vec<u8>,
181    /// ParticipantBuiltinTopicData (opaque).
182    pub pdata: Vec<u8>,
183    /// Digital-Signature-Algorithmus (z.B. `"ECDSA-SHA256"`).
184    pub dsign_algo: String,
185    /// Key-Agreement-Algorithmus (z.B. `"X25519"`).
186    pub kagree_algo: String,
187    /// SHA-256 ueber die obigen 5 Properties.
188    pub hash_c1: [u8; 32],
189    /// Initiator-DH-Public-Key.
190    pub dh1: Vec<u8>,
191    /// Initiator-Random-Challenge (32 byte).
192    pub challenge1: [u8; 32],
193    /// OCSP-Response-Bytes (kann leer sein).
194    pub ocsp_status: Vec<u8>,
195}
196
197/// Parsed-View eines Reply-Tokens (Replier → Initiator).
198#[derive(Debug, Clone)]
199pub struct ReplyTokenView {
200    /// Replier-Identity-Cert (DER).
201    pub cert_der: Vec<u8>,
202    /// Replier-Permissions.
203    pub permissions: Vec<u8>,
204    /// Replier-pdata.
205    pub pdata: Vec<u8>,
206    /// Replier-Digital-Signature-Algo.
207    pub dsign_algo: String,
208    /// Replier-Key-Agreement-Algo.
209    pub kagree_algo: String,
210    /// SHA-256 ueber Replier-Properties.
211    pub hash_c2: [u8; 32],
212    /// Echo des Initiator-`hash_c1`.
213    pub hash_c1: [u8; 32],
214    /// Replier-DH-Public-Key.
215    pub dh2: Vec<u8>,
216    /// Echo des Initiator-DH-Public.
217    pub dh1: Vec<u8>,
218    /// Replier-Challenge.
219    pub challenge2: [u8; 32],
220    /// Echo des Initiator-Challenge.
221    pub challenge1: [u8; 32],
222    /// Replier-OCSP-Status.
223    pub ocsp_status: Vec<u8>,
224    /// Replier-Signatur über `signing_bytes(kagree, ch1, dh1, ch2, dh2)`.
225    pub signature: Vec<u8>,
226}
227
228/// Parsed-View eines Final-Tokens (Initiator → Replier).
229#[derive(Debug, Clone)]
230pub struct FinalTokenView {
231    /// Echo `hash_c1`.
232    pub hash_c1: [u8; 32],
233    /// Echo `hash_c2`.
234    pub hash_c2: [u8; 32],
235    /// Echo `dh1`.
236    pub dh1: Vec<u8>,
237    /// Echo `dh2`.
238    pub dh2: Vec<u8>,
239    /// Echo `challenge1`.
240    pub challenge1: [u8; 32],
241    /// Echo `challenge2`.
242    pub challenge2: [u8; 32],
243    /// Initiator-OCSP-Status.
244    pub ocsp_status: Vec<u8>,
245    /// Initiator-Signatur über `signing_bytes(kagree, ch2, dh2, ch1, dh1)`.
246    pub signature: Vec<u8>,
247}
248
249/// Eingaben zum Bauen des Request-Tokens.
250pub struct RequestBuildInput<'a> {
251    /// Cert-DER.
252    pub cert_der: &'a [u8],
253    /// Permissions (kann leer sein).
254    pub permissions: &'a [u8],
255    /// Pdata (opaque).
256    pub pdata: &'a [u8],
257    /// `c.dsign_algo` Wert.
258    pub dsign_algo: &'a str,
259    /// `c.kagree_algo` Wert.
260    pub kagree_algo: &'a str,
261    /// Initiator-DH-Public.
262    pub dh1: &'a [u8],
263    /// Initiator-Challenge (32 byte).
264    pub challenge1: &'a [u8; 32],
265    /// OCSP-Response-Bytes (kann leer sein).
266    pub ocsp_status: &'a [u8],
267}
268
269/// Baut den Wire-Bytes eines Request-Tokens via DataHolder.
270///
271/// # Errors
272/// Wenn DoS-Caps verletzt sind (Cert-DER > 16 KiB, DH > 256 byte, ...).
273pub fn build_request_token(input: &RequestBuildInput<'_>) -> SecurityResult<Vec<u8>> {
274    cap_check_request(input)?;
275    let hash_c1 = compute_hash_c(
276        input.cert_der,
277        input.permissions,
278        input.pdata,
279        input.dsign_algo,
280        input.kagree_algo,
281    );
282    let mut h = DataHolder::new(class_id::REQUEST);
283    h.set_property(prop::C_DSIGN_ALGO, input.dsign_algo.to_owned());
284    h.set_property(prop::C_KAGREE_ALGO, input.kagree_algo.to_owned());
285    h.set_binary_property(prop::C_ID, input.cert_der.to_vec());
286    h.set_binary_property(prop::C_PERM, input.permissions.to_vec());
287    h.set_binary_property(prop::C_PDATA, input.pdata.to_vec());
288    h.set_binary_property(prop::HASH_C1, hash_c1.to_vec());
289    h.set_binary_property(prop::DH1, input.dh1.to_vec());
290    h.set_binary_property(prop::CHALLENGE1, input.challenge1.to_vec());
291    h.set_binary_property(prop::OCSP_STATUS, input.ocsp_status.to_vec());
292    Ok(h.to_cdr_le())
293}
294
295/// Parsed Wire-Bytes eines Request-Tokens und re-validiert `hash_c1`.
296///
297/// # Errors
298/// `AuthenticationFailed` wenn `hash_c1` nicht zu den uebrigen
299/// Properties passt; `BadArgument` bei Decode-/Cap-Fehlern.
300pub fn parse_request_token(bytes: &[u8]) -> SecurityResult<RequestTokenView> {
301    let h = DataHolder::from_cdr_le(bytes)?;
302    if h.class_id != class_id::REQUEST {
303        return Err(SecurityError::new(
304            SecurityErrorKind::AuthenticationFailed,
305            "request: class_id mismatch",
306        ));
307    }
308    let cert_der = take_bin(&h, prop::C_ID, MAX_CERT_DER)?;
309    let permissions = take_bin(&h, prop::C_PERM, MAX_TOKEN_BYTES)?;
310    let pdata = take_bin(&h, prop::C_PDATA, MAX_TOKEN_BYTES)?;
311    let dsign_algo = take_prop(&h, prop::C_DSIGN_ALGO)?.to_owned();
312    let kagree_algo = take_prop(&h, prop::C_KAGREE_ALGO)?.to_owned();
313    let hash_c1 = take_fixed::<32>(&h, prop::HASH_C1)?;
314    let dh1 = take_bin(&h, prop::DH1, MAX_DH_PUB)?;
315    let challenge1 = take_fixed::<32>(&h, prop::CHALLENGE1)?;
316    let ocsp_status = h.binary_property(prop::OCSP_STATUS).unwrap_or(&[]).to_vec();
317
318    // Recompute hash_c1 — Schutz gegen MitM, der die Properties ohne
319    // Cert-Tausch umschreibt.
320    let recomputed = compute_hash_c(&cert_der, &permissions, &pdata, &dsign_algo, &kagree_algo);
321    if !ct_eq(&recomputed, &hash_c1) {
322        return Err(SecurityError::new(
323            SecurityErrorKind::AuthenticationFailed,
324            "request: hash_c1 mismatch (token tampered)",
325        ));
326    }
327
328    Ok(RequestTokenView {
329        cert_der,
330        permissions,
331        pdata,
332        dsign_algo,
333        kagree_algo,
334        hash_c1,
335        dh1,
336        challenge1,
337        ocsp_status,
338    })
339}
340
341/// Eingaben zum Bauen des Reply-Tokens.
342pub struct ReplyBuildInput<'a> {
343    /// Replier-Cert-DER.
344    pub cert_der: &'a [u8],
345    /// Replier-Permissions.
346    pub permissions: &'a [u8],
347    /// Replier-pdata.
348    pub pdata: &'a [u8],
349    /// Replier-`c.dsign_algo`.
350    pub dsign_algo: &'a str,
351    /// Replier-`c.kagree_algo` (Echo des Initiator-Werts).
352    pub kagree_algo: &'a str,
353    /// Replier-DH-Public.
354    pub dh2: &'a [u8],
355    /// Replier-Challenge.
356    pub challenge2: &'a [u8; 32],
357    /// Echo `hash_c1`.
358    pub hash_c1: &'a [u8; 32],
359    /// Echo `dh1`.
360    pub dh1: &'a [u8],
361    /// Echo `challenge1`.
362    pub challenge1: &'a [u8; 32],
363    /// OCSP-Status.
364    pub ocsp_status: &'a [u8],
365    /// Replier-Signatur ueber `signing_bytes`.
366    pub signature: &'a [u8],
367}
368
369/// Baut Wire-Bytes des Reply-Tokens.
370///
371/// # Errors
372/// DoS-Cap-Verletzung.
373pub fn build_reply_token(input: &ReplyBuildInput<'_>) -> SecurityResult<Vec<u8>> {
374    cap_check_reply(input)?;
375    let hash_c2 = compute_hash_c(
376        input.cert_der,
377        input.permissions,
378        input.pdata,
379        input.dsign_algo,
380        input.kagree_algo,
381    );
382    let mut h = DataHolder::new(class_id::REPLY);
383    h.set_property(prop::C_DSIGN_ALGO, input.dsign_algo.to_owned());
384    h.set_property(prop::C_KAGREE_ALGO, input.kagree_algo.to_owned());
385    h.set_binary_property(prop::C_ID, input.cert_der.to_vec());
386    h.set_binary_property(prop::C_PERM, input.permissions.to_vec());
387    h.set_binary_property(prop::C_PDATA, input.pdata.to_vec());
388    h.set_binary_property(prop::HASH_C2, hash_c2.to_vec());
389    h.set_binary_property(prop::HASH_C1, input.hash_c1.to_vec());
390    h.set_binary_property(prop::DH2, input.dh2.to_vec());
391    h.set_binary_property(prop::DH1, input.dh1.to_vec());
392    h.set_binary_property(prop::CHALLENGE2, input.challenge2.to_vec());
393    h.set_binary_property(prop::CHALLENGE1, input.challenge1.to_vec());
394    h.set_binary_property(prop::OCSP_STATUS, input.ocsp_status.to_vec());
395    h.set_binary_property(prop::SIGNATURE, input.signature.to_vec());
396    Ok(h.to_cdr_le())
397}
398
399/// Parses Reply-Token. Rekomputiert hash_c2.
400///
401/// # Errors
402/// Decode-/Cap-/Hash-Mismatch.
403pub fn parse_reply_token(bytes: &[u8]) -> SecurityResult<ReplyTokenView> {
404    let h = DataHolder::from_cdr_le(bytes)?;
405    if h.class_id != class_id::REPLY {
406        return Err(SecurityError::new(
407            SecurityErrorKind::AuthenticationFailed,
408            "reply: class_id mismatch",
409        ));
410    }
411    let cert_der = take_bin(&h, prop::C_ID, MAX_CERT_DER)?;
412    let permissions = take_bin(&h, prop::C_PERM, MAX_TOKEN_BYTES)?;
413    let pdata = take_bin(&h, prop::C_PDATA, MAX_TOKEN_BYTES)?;
414    let dsign_algo = take_prop(&h, prop::C_DSIGN_ALGO)?.to_owned();
415    let kagree_algo = take_prop(&h, prop::C_KAGREE_ALGO)?.to_owned();
416    let hash_c2 = take_fixed::<32>(&h, prop::HASH_C2)?;
417    let hash_c1 = take_fixed::<32>(&h, prop::HASH_C1)?;
418    let dh2 = take_bin(&h, prop::DH2, MAX_DH_PUB)?;
419    let dh1 = take_bin(&h, prop::DH1, MAX_DH_PUB)?;
420    let challenge2 = take_fixed::<32>(&h, prop::CHALLENGE2)?;
421    let challenge1 = take_fixed::<32>(&h, prop::CHALLENGE1)?;
422    let ocsp_status = h.binary_property(prop::OCSP_STATUS).unwrap_or(&[]).to_vec();
423    let signature = take_bin(&h, prop::SIGNATURE, MAX_SIGNATURE)?;
424
425    let recomputed = compute_hash_c(&cert_der, &permissions, &pdata, &dsign_algo, &kagree_algo);
426    if !ct_eq(&recomputed, &hash_c2) {
427        return Err(SecurityError::new(
428            SecurityErrorKind::AuthenticationFailed,
429            "reply: hash_c2 mismatch",
430        ));
431    }
432
433    Ok(ReplyTokenView {
434        cert_der,
435        permissions,
436        pdata,
437        dsign_algo,
438        kagree_algo,
439        hash_c2,
440        hash_c1,
441        dh2,
442        dh1,
443        challenge2,
444        challenge1,
445        ocsp_status,
446        signature,
447    })
448}
449
450/// Eingaben zum Bauen des Final-Tokens.
451pub struct FinalBuildInput<'a> {
452    /// Echo `hash_c1`.
453    pub hash_c1: &'a [u8; 32],
454    /// Echo `hash_c2`.
455    pub hash_c2: &'a [u8; 32],
456    /// Echo `dh1`.
457    pub dh1: &'a [u8],
458    /// Echo `dh2`.
459    pub dh2: &'a [u8],
460    /// Echo `challenge1`.
461    pub challenge1: &'a [u8; 32],
462    /// Echo `challenge2`.
463    pub challenge2: &'a [u8; 32],
464    /// Initiator-OCSP-Status.
465    pub ocsp_status: &'a [u8],
466    /// Initiator-Signatur ueber `signing_bytes(kagree, ch2, dh2, ch1, dh1)`.
467    pub signature: &'a [u8],
468}
469
470/// Baut Wire-Bytes des Final-Tokens.
471///
472/// # Errors
473/// DoS-Cap.
474pub fn build_final_token(input: &FinalBuildInput<'_>) -> SecurityResult<Vec<u8>> {
475    if input.signature.len() > MAX_SIGNATURE {
476        return Err(SecurityError::new(
477            SecurityErrorKind::BadArgument,
478            "final: signature > cap",
479        ));
480    }
481    if input.dh1.len() > MAX_DH_PUB || input.dh2.len() > MAX_DH_PUB {
482        return Err(SecurityError::new(
483            SecurityErrorKind::BadArgument,
484            "final: dh > cap",
485        ));
486    }
487    let mut h = DataHolder::new(class_id::FINAL);
488    h.set_binary_property(prop::HASH_C1, input.hash_c1.to_vec());
489    h.set_binary_property(prop::HASH_C2, input.hash_c2.to_vec());
490    h.set_binary_property(prop::DH1, input.dh1.to_vec());
491    h.set_binary_property(prop::DH2, input.dh2.to_vec());
492    h.set_binary_property(prop::CHALLENGE1, input.challenge1.to_vec());
493    h.set_binary_property(prop::CHALLENGE2, input.challenge2.to_vec());
494    h.set_binary_property(prop::OCSP_STATUS, input.ocsp_status.to_vec());
495    h.set_binary_property(prop::SIGNATURE, input.signature.to_vec());
496    Ok(h.to_cdr_le())
497}
498
499/// Parses Final-Token.
500///
501/// # Errors
502/// Decode-/Cap.
503pub fn parse_final_token(bytes: &[u8]) -> SecurityResult<FinalTokenView> {
504    let h = DataHolder::from_cdr_le(bytes)?;
505    if h.class_id != class_id::FINAL {
506        return Err(SecurityError::new(
507            SecurityErrorKind::AuthenticationFailed,
508            "final: class_id mismatch",
509        ));
510    }
511    Ok(FinalTokenView {
512        hash_c1: take_fixed::<32>(&h, prop::HASH_C1)?,
513        hash_c2: take_fixed::<32>(&h, prop::HASH_C2)?,
514        dh1: take_bin(&h, prop::DH1, MAX_DH_PUB)?,
515        dh2: take_bin(&h, prop::DH2, MAX_DH_PUB)?,
516        challenge1: take_fixed::<32>(&h, prop::CHALLENGE1)?,
517        challenge2: take_fixed::<32>(&h, prop::CHALLENGE2)?,
518        ocsp_status: h.binary_property(prop::OCSP_STATUS).unwrap_or(&[]).to_vec(),
519        signature: take_bin(&h, prop::SIGNATURE, MAX_SIGNATURE)?,
520    })
521}
522
523// ----------------------------------------------------------------------
524// Lookup-Helper
525// ----------------------------------------------------------------------
526
527fn cap_check_request(input: &RequestBuildInput<'_>) -> SecurityResult<()> {
528    if input.cert_der.len() > MAX_CERT_DER {
529        return Err(SecurityError::new(
530            SecurityErrorKind::BadArgument,
531            "request: cert > 16 KiB",
532        ));
533    }
534    if input.dh1.len() > MAX_DH_PUB {
535        return Err(SecurityError::new(
536            SecurityErrorKind::BadArgument,
537            "request: dh1 > 256 byte",
538        ));
539    }
540    // Hinweis: input.challenge1 ist `&[u8; 32]` (statisch fix). Der
541    // MAX_CHALLENGE=64-Cap kann nicht erreicht werden — historischer
542    // Defensive-Check, vor Aenderung zu fixed-array. Entfernt, weil
543    // er als always-false-Branch zu equivalent-Mutationen fuehrte.
544    Ok(())
545}
546
547fn cap_check_reply(input: &ReplyBuildInput<'_>) -> SecurityResult<()> {
548    if input.cert_der.len() > MAX_CERT_DER {
549        return Err(SecurityError::new(
550            SecurityErrorKind::BadArgument,
551            "reply: cert > 16 KiB",
552        ));
553    }
554    if input.dh1.len() > MAX_DH_PUB || input.dh2.len() > MAX_DH_PUB {
555        return Err(SecurityError::new(
556            SecurityErrorKind::BadArgument,
557            "reply: dh > 256 byte",
558        ));
559    }
560    if input.signature.len() > MAX_SIGNATURE {
561        return Err(SecurityError::new(
562            SecurityErrorKind::BadArgument,
563            "reply: signature > cap",
564        ));
565    }
566    Ok(())
567}
568
569fn take_bin(h: &DataHolder, key: &str, max: usize) -> SecurityResult<Vec<u8>> {
570    let v = h.binary_property(key).ok_or_else(|| {
571        SecurityError::new(
572            SecurityErrorKind::AuthenticationFailed,
573            alloc::format!("missing binary property: {key}"),
574        )
575    })?;
576    if v.len() > max {
577        return Err(SecurityError::new(
578            SecurityErrorKind::BadArgument,
579            alloc::format!("property {key} > cap"),
580        ));
581    }
582    Ok(v.to_vec())
583}
584
585fn take_fixed<const N: usize>(h: &DataHolder, key: &str) -> SecurityResult<[u8; N]> {
586    let v = h.binary_property(key).ok_or_else(|| {
587        SecurityError::new(
588            SecurityErrorKind::AuthenticationFailed,
589            alloc::format!("missing binary property: {key}"),
590        )
591    })?;
592    if v.len() != N {
593        return Err(SecurityError::new(
594            SecurityErrorKind::BadArgument,
595            alloc::format!("property {key} expected {N} byte"),
596        ));
597    }
598    let mut out = [0u8; N];
599    out.copy_from_slice(v);
600    Ok(out)
601}
602
603fn take_prop<'a>(h: &'a DataHolder, key: &str) -> SecurityResult<&'a str> {
604    h.property(key).ok_or_else(|| {
605        SecurityError::new(
606            SecurityErrorKind::AuthenticationFailed,
607            alloc::format!("missing property: {key}"),
608        )
609    })
610}
611
612/// Konstantes-Zeit-Vergleich (gegen Timing-Side-Channels).
613#[must_use]
614pub fn ct_eq(a: &[u8], b: &[u8]) -> bool {
615    if a.len() != b.len() {
616        return false;
617    }
618    let mut acc = 0u8;
619    for (x, y) in a.iter().zip(b.iter()) {
620        acc |= x ^ y;
621    }
622    acc == 0
623}
624
625#[cfg(test)]
626#[allow(clippy::expect_used, clippy::unwrap_used)]
627mod tests {
628    use super::*;
629
630    #[test]
631    fn data_holder_roundtrip() {
632        let mut h = DataHolder::new("DDS:Auth:PKI-DH:1.2+AuthReq");
633        h.set_property("c.dsign_algo", "ECDSA-SHA256");
634        h.set_binary_property("c.id", alloc::vec![0xAA; 200]);
635        let bytes = h.to_cdr_le();
636        let parsed = DataHolder::from_cdr_le(&bytes).unwrap();
637        assert_eq!(parsed.class_id, h.class_id);
638        assert_eq!(parsed.property("c.dsign_algo"), Some("ECDSA-SHA256"));
639        assert_eq!(parsed.binary_property("c.id").unwrap().len(), 200);
640    }
641
642    #[test]
643    fn data_holder_truncation_rejected() {
644        let mut h = DataHolder::new("X");
645        h.set_binary_property("a", alloc::vec![1, 2, 3]);
646        let bytes = h.to_cdr_le();
647        let truncated = &bytes[..bytes.len() - 1];
648        assert!(DataHolder::from_cdr_le(truncated).is_err());
649    }
650
651    #[test]
652    fn data_holder_replace_dup() {
653        let mut h = DataHolder::new("X");
654        h.set_property("k", "v1");
655        h.set_property("k", "v2");
656        assert_eq!(h.properties.len(), 1);
657        assert_eq!(h.property("k"), Some("v2"));
658    }
659
660    #[test]
661    fn hash_c_is_deterministic_and_input_separated() {
662        let h1 = compute_hash_c(b"a", b"b", b"c", "d", "e");
663        let h2 = compute_hash_c(b"a", b"b", b"c", "d", "e");
664        assert_eq!(h1, h2);
665        // Unterschiedliches Splitting derselben Bytes -> anderer Hash.
666        let h3 = compute_hash_c(b"ab", b"", b"c", "d", "e");
667        assert_ne!(h1, h3);
668    }
669
670    #[test]
671    fn signing_bytes_is_length_prefixed() {
672        let s1 = signing_bytes("X25519", &[0xAA; 32], &[0xBB; 32], &[0xCC; 32], &[0xDD; 32]);
673        let s2 = signing_bytes("X25519", &[0xAA; 32], &[0xBB; 32], &[0xCC; 32], &[0xDD; 32]);
674        assert_eq!(s1, s2);
675        let s3 = signing_bytes("X25519", &[0xAA; 32], &[0xBB; 32], &[0xCC; 32], &[0xDE; 32]);
676        assert_ne!(s1, s3);
677    }
678
679    // -------------------------------------------------------------
680    // Mutation-Killer (2026-05-01) — Cap-Boundary-Tests
681    // -------------------------------------------------------------
682
683    fn make_request_input<'a>(
684        cert_der: &'a [u8],
685        dh1: &'a [u8],
686        challenge1: &'a [u8; 32],
687    ) -> RequestBuildInput<'a> {
688        RequestBuildInput {
689            cert_der,
690            permissions: &[],
691            pdata: &[],
692            dsign_algo: "ECDSA-SHA256",
693            kagree_algo: "DH+MODP-2048-256",
694            dh1,
695            challenge1,
696            ocsp_status: &[],
697        }
698    }
699
700    /// Faengt `>` -> `==`/`>=` auf cap_check_request.cert_der-Cap.
701    /// cert_der EXAKT MAX_CERT_DER muss durchgehen, MAX_CERT_DER+1 erroren.
702    #[test]
703    fn cap_check_request_cert_der_at_cap_accepted() {
704        let cert = vec![0u8; MAX_CERT_DER];
705        let dh = vec![0u8; 32];
706        let ch = [0u8; 32];
707        let res = cap_check_request(&make_request_input(&cert, &dh, &ch));
708        assert!(res.is_ok());
709    }
710    #[test]
711    fn cap_check_request_cert_der_over_cap_rejected() {
712        let cert = vec![0u8; MAX_CERT_DER + 1];
713        let dh = vec![0u8; 32];
714        let ch = [0u8; 32];
715        let err = cap_check_request(&make_request_input(&cert, &dh, &ch)).unwrap_err();
716        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
717    }
718
719    /// dh1-Cap auf cap_check_request.
720    #[test]
721    fn cap_check_request_dh1_at_cap_accepted() {
722        let cert = vec![0u8; 100];
723        let dh = vec![0u8; MAX_DH_PUB];
724        let ch = [0u8; 32];
725        assert!(cap_check_request(&make_request_input(&cert, &dh, &ch)).is_ok());
726    }
727    #[test]
728    fn cap_check_request_dh1_over_cap_rejected() {
729        let cert = vec![0u8; 100];
730        let dh = vec![0u8; MAX_DH_PUB + 1];
731        let ch = [0u8; 32];
732        let err = cap_check_request(&make_request_input(&cert, &dh, &ch)).unwrap_err();
733        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
734    }
735
736    /// build_final_token: dh1 + dh2 EXAKT bei MAX_DH_PUB muss durchgehen.
737    /// Faengt `>` -> `>=` Mutation auf der dh-Pruefung (Zeile 477).
738    #[test]
739    fn build_final_dh_both_exactly_at_cap_accepted() {
740        let dh = vec![0u8; MAX_DH_PUB];
741        let sig = vec![0u8; 64];
742        assert!(build_final_token(&make_final_input(&dh, &dh, &sig)).is_ok());
743    }
744
745    fn make_reply_input<'a>(
746        cert_der: &'a [u8],
747        dh1: &'a [u8],
748        dh2: &'a [u8],
749        signature: &'a [u8],
750    ) -> ReplyBuildInput<'a> {
751        ReplyBuildInput {
752            cert_der,
753            permissions: &[],
754            pdata: &[],
755            dsign_algo: "ECDSA-SHA256",
756            kagree_algo: "DH+MODP-2048-256",
757            dh1,
758            dh2,
759            challenge1: &[0u8; 32],
760            challenge2: &[0u8; 32],
761            ocsp_status: &[],
762            signature,
763            hash_c1: &[0u8; 32],
764        }
765    }
766
767    /// cap_check_reply.cert_der Cap-Boundary.
768    #[test]
769    fn cap_check_reply_cert_der_at_and_over_cap() {
770        let dh = vec![0u8; 32];
771        let sig = vec![0u8; 64];
772
773        let cert_at = vec![0u8; MAX_CERT_DER];
774        assert!(cap_check_reply(&make_reply_input(&cert_at, &dh, &dh, &sig)).is_ok());
775
776        let cert_over = vec![0u8; MAX_CERT_DER + 1];
777        let err = cap_check_reply(&make_reply_input(&cert_over, &dh, &dh, &sig)).unwrap_err();
778        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
779    }
780
781    /// cap_check_reply: `||` -> `&&` auf `dh1>cap || dh2>cap`.
782    /// Pro Seite (dh1 over, dh2 ok) und (dh1 ok, dh2 over) muss Reject kommen.
783    /// Mit `&&` waere nur (BEIDE over) Reject.
784    #[test]
785    fn cap_check_reply_dh1_only_over_cap_rejected() {
786        let cert = vec![0u8; 100];
787        let dh1 = vec![0u8; MAX_DH_PUB + 1];
788        let dh2 = vec![0u8; MAX_DH_PUB];
789        let sig = vec![0u8; 64];
790        let err = cap_check_reply(&make_reply_input(&cert, &dh1, &dh2, &sig)).unwrap_err();
791        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
792    }
793    #[test]
794    fn cap_check_reply_dh2_only_over_cap_rejected() {
795        let cert = vec![0u8; 100];
796        let dh1 = vec![0u8; MAX_DH_PUB];
797        let dh2 = vec![0u8; MAX_DH_PUB + 1];
798        let sig = vec![0u8; 64];
799        let err = cap_check_reply(&make_reply_input(&cert, &dh1, &dh2, &sig)).unwrap_err();
800        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
801    }
802    #[test]
803    fn cap_check_reply_dh_both_at_cap_accepted() {
804        let cert = vec![0u8; 100];
805        let dh = vec![0u8; MAX_DH_PUB];
806        let sig = vec![0u8; 64];
807        assert!(cap_check_reply(&make_reply_input(&cert, &dh, &dh, &sig)).is_ok());
808    }
809
810    /// signature-Cap.
811    #[test]
812    fn cap_check_reply_signature_at_and_over_cap() {
813        let cert = vec![0u8; 100];
814        let dh = vec![0u8; 32];
815
816        let sig_at = vec![0u8; MAX_SIGNATURE];
817        assert!(cap_check_reply(&make_reply_input(&cert, &dh, &dh, &sig_at)).is_ok());
818
819        let sig_over = vec![0u8; MAX_SIGNATURE + 1];
820        let err = cap_check_reply(&make_reply_input(&cert, &dh, &dh, &sig_over)).unwrap_err();
821        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
822    }
823
824    /// build_final_token: signature-Cap-Boundary.
825    fn make_final_input<'a>(dh1: &'a [u8], dh2: &'a [u8], sig: &'a [u8]) -> FinalBuildInput<'a> {
826        FinalBuildInput {
827            hash_c1: &[0u8; 32],
828            hash_c2: &[0u8; 32],
829            dh1,
830            dh2,
831            challenge1: &[0u8; 32],
832            challenge2: &[0u8; 32],
833            ocsp_status: &[],
834            signature: sig,
835        }
836    }
837
838    #[test]
839    fn build_final_signature_at_and_over_cap() {
840        let dh = vec![0u8; 32];
841
842        let sig_at = vec![0u8; MAX_SIGNATURE];
843        assert!(build_final_token(&make_final_input(&dh, &dh, &sig_at)).is_ok());
844
845        let sig_over = vec![0u8; MAX_SIGNATURE + 1];
846        let err = build_final_token(&make_final_input(&dh, &dh, &sig_over)).unwrap_err();
847        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
848    }
849
850    /// build_final_token: dh1/dh2 Cap-Pruefung pro Seite (`||` -> `&&`).
851    #[test]
852    fn build_final_dh1_only_over_cap_rejected() {
853        let dh1 = vec![0u8; MAX_DH_PUB + 1];
854        let dh2 = vec![0u8; MAX_DH_PUB];
855        let sig = vec![0u8; 64];
856        let err = build_final_token(&make_final_input(&dh1, &dh2, &sig)).unwrap_err();
857        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
858    }
859    #[test]
860    fn build_final_dh2_only_over_cap_rejected() {
861        let dh1 = vec![0u8; MAX_DH_PUB];
862        let dh2 = vec![0u8; MAX_DH_PUB + 1];
863        let sig = vec![0u8; 64];
864        let err = build_final_token(&make_final_input(&dh1, &dh2, &sig)).unwrap_err();
865        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
866    }
867
868    /// take_bin: max-Boundary. v.len() == max muss durchgehen,
869    /// v.len() > max muss erroren.
870    #[test]
871    fn take_bin_at_and_over_max() {
872        let mut h = DataHolder::new("test");
873        h.set_binary_property("k", vec![0u8; 100]);
874        // exact-fit:
875        assert!(take_bin(&h, "k", 100).is_ok());
876        // over:
877        let err = take_bin(&h, "k", 99).unwrap_err();
878        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
879    }
880}