Skip to main content

bcx_crypto/
envelope.rs

1use crate::VerificationError;
2use bcx_core::Digest;
3use bcx_wire::WireLimits;
4use core::fmt;
5
6/// Signature algorithms named by BCX metadata.
7#[derive(Clone, Copy, Debug, Eq, PartialEq)]
8pub enum SignatureAlgorithm {
9    /// Ed25519 for compact classical signatures.
10    Ed25519,
11    /// ML-DSA-65 for post-quantum-ready deployments.
12    MlDsa65,
13    /// SLH-DSA-SHA2-128s for conservative stateless signatures.
14    SlhDsaSha2_128s,
15    /// Hybrid Ed25519 plus ML-DSA-65 signature envelope.
16    ///
17    /// Canonical layout is `[ed25519: 64 bytes][ml-dsa-65: 3293 bytes]`.
18    /// Verifiers must verify both components before returning `Ok`.
19    HybridEd25519MlDsa65,
20}
21
22impl SignatureAlgorithm {
23    /// Ed25519 signature byte length.
24    pub const ED25519_SIGNATURE_LEN: usize = 64;
25    /// ML-DSA-65 signature byte length.
26    pub const ML_DSA_65_SIGNATURE_LEN: usize = 3_293;
27    /// SLH-DSA-SHA2-128s signature byte length.
28    pub const SLH_DSA_SHA2_128S_SIGNATURE_LEN: usize = 7_856;
29    /// Hybrid Ed25519 plus ML-DSA-65 signature byte length.
30    pub const HYBRID_ED25519_ML_DSA_65_SIGNATURE_LEN: usize =
31        Self::ED25519_SIGNATURE_LEN + Self::ML_DSA_65_SIGNATURE_LEN;
32
33    /// Returns the exact signature length admitted for this algorithm.
34    #[must_use]
35    pub const fn expected_signature_len(self) -> usize {
36        match self {
37            Self::Ed25519 => Self::ED25519_SIGNATURE_LEN,
38            Self::MlDsa65 => Self::ML_DSA_65_SIGNATURE_LEN,
39            Self::SlhDsaSha2_128s => Self::SLH_DSA_SHA2_128S_SIGNATURE_LEN,
40            Self::HybridEd25519MlDsa65 => Self::HYBRID_ED25519_ML_DSA_65_SIGNATURE_LEN,
41        }
42    }
43
44    /// Splits a hybrid signature into Ed25519 and ML-DSA-65 components.
45    ///
46    /// Layout: `[ed25519: 64 bytes][ml-dsa-65: 3293 bytes]`. Verifiers for
47    /// `HybridEd25519MlDsa65` must verify both returned components.
48    #[must_use]
49    pub fn split_hybrid(self, signature: &[u8]) -> Option<(&[u8], &[u8])> {
50        match self {
51            Self::HybridEd25519MlDsa65
52                if signature.len() == Self::HYBRID_ED25519_ML_DSA_65_SIGNATURE_LEN =>
53            {
54                Some(signature.split_at(Self::ED25519_SIGNATURE_LEN))
55            }
56            Self::Ed25519 | Self::MlDsa65 | Self::SlhDsaSha2_128s | Self::HybridEd25519MlDsa65 => {
57                None
58            }
59        }
60    }
61}
62
63/// Closed algorithm admission policy for a verification context.
64#[derive(Clone, Copy, Debug, Eq, PartialEq)]
65pub struct AlgorithmPolicy<'a> {
66    admitted: &'a [SignatureAlgorithm],
67}
68
69impl<'a> AlgorithmPolicy<'a> {
70    /// Creates an algorithm admission policy from an explicit allow-list.
71    ///
72    /// Admitting several algorithms lets the sender choose any admitted
73    /// algorithm, including the weakest one. High-assurance deployments should
74    /// admit exactly one algorithm for an operation class unless downgrade
75    /// behavior is explicitly part of the profile security contract.
76    pub const fn new(admitted: &'a [SignatureAlgorithm]) -> Result<Self, VerificationError> {
77        if admitted.is_empty() {
78            Err(VerificationError::EmptyAlgorithmPolicy)
79        } else {
80            Ok(Self { admitted })
81        }
82    }
83
84    /// Creates an explicit deny-all algorithm policy.
85    #[must_use]
86    pub const fn deny_all() -> Self {
87        Self { admitted: &[] }
88    }
89
90    /// Returns true when the algorithm is admitted by this policy.
91    #[must_use]
92    pub const fn admits(&self, algorithm: SignatureAlgorithm) -> bool {
93        let mut index = 0;
94        while index < self.admitted.len() {
95            if self.admitted[index].eq_const(algorithm) {
96                return true;
97            }
98            index += 1;
99        }
100        false
101    }
102}
103
104/// Single-algorithm admission policy for high-assurance verification contexts.
105///
106/// This avoids sender-choice downgrade behavior entirely: exactly one
107/// algorithm is admitted for the operation class.
108#[derive(Clone, Copy, Debug, Eq, PartialEq)]
109pub struct ExactAlgorithmPolicy {
110    admitted: SignatureAlgorithm,
111}
112
113impl ExactAlgorithmPolicy {
114    /// Creates a policy that admits exactly one signature algorithm.
115    #[must_use]
116    pub const fn new(admitted: SignatureAlgorithm) -> Self {
117        Self { admitted }
118    }
119
120    /// Returns the only admitted algorithm.
121    #[must_use]
122    pub const fn algorithm(self) -> SignatureAlgorithm {
123        self.admitted
124    }
125
126    /// Returns true when the algorithm is the exact admitted algorithm.
127    #[must_use]
128    pub const fn admits(self, algorithm: SignatureAlgorithm) -> bool {
129        self.admitted.eq_const(algorithm)
130    }
131}
132
133impl SignatureAlgorithm {
134    const fn eq_const(self, other: Self) -> bool {
135        match (self, other) {
136            (Self::Ed25519, Self::Ed25519) => true,
137            (Self::Ed25519, Self::MlDsa65) => false,
138            (Self::Ed25519, Self::SlhDsaSha2_128s) => false,
139            (Self::Ed25519, Self::HybridEd25519MlDsa65) => false,
140            (Self::MlDsa65, Self::Ed25519) => false,
141            (Self::MlDsa65, Self::MlDsa65) => true,
142            (Self::MlDsa65, Self::SlhDsaSha2_128s) => false,
143            (Self::MlDsa65, Self::HybridEd25519MlDsa65) => false,
144            (Self::SlhDsaSha2_128s, Self::Ed25519) => false,
145            (Self::SlhDsaSha2_128s, Self::MlDsa65) => false,
146            (Self::SlhDsaSha2_128s, Self::SlhDsaSha2_128s) => true,
147            (Self::SlhDsaSha2_128s, Self::HybridEd25519MlDsa65) => false,
148            (Self::HybridEd25519MlDsa65, Self::Ed25519) => false,
149            (Self::HybridEd25519MlDsa65, Self::MlDsa65) => false,
150            (Self::HybridEd25519MlDsa65, Self::SlhDsaSha2_128s) => false,
151            (Self::HybridEd25519MlDsa65, Self::HybridEd25519MlDsa65) => true,
152        }
153    }
154}
155
156/// Signature metadata over a canonical BCX payload.
157#[derive(Clone, Copy, Eq, PartialEq)]
158pub struct SignatureEnvelope<'a> {
159    key_id: Digest,
160    algorithm: SignatureAlgorithm,
161    signature: &'a [u8],
162}
163
164impl<'a> SignatureEnvelope<'a> {
165    /// Creates a validated signature envelope.
166    pub fn new(
167        key_id: Digest,
168        algorithm: SignatureAlgorithm,
169        signature: &'a [u8],
170        limits: WireLimits,
171    ) -> Result<Self, VerificationError> {
172        let envelope = Self {
173            key_id,
174            algorithm,
175            signature,
176        };
177        match envelope.validate(limits) {
178            Ok(()) => Ok(envelope),
179            Err(error) => Err(error),
180        }
181    }
182
183    /// Validates envelope shape before algorithm dispatch.
184    pub(crate) fn validate(&self, limits: WireLimits) -> Result<(), VerificationError> {
185        if self.key_id.is_zero() {
186            return Err(VerificationError::EmptyKeyId);
187        }
188        if self.signature.is_empty() {
189            return Err(VerificationError::EmptySignature);
190        }
191        if self.signature.len() > limits.maximum_message_len() {
192            return Err(VerificationError::SignatureTooLarge);
193        }
194        if self.signature.len() != self.algorithm.expected_signature_len() {
195            return Err(VerificationError::InvalidSignature);
196        }
197        Ok(())
198    }
199
200    /// Returns the signing key or certificate-chain commitment.
201    #[must_use]
202    pub const fn key_id(&self) -> Digest {
203        self.key_id
204    }
205
206    /// Returns the signature algorithm.
207    #[must_use]
208    pub const fn algorithm(&self) -> SignatureAlgorithm {
209        self.algorithm
210    }
211
212    /// Returns raw signature bytes.
213    #[must_use]
214    pub const fn signature(&self) -> &'a [u8] {
215        self.signature
216    }
217}
218
219impl<'a> fmt::Debug for SignatureEnvelope<'a> {
220    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
221        formatter
222            .debug_struct("SignatureEnvelope")
223            .field("key_id", &self.key_id)
224            .field("algorithm", &self.algorithm)
225            .field(
226                "signature",
227                &format_args!("[{} bytes]", self.signature.len()),
228            )
229            .finish()
230    }
231}
232
233/// Payload paired with a signature envelope.
234#[derive(Clone, Copy, Debug, Eq, PartialEq)]
235pub struct SignedEnvelope<'a, T> {
236    payload: T,
237    signature: SignatureEnvelope<'a>,
238}
239
240impl<'a, T> SignedEnvelope<'a, T> {
241    /// Creates a signed envelope from a payload and validated signature metadata.
242    #[must_use]
243    pub const fn new(payload: T, signature: SignatureEnvelope<'a>) -> Self {
244        Self { payload, signature }
245    }
246
247    /// Verifies a detached canonical byte representation of this envelope.
248    ///
249    /// The caller must ensure `canonical_payload` is the exact canonical
250    /// encoding of `self.payload()`. BCX will replace this detached helper with
251    /// typed canonical encoding once `bcx-codec` is introduced.
252    pub fn verify_detached_bytes<V: Verifier>(
253        &self,
254        verifier: &V,
255        algorithm_policy: &AlgorithmPolicy<'_>,
256        canonical_payload: &[u8],
257        limits: WireLimits,
258    ) -> Result<(), VerificationError> {
259        if !algorithm_policy.admits(self.signature.algorithm) {
260            return Err(VerificationError::AlgorithmNotAdmitted);
261        }
262        self.verify_admitted_detached_bytes(verifier, canonical_payload, limits)
263    }
264
265    /// Verifies detached bytes with an exact single-algorithm policy.
266    ///
267    /// Prefer this helper for high-assurance profiles where sender-choice
268    /// algorithm downgrade is not acceptable.
269    pub fn verify_detached_bytes_exact<V: Verifier>(
270        &self,
271        verifier: &V,
272        algorithm_policy: ExactAlgorithmPolicy,
273        canonical_payload: &[u8],
274        limits: WireLimits,
275    ) -> Result<(), VerificationError> {
276        if !algorithm_policy.admits(self.signature.algorithm) {
277            return Err(VerificationError::AlgorithmNotAdmitted);
278        }
279        self.verify_admitted_detached_bytes(verifier, canonical_payload, limits)
280    }
281
282    fn verify_admitted_detached_bytes<V: Verifier>(
283        &self,
284        verifier: &V,
285        canonical_payload: &[u8],
286        limits: WireLimits,
287    ) -> Result<(), VerificationError> {
288        self.signature.validate(limits)?;
289        if canonical_payload.len() > limits.maximum_message_len() {
290            return Err(VerificationError::PayloadTooLarge);
291        }
292        match self.signature.algorithm {
293            SignatureAlgorithm::HybridEd25519MlDsa65 => verifier
294                .verify_hybrid(&self.signature, canonical_payload)
295                .map(|_| ()),
296            SignatureAlgorithm::Ed25519
297            | SignatureAlgorithm::MlDsa65
298            | SignatureAlgorithm::SlhDsaSha2_128s => {
299                verifier.verify(&self.signature, canonical_payload)
300            }
301        }
302    }
303
304    /// Returns the payload value.
305    #[must_use]
306    pub const fn payload(&self) -> &T {
307        &self.payload
308    }
309
310    /// Returns the signature envelope.
311    #[must_use]
312    pub const fn signature(&self) -> SignatureEnvelope<'a> {
313        self.signature
314    }
315}
316
317/// Proof that both components of a hybrid signature verified.
318#[derive(Clone, Copy, Debug, Eq, PartialEq)]
319pub struct HybridVerified(());
320
321/// Verification backend boundary for hybrid signature components.
322pub trait HybridVerifier {
323    /// Verifies the Ed25519 component of a hybrid signature.
324    fn verify_ed25519(
325        &self,
326        ed25519_signature: &[u8],
327        canonical_payload: &[u8],
328    ) -> Result<(), VerificationError>;
329
330    /// Verifies the ML-DSA-65 component of a hybrid signature.
331    fn verify_ml_dsa_65(
332        &self,
333        ml_dsa_65_signature: &[u8],
334        canonical_payload: &[u8],
335    ) -> Result<(), VerificationError>;
336
337    /// Verifies both components of a hybrid signature.
338    ///
339    /// Implementors must run component verification to completion and must not
340    /// use intermediate component failures to skip later component work. The
341    /// default implementation always invokes both component methods before
342    /// combining their results.
343    fn verify_hybrid(
344        &self,
345        envelope: &SignatureEnvelope<'_>,
346        canonical_payload: &[u8],
347    ) -> Result<HybridVerified, VerificationError> {
348        let (ed25519, ml_dsa_65) = envelope
349            .algorithm()
350            .split_hybrid(envelope.signature())
351            .ok_or(VerificationError::InvalidSignature)?;
352        let ed25519_ok = self.verify_ed25519(ed25519, canonical_payload).is_ok();
353        let ml_dsa_65_ok = self.verify_ml_dsa_65(ml_dsa_65, canonical_payload).is_ok();
354        if ed25519_ok & ml_dsa_65_ok {
355            Ok(HybridVerified(()))
356        } else {
357            Err(VerificationError::InvalidSignature)
358        }
359    }
360}
361
362/// Signature verification backend boundary.
363pub trait Verifier: HybridVerifier {
364    /// Verifies one signature envelope over canonical payload bytes.
365    fn verify(
366        &self,
367        envelope: &SignatureEnvelope<'_>,
368        canonical_payload: &[u8],
369    ) -> Result<(), VerificationError>;
370}