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
104impl SignatureAlgorithm {
105    const fn eq_const(self, other: Self) -> bool {
106        match (self, other) {
107            (Self::Ed25519, Self::Ed25519) => true,
108            (Self::Ed25519, Self::MlDsa65) => false,
109            (Self::Ed25519, Self::SlhDsaSha2_128s) => false,
110            (Self::Ed25519, Self::HybridEd25519MlDsa65) => false,
111            (Self::MlDsa65, Self::Ed25519) => false,
112            (Self::MlDsa65, Self::MlDsa65) => true,
113            (Self::MlDsa65, Self::SlhDsaSha2_128s) => false,
114            (Self::MlDsa65, Self::HybridEd25519MlDsa65) => false,
115            (Self::SlhDsaSha2_128s, Self::Ed25519) => false,
116            (Self::SlhDsaSha2_128s, Self::MlDsa65) => false,
117            (Self::SlhDsaSha2_128s, Self::SlhDsaSha2_128s) => true,
118            (Self::SlhDsaSha2_128s, Self::HybridEd25519MlDsa65) => false,
119            (Self::HybridEd25519MlDsa65, Self::Ed25519) => false,
120            (Self::HybridEd25519MlDsa65, Self::MlDsa65) => false,
121            (Self::HybridEd25519MlDsa65, Self::SlhDsaSha2_128s) => false,
122            (Self::HybridEd25519MlDsa65, Self::HybridEd25519MlDsa65) => true,
123        }
124    }
125}
126
127/// Signature metadata over a canonical BCX payload.
128#[derive(Clone, Copy, Eq, PartialEq)]
129pub struct SignatureEnvelope<'a> {
130    key_id: Digest,
131    algorithm: SignatureAlgorithm,
132    signature: &'a [u8],
133}
134
135impl<'a> SignatureEnvelope<'a> {
136    /// Creates a validated signature envelope.
137    pub fn new(
138        key_id: Digest,
139        algorithm: SignatureAlgorithm,
140        signature: &'a [u8],
141        limits: WireLimits,
142    ) -> Result<Self, VerificationError> {
143        let envelope = Self {
144            key_id,
145            algorithm,
146            signature,
147        };
148        match envelope.validate(limits) {
149            Ok(()) => Ok(envelope),
150            Err(error) => Err(error),
151        }
152    }
153
154    /// Validates envelope shape before algorithm dispatch.
155    pub(crate) fn validate(&self, limits: WireLimits) -> Result<(), VerificationError> {
156        if self.key_id.is_zero() {
157            return Err(VerificationError::EmptyKeyId);
158        }
159        if self.signature.is_empty() {
160            return Err(VerificationError::EmptySignature);
161        }
162        if self.signature.len() > limits.maximum_message_len() {
163            return Err(VerificationError::SignatureTooLarge);
164        }
165        if self.signature.len() != self.algorithm.expected_signature_len() {
166            return Err(VerificationError::InvalidSignature);
167        }
168        Ok(())
169    }
170
171    /// Returns the signing key or certificate-chain commitment.
172    #[must_use]
173    pub const fn key_id(&self) -> Digest {
174        self.key_id
175    }
176
177    /// Returns the signature algorithm.
178    #[must_use]
179    pub const fn algorithm(&self) -> SignatureAlgorithm {
180        self.algorithm
181    }
182
183    /// Returns raw signature bytes.
184    #[must_use]
185    pub const fn signature(&self) -> &'a [u8] {
186        self.signature
187    }
188}
189
190impl<'a> fmt::Debug for SignatureEnvelope<'a> {
191    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
192        formatter
193            .debug_struct("SignatureEnvelope")
194            .field("key_id", &self.key_id)
195            .field("algorithm", &self.algorithm)
196            .field(
197                "signature",
198                &format_args!("[{} bytes]", self.signature.len()),
199            )
200            .finish()
201    }
202}
203
204/// Payload paired with a signature envelope.
205#[derive(Clone, Copy, Debug, Eq, PartialEq)]
206pub struct SignedEnvelope<'a, T> {
207    payload: T,
208    signature: SignatureEnvelope<'a>,
209}
210
211impl<'a, T> SignedEnvelope<'a, T> {
212    /// Creates a signed envelope from a payload and validated signature metadata.
213    #[must_use]
214    pub const fn new(payload: T, signature: SignatureEnvelope<'a>) -> Self {
215        Self { payload, signature }
216    }
217
218    /// Verifies a detached canonical byte representation of this envelope.
219    ///
220    /// The caller must ensure `canonical_payload` is the exact canonical
221    /// encoding of `self.payload()`. BCX will replace this detached helper with
222    /// typed canonical encoding once `bcx-codec` is introduced.
223    pub fn verify_detached_bytes<V: Verifier>(
224        &self,
225        verifier: &V,
226        algorithm_policy: &AlgorithmPolicy<'_>,
227        canonical_payload: &[u8],
228        limits: WireLimits,
229    ) -> Result<(), VerificationError> {
230        if !algorithm_policy.admits(self.signature.algorithm) {
231            return Err(VerificationError::AlgorithmNotAdmitted);
232        }
233        self.signature.validate(limits)?;
234        if canonical_payload.len() > limits.maximum_message_len() {
235            return Err(VerificationError::PayloadTooLarge);
236        }
237        match self.signature.algorithm {
238            SignatureAlgorithm::HybridEd25519MlDsa65 => verifier
239                .verify_hybrid(&self.signature, canonical_payload)
240                .map(|_| ()),
241            SignatureAlgorithm::Ed25519
242            | SignatureAlgorithm::MlDsa65
243            | SignatureAlgorithm::SlhDsaSha2_128s => {
244                verifier.verify(&self.signature, canonical_payload)
245            }
246        }
247    }
248
249    /// Returns the payload value.
250    #[must_use]
251    pub const fn payload(&self) -> &T {
252        &self.payload
253    }
254
255    /// Returns the signature envelope.
256    #[must_use]
257    pub const fn signature(&self) -> SignatureEnvelope<'a> {
258        self.signature
259    }
260}
261
262/// Proof that both components of a hybrid signature verified.
263#[derive(Clone, Copy, Debug, Eq, PartialEq)]
264pub struct HybridVerified(());
265
266/// Verification backend boundary for hybrid signature components.
267pub trait HybridVerifier {
268    /// Verifies the Ed25519 component of a hybrid signature.
269    fn verify_ed25519(
270        &self,
271        ed25519_signature: &[u8],
272        canonical_payload: &[u8],
273    ) -> Result<(), VerificationError>;
274
275    /// Verifies the ML-DSA-65 component of a hybrid signature.
276    fn verify_ml_dsa_65(
277        &self,
278        ml_dsa_65_signature: &[u8],
279        canonical_payload: &[u8],
280    ) -> Result<(), VerificationError>;
281
282    /// Verifies both components of a hybrid signature.
283    ///
284    /// Implementors must run component verification to completion and must not
285    /// use intermediate component failures to skip later component work. The
286    /// default implementation always invokes both component methods before
287    /// combining their results.
288    fn verify_hybrid(
289        &self,
290        envelope: &SignatureEnvelope<'_>,
291        canonical_payload: &[u8],
292    ) -> Result<HybridVerified, VerificationError> {
293        let (ed25519, ml_dsa_65) = envelope
294            .algorithm()
295            .split_hybrid(envelope.signature())
296            .ok_or(VerificationError::InvalidSignature)?;
297        let ed25519_ok = self.verify_ed25519(ed25519, canonical_payload).is_ok();
298        let ml_dsa_65_ok = self.verify_ml_dsa_65(ml_dsa_65, canonical_payload).is_ok();
299        if ed25519_ok & ml_dsa_65_ok {
300            Ok(HybridVerified(()))
301        } else {
302            Err(VerificationError::InvalidSignature)
303        }
304    }
305}
306
307/// Signature verification backend boundary.
308pub trait Verifier: HybridVerifier {
309    /// Verifies one signature envelope over canonical payload bytes.
310    fn verify(
311        &self,
312        envelope: &SignatureEnvelope<'_>,
313        canonical_payload: &[u8],
314    ) -> Result<(), VerificationError>;
315}