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