Skip to main content

confium_wasm/
composite.rs

1//! `CompositeSignature` — PQ-migration composite signature verifier for
2//! browser/Node.js consumers.
3
4use confium_composite::{CompositeSignature as RustComposite, VerificationResult};
5use wasm_bindgen::prelude::*;
6
7/// A composite signature — multiple algorithm components over the same
8/// message. Construct via [`CompositeSignature::from_json`] (the canonical
9/// wire format) and verify with [`CompositeSignature::verify`].
10#[wasm_bindgen]
11pub struct CompositeSignature {
12    inner: RustComposite,
13}
14
15#[wasm_bindgen]
16impl CompositeSignature {
17    /// Parse a composite signature from its canonical JSON wire format.
18    ///
19    /// ```json
20    /// { "components": [
21    ///   { "algorithm": "Ed25519",
22    ///     "public_key": "<base64>",
23    ///     "signature": "<base64>" },
24    ///   ...
25    /// ] }
26    /// ```
27    #[wasm_bindgen(constructor)]
28    pub fn from_json(json: &str) -> Result<CompositeSignature, JsValue> {
29        let inner: RustComposite = serde_json::from_str(json)
30            .map_err(|e| js_err(&format!("invalid composite signature JSON: {e}")))?;
31        Ok(Self { inner })
32    }
33
34    /// Number of components.
35    #[wasm_bindgen(getter)]
36    pub fn component_count(&self) -> usize {
37        self.inner.component_count()
38    }
39
40    /// Algorithm identifiers in component order.
41    #[wasm_bindgen(getter)]
42    pub fn algorithms(&self) -> Vec<String> {
43        self.inner
44            .algorithms()
45            .into_iter()
46            .map(String::from)
47            .collect()
48    }
49
50    /// Verify all components against `message`. Returns a
51    /// [`CompositeVerificationResult`] describing per-component outcomes.
52    ///
53    /// Built-in verifiers: Ed25519 + ECDSA-P256. Unknown algorithms are
54    /// reported as failed components. JS callers needing ML-DSA-65 or
55    /// SLH-DSA verification must preprocess the composite and supply
56    /// their own verifier callback (Phase 2D).
57    #[wasm_bindgen]
58    pub fn verify(&self, message: &[u8]) -> Result<CompositeVerificationResult, JsValue> {
59        let result = self
60            .inner
61            .verify(message, |algorithm, public_key, m, signature| {
62                if algorithm == confium_composite::ED25519 {
63                    confium_composite::ed25519_verifier(algorithm, public_key, m, signature)
64                } else if algorithm == "ECDSA-P256" || algorithm == "ECDSA" {
65                    p256_verifier(public_key, m, signature)
66                } else {
67                    Err(format!("unsupported algorithm: {algorithm}"))
68                }
69            })
70            .map_err(|e| js_err(&e.to_string()))?;
71        Ok(CompositeVerificationResult { inner: result })
72    }
73}
74
75/// Verify an ECDSA-P256 signature. `public_key` is SEC1-encoded verifying
76/// key (compressed 33 bytes or uncompressed 65 bytes). `signature` is
77/// DER-encoded. SHA-256 is used as the digest.
78fn p256_verifier(public_key: &[u8], message: &[u8], signature: &[u8]) -> Result<(), String> {
79    use p256::ecdsa::{Signature, VerifyingKey, signature::Verifier};
80    let vk = VerifyingKey::from_sec1_bytes(public_key)
81        .map_err(|e| format!("invalid P-256 public key: {e}"))?;
82    let sig = Signature::from_der(signature).map_err(|e| format!("invalid DER signature: {e}"))?;
83    vk.verify(message, &sig).map_err(|e| format!("verify: {e}"))
84}
85
86/// Per-component + aggregate verification outcome.
87#[wasm_bindgen]
88pub struct CompositeVerificationResult {
89    inner: VerificationResult,
90}
91
92#[wasm_bindgen]
93impl CompositeVerificationResult {
94    /// True iff every component verified.
95    #[wasm_bindgen(getter)]
96    pub fn all_verified(&self) -> bool {
97        self.inner.all_verified
98    }
99
100    /// JSON array of `{ index, algorithm, verified, error? }` entries —
101    /// one per component. Returned as a JSON string because wasm-bindgen
102    /// doesn't natively marshal Vec<struct-with-Option> across the boundary.
103    #[wasm_bindgen(getter)]
104    pub fn per_component_json(&self) -> String {
105        // Build manually — the upstream `ComponentResult` doesn't derive
106        // Serialize, and we don't want a breaking change to a published crate.
107        let entries: Vec<String> = self
108            .inner
109            .per_component
110            .iter()
111            .map(|c| {
112                let alg = serde_json::to_string(&c.algorithm).unwrap_or_else(|_| "\"\"".into());
113                let err = match &c.error {
114                    Some(e) => format!(
115                        ",\"error\":{}",
116                        serde_json::to_string(e).unwrap_or_else(|_| "null".into())
117                    ),
118                    None => String::new(),
119                };
120                format!(
121                    "{{\"index\":{},\"algorithm\":{},\"verified\":{}{}}}",
122                    c.index, alg, c.verified, err
123                )
124            })
125            .collect();
126        format!("[{}]", entries.join(","))
127    }
128}
129
130fn js_err(msg: &str) -> JsValue {
131    JsValue::from_str(msg)
132}