Skip to main content

confium_daemon/methods/
composite.rs

1//! Composite signature verification methods.
2//!
3//! `composite_verify` is the first end-to-end crypto method in the
4//! daemon: it accepts a JSON envelope + base64 message, runs the
5//! built-in Ed25519 + ECDSA-P256 verifiers, and returns a structured
6//! per-component result.
7//!
8//! Stateless and self-contained — no handle store, no plugin loading.
9//! This is the shape we want for every "verifier" JSON-RPC method.
10
11use base64::{Engine as _, engine::general_purpose};
12use serde::Deserialize;
13use serde_json::{Value, json};
14
15use crate::error::RpcError;
16use crate::server::SharedConfium;
17
18/// Request body for `composite_verify`.
19#[derive(Debug, Deserialize)]
20struct CompositeVerifyRequest {
21    /// Base64-encoded signed message.
22    message: String,
23    /// JSON-serialized [`confium_composite::CompositeSignature`].
24    composite: Value,
25}
26
27/// `composite_verify({ "message": "<base64>", "composite": {...} })`
28///
29/// Returns:
30/// ```json
31/// {
32///   "all_verified": true,
33///   "per_component": [
34///     { "index": 0, "algorithm": "Ed25519", "verified": true, "error": null }
35///   ]
36/// }
37/// ```
38pub async fn composite_verify(
39    _cfm: SharedConfium,
40    params: Value,
41) -> std::result::Result<Value, RpcError> {
42    let req: CompositeVerifyRequest =
43        serde_json::from_value(params).map_err(|e| RpcError::InvalidParams {
44            detail: format!("composite_verify params: {e}"),
45        })?;
46
47    let message = general_purpose::STANDARD
48        .decode(req.message.as_bytes())
49        .map_err(|e| RpcError::InvalidParams {
50            detail: format!("message: not valid base64: {e}"),
51        })?;
52
53    let composite_json = serde_json::to_string(&req.composite).unwrap_or_else(|_| "{}".into());
54    let composite: confium_composite::CompositeSignature = serde_json::from_str(&composite_json)
55        .map_err(|e| RpcError::InvalidParams {
56            detail: format!("composite: invalid envelope: {e}"),
57        })?;
58
59    let result = composite
60        .verify(&message, |alg, pk, msg, sig| match alg {
61            confium_composite::ED25519 => confium_composite::ed25519_verifier(alg, pk, msg, sig),
62            confium_composite::ECDSA_P256 => confium_composite::p256_verifier(alg, pk, msg, sig),
63            other => Err(format!(
64                "no builtin verifier for algorithm '{other}' \
65                 (composite_verify supports Ed25519 and ECDSA-P256)"
66            )),
67        })
68        .map_err(|e| RpcError::Engine {
69            message: format!("composite verify: {e}"),
70        })?;
71
72    let per_component: Vec<Value> = result
73        .per_component
74        .iter()
75        .map(|c| {
76            json!({
77                "index": c.index,
78                "algorithm": c.algorithm,
79                "verified": c.verified,
80                "error": c.error,
81            })
82        })
83        .collect();
84
85    Ok(json!({
86        "all_verified": result.all_verified,
87        "per_component": per_component,
88    }))
89}
90
91#[cfg(test)]
92mod tests {
93    use super::*;
94    use crate::test_util::test_confium;
95    use base64::engine::general_purpose;
96    use ed25519_dalek::rand_core::UnwrapErr;
97    use ed25519_dalek::{Signature, Signer, SigningKey, VerifyingKey};
98
99    fn make_ed25519_component(message: &[u8]) -> (Value, Vec<u8>) {
100        let signing = SigningKey::generate(&mut UnwrapErr(getrandom::SysRng));
101        let sig: Signature = signing.sign(message);
102        let vk: VerifyingKey = signing.verifying_key();
103        let pk_bytes = vk.to_bytes().to_vec();
104        let sig_bytes = sig.to_bytes().to_vec();
105        let composite = json!({
106            "components": [{
107                "algorithm": "Ed25519",
108                "public_key": pk_bytes,
109                "signature": sig_bytes,
110            }]
111        });
112        (composite, message.to_vec())
113    }
114
115    #[tokio::test]
116    async fn composite_verify_accepts_valid_signature() {
117        let message = b"daemon composite_verify integration";
118        let (composite, _) = make_ed25519_component(message);
119        let msg_b64 = general_purpose::STANDARD.encode(message);
120
121        let result = composite_verify(
122            test_confium(),
123            json!({ "message": msg_b64, "composite": composite }),
124        )
125        .await
126        .unwrap();
127
128        assert_eq!(result["all_verified"], json!(true));
129        assert_eq!(result["per_component"][0]["verified"], json!(true));
130        assert_eq!(result["per_component"][0]["algorithm"], "Ed25519");
131    }
132
133    #[tokio::test]
134    async fn composite_verify_rejects_tampered_message() {
135        let message = b"original message";
136        let (composite, _) = make_ed25519_component(message);
137        let tampered_b64 = general_purpose::STANDARD.encode(b"different message");
138
139        let result = composite_verify(
140            test_confium(),
141            json!({ "message": tampered_b64, "composite": composite }),
142        )
143        .await
144        .unwrap();
145
146        assert_eq!(result["all_verified"], json!(false));
147        assert_eq!(result["per_component"][0]["verified"], json!(false));
148        assert!(result["per_component"][0]["error"].is_string());
149    }
150
151    #[tokio::test]
152    async fn composite_verify_rejects_invalid_base64() {
153        let result = composite_verify(
154            test_confium(),
155            json!({ "message": "not base64 !!!", "composite": {} }),
156        )
157        .await;
158        assert!(result.is_err());
159    }
160
161    #[tokio::test]
162    async fn composite_verify_rejects_missing_fields() {
163        let result = composite_verify(test_confium(), json!({})).await;
164        assert!(result.is_err());
165    }
166
167    #[tokio::test]
168    async fn composite_verify_reports_unsupported_algorithm() {
169        let composite = json!({
170            "components": [{
171                "algorithm": "ML-DSA-65",
172                "public_key": vec![0u8; 32],
173                "signature": vec![0u8; 64],
174            }]
175        });
176        let msg_b64 = general_purpose::STANDARD.encode(b"msg");
177        let result = composite_verify(
178            test_confium(),
179            json!({ "message": msg_b64, "composite": composite }),
180        )
181        .await
182        .unwrap();
183        assert_eq!(result["all_verified"], json!(false));
184        let err = result["per_component"][0]["error"].as_str().unwrap();
185        assert!(err.contains("no builtin verifier"));
186    }
187}