Skip to main content

qhermes_kernel/
lib.rs

1// Copyright (c) 2026 Copertino All rights reserved.
2// SPDX-License-Identifier: LicenseRef-Copertino-1.0
3// Licensed under the Copertino Source License 1.0 — see LICENSE at the repository root.
4// Commercial use requires a license from Copertino: hello@copertino.world
5
6//! Cryptographic and authorization primitives for QHermes delegation chains.
7//!
8//! # Layers
9//!
10//! ```text
11//! Crypto     — key derivation and ML-DSA-65 signing/verification
12//! Policy     — permission encoding, scope subset enforcement, caveat evaluation
13//! Delegation — credential issuance and chain verification
14//! Wire       — credential chain serialization and deserialization
15//! ```
16//!
17//! # Permission format
18//!
19//! ```text
20//! [res_len: u8][resource bytes][verb_len: u8][verb bytes]
21//! ```
22//!
23//! # Wire format
24//!
25//! ```text
26//! [version: u8 = 0x01][count: u32 LE][credentials...]
27//! [issuer_pk: PK_SIZE][signature: SIG_SIZE][payload_len: u32 LE][payload bytes]
28//! ```
29//!
30//! # Timestamps
31//!
32//! All `Timestamp` values are seconds since the Unix epoch. Passing
33//! milliseconds or nanoseconds produces incorrect caveat evaluation.
34//!
35//! # Domain separation
36//!
37//! `IdentityIsland::derive` folds `deployment` and `context` into the HKDF
38//! info parameter. Different `deployment` values yield independent key material
39//! even when sharing the same `master`.
40
41#![no_std]
42#![forbid(unsafe_code)]
43
44use hybrid_array::Array;
45use hkdf::Hkdf;
46use ml_dsa::{EncodedSignature, EncodedVerifyingKey, KeyGen, MlDsa65, SigningKey, VerifyingKey};
47use ml_dsa::signature::Verifier;
48use sha3::Sha3_512;
49use zeroize::Zeroizing;
50
51// ── Constants ─────────────────────────────────────────────────────────────────
52
53/// Byte length of the HKDF seed and derived key material.
54pub const SEED_SIZE: usize = 32;
55
56/// Byte length of an ML-DSA-65 public key (verifying key).
57pub const PK_SIZE: usize = 1952;
58
59/// Byte length of an ML-DSA-65 signature.
60pub const SIG_SIZE: usize = 3309;
61
62/// Maximum byte length of the resource field in a permission.
63/// Fits a `u8`-typed TLV length prefix.
64pub const RESOURCE_LEN: usize = 255;
65
66/// Maximum byte length of the verb field in a permission.
67/// Fits a `u8`-typed TLV length prefix.
68pub const VERB_LEN: usize = 255;
69
70/// Maximum byte length of the TLV encoding of one permission:
71/// `1 (res_len) + RESOURCE_LEN + 1 (verb_len) + VERB_LEN`.
72pub const PERM_TLV_MAX: usize = 1 + RESOURCE_LEN + 1 + VERB_LEN;
73
74/// Byte length of one serialized caveat record: 1-byte tag + 8-byte timestamp.
75pub const CAVEAT_SIZE: usize = 9;
76
77/// Maximum number of permissions in a credential scope.
78pub const MAX_SCOPE_PERMS: usize = 64;
79
80/// Maximum number of caveats in a credential.
81pub const MAX_CAVEATS: usize = 64;
82
83/// Maximum depth of a delegation chain. Longer chains are rejected at
84/// issuance, verification, and deserialization.
85pub const MAX_DEPTH: u32 = 16;
86
87/// Maximum byte length of a payload, assuming all permissions at maximum TLV
88/// size and all caveat slots occupied.
89///
90/// Layout: `PK_SIZE + 1 (role) + 4 (depth) + 4 (scope_len)
91///          + PERM_TLV_MAX * MAX_SCOPE_PERMS + 4 (cav_len) + CAVEAT_SIZE * MAX_CAVEATS`
92pub const MAX_PAYLOAD_SIZE: usize =
93    PK_SIZE + 1 + 4 + 4 + PERM_TLV_MAX * MAX_SCOPE_PERMS + 4 + CAVEAT_SIZE * MAX_CAVEATS;
94
95/// Fixed bytes per credential record, excluding the payload:
96/// `PK_SIZE + SIG_SIZE + 4 (payload_len field)`.
97pub const CREDENTIAL_FIXED_SIZE: usize = PK_SIZE + SIG_SIZE + 4;
98
99// Changing SALT invalidates all previously derived key material.
100const SALT: &[u8] = b"QHermes-Kernel-v1";
101
102// Increment WIRE_VERSION on any breaking change to the wire format.
103const WIRE_VERSION: u8 = 0x01;
104
105const CAVEAT_NOT_BEFORE: u8 = 0x01;
106const CAVEAT_NOT_AFTER: u8  = 0x02;
107
108// ── Error ─────────────────────────────────────────────────────────────────────
109
110/// Errors returned by QHermes kernel operations.
111///
112/// This enum is marked `#[non_exhaustive]`. Match arms must include a wildcard
113/// to remain compatible with variants added in minor releases.
114#[non_exhaustive]
115#[derive(Debug, PartialEq, Eq)]
116pub enum KernelError {
117    /// HKDF key expansion returned an error.
118    HkdfExpandFailed,
119    /// The ML-DSA-65 signing operation failed.
120    SigningFailed,
121    /// The signature does not match the payload under the given public key.
122    SignatureInvalid,
123    /// The public key bytes are not a valid ML-DSA-65 verifying key.
124    KeyDecodingFailed,
125    /// The signature bytes are not a valid ML-DSA-65 signature.
126    SignatureDecodingFailed,
127    /// A `not_before` caveat was evaluated with a timestamp earlier than the caveat value.
128    NotBeforeViolation,
129    /// A `not_after` caveat was evaluated with a timestamp later than the caveat value.
130    NotAfterViolation,
131    /// The caveat buffer contains an unrecognized tag byte or has an invalid layout.
132    MalformedCaveatBuffer,
133    /// A child credential claims a permission not covered by the parent scope.
134    ScopeEscalation,
135    /// The scope was constructed from an empty slice. Every credential must grant at least one permission.
136    ScopeEmpty,
137    /// The scope contains more than `MAX_SCOPE_PERMS` permissions.
138    ScopeTooLarge,
139    /// The caveat buffer exceeds `CAVEAT_SIZE * MAX_CAVEATS` bytes.
140    CaveatsTooLarge,
141    /// An operation was attempted on an empty credential chain.
142    EmptyChain,
143    /// The chain depth exceeds `MAX_DEPTH`.
144    ChainTooDeep,
145    /// The issuer public key in a link does not match the child public key of the previous link.
146    ParentKeyMismatch,
147    /// A credential with role `Leaf` attempted to issue a further delegation.
148    LeafCannotDelegate,
149    /// The `depth` field of the payload does not equal the expected sequential position in the chain.
150    DepthMismatch,
151    /// The role byte in the payload is neither `0` (Leaf) nor `1` (Node).
152    InvalidRoleByte,
153    /// The first byte of the wire buffer does not match `WIRE_VERSION`.
154    WireVersionMismatch,
155    /// The wire buffer or a sub-slice ends before the expected bytes.
156    WireTruncated,
157    /// The wire buffer is structurally inconsistent: a length field, role byte,
158    /// or trailing-bytes check failed.
159    WireInvalid,
160    /// The `resource` argument to `perm_tlv` exceeds `RESOURCE_LEN` bytes.
161    ResourceTooLong,
162    /// The `verb` argument to `perm_tlv` exceeds `VERB_LEN` bytes.
163    VerbTooLong,
164}
165
166// ── Domain types ──────────────────────────────────────────────────────────────
167
168/// Reference to a `PK_SIZE`-byte ML-DSA-65 public key.
169#[derive(Clone, Copy, PartialEq, Eq, Debug)]
170pub struct PublicKey<'a>(pub &'a [u8; PK_SIZE]);
171
172/// Reference to a `SIG_SIZE`-byte ML-DSA-65 signature.
173#[derive(Clone, Copy, PartialEq, Eq, Debug)]
174pub struct Signature<'a>(pub &'a [u8; SIG_SIZE]);
175
176/// Role encoded in a credential payload.
177///
178/// - `Node`: this identity may issue further delegations.
179/// - `Leaf`: this identity may not issue further delegations.
180#[derive(Debug, Clone, Copy, PartialEq, Eq)]
181pub enum Role {
182    Leaf = 0,
183    Node = 1,
184}
185
186impl TryFrom<u8> for Role {
187    type Error = KernelError;
188    fn try_from(b: u8) -> Result<Self, Self::Error> {
189        match b {
190            0 => Ok(Role::Leaf),
191            1 => Ok(Role::Node),
192            _ => Err(KernelError::InvalidRoleByte),
193        }
194    }
195}
196
197/// Timestamp in seconds since the Unix epoch.
198///
199/// All kernel functions that accept or produce `Timestamp` values operate in
200/// seconds. Passing milliseconds or nanoseconds produces incorrect caveat evaluation.
201#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
202pub struct Timestamp(pub u64);
203
204/// Sequential position of a credential within its delegation chain.
205/// The first credential issued by the root has depth 1.
206#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
207pub struct Depth(pub u32);
208
209/// Validated, non-empty TLV sequence of permissions.
210///
211/// Each entry has the layout:
212///
213/// ```text
214/// [res_len: u8][resource bytes][verb_len: u8][verb bytes]
215/// ```
216///
217/// `try_new` walks the slice and verifies that:
218/// - the slice is non-empty,
219/// - every entry is complete (no truncated fields),
220/// - the total number of entries does not exceed `MAX_SCOPE_PERMS`.
221///
222/// After construction the slice is guaranteed to be fully consumed by
223/// well-formed entries and iterable via `iter()`.
224#[derive(Clone, Copy, Debug, PartialEq, Eq)]
225pub struct BoundedScope<'a>(&'a [u8]);
226
227impl<'a> BoundedScope<'a> {
228    /// Returns an iterator yielding `(resource, verb)` pairs in order.
229    pub fn iter(&self) -> ScopeIter<'a> {
230        ScopeIter { raw: self.0, pos: 0 }
231    }
232
233    /// Parses and validates a raw TLV slice.
234    ///
235    /// Returns `ScopeEmpty` if `raw` is empty, `WireInvalid` if any entry is
236    /// truncated or structurally incorrect, and `ScopeTooLarge` if the entry
237    /// count exceeds `MAX_SCOPE_PERMS`.
238    pub fn try_new(raw: &'a [u8]) -> Result<Self, KernelError> {
239        if raw.is_empty() {
240            return Err(KernelError::ScopeEmpty);
241        }
242        let mut pos = 0;
243        let mut count = 0usize;
244        while pos < raw.len() {
245            if count >= MAX_SCOPE_PERMS {
246                return Err(KernelError::ScopeTooLarge);
247            }
248            tlv_next_field(raw, &mut pos).ok_or(KernelError::WireInvalid)?; // resource
249            tlv_next_field(raw, &mut pos).ok_or(KernelError::WireInvalid)?; // verb
250            count += 1;
251        }
252        Ok(Self(raw))
253    }
254}
255
256/// Validated sequence of fixed-size caveat records.
257///
258/// The raw slice must satisfy:
259/// - `len % CAVEAT_SIZE == 0`
260/// - `len <= CAVEAT_SIZE * MAX_CAVEATS`
261/// - every tag byte is `0x01` (not_before) or `0x02` (not_after)
262///
263/// An empty slice (no caveats) is accepted.
264#[derive(Clone, Copy, Debug, PartialEq, Eq)]
265pub struct BoundedCaveats<'a>(&'a [u8]);
266
267impl<'a> BoundedCaveats<'a> {
268    /// Parses and validates a raw caveat slice.
269    ///
270    /// Returns `CaveatsTooLarge` if the slice exceeds the maximum total size,
271    /// `MalformedCaveatBuffer` if the length is not a multiple of `CAVEAT_SIZE`
272    /// or any tag byte is unrecognized.
273    pub fn try_new(raw: &'a [u8]) -> Result<Self, KernelError> {
274        if raw.len() > CAVEAT_SIZE * MAX_CAVEATS {
275            return Err(KernelError::CaveatsTooLarge);
276        }
277        if raw.len() % CAVEAT_SIZE != 0 {
278            return Err(KernelError::MalformedCaveatBuffer);
279        }
280        for c in raw.chunks_exact(CAVEAT_SIZE) {
281            match c[0] {
282                CAVEAT_NOT_BEFORE | CAVEAT_NOT_AFTER => {}
283                _ => return Err(KernelError::MalformedCaveatBuffer),
284            }
285        }
286        Ok(Self(raw))
287    }
288}
289
290/// Parameters for issuing a credential in a delegation chain.
291pub struct DelegationManifest<'a> {
292    /// Public key of the identity receiving the delegation.
293    pub child_pk: PublicKey<'a>,
294    /// Role assigned to the child identity.
295    pub role:     Role,
296    /// Sequential position of this credential in the chain.
297    /// The root issues at depth 1; each subsequent link increments by one.
298    pub depth:    Depth,
299    /// Permissions granted to the child identity, in TLV encoding.
300    pub scope:    BoundedScope<'a>,
301    /// Time-based restrictions on the validity of the credential.
302    pub caveats:  BoundedCaveats<'a>,
303}
304
305/// One link in a serialized credential chain as it appears on the wire.
306pub struct Credential<'a> {
307    /// Public key of the identity that signed this credential.
308    pub issuer_pk: PublicKey<'a>,
309    /// ML-DSA-65 signature over `payload`.
310    pub signature: Signature<'a>,
311    /// Signed payload bytes.
312    pub payload:   &'a [u8],
313}
314
315// ── Crypto ────────────────────────────────────────────────────────────────────
316
317/// Signing interface for any ML-DSA-65 identity: in-memory keys, HSM, TPM, or enclave.
318pub trait IdentitySigner {
319    /// Returns the public key corresponding to this signer's signing key.
320    fn public_key(&self) -> PublicKey<'_>;
321
322    /// Signs `payload` and writes the `SIG_SIZE`-byte signature into `out`.
323    ///
324    /// Returns `SigningFailed` if the underlying signing operation fails.
325    fn sign_into(&self, payload: &[u8], out: &mut [u8; SIG_SIZE]) -> Result<(), KernelError>;
326}
327
328/// ML-DSA-65 identity deterministically derived from a master seed via HKDF-SHA3-512.
329///
330/// For hardware-backed or enclave-resident keys, implement `IdentitySigner` directly.
331pub struct IdentityIsland {
332    pk: [u8; PK_SIZE],
333    sk: SigningKey<MlDsa65>,
334}
335
336impl IdentityIsland {
337    /// Derives a key pair from `master` (secret), `deployment`, and `context`.
338    ///
339    /// HKDF info = `deployment || ':' || context`. Different deployments or contexts
340    /// yield independent key material from the same master seed.
341    ///
342    /// Returns `HkdfExpandFailed` if the combined info string exceeds 512 bytes.
343    pub fn derive(
344        master:     &[u8; SEED_SIZE],
345        deployment: &[u8],
346        context:    &[u8],
347    ) -> Result<Self, KernelError> {
348        let mut seed = Zeroizing::new([0u8; SEED_SIZE]);
349        Hkdf::<Sha3_512>::new(Some(SALT), master)
350            .expand_multi_info(&[deployment, b":", context], seed.as_mut())
351            .map_err(|_| KernelError::HkdfExpandFailed)?;
352
353        let kp = MlDsa65::from_seed(&Array::<u8, hybrid_array::typenum::U32>::from(*seed));
354        let mut pk = [0u8; PK_SIZE];
355        pk.copy_from_slice(kp.verifying_key().encode().as_slice());
356
357        Ok(Self { pk, sk: kp.signing_key().clone() })
358    }
359}
360
361impl IdentitySigner for IdentityIsland {
362    fn public_key(&self) -> PublicKey<'_> {
363        PublicKey(&self.pk)
364    }
365
366    fn sign_into(&self, payload: &[u8], out: &mut [u8; SIG_SIZE]) -> Result<(), KernelError> {
367        let sig = self.sk.sign_deterministic(payload, &[])
368            .map_err(|_| KernelError::SigningFailed)?;
369        out.copy_from_slice(&sig.encode());
370        Ok(())
371    }
372}
373
374/// Verifies an ML-DSA-65 signature over `payload` under `pk`.
375///
376/// Returns `Ok(())` if the signature is valid. Returns `KeyDecodingFailed`,
377/// `SignatureDecodingFailed`, or `SignatureInvalid` according to the failure.
378pub fn verify_signature(
379    pk:      PublicKey<'_>,
380    payload: &[u8],
381    sig:     Signature<'_>,
382) -> Result<(), KernelError> {
383    let vk = VerifyingKey::<MlDsa65>::decode(
384        &EncodedVerifyingKey::<MlDsa65>::try_from(pk.0.as_slice())
385            .map_err(|_| KernelError::KeyDecodingFailed)?,
386    );
387    let decoded = ml_dsa::Signature::<MlDsa65>::decode(
388        &EncodedSignature::<MlDsa65>::try_from(sig.0.as_slice())
389            .map_err(|_| KernelError::SignatureDecodingFailed)?,
390    )
391    .ok_or(KernelError::SignatureDecodingFailed)?;
392
393    vk.verify(payload, &decoded).map_err(|_| KernelError::SignatureInvalid)
394}
395
396// ── Policy ────────────────────────────────────────────────────────────────────
397
398/// Encodes a `(resource, verb)` permission into `out` using TLV format.
399///
400/// Written layout: `[res_len: u8][resource bytes][verb_len: u8][verb bytes]`.
401///
402/// Returns the number of bytes written on success.
403/// Returns `ResourceTooLong` if `resource.len() > RESOURCE_LEN`,
404/// `VerbTooLong` if `verb.len() > VERB_LEN`,
405/// and `WireTruncated` if `out` is too small.
406pub fn perm_tlv(resource: &[u8], verb: &[u8], out: &mut [u8]) -> Result<usize, KernelError> {
407    if resource.len() > RESOURCE_LEN {
408        return Err(KernelError::ResourceTooLong);
409    }
410    if verb.len() > VERB_LEN {
411        return Err(KernelError::VerbTooLong);
412    }
413    let need = 1 + resource.len() + 1 + verb.len();
414    if out.len() < need {
415        return Err(KernelError::WireTruncated);
416    }
417    out[0] = resource.len() as u8;
418    out[1..1 + resource.len()].copy_from_slice(resource);
419    let v = 1 + resource.len();
420    out[v] = verb.len() as u8;
421    out[v + 1..v + 1 + verb.len()].copy_from_slice(verb);
422    Ok(need)
423}
424
425/// Constructs a not-before caveat record.
426///
427/// The credential is invalid if evaluated with a `Timestamp` (Unix seconds)
428/// strictly less than `t`.
429pub fn not_before(t: Timestamp) -> [u8; CAVEAT_SIZE] {
430    build_caveat(CAVEAT_NOT_BEFORE, t)
431}
432
433/// Constructs a not-after caveat record.
434///
435/// The credential is invalid if evaluated with a `Timestamp` (Unix seconds)
436/// strictly greater than `t`.
437pub fn not_after(t: Timestamp) -> [u8; CAVEAT_SIZE] {
438    build_caveat(CAVEAT_NOT_AFTER, t)
439}
440
441fn build_caveat(tag: u8, t: Timestamp) -> [u8; CAVEAT_SIZE] {
442    let mut c = [0u8; CAVEAT_SIZE];
443    c[0] = tag;
444    c[1..].copy_from_slice(&t.0.to_le_bytes());
445    c
446}
447
448fn tlv_next_field<'a>(raw: &'a [u8], pos: &mut usize) -> Option<&'a [u8]> {
449    let len = *raw.get(*pos)? as usize;
450    *pos += 1;
451    let end = pos.checked_add(len).filter(|&e| e <= raw.len())?;
452    let field = &raw[*pos..end];
453    *pos = end;
454    Some(field)
455}
456
457pub struct ScopeIter<'a> {
458    raw: &'a [u8],
459    pos: usize,
460}
461
462impl<'a> Iterator for ScopeIter<'a> {
463    type Item = (&'a [u8], &'a [u8]);
464
465    fn next(&mut self) -> Option<Self::Item> {
466        let resource = tlv_next_field(self.raw, &mut self.pos)?;
467        let verb     = tlv_next_field(self.raw, &mut self.pos)?;
468        Some((resource, verb))
469    }
470}
471
472/// Checks that every permission in `child` is covered by at least one permission in `parent`.
473///
474/// A parent permission covers a child permission when:
475/// - the parent resource equals the child resource, or the parent resource is `*`; and
476/// - the parent verb equals the child verb, or the parent verb is `*`.
477///
478/// **Resources are opaque byte strings compared for exact equality.** The kernel
479/// does not interpret path separators, glob patterns, or any structure within a
480/// resource value. The only special value is `b"*"`, which matches any single
481/// resource. A resource such as `b"src/**"` is treated as a fixed identifier, not
482/// as a glob — it covers only another permission whose resource field is the
483/// identical byte string `b"src/**"`.
484///
485/// The correct pattern for hierarchical namespacing is to use consistent literals
486/// throughout the chain. If the root grants `(b"src/**", b"WRITE")`, every
487/// descendant that needs write access to the same namespace must carry the same
488/// `b"src/**"` resource identifier, not a more specific path such as
489/// `b"src/main.rs"`. Finer-grained access control belongs in the application
490/// layer, not in the credential chain.
491///
492/// Returns `ScopeEscalation` if any child permission is not covered.
493pub fn enforce_scope_subset(
494    child:  &BoundedScope<'_>,
495    parent: &BoundedScope<'_>,
496) -> Result<(), KernelError> {
497    for (cr, cv) in child.iter() {
498        let covered = parent.iter().any(|(pr, pv)| {
499            (pr == b"*" || pr == cr) && (pv == b"*" || pv == cv)
500        });
501        if !covered {
502            return Err(KernelError::ScopeEscalation);
503        }
504    }
505    Ok(())
506}
507
508/// Evaluates all caveats in `caveats` against `now`.
509///
510/// `now` must be a timestamp in seconds since the Unix epoch.
511///
512/// - `CAVEAT_NOT_BEFORE` (`0x01`): returns `NotBeforeViolation` if `now < ts`.
513/// - `CAVEAT_NOT_AFTER`  (`0x02`): returns `NotAfterViolation`  if `now > ts`.
514pub fn evaluate_caveats(
515    caveats: &BoundedCaveats<'_>,
516    now:     Timestamp,
517) -> Result<(), KernelError> {
518    for c in caveats.0.chunks_exact(CAVEAT_SIZE) {
519        let ts = Timestamp(u64::from_le_bytes(c[1..].try_into().unwrap()));
520        match c[0] {
521            CAVEAT_NOT_BEFORE if now < ts => return Err(KernelError::NotBeforeViolation),
522            CAVEAT_NOT_AFTER  if now > ts => return Err(KernelError::NotAfterViolation),
523            _ => {}
524        }
525    }
526    Ok(())
527}
528
529// ── Delegation ────────────────────────────────────────────────────────────────
530
531/// Serializes `manifest` into `payload_out` and signs it into `sig_out`.
532///
533/// Returns the number of bytes written. Only `payload_out[..n]` contains the valid payload.
534///
535/// # Payload layout
536///
537/// ```text
538/// [child_pk: PK_SIZE][role: u8][depth: u32 LE][scope_len: u32 LE][scope bytes]
539/// [cav_len: u32 LE][caveat bytes]
540/// ```
541///
542/// # Errors
543///
544/// - `ChainTooDeep`  — `manifest.depth.0 > MAX_DEPTH`
545/// - `SigningFailed` — the underlying signing operation failed
546pub fn issue_credential(
547    issuer:      &impl IdentitySigner,
548    manifest:    &DelegationManifest<'_>,
549    payload_out: &mut [u8; MAX_PAYLOAD_SIZE],
550    sig_out:     &mut [u8; SIG_SIZE],
551) -> Result<usize, KernelError> {
552    if manifest.depth.0 > MAX_DEPTH {
553        return Err(KernelError::ChainTooDeep);
554    }
555    let scope   = manifest.scope.0;
556    let caveats = manifest.caveats.0;
557    let mut pos = 0;
558
559    payload_out[pos..pos + PK_SIZE].copy_from_slice(manifest.child_pk.0);             pos += PK_SIZE;
560    payload_out[pos] = manifest.role as u8;                                            pos += 1;
561    payload_out[pos..pos + 4].copy_from_slice(&manifest.depth.0.to_le_bytes());       pos += 4;
562    payload_out[pos..pos + 4].copy_from_slice(&(scope.len() as u32).to_le_bytes());   pos += 4;
563    payload_out[pos..pos + scope.len()].copy_from_slice(scope);                        pos += scope.len();
564    payload_out[pos..pos + 4].copy_from_slice(&(caveats.len() as u32).to_le_bytes()); pos += 4;
565    payload_out[pos..pos + caveats.len()].copy_from_slice(caveats);                    pos += caveats.len();
566
567    issuer.sign_into(&payload_out[..pos], sig_out)?;
568    Ok(pos)
569}
570
571/// Verifies a credential chain rooted at `root_pk` against `timestamp`.
572///
573/// Checks signatures, depth sequence, caveat validity, scope subset enforcement,
574/// and Leaf role compliance.
575pub fn verify_delegation(
576    root_pk:   PublicKey<'_>,
577    chain:     &[Credential<'_>],
578    timestamp: Timestamp,
579) -> Result<(), KernelError> {
580    if chain.len() > MAX_DEPTH as usize {
581        return Err(KernelError::ChainTooDeep);
582    }
583    let (first, rest) = chain.split_first().ok_or(KernelError::EmptyChain)?;
584
585    if first.issuer_pk != root_pk {
586        return Err(KernelError::ParentKeyMismatch);
587    }
588    verify_signature(first.issuer_pk, first.payload, first.signature)?;
589    let m0 = parse_payload(first.payload)?;
590    if m0.depth != Depth(1) {
591        return Err(KernelError::DepthMismatch);
592    }
593    evaluate_caveats(&m0.caveats, timestamp)?;
594
595    let mut prev_child_pk    = m0.child_pk;
596    let mut prev_child_scope = m0.scope;
597    let mut prev_child_role  = m0.role;
598
599    for (i, cred) in rest.iter().enumerate() {
600        if cred.issuer_pk != prev_child_pk {
601            return Err(KernelError::ParentKeyMismatch);
602        }
603        if prev_child_role == Role::Leaf {
604            return Err(KernelError::LeafCannotDelegate);
605        }
606        verify_signature(cred.issuer_pk, cred.payload, cred.signature)?;
607        let m = parse_payload(cred.payload)?;
608        if m.depth != Depth(i as u32 + 2) {
609            return Err(KernelError::DepthMismatch);
610        }
611        enforce_scope_subset(&m.scope, &prev_child_scope)?;
612        evaluate_caveats(&m.caveats, timestamp)?;
613        prev_child_pk    = m.child_pk;
614        prev_child_scope = m.scope;
615        prev_child_role  = m.role;
616    }
617    Ok(())
618}
619
620// ── Wire ──────────────────────────────────────────────────────────────────────
621
622/// Serializes `chain` into `out`. Returns bytes written, or `WireTruncated` if `out` is too small.
623pub fn write_credential_chain(
624    out:   &mut [u8],
625    chain: &[Credential<'_>],
626) -> Result<usize, KernelError> {
627    if out.len() < 5 {
628        return Err(KernelError::WireTruncated);
629    }
630    out[0] = WIRE_VERSION;
631    let count32 = u32::try_from(chain.len()).map_err(|_| KernelError::WireTruncated)?;
632    out[1..5].copy_from_slice(&count32.to_le_bytes());
633    let mut pos: usize = 5;
634
635    for cred in chain {
636        let plen  = cred.payload.len();
637        let need  = pos.saturating_add(CREDENTIAL_FIXED_SIZE).saturating_add(plen);
638        if out.len() < need {
639            return Err(KernelError::WireTruncated);
640        }
641        let plen32 = u32::try_from(plen).map_err(|_| KernelError::WireTruncated)?;
642        out[pos..pos + PK_SIZE].copy_from_slice(cred.issuer_pk.0);  pos += PK_SIZE;
643        out[pos..pos + SIG_SIZE].copy_from_slice(cred.signature.0); pos += SIG_SIZE;
644        out[pos..pos + 4].copy_from_slice(&plen32.to_le_bytes());   pos += 4;
645        out[pos..pos + plen].copy_from_slice(cred.payload);         pos += plen;
646    }
647    Ok(pos)
648}
649
650/// Deserializes `wire` into `chain`. All slices point into `wire`. Returns the credential count.
651///
652/// Trailing bytes after the last credential are not rejected. Each individual credential payload
653/// is parsed with strict no-trailing-bytes enforcement, but the outer buffer may contain extra
654/// bytes beyond the last credential without error. This is intentional: the wire chain is
655/// typically embedded inside a larger message, and the caller controls how much of the buffer
656/// to pass in. `verify_chain` and `verify_delegation` operate on the returned credential slice
657/// and are unaffected by any trailing bytes in the original buffer.
658///
659/// # Errors
660///
661/// - `WireVersionMismatch` — first byte does not match `WIRE_VERSION`
662/// - `EmptyChain`          — count field is zero
663/// - `ChainTooDeep`        — count field exceeds `MAX_DEPTH`
664/// - `WireInvalid`         — count exceeds length of `chain`
665/// - `WireTruncated`       — buffer ends before the expected bytes
666pub fn read_credential_chain<'a>(
667    wire:  &'a [u8],
668    chain: &mut [Credential<'a>],
669) -> Result<usize, KernelError> {
670    let ([version], rest)   = split_array::<1>(wire)?;
671    if *version != WIRE_VERSION {
672        return Err(KernelError::WireVersionMismatch);
673    }
674    let (count_bytes, rest) = split_array::<4>(rest)?;
675    let num = usize::try_from(u32::from_le_bytes(*count_bytes)).map_err(|_| KernelError::WireInvalid)?;
676    if num == 0 {
677        return Err(KernelError::EmptyChain);
678    }
679    if num > MAX_DEPTH as usize {
680        return Err(KernelError::ChainTooDeep);
681    }
682    if num > chain.len() {
683        return Err(KernelError::WireInvalid);
684    }
685    let mut rest = rest;
686
687    for slot in chain[..num].iter_mut() {
688        let (pk_bytes, tail)  = split_array::<PK_SIZE>(rest)?;
689        let (sig_bytes, tail) = split_array::<SIG_SIZE>(tail)?;
690        let (len_bytes, tail) = split_array::<4>(tail)?;
691        let pl_len = usize::try_from(u32::from_le_bytes(*len_bytes)).map_err(|_| KernelError::WireInvalid)?;
692        let (payload, tail)   = split_at_checked(tail, pl_len)?;
693
694        *slot = Credential {
695            issuer_pk: PublicKey(pk_bytes),
696            signature: Signature(sig_bytes),
697            payload,
698        };
699        rest = tail;
700    }
701    Ok(num)
702}
703
704fn split_at_checked(buf: &[u8], n: usize) -> Result<(&[u8], &[u8]), KernelError> {
705    if buf.len() < n {
706        return Err(KernelError::WireTruncated);
707    }
708    Ok(buf.split_at(n))
709}
710
711fn split_array<const N: usize>(buf: &[u8]) -> Result<(&[u8; N], &[u8]), KernelError> {
712    buf.split_first_chunk::<N>().ok_or(KernelError::WireTruncated)
713}
714
715// Trailing bytes after all fields return WireInvalid (anti-malleability).
716#[cfg(test)]
717pub(crate) fn parse_payload_pub(p: &[u8]) -> Result<DelegationManifest<'_>, KernelError> {
718    parse_payload(p)
719}
720
721fn parse_payload(p: &[u8]) -> Result<DelegationManifest<'_>, KernelError> {
722    let (child_pk, rest)        = split_array::<PK_SIZE>(p)?;
723
724    let (role_byte, rest)       = split_array::<1>(rest)?;
725    let role = Role::try_from(role_byte[0])?;
726
727    let (depth_bytes, rest)     = split_array::<4>(rest)?;
728    let depth = u32::from_le_bytes(*depth_bytes);
729
730    let (scope_len_bytes, rest) = split_array::<4>(rest)?;
731    let scope_len = usize::try_from(u32::from_le_bytes(*scope_len_bytes)).map_err(|_| KernelError::WireInvalid)?;
732    let (scope_bytes, rest)     = split_at_checked(rest, scope_len)?;
733
734    let (cav_len_bytes, rest)   = split_array::<4>(rest)?;
735    let cav_len = usize::try_from(u32::from_le_bytes(*cav_len_bytes)).map_err(|_| KernelError::WireInvalid)?;
736    let (cav_bytes, tail)       = split_at_checked(rest, cav_len)?;
737
738    if !tail.is_empty() {
739        return Err(KernelError::WireInvalid);
740    }
741
742    Ok(DelegationManifest {
743        child_pk: PublicKey(child_pk),
744        role,
745        depth:   Depth(depth),
746        scope:   BoundedScope::try_new(scope_bytes)?,
747        caveats: BoundedCaveats::try_new(cav_bytes)?,
748    })
749}
750
751// ── Tests ─────────────────────────────────────────────────────────────────────
752
753#[cfg(test)]
754mod tests {
755    use super::*;
756
757    extern crate std;
758    use std::vec;
759    use std::vec::Vec;
760
761    // ── Helpers ──
762
763    /// Derives a test identity using a fixed master seed and the given context.
764    fn make_test_identity(context: &[u8]) -> IdentityIsland {
765        let master = [0x42u8; SEED_SIZE];
766        IdentityIsland::derive(&master, b"test-deploy", context).unwrap()
767    }
768
769    /// Builds a single-permission TLV for (/test, GET).
770    fn make_test_scope() -> Vec<u8> {
771        let mut buf = vec![0u8; PERM_TLV_MAX];
772        let n = perm_tlv(b"/test", b"GET", &mut buf).unwrap();
773        buf.truncate(n);
774        buf
775    }
776
777    /// Returns an empty caveats buffer (no time restrictions).
778    fn make_test_caveats() -> Vec<u8> {
779        vec![]
780    }
781
782    // ── Identity / key derivation ──
783
784    #[test]
785    fn derive_same_inputs_same_pubkey() {
786        let a = make_test_identity(b"ctx-alpha");
787        let b = make_test_identity(b"ctx-alpha");
788        assert_eq!(a.public_key(), b.public_key());
789    }
790
791    #[test]
792    fn derive_different_context_different_pubkey() {
793        let a = make_test_identity(b"ctx-alpha");
794        let b = make_test_identity(b"ctx-beta");
795        assert_ne!(a.public_key(), b.public_key());
796    }
797
798    #[test]
799    fn derive_different_deployment_different_pubkey() {
800        let master = [0x42u8; SEED_SIZE];
801        let a = IdentityIsland::derive(&master, b"deploy-one", b"ctx").unwrap();
802        let b = IdentityIsland::derive(&master, b"deploy-two", b"ctx").unwrap();
803        assert_ne!(a.public_key(), b.public_key());
804    }
805
806    #[test]
807    fn derive_different_master_different_pubkey() {
808        let master_a = [0x42u8; SEED_SIZE];
809        let master_b = [0x43u8; SEED_SIZE];
810        let a = IdentityIsland::derive(&master_a, b"dep", b"ctx").unwrap();
811        let b = IdentityIsland::derive(&master_b, b"dep", b"ctx").unwrap();
812        assert_ne!(a.public_key(), b.public_key());
813    }
814
815    #[test]
816    fn derive_valid_master_returns_ok() {
817        let master = [0x11u8; SEED_SIZE];
818        assert!(IdentityIsland::derive(&master, b"dep", b"ctx").is_ok());
819    }
820
821    // ── perm_tlv ──
822
823    #[test]
824    fn perm_tlv_single_byte_resource_and_verb() {
825        let mut buf = [0u8; 64];
826        let n = perm_tlv(b"R", b"V", &mut buf).unwrap();
827        // [res_len=1][R][verb_len=1][V] = 4 bytes
828        assert_eq!(n, 4);
829        assert_eq!(buf[0], 1);
830        assert_eq!(buf[1], b'R');
831        assert_eq!(buf[2], 1);
832        assert_eq!(buf[3], b'V');
833    }
834
835    #[test]
836    fn perm_tlv_max_resource_succeeds() {
837        let resource = vec![0xAAu8; RESOURCE_LEN];
838        let mut buf = vec![0u8; PERM_TLV_MAX];
839        let n = perm_tlv(&resource, b"V", &mut buf).unwrap();
840        assert_eq!(n, 1 + RESOURCE_LEN + 1 + 1);
841    }
842
843    #[test]
844    fn perm_tlv_max_verb_succeeds() {
845        let verb = vec![0xBBu8; VERB_LEN];
846        let mut buf = vec![0u8; PERM_TLV_MAX];
847        let n = perm_tlv(b"R", &verb, &mut buf).unwrap();
848        assert_eq!(n, 1 + 1 + 1 + VERB_LEN);
849    }
850
851    #[test]
852    fn perm_tlv_resource_256_returns_resource_too_long() {
853        let resource = vec![0u8; 256];
854        let mut buf = vec![0u8; PERM_TLV_MAX + 4];
855        assert!(matches!(
856            perm_tlv(&resource, b"V", &mut buf),
857            Err(KernelError::ResourceTooLong)
858        ));
859    }
860
861    #[test]
862    fn perm_tlv_verb_256_returns_verb_too_long() {
863        let verb = vec![0u8; 256];
864        let mut buf = vec![0u8; PERM_TLV_MAX + 4];
865        assert!(matches!(
866            perm_tlv(b"R", &verb, &mut buf),
867            Err(KernelError::VerbTooLong)
868        ));
869    }
870
871    #[test]
872    fn perm_tlv_empty_resource_and_verb() {
873        let mut buf = [0u8; 8];
874        let n = perm_tlv(b"", b"", &mut buf).unwrap();
875        // [res_len=0][verb_len=0] = 2 bytes
876        assert_eq!(n, 2);
877        assert_eq!(buf[0], 0);
878        assert_eq!(buf[1], 0);
879    }
880
881    #[test]
882    fn perm_tlv_output_too_small_returns_wire_truncated() {
883        // Need 4 bytes for (b"R", b"V"), supply only 3.
884        let mut buf = [0u8; 3];
885        assert!(matches!(
886            perm_tlv(b"R", b"V", &mut buf),
887            Err(KernelError::WireTruncated)
888        ));
889    }
890
891    // ── not_before / not_after ──
892
893    #[test]
894    fn not_before_returns_nine_bytes() {
895        let c = not_before(Timestamp(1_000_000));
896        assert_eq!(c.len(), 9);
897    }
898
899    #[test]
900    fn not_before_first_byte_is_0x01() {
901        let c = not_before(Timestamp(42));
902        assert_eq!(c[0], 0x01);
903    }
904
905    #[test]
906    fn not_before_timestamp_little_endian() {
907        let ts = 0x0102_0304_0506_0708u64;
908        let c = not_before(Timestamp(ts));
909        assert_eq!(&c[1..9], &ts.to_le_bytes());
910    }
911
912    #[test]
913    fn not_after_returns_nine_bytes() {
914        let c = not_after(Timestamp(9_999_999));
915        assert_eq!(c.len(), 9);
916    }
917
918    #[test]
919    fn not_after_first_byte_is_0x02() {
920        let c = not_after(Timestamp(99));
921        assert_eq!(c[0], 0x02);
922    }
923
924    #[test]
925    fn not_after_timestamp_little_endian() {
926        let ts = 0xDEAD_BEEF_CAFE_BABEu64;
927        let c = not_after(Timestamp(ts));
928        assert_eq!(&c[1..9], &ts.to_le_bytes());
929    }
930
931    // ── BoundedScope ──
932
933    #[test]
934    fn bounded_scope_empty_slice_returns_scope_empty() {
935        assert!(matches!(
936            BoundedScope::try_new(&[]),
937            Err(KernelError::ScopeEmpty)
938        ));
939    }
940
941    #[test]
942    fn bounded_scope_single_permission_succeeds() {
943        let scope_bytes = make_test_scope();
944        assert!(BoundedScope::try_new(&scope_bytes).is_ok());
945    }
946
947    #[test]
948    fn bounded_scope_multi_permission_succeeds() {
949        let mut buf = vec![0u8; PERM_TLV_MAX * 2];
950        let n1 = perm_tlv(b"/a", b"GET", &mut buf).unwrap();
951        let n2 = perm_tlv(b"/b", b"POST", &mut buf[n1..]).unwrap();
952        let total = n1 + n2;
953        assert!(BoundedScope::try_new(&buf[..total]).is_ok());
954    }
955
956    #[test]
957    fn bounded_scope_truncated_tlv_returns_wire_invalid() {
958        // Write a valid TLV then truncate the resource bytes.
959        let mut buf = vec![0u8; PERM_TLV_MAX];
960        let n = perm_tlv(b"hello", b"GET", &mut buf).unwrap();
961        // Truncate to only the resource length byte — the actual resource bytes are missing.
962        assert!(matches!(
963            BoundedScope::try_new(&buf[..1]),
964            Err(KernelError::WireInvalid)
965        ));
966        let _ = n; // suppress unused warning
967    }
968
969    #[test]
970    fn bounded_scope_too_many_perms_returns_scope_too_large() {
971        // Build MAX_SCOPE_PERMS + 1 minimal permissions (each 2 bytes: res_len=0, verb_len=0).
972        let count = MAX_SCOPE_PERMS + 1;
973        let raw = vec![0u8; count * 2]; // each entry = [0x00][0x00]
974        assert!(matches!(
975            BoundedScope::try_new(&raw),
976            Err(KernelError::ScopeTooLarge)
977        ));
978    }
979
980    // ── BoundedCaveats ──
981
982    #[test]
983    fn bounded_caveats_empty_slice_succeeds() {
984        assert!(BoundedCaveats::try_new(&[]).is_ok());
985    }
986
987    #[test]
988    fn bounded_caveats_single_not_before_succeeds() {
989        let c = not_before(Timestamp(100));
990        assert!(BoundedCaveats::try_new(&c).is_ok());
991    }
992
993    #[test]
994    fn bounded_caveats_single_not_after_succeeds() {
995        let c = not_after(Timestamp(200));
996        assert!(BoundedCaveats::try_new(&c).is_ok());
997    }
998
999    #[test]
1000    fn bounded_caveats_length_not_multiple_of_9_returns_malformed() {
1001        // 10 bytes is not a multiple of CAVEAT_SIZE (9).
1002        let raw = [0x01u8; 10];
1003        assert!(matches!(
1004            BoundedCaveats::try_new(&raw),
1005            Err(KernelError::MalformedCaveatBuffer)
1006        ));
1007    }
1008
1009    #[test]
1010    fn bounded_caveats_unknown_tag_returns_malformed() {
1011        // 9 bytes aligned, but tag = 0xFF is unknown.
1012        let mut raw = [0u8; CAVEAT_SIZE];
1013        raw[0] = 0xFF;
1014        assert!(matches!(
1015            BoundedCaveats::try_new(&raw),
1016            Err(KernelError::MalformedCaveatBuffer)
1017        ));
1018    }
1019
1020    #[test]
1021    fn bounded_caveats_too_large_returns_caveats_too_large() {
1022        // One byte over the maximum allowed.
1023        let raw = vec![0x01u8; CAVEAT_SIZE * MAX_CAVEATS + CAVEAT_SIZE];
1024        assert!(matches!(
1025            BoundedCaveats::try_new(&raw),
1026            Err(KernelError::CaveatsTooLarge)
1027        ));
1028    }
1029
1030    // ── enforce_scope_subset ──
1031
1032    #[test]
1033    fn enforce_scope_subset_exact_match_passes() {
1034        let scope_bytes = make_test_scope();
1035        let scope = BoundedScope::try_new(&scope_bytes).unwrap();
1036        assert!(enforce_scope_subset(&scope, &scope).is_ok());
1037    }
1038
1039    #[test]
1040    fn enforce_scope_subset_wildcard_resource_covers_child() {
1041        // Parent grants (*,  GET); child requests (/specific, GET).
1042        let mut parent_buf = [0u8; PERM_TLV_MAX];
1043        let pn = perm_tlv(b"*", b"GET", &mut parent_buf).unwrap();
1044        let parent = BoundedScope::try_new(&parent_buf[..pn]).unwrap();
1045
1046        let mut child_buf = [0u8; PERM_TLV_MAX];
1047        let cn = perm_tlv(b"/specific", b"GET", &mut child_buf).unwrap();
1048        let child = BoundedScope::try_new(&child_buf[..cn]).unwrap();
1049
1050        assert!(enforce_scope_subset(&child, &parent).is_ok());
1051    }
1052
1053    #[test]
1054    fn enforce_scope_subset_wildcard_verb_covers_child() {
1055        // Parent grants (/res, *); child requests (/res, DELETE).
1056        let mut parent_buf = [0u8; PERM_TLV_MAX];
1057        let pn = perm_tlv(b"/res", b"*", &mut parent_buf).unwrap();
1058        let parent = BoundedScope::try_new(&parent_buf[..pn]).unwrap();
1059
1060        let mut child_buf = [0u8; PERM_TLV_MAX];
1061        let cn = perm_tlv(b"/res", b"DELETE", &mut child_buf).unwrap();
1062        let child = BoundedScope::try_new(&child_buf[..cn]).unwrap();
1063
1064        assert!(enforce_scope_subset(&child, &parent).is_ok());
1065    }
1066
1067    #[test]
1068    fn enforce_scope_subset_child_has_extra_perm_returns_scope_escalation() {
1069        // Parent grants (/test, GET); child requests (/test, POST) which parent does not cover.
1070        let scope_bytes = make_test_scope(); // (/test, GET)
1071        let parent = BoundedScope::try_new(&scope_bytes).unwrap();
1072
1073        let mut child_buf = [0u8; PERM_TLV_MAX];
1074        let cn = perm_tlv(b"/test", b"POST", &mut child_buf).unwrap();
1075        let child = BoundedScope::try_new(&child_buf[..cn]).unwrap();
1076
1077        assert!(matches!(
1078            enforce_scope_subset(&child, &parent),
1079            Err(KernelError::ScopeEscalation)
1080        ));
1081    }
1082
1083    // ── evaluate_caveats ──
1084
1085    #[test]
1086    fn evaluate_caveats_not_before_now_gte_ts_passes() {
1087        let c = not_before(Timestamp(1000));
1088        let caveats = BoundedCaveats::try_new(&c).unwrap();
1089        // now == ts: passes (not_before fails only when now < ts)
1090        assert!(evaluate_caveats(&caveats, Timestamp(1000)).is_ok());
1091        // now > ts: also passes
1092        assert!(evaluate_caveats(&caveats, Timestamp(2000)).is_ok());
1093    }
1094
1095    #[test]
1096    fn evaluate_caveats_not_before_now_lt_ts_returns_violation() {
1097        let c = not_before(Timestamp(5000));
1098        let caveats = BoundedCaveats::try_new(&c).unwrap();
1099        assert!(matches!(
1100            evaluate_caveats(&caveats, Timestamp(4999)),
1101            Err(KernelError::NotBeforeViolation)
1102        ));
1103    }
1104
1105    #[test]
1106    fn evaluate_caveats_not_after_now_lte_ts_passes() {
1107        let c = not_after(Timestamp(3000));
1108        let caveats = BoundedCaveats::try_new(&c).unwrap();
1109        // now == ts: passes (not_after fails only when now > ts)
1110        assert!(evaluate_caveats(&caveats, Timestamp(3000)).is_ok());
1111        // now < ts: also passes
1112        assert!(evaluate_caveats(&caveats, Timestamp(1000)).is_ok());
1113    }
1114
1115    #[test]
1116    fn evaluate_caveats_not_after_now_gt_ts_returns_violation() {
1117        let c = not_after(Timestamp(3000));
1118        let caveats = BoundedCaveats::try_new(&c).unwrap();
1119        assert!(matches!(
1120            evaluate_caveats(&caveats, Timestamp(3001)),
1121            Err(KernelError::NotAfterViolation)
1122        ));
1123    }
1124
1125    #[test]
1126    fn evaluate_caveats_empty_always_passes() {
1127        let caveats = BoundedCaveats::try_new(&[]).unwrap();
1128        assert!(evaluate_caveats(&caveats, Timestamp(u64::MAX)).is_ok());
1129    }
1130
1131    // ── issue_credential + parse_payload (round-trip) ──
1132
1133    #[test]
1134    fn issue_credential_round_trip_leaf() {
1135        let issuer = make_test_identity(b"issuer");
1136        let child  = make_test_identity(b"child");
1137        let scope_bytes  = make_test_scope();
1138        let caveat_bytes = make_test_caveats();
1139        let child_pk = *child.public_key().0;
1140
1141        let scope   = BoundedScope::try_new(&scope_bytes).unwrap();
1142        let caveats = BoundedCaveats::try_new(&caveat_bytes).unwrap();
1143        let manifest = DelegationManifest {
1144            child_pk: PublicKey(&child_pk),
1145            role:     Role::Leaf,
1146            depth:    Depth(1),
1147            scope,
1148            caveats,
1149        };
1150        let mut payload_buf = [0u8; MAX_PAYLOAD_SIZE];
1151        let mut sig_buf     = [0u8; SIG_SIZE];
1152        let n = issue_credential(&issuer, &manifest, &mut payload_buf, &mut sig_buf).unwrap();
1153
1154        let parsed = parse_payload_pub(&payload_buf[..n]).unwrap();
1155        assert_eq!(parsed.role, Role::Leaf);
1156        assert_eq!(parsed.depth, Depth(1));
1157        assert_eq!(parsed.child_pk.0, &child_pk);
1158    }
1159
1160    #[test]
1161    fn issue_credential_round_trip_node() {
1162        let issuer = make_test_identity(b"issuer");
1163        let child  = make_test_identity(b"child-node");
1164        let scope_bytes  = make_test_scope();
1165        let caveat_bytes = make_test_caveats();
1166        let child_pk = *child.public_key().0;
1167
1168        let scope   = BoundedScope::try_new(&scope_bytes).unwrap();
1169        let caveats = BoundedCaveats::try_new(&caveat_bytes).unwrap();
1170        let manifest = DelegationManifest {
1171            child_pk: PublicKey(&child_pk),
1172            role:     Role::Node,
1173            depth:    Depth(1),
1174            scope,
1175            caveats,
1176        };
1177        let mut payload_buf = [0u8; MAX_PAYLOAD_SIZE];
1178        let mut sig_buf     = [0u8; SIG_SIZE];
1179        let n = issue_credential(&issuer, &manifest, &mut payload_buf, &mut sig_buf).unwrap();
1180
1181        let parsed = parse_payload_pub(&payload_buf[..n]).unwrap();
1182        assert_eq!(parsed.role, Role::Node);
1183    }
1184
1185    #[test]
1186    fn issue_credential_depth_too_large_returns_chain_too_deep() {
1187        let issuer = make_test_identity(b"issuer");
1188        let child  = make_test_identity(b"child");
1189        let scope_bytes = make_test_scope();
1190        let scope   = BoundedScope::try_new(&scope_bytes).unwrap();
1191        let caveats = BoundedCaveats::try_new(&[]).unwrap();
1192        let manifest = DelegationManifest {
1193            child_pk: child.public_key(),
1194            role:     Role::Leaf,
1195            depth:    Depth(MAX_DEPTH + 1),
1196            scope,
1197            caveats,
1198        };
1199        let mut payload_out = [0u8; MAX_PAYLOAD_SIZE];
1200        let mut sig_out = [0u8; SIG_SIZE];
1201        assert!(matches!(
1202            issue_credential(&issuer, &manifest, &mut payload_out, &mut sig_out),
1203            Err(KernelError::ChainTooDeep)
1204        ));
1205    }
1206
1207    #[test]
1208    fn issue_credential_leaf_role_encoded_as_zero() {
1209        let issuer = make_test_identity(b"issuer");
1210        let child  = make_test_identity(b"child");
1211        let scope_bytes = make_test_scope();
1212        let scope   = BoundedScope::try_new(&scope_bytes).unwrap();
1213        let caveats = BoundedCaveats::try_new(&[]).unwrap();
1214        let manifest = DelegationManifest {
1215            child_pk: child.public_key(),
1216            role:     Role::Leaf,
1217            depth:    Depth(1),
1218            scope,
1219            caveats,
1220        };
1221        let mut payload_out = [0u8; MAX_PAYLOAD_SIZE];
1222        let mut sig_out = [0u8; SIG_SIZE];
1223        let n = issue_credential(&issuer, &manifest, &mut payload_out, &mut sig_out).unwrap();
1224        // The role byte sits immediately after child_pk (PK_SIZE bytes).
1225        assert_eq!(payload_out[PK_SIZE], 0u8);
1226        let _ = n;
1227    }
1228
1229    #[test]
1230    fn issue_credential_node_role_encoded_as_one() {
1231        let issuer = make_test_identity(b"issuer");
1232        let child  = make_test_identity(b"child");
1233        let scope_bytes = make_test_scope();
1234        let scope   = BoundedScope::try_new(&scope_bytes).unwrap();
1235        let caveats = BoundedCaveats::try_new(&[]).unwrap();
1236        let manifest = DelegationManifest {
1237            child_pk: child.public_key(),
1238            role:     Role::Node,
1239            depth:    Depth(1),
1240            scope,
1241            caveats,
1242        };
1243        let mut payload_out = [0u8; MAX_PAYLOAD_SIZE];
1244        let mut sig_out = [0u8; SIG_SIZE];
1245        let n = issue_credential(&issuer, &manifest, &mut payload_out, &mut sig_out).unwrap();
1246        assert_eq!(payload_out[PK_SIZE], 1u8);
1247        let _ = n;
1248    }
1249
1250    // ── write_credential_chain / read_credential_chain (wire round-trip) ──
1251
1252    /// Builds a single-hop chain: root issues to child, returns the wire bytes and the
1253    /// original issuer_pk and sig_bytes for comparison.
1254    fn build_single_hop_wire() -> (Vec<u8>, [u8; PK_SIZE], [u8; SIG_SIZE], Vec<u8>) {
1255        let issuer = make_test_identity(b"root");
1256        let child  = make_test_identity(b"child");
1257        let scope_bytes = make_test_scope();
1258        let scope   = BoundedScope::try_new(&scope_bytes).unwrap();
1259        let caveats = BoundedCaveats::try_new(&[]).unwrap();
1260        let manifest = DelegationManifest {
1261            child_pk: child.public_key(),
1262            role:     Role::Leaf,
1263            depth:    Depth(1),
1264            scope,
1265            caveats,
1266        };
1267        let mut payload_buf = [0u8; MAX_PAYLOAD_SIZE];
1268        let mut sig_buf     = [0u8; SIG_SIZE];
1269        let payload_len = issue_credential(&issuer, &manifest, &mut payload_buf, &mut sig_buf).unwrap();
1270
1271        let issuer_pk_copy = *issuer.public_key().0;
1272        let sig_copy       = sig_buf;
1273        let payload_copy   = payload_buf[..payload_len].to_vec();
1274
1275        let cred = Credential {
1276            issuer_pk: PublicKey(&issuer_pk_copy),
1277            signature: Signature(&sig_copy),
1278            payload:   &payload_copy,
1279        };
1280
1281        let chain = [cred];
1282        // Allocate a generously sized wire buffer.
1283        let mut wire = vec![0u8; 5 + CREDENTIAL_FIXED_SIZE + MAX_PAYLOAD_SIZE];
1284        let n = write_credential_chain(&mut wire, &chain).unwrap();
1285        wire.truncate(n);
1286        (wire, issuer_pk_copy, sig_copy, payload_copy)
1287    }
1288
1289    #[test]
1290    fn wire_round_trip_single_credential() {
1291        let (wire, expected_pk, expected_sig, expected_payload) = build_single_hop_wire();
1292
1293        let mut chain_buf: [Credential; 16] = core::array::from_fn(|_| Credential {
1294            issuer_pk: PublicKey(&[0u8; PK_SIZE]),
1295            signature: Signature(&[0u8; SIG_SIZE]),
1296            payload:   &[],
1297        });
1298        // Borrow trick: read back from wire slice.
1299        let n = read_credential_chain(&wire, &mut chain_buf).unwrap();
1300        assert_eq!(n, 1);
1301        assert_eq!(chain_buf[0].issuer_pk.0, &expected_pk);
1302        assert_eq!(chain_buf[0].signature.0, &expected_sig);
1303        assert_eq!(chain_buf[0].payload, expected_payload.as_slice());
1304    }
1305
1306    #[test]
1307    fn wire_empty_chain_write_then_read_returns_empty_chain() {
1308        // write_credential_chain with count=0 is valid to write.
1309        let mut wire = vec![0u8; 16];
1310        let n = write_credential_chain(&mut wire, &[]).unwrap();
1311        wire.truncate(n);
1312
1313        let mut chain_buf: [Credential; 16] = core::array::from_fn(|_| Credential {
1314            issuer_pk: PublicKey(&[0u8; PK_SIZE]),
1315            signature: Signature(&[0u8; SIG_SIZE]),
1316            payload:   &[],
1317        });
1318        assert!(matches!(
1319            read_credential_chain(&wire, &mut chain_buf),
1320            Err(KernelError::EmptyChain)
1321        ));
1322    }
1323
1324    #[test]
1325    fn wire_count_exceeds_max_depth_returns_chain_too_deep() {
1326        // Manually craft a wire buffer with a count of MAX_DEPTH + 1.
1327        let mut wire = vec![0u8; 16];
1328        wire[0] = 0x01; // WIRE_VERSION
1329        let bad_count = (MAX_DEPTH + 1).to_le_bytes();
1330        wire[1..5].copy_from_slice(&bad_count);
1331
1332        let mut chain_buf: [Credential; 32] = core::array::from_fn(|_| Credential {
1333            issuer_pk: PublicKey(&[0u8; PK_SIZE]),
1334            signature: Signature(&[0u8; SIG_SIZE]),
1335            payload:   &[],
1336        });
1337        assert!(matches!(
1338            read_credential_chain(&wire, &mut chain_buf),
1339            Err(KernelError::ChainTooDeep)
1340        ));
1341    }
1342
1343    #[test]
1344    fn wire_wrong_version_returns_version_mismatch() {
1345        let (mut wire, _, _, _) = build_single_hop_wire();
1346        wire[0] = 0xFF; // corrupt version
1347        let mut chain_buf: [Credential; 16] = core::array::from_fn(|_| Credential {
1348            issuer_pk: PublicKey(&[0u8; PK_SIZE]),
1349            signature: Signature(&[0u8; SIG_SIZE]),
1350            payload:   &[],
1351        });
1352        assert!(matches!(
1353            read_credential_chain(&wire, &mut chain_buf),
1354            Err(KernelError::WireVersionMismatch)
1355        ));
1356    }
1357
1358    #[test]
1359    fn wire_truncated_buffer_returns_wire_truncated() {
1360        let (wire, _, _, _) = build_single_hop_wire();
1361        // Truncate to just the header, leaving the credential body incomplete.
1362        let truncated = &wire[..5];
1363        let mut chain_buf: [Credential; 16] = core::array::from_fn(|_| Credential {
1364            issuer_pk: PublicKey(&[0u8; PK_SIZE]),
1365            signature: Signature(&[0u8; SIG_SIZE]),
1366            payload:   &[],
1367        });
1368        assert!(matches!(
1369            read_credential_chain(truncated, &mut chain_buf),
1370            Err(KernelError::WireTruncated)
1371        ));
1372    }
1373
1374    #[test]
1375    fn wire_trailing_bytes_after_valid_chain() {
1376        // The wire reader stops consuming after the declared credentials;
1377        // trailing bytes are NOT consumed (no anti-malleability check at the
1378        // chain level — the function returns Ok with the correct count).
1379        // This test documents the actual behaviour.
1380        let (wire, _, _, _) = build_single_hop_wire();
1381        let mut extended = wire.clone();
1382        extended.extend_from_slice(&[0xFFu8; 8]);
1383
1384        let mut chain_buf: [Credential; 16] = core::array::from_fn(|_| Credential {
1385            issuer_pk: PublicKey(&[0u8; PK_SIZE]),
1386            signature: Signature(&[0u8; SIG_SIZE]),
1387            payload:   &[],
1388        });
1389        // read_credential_chain does not enforce no-trailing-bytes at the outer
1390        // level; it returns Ok(1) with trailing bytes ignored.
1391        let result = read_credential_chain(&extended, &mut chain_buf);
1392        assert!(result.is_ok());
1393        assert_eq!(result.unwrap(), 1);
1394    }
1395
1396    // ── verify_delegation (full chain) ──
1397
1398    /// Builds a complete, valid, one-hop Credential value whose lifetime is
1399    /// tied to the provided storage arrays.  All fields are copied into the
1400    /// caller-supplied buffers so the returned `Credential` can borrow them.
1401    fn build_credential<'a>(
1402        issuer:         &'a impl IdentitySigner,
1403        child_pk_bytes: &'a [u8; PK_SIZE],
1404        role:           Role,
1405        depth:          Depth,
1406        scope_bytes:    &[u8],
1407        caveat_bytes:   &[u8],
1408        payload_store:  &'a mut [u8; MAX_PAYLOAD_SIZE],
1409        sig_store:      &'a mut [u8; SIG_SIZE],
1410    ) -> Credential<'a> {
1411        let scope   = BoundedScope::try_new(scope_bytes).unwrap();
1412        let caveats = BoundedCaveats::try_new(caveat_bytes).unwrap();
1413        let manifest = DelegationManifest {
1414            child_pk: PublicKey(child_pk_bytes),
1415            role,
1416            depth,
1417            scope,
1418            caveats,
1419        };
1420        let payload_len =
1421            issue_credential(issuer, &manifest, payload_store, sig_store).unwrap();
1422        Credential {
1423            issuer_pk: issuer.public_key(),
1424            signature: Signature(sig_store),
1425            payload:   &payload_store[..payload_len],
1426        }
1427    }
1428
1429    #[test]
1430    fn verify_delegation_single_hop_ok() {
1431        let root  = make_test_identity(b"root");
1432        let child = make_test_identity(b"child");
1433        let scope_bytes  = make_test_scope();
1434        let caveat_bytes = make_test_caveats();
1435        let child_pk = *child.public_key().0;
1436
1437        let mut payload_store = [0u8; MAX_PAYLOAD_SIZE];
1438        let mut sig_store     = [0u8; SIG_SIZE];
1439
1440        let cred = build_credential(
1441            &root, &child_pk, Role::Leaf, Depth(1),
1442            &scope_bytes, &caveat_bytes,
1443            &mut payload_store, &mut sig_store,
1444        );
1445
1446        let chain = [cred];
1447        assert!(verify_delegation(root.public_key(), &chain, Timestamp(0)).is_ok());
1448    }
1449
1450    #[test]
1451    fn verify_delegation_two_hop_ok() {
1452        let root  = make_test_identity(b"root");
1453        let node  = make_test_identity(b"node");
1454        let leaf  = make_test_identity(b"leaf");
1455        let scope_bytes  = make_test_scope();
1456        let caveat_bytes = make_test_caveats();
1457
1458        let node_pk = *node.public_key().0;
1459        let leaf_pk = *leaf.public_key().0;
1460
1461        // root → node (depth 1, Node role)
1462        let mut p1 = [0u8; MAX_PAYLOAD_SIZE];
1463        let mut s1 = [0u8; SIG_SIZE];
1464        let cred1 = build_credential(
1465            &root, &node_pk, Role::Node, Depth(1),
1466            &scope_bytes, &caveat_bytes, &mut p1, &mut s1,
1467        );
1468
1469        // node → leaf (depth 2, Leaf role)
1470        let mut p2 = [0u8; MAX_PAYLOAD_SIZE];
1471        let mut s2 = [0u8; SIG_SIZE];
1472        let cred2 = build_credential(
1473            &node, &leaf_pk, Role::Leaf, Depth(2),
1474            &scope_bytes, &caveat_bytes, &mut p2, &mut s2,
1475        );
1476
1477        let chain = [cred1, cred2];
1478        assert!(verify_delegation(root.public_key(), &chain, Timestamp(0)).is_ok());
1479    }
1480
1481    #[test]
1482    fn verify_delegation_empty_chain_returns_empty_chain() {
1483        let root = make_test_identity(b"root");
1484        assert!(matches!(
1485            verify_delegation(root.public_key(), &[], Timestamp(0)),
1486            Err(KernelError::EmptyChain)
1487        ));
1488    }
1489
1490    #[test]
1491    fn verify_delegation_wrong_root_pk_returns_parent_key_mismatch() {
1492        let root   = make_test_identity(b"root");
1493        let wrong  = make_test_identity(b"wrong-root");
1494        let child  = make_test_identity(b"child");
1495        let scope_bytes  = make_test_scope();
1496        let caveat_bytes = make_test_caveats();
1497        let child_pk = *child.public_key().0;
1498
1499        let mut payload_store = [0u8; MAX_PAYLOAD_SIZE];
1500        let mut sig_store     = [0u8; SIG_SIZE];
1501
1502        let cred = build_credential(
1503            &root, &child_pk, Role::Leaf, Depth(1),
1504            &scope_bytes, &caveat_bytes,
1505            &mut payload_store, &mut sig_store,
1506        );
1507        let chain = [cred];
1508        assert!(matches!(
1509            verify_delegation(wrong.public_key(), &chain, Timestamp(0)),
1510            Err(KernelError::ParentKeyMismatch)
1511        ));
1512    }
1513
1514    #[test]
1515    fn verify_delegation_tampered_signature_returns_error() {
1516        let root  = make_test_identity(b"root");
1517        let child = make_test_identity(b"child");
1518        let scope_bytes  = make_test_scope();
1519        let caveat_bytes = make_test_caveats();
1520        let child_pk = *child.public_key().0;
1521
1522        let mut payload_store = [0u8; MAX_PAYLOAD_SIZE];
1523        let mut sig_store     = [0u8; SIG_SIZE];
1524
1525        build_credential(
1526            &root, &child_pk, Role::Leaf, Depth(1),
1527            &scope_bytes, &caveat_bytes,
1528            &mut payload_store, &mut sig_store,
1529        );
1530
1531        // Corrupt one byte of the signature.
1532        sig_store[100] ^= 0xFF;
1533
1534        let scope   = BoundedScope::try_new(&scope_bytes).unwrap();
1535        let caveats = BoundedCaveats::try_new(&caveat_bytes).unwrap();
1536        let manifest = DelegationManifest {
1537            child_pk: PublicKey(&child_pk),
1538            role:     Role::Leaf,
1539            depth:    Depth(1),
1540            scope,
1541            caveats,
1542        };
1543        let mut payload_buf = [0u8; MAX_PAYLOAD_SIZE];
1544        let mut dummy_sig   = [0u8; SIG_SIZE];
1545        let payload_len =
1546            issue_credential(&root, &manifest, &mut payload_buf, &mut dummy_sig).unwrap();
1547
1548        let tampered_cred = Credential {
1549            issuer_pk: root.public_key(),
1550            signature: Signature(&sig_store),
1551            payload:   &payload_buf[..payload_len],
1552        };
1553        let chain = [tampered_cred];
1554        let result = verify_delegation(root.public_key(), &chain, Timestamp(0));
1555        assert!(
1556            matches!(result, Err(KernelError::SignatureInvalid))
1557                || matches!(result, Err(KernelError::SignatureDecodingFailed)),
1558            "expected SignatureInvalid or SignatureDecodingFailed, got {:?}",
1559            result
1560        );
1561    }
1562
1563    #[test]
1564    fn verify_delegation_expired_caveat_returns_not_after_violation() {
1565        let root  = make_test_identity(b"root");
1566        let child = make_test_identity(b"child");
1567        let scope_bytes = make_test_scope();
1568
1569        // expires at t=1000
1570        let caveat = not_after(Timestamp(1000));
1571        let scope   = BoundedScope::try_new(&scope_bytes).unwrap();
1572        let caveats = BoundedCaveats::try_new(&caveat).unwrap();
1573        let child_pk = *child.public_key().0;
1574        let manifest = DelegationManifest {
1575            child_pk: PublicKey(&child_pk),
1576            role:     Role::Leaf,
1577            depth:    Depth(1),
1578            scope,
1579            caveats,
1580        };
1581
1582        let mut payload_store = [0u8; MAX_PAYLOAD_SIZE];
1583        let mut sig_store     = [0u8; SIG_SIZE];
1584        let payload_len =
1585            issue_credential(&root, &manifest, &mut payload_store, &mut sig_store).unwrap();
1586
1587        let cred = Credential {
1588            issuer_pk: root.public_key(),
1589            signature: Signature(&sig_store),
1590            payload:   &payload_store[..payload_len],
1591        };
1592        let chain = [cred];
1593        // now = 1001, after expiry
1594        assert!(matches!(
1595            verify_delegation(root.public_key(), &chain, Timestamp(1001)),
1596            Err(KernelError::NotAfterViolation)
1597        ));
1598    }
1599
1600    #[test]
1601    fn verify_delegation_not_yet_valid_returns_not_before_violation() {
1602        let root  = make_test_identity(b"root");
1603        let child = make_test_identity(b"child");
1604        let scope_bytes = make_test_scope();
1605
1606        // valid from t=5000
1607        let caveat  = not_before(Timestamp(5000));
1608        let scope   = BoundedScope::try_new(&scope_bytes).unwrap();
1609        let caveats = BoundedCaveats::try_new(&caveat).unwrap();
1610        let child_pk = *child.public_key().0;
1611        let manifest = DelegationManifest {
1612            child_pk: PublicKey(&child_pk),
1613            role:     Role::Leaf,
1614            depth:    Depth(1),
1615            scope,
1616            caveats,
1617        };
1618
1619        let mut payload_store = [0u8; MAX_PAYLOAD_SIZE];
1620        let mut sig_store     = [0u8; SIG_SIZE];
1621        let payload_len =
1622            issue_credential(&root, &manifest, &mut payload_store, &mut sig_store).unwrap();
1623
1624        let cred = Credential {
1625            issuer_pk: root.public_key(),
1626            signature: Signature(&sig_store),
1627            payload:   &payload_store[..payload_len],
1628        };
1629        let chain = [cred];
1630        // now = 4999, before the not_before timestamp
1631        assert!(matches!(
1632            verify_delegation(root.public_key(), &chain, Timestamp(4999)),
1633            Err(KernelError::NotBeforeViolation)
1634        ));
1635    }
1636
1637    #[test]
1638    fn verify_delegation_leaf_cannot_delegate() {
1639        let root = make_test_identity(b"root");
1640        let node = make_test_identity(b"node");
1641        let leaf = make_test_identity(b"leaf");
1642        let scope_bytes  = make_test_scope();
1643        let caveat_bytes = make_test_caveats();
1644
1645        let node_pk = *node.public_key().0;
1646        let leaf_pk = *leaf.public_key().0;
1647
1648        // root → node with Leaf role (should not be able to sub-delegate)
1649        let mut p1 = [0u8; MAX_PAYLOAD_SIZE];
1650        let mut s1 = [0u8; SIG_SIZE];
1651        let cred1 = build_credential(
1652            &root, &node_pk, Role::Leaf, Depth(1),
1653            &scope_bytes, &caveat_bytes, &mut p1, &mut s1,
1654        );
1655
1656        // node → leaf: node had Leaf role, so this must fail.
1657        let mut p2 = [0u8; MAX_PAYLOAD_SIZE];
1658        let mut s2 = [0u8; SIG_SIZE];
1659        let cred2 = build_credential(
1660            &node, &leaf_pk, Role::Leaf, Depth(2),
1661            &scope_bytes, &caveat_bytes, &mut p2, &mut s2,
1662        );
1663
1664        let chain = [cred1, cred2];
1665        assert!(matches!(
1666            verify_delegation(root.public_key(), &chain, Timestamp(0)),
1667            Err(KernelError::LeafCannotDelegate)
1668        ));
1669    }
1670
1671    #[test]
1672    fn verify_delegation_depth_out_of_sequence_returns_depth_mismatch() {
1673        let root  = make_test_identity(b"root");
1674        let child = make_test_identity(b"child");
1675        let scope_bytes  = make_test_scope();
1676        let caveat_bytes = make_test_caveats();
1677        let child_pk = *child.public_key().0;
1678
1679        // Issue at depth 2 instead of the required 1 for the first credential.
1680        let mut payload_store = [0u8; MAX_PAYLOAD_SIZE];
1681        let mut sig_store     = [0u8; SIG_SIZE];
1682        let cred = build_credential(
1683            &root, &child_pk, Role::Leaf, Depth(2),
1684            &scope_bytes, &caveat_bytes,
1685            &mut payload_store, &mut sig_store,
1686        );
1687        let chain = [cred];
1688        assert!(matches!(
1689            verify_delegation(root.public_key(), &chain, Timestamp(0)),
1690            Err(KernelError::DepthMismatch)
1691        ));
1692    }
1693}