1use axum::extract::State;
4use axum::http::StatusCode;
5use axum::response::Json;
6use serde::{Deserialize, Serialize};
7
8use confium_composite::{
9 ComponentSignature, CompositeSignature, ECDSA_P256, ED25519, ed25519_verifier, p256_verifier,
10};
11use confium_signatif::graph::TrustGraph;
12use confium_signatif::{artifact::TrustedArtifact, bundle::TrustAnchorBundle, registry::Registry};
13use confium_transparency::entry::MerkleEntry;
14use confium_transparency::merkle::{Hash, InclusionProof, MerkleTree, ProofStep, Side};
15
16#[derive(Debug, Deserialize)]
18pub struct VerifyCompositeRequest {
19 pub message_hex: String,
21 pub components: Vec<ComponentInput>,
23}
24
25#[derive(Debug, Deserialize)]
27pub struct ComponentInput {
28 pub algorithm: String,
30 pub public_key_hex: String,
32 pub signature_hex: String,
34}
35
36#[derive(Debug, Serialize)]
38pub struct VerifyResponse {
39 pub verified: bool,
40 pub component_count: usize,
41 pub errors: Vec<String>,
42}
43
44#[derive(Debug, Deserialize)]
46pub struct VerifyInclusionRequest {
47 pub sequence: u64,
49 pub artifact_hash_hex: String,
51 pub root_hex: String,
53 pub steps: Vec<ProofStepInput>,
55}
56
57#[derive(Debug, Deserialize)]
59pub struct ProofStepInput {
60 pub sibling_hex: String,
62 pub side: String,
64}
65
66#[derive(Clone, Default)]
68pub struct AppState;
69
70pub async fn verify_composite(
72 State(_state): State<AppState>,
73 Json(req): Json<VerifyCompositeRequest>,
74) -> Result<Json<VerifyResponse>, (StatusCode, String)> {
75 let message = hex::decode(&req.message_hex)
76 .map_err(|e| (StatusCode::BAD_REQUEST, format!("invalid message_hex: {e}")))?;
77
78 let mut components = Vec::new();
79 for (i, c) in req.components.iter().enumerate() {
80 let pk = hex::decode(&c.public_key_hex).map_err(|e| {
81 (
82 StatusCode::BAD_REQUEST,
83 format!("invalid public_key_hex at index {i}: {e}"),
84 )
85 })?;
86 let sig = hex::decode(&c.signature_hex).map_err(|e| {
87 (
88 StatusCode::BAD_REQUEST,
89 format!("invalid signature_hex at index {i}: {e}"),
90 )
91 })?;
92 components.push(ComponentSignature {
93 algorithm: c.algorithm.clone(),
94 public_key: pk,
95 signature: sig,
96 });
97 }
98
99 let composite = CompositeSignature::new(components);
100 let result = composite
101 .verify(&message, |alg, pk, m, sig| {
102 if alg == ED25519 {
103 ed25519_verifier(alg, pk, m, sig)
104 } else if alg == ECDSA_P256 {
105 p256_verifier(alg, pk, m, sig)
106 } else {
107 Err(format!("unsupported algorithm: {alg}"))
108 }
109 })
110 .map_err(|e| (StatusCode::BAD_REQUEST, format!("verify error: {e}")))?;
111
112 let errors: Vec<String> = result
113 .per_component
114 .iter()
115 .filter_map(|c| c.error.clone())
116 .collect();
117
118 Ok(Json(VerifyResponse {
119 verified: result.all_verified,
120 component_count: result.per_component.len(),
121 errors,
122 }))
123}
124
125pub async fn verify_inclusion(
127 State(_state): State<AppState>,
128 Json(req): Json<VerifyInclusionRequest>,
129) -> Result<Json<VerifyResponse>, (StatusCode, String)> {
130 let artifact_hash = decode_hash(&req.artifact_hash_hex).map_err(|e| {
131 (
132 StatusCode::BAD_REQUEST,
133 format!("invalid artifact_hash_hex: {e}"),
134 )
135 })?;
136 let root = decode_hash(&req.root_hex)
137 .map_err(|e| (StatusCode::BAD_REQUEST, format!("invalid root_hex: {e}")))?;
138
139 let mut steps = Vec::new();
140 for (i, s) in req.steps.iter().enumerate() {
141 let sibling = decode_hash(&s.sibling_hex).map_err(|e| {
142 (
143 StatusCode::BAD_REQUEST,
144 format!("invalid sibling_hex at step {i}: {e}"),
145 )
146 })?;
147 let side = match s.side.to_lowercase().as_str() {
148 "left" => Side::Left,
149 "right" => Side::Right,
150 _ => {
151 return Err((
152 StatusCode::BAD_REQUEST,
153 format!("invalid side at step {i}: {}", s.side),
154 ));
155 }
156 };
157 steps.push(ProofStep { sibling, side });
158 }
159
160 let entry = MerkleEntry::new(
161 req.sequence,
162 confium_transparency::entry::ArtifactType::ThresholdSignature,
163 artifact_hash,
164 );
165 let proof = InclusionProof {
166 sequence: req.sequence,
167 steps,
168 };
169
170 match MerkleTree::verify_inclusion(&entry, &proof, root) {
171 Ok(()) => Ok(Json(VerifyResponse {
172 verified: true,
173 component_count: 1,
174 errors: vec![],
175 })),
176 Err(e) => Ok(Json(VerifyResponse {
177 verified: false,
178 component_count: 1,
179 errors: vec![format!("{e:?}")],
180 })),
181 }
182}
183
184pub async fn healthz() -> &'static str {
186 "ok"
187}
188
189fn decode_hash(hex_str: &str) -> Result<Hash, String> {
190 let bytes = hex::decode(hex_str).map_err(|e| e.to_string())?;
191 if bytes.len() != 32 {
192 return Err(format!("expected 32 bytes, got {}", bytes.len()));
193 }
194 let mut arr = [0u8; 32];
195 arr.copy_from_slice(&bytes);
196 Ok(arr)
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use axum::body::Body;
203 use axum::http::{Method, Request};
204 use tower::ServiceExt;
205
206 fn app() -> axum::Router {
207 axum::Router::new()
208 .route("/verify/composite", axum::routing::post(verify_composite))
209 .route("/verify/signatif", axum::routing::post(verify_signatif))
210 .route("/verify/inclusion", axum::routing::post(verify_inclusion))
211 .route("/healthz", axum::routing::get(healthz))
212 .with_state(AppState)
213 }
214
215 #[tokio::test]
216 async fn healthz_returns_ok() {
217 let response = app()
218 .oneshot(
219 Request::builder()
220 .uri("/healthz")
221 .body(Body::empty())
222 .unwrap(),
223 )
224 .await
225 .unwrap();
226 assert_eq!(response.status(), StatusCode::OK);
227 }
228
229 #[tokio::test]
230 async fn verify_composite_rejects_bad_hex() {
231 let body = serde_json::json!({
232 "message_hex": "not-hex!!",
233 "components": []
234 });
235 let response = app()
236 .oneshot(
237 Request::builder()
238 .method(Method::POST)
239 .uri("/verify/composite")
240 .header("content-type", "application/json")
241 .body(Body::from(body.to_string()))
242 .unwrap(),
243 )
244 .await
245 .unwrap();
246 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
247 }
248
249 #[tokio::test]
250 async fn verify_inclusion_rejects_bad_root() {
251 let body = serde_json::json!({
252 "sequence": 0,
253 "artifact_hash_hex": "00",
254 "root_hex": "00",
255 "steps": []
256 });
257 let response = app()
258 .oneshot(
259 Request::builder()
260 .method(Method::POST)
261 .uri("/verify/inclusion")
262 .header("content-type", "application/json")
263 .body(Body::from(body.to_string()))
264 .unwrap(),
265 )
266 .await
267 .unwrap();
268 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
269 }
270
271 #[tokio::test]
272 async fn verify_inclusion_accepts_valid_proof() {
273 let mut tree = MerkleTree::new();
274 tree.append(MerkleEntry::new(
275 0,
276 confium_transparency::entry::ArtifactType::ThresholdSignature,
277 [1u8; 32],
278 ));
279 let root = tree.root();
280 let proof = tree.inclusion_proof(0).unwrap();
281 let entry = tree.entry(0).unwrap();
282
283 let steps_json: Vec<_> = proof
284 .steps
285 .iter()
286 .map(|s| {
287 serde_json::json!({
288 "sibling_hex": hex::encode(s.sibling),
289 "side": match s.side {
290 Side::Left => "left",
291 Side::Right => "right",
292 }
293 })
294 })
295 .collect();
296
297 let body = serde_json::json!({
298 "sequence": 0,
299 "artifact_hash_hex": hex::encode(entry.artifact_hash),
300 "root_hex": hex::encode(root),
301 "steps": steps_json
302 });
303
304 let response = app()
305 .oneshot(
306 Request::builder()
307 .method(Method::POST)
308 .uri("/verify/inclusion")
309 .header("content-type", "application/json")
310 .body(Body::from(body.to_string()))
311 .unwrap(),
312 )
313 .await
314 .unwrap();
315 assert_eq!(response.status(), StatusCode::OK);
316 }
317
318 #[test]
319 fn decode_hash_validates_length() {
320 assert!(decode_hash("00").is_err());
321 assert!(decode_hash(&"ab".repeat(32)).is_ok());
322 }
323}
324
325#[derive(Debug, Deserialize)]
328pub struct VerifySignatifRequest {
329 pub artifact: serde_json::Value,
331 pub bundle: serde_json::Value,
333 pub graph: serde_json::Value,
335 #[serde(default)]
337 pub registry: Option<serde_json::Value>,
338 #[serde(default)]
341 pub options: VerifySignatifOptions,
342}
343
344#[derive(Debug, Default, Deserialize)]
348#[serde(default)]
349pub struct VerifySignatifOptions {
350 #[serde(default)]
352 pub transparency_included: bool,
353 #[serde(default)]
355 pub time_anchored: bool,
356 #[serde(default)]
358 pub time_attested_at: Option<String>,
359 #[serde(default)]
361 pub multi_log_quorum: bool,
362 #[serde(default)]
364 pub accepted_labels: Vec<String>,
365}
366
367impl From<&VerifySignatifOptions> for confium_signatif::verify::VerifyOptions {
368 fn from(o: &VerifySignatifOptions) -> Self {
369 Self {
370 transparency_included: o.transparency_included,
371 time_anchored: o.time_anchored,
372 time_attested_at: o.time_attested_at.clone(),
373 multi_log_quorum: o.multi_log_quorum,
374 accepted_labels: o.accepted_labels.clone(),
375 ..Self::default()
376 }
377 }
378}
379
380#[derive(Debug, Serialize)]
382pub struct VerifySignatifResponse {
383 pub label: String,
385 pub accept: bool,
387 pub coverage: confium_signatif::coverage::CoverageReport,
389}
390
391pub async fn verify_signatif(
393 State(_state): State<AppState>,
394 Json(req): Json<VerifySignatifRequest>,
395) -> (StatusCode, Json<serde_json::Value>) {
396 let respond = |status: StatusCode, body: serde_json::Value| (status, Json(body));
397 let artifact: TrustedArtifact = match serde_json::from_value(req.artifact) {
398 Ok(a) => a,
399 Err(e) => {
400 return respond(
401 StatusCode::BAD_REQUEST,
402 serde_json::json!({"error": format!("artifact: {e}")}),
403 );
404 }
405 };
406 let bundle: TrustAnchorBundle = match serde_json::from_value(req.bundle) {
407 Ok(b) => b,
408 Err(e) => {
409 return respond(
410 StatusCode::BAD_REQUEST,
411 serde_json::json!({"error": format!("bundle: {e}")}),
412 );
413 }
414 };
415 let graph: TrustGraph = match serde_json::from_value(req.graph) {
416 Ok(g) => g,
417 Err(e) => {
418 return respond(
419 StatusCode::BAD_REQUEST,
420 serde_json::json!({"error": format!("graph: {e}")}),
421 );
422 }
423 };
424 let registry: Registry = match req.registry {
425 Some(v) => match serde_json::from_value(v) {
426 Ok(r) => r,
427 Err(e) => {
428 return respond(
429 StatusCode::BAD_REQUEST,
430 serde_json::json!({"error": format!("registry: {e}")}),
431 );
432 }
433 },
434 None => Registry::with_initial_values(),
435 };
436 let options = confium_signatif::verify::VerifyOptions::from(&req.options);
437 match confium_signatif::verify::verify_trusted_artifact(
438 &artifact, &bundle, &graph, ®istry, &options,
439 ) {
440 Ok(verdict) => respond(
441 StatusCode::OK,
442 serde_json::to_value(VerifySignatifResponse {
443 label: verdict.label,
444 accept: verdict.accept,
445 coverage: verdict.coverage,
446 })
447 .unwrap_or_default(),
448 ),
449 Err(e) => respond(
450 StatusCode::UNPROCESSABLE_ENTITY,
451 serde_json::json!({"error": format!("{e}"), "label": "rejected"}),
452 ),
453 }
454}