1use std::collections::BTreeMap;
2
3use anyhow::{anyhow, Context, Result};
4use chrono::{DateTime, SecondsFormat, Utc};
5use serde::{Deserialize, Serialize};
6use serde_json::{json, Map, Value};
7use uuid::Uuid;
8
9use super::{
10 c2pa_signer, digest_bytes, verify_signature_envelope, CapabilityToken, CapabilityTokenScope,
11 SignatureEnvelope,
12};
13
14#[derive(Clone, Debug, Serialize, Deserialize)]
16pub struct C2paManifestEnvelope {
17 pub manifest: Value,
18 pub signature: SignatureEnvelope,
19}
20
21pub struct VerifiedManifest {
22 pub manifest: Value,
23 pub attestation_ids: Vec<Uuid>,
24 pub artifact_sha256: String,
25}
26
27#[derive(Clone, Copy, Debug, Serialize, Deserialize)]
28#[serde(rename_all = "kebab-case")]
29pub enum ManifestProfile {
30 Basic,
31 PolicyEvidence,
32 Full,
33}
34
35impl ManifestProfile {
36 pub fn as_str(&self) -> &'static str {
37 match self {
38 ManifestProfile::Basic => "basic",
39 ManifestProfile::PolicyEvidence => "policy-evidence",
40 ManifestProfile::Full => "full",
41 }
42 }
43}
44
45impl Default for ManifestProfile {
46 fn default() -> Self {
47 ManifestProfile::Basic
48 }
49}
50
51#[derive(Clone, Debug)]
52pub struct PolicyEvidence {
53 pub effect: String,
54 pub boundaries: Vec<String>,
55 pub reasons: Vec<String>,
56 pub previews: Vec<String>,
57 pub decision_attestation_ids: Vec<Uuid>,
58}
59
60impl PolicyEvidence {
61 pub fn is_empty(&self) -> bool {
62 self.effect.is_empty()
63 && self.boundaries.is_empty()
64 && self.reasons.is_empty()
65 && self.previews.is_empty()
66 && self.decision_attestation_ids.is_empty()
67 }
68}
69
70#[derive(Clone, Debug, Default)]
71pub struct IdentityEvidence {
72 pub run_id: Option<Uuid>,
73 pub step_id: Option<Uuid>,
74 pub subject_label: Option<String>,
75}
76
77impl IdentityEvidence {
78 pub fn new() -> Self {
79 Self::default()
80 }
81
82 pub fn with_run_id(mut self, run_id: Uuid) -> Self {
83 self.run_id = Some(run_id);
84 self
85 }
86
87 pub fn with_step_id(mut self, step_id: Uuid) -> Self {
88 self.step_id = Some(step_id);
89 self
90 }
91
92 pub fn with_subject_label(mut self, label: impl Into<String>) -> Self {
93 self.subject_label = Some(label.into());
94 self
95 }
96
97 pub fn is_empty(&self) -> bool {
98 self.run_id.is_none() && self.step_id.is_none() && self.subject_label.is_none()
99 }
100}
101
102#[derive(Clone, Debug)]
103pub struct CapabilityEvidenceEntry {
104 pub token_id: Uuid,
105 pub issued_at: DateTime<Utc>,
106 pub expires_at: DateTime<Utc>,
107 pub scope: CapabilityTokenScope,
108 pub audience: Option<Vec<String>>,
109}
110
111#[derive(Clone, Debug, Default)]
112pub struct CapabilityEvidence {
113 pub chain: Vec<CapabilityEvidenceEntry>,
114}
115
116impl CapabilityEvidence {
117 pub fn from_tokens(tokens: &[CapabilityToken]) -> Self {
118 let chain = tokens
119 .iter()
120 .map(|token| CapabilityEvidenceEntry {
121 token_id: token.claims.token_id,
122 issued_at: token.claims.issued_at,
123 expires_at: token.claims.expires_at,
124 scope: token.claims.scope.clone(),
125 audience: token.claims.audience.clone(),
126 })
127 .collect();
128 Self { chain }
129 }
130
131 pub fn is_empty(&self) -> bool {
132 self.chain.is_empty()
133 }
134}
135
136pub struct ManifestInput<'a> {
137 pub bytes: &'a [u8],
138 pub media_type: &'a str,
139 pub subject: Option<&'a str>,
140 pub attestation_ids: &'a [Uuid],
141 pub profile: ManifestProfile,
142 pub policy_evidence: Option<&'a PolicyEvidence>,
143 pub identity: Option<&'a IdentityEvidence>,
144 pub capability_evidence: Option<&'a CapabilityEvidence>,
145}
146
147pub fn generate_c2pa_manifest(input: ManifestInput<'_>) -> Result<Value> {
149 let digest = digest_bytes(input.bytes);
150 let issued_at = Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true);
151 let attestation_values: Vec<Value> = input
152 .attestation_ids
153 .iter()
154 .map(|id| Value::String(id.to_string()))
155 .collect();
156
157 let mut assertions = Map::new();
158 assertions.insert(
159 "attestation_ids".to_string(),
160 Value::Array(attestation_values),
161 );
162 assertions.insert("issued_at".to_string(), Value::String(issued_at.clone()));
163
164 if let Some(policy) = input.policy_evidence {
165 if !policy.is_empty() {
166 assertions.insert("policy".to_string(), policy_assertion(policy));
167 }
168 }
169
170 if let Some(identity) = input.identity {
171 if !identity.is_empty() {
172 assertions.insert("identity".to_string(), identity_assertion(identity));
173 }
174 }
175
176 if let Some(capabilities) = input.capability_evidence {
177 if !capabilities.is_empty() {
178 assertions.insert(
179 "capability_chain".to_string(),
180 capability_assertion(capabilities),
181 );
182 }
183 }
184
185 let manifest = json!({
186 "version": "1.0",
187 "profile": input.profile.as_str(),
188 "claim_generator": "FleetForge Trust Mesh",
189 "asset": {
190 "media_type": input.media_type,
191 "subject": input.subject.unwrap_or("fleetforge-artifact"),
192 "digest": {
193 "algorithm": "sha256",
194 "value": digest,
195 }
196 },
197 "assertions": assertions,
198 });
199
200 let signer = c2pa_signer().unwrap_or_else(|_| super::trust_signer());
201 let canonical_bytes = canonicalize(&manifest)?;
202 let signature = signer
203 .sign(&canonical_bytes)
204 .context("failed to sign C2PA manifest")?;
205
206 let envelope = C2paManifestEnvelope {
207 manifest,
208 signature,
209 };
210 Ok(serde_json::to_value(envelope)?)
211}
212
213pub fn verify_c2pa_manifest(envelope: &Value, bytes: &[u8]) -> Result<VerifiedManifest> {
215 let envelope: C2paManifestEnvelope = serde_json::from_value(envelope.clone())
216 .map_err(|err| anyhow!("invalid C2PA envelope: {err}"))?;
217
218 let manifest_obj = envelope
219 .manifest
220 .as_object()
221 .ok_or_else(|| anyhow!("manifest must be a JSON object"))?;
222
223 let asset_obj = manifest_obj
224 .get("asset")
225 .and_then(Value::as_object)
226 .ok_or_else(|| anyhow!("manifest missing asset block"))?;
227
228 let digest_obj = asset_obj
229 .get("digest")
230 .and_then(Value::as_object)
231 .ok_or_else(|| anyhow!("manifest asset missing digest block"))?;
232
233 let algorithm = digest_obj
234 .get("algorithm")
235 .and_then(Value::as_str)
236 .ok_or_else(|| anyhow!("manifest digest missing algorithm"))?;
237
238 if algorithm.to_ascii_lowercase() != "sha256" {
239 return Err(anyhow!("unsupported digest algorithm '{}'", algorithm));
240 }
241
242 let expected = digest_obj
243 .get("value")
244 .and_then(Value::as_str)
245 .ok_or_else(|| anyhow!("manifest digest missing value"))?;
246
247 let actual = digest_bytes(bytes);
248
249 if expected != actual {
250 return Err(anyhow!(
251 "digest mismatch: manifest={}, computed={}",
252 expected,
253 actual
254 ));
255 }
256
257 let canonical_bytes = canonicalize(&envelope.manifest)?;
258 let signer_lookup = |kid: &str| {
259 c2pa_signer().ok().and_then(|signer| {
260 if signer.key_id() == kid {
261 Some(signer)
262 } else {
263 None
264 }
265 })
266 };
267 let verified = verify_signature_envelope(&canonical_bytes, &envelope.signature, signer_lookup)
268 .map_err(|err| anyhow!("manifest signature validation failed: {err}"))?;
269 if !verified {
270 return Err(anyhow!("manifest signature verification failed"));
271 }
272
273 let attestation_ids = envelope
274 .manifest
275 .pointer("/assertions/attestation_ids")
276 .and_then(Value::as_array)
277 .ok_or_else(|| anyhow!("manifest missing assertions.attestation_ids array"))?
278 .iter()
279 .map(|value| {
280 value
281 .as_str()
282 .ok_or_else(|| anyhow!("attestation id must be a string"))
283 .and_then(|s| {
284 Uuid::parse_str(s).map_err(|err| anyhow!("invalid attestation id '{s}': {err}"))
285 })
286 })
287 .collect::<Result<Vec<_>>>()?;
288
289 Ok(VerifiedManifest {
290 manifest: envelope.manifest,
291 attestation_ids,
292 artifact_sha256: actual,
293 })
294}
295
296fn policy_assertion(policy: &PolicyEvidence) -> Value {
297 json!({
298 "effect": policy.effect,
299 "boundaries": policy.boundaries,
300 "reasons": policy.reasons,
301 "previews": policy.previews,
302 "decision_attestation_ids": policy
303 .decision_attestation_ids
304 .iter()
305 .map(|id| id.to_string())
306 .collect::<Vec<_>>(),
307 })
308}
309
310fn identity_assertion(identity: &IdentityEvidence) -> Value {
311 let mut map = Map::new();
312 if let Some(run) = identity.run_id {
313 map.insert("run_id".to_string(), Value::String(run.to_string()));
314 }
315 if let Some(step) = identity.step_id {
316 map.insert("step_id".to_string(), Value::String(step.to_string()));
317 }
318 if let Some(subject) = identity.subject_label.as_ref() {
319 map.insert("subject_label".to_string(), Value::String(subject.clone()));
320 }
321 Value::Object(map)
322}
323
324fn capability_assertion(capabilities: &CapabilityEvidence) -> Value {
325 let entries: Vec<Value> = capabilities
326 .chain
327 .iter()
328 .map(|entry| {
329 json!({
330 "token_id": entry.token_id,
331 "issued_at": entry.issued_at.to_rfc3339(),
332 "expires_at": entry.expires_at.to_rfc3339(),
333 "scope": entry.scope,
334 "audience": entry.audience,
335 })
336 })
337 .collect();
338 Value::Array(entries)
339}
340
341fn canonicalize(value: &Value) -> Result<Vec<u8>> {
342 fn recurse(value: &Value) -> Value {
343 match value {
344 Value::Object(map) => {
345 let mut ordered = BTreeMap::new();
346 for (key, val) in map.iter() {
347 ordered.insert(key.clone(), recurse(val));
348 }
349 let mut object = Map::new();
350 for (key, val) in ordered {
351 object.insert(key, val);
352 }
353 Value::Object(object)
354 }
355 Value::Array(items) => Value::Array(items.iter().map(recurse).collect()),
356 other => other.clone(),
357 }
358 }
359
360 let canonical = recurse(value);
361 serde_json::to_vec(&canonical).map_err(|err| anyhow!("failed to canonicalise manifest: {err}"))
362}