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}