Skip to main content

zerodds_security/
token.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! DDS-Security 1.2 Token-Strukturen (DataHolder + Property-Records).
5//!
6//! Spec: §7.2.6 (Property_t / BinaryProperty_t), §7.2.7 (DataHolder),
7//! §7.4.1.4 (IdentityToken im SPDP-Announce).
8//!
9//! Ein **DataHolder** ist die generische Wire-Repraesentation aller
10//! Security-Tokens (IdentityToken, PermissionsToken, IdentityStatus-
11//! Token, AuthRequestMessageToken, HandshakeMessageToken, CryptoToken,
12//! ParticipantCryptoToken etc.). Felder:
13//!
14//! ```text
15//! struct DataHolder_t {
16//!     string                       class_id;
17//!     sequence<Property_t>         properties;
18//!     sequence<BinaryProperty_t>   binary_properties;
19//! };
20//! struct Property_t        { string name; string value; };
21//! struct BinaryProperty_t  { string name; sequence<octet> value; };
22//! ```
23//!
24//! WICHTIG: das Wire-`Property_t` (Spec §7.2.6 Tab.5) hat **kein**
25//! `propagate`-Feld — das ist Local-Filter-State im plug-konfigurierten
26//! `zerodds_security::PropertyList`.
27//!
28//! Encoding ist OMG-CDR (XCDR1) — der DataHolder wird als Big-/Little-
29//! Endian Stream serialisiert, je nachdem in welchem Encapsulation-
30//! Kontext er sitzt (typisch PL_CDR_LE der ParameterList, also LE).
31//!
32//! # Verwendung
33//!
34//! ```
35//! use zerodds_security::token::{DataHolder, IdentityToken};
36//! let tok = IdentityToken::pki_dh_v12("01:23:45", "rsa-2048", "FA:CE", "rsa-2048");
37//! let bytes = tok.to_cdr_le();
38//! let back = DataHolder::from_cdr_le(&bytes).unwrap();
39//! assert_eq!(tok, back);
40//! ```
41
42extern crate alloc;
43
44use alloc::string::{String, ToString};
45use alloc::vec::Vec;
46
47use crate::error::{SecurityError, SecurityErrorKind, SecurityResult};
48
49/// Spec §7.2.6 Tab.5 — Wire-`Property_t` (`name` + `value`, beide
50/// CDR-Strings). Im Gegensatz zu [`crate::Property`] kein `propagate`-
51/// Feld; Token-Properties werden grundsaetzlich propagiert.
52#[derive(Debug, Clone, PartialEq, Eq, Default)]
53pub struct WireProperty {
54    /// Property-Name (reverse-DNS, z.B. `"dds.cert.sn"`).
55    pub name: String,
56    /// Property-Value (UTF-8).
57    pub value: String,
58}
59
60impl WireProperty {
61    /// Konstruktor.
62    #[must_use]
63    pub fn new(name: impl Into<String>, value: impl Into<String>) -> Self {
64        Self {
65            name: name.into(),
66            value: value.into(),
67        }
68    }
69}
70
71/// Spec §7.2.6 Tab.6 — Wire-`BinaryProperty_t` (`name` + `value` als
72/// `sequence<octet>`).
73#[derive(Debug, Clone, PartialEq, Eq, Default)]
74pub struct BinaryProperty {
75    /// Property-Name (reverse-DNS).
76    pub name: String,
77    /// Roher Byte-Wert.
78    pub value: Vec<u8>,
79}
80
81impl BinaryProperty {
82    /// Konstruktor.
83    #[must_use]
84    pub fn new(name: impl Into<String>, value: impl Into<Vec<u8>>) -> Self {
85        Self {
86            name: name.into(),
87            value: value.into(),
88        }
89    }
90}
91
92/// Spec §7.2.7 Tab.7 — generische `DataHolder_t` Wire-Struktur. Alle
93/// Security-Tokens sind Type-Aliases von `DataHolder` mit fixem
94/// `class_id`-Wert.
95#[derive(Debug, Clone, PartialEq, Eq, Default)]
96pub struct DataHolder {
97    /// Plugin-Class-Id, z.B. `"DDS:Auth:PKI-DH:1.2"`.
98    pub class_id: String,
99    /// String-Properties (Spec §7.2.6 Tab.5).
100    pub properties: Vec<WireProperty>,
101    /// Binary-Properties (Spec §7.2.6 Tab.6).
102    pub binary_properties: Vec<BinaryProperty>,
103}
104
105impl DataHolder {
106    /// Konstruktor mit nur `class_id`.
107    #[must_use]
108    pub fn new(class_id: impl Into<String>) -> Self {
109        Self {
110            class_id: class_id.into(),
111            properties: Vec::new(),
112            binary_properties: Vec::new(),
113        }
114    }
115
116    /// Builder: fuegt eine String-Property hinzu.
117    #[must_use]
118    pub fn with_property(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
119        self.properties.push(WireProperty::new(name, value));
120        self
121    }
122
123    /// Builder: fuegt eine Binary-Property hinzu.
124    #[must_use]
125    pub fn with_binary_property(
126        mut self,
127        name: impl Into<String>,
128        value: impl Into<Vec<u8>>,
129    ) -> Self {
130        self.binary_properties
131            .push(BinaryProperty::new(name, value));
132        self
133    }
134
135    /// Mut-Variante: setzt eine String-Property mit Replace-on-Dup-
136    /// Semantik (wenn Name schon existiert, wird der alte Wert
137    /// ueberschrieben — Spec §7.2.6: pro Token darf jeder Property-
138    /// Name nur einmal vorkommen).
139    pub fn set_property(&mut self, name: impl Into<String>, value: impl Into<String>) {
140        let n = name.into();
141        if let Some(existing) = self.properties.iter_mut().find(|p| p.name == n) {
142            existing.value = value.into();
143        } else {
144            self.properties.push(WireProperty {
145                name: n,
146                value: value.into(),
147            });
148        }
149    }
150
151    /// Mut-Variante: setzt eine Binary-Property mit Replace-on-Dup.
152    pub fn set_binary_property(&mut self, name: impl Into<String>, value: impl Into<Vec<u8>>) {
153        let n = name.into();
154        if let Some(existing) = self.binary_properties.iter_mut().find(|p| p.name == n) {
155            existing.value = value.into();
156        } else {
157            self.binary_properties.push(BinaryProperty {
158                name: n,
159                value: value.into(),
160            });
161        }
162    }
163
164    /// Sucht eine String-Property nach Name.
165    #[must_use]
166    pub fn property(&self, name: &str) -> Option<&str> {
167        self.properties
168            .iter()
169            .find(|p| p.name == name)
170            .map(|p| p.value.as_str())
171    }
172
173    /// Sucht eine Binary-Property nach Name.
174    #[must_use]
175    pub fn binary_property(&self, name: &str) -> Option<&[u8]> {
176        self.binary_properties
177            .iter()
178            .find(|p| p.name == name)
179            .map(|p| p.value.as_slice())
180    }
181
182    /// XCDR1-Little-Endian-Encoder. Liefert die Bytes ohne
183    /// Encapsulation-Header — der wird vom Caller (z.B. ParameterList-
184    /// Wert) bereitgestellt.
185    #[must_use]
186    pub fn to_cdr_le(&self) -> Vec<u8> {
187        encode(self, true)
188    }
189
190    /// XCDR1-Big-Endian-Encoder.
191    #[must_use]
192    pub fn to_cdr_be(&self) -> Vec<u8> {
193        encode(self, false)
194    }
195
196    /// XCDR1-Decoder, Little-Endian.
197    ///
198    /// # Errors
199    /// `BadArgument` wenn die Bytes nicht spec-konform sind (z.B.
200    /// vorzeitiges Ende, zu lange Length-Prefixes).
201    pub fn from_cdr_le(bytes: &[u8]) -> SecurityResult<Self> {
202        decode(bytes, true)
203    }
204
205    /// XCDR1-Decoder, Big-Endian.
206    ///
207    /// # Errors
208    /// `BadArgument` wenn die Bytes nicht spec-konform sind.
209    pub fn from_cdr_be(bytes: &[u8]) -> SecurityResult<Self> {
210        decode(bytes, false)
211    }
212}
213
214// ---------------------------------------------------------------------
215// Type-Aliases fuer die Token-Familien (Spec-Bezeichnung / class_id)
216// ---------------------------------------------------------------------
217
218/// IdentityToken (Spec §7.4.1.4 Tab.16; PKI-DH §10.3.2 Tab.51).
219/// Wird im SPDP-Announce als `PID_IDENTITY_TOKEN` (0x1001) verschickt.
220pub type IdentityToken = DataHolder;
221
222/// PermissionsToken (Spec §7.4.1.5 Tab.17; PKI-Permissions §10.4.2
223/// Tab.65). `PID_PERMISSIONS_TOKEN` (0x1002).
224pub type PermissionsToken = DataHolder;
225
226/// IdentityStatusToken (Spec §7.4.1.6, §10.3.2 Tab.53). `PID_IDENTITY_
227/// STATUS_TOKEN` (0x1006). Traeger fuer OCSP-Live-Status.
228pub type IdentityStatusToken = DataHolder;
229
230/// PermissionsCredentialToken (Spec §10.4.2 Tab.64). Wird im
231/// Stateless-Topic-Handshake transportiert, nicht im SPDP.
232pub type PermissionsCredentialToken = DataHolder;
233
234/// AuthRequestMessageToken (Spec §10.3.2 Tab.55).
235pub type AuthRequestMessageToken = DataHolder;
236
237/// HandshakeMessageToken — Request/Reply/Final (Spec §10.3.2 Tab.56-58).
238pub type HandshakeMessageToken = DataHolder;
239
240/// CryptoToken (Spec §10.5.2 Tab.72).
241pub type CryptoToken = DataHolder;
242
243// ---------------------------------------------------------------------
244// Convenience-Konstruktoren fuer die typischen Token-Auspraegungen.
245// ---------------------------------------------------------------------
246
247/// Plugin-Class-Id-Konstanten — Spec-1.2-Versioniert (siehe C3.3).
248pub mod class_id {
249    /// Builtin-PKI-Authentication (§10.3.2.1 Tab.51).
250    pub const AUTH_PKI_DH_V12: &str = "DDS:Auth:PKI-DH:1.2";
251    /// Builtin-Permissions Access-Control (§10.4 Tab.69).
252    pub const ACCESS_PERMISSIONS_V12: &str = "DDS:Access:Permissions:1.2";
253    /// Builtin-AES-GCM-GMAC Crypto (§10.5 Tab.72).
254    pub const CRYPTO_AES_GCM_GMAC_V12: &str = "DDS:Crypto:AES-GCM-GMAC:1.2";
255    /// Permissions-Credential (§10.4.2 Tab.64).
256    pub const ACCESS_PERMISSIONS_CREDENTIAL: &str = "DDS:Access:PermissionsCredential";
257}
258
259/// Property-Namen — Spec-1.2 (Auswahl).
260pub mod prop {
261    /// IdentityToken: Cert-Subject-Name-Serial. Spec §10.3.2.1 Tab.51.
262    pub const CERT_SN: &str = "dds.cert.sn";
263    /// IdentityToken: Cert-Signature-Algorithmus.
264    pub const CERT_ALGO: &str = "dds.cert.algo";
265    /// IdentityToken: CA-Subject-Name-Serial.
266    pub const CA_SN: &str = "dds.ca.sn";
267    /// IdentityToken: CA-Signature-Algorithmus.
268    pub const CA_ALGO: &str = "dds.ca.algo";
269    /// PermissionsToken: Permissions-CA-Subject-Name-Serial.
270    /// Spec §10.4.2 Tab.65.
271    pub const PERM_CA_SN: &str = "dds.perm_ca.sn";
272    /// PermissionsToken: Permissions-CA-Signature-Algorithmus.
273    pub const PERM_CA_ALGO: &str = "dds.perm_ca.algo";
274}
275
276impl IdentityToken {
277    /// Builtin-Helfer fuer den PKI-DH-IdentityToken (§10.3.2.1 Tab.51).
278    /// Properties `dds.cert.sn`, `dds.cert.algo`, `dds.ca.sn`,
279    /// `dds.ca.algo`. Keine binary_properties.
280    #[must_use]
281    pub fn pki_dh_v12(
282        cert_sn: impl Into<String>,
283        cert_algo: impl Into<String>,
284        ca_sn: impl Into<String>,
285        ca_algo: impl Into<String>,
286    ) -> Self {
287        DataHolder::new(class_id::AUTH_PKI_DH_V12)
288            .with_property(prop::CERT_SN, cert_sn)
289            .with_property(prop::CERT_ALGO, cert_algo)
290            .with_property(prop::CA_SN, ca_sn)
291            .with_property(prop::CA_ALGO, ca_algo)
292    }
293
294    /// Builtin-Helfer fuer den Permissions-Token (§10.4.2 Tab.65).
295    /// `class_id="DDS:Access:Permissions:1.2"`, properties
296    /// `dds.perm_ca.sn` + `dds.perm_ca.algo`.
297    #[must_use]
298    pub fn permissions_v12(perm_ca_sn: impl Into<String>, perm_ca_algo: impl Into<String>) -> Self {
299        DataHolder::new(class_id::ACCESS_PERMISSIONS_V12)
300            .with_property(prop::PERM_CA_SN, perm_ca_sn)
301            .with_property(prop::PERM_CA_ALGO, perm_ca_algo)
302    }
303}
304
305// ---------------------------------------------------------------------
306// XCDR1-Codec
307// ---------------------------------------------------------------------
308//
309// Layout (LE oder BE, je nach `little_endian`):
310//   string class_id              -- aligned 4
311//   sequence<Property> props     -- aligned 4
312//     uint32 count               -- 4 byte
313//     N x { string name; string value; }  -- aligned 4 each
314//   sequence<BinaryProperty> bp  -- aligned 4
315//     uint32 count
316//     N x { string name; sequence<octet> value; }  -- aligned 4 each
317//
318// CDR-String: uint32 length (incl. trailing \0) + UTF-8 bytes + \0 +
319// padding to 4 byte. Length=0 ist erlaubt (= leerer String).
320
321/// Maximale Zaehler/-Laengen die wir aus der Wire akzeptieren —
322/// DoS-Cap. 64 KiB pro Token, 256 Properties, 256 Binary-Properties,
323/// 8 KiB pro Property-Wert.
324const MAX_TOKEN_BYTES: usize = 64 * 1024;
325const MAX_PROPS: u32 = 256;
326const MAX_BIN_PROPS: u32 = 256;
327const MAX_STRING_LEN: u32 = 8 * 1024;
328const MAX_BINARY_LEN: u32 = 8 * 1024;
329
330fn encode(d: &DataHolder, le: bool) -> Vec<u8> {
331    let mut out = Vec::with_capacity(64);
332    encode_string(&mut out, &d.class_id, le);
333    encode_u32(&mut out, d.properties.len() as u32, le);
334    for p in &d.properties {
335        encode_string(&mut out, &p.name, le);
336        encode_string(&mut out, &p.value, le);
337    }
338    encode_u32(&mut out, d.binary_properties.len() as u32, le);
339    for p in &d.binary_properties {
340        encode_string(&mut out, &p.name, le);
341        encode_octet_seq(&mut out, &p.value, le);
342    }
343    out
344}
345
346fn decode(bytes: &[u8], le: bool) -> SecurityResult<DataHolder> {
347    if bytes.len() > MAX_TOKEN_BYTES {
348        return Err(SecurityError::new(
349            SecurityErrorKind::BadArgument,
350            "token: payload exceeds DoS cap",
351        ));
352    }
353    let mut cur = Cursor::new(bytes);
354    let class_id = cur.read_string(le)?;
355    let prop_count = cur.read_u32(le)?;
356    if prop_count > MAX_PROPS {
357        return Err(SecurityError::new(
358            SecurityErrorKind::BadArgument,
359            "token: property count exceeds cap",
360        ));
361    }
362    let mut properties = Vec::with_capacity(prop_count as usize);
363    for _ in 0..prop_count {
364        let name = cur.read_string(le)?;
365        let value = cur.read_string(le)?;
366        properties.push(WireProperty { name, value });
367    }
368    let bin_count = cur.read_u32(le)?;
369    if bin_count > MAX_BIN_PROPS {
370        return Err(SecurityError::new(
371            SecurityErrorKind::BadArgument,
372            "token: binary_property count exceeds cap",
373        ));
374    }
375    let mut binary_properties = Vec::with_capacity(bin_count as usize);
376    for _ in 0..bin_count {
377        let name = cur.read_string(le)?;
378        let value = cur.read_octet_seq(le)?;
379        binary_properties.push(BinaryProperty { name, value });
380    }
381    Ok(DataHolder {
382        class_id,
383        properties,
384        binary_properties,
385    })
386}
387
388fn align_to(out: &mut Vec<u8>, n: usize) {
389    let pad = (n - out.len() % n) % n;
390    for _ in 0..pad {
391        out.push(0);
392    }
393}
394
395fn encode_u32(out: &mut Vec<u8>, v: u32, le: bool) {
396    align_to(out, 4);
397    if le {
398        out.extend_from_slice(&v.to_le_bytes());
399    } else {
400        out.extend_from_slice(&v.to_be_bytes());
401    }
402}
403
404fn encode_string(out: &mut Vec<u8>, s: &str, le: bool) {
405    let bytes = s.as_bytes();
406    let len = (bytes.len() + 1) as u32;
407    encode_u32(out, len, le);
408    out.extend_from_slice(bytes);
409    out.push(0);
410}
411
412fn encode_octet_seq(out: &mut Vec<u8>, v: &[u8], le: bool) {
413    encode_u32(out, v.len() as u32, le);
414    out.extend_from_slice(v);
415}
416
417struct Cursor<'a> {
418    buf: &'a [u8],
419    pos: usize,
420}
421
422impl<'a> Cursor<'a> {
423    fn new(buf: &'a [u8]) -> Self {
424        Self { buf, pos: 0 }
425    }
426
427    fn align_to(&mut self, n: usize) {
428        let pad = (n - self.pos % n) % n;
429        self.pos = self.pos.saturating_add(pad);
430    }
431
432    fn read_u32(&mut self, le: bool) -> SecurityResult<u32> {
433        self.align_to(4);
434        if self.pos + 4 > self.buf.len() {
435            return Err(SecurityError::new(
436                SecurityErrorKind::BadArgument,
437                "token: truncated u32",
438            ));
439        }
440        let raw = [
441            self.buf[self.pos],
442            self.buf[self.pos + 1],
443            self.buf[self.pos + 2],
444            self.buf[self.pos + 3],
445        ];
446        self.pos += 4;
447        Ok(if le {
448            u32::from_le_bytes(raw)
449        } else {
450            u32::from_be_bytes(raw)
451        })
452    }
453
454    fn read_string(&mut self, le: bool) -> SecurityResult<String> {
455        let len = self.read_u32(le)?;
456        if len > MAX_STRING_LEN {
457            return Err(SecurityError::new(
458                SecurityErrorKind::BadArgument,
459                "token: string exceeds cap",
460            ));
461        }
462        if len == 0 {
463            return Ok(String::new());
464        }
465        if self.pos + len as usize > self.buf.len() {
466            return Err(SecurityError::new(
467                SecurityErrorKind::BadArgument,
468                "token: truncated string",
469            ));
470        }
471        let body = &self.buf[self.pos..self.pos + len as usize];
472        self.pos += len as usize;
473        // Spec: trailing NUL ist Pflicht; UTF-8 muss valide sein.
474        if let Some((_, rest)) = body.split_last() {
475            if body.last() != Some(&0) {
476                return Err(SecurityError::new(
477                    SecurityErrorKind::BadArgument,
478                    "token: string missing trailing NUL",
479                ));
480            }
481            let s = core::str::from_utf8(rest).map_err(|_| {
482                SecurityError::new(SecurityErrorKind::BadArgument, "token: string not UTF-8")
483            })?;
484            Ok(s.to_string())
485        } else {
486            Err(SecurityError::new(
487                SecurityErrorKind::BadArgument,
488                "token: zero-length string body",
489            ))
490        }
491    }
492
493    fn read_octet_seq(&mut self, le: bool) -> SecurityResult<Vec<u8>> {
494        let len = self.read_u32(le)?;
495        if len > MAX_BINARY_LEN {
496            return Err(SecurityError::new(
497                SecurityErrorKind::BadArgument,
498                "token: binary value exceeds cap",
499            ));
500        }
501        if self.pos + len as usize > self.buf.len() {
502            return Err(SecurityError::new(
503                SecurityErrorKind::BadArgument,
504                "token: truncated binary",
505            ));
506        }
507        let v = self.buf[self.pos..self.pos + len as usize].to_vec();
508        self.pos += len as usize;
509        Ok(v)
510    }
511}
512
513#[cfg(test)]
514#[allow(clippy::expect_used, clippy::unwrap_used)]
515mod tests {
516    use super::*;
517
518    #[test]
519    fn empty_data_holder_roundtrip_le() {
520        let dh = DataHolder::new("DDS:Auth:PKI-DH:1.2");
521        let bytes = dh.to_cdr_le();
522        let back = DataHolder::from_cdr_le(&bytes).unwrap();
523        assert_eq!(dh, back);
524    }
525
526    #[test]
527    fn empty_data_holder_roundtrip_be() {
528        let dh = DataHolder::new("DDS:Auth:PKI-DH:1.2");
529        let bytes = dh.to_cdr_be();
530        let back = DataHolder::from_cdr_be(&bytes).unwrap();
531        assert_eq!(dh, back);
532    }
533
534    #[test]
535    fn pki_dh_identity_token_roundtrip() {
536        let tok =
537            IdentityToken::pki_dh_v12("01:23:45:67", "ECDSA-SHA256", "FA:CE:0B:01", "RSA-SHA256");
538        let bytes = tok.to_cdr_le();
539        let back = DataHolder::from_cdr_le(&bytes).unwrap();
540        assert_eq!(tok, back);
541        assert_eq!(back.class_id, "DDS:Auth:PKI-DH:1.2");
542        assert_eq!(back.property("dds.cert.sn"), Some("01:23:45:67"));
543        assert_eq!(back.property("dds.cert.algo"), Some("ECDSA-SHA256"));
544        assert_eq!(back.property("dds.ca.sn"), Some("FA:CE:0B:01"));
545        assert_eq!(back.property("dds.ca.algo"), Some("RSA-SHA256"));
546        assert!(back.binary_properties.is_empty());
547    }
548
549    #[test]
550    fn permissions_token_roundtrip() {
551        let tok = IdentityToken::permissions_v12("DE:AD:BE:EF", "ECDSA-SHA256");
552        let le = tok.to_cdr_le();
553        let be = tok.to_cdr_be();
554        assert_eq!(tok, DataHolder::from_cdr_le(&le).unwrap());
555        assert_eq!(tok, DataHolder::from_cdr_be(&be).unwrap());
556        assert_ne!(le, be, "BE/LE Streams unterscheiden sich");
557    }
558
559    #[test]
560    fn token_with_binary_property_roundtrip() {
561        let tok = DataHolder::new("DDS:Auth:PKI-DH:1.2")
562            .with_property("dds.cert.sn", "01:23")
563            .with_binary_property("dds.cert.bytes", vec![0xCA, 0xFE, 0xBA, 0xBE, 0xDE]);
564        let bytes = tok.to_cdr_le();
565        let back = DataHolder::from_cdr_le(&bytes).unwrap();
566        assert_eq!(tok, back);
567        assert_eq!(
568            back.binary_property("dds.cert.bytes"),
569            Some(&[0xCA, 0xFE, 0xBA, 0xBE, 0xDE][..])
570        );
571    }
572
573    #[test]
574    fn cdr_le_layout_class_id_only() {
575        // class_id="A" (1 char + NUL = len=2). Expected stream:
576        //   uint32 len=2 LE: 02 00 00 00
577        //   bytes        :  41 00
578        //   pad-to-4     :  00 00
579        //   prop_count=0 :  00 00 00 00
580        //   bin_count=0  :  00 00 00 00
581        let dh = DataHolder::new("A");
582        let bytes = dh.to_cdr_le();
583        assert_eq!(
584            bytes,
585            vec![
586                0x02, 0x00, 0x00, 0x00, b'A', 0x00, 0x00, 0x00, 0, 0, 0, 0, 0, 0, 0, 0
587            ]
588        );
589    }
590
591    #[test]
592    fn truncated_buffer_is_error() {
593        let err = DataHolder::from_cdr_le(&[0x10, 0x00, 0x00]).unwrap_err();
594        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
595    }
596
597    #[test]
598    fn property_count_cap_rejects_huge() {
599        // Forge a stream with prop_count = 1_000_000.
600        let mut bytes = Vec::new();
601        encode_string(&mut bytes, "X", true);
602        encode_u32(&mut bytes, 1_000_000, true); // prop count > cap
603        let err = DataHolder::from_cdr_le(&bytes).unwrap_err();
604        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
605    }
606
607    #[test]
608    fn missing_trailing_nul_rejected() {
609        // Forge: class_id len=1 (no NUL).
610        let bytes = vec![0x01, 0x00, 0x00, 0x00, b'A'];
611        let err = DataHolder::from_cdr_le(&bytes).unwrap_err();
612        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
613    }
614
615    #[test]
616    fn dos_cap_overall_payload() {
617        let big = vec![0u8; MAX_TOKEN_BYTES + 1];
618        let err = DataHolder::from_cdr_le(&big).unwrap_err();
619        assert_eq!(err.kind, SecurityErrorKind::BadArgument);
620    }
621}