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}