Skip to main content

bcx_crypto/
lib.rs

1#![no_std]
2#![doc = "Crypto-agile envelope metadata for BCX."]
3
4mod envelope;
5mod error;
6
7pub use envelope::{
8    AlgorithmPolicy, HybridVerified, HybridVerifier, SignatureAlgorithm, SignatureEnvelope,
9    SignedEnvelope, Verifier,
10};
11pub use error::VerificationError;
12
13#[cfg(test)]
14mod tests {
15    use super::*;
16    use bcx_core::Digest;
17    use bcx_wire::WireLimits;
18    use core::cell::Cell;
19
20    #[test]
21    fn empty_signature_is_rejected() {
22        assert_eq!(
23            SignatureEnvelope::new(
24                Digest::new([1; Digest::LEN]),
25                SignatureAlgorithm::Ed25519,
26                &[],
27                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
28            ),
29            Err(VerificationError::EmptySignature)
30        );
31    }
32
33    #[test]
34    fn wrong_signature_length_is_rejected() {
35        assert_eq!(
36            SignatureEnvelope::new(
37                Digest::new([1; Digest::LEN]),
38                SignatureAlgorithm::Ed25519,
39                &[7; 63],
40                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
41            ),
42            Err(VerificationError::InvalidSignature)
43        );
44    }
45
46    #[test]
47    fn hybrid_signature_split_uses_documented_layout() {
48        let signature = [5; SignatureAlgorithm::HYBRID_ED25519_ML_DSA_65_SIGNATURE_LEN];
49        let split = SignatureAlgorithm::HybridEd25519MlDsa65.split_hybrid(&signature);
50
51        assert_eq!(
52            split.map(|(ed25519, ml_dsa)| (ed25519.len(), ml_dsa.len())),
53            Some((
54                SignatureAlgorithm::ED25519_SIGNATURE_LEN,
55                SignatureAlgorithm::ML_DSA_65_SIGNATURE_LEN,
56            ))
57        );
58        assert_eq!(SignatureAlgorithm::Ed25519.split_hybrid(&signature), None);
59    }
60
61    #[test]
62    fn empty_algorithm_policy_requires_explicit_deny_all() {
63        assert_eq!(
64            AlgorithmPolicy::new(&[]),
65            Err(VerificationError::EmptyAlgorithmPolicy)
66        );
67        assert!(!AlgorithmPolicy::deny_all().admits(SignatureAlgorithm::Ed25519));
68    }
69
70    #[test]
71    fn policy_rejects_unadmitted_algorithm_before_verifier() -> Result<(), VerificationError> {
72        struct RejectingVerifier;
73
74        impl HybridVerifier for RejectingVerifier {
75            fn verify_ed25519(
76                &self,
77                _ed25519_signature: &[u8],
78                _canonical_payload: &[u8],
79            ) -> Result<(), VerificationError> {
80                Err(VerificationError::InvalidSignature)
81            }
82
83            fn verify_ml_dsa_65(
84                &self,
85                _ml_dsa_65_signature: &[u8],
86                _canonical_payload: &[u8],
87            ) -> Result<(), VerificationError> {
88                Err(VerificationError::InvalidSignature)
89            }
90        }
91
92        impl Verifier for RejectingVerifier {
93            fn verify(
94                &self,
95                _envelope: &SignatureEnvelope<'_>,
96                _canonical_payload: &[u8],
97            ) -> Result<(), VerificationError> {
98                Err(VerificationError::InvalidSignature)
99            }
100        }
101
102        let signature = [1; 64];
103        let envelope = SignedEnvelope::new(
104            (),
105            SignatureEnvelope::new(
106                Digest::new([1; Digest::LEN]),
107                SignatureAlgorithm::Ed25519,
108                &signature,
109                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
110            )?,
111        );
112        let policy = AlgorithmPolicy::new(&[SignatureAlgorithm::MlDsa65])?;
113
114        assert_eq!(
115            envelope.verify_detached_bytes(
116                &RejectingVerifier,
117                &policy,
118                b"payload",
119                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
120            ),
121            Err(VerificationError::AlgorithmNotAdmitted)
122        );
123        Ok(())
124    }
125
126    #[test]
127    fn detached_payload_is_bounded_by_wire_limits() -> Result<(), VerificationError> {
128        struct AcceptingVerifier;
129
130        impl HybridVerifier for AcceptingVerifier {
131            fn verify_ed25519(
132                &self,
133                _ed25519_signature: &[u8],
134                _canonical_payload: &[u8],
135            ) -> Result<(), VerificationError> {
136                Ok(())
137            }
138
139            fn verify_ml_dsa_65(
140                &self,
141                _ml_dsa_65_signature: &[u8],
142                _canonical_payload: &[u8],
143            ) -> Result<(), VerificationError> {
144                Ok(())
145            }
146        }
147
148        impl Verifier for AcceptingVerifier {
149            fn verify(
150                &self,
151                _envelope: &SignatureEnvelope<'_>,
152                _canonical_payload: &[u8],
153            ) -> Result<(), VerificationError> {
154                Ok(())
155            }
156        }
157
158        let signature = [1; 64];
159        let envelope = SignedEnvelope::new(
160            (),
161            SignatureEnvelope::new(
162                Digest::new([1; Digest::LEN]),
163                SignatureAlgorithm::Ed25519,
164                &signature,
165                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
166            )?,
167        );
168        let payload = [0; 65];
169        let limits =
170            WireLimits::new(64, 1, 1, 1).map_err(|_| VerificationError::InvalidSignature)?;
171        let policy = AlgorithmPolicy::new(&[SignatureAlgorithm::Ed25519])?;
172
173        assert_eq!(
174            envelope.verify_detached_bytes(&AcceptingVerifier, &policy, &payload, limits),
175            Err(VerificationError::PayloadTooLarge)
176        );
177        Ok(())
178    }
179
180    #[test]
181    fn signature_envelope_debug_redacts_signature_bytes() -> Result<(), VerificationError> {
182        extern crate std;
183        use std::{format, string::String};
184
185        let signature = [7; SignatureAlgorithm::ED25519_SIGNATURE_LEN];
186        let envelope = SignatureEnvelope::new(
187            Digest::new([1; Digest::LEN]),
188            SignatureAlgorithm::Ed25519,
189            &signature,
190            WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
191        )?;
192
193        assert_eq!(
194            format!("{envelope:?}"),
195            String::from(
196                "SignatureEnvelope { key_id: Digest(..), algorithm: Ed25519, signature: [64 bytes] }"
197            )
198        );
199        Ok(())
200    }
201
202    #[test]
203    fn hybrid_verification_requires_both_components() -> Result<(), VerificationError> {
204        struct PartialHybridVerifier;
205
206        impl HybridVerifier for PartialHybridVerifier {
207            fn verify_ed25519(
208                &self,
209                ed25519_signature: &[u8],
210                _canonical_payload: &[u8],
211            ) -> Result<(), VerificationError> {
212                if ed25519_signature.len() == SignatureAlgorithm::ED25519_SIGNATURE_LEN {
213                    Ok(())
214                } else {
215                    Err(VerificationError::InvalidSignature)
216                }
217            }
218
219            fn verify_ml_dsa_65(
220                &self,
221                _ml_dsa_65_signature: &[u8],
222                _canonical_payload: &[u8],
223            ) -> Result<(), VerificationError> {
224                Err(VerificationError::InvalidSignature)
225            }
226        }
227
228        impl Verifier for PartialHybridVerifier {
229            fn verify(
230                &self,
231                _envelope: &SignatureEnvelope<'_>,
232                _canonical_payload: &[u8],
233            ) -> Result<(), VerificationError> {
234                Ok(())
235            }
236        }
237
238        let signature = [9; SignatureAlgorithm::HYBRID_ED25519_ML_DSA_65_SIGNATURE_LEN];
239        let envelope = SignedEnvelope::new(
240            (),
241            SignatureEnvelope::new(
242                Digest::new([1; Digest::LEN]),
243                SignatureAlgorithm::HybridEd25519MlDsa65,
244                &signature,
245                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
246            )?,
247        );
248        let policy = AlgorithmPolicy::new(&[SignatureAlgorithm::HybridEd25519MlDsa65])?;
249
250        assert_eq!(
251            envelope.verify_detached_bytes(
252                &PartialHybridVerifier,
253                &policy,
254                b"payload",
255                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
256            ),
257            Err(VerificationError::InvalidSignature)
258        );
259        Ok(())
260    }
261
262    #[test]
263    fn hybrid_verification_invokes_both_components_on_failure() -> Result<(), VerificationError> {
264        struct CountingHybridVerifier {
265            ed25519_calls: Cell<usize>,
266            ml_dsa_65_calls: Cell<usize>,
267        }
268
269        impl HybridVerifier for CountingHybridVerifier {
270            fn verify_ed25519(
271                &self,
272                _ed25519_signature: &[u8],
273                _canonical_payload: &[u8],
274            ) -> Result<(), VerificationError> {
275                self.ed25519_calls.set(self.ed25519_calls.get() + 1);
276                Err(VerificationError::InvalidSignature)
277            }
278
279            fn verify_ml_dsa_65(
280                &self,
281                _ml_dsa_65_signature: &[u8],
282                _canonical_payload: &[u8],
283            ) -> Result<(), VerificationError> {
284                self.ml_dsa_65_calls.set(self.ml_dsa_65_calls.get() + 1);
285                Err(VerificationError::InvalidSignature)
286            }
287        }
288
289        impl Verifier for CountingHybridVerifier {
290            fn verify(
291                &self,
292                _envelope: &SignatureEnvelope<'_>,
293                _canonical_payload: &[u8],
294            ) -> Result<(), VerificationError> {
295                Err(VerificationError::InvalidSignature)
296            }
297        }
298
299        let signature = [9; SignatureAlgorithm::HYBRID_ED25519_ML_DSA_65_SIGNATURE_LEN];
300        let envelope = SignedEnvelope::new(
301            (),
302            SignatureEnvelope::new(
303                Digest::new([1; Digest::LEN]),
304                SignatureAlgorithm::HybridEd25519MlDsa65,
305                &signature,
306                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
307            )?,
308        );
309        let policy = AlgorithmPolicy::new(&[SignatureAlgorithm::HybridEd25519MlDsa65])?;
310        let verifier = CountingHybridVerifier {
311            ed25519_calls: Cell::new(0),
312            ml_dsa_65_calls: Cell::new(0),
313        };
314
315        assert_eq!(
316            envelope.verify_detached_bytes(
317                &verifier,
318                &policy,
319                b"payload",
320                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
321            ),
322            Err(VerificationError::InvalidSignature)
323        );
324        assert_eq!(verifier.ed25519_calls.get(), 1);
325        assert_eq!(verifier.ml_dsa_65_calls.get(), 1);
326        Ok(())
327    }
328}