Skip to main content

confium_signatif/
verify.rs

1//! The deep verification entry: one interface for every surface.
2//!
3//! The [`Pipeline`] is the ordered hard/soft check engine, but its
4//! constructor takes every dependency as a raw parameter, so each
5//! transport adapter (browser WASM, HTTP, CLI, Python) used to
6//! re-assemble the same wiring — a verifier fleet, a revocation view,
7//! transparency inputs, an acceptance policy — just to reach it.
8//! This module owns that assembly once.
9//!
10//! ```no_run
11//! use confium_signatif::verify::{verify_trusted_artifact, VerifyOptions};
12//! # use confium_signatif::artifact::TrustedArtifact;
13//! # use confium_signatif::bundle::TrustAnchorBundle;
14//! # use confium_signatif::graph::TrustGraph;
15//! # use confium_signatif::registry::Registry;
16//! # fn main() -> Result<(), confium_signatif::SignatifError> {
17//! # let (artifact, bundle, graph, registry) = unimplemented!();
18//! let options = VerifyOptions {
19//!     transparency_included: true,
20//!     accepted_labels: vec!["verified".into()],
21//!     ..VerifyOptions::default()
22//! };
23//! let verdict = verify_trusted_artifact(&artifact, &bundle, &graph, &registry, &options)?;
24//! assert!(verdict.accept);
25//! # Ok(())
26//! # }
27//! ```
28//!
29//! Revocation checking currently runs with
30//! [`NoRevocations`](crate::revocation::NoRevocations); this
31//! function is the single place that changes when a surface grows a
32//! revocation input. Schemes that need a [`CrlView`](crate::revocation::CrlView)
33//! today assemble the [`Pipeline`] directly.
34
35use serde::{Deserialize, Serialize};
36
37use crate::artifact::TrustedArtifact;
38use crate::bundle::TrustAnchorBundle;
39use crate::coverage::{Acceptance, AcceptancePolicy, CoverageReport};
40use crate::graph::{SignatureVerifier, TrustGraph};
41use crate::pipeline::{Pipeline, TransparencyInputs};
42use crate::registry::Registry;
43use crate::{SignatifError, SignatifResult};
44
45/// Which signature algorithms a verifier checks.
46///
47/// The fleet is scheme policy, not per-surface code: pick a variant
48/// instead of hand-writing a [`SignatureVerifier`]. The seam stays
49/// open — exotic fleets (threshold, HSM) still implement the trait.
50#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51pub enum Fleet {
52    /// Ed25519 only — the browser/WASM verifier profile.
53    #[serde(rename = "ed25519")]
54    Ed25519,
55    /// Ed25519 or ECDSA-P256 — the classical algorithms of the
56    /// default registry.
57    #[serde(rename = "ed25519_p256")]
58    Ed25519P256,
59}
60
61impl SignatureVerifier for Fleet {
62    fn verify(&self, public_key: &[u8], message: &[u8], signature: &[u8]) -> bool {
63        match self {
64            Fleet::Ed25519 => ed25519(public_key, message, signature),
65            Fleet::Ed25519P256 => {
66                ed25519(public_key, message, signature) || p256(public_key, message, signature)
67            }
68        }
69    }
70}
71
72fn ed25519(public_key: &[u8], message: &[u8], signature: &[u8]) -> bool {
73    confium_composite::ed25519_verifier(confium_composite::ED25519, public_key, message, signature)
74        .is_ok()
75}
76
77fn p256(public_key: &[u8], message: &[u8], signature: &[u8]) -> bool {
78    confium_composite::p256_verifier(
79        confium_composite::ECDSA_P256,
80        public_key,
81        message,
82        signature,
83    )
84    .is_ok()
85}
86
87/// The verification inputs a transport collects, in the shape every
88/// surface already speaks: field names match the JSON options of the
89/// HTTP endpoint, the browser binding, and the CLI flags.
90#[derive(Debug, Clone, Deserialize)]
91#[serde(default)]
92pub struct VerifyOptions {
93    /// Which algorithms the verifier fleet checks.
94    pub fleet: Fleet,
95    /// Transparency inclusion was verified for this artifact.
96    pub transparency_included: bool,
97    /// An external time anchor was verified.
98    pub time_anchored: bool,
99    /// Externally-attested time (RFC 3339) from a verified time
100    /// authority.
101    pub time_attested_at: Option<String>,
102    /// The M-of-K multi-log quorum was met.
103    pub multi_log_quorum: bool,
104    /// Classification labels this verifier accepts (empty = reject
105    /// everything).
106    pub accepted_labels: Vec<String>,
107}
108
109impl Default for VerifyOptions {
110    fn default() -> Self {
111        Self {
112            fleet: Fleet::Ed25519P256,
113            transparency_included: false,
114            time_anchored: false,
115            time_attested_at: None,
116            multi_log_quorum: false,
117            accepted_labels: Vec::new(),
118        }
119    }
120}
121
122/// The graduated verification outcome: the objective coverage report,
123/// the scheme's classification label, and this verifier's acceptance
124/// decision — the triple every surface serializes.
125#[derive(Debug, Serialize)]
126pub struct Verdict {
127    /// The scheme's classification label.
128    pub label: String,
129    /// The verifier's acceptance decision.
130    pub accept: bool,
131    /// The objective coverage report.
132    pub coverage: CoverageReport,
133}
134
135/// Verify one trusted artifact through the full pipeline.
136///
137/// This is the single entry every transport adapter calls; the
138/// pipeline assembly (fleet, revocation view, transparency inputs,
139/// acceptance policy) lives here and nowhere else.
140///
141/// # Errors
142///
143/// Input decoding failures (including a malformed `time_attested_at`)
144/// and hard-check failures surface as [`SignatifError`]; the error
145/// names the failing check.
146pub fn verify_trusted_artifact(
147    artifact: &TrustedArtifact,
148    bundle: &TrustAnchorBundle,
149    graph: &TrustGraph,
150    registry: &Registry,
151    options: &VerifyOptions,
152) -> SignatifResult<Verdict> {
153    let time_attested_at = match &options.time_attested_at {
154        None => None,
155        Some(s) => Some(
156            chrono::DateTime::parse_from_rfc3339(s)
157                .map_err(|e| SignatifError::Encoding(format!("time_attested_at: {e}")))?
158                .with_timezone(&chrono::Utc),
159        ),
160    };
161    let no_revocations = crate::revocation::NoRevocations;
162    let acceptance = AcceptancePolicy {
163        accepted_labels: options.accepted_labels.clone(),
164    };
165    let pipe = Pipeline::new(
166        bundle,
167        graph,
168        registry,
169        &options.fleet,
170        &no_revocations,
171        TransparencyInputs {
172            artifact_included: options.transparency_included,
173            time_anchored: options.time_anchored,
174            time_attested_at,
175            multi_log_quorum: options.multi_log_quorum,
176            downgrades: vec![],
177        },
178        &acceptance,
179    );
180    let outcome = pipe.run(artifact, chrono::Utc::now())?;
181    Ok(Verdict {
182        label: outcome.label.0,
183        accept: outcome.acceptance == Acceptance::Accept,
184        coverage: outcome.report,
185    })
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191
192    fn fixture() -> (
193        crate::artifact::TrustedArtifact,
194        crate::bundle::TrustAnchorBundle,
195        crate::graph::TrustGraph,
196        crate::registry::Registry,
197    ) {
198        let f = crate::testing::Fixture::valid();
199        (f.artifact, f.bundle, f.graph, f.registry)
200    }
201
202    #[test]
203    fn verdict_ladders_and_accepts() {
204        let (artifact, bundle, graph, registry) = fixture();
205        let options = VerifyOptions {
206            accepted_labels: vec!["unverified".into()],
207            ..VerifyOptions::default()
208        };
209        let verdict =
210            verify_trusted_artifact(&artifact, &bundle, &graph, &registry, &options).unwrap();
211        assert_eq!(verdict.label, "unverified");
212        assert!(verdict.accept);
213
214        let options = VerifyOptions {
215            transparency_included: true,
216            time_anchored: true,
217            time_attested_at: Some(chrono::Utc::now().to_rfc3339()),
218            accepted_labels: vec!["verified".into()],
219            ..VerifyOptions::default()
220        };
221        let verdict =
222            verify_trusted_artifact(&artifact, &bundle, &graph, &registry, &options).unwrap();
223        assert_eq!(verdict.label, "verified");
224        assert!(verdict.accept);
225    }
226
227    #[test]
228    fn tampered_artifact_hard_fails() {
229        let (artifact, bundle, graph, registry) = fixture();
230        let mut value = serde_json::to_value(&artifact).unwrap();
231        value["payload"]["dose"] = serde_json::json!(999);
232        let tampered: TrustedArtifact = serde_json::from_value(value).unwrap();
233        let options = VerifyOptions::default();
234        let err =
235            verify_trusted_artifact(&tampered, &bundle, &graph, &registry, &options).unwrap_err();
236        assert!(format!("{err}").contains("signature_validity"), "{err}");
237    }
238
239    #[test]
240    fn malformed_time_is_an_input_error() {
241        let (artifact, bundle, graph, registry) = fixture();
242        let options = VerifyOptions {
243            time_attested_at: Some("not-a-time".into()),
244            ..VerifyOptions::default()
245        };
246        let err =
247            verify_trusted_artifact(&artifact, &bundle, &graph, &registry, &options).unwrap_err();
248        assert!(err.to_string().contains("time_attested_at"), "{err}");
249    }
250
251    #[test]
252    fn options_deserialize_from_surface_json() {
253        let json = r#"{
254            "transparency_included": true,
255            "time_anchored": true,
256            "time_attested_at": "2026-08-19T00:00:00Z",
257            "multi_log_quorum": false,
258            "accepted_labels": ["verified"]
259        }"#;
260        let options: VerifyOptions = serde_json::from_str(json).unwrap();
261        assert_eq!(options.fleet, Fleet::Ed25519P256);
262        assert!(options.transparency_included);
263        assert_eq!(options.accepted_labels, vec!["verified"]);
264
265        let browser: VerifyOptions = serde_json::from_str(r#"{"fleet": "ed25519"}"#).unwrap();
266        assert_eq!(browser.fleet, Fleet::Ed25519);
267    }
268}