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, ExactAlgorithmPolicy, HybridVerified, HybridVerifier, SignatureAlgorithm,
9    SignatureEnvelope, 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 exact_algorithm_policy_admits_only_one_algorithm() {
72        let policy = ExactAlgorithmPolicy::new(SignatureAlgorithm::HybridEd25519MlDsa65);
73
74        assert_eq!(policy.algorithm(), SignatureAlgorithm::HybridEd25519MlDsa65);
75        assert!(policy.admits(SignatureAlgorithm::HybridEd25519MlDsa65));
76        assert!(!policy.admits(SignatureAlgorithm::Ed25519));
77    }
78
79    #[test]
80    fn policy_rejects_unadmitted_algorithm_before_verifier() -> Result<(), VerificationError> {
81        struct RejectingVerifier;
82
83        impl HybridVerifier for RejectingVerifier {
84            fn verify_ed25519(
85                &self,
86                _ed25519_signature: &[u8],
87                _canonical_payload: &[u8],
88            ) -> Result<(), VerificationError> {
89                Err(VerificationError::InvalidSignature)
90            }
91
92            fn verify_ml_dsa_65(
93                &self,
94                _ml_dsa_65_signature: &[u8],
95                _canonical_payload: &[u8],
96            ) -> Result<(), VerificationError> {
97                Err(VerificationError::InvalidSignature)
98            }
99        }
100
101        impl Verifier for RejectingVerifier {
102            fn verify(
103                &self,
104                _envelope: &SignatureEnvelope<'_>,
105                _canonical_payload: &[u8],
106            ) -> Result<(), VerificationError> {
107                Err(VerificationError::InvalidSignature)
108            }
109        }
110
111        let signature = [1; 64];
112        let envelope = SignedEnvelope::new(
113            (),
114            SignatureEnvelope::new(
115                Digest::new([1; Digest::LEN]),
116                SignatureAlgorithm::Ed25519,
117                &signature,
118                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
119            )?,
120        );
121        let policy = AlgorithmPolicy::new(&[SignatureAlgorithm::MlDsa65])?;
122
123        assert_eq!(
124            envelope.verify_detached_bytes(
125                &RejectingVerifier,
126                &policy,
127                b"payload",
128                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
129            ),
130            Err(VerificationError::AlgorithmNotAdmitted)
131        );
132        Ok(())
133    }
134
135    #[test]
136    fn detached_payload_is_bounded_by_wire_limits() -> Result<(), VerificationError> {
137        struct AcceptingVerifier;
138
139        impl HybridVerifier for AcceptingVerifier {
140            fn verify_ed25519(
141                &self,
142                _ed25519_signature: &[u8],
143                _canonical_payload: &[u8],
144            ) -> Result<(), VerificationError> {
145                Ok(())
146            }
147
148            fn verify_ml_dsa_65(
149                &self,
150                _ml_dsa_65_signature: &[u8],
151                _canonical_payload: &[u8],
152            ) -> Result<(), VerificationError> {
153                Ok(())
154            }
155        }
156
157        impl Verifier for AcceptingVerifier {
158            fn verify(
159                &self,
160                _envelope: &SignatureEnvelope<'_>,
161                _canonical_payload: &[u8],
162            ) -> Result<(), VerificationError> {
163                Ok(())
164            }
165        }
166
167        let signature = [1; 64];
168        let envelope = SignedEnvelope::new(
169            (),
170            SignatureEnvelope::new(
171                Digest::new([1; Digest::LEN]),
172                SignatureAlgorithm::Ed25519,
173                &signature,
174                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
175            )?,
176        );
177        let payload = [0; 65];
178        let limits =
179            WireLimits::new(64, 1, 1, 1).map_err(|_| VerificationError::InvalidSignature)?;
180        let policy = AlgorithmPolicy::new(&[SignatureAlgorithm::Ed25519])?;
181
182        assert_eq!(
183            envelope.verify_detached_bytes(&AcceptingVerifier, &policy, &payload, limits),
184            Err(VerificationError::PayloadTooLarge)
185        );
186        Ok(())
187    }
188
189    #[test]
190    fn exact_policy_rejects_algorithm_downgrade() -> Result<(), VerificationError> {
191        struct AcceptingVerifier;
192
193        impl HybridVerifier for AcceptingVerifier {
194            fn verify_ed25519(
195                &self,
196                _ed25519_signature: &[u8],
197                _canonical_payload: &[u8],
198            ) -> Result<(), VerificationError> {
199                Ok(())
200            }
201
202            fn verify_ml_dsa_65(
203                &self,
204                _ml_dsa_65_signature: &[u8],
205                _canonical_payload: &[u8],
206            ) -> Result<(), VerificationError> {
207                Ok(())
208            }
209        }
210
211        impl Verifier for AcceptingVerifier {
212            fn verify(
213                &self,
214                _envelope: &SignatureEnvelope<'_>,
215                _canonical_payload: &[u8],
216            ) -> Result<(), VerificationError> {
217                Ok(())
218            }
219        }
220
221        let signature = [1; SignatureAlgorithm::ED25519_SIGNATURE_LEN];
222        let envelope = SignedEnvelope::new(
223            (),
224            SignatureEnvelope::new(
225                Digest::new([1; Digest::LEN]),
226                SignatureAlgorithm::Ed25519,
227                &signature,
228                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
229            )?,
230        );
231        let policy = ExactAlgorithmPolicy::new(SignatureAlgorithm::HybridEd25519MlDsa65);
232
233        assert_eq!(
234            envelope.verify_detached_bytes_exact(
235                &AcceptingVerifier,
236                policy,
237                b"payload",
238                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
239            ),
240            Err(VerificationError::AlgorithmNotAdmitted)
241        );
242        Ok(())
243    }
244
245    #[test]
246    fn signature_envelope_debug_redacts_signature_bytes() -> Result<(), VerificationError> {
247        extern crate std;
248        use std::{format, string::String};
249
250        let signature = [7; SignatureAlgorithm::ED25519_SIGNATURE_LEN];
251        let envelope = SignatureEnvelope::new(
252            Digest::new([1; Digest::LEN]),
253            SignatureAlgorithm::Ed25519,
254            &signature,
255            WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
256        )?;
257
258        assert_eq!(
259            format!("{envelope:?}"),
260            String::from(
261                "SignatureEnvelope { key_id: Digest(..), algorithm: Ed25519, signature: [64 bytes] }"
262            )
263        );
264        Ok(())
265    }
266
267    #[test]
268    fn hybrid_verification_requires_both_components() -> Result<(), VerificationError> {
269        struct PartialHybridVerifier;
270
271        impl HybridVerifier for PartialHybridVerifier {
272            fn verify_ed25519(
273                &self,
274                ed25519_signature: &[u8],
275                _canonical_payload: &[u8],
276            ) -> Result<(), VerificationError> {
277                if ed25519_signature.len() == SignatureAlgorithm::ED25519_SIGNATURE_LEN {
278                    Ok(())
279                } else {
280                    Err(VerificationError::InvalidSignature)
281                }
282            }
283
284            fn verify_ml_dsa_65(
285                &self,
286                _ml_dsa_65_signature: &[u8],
287                _canonical_payload: &[u8],
288            ) -> Result<(), VerificationError> {
289                Err(VerificationError::InvalidSignature)
290            }
291        }
292
293        impl Verifier for PartialHybridVerifier {
294            fn verify(
295                &self,
296                _envelope: &SignatureEnvelope<'_>,
297                _canonical_payload: &[u8],
298            ) -> Result<(), VerificationError> {
299                Ok(())
300            }
301        }
302
303        let signature = [9; SignatureAlgorithm::HYBRID_ED25519_ML_DSA_65_SIGNATURE_LEN];
304        let envelope = SignedEnvelope::new(
305            (),
306            SignatureEnvelope::new(
307                Digest::new([1; Digest::LEN]),
308                SignatureAlgorithm::HybridEd25519MlDsa65,
309                &signature,
310                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
311            )?,
312        );
313        let policy = AlgorithmPolicy::new(&[SignatureAlgorithm::HybridEd25519MlDsa65])?;
314
315        assert_eq!(
316            envelope.verify_detached_bytes(
317                &PartialHybridVerifier,
318                &policy,
319                b"payload",
320                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
321            ),
322            Err(VerificationError::InvalidSignature)
323        );
324        Ok(())
325    }
326
327    #[test]
328    fn hybrid_verification_invokes_both_components_on_failure() -> Result<(), VerificationError> {
329        struct CountingHybridVerifier {
330            ed25519_calls: Cell<usize>,
331            ml_dsa_65_calls: Cell<usize>,
332        }
333
334        impl HybridVerifier for CountingHybridVerifier {
335            fn verify_ed25519(
336                &self,
337                _ed25519_signature: &[u8],
338                _canonical_payload: &[u8],
339            ) -> Result<(), VerificationError> {
340                self.ed25519_calls.set(self.ed25519_calls.get() + 1);
341                Err(VerificationError::InvalidSignature)
342            }
343
344            fn verify_ml_dsa_65(
345                &self,
346                _ml_dsa_65_signature: &[u8],
347                _canonical_payload: &[u8],
348            ) -> Result<(), VerificationError> {
349                self.ml_dsa_65_calls.set(self.ml_dsa_65_calls.get() + 1);
350                Err(VerificationError::InvalidSignature)
351            }
352        }
353
354        impl Verifier for CountingHybridVerifier {
355            fn verify(
356                &self,
357                _envelope: &SignatureEnvelope<'_>,
358                _canonical_payload: &[u8],
359            ) -> Result<(), VerificationError> {
360                Err(VerificationError::InvalidSignature)
361            }
362        }
363
364        let signature = [9; SignatureAlgorithm::HYBRID_ED25519_ML_DSA_65_SIGNATURE_LEN];
365        let envelope = SignedEnvelope::new(
366            (),
367            SignatureEnvelope::new(
368                Digest::new([1; Digest::LEN]),
369                SignatureAlgorithm::HybridEd25519MlDsa65,
370                &signature,
371                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
372            )?,
373        );
374        let policy = AlgorithmPolicy::new(&[SignatureAlgorithm::HybridEd25519MlDsa65])?;
375        let verifier = CountingHybridVerifier {
376            ed25519_calls: Cell::new(0),
377            ml_dsa_65_calls: Cell::new(0),
378        };
379
380        assert_eq!(
381            envelope.verify_detached_bytes(
382                &verifier,
383                &policy,
384                b"payload",
385                WireLimits::UNSAFE_DEVELOPMENT_DO_NOT_USE_IN_PRODUCTION,
386            ),
387            Err(VerificationError::InvalidSignature)
388        );
389        assert_eq!(verifier.ed25519_calls.get(), 1);
390        assert_eq!(verifier.ml_dsa_65_calls.get(), 1);
391        Ok(())
392    }
393}