1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
51pub enum Fleet {
52 #[serde(rename = "ed25519")]
54 Ed25519,
55 #[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#[derive(Debug, Clone, Deserialize)]
91#[serde(default)]
92pub struct VerifyOptions {
93 pub fleet: Fleet,
95 pub transparency_included: bool,
97 pub time_anchored: bool,
99 pub time_attested_at: Option<String>,
102 pub multi_log_quorum: bool,
104 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#[derive(Debug, Serialize)]
126pub struct Verdict {
127 pub label: String,
129 pub accept: bool,
131 pub coverage: CoverageReport,
133}
134
135pub 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, ®istry, &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, ®istry, &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, ®istry, &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, ®istry, &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}