Skip to main content

zerodds_security_pki/
delegation.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Delegation-Link / Delegation-Chain.
5//!
6//! Datenmodell + Sign/Verify fuer kryptographische Delegations-Ketten.
7//! Architektur-Referenz: `docs/architecture/09_delegation.md` §5.
8//!
9//! Ein [`DelegationLink`] traegt eine signierte Aussage:
10//! "Delegator (mit X.509-Cert) erlaubt Delegatee (identifiziert via
11//! `delegatee_guid`), in einem definierten Zeit- und Scope-Fenster
12//! Samples zu schreiben/lesen."
13//!
14//! Eine [`DelegationChain`] verkettet 1..N solcher Links zu einer
15//! Beweisreihe Origin → Edge. Jeder Link wird vom **vorigen
16//! Delegatee** signiert (Initial-Link vom Trust-Anchor selbst).
17//!
18//! # Layout (deterministisch fuer Signing-Input)
19//!
20//! ```text
21//! magic        = b"ZERODDSD" (8 byte)
22//! version      = 1 (u8)
23//! delegator    = 16 byte GUID
24//! delegatee    = 16 byte GUID
25//! not_before   = i64 big-endian (Unix-Sekunden)
26//! not_after    = i64 big-endian
27//! algorithm    = u8 (siehe SignatureAlgorithm::wire_id)
28//! n_topics     = u32 big-endian
29//! [u32_be len + utf8_bytes] * n_topics
30//! n_partitions = u32 big-endian
31//! [u32_be len + utf8_bytes] * n_partitions
32//! ```
33//!
34//! Signature-Layout: derselbe Input wie oben (ohne `signature`-Suffix),
35//! Hash impliziert durch `SignatureAlgorithm` (SHA-256 fuer P-256 +
36//! Ed25519, SHA-384 fuer P-384, SHA-256 fuer RSA-PSS-2048).
37//!
38//! zerodds-lint: allow no_dyn_in_safe
39//! (Cert-Validation via `rustls-webpki` benoetigt object-safe Trait-Refs.)
40
41extern crate alloc;
42
43use alloc::string::{String, ToString};
44use alloc::vec::Vec;
45
46use ring::rand::SystemRandom;
47use ring::signature::{
48    self, ECDSA_P256_SHA256_FIXED, ECDSA_P384_SHA384_FIXED, ED25519, RSA_PSS_2048_8192_SHA256,
49    UnparsedPublicKey,
50};
51
52/// Signatur-Algorithmus fuer Delegation-Links.
53///
54/// Default-Empfehlung: [`SignatureAlgorithm::EcdsaP256`] — kompakte
55/// Signatur (~64 byte), schnelle Verify auf Edge-Hardware.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57#[non_exhaustive]
58pub enum SignatureAlgorithm {
59    /// ECDSA mit P-256 + SHA-256, fixed-length encoding (64 byte sig).
60    EcdsaP256,
61    /// ECDSA mit P-384 + SHA-384, fixed-length encoding (96 byte sig).
62    EcdsaP384,
63    /// RSA-PSS mit 2048-bit Key + SHA-256 (256 byte sig).
64    RsaPss2048,
65    /// Ed25519 (64 byte sig).
66    Ed25519,
67}
68
69impl SignatureAlgorithm {
70    /// Wire-Id fuer das deterministische Sign-Input-Layout.
71    #[must_use]
72    pub const fn wire_id(self) -> u8 {
73        match self {
74            Self::EcdsaP256 => 1,
75            Self::EcdsaP384 => 2,
76            Self::RsaPss2048 => 3,
77            Self::Ed25519 => 4,
78        }
79    }
80
81    /// Decode aus Wire-Id; `None` bei unknown-Algorithm.
82    #[must_use]
83    pub const fn from_wire_id(id: u8) -> Option<Self> {
84        match id {
85            1 => Some(Self::EcdsaP256),
86            2 => Some(Self::EcdsaP384),
87            3 => Some(Self::RsaPss2048),
88            4 => Some(Self::Ed25519),
89            _ => None,
90        }
91    }
92
93    /// Erwartete Signatur-Laenge in Bytes (fixed) oder `None` fuer
94    /// variabel (RSA-PSS abhaengig von Key-Size — wir fixieren auf 2048
95    /// und damit 256 byte).
96    #[must_use]
97    pub const fn expected_signature_len(self) -> Option<usize> {
98        match self {
99            Self::EcdsaP256 | Self::Ed25519 => Some(64),
100            Self::EcdsaP384 => Some(96),
101            Self::RsaPss2048 => Some(256),
102        }
103    }
104}
105
106/// Magic-Bytes fuer das Sign-Input-Layout (8 byte).
107pub const DELEGATION_MAGIC: &[u8; 8] = b"ZERODDSD";
108
109/// Wire-Layout-Version.
110pub const DELEGATION_VERSION: u8 = 1;
111
112/// Maximale Anzahl Topic-Patterns pro Link (DoS-Cap).
113pub const MAX_TOPIC_PATTERNS: usize = 64;
114/// Maximale Anzahl Partition-Patterns pro Link (DoS-Cap).
115pub const MAX_PARTITION_PATTERNS: usize = 64;
116/// Maximale Pattern-String-Laenge in Bytes.
117pub const MAX_PATTERN_LEN: usize = 256;
118
119/// Errors aus dem Delegation-Modul.
120#[derive(Debug, Clone, PartialEq, Eq)]
121#[non_exhaustive]
122pub enum DelegationError {
123    /// Pattern-Liste ist groesser als der DoS-Cap.
124    TooManyPatterns {
125        /// Welche Liste — `"topic"` oder `"partition"`.
126        kind: &'static str,
127        /// Tatsaechliche Anzahl.
128        count: usize,
129        /// Erlaubtes Maximum.
130        max: usize,
131    },
132    /// Pattern ist laenger als der String-Cap.
133    PatternTooLong {
134        /// Tatsaechliche Laenge.
135        len: usize,
136        /// Erlaubtes Maximum.
137        max: usize,
138    },
139    /// Sign-Operation fehlgeschlagen (z.B. ungueltiger Key).
140    SignFailed(String),
141    /// Verify-Operation fehlgeschlagen.
142    VerifyFailed(String),
143    /// Wire-Bytes konnten nicht decodiert werden.
144    Malformed(String),
145    /// Unsupported Wire-Algorithm-Id.
146    UnknownAlgorithm(u8),
147    /// `not_before > not_after`.
148    InvalidTimeWindow,
149    /// Magic-Bytes stimmen nicht.
150    BadMagic,
151    /// Wire-Layout-Version unbekannt.
152    UnsupportedVersion(u8),
153}
154
155impl core::fmt::Display for DelegationError {
156    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
157        match self {
158            Self::TooManyPatterns { kind, count, max } => {
159                write!(f, "{kind} patterns: {count} > max {max}")
160            }
161            Self::PatternTooLong { len, max } => {
162                write!(f, "pattern length {len} > max {max}")
163            }
164            Self::SignFailed(s) => write!(f, "sign failed: {s}"),
165            Self::VerifyFailed(s) => write!(f, "verify failed: {s}"),
166            Self::Malformed(s) => write!(f, "malformed delegation: {s}"),
167            Self::UnknownAlgorithm(id) => write!(f, "unknown algorithm id: {id}"),
168            Self::InvalidTimeWindow => write!(f, "not_before > not_after"),
169            Self::BadMagic => write!(f, "bad magic bytes"),
170            Self::UnsupportedVersion(v) => write!(f, "unsupported version: {v}"),
171        }
172    }
173}
174
175#[cfg(feature = "std")]
176impl std::error::Error for DelegationError {}
177
178/// Result-Alias fuer Delegation-Operationen.
179pub type DelegationResult<T> = Result<T, DelegationError>;
180
181/// Eine einzelne Delegation-Aussage (signiert vom Delegator).
182///
183/// Architektur-Referenz: `09_delegation.md` §5.1.
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct DelegationLink {
186    /// 16-byte Participant-GUID des Delegators (= Eigentuemer des
187    /// Signing-Keys, gleichzeitig Subject des X.509-Certs, das den
188    /// Verify-Key liefert).
189    pub delegator_guid: [u8; 16],
190    /// 16-byte Participant-GUID des Delegatees (= Edge-Peer, der ueber
191    /// diesen Link Berechtigung erbt).
192    pub delegatee_guid: [u8; 16],
193    /// Erlaubte Topic-Glob-Patterns. Leere Liste = "alle" (nur in
194    /// Initial-Link, sonst Validation-Fail).
195    pub allowed_topic_patterns: Vec<String>,
196    /// Erlaubte Partition-Patterns. Leere Liste = Default-Partition.
197    pub allowed_partition_patterns: Vec<String>,
198    /// Unix-Sekunden ab wann der Link gilt.
199    pub not_before: i64,
200    /// Unix-Sekunden bis wann der Link gilt.
201    pub not_after: i64,
202    /// Signatur-Algorithmus, mit dem der Link signiert ist.
203    pub algorithm: SignatureAlgorithm,
204    /// Signatur-Bytes (Layout abhaengig von `algorithm`).
205    pub signature: Vec<u8>,
206}
207
208impl DelegationLink {
209    /// Erzeugt einen unsigned Link (signature leer). Nutze
210    /// [`Self::sign`] zum Signieren.
211    ///
212    /// # Errors
213    /// * [`DelegationError::TooManyPatterns`] wenn `topic_patterns` oder
214    ///   `partition_patterns` den DoS-Cap ueberschreiten.
215    /// * [`DelegationError::PatternTooLong`] wenn ein Einzel-Pattern den
216    ///   String-Cap ueberschreitet.
217    /// * [`DelegationError::InvalidTimeWindow`] wenn
218    ///   `not_before > not_after`.
219    pub fn new(
220        delegator_guid: [u8; 16],
221        delegatee_guid: [u8; 16],
222        allowed_topic_patterns: Vec<String>,
223        allowed_partition_patterns: Vec<String>,
224        not_before: i64,
225        not_after: i64,
226        algorithm: SignatureAlgorithm,
227    ) -> DelegationResult<Self> {
228        if allowed_topic_patterns.len() > MAX_TOPIC_PATTERNS {
229            return Err(DelegationError::TooManyPatterns {
230                kind: "topic",
231                count: allowed_topic_patterns.len(),
232                max: MAX_TOPIC_PATTERNS,
233            });
234        }
235        if allowed_partition_patterns.len() > MAX_PARTITION_PATTERNS {
236            return Err(DelegationError::TooManyPatterns {
237                kind: "partition",
238                count: allowed_partition_patterns.len(),
239                max: MAX_PARTITION_PATTERNS,
240            });
241        }
242        for p in allowed_topic_patterns
243            .iter()
244            .chain(allowed_partition_patterns.iter())
245        {
246            if p.len() > MAX_PATTERN_LEN {
247                return Err(DelegationError::PatternTooLong {
248                    len: p.len(),
249                    max: MAX_PATTERN_LEN,
250                });
251            }
252        }
253        if not_before > not_after {
254            return Err(DelegationError::InvalidTimeWindow);
255        }
256        Ok(Self {
257            delegator_guid,
258            delegatee_guid,
259            allowed_topic_patterns,
260            allowed_partition_patterns,
261            not_before,
262            not_after,
263            algorithm,
264            signature: Vec::new(),
265        })
266    }
267
268    /// Sign-Input (deterministisch). Identisch fuer Sign + Verify.
269    #[must_use]
270    pub fn signing_bytes(&self) -> Vec<u8> {
271        let mut buf = Vec::with_capacity(64 + 32 * self.allowed_topic_patterns.len());
272        buf.extend_from_slice(DELEGATION_MAGIC);
273        buf.push(DELEGATION_VERSION);
274        buf.extend_from_slice(&self.delegator_guid);
275        buf.extend_from_slice(&self.delegatee_guid);
276        buf.extend_from_slice(&self.not_before.to_be_bytes());
277        buf.extend_from_slice(&self.not_after.to_be_bytes());
278        buf.push(self.algorithm.wire_id());
279
280        // Topic-Patterns.
281        let n_topic = u32::try_from(self.allowed_topic_patterns.len()).unwrap_or(u32::MAX);
282        buf.extend_from_slice(&n_topic.to_be_bytes());
283        for p in &self.allowed_topic_patterns {
284            let len = u32::try_from(p.len()).unwrap_or(u32::MAX);
285            buf.extend_from_slice(&len.to_be_bytes());
286            buf.extend_from_slice(p.as_bytes());
287        }
288
289        // Partition-Patterns.
290        let n_part = u32::try_from(self.allowed_partition_patterns.len()).unwrap_or(u32::MAX);
291        buf.extend_from_slice(&n_part.to_be_bytes());
292        for p in &self.allowed_partition_patterns {
293            let len = u32::try_from(p.len()).unwrap_or(u32::MAX);
294            buf.extend_from_slice(&len.to_be_bytes());
295            buf.extend_from_slice(p.as_bytes());
296        }
297
298        buf
299    }
300
301    /// Signiert den Link mit `signing_key`. `signing_key`-Format:
302    /// * `EcdsaP256` / `EcdsaP384` — PKCS#8-DER ECDSA-Privatekey.
303    /// * `RsaPss2048` — PKCS#8-DER RSA-Privatekey.
304    /// * `Ed25519` — PKCS#8-DER Ed25519-Privatekey (rfc8410-Format).
305    ///
306    /// Setzt `self.signature` auf den Sign-Output.
307    ///
308    /// # Errors
309    /// [`DelegationError::SignFailed`] bei Key-Parsing-Fehler oder
310    /// `ring`-internem Fehlschlag.
311    pub fn sign(&mut self, signing_key_pkcs8: &[u8]) -> DelegationResult<()> {
312        let input = self.signing_bytes();
313        let sig = match self.algorithm {
314            SignatureAlgorithm::EcdsaP256 => sign_ecdsa(
315                &signature::ECDSA_P256_SHA256_FIXED_SIGNING,
316                signing_key_pkcs8,
317                &input,
318            )?,
319            SignatureAlgorithm::EcdsaP384 => sign_ecdsa(
320                &signature::ECDSA_P384_SHA384_FIXED_SIGNING,
321                signing_key_pkcs8,
322                &input,
323            )?,
324            SignatureAlgorithm::RsaPss2048 => sign_rsa_pss(signing_key_pkcs8, &input)?,
325            SignatureAlgorithm::Ed25519 => sign_ed25519(signing_key_pkcs8, &input)?,
326        };
327        self.signature = sig;
328        Ok(())
329    }
330
331    /// Verifiziert den Link gegen einen **Public-Key-DER**, dessen
332    /// Format pro Algorithmus unterschiedlich ist:
333    /// * `EcdsaP256`/`EcdsaP384` — `SubjectPublicKeyInfo`-Bytes
334    ///   (Uncompressed-Point-Encoding nach SEC1 §2.3.3).
335    /// * `RsaPss2048` — `SubjectPublicKeyInfo`-Bytes (RFC 8017).
336    /// * `Ed25519` — 32 byte raw-Public-Key.
337    ///
338    /// In der Praxis wird der Verify-Key aus dem **Delegator-X.509-Cert**
339    /// extrahiert (siehe `delegation_check.rs` in delegation_check (security-permissions)).
340    ///
341    /// # Errors
342    /// * [`DelegationError::VerifyFailed`] bei Signatur-Mismatch oder
343    ///   Public-Key-Parse-Fehler.
344    /// * [`DelegationError::SignFailed`] wenn `signature` leer ist.
345    pub fn verify(&self, verify_public_key: &[u8]) -> DelegationResult<()> {
346        if self.signature.is_empty() {
347            return Err(DelegationError::SignFailed("empty signature".to_string()));
348        }
349        if let Some(expected_len) = self.algorithm.expected_signature_len() {
350            if self.signature.len() != expected_len {
351                return Err(DelegationError::VerifyFailed(alloc::format!(
352                    "sig len {} != expected {}",
353                    self.signature.len(),
354                    expected_len
355                )));
356            }
357        }
358        let input = self.signing_bytes();
359        let alg: &dyn signature::VerificationAlgorithm = match self.algorithm {
360            SignatureAlgorithm::EcdsaP256 => &ECDSA_P256_SHA256_FIXED,
361            SignatureAlgorithm::EcdsaP384 => &ECDSA_P384_SHA384_FIXED,
362            SignatureAlgorithm::RsaPss2048 => &RSA_PSS_2048_8192_SHA256,
363            SignatureAlgorithm::Ed25519 => &ED25519,
364        };
365        let pk = UnparsedPublicKey::new(alg, verify_public_key);
366        pk.verify(&input, &self.signature)
367            .map_err(|_| DelegationError::VerifyFailed("ring::verify".to_string()))
368    }
369
370    /// Wire-Encoding eines kompletten Links (Sign-Input + Signatur-Suffix).
371    ///
372    /// Layout = `signing_bytes()` || `u16_be(sig_len)` || `signature`.
373    #[must_use]
374    pub fn encode(&self) -> Vec<u8> {
375        let mut buf = self.signing_bytes();
376        let sig_len = u16::try_from(self.signature.len()).unwrap_or(u16::MAX);
377        buf.extend_from_slice(&sig_len.to_be_bytes());
378        buf.extend_from_slice(&self.signature);
379        buf
380    }
381
382    /// Decode eines kompletten Links.
383    ///
384    /// Konsumiert exakt soviele Bytes wie der Link gross ist; gibt den
385    /// Rest des Slices als `tail` zurueck (Chain-Decoder reicht das
386    /// weiter an den naechsten Link).
387    ///
388    /// # Errors
389    /// [`DelegationError::Malformed`] wenn das Layout nicht stimmt.
390    pub fn decode(buf: &[u8]) -> DelegationResult<(Self, &[u8])> {
391        let mut p = 0usize;
392        let need = |needed: usize, p: usize, buf: &[u8]| -> DelegationResult<()> {
393            if buf.len() < p + needed {
394                return Err(DelegationError::Malformed(alloc::format!(
395                    "truncated at offset {p}, needed {needed} bytes"
396                )));
397            }
398            Ok(())
399        };
400
401        need(8, p, buf)?;
402        if &buf[p..p + 8] != DELEGATION_MAGIC {
403            return Err(DelegationError::BadMagic);
404        }
405        p += 8;
406
407        need(1, p, buf)?;
408        let version = buf[p];
409        if version != DELEGATION_VERSION {
410            return Err(DelegationError::UnsupportedVersion(version));
411        }
412        p += 1;
413
414        need(16, p, buf)?;
415        let mut delegator_guid = [0u8; 16];
416        delegator_guid.copy_from_slice(&buf[p..p + 16]);
417        p += 16;
418
419        need(16, p, buf)?;
420        let mut delegatee_guid = [0u8; 16];
421        delegatee_guid.copy_from_slice(&buf[p..p + 16]);
422        p += 16;
423
424        need(8, p, buf)?;
425        let not_before = i64::from_be_bytes(buf[p..p + 8].try_into().unwrap_or([0u8; 8]));
426        p += 8;
427
428        need(8, p, buf)?;
429        let not_after = i64::from_be_bytes(buf[p..p + 8].try_into().unwrap_or([0u8; 8]));
430        p += 8;
431
432        need(1, p, buf)?;
433        let algo_id = buf[p];
434        p += 1;
435        let algorithm = SignatureAlgorithm::from_wire_id(algo_id)
436            .ok_or(DelegationError::UnknownAlgorithm(algo_id))?;
437
438        // Topic-Patterns.
439        need(4, p, buf)?;
440        let n_topic = u32::from_be_bytes(buf[p..p + 4].try_into().unwrap_or([0u8; 4])) as usize;
441        p += 4;
442        if n_topic > MAX_TOPIC_PATTERNS {
443            return Err(DelegationError::TooManyPatterns {
444                kind: "topic",
445                count: n_topic,
446                max: MAX_TOPIC_PATTERNS,
447            });
448        }
449        let mut allowed_topic_patterns = Vec::with_capacity(n_topic);
450        for _ in 0..n_topic {
451            need(4, p, buf)?;
452            let len = u32::from_be_bytes(buf[p..p + 4].try_into().unwrap_or([0u8; 4])) as usize;
453            p += 4;
454            if len > MAX_PATTERN_LEN {
455                return Err(DelegationError::PatternTooLong {
456                    len,
457                    max: MAX_PATTERN_LEN,
458                });
459            }
460            need(len, p, buf)?;
461            let s = core::str::from_utf8(&buf[p..p + len])
462                .map_err(|e| DelegationError::Malformed(alloc::format!("utf8 topic: {e}")))?;
463            allowed_topic_patterns.push(s.to_string());
464            p += len;
465        }
466
467        // Partition-Patterns.
468        need(4, p, buf)?;
469        let n_part = u32::from_be_bytes(buf[p..p + 4].try_into().unwrap_or([0u8; 4])) as usize;
470        p += 4;
471        if n_part > MAX_PARTITION_PATTERNS {
472            return Err(DelegationError::TooManyPatterns {
473                kind: "partition",
474                count: n_part,
475                max: MAX_PARTITION_PATTERNS,
476            });
477        }
478        let mut allowed_partition_patterns = Vec::with_capacity(n_part);
479        for _ in 0..n_part {
480            need(4, p, buf)?;
481            let len = u32::from_be_bytes(buf[p..p + 4].try_into().unwrap_or([0u8; 4])) as usize;
482            p += 4;
483            if len > MAX_PATTERN_LEN {
484                return Err(DelegationError::PatternTooLong {
485                    len,
486                    max: MAX_PATTERN_LEN,
487                });
488            }
489            need(len, p, buf)?;
490            let s = core::str::from_utf8(&buf[p..p + len])
491                .map_err(|e| DelegationError::Malformed(alloc::format!("utf8 part: {e}")))?;
492            allowed_partition_patterns.push(s.to_string());
493            p += len;
494        }
495
496        // Signatur.
497        need(2, p, buf)?;
498        let sig_len = u16::from_be_bytes(buf[p..p + 2].try_into().unwrap_or([0u8; 2])) as usize;
499        p += 2;
500        need(sig_len, p, buf)?;
501        let signature = buf[p..p + sig_len].to_vec();
502        p += sig_len;
503
504        let link = Self {
505            delegator_guid,
506            delegatee_guid,
507            allowed_topic_patterns,
508            allowed_partition_patterns,
509            not_before,
510            not_after,
511            algorithm,
512            signature,
513        };
514        Ok((link, &buf[p..]))
515    }
516}
517
518/// Verkettete Delegation: Origin (Trust-Anchor) → ... → Edge.
519///
520/// Architektur-Referenz: `09_delegation.md` §5.2.
521#[derive(Debug, Clone, PartialEq, Eq)]
522pub struct DelegationChain {
523    /// 16-byte GUID des Origin-Participants. Bei 1-Hop = Delegator des
524    /// einzigen Links; bei N-Hop = Delegator des **ersten** Links.
525    pub origin_guid: [u8; 16],
526    /// Links in Order: links[0] = Origin → erste Zwischenstufe,
527    /// links[N-1] = letzte Zwischenstufe → Edge.
528    pub links: Vec<DelegationLink>,
529}
530
531/// Maximale Chain-Tiefe (DoS-Cap, hart). Profile-Override siehe
532/// `DelegationProfile::max_chain_depth`.
533pub const MAX_CHAIN_DEPTH_HARD_CAP: usize = 8;
534
535impl DelegationChain {
536    /// Konstruktor, prueft Hop-Cap.
537    ///
538    /// # Errors
539    /// [`DelegationError::TooManyPatterns`] mit `kind = "chain"` wenn
540    /// `links.len() > MAX_CHAIN_DEPTH_HARD_CAP`.
541    pub fn new(origin_guid: [u8; 16], links: Vec<DelegationLink>) -> DelegationResult<Self> {
542        if links.len() > MAX_CHAIN_DEPTH_HARD_CAP {
543            return Err(DelegationError::TooManyPatterns {
544                kind: "chain",
545                count: links.len(),
546                max: MAX_CHAIN_DEPTH_HARD_CAP,
547            });
548        }
549        Ok(Self { origin_guid, links })
550    }
551
552    /// Anzahl Links (Chain-Tiefe).
553    #[must_use]
554    pub fn depth(&self) -> usize {
555        self.links.len()
556    }
557
558    /// GUID des Edge-Peers (letzter Delegatee).
559    #[must_use]
560    pub fn edge_guid(&self) -> Option<[u8; 16]> {
561        self.links.last().map(|l| l.delegatee_guid)
562    }
563
564    /// Wire-Encoding der Chain.
565    ///
566    /// Layout:
567    /// ```text
568    /// version       = 1 (u8)
569    /// origin_guid   = 16 byte
570    /// n_links       = u8 (max 255 — durch HARD_CAP=8 ohnehin <)
571    /// [encoded link]*
572    /// ```
573    #[must_use]
574    pub fn encode(&self) -> Vec<u8> {
575        let mut buf = Vec::with_capacity(32 + 256 * self.links.len());
576        buf.push(DELEGATION_VERSION);
577        buf.extend_from_slice(&self.origin_guid);
578        let n = u8::try_from(self.links.len()).unwrap_or(u8::MAX);
579        buf.push(n);
580        for link in &self.links {
581            buf.extend_from_slice(&link.encode());
582        }
583        buf
584    }
585
586    /// Decode aus Wire-Bytes.
587    ///
588    /// # Errors
589    /// [`DelegationError::Malformed`] bei Layout-Verletzung.
590    pub fn decode(buf: &[u8]) -> DelegationResult<Self> {
591        if buf.len() < 1 + 16 + 1 {
592            return Err(DelegationError::Malformed(
593                "chain header truncated".to_string(),
594            ));
595        }
596        let version = buf[0];
597        if version != DELEGATION_VERSION {
598            return Err(DelegationError::UnsupportedVersion(version));
599        }
600        let mut origin_guid = [0u8; 16];
601        origin_guid.copy_from_slice(&buf[1..17]);
602        let n_links = buf[17] as usize;
603        if n_links > MAX_CHAIN_DEPTH_HARD_CAP {
604            return Err(DelegationError::TooManyPatterns {
605                kind: "chain",
606                count: n_links,
607                max: MAX_CHAIN_DEPTH_HARD_CAP,
608            });
609        }
610        let mut tail = &buf[18..];
611        let mut links = Vec::with_capacity(n_links);
612        for _ in 0..n_links {
613            let (link, rest) = DelegationLink::decode(tail)?;
614            links.push(link);
615            tail = rest;
616        }
617        Ok(Self { origin_guid, links })
618    }
619}
620
621// ---------- private signing helpers ----------
622
623fn sign_ecdsa(
624    alg: &'static signature::EcdsaSigningAlgorithm,
625    pkcs8: &[u8],
626    input: &[u8],
627) -> DelegationResult<Vec<u8>> {
628    let rng = SystemRandom::new();
629    let key_pair = signature::EcdsaKeyPair::from_pkcs8(alg, pkcs8, &rng)
630        .map_err(|e| DelegationError::SignFailed(alloc::format!("ecdsa key parse: {e}")))?;
631    let sig = key_pair
632        .sign(&rng, input)
633        .map_err(|e| DelegationError::SignFailed(alloc::format!("ecdsa sign: {e}")))?;
634    Ok(sig.as_ref().to_vec())
635}
636
637fn sign_rsa_pss(pkcs8: &[u8], input: &[u8]) -> DelegationResult<Vec<u8>> {
638    let key_pair = signature::RsaKeyPair::from_pkcs8(pkcs8)
639        .map_err(|e| DelegationError::SignFailed(alloc::format!("rsa key parse: {e}")))?;
640    if key_pair.public().modulus_len() != 256 {
641        return Err(DelegationError::SignFailed(alloc::format!(
642            "rsa key is {} bits, expected 2048",
643            key_pair.public().modulus_len() * 8
644        )));
645    }
646    let mut sig = alloc::vec![0u8; key_pair.public().modulus_len()];
647    let rng = SystemRandom::new();
648    key_pair
649        .sign(&signature::RSA_PSS_SHA256, &rng, input, &mut sig)
650        .map_err(|e| DelegationError::SignFailed(alloc::format!("rsa sign: {e}")))?;
651    Ok(sig)
652}
653
654fn sign_ed25519(pkcs8: &[u8], input: &[u8]) -> DelegationResult<Vec<u8>> {
655    let key_pair = signature::Ed25519KeyPair::from_pkcs8(pkcs8)
656        .map_err(|e| DelegationError::SignFailed(alloc::format!("ed25519 key parse: {e}")))?;
657    let sig = key_pair.sign(input);
658    Ok(sig.as_ref().to_vec())
659}
660
661#[cfg(test)]
662#[allow(clippy::expect_used, clippy::unwrap_used)]
663mod tests {
664    use super::*;
665    use ring::rand::SystemRandom;
666    use ring::signature::{
667        ECDSA_P256_SHA256_FIXED_SIGNING, ECDSA_P384_SHA384_FIXED_SIGNING, EcdsaKeyPair,
668        Ed25519KeyPair, KeyPair,
669    };
670
671    fn link_skeleton() -> DelegationLink {
672        DelegationLink::new(
673            [0xAA; 16],
674            [0xBB; 16],
675            alloc::vec!["sensor/*".to_string()],
676            alloc::vec!["public".to_string()],
677            1_700_000_000,
678            1_800_000_000,
679            SignatureAlgorithm::EcdsaP256,
680        )
681        .expect("valid skeleton")
682    }
683
684    fn ecdsa_key(alg: &'static signature::EcdsaSigningAlgorithm) -> (Vec<u8>, Vec<u8>) {
685        let rng = SystemRandom::new();
686        let pkcs8 = EcdsaKeyPair::generate_pkcs8(alg, &rng).expect("gen ecdsa");
687        let pkcs8_vec = pkcs8.as_ref().to_vec();
688        let key = EcdsaKeyPair::from_pkcs8(alg, &pkcs8_vec, &rng).expect("parse");
689        let pub_key = key.public_key().as_ref().to_vec();
690        (pkcs8_vec, pub_key)
691    }
692
693    fn ed25519_key() -> (Vec<u8>, Vec<u8>) {
694        let rng = SystemRandom::new();
695        let pkcs8_bytes = Ed25519KeyPair::generate_pkcs8(&rng).expect("gen");
696        let kp = Ed25519KeyPair::from_pkcs8(pkcs8_bytes.as_ref()).expect("parse");
697        (
698            pkcs8_bytes.as_ref().to_vec(),
699            kp.public_key().as_ref().to_vec(),
700        )
701    }
702
703    #[test]
704    fn signing_bytes_deterministic() {
705        let l = link_skeleton();
706        let a = l.signing_bytes();
707        let b = l.signing_bytes();
708        assert_eq!(a, b);
709        // Magic + version sind die ersten 9 byte.
710        assert_eq!(&a[..8], DELEGATION_MAGIC);
711        assert_eq!(a[8], DELEGATION_VERSION);
712    }
713
714    #[test]
715    fn ecdsa_p256_sign_verify_roundtrip() {
716        let (pkcs8, pub_key) = ecdsa_key(&ECDSA_P256_SHA256_FIXED_SIGNING);
717        let mut l = link_skeleton();
718        l.algorithm = SignatureAlgorithm::EcdsaP256;
719        l.sign(&pkcs8).expect("sign");
720        l.verify(&pub_key).expect("verify");
721        assert_eq!(l.signature.len(), 64);
722    }
723
724    #[test]
725    fn ecdsa_p384_sign_verify_roundtrip() {
726        let (pkcs8, pub_key) = ecdsa_key(&ECDSA_P384_SHA384_FIXED_SIGNING);
727        let mut l = link_skeleton();
728        l.algorithm = SignatureAlgorithm::EcdsaP384;
729        l.sign(&pkcs8).expect("sign");
730        l.verify(&pub_key).expect("verify");
731        assert_eq!(l.signature.len(), 96);
732    }
733
734    #[test]
735    fn ed25519_sign_verify_roundtrip() {
736        let (pkcs8, pub_key) = ed25519_key();
737        let mut l = link_skeleton();
738        l.algorithm = SignatureAlgorithm::Ed25519;
739        l.sign(&pkcs8).expect("sign");
740        l.verify(&pub_key).expect("verify");
741        assert_eq!(l.signature.len(), 64);
742    }
743
744    /// PKCS#8-DER eines mit `openssl genpkey -algorithm RSA
745    /// -pkeyopt rsa_keygen_bits:2048` erzeugten Test-Schluessels (1192 byte,
746    /// committet in `tests/fixtures/`).
747    /// Nicht-sensitiv: Test-Vector, niemals fuer echte Signaturen.
748    const TEST_RSA_2048_PKCS8: &[u8] = include_bytes!("../tests/fixtures/rsa_2048_test_pkcs8.der");
749
750    /// RSA-PSS-2048-Sign-Test mit hardcoded PKCS#8-DER Test-Vector.
751    /// Faengt die `modulus_len != 256` -> `==` Mutation in
752    /// `sign_rsa_pss` (Zeile 637): mit `==` wuerde JEDER 2048-bit
753    /// RSA-Key zur SignFailed fuehren.
754    #[test]
755    fn rsa_pss_2048_sign_succeeds_with_2048_bit_key() {
756        let mut l = link_skeleton();
757        l.algorithm = SignatureAlgorithm::RsaPss2048;
758        l.sign(TEST_RSA_2048_PKCS8).expect("RSA-PSS-2048 sign");
759        // 2048-bit Modulus = 256 byte Signatur (RFC 8017).
760        assert_eq!(l.signature.len(), 256);
761    }
762
763    #[test]
764    fn tampered_byte_breaks_verify() {
765        let (pkcs8, pub_key) = ecdsa_key(&ECDSA_P256_SHA256_FIXED_SIGNING);
766        let mut l = link_skeleton();
767        l.sign(&pkcs8).expect("sign");
768        // Veraendere ein Topic-Pattern — Sign-Input aendert sich, alte
769        // Sig wird invalid.
770        l.allowed_topic_patterns[0] = "/different/*".to_string();
771        let err = l.verify(&pub_key).expect_err("must fail");
772        assert!(matches!(err, DelegationError::VerifyFailed(_)));
773    }
774
775    #[test]
776    fn wrong_pubkey_breaks_verify() {
777        let (pkcs8_a, _pub_a) = ecdsa_key(&ECDSA_P256_SHA256_FIXED_SIGNING);
778        let (_pkcs8_b, pub_b) = ecdsa_key(&ECDSA_P256_SHA256_FIXED_SIGNING);
779        let mut l = link_skeleton();
780        l.sign(&pkcs8_a).expect("sign");
781        let err = l.verify(&pub_b).expect_err("must fail");
782        assert!(matches!(err, DelegationError::VerifyFailed(_)));
783    }
784
785    #[test]
786    fn empty_signature_rejects_verify() {
787        let (_pkcs8, pub_key) = ecdsa_key(&ECDSA_P256_SHA256_FIXED_SIGNING);
788        let l = link_skeleton(); // unsigned
789        let err = l.verify(&pub_key).expect_err("must fail");
790        assert!(matches!(err, DelegationError::SignFailed(_)));
791    }
792
793    #[test]
794    fn link_encode_decode_roundtrip() {
795        let (pkcs8, _) = ecdsa_key(&ECDSA_P256_SHA256_FIXED_SIGNING);
796        let mut l = link_skeleton();
797        l.sign(&pkcs8).expect("sign");
798        let wire = l.encode();
799        let (decoded, tail) = DelegationLink::decode(&wire).expect("decode");
800        assert!(tail.is_empty());
801        assert_eq!(decoded, l);
802    }
803
804    #[test]
805    fn link_decode_bad_magic_rejects() {
806        let mut bad = alloc::vec![0u8; 64];
807        bad[..8].copy_from_slice(b"NOTMAGIC");
808        let err = DelegationLink::decode(&bad).expect_err("must fail");
809        assert!(matches!(err, DelegationError::BadMagic));
810    }
811
812    #[test]
813    fn link_decode_bad_version_rejects() {
814        let mut wire = link_skeleton().encode();
815        wire[8] = 99; // Version-Byte
816        let err = DelegationLink::decode(&wire).expect_err("must fail");
817        assert!(matches!(err, DelegationError::UnsupportedVersion(99)));
818    }
819
820    #[test]
821    fn link_decode_unknown_algorithm_rejects() {
822        let mut l = link_skeleton();
823        l.signature = alloc::vec![0u8; 64];
824        let mut wire = l.encode();
825        // Algorithm-Byte sitzt nach magic(8) + version(1) + delegator(16)
826        // + delegatee(16) + not_before(8) + not_after(8) = offset 57.
827        wire[57] = 99;
828        let err = DelegationLink::decode(&wire).expect_err("must fail");
829        assert!(matches!(err, DelegationError::UnknownAlgorithm(99)));
830    }
831
832    #[test]
833    fn link_new_rejects_too_many_topics() {
834        let topics = (0..MAX_TOPIC_PATTERNS + 1)
835            .map(|i| alloc::format!("topic_{i}"))
836            .collect();
837        let err = DelegationLink::new(
838            [0; 16],
839            [0; 16],
840            topics,
841            alloc::vec![],
842            0,
843            1,
844            SignatureAlgorithm::EcdsaP256,
845        )
846        .expect_err("must fail");
847        assert!(matches!(
848            err,
849            DelegationError::TooManyPatterns { kind: "topic", .. }
850        ));
851    }
852
853    #[test]
854    fn link_new_rejects_inverted_window() {
855        let err = DelegationLink::new(
856            [0; 16],
857            [0; 16],
858            alloc::vec![],
859            alloc::vec![],
860            100,
861            50,
862            SignatureAlgorithm::EcdsaP256,
863        )
864        .expect_err("must fail");
865        assert!(matches!(err, DelegationError::InvalidTimeWindow));
866    }
867
868    #[test]
869    fn chain_encode_decode_roundtrip() {
870        let (pkcs8, _) = ecdsa_key(&ECDSA_P256_SHA256_FIXED_SIGNING);
871        let mut l1 = link_skeleton();
872        l1.sign(&pkcs8).expect("sign1");
873        let mut l2 = DelegationLink::new(
874            [0xBB; 16],
875            [0xCC; 16],
876            alloc::vec!["sensor/lidar".to_string()],
877            alloc::vec![],
878            1_700_000_000,
879            1_800_000_000,
880            SignatureAlgorithm::EcdsaP256,
881        )
882        .expect("l2 new");
883        l2.sign(&pkcs8).expect("sign2");
884
885        let chain =
886            DelegationChain::new([0xAA; 16], alloc::vec![l1.clone(), l2.clone()]).expect("chain");
887        assert_eq!(chain.depth(), 2);
888        assert_eq!(chain.edge_guid(), Some([0xCC; 16]));
889        let wire = chain.encode();
890        let decoded = DelegationChain::decode(&wire).expect("decode");
891        assert_eq!(decoded, chain);
892    }
893
894    #[test]
895    fn chain_new_rejects_too_deep() {
896        let dummy = link_skeleton();
897        let too_deep = alloc::vec![dummy; MAX_CHAIN_DEPTH_HARD_CAP + 1];
898        let err = DelegationChain::new([0; 16], too_deep).expect_err("must fail");
899        assert!(matches!(
900            err,
901            DelegationError::TooManyPatterns { kind: "chain", .. }
902        ));
903    }
904
905    #[test]
906    fn algorithm_wire_id_roundtrip() {
907        for a in [
908            SignatureAlgorithm::EcdsaP256,
909            SignatureAlgorithm::EcdsaP384,
910            SignatureAlgorithm::RsaPss2048,
911            SignatureAlgorithm::Ed25519,
912        ] {
913            let id = a.wire_id();
914            assert_eq!(SignatureAlgorithm::from_wire_id(id), Some(a));
915        }
916        assert_eq!(SignatureAlgorithm::from_wire_id(0), None);
917        assert_eq!(SignatureAlgorithm::from_wire_id(255), None);
918    }
919
920    // -------------------------------------------------------------
921    // Mutation-Killer (2026-05-01)
922    // -------------------------------------------------------------
923
924    /// expected_signature_len pro Algorithmus liefert spezifischen Wert.
925    /// Faengt `expected_signature_len -> None`-Mutation.
926    #[test]
927    fn expected_signature_len_per_algorithm() {
928        assert_eq!(
929            SignatureAlgorithm::EcdsaP256.expected_signature_len(),
930            Some(64)
931        );
932        assert_eq!(
933            SignatureAlgorithm::EcdsaP384.expected_signature_len(),
934            Some(96)
935        );
936        assert_eq!(
937            SignatureAlgorithm::Ed25519.expected_signature_len(),
938            Some(64)
939        );
940        assert_eq!(
941            SignatureAlgorithm::RsaPss2048.expected_signature_len(),
942            Some(256)
943        );
944    }
945
946    /// Display-Format aller DelegationError-Variants.
947    /// Faengt `Display::fmt -> Ok(Default)` Mutation.
948    #[test]
949    fn delegation_error_display_messages_specific() {
950        assert_eq!(
951            alloc::format!(
952                "{}",
953                DelegationError::TooManyPatterns {
954                    kind: "topic",
955                    count: 100,
956                    max: 64
957                }
958            ),
959            "topic patterns: 100 > max 64"
960        );
961        assert_eq!(
962            alloc::format!("{}", DelegationError::PatternTooLong { len: 500, max: 256 }),
963            "pattern length 500 > max 256"
964        );
965        assert_eq!(
966            alloc::format!("{}", DelegationError::SignFailed("bad".into())),
967            "sign failed: bad"
968        );
969        assert_eq!(
970            alloc::format!("{}", DelegationError::VerifyFailed("nope".into())),
971            "verify failed: nope"
972        );
973        assert_eq!(
974            alloc::format!("{}", DelegationError::Malformed("hdr".into())),
975            "malformed delegation: hdr"
976        );
977        assert_eq!(
978            alloc::format!("{}", DelegationError::UnknownAlgorithm(42)),
979            "unknown algorithm id: 42"
980        );
981        assert_eq!(
982            alloc::format!("{}", DelegationError::InvalidTimeWindow),
983            "not_before > not_after"
984        );
985        assert_eq!(
986            alloc::format!("{}", DelegationError::BadMagic),
987            "bad magic bytes"
988        );
989        assert_eq!(
990            alloc::format!("{}", DelegationError::UnsupportedVersion(2)),
991            "unsupported version: 2"
992        );
993    }
994
995    // ---- Cap-Boundary-Tests fuer DelegationLink::decode ----
996
997    /// Baut einen minimal-validen Link-Wire mit n_topic Topic-Patterns
998    /// und n_part Partition-Patterns; jedes Pattern leerer string.
999    fn build_link_wire(
1000        n_topic: u32,
1001        n_part: u32,
1002        topic_lens: &[u32],
1003        part_lens: &[u32],
1004    ) -> Vec<u8> {
1005        let mut buf = Vec::new();
1006        buf.extend_from_slice(DELEGATION_MAGIC);
1007        buf.push(DELEGATION_VERSION);
1008        buf.extend_from_slice(&[0u8; 16]); // delegator
1009        buf.extend_from_slice(&[0u8; 16]); // delegatee
1010        buf.extend_from_slice(&0i64.to_be_bytes()); // not_before
1011        buf.extend_from_slice(&0i64.to_be_bytes()); // not_after
1012        buf.push(1); // EcdsaP256
1013        // Topic
1014        buf.extend_from_slice(&n_topic.to_be_bytes());
1015        for &len in topic_lens {
1016            buf.extend_from_slice(&len.to_be_bytes());
1017            buf.extend(std::iter::repeat_n(b'a', len as usize));
1018        }
1019        // Partition
1020        buf.extend_from_slice(&n_part.to_be_bytes());
1021        for &len in part_lens {
1022            buf.extend_from_slice(&len.to_be_bytes());
1023            buf.extend(std::iter::repeat_n(b'b', len as usize));
1024        }
1025        // sig_len + sig
1026        buf.extend_from_slice(&0u16.to_be_bytes());
1027        buf
1028    }
1029
1030    /// n_topic == MAX_TOPIC_PATTERNS muss durchgehen.
1031    /// n_topic > MAX muss erroren.
1032    #[test]
1033    fn link_decode_n_topic_at_and_over_cap() {
1034        let topic_lens = vec![1u32; MAX_TOPIC_PATTERNS];
1035        let wire = build_link_wire(MAX_TOPIC_PATTERNS as u32, 0, &topic_lens, &[]);
1036        let res = DelegationLink::decode(&wire);
1037        assert!(res.is_ok(), "n_topic=MAX must succeed, got {res:?}");
1038
1039        let topic_lens_over = vec![1u32; MAX_TOPIC_PATTERNS + 1];
1040        let wire_over = build_link_wire((MAX_TOPIC_PATTERNS + 1) as u32, 0, &topic_lens_over, &[]);
1041        let err = DelegationLink::decode(&wire_over).unwrap_err();
1042        assert!(matches!(err, DelegationError::TooManyPatterns { .. }));
1043    }
1044
1045    /// Topic-pattern-len == MAX_PATTERN_LEN durchgehen, > MAX erroren.
1046    #[test]
1047    fn link_decode_topic_pattern_len_at_and_over_cap() {
1048        let wire = build_link_wire(1, 0, &[MAX_PATTERN_LEN as u32], &[]);
1049        assert!(DelegationLink::decode(&wire).is_ok());
1050
1051        let wire_over = build_link_wire(1, 0, &[(MAX_PATTERN_LEN + 1) as u32], &[]);
1052        let err = DelegationLink::decode(&wire_over).unwrap_err();
1053        assert!(matches!(err, DelegationError::PatternTooLong { .. }));
1054    }
1055
1056    /// n_part == MAX, > MAX.
1057    #[test]
1058    fn link_decode_n_part_at_and_over_cap() {
1059        let part_lens = vec![1u32; MAX_PARTITION_PATTERNS];
1060        let wire = build_link_wire(0, MAX_PARTITION_PATTERNS as u32, &[], &part_lens);
1061        assert!(DelegationLink::decode(&wire).is_ok());
1062
1063        let part_lens_over = vec![1u32; MAX_PARTITION_PATTERNS + 1];
1064        let wire_over =
1065            build_link_wire(0, (MAX_PARTITION_PATTERNS + 1) as u32, &[], &part_lens_over);
1066        let err = DelegationLink::decode(&wire_over).unwrap_err();
1067        assert!(matches!(err, DelegationError::TooManyPatterns { .. }));
1068    }
1069
1070    /// Part-pattern-len cap.
1071    #[test]
1072    fn link_decode_part_pattern_len_at_and_over_cap() {
1073        let wire = build_link_wire(0, 1, &[], &[MAX_PATTERN_LEN as u32]);
1074        assert!(DelegationLink::decode(&wire).is_ok());
1075
1076        let wire_over = build_link_wire(0, 1, &[], &[(MAX_PATTERN_LEN + 1) as u32]);
1077        let err = DelegationLink::decode(&wire_over).unwrap_err();
1078        assert!(matches!(err, DelegationError::PatternTooLong { .. }));
1079    }
1080
1081    // ---- DelegationChain::decode header-len boundary ----
1082
1083    /// Faengt Mutationen `<` -> `==`/`<=` und `+` -> `-`/`*` auf
1084    /// `if buf.len() < 1 + 16 + 1` Header-Check.
1085    /// Layout: 1 byte version + 16 byte origin_guid + 1 byte n_links = 18.
1086    /// Buffer EXAKT 18 byte (mit n_links=0): muss durchgehen.
1087    /// Buffer 17 byte: muss erroren (Truncated).
1088    #[test]
1089    fn chain_decode_header_at_minimum_size() {
1090        let mut buf = Vec::new();
1091        buf.push(DELEGATION_VERSION); // 1
1092        buf.extend_from_slice(&[0u8; 16]); // 16 — origin_guid
1093        buf.push(0); // 1 — n_links=0
1094        assert_eq!(buf.len(), 18);
1095        let chain = DelegationChain::decode(&buf).expect("18-byte header must decode");
1096        assert_eq!(chain.links.len(), 0);
1097    }
1098
1099    #[test]
1100    fn chain_decode_header_one_byte_too_short() {
1101        let mut buf = Vec::new();
1102        buf.push(DELEGATION_VERSION);
1103        buf.extend_from_slice(&[0u8; 16]);
1104        // Missing n_links byte: total 17.
1105        assert_eq!(buf.len(), 17);
1106        let err = DelegationChain::decode(&buf).unwrap_err();
1107        assert!(matches!(err, DelegationError::Malformed(_)));
1108    }
1109
1110    /// n_links == MAX_CHAIN_DEPTH_HARD_CAP muss durchgehen (mit korrekt
1111    /// vielen Links danach), n_links > MAX muss erroren.
1112    /// Kein Versuch alle Links zu encoden — nur header check.
1113    #[test]
1114    fn chain_decode_n_links_over_hard_cap_rejected() {
1115        let mut buf = Vec::new();
1116        buf.push(DELEGATION_VERSION);
1117        buf.extend_from_slice(&[0u8; 16]);
1118        buf.push((MAX_CHAIN_DEPTH_HARD_CAP + 1) as u8);
1119        let err = DelegationChain::decode(&buf).unwrap_err();
1120        assert!(matches!(
1121            err,
1122            DelegationError::TooManyPatterns { kind: "chain", .. }
1123        ));
1124    }
1125
1126    /// n_links == MAX_CHAIN_DEPTH_HARD_CAP MUSS den Cap-Check passieren
1127    /// (Original `>` ist false). Faengt `>` -> `>=` (an MAX wuerde
1128    /// Mutation TooManyPatterns liefern statt Malformed-aus-Loop).
1129    /// Wir liefern bewusst KEINE Link-Bodies — Original geht in Loop und
1130    /// liefert dort einen anderen Error (Malformed); Mutation feuert die
1131    /// Cap-Branch.
1132    #[test]
1133    fn chain_decode_n_links_at_hard_cap_passes_cap_check() {
1134        let mut buf = Vec::new();
1135        buf.push(DELEGATION_VERSION);
1136        buf.extend_from_slice(&[0u8; 16]);
1137        buf.push(MAX_CHAIN_DEPTH_HARD_CAP as u8);
1138        let err = DelegationChain::decode(&buf).unwrap_err();
1139        // Original `>`: cap check geht durch, dann Loop scheitert auf
1140        // fehlenden Link-Bytes -> Malformed.
1141        // Mutation `>=`: cap check wuerde TooManyPatterns liefern.
1142        assert!(
1143            matches!(err, DelegationError::Malformed(_)),
1144            "expected Malformed (loop failure), got {err:?}"
1145        );
1146    }
1147}