1#![forbid(unsafe_code)]
29#![allow(missing_docs)] use serde::{Deserialize, Serialize};
32
33pub mod cache;
34pub mod cose;
35#[cfg(any(feature = "pq", feature = "pq-slh"))]
36pub mod pq;
37
38#[cfg(test)]
39mod props;
40
41#[cfg(feature = "wycheproof")]
42pub mod wycheproof;
43
44pub const ED25519: &str = "Ed25519";
46pub const ECDSA_P256: &str = "ECDSA-P256";
48pub const ML_DSA_65: &str = "ML-DSA-65";
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct ComponentSignature {
54 pub algorithm: String,
56 pub public_key: Vec<u8>,
58 pub signature: Vec<u8>,
60}
61
62#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct CompositeSignature {
65 pub components: Vec<ComponentSignature>,
67}
68
69#[derive(Debug, thiserror::Error)]
71pub enum CompositeError {
72 #[error("verification failed: {0}")]
74 Verify(String),
75 #[error("composite signature has no components")]
77 Empty,
78 #[error("serialization error: {0}")]
80 Serde(#[from] serde_json::Error),
81}
82
83impl CompositeSignature {
84 pub fn new(components: Vec<ComponentSignature>) -> Self {
86 Self { components }
87 }
88
89 pub fn component_count(&self) -> usize {
91 self.components.len()
92 }
93
94 pub fn algorithms(&self) -> Vec<&str> {
96 self.components
97 .iter()
98 .map(|c| c.algorithm.as_str())
99 .collect()
100 }
101
102 pub fn verify<F>(
105 &self,
106 message: &[u8],
107 verifier: F,
108 ) -> Result<VerificationResult, CompositeError>
109 where
110 F: Fn(&str, &[u8], &[u8], &[u8]) -> Result<(), String>,
111 {
112 if self.components.is_empty() {
113 return Err(CompositeError::Empty);
114 }
115 let mut per_component = Vec::new();
116 let mut all_ok = true;
117 for (i, c) in self.components.iter().enumerate() {
118 match verifier(&c.algorithm, &c.public_key, message, &c.signature) {
119 Ok(()) => per_component.push(ComponentResult {
120 index: i,
121 algorithm: c.algorithm.clone(),
122 verified: true,
123 error: None,
124 }),
125 Err(e) => {
126 all_ok = false;
127 per_component.push(ComponentResult {
128 index: i,
129 algorithm: c.algorithm.clone(),
130 verified: false,
131 error: Some(e),
132 });
133 }
134 }
135 }
136 Ok(VerificationResult {
137 all_verified: all_ok,
138 per_component,
139 })
140 }
141}
142
143#[derive(Debug, Clone)]
145pub struct ComponentResult {
146 pub index: usize,
148 pub algorithm: String,
150 pub verified: bool,
152 pub error: Option<String>,
154}
155
156#[derive(Debug, Clone)]
158pub struct VerificationResult {
159 pub all_verified: bool,
161 pub per_component: Vec<ComponentResult>,
163}
164
165pub mod algorithm_ids {
167 pub const ED25519_MLDSA65: &str = "id-MLDSA65-Ed25519";
169 pub const ECDSAP256_MLDSA65: &str = "id-MLDSA65-ECDSA-P256";
171 pub const ECDSAP384_MLDSA87: &str = "id-MLDSA87-ECDSA-P384";
173 pub const ED25519_SLHDSA128S: &str = "id-SLHDSA-SHA2-128S-Ed25519";
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180
181 #[test]
182 fn composite_round_trip() {
183 let composite = CompositeSignature::new(vec![
184 ComponentSignature {
185 algorithm: "Ed25519".into(),
186 public_key: vec![1u8; 32],
187 signature: vec![2u8; 64],
188 },
189 ComponentSignature {
190 algorithm: "ML-DSA-65".into(),
191 public_key: vec![3u8; 1952],
192 signature: vec![4u8; 3309],
193 },
194 ]);
195 assert_eq!(composite.component_count(), 2);
196
197 let result = composite.verify(b"hello", |_, _, _, _| Ok(())).unwrap();
198 assert!(result.all_verified);
199 }
200
201 #[test]
202 fn composite_fails_if_any_component_fails() {
203 let composite = CompositeSignature::new(vec![
204 ComponentSignature {
205 algorithm: "Ed25519".into(),
206 public_key: vec![1u8; 32],
207 signature: vec![2u8; 64],
208 },
209 ComponentSignature {
210 algorithm: "ML-DSA-65".into(),
211 public_key: vec![3u8; 1952],
212 signature: vec![4u8; 3309],
213 },
214 ]);
215 let result = composite
216 .verify(b"hello", |alg, _, _, _| {
217 if alg == "Ed25519" {
218 Ok(())
219 } else {
220 Err("bad".into())
221 }
222 })
223 .unwrap();
224 assert!(!result.all_verified);
225 }
226
227 #[test]
228 fn empty_composite_errors() {
229 let composite = CompositeSignature::new(vec![]);
230 let result = composite.verify(b"x", |_, _, _, _| Ok(()));
231 assert!(matches!(result, Err(CompositeError::Empty)));
232 }
233}
234
235pub fn ed25519_verifier(
238 algorithm: &str,
239 public_key: &[u8],
240 message: &[u8],
241 signature: &[u8],
242) -> Result<(), String> {
243 if algorithm != ED25519 {
244 return Err(format!("not Ed25519: {algorithm}"));
245 }
246 use ed25519_dalek::{Signature, Verifier, VerifyingKey};
247 let pk: [u8; 32] = public_key
248 .try_into()
249 .map_err(|_| "Ed25519 pubkey must be 32 bytes".to_string())?;
250 let sig_bytes: [u8; 64] = signature
251 .try_into()
252 .map_err(|_| "Ed25519 sig must be 64 bytes".to_string())?;
253 let vk = VerifyingKey::from_bytes(&pk).map_err(|e| format!("bad pubkey: {e}"))?;
254 let sig = Signature::from_bytes(&sig_bytes);
255 vk.verify(message, &sig).map_err(|e| format!("verify: {e}"))
256}
257
258pub const MLDSA65: &str = "ML-DSA-65";
267
268#[cfg(feature = "pq")]
275pub fn mldsa65_verifier(
276 algorithm: &str,
277 public_key: &[u8],
278 message: &[u8],
279 signature: &[u8],
280) -> Result<(), String> {
281 if algorithm != MLDSA65 {
282 return Err(format!("not ML-DSA-65: {algorithm}"));
283 }
284 crate::pq::verify_mldsa65(public_key, message, signature).map_err(|e| e.to_string())
285}
286
287#[cfg(feature = "pq-slh")]
289pub const SLHDSA128S: &str = "SLH-DSA-128s";
290
291#[cfg(feature = "pq-slh")]
298pub fn slhdsa128s_verifier(
299 algorithm: &str,
300 public_key: &[u8],
301 message: &[u8],
302 signature: &[u8],
303) -> Result<(), String> {
304 if algorithm != SLHDSA128S {
305 return Err(format!("not SLH-DSA-128s: {algorithm}"));
306 }
307 crate::pq::verify_slhdsa128s(public_key, message, signature)
308}
309
310#[cfg(feature = "pq")]
320pub fn transition_verifier(
321 algorithm: &str,
322 public_key: &[u8],
323 message: &[u8],
324 signature: &[u8],
325) -> Result<(), String> {
326 match algorithm {
327 ED25519 => ed25519_verifier(algorithm, public_key, message, signature),
328 ECDSA_P256 => p256_verifier(algorithm, public_key, message, signature),
329 MLDSA65 => mldsa65_verifier(algorithm, public_key, message, signature),
330 #[cfg(feature = "pq-slh")]
331 SLHDSA128S => slhdsa128s_verifier(algorithm, public_key, message, signature),
332 other => Err(format!("unsupported algorithm: {other}")),
333 }
334}
335
336pub fn p256_verifier(
337 algorithm: &str,
338 public_key: &[u8],
339 message: &[u8],
340 signature: &[u8],
341) -> Result<(), String> {
342 if algorithm != ECDSA_P256 && algorithm != "ECDSA" {
343 return Err(format!("not ECDSA-P256: {algorithm}"));
344 }
345 use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
346 let vk = VerifyingKey::from_sec1_bytes(public_key)
347 .map_err(|e| format!("invalid P-256 public key: {e}"))?;
348 let sig = Signature::from_der(signature).map_err(|e| format!("invalid DER signature: {e}"))?;
349 vk.verify(message, &sig).map_err(|e| format!("verify: {e}"))
350}
351
352pub fn build_ed25519_component(
355 signing_key: &ed25519_dalek::SigningKey,
356 message: &[u8],
357) -> Result<ComponentSignature, CompositeError> {
358 use ed25519_dalek::Signer;
359 let sig = signing_key.sign(message);
360 Ok(ComponentSignature {
361 algorithm: ED25519.into(),
362 public_key: signing_key.verifying_key().to_bytes().to_vec(),
363 signature: sig.to_bytes().to_vec(),
364 })
365}
366
367pub fn build_p256_component(
375 signing_key: &p256::ecdsa::SigningKey,
376 message: &[u8],
377) -> Result<ComponentSignature, CompositeError> {
378 use p256::ecdsa::signature::Signer;
379 let verifying = signing_key.verifying_key();
380 let sig: p256::ecdsa::Signature = signing_key.sign(message);
381 let sig_der = sig.to_der();
382 Ok(ComponentSignature {
383 algorithm: ECDSA_P256.into(),
384 public_key: verifying.to_sec1_bytes().to_vec(),
385 signature: sig_der.to_bytes().to_vec(),
386 })
387}
388
389#[cfg(test)]
390mod real_ed25519_tests {
391 use super::*;
392 use ed25519_dalek::SigningKey;
393 use ed25519_dalek::rand_core::UnwrapErr;
394
395 #[test]
396 fn real_ed25519_round_trip() {
397 let signing = SigningKey::generate(&mut UnwrapErr(getrandom::SysRng));
398 let message = b"composite signature test message";
399 let component = build_ed25519_component(&signing, message).unwrap();
400 let result = ed25519_verifier(
401 &component.algorithm,
402 &component.public_key,
403 message,
404 &component.signature,
405 );
406 assert!(result.is_ok());
407 }
408
409 #[test]
410 fn real_ed25519_rejects_wrong_message() {
411 let signing = SigningKey::generate(&mut UnwrapErr(getrandom::SysRng));
412 let component = build_ed25519_component(&signing, b"original").unwrap();
413 let result = ed25519_verifier(
414 &component.algorithm,
415 &component.public_key,
416 b"different",
417 &component.signature,
418 );
419 assert!(result.is_err());
420 }
421
422 #[test]
423 fn real_p256_round_trip() {
424 use p256::ecdsa::{Signature, SigningKey, signature::Signer};
425 use p256::elliptic_curve::Generate;
426 let signing = SigningKey::generate();
427 let verifying = signing.verifying_key();
428 let message = b"composite p256 test message";
429 let sig: Signature = signing.sign(message);
430 let sig_der = sig.to_der();
431 let pk_bytes: Vec<u8> = verifying.to_sec1_bytes().to_vec();
432 let sig_bytes: Vec<u8> = sig_der.to_bytes().to_vec();
433 let result = p256_verifier(ECDSA_P256, &pk_bytes, message, &sig_bytes);
434 assert!(result.is_ok(), "p256 verifier should accept valid sig");
435 }
436
437 #[test]
438 fn real_p256_rejects_wrong_message() {
439 use p256::ecdsa::{Signature, SigningKey, signature::Signer};
440 use p256::elliptic_curve::Generate;
441 let signing = SigningKey::generate();
442 let verifying = signing.verifying_key();
443 let sig: Signature = signing.sign(b"original");
444 let sig_der = sig.to_der();
445 let pk_bytes: Vec<u8> = verifying.to_sec1_bytes().to_vec();
446 let sig_bytes: Vec<u8> = sig_der.to_bytes().to_vec();
447 let result = p256_verifier(ECDSA_P256, &pk_bytes, b"different", &sig_bytes);
448 assert!(result.is_err());
449 }
450
451 #[test]
452 fn composite_with_real_ed25519_verifies() {
453 let signing = SigningKey::generate(&mut UnwrapErr(getrandom::SysRng));
454 let message = b"composite with real crypto";
455 let component = build_ed25519_component(&signing, message).unwrap();
456 let composite = CompositeSignature::new(vec![component]);
457 let result = composite
458 .verify(message, |alg, pk, msg, sig| {
459 ed25519_verifier(alg, pk, msg, sig)
460 })
461 .unwrap();
462 assert!(result.all_verified);
463 assert_eq!(result.per_component.len(), 1);
464 }
465
466 #[test]
467 fn composite_with_real_ed25519_plus_mock_ml_dsa() {
468 let signing = SigningKey::generate(&mut UnwrapErr(getrandom::SysRng));
469 let message = b"PQ migration composite";
470 let ed_component = build_ed25519_component(&signing, message).unwrap();
471 let ml_component = ComponentSignature {
473 algorithm: ML_DSA_65.into(),
474 public_key: vec![0u8; 1952],
475 signature: vec![0u8; 3309],
476 };
477 let composite = CompositeSignature::new(vec![ed_component, ml_component]);
478 let result = composite
479 .verify(message, |alg, pk, msg, sig| {
480 if alg == ED25519 {
481 ed25519_verifier(alg, pk, msg, sig)
482 } else if alg == ML_DSA_65 {
483 Ok(())
484 } else {
485 Err(format!("unknown algorithm: {alg}"))
486 }
487 })
488 .unwrap();
489 assert!(result.all_verified);
490 assert_eq!(result.per_component.len(), 2);
491 }
492}
493
494#[cfg(test)]
495mod proptests {
496 use super::*;
497 use proptest::prelude::*;
498
499 proptest! {
502 #[test]
503 fn ed25519_roundtrip_json_verifies(msg in proptest::collection::vec(any::<u8>(), 0..256)) {
504 use ed25519_dalek::SigningKey;
505 use ed25519_dalek::rand_core::UnwrapErr;
506 let signing = SigningKey::generate(&mut UnwrapErr(getrandom::SysRng));
507 let verifying: ed25519_dalek::VerifyingKey = signing.verifying_key();
508 let component = build_ed25519_component(&signing, &msg)?;
509 let composite = CompositeSignature::new(vec![component]);
510 let json = serde_json::to_string(&composite)?;
511 let parsed: CompositeSignature = serde_json::from_str(&json)?;
512 let result = parsed.verify(&msg, |alg, pk, m, sig| {
513 if alg == ED25519 {
514 ed25519_verifier(alg, pk, m, sig)
515 } else {
516 Err(format!("unknown algorithm: {alg}"))
517 }
518 })?;
519 prop_assert!(result.all_verified);
520 prop_assert_eq!(result.per_component.len(), 1);
521 let _ = verifying; }
523 }
524
525 proptest! {
528 #[test]
529 fn ed25519_tamper_fails(
530 msg in proptest::collection::vec(any::<u8>(), 1..256),
531 flip_index in 0usize..256,
532 ) {
533 use ed25519_dalek::SigningKey;
534 use ed25519_dalek::rand_core::UnwrapErr;
535 let signing = SigningKey::generate(&mut UnwrapErr(getrandom::SysRng));
536 let component = build_ed25519_component(&signing, &msg)?;
537 let composite = CompositeSignature::new(vec![component]);
538
539 let mut tampered_msg = msg.clone();
540 let mut tampered_sig = composite.components[0].signature.clone();
541 if flip_index < tampered_msg.len() {
542 tampered_msg[flip_index] ^= 0x01;
543 } else {
544 let sig_idx = flip_index - tampered_msg.len();
545 if sig_idx < tampered_sig.len() {
546 tampered_sig[sig_idx] ^= 0x01;
547 } else {
548 return Ok(()); }
550 }
551 let tampered = CompositeSignature::new(vec![ComponentSignature {
552 algorithm: ED25519.to_string(),
553 public_key: composite.components[0].public_key.clone(),
554 signature: tampered_sig,
555 }]);
556 let result = tampered.verify(&tampered_msg, |alg, pk, m, sig| {
557 if alg == ED25519 {
558 ed25519_verifier(alg, pk, m, sig)
559 } else {
560 Err(format!("unknown algorithm: {alg}"))
561 }
562 })?;
563 prop_assert!(!result.all_verified);
564 }
565 }
566}