Skip to main content

confium_verify_server/
handlers.rs

1//! HTTP handlers for the verification service.
2
3use 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/// Request: verify a composite signature.
17#[derive(Debug, Deserialize)]
18pub struct VerifyCompositeRequest {
19    /// Message bytes (hex).
20    pub message_hex: String,
21    /// Composite signature components.
22    pub components: Vec<ComponentInput>,
23}
24
25/// One component of a composite signature.
26#[derive(Debug, Deserialize)]
27pub struct ComponentInput {
28    /// Algorithm (e.g., "Ed25519", "ECDSA-P256").
29    pub algorithm: String,
30    /// Public key (hex).
31    pub public_key_hex: String,
32    /// Signature (hex).
33    pub signature_hex: String,
34}
35
36/// Response: verification result.
37#[derive(Debug, Serialize)]
38pub struct VerifyResponse {
39    pub verified: bool,
40    pub component_count: usize,
41    pub errors: Vec<String>,
42}
43
44/// Request: verify an inclusion proof.
45#[derive(Debug, Deserialize)]
46pub struct VerifyInclusionRequest {
47    /// Sequence number.
48    pub sequence: u64,
49    /// Leaf artifact hash (hex, 32 bytes).
50    pub artifact_hash_hex: String,
51    /// Root hash (hex, 32 bytes).
52    pub root_hex: String,
53    /// Proof steps: each is { sibling_hex, side }.
54    pub steps: Vec<ProofStepInput>,
55}
56
57/// One step of an inclusion proof.
58#[derive(Debug, Deserialize)]
59pub struct ProofStepInput {
60    /// Sibling hash (hex, 32 bytes).
61    pub sibling_hex: String,
62    /// Side: "left" or "right".
63    pub side: String,
64}
65
66/// Shared application state.
67#[derive(Clone, Default)]
68pub struct AppState;
69
70/// POST /verify/composite
71pub 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
125/// POST /verify/inclusion
126pub 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
184/// GET /healthz
185pub 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/// Request: verify a SIGNATIF trusted artifact through the full
326/// pipeline. The four framework objects arrive as embedded JSON.
327#[derive(Debug, Deserialize)]
328pub struct VerifySignatifRequest {
329    /// The trusted artifact.
330    pub artifact: serde_json::Value,
331    /// The trust anchor bundle.
332    pub bundle: serde_json::Value,
333    /// The trust graph.
334    pub graph: serde_json::Value,
335    /// The scheme registry; defaults to the initial values when absent.
336    #[serde(default)]
337    pub registry: Option<serde_json::Value>,
338    /// Verification options (transparency/time inputs, accepted
339    /// labels); defaults when absent.
340    #[serde(default)]
341    pub options: VerifySignatifOptions,
342}
343
344/// Options for the pipeline run. Mirrors
345/// `confium_signatif::verify::VerifyOptions`; the wire-facing struct
346/// stays put for API compatibility and converts at the pipeline call.
347#[derive(Debug, Default, Deserialize)]
348#[serde(default)]
349pub struct VerifySignatifOptions {
350    /// Transparency inclusion was verified for this artifact.
351    #[serde(default)]
352    pub transparency_included: bool,
353    /// An external time anchor was verified.
354    #[serde(default)]
355    pub time_anchored: bool,
356    /// Externally-attested time (RFC 3339).
357    #[serde(default)]
358    pub time_attested_at: Option<String>,
359    /// Multi-log quorum met.
360    #[serde(default)]
361    pub multi_log_quorum: bool,
362    /// Accepted classification labels (empty = reject everything).
363    #[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/// Response: the graduated verification outcome.
381#[derive(Debug, Serialize)]
382pub struct VerifySignatifResponse {
383    /// The scheme's classification label.
384    pub label: String,
385    /// The verifier's acceptance decision.
386    pub accept: bool,
387    /// The objective coverage report.
388    pub coverage: confium_signatif::coverage::CoverageReport,
389}
390
391/// POST /verify/signatif
392pub 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, &registry, &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}