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//! Data model + sign/verify for cryptographic delegation chains.
7//! Architecture reference: `docs/architecture/09_delegation.md` §5.
8//!
9//! A [`DelegationLink`] carries a signed statement:
10//! "the delegator (with an X.509 cert) allows the delegatee (identified via
11//! `delegatee_guid`) to write/read samples within a defined time and scope
12//! window."
13//!
14//! A [`DelegationChain`] chains 1..N such links into a
15//! proof series origin → edge. Each link is signed by the **previous
16//! delegatee** (the initial link by the trust anchor itself).
17//!
18//! # Layout (deterministic for the signing input)
19//!
20//! ```text
21//! magic        = b"ZERODDSD" (8 bytes)
22//! version      = 1 (u8)
23//! delegator    = 16 byte GUID
24//! delegatee    = 16 byte GUID
25//! not_before   = i64 big-endian (Unix seconds)
26//! not_after    = i64 big-endian
27//! algorithm    = u8 (see 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: the same input as above (without the `signature` suffix),
35//! the hash implied by `SignatureAlgorithm` (SHA-256 for P-256 +
36//! Ed25519, SHA-384 for P-384, SHA-256 for RSA-PSS-2048).
37//!
38//! zerodds-lint: allow no_dyn_in_safe
39//! (Cert validation via `rustls-webpki` needs object-safe trait refs.)
40
41extern crate alloc;
42
43use alloc::string::{String, ToString};
44use alloc::vec::Vec;
45
46use crate::backend::rand::SystemRandom;
47use crate::backend::signature::{
48    self, ECDSA_P256_SHA256_FIXED, ECDSA_P384_SHA384_FIXED, ED25519, RSA_PSS_2048_8192_SHA256,
49    UnparsedPublicKey,
50};
51
52/// Signature algorithm for delegation links.
53///
54/// Default recommendation: [`SignatureAlgorithm::EcdsaP256`] — compact
55/// signature (~64 bytes), fast verify on edge hardware.
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57#[non_exhaustive]
58pub enum SignatureAlgorithm {
59    /// ECDSA with P-256 + SHA-256, fixed-length encoding (64-byte sig).
60    EcdsaP256,
61    /// ECDSA with P-384 + SHA-384, fixed-length encoding (96-byte sig).
62    EcdsaP384,
63    /// RSA-PSS with a 2048-bit key + SHA-256 (256-byte sig).
64    RsaPss2048,
65    /// Ed25519 (64-byte sig).
66    Ed25519,
67}
68
69impl SignatureAlgorithm {
70    /// Wire ID for the deterministic 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 from the wire ID; `None` for an 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    /// Expected signature length in bytes (fixed) or `None` for
94    /// variable (RSA-PSS depending on key size — we fix it at 2048
95    /// and thus 256 bytes).
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 for the sign-input layout (8 bytes).
107pub const DELEGATION_MAGIC: &[u8; 8] = b"ZERODDSD";
108
109/// Wire-layout version.
110pub const DELEGATION_VERSION: u8 = 1;
111
112/// Maximum number of topic patterns per link (DoS cap).
113pub const MAX_TOPIC_PATTERNS: usize = 64;
114/// Maximum number of partition patterns per link (DoS cap).
115pub const MAX_PARTITION_PATTERNS: usize = 64;
116/// Maximum pattern string length in bytes.
117pub const MAX_PATTERN_LEN: usize = 256;
118
119/// Errors from the delegation module.
120#[derive(Debug, Clone, PartialEq, Eq)]
121#[non_exhaustive]
122pub enum DelegationError {
123    /// The pattern list is larger than the DoS cap.
124    TooManyPatterns {
125        /// Which list — `"topic"` or `"partition"`.
126        kind: &'static str,
127        /// Actual count.
128        count: usize,
129        /// Allowed maximum.
130        max: usize,
131    },
132    /// The pattern is longer than the string cap.
133    PatternTooLong {
134        /// Actual length.
135        len: usize,
136        /// Allowed maximum.
137        max: usize,
138    },
139    /// Sign operation failed (e.g. invalid key).
140    SignFailed(String),
141    /// Verify operation failed.
142    VerifyFailed(String),
143    /// The wire bytes could not be decoded.
144    Malformed(String),
145    /// Unsupported wire algorithm ID.
146    UnknownAlgorithm(u8),
147    /// `not_before > not_after`.
148    InvalidTimeWindow,
149    /// The magic bytes do not match.
150    BadMagic,
151    /// Unknown wire-layout version.
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 for delegation operations.
179pub type DelegationResult<T> = Result<T, DelegationError>;
180
181/// A single delegation statement (signed by the delegator).
182///
183/// Architecture reference: `09_delegation.md` §5.1.
184#[derive(Debug, Clone, PartialEq, Eq)]
185pub struct DelegationLink {
186    /// 16-byte participant GUID of the delegator (= owner of the
187    /// signing key, at the same time the subject of the X.509 cert that
188    /// provides the verify key).
189    pub delegator_guid: [u8; 16],
190    /// 16-byte participant GUID of the delegatee (= edge peer that
191    /// inherits permission via this link).
192    pub delegatee_guid: [u8; 16],
193    /// Allowed topic glob patterns. Empty list = "all" (only in the
194    /// initial link, otherwise a validation fail).
195    pub allowed_topic_patterns: Vec<String>,
196    /// Allowed partition patterns. Empty list = default partition.
197    pub allowed_partition_patterns: Vec<String>,
198    /// Unix seconds from when the link is valid.
199    pub not_before: i64,
200    /// Unix seconds until when the link is valid.
201    pub not_after: i64,
202    /// Signature algorithm the link is signed with.
203    pub algorithm: SignatureAlgorithm,
204    /// Signature bytes (layout depending on `algorithm`).
205    pub signature: Vec<u8>,
206}
207
208impl DelegationLink {
209    /// Creates an unsigned link (signature empty). Use
210    /// [`Self::sign`] to sign.
211    ///
212    /// # Errors
213    /// * [`DelegationError::TooManyPatterns`] if `topic_patterns` or
214    ///   `partition_patterns` exceed the DoS cap.
215    /// * [`DelegationError::PatternTooLong`] if a single pattern exceeds the
216    ///   string cap.
217    /// * [`DelegationError::InvalidTimeWindow`] if
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 (deterministic). Identical for 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    /// Signs the link with `signing_key`. `signing_key` format:
302    /// * `EcdsaP256` / `EcdsaP384` — PKCS#8-DER ECDSA private key.
303    /// * `RsaPss2048` — PKCS#8-DER RSA private key.
304    /// * `Ed25519` — PKCS#8-DER Ed25519 private key (rfc8410 format).
305    ///
306    /// Sets `self.signature` to the sign output.
307    ///
308    /// # Errors
309    /// [`DelegationError::SignFailed`] on a key-parsing error or
310    /// a `ring`-internal failure.
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    /// Verifies the link against a **public-key DER** whose
332    /// format differs per algorithm:
333    /// * `EcdsaP256`/`EcdsaP384` — `SubjectPublicKeyInfo` bytes
334    ///   (uncompressed point encoding per SEC1 §2.3.3).
335    /// * `RsaPss2048` — `SubjectPublicKeyInfo` bytes (RFC 8017).
336    /// * `Ed25519` — 32-byte raw public key.
337    ///
338    /// In practice the verify key is extracted from the **delegator X.509 cert**
339    /// (see `delegation_check.rs` in delegation_check (security-permissions)).
340    ///
341    /// # Errors
342    /// * [`DelegationError::VerifyFailed`] on a signature mismatch or
343    ///   public-key parse error.
344    /// * [`DelegationError::SignFailed`] if `signature` is empty.
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("crate::backend::verify".to_string()))
368    }
369
370    /// Wire encoding of a complete link (sign input + signature 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 of a complete link.
383    ///
384    /// Consumes exactly as many bytes as the link is large; returns the
385    /// rest of the slice as `tail` (the chain decoder passes that
386    /// on to the next link).
387    ///
388    /// # Errors
389    /// [`DelegationError::Malformed`] if the layout is wrong.
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        // Signature.
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/// Chained delegation: origin (trust anchor) → ... → edge.
519///
520/// Architecture reference: `09_delegation.md` §5.2.
521#[derive(Debug, Clone, PartialEq, Eq)]
522pub struct DelegationChain {
523    /// 16-byte GUID of the origin participant. For 1-hop = delegator of
524    /// the only link; for N-hop = delegator of the **first** link.
525    pub origin_guid: [u8; 16],
526    /// Links in order: links[0] = origin → first intermediate stage,
527    /// links[N-1] = last intermediate stage → edge.
528    pub links: Vec<DelegationLink>,
529}
530
531/// Maximum chain depth (DoS cap, hard). For a profile override see
532/// `DelegationProfile::max_chain_depth`.
533pub const MAX_CHAIN_DEPTH_HARD_CAP: usize = 8;
534
535impl DelegationChain {
536    /// Constructor, checks the hop cap.
537    ///
538    /// # Errors
539    /// [`DelegationError::TooManyPatterns`] with `kind = "chain"` if
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    /// Number of links (chain depth).
553    #[must_use]
554    pub fn depth(&self) -> usize {
555        self.links.len()
556    }
557
558    /// GUID of the edge peer (last 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 of the chain.
565    ///
566    /// Layout:
567    /// ```text
568    /// version       = 1 (u8)
569    /// origin_guid   = 16 bytes
570    /// n_links       = u8 (max 255 — < anyway because of HARD_CAP=8)
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 from wire bytes.
587    ///
588    /// # Errors
589    /// [`DelegationError::Malformed`] on a layout violation.
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 = crate::compat::ecdsa_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 crate::compat::rsa_modulus_len(&key_pair) != 256 {
641        return Err(DelegationError::SignFailed(alloc::format!(
642            "rsa key is {} bits, expected 2048",
643            crate::compat::rsa_modulus_len(&key_pair) * 8
644        )));
645    }
646    let mut sig = alloc::vec![0u8; crate::compat::rsa_modulus_len(&key_pair)];
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 crate::backend::rand::SystemRandom;
666    use crate::backend::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 = crate::compat::ecdsa_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 are the first 9 bytes.
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 of a test key generated with `openssl genpkey -algorithm RSA
745    /// -pkeyopt rsa_keygen_bits:2048` (1192 bytes,
746    /// committed in `tests/fixtures/`).
747    /// Non-sensitive: a test vector, never for real signatures.
748    const TEST_RSA_2048_PKCS8: &[u8] = include_bytes!("../tests/fixtures/rsa_2048_test_pkcs8.der");
749
750    /// RSA-PSS-2048 sign test with a hardcoded PKCS#8-DER test vector.
751    /// Catches the `modulus_len != 256` -> `==` mutation in
752    /// `sign_rsa_pss` (line 637): with `==` EVERY 2048-bit
753    /// RSA key would lead to SignFailed.
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 signature (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        // Change a topic pattern — the sign input changes, the old
769        // sig becomes 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        // The algorithm byte sits after 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 per algorithm returns a specific value.
925    /// Catches the `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 of all DelegationError variants.
947    /// Catches the `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 for DelegationLink::decode ----
996
997    /// Builds a minimally valid link wire with n_topic topic patterns
998    /// and n_part partition patterns; each pattern an empty 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 must pass.
1031    /// n_topic > MAX must error.
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 passes, > MAX errors.
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    /// Catches mutations `<` -> `==`/`<=` and `+` -> `-`/`*` on the
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 EXACTLY 18 bytes (with n_links=0): must pass.
1087    /// Buffer 17 bytes: must error (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 must pass (with the correct
1111    /// number of links after it), n_links > MAX must error.
1112    /// No attempt to encode all links — only the 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 MUST pass the cap check
1127    /// (original `>` is false). Catches `>` -> `>=` (at MAX the
1128    /// mutation would return TooManyPatterns instead of Malformed-from-loop).
1129    /// We deliberately supply NO link bodies — the original goes into the loop and
1130    /// returns a different error there (Malformed); the mutation fires the
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 `>`: the cap check passes, then the loop fails on
1140        // missing link bytes -> Malformed.
1141        // Mutation `>=`: the cap check would return TooManyPatterns.
1142        assert!(
1143            matches!(err, DelegationError::Malformed(_)),
1144            "expected Malformed (loop failure), got {err:?}"
1145        );
1146    }
1147}