confium_daemon/methods/
composite.rs1use 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#[derive(Debug, Deserialize)]
20struct CompositeVerifyRequest {
21 message: String,
23 composite: Value,
25}
26
27pub 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}