Skip to main content

confium_composite/
lib.rs

1//! Composite multi-algorithm signature aggregation.
2//!
3//! Combines classical (Ed25519, ECDSA) and PQ (ML-DSA, SLH-DSA)
4//! signatures so that breaking either alone doesn't break the
5//! composite. Used for PQ migration without breaking verifiers.
6//!
7//! See `TODO.roadmap/35-pq-composite-signatures.md` for the full spec.
8//!
9//! # Example
10//!
11//! ```
12//! use confium_composite::{CompositeSignature, build_ed25519_component, ed25519_verifier, ED25519};
13//! use ed25519_dalek::{Signer, SigningKey};
14//! use ed25519_dalek::rand_core::UnwrapErr;
15//!
16//! let signing = SigningKey::generate(&mut UnwrapErr(getrandom::SysRng));
17//! let message = b"hybrid sig demo";
18//! let component = build_ed25519_component(&signing, message)?;
19//! let composite = CompositeSignature::new(vec![component]);
20//! let result = composite.verify(message, |alg, pk, msg, sig| {
21//!     if alg == ED25519 { ed25519_verifier(alg, pk, msg, sig) }
22//!     else { Err(format!("unknown algorithm: {alg}")) }
23//! })?;
24//! assert!(result.all_verified);
25//! # Ok::<(), Box<dyn std::error::Error>>(())
26//! ```
27
28#![forbid(unsafe_code)]
29#![allow(missing_docs)] // TODO: document before 1.0
30
31use 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
44/// Algorithm identifier for Ed25519 components.
45pub const ED25519: &str = "Ed25519";
46/// Algorithm identifier for ECDSA-P256 components (NIST P-256 + SHA-256).
47pub const ECDSA_P256: &str = "ECDSA-P256";
48/// Algorithm identifier for ML-DSA-65 components (placeholder; no real verifier).
49pub const ML_DSA_65: &str = "ML-DSA-65";
50
51/// A single component of a composite signature.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub struct ComponentSignature {
54    /// Algorithm identifier (e.g., "Ed25519", "ML-DSA-65").
55    pub algorithm: String,
56    /// Public key bytes.
57    pub public_key: Vec<u8>,
58    /// Signature bytes.
59    pub signature: Vec<u8>,
60}
61
62/// A composite signature — multiple components over the same message.
63#[derive(Debug, Clone, Serialize, Deserialize)]
64pub struct CompositeSignature {
65    /// Component signatures.
66    pub components: Vec<ComponentSignature>,
67}
68
69/// Errors during composite signature operations.
70#[derive(Debug, thiserror::Error)]
71pub enum CompositeError {
72    /// Verification failed (at least one component invalid).
73    #[error("verification failed: {0}")]
74    Verify(String),
75    /// No components.
76    #[error("composite signature has no components")]
77    Empty,
78    /// Serialization error.
79    #[error("serialization error: {0}")]
80    Serde(#[from] serde_json::Error),
81}
82
83impl CompositeSignature {
84    /// Build a composite from components.
85    pub fn new(components: Vec<ComponentSignature>) -> Self {
86        Self { components }
87    }
88
89    /// Number of components.
90    pub fn component_count(&self) -> usize {
91        self.components.len()
92    }
93
94    /// List the algorithm identifiers.
95    pub fn algorithms(&self) -> Vec<&str> {
96        self.components
97            .iter()
98            .map(|c| c.algorithm.as_str())
99            .collect()
100    }
101
102    /// Verify all components. Caller provides the verifier function:
103    /// (algorithm, public_key, message, signature) → Result<(), String>.
104    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/// Per-component verification result.
144#[derive(Debug, Clone)]
145pub struct ComponentResult {
146    /// Index in components vector.
147    pub index: usize,
148    /// Algorithm.
149    pub algorithm: String,
150    /// Whether this component verified.
151    pub verified: bool,
152    /// Error message if verification failed.
153    pub error: Option<String>,
154}
155
156/// Aggregate verification result.
157#[derive(Debug, Clone)]
158pub struct VerificationResult {
159    /// True iff every component verified.
160    pub all_verified: bool,
161    /// Per-component results.
162    pub per_component: Vec<ComponentResult>,
163}
164
165/// Standard composite algorithm IDs per IETF LAMPS COMPOSITE SIG draft.
166pub mod algorithm_ids {
167    /// Ed25519 + ML-DSA-65 composite.
168    pub const ED25519_MLDSA65: &str = "id-MLDSA65-Ed25519";
169    /// ECDSA-P256 + ML-DSA-65 composite.
170    pub const ECDSAP256_MLDSA65: &str = "id-MLDSA65-ECDSA-P256";
171    /// ECDSA-P384 + ML-DSA-87 composite.
172    pub const ECDSAP384_MLDSA87: &str = "id-MLDSA87-ECDSA-P384";
173    /// Ed25519 + SLH-DSA-128s composite.
174    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
235/// Real Ed25519 verifier. Use as the verifier callback when the composite
236/// contains an Ed25519 component. Returns Ok if the Ed25519 component verifies.
237pub 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
258/// Verify an ECDSA-P256 signature (NIST P-256 over SHA-256). The public
259/// key is encoded as SEC1 (compressed 33 bytes or uncompressed 65
260/// bytes). The signature is DER-encoded per RFC 5480.
261///
262/// Use as the per-component verifier callback in
263/// [`CompositeSignature::verify`] when the composite contains an
264/// ECDSA-P256 component.
265/// The ML-DSA-65 algorithm identifier (FIPS 204, category 3).
266pub const MLDSA65: &str = "ML-DSA-65";
267
268/// Verify a single ML-DSA-65 component (requires the `pq` feature).
269///
270/// # Errors
271///
272/// Returns a human-readable error for wrong algorithm, malformed key
273/// or signature bytes, or verification failure.
274#[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/// The SLH-DSA-SHA2-128s algorithm identifier (FIPS 205, category 1).
288#[cfg(feature = "pq-slh")]
289pub const SLHDSA128S: &str = "SLH-DSA-128s";
290
291/// Verify a single SLH-DSA-SHA2-128s component (feature `pq-slh`).
292///
293/// # Errors
294///
295/// Human-readable errors for wrong algorithm, malformed inputs, or
296/// verification failure.
297#[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/// Transition composite verifier: Ed25519 + ECDSA-P256 + ML-DSA-65 in
311/// one dispatch closure — the classical+PQC AND-composition of
312/// SIGNATIF §9.4 during the migration's composite phase. With the
313/// `pq-slh` feature it additionally accepts SLH-DSA-128s, enabling
314/// PQC-only composites (two post-quantum algorithms AND-composed).
315///
316/// # Errors
317///
318/// Returns the failing component's error.
319#[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
352/// Build a real Ed25519 component signature. Used for testing and as a
353/// reference for plugin authors.
354pub 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
367/// Build a real ECDSA-P256 component signature (NIST P-256 over SHA-256).
368/// The signature is DER-encoded per RFC 5480; the public key is SEC1
369/// (uncompressed, 65 bytes).
370///
371/// Sibling to [`build_ed25519_component`]. Use both to construct a
372/// hybrid classical-classical composite, or pair either with an
373/// ML-DSA component for PQ migration.
374pub 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        // Mock ML-DSA component (always verifies for now)
472        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    // Round-trip: build an Ed25519 component, encode to JSON, parse back,
500    // verify. Should hold for arbitrary messages.
501    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; // dummy use to silence warning
522        }
523    }
524
525    // Tamper detection: flipping any bit of the signature or message
526    // must cause verification to fail.
527    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(()); // index out of both ranges — skip
549                }
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}