Skip to main content

holographic_memory/core/provenance/
mod.rs

1// Copyright 2024-2026 WritersLogic Contributors
2// SPDX-License-Identifier: Apache-2.0
3
4pub mod access;
5pub mod c2pa_manifest;
6pub mod cawg;
7pub mod cose;
8pub mod did;
9pub mod jumbf;
10pub mod keri;
11pub mod scitt;
12pub mod sigstore;
13pub mod trust;
14pub mod types;
15pub mod vc;
16
17use anyhow::Result;
18use ed25519_dalek::SigningKey;
19use parking_lot::RwLock;
20use sha2::{Digest, Sha256};
21use std::io::{BufRead, Write};
22use std::path::{Path, PathBuf};
23use trust::TrustStore;
24
25use types::{
26    DeletionRecord, ProvenanceRecord, StoreManifest, VerificationDetail, VerificationResult,
27};
28
29pub struct TripleProvenanceParams<'a> {
30    pub fact_id: &'a str,
31    pub content: &'a [u8],
32    pub dimensions: u32,
33    pub subject: &'a str,
34    pub relation: &'a str,
35    pub object: &'a str,
36    pub source_uri: Option<&'a str>,
37}
38
39#[derive(serde::Serialize, serde::Deserialize)]
40struct LogEntry {
41    prev_hash: String,
42    fact_id: String,
43    record: ProvenanceRecord,
44}
45
46pub struct ProvenanceManager {
47    signing_key: SigningKey,
48    issuer_did: String,
49    records: RwLock<fxhash::FxHashMap<String, ProvenanceRecord>>,
50    chain_head: RwLock<[u8; 32]>,
51    logical_clock: std::sync::atomic::AtomicU64,
52    log_path: Option<PathBuf>,
53    revocation_bitstring: RwLock<Vec<u8>>,
54    kel: RwLock<keri::KeyEventLog>,
55    #[cfg(feature = "provenance-scitt")]
56    scitt_endpoint: Option<String>,
57}
58
59impl ProvenanceManager {
60    pub fn new(key_path: &Path, storage_path: Option<&Path>) -> Result<Self> {
61        let signing_key = super::security::SigningManager::new(key_path)?.into_signing_key();
62        let issuer_did = did::did_key_from_ed25519(&signing_key.verifying_key().to_bytes());
63        let log_path = storage_path.map(|p| p.join("provenance_log.jsonl"));
64        let (records, chain_head) = Self::load_log(log_path.as_deref());
65        let clock_init = records
66            .values()
67            .map(|r| r.sequence)
68            .max()
69            .map_or(0, |max| max + 1);
70        let mut kel = match storage_path {
71            Some(p) => keri::KeyEventLog::with_path(&p.join("kel.json")).unwrap_or_default(),
72            None => keri::KeyEventLog::new(),
73        };
74        if kel.events().is_empty() {
75            if let Err(e) = kel.inception(&signing_key, None) {
76                tracing::warn!("KEL inception failed: {e}");
77            }
78        }
79
80        Ok(Self {
81            signing_key,
82            issuer_did,
83            records: RwLock::new(records),
84            chain_head: RwLock::new(chain_head),
85            logical_clock: std::sync::atomic::AtomicU64::new(clock_init),
86            log_path: log_path.clone(),
87            revocation_bitstring: RwLock::new(Self::load_revocation_bitstring(log_path.as_deref())),
88            kel: RwLock::new(kel),
89            #[cfg(feature = "provenance-scitt")]
90            scitt_endpoint: None,
91        })
92    }
93
94    pub fn from_signing_key(signing_key: SigningKey) -> Self {
95        let issuer_did = did::did_key_from_ed25519(&signing_key.verifying_key().to_bytes());
96        let mut kel = keri::KeyEventLog::new();
97        let _ = kel.inception(&signing_key, None);
98        Self {
99            signing_key,
100            issuer_did,
101            records: RwLock::new(fxhash::FxHashMap::default()),
102            chain_head: RwLock::new([0u8; 32]),
103            logical_clock: std::sync::atomic::AtomicU64::new(0),
104            log_path: None,
105            revocation_bitstring: RwLock::new(Vec::new()),
106            kel: RwLock::new(kel),
107            #[cfg(feature = "provenance-scitt")]
108            scitt_endpoint: None,
109        }
110    }
111
112    fn next_sequence(&self) -> u64 {
113        self.logical_clock
114            .fetch_add(1, std::sync::atomic::Ordering::SeqCst)
115    }
116
117    #[cfg(feature = "provenance-scitt")]
118    pub fn with_scitt_endpoint(mut self, endpoint: String) -> Self {
119        self.scitt_endpoint = Some(endpoint);
120        self
121    }
122
123    pub fn issuer_did(&self) -> &str {
124        &self.issuer_did
125    }
126
127    pub fn store_record(&self, fact_id: &str, record: ProvenanceRecord) {
128        if let Some(ref path) = self.log_path {
129            let prev_hash = hex_encode(&*self.chain_head.read());
130            let entry = LogEntry {
131                prev_hash,
132                fact_id: fact_id.to_string(),
133                record: record.clone(),
134            };
135            if let Ok(line) = serde_json::to_string(&entry) {
136                let new_hash: [u8; 32] = Sha256::digest(line.as_bytes()).into();
137                let mut buf = line;
138                buf.push('\n');
139                if let Err(e) = std::fs::OpenOptions::new()
140                    .create(true)
141                    .append(true)
142                    .open(path)
143                    .and_then(|mut f| f.write_all(buf.as_bytes()))
144                {
145                    tracing::warn!("provenance log write failed: {e}");
146                }
147                *self.chain_head.write() = new_hash;
148                self.write_signed_head(&new_hash, path);
149            }
150        }
151        self.records.write().insert(fact_id.to_string(), record);
152    }
153
154    fn write_signed_head(&self, head: &[u8; 32], log_path: &Path) {
155        let head_path = log_path.with_extension("head");
156        match cose::sign_payload(&self.signing_key, head) {
157            Ok(signed) => {
158                if let Err(e) = std::fs::write(head_path, signed) {
159                    tracing::warn!("signed head write failed: {e}");
160                }
161            }
162            Err(e) => tracing::warn!("signed head signing failed: {e}"),
163        }
164    }
165
166    pub fn get_record(&self, fact_id: &str) -> Option<ProvenanceRecord> {
167        self.records.read().get(fact_id).cloned()
168    }
169
170    pub fn record_count(&self) -> usize {
171        self.records.read().len()
172    }
173
174    pub fn record_deletion(&self, fact_id: &str, reason: Option<&str>) -> Result<DeletionRecord> {
175        let timestamp_ms = std::time::SystemTime::now()
176            .duration_since(std::time::UNIX_EPOCH)
177            .unwrap_or_default()
178            .as_millis() as u64;
179
180        let mut claim_payload = Vec::new();
181        let cbor_claim = ciborium::Value::Map(vec![
182            (
183                ciborium::Value::Text("action".to_string()),
184                ciborium::Value::Text("delete".to_string()),
185            ),
186            (
187                ciborium::Value::Text("fact_id".to_string()),
188                ciborium::Value::Text(fact_id.to_string()),
189            ),
190            (
191                ciborium::Value::Text("timestamp_ms".to_string()),
192                ciborium::Value::Integer(timestamp_ms.into()),
193            ),
194        ]);
195        ciborium::into_writer(&cbor_claim, &mut claim_payload)
196            .map_err(|e| anyhow::anyhow!("CBOR encoding failed: {e}"))?;
197
198        let cose_envelope = cose::sign_payload(&self.signing_key, &claim_payload)?;
199
200        let record = DeletionRecord {
201            fact_id: fact_id.to_string(),
202            deleted_by: Some(self.issuer_did.clone()),
203            timestamp_ms,
204            reason: reason.map(String::from),
205            cose_envelope: Some(cose_envelope),
206        };
207
208        self.records.write().remove(fact_id);
209
210        if let Some(ref path) = self.log_path {
211            let prev_hash = hex_encode(&*self.chain_head.read());
212            let entry_json = serde_json::json!({
213                "prev_hash": prev_hash,
214                "action": "delete",
215                "fact_id": fact_id,
216                "deletion": record,
217            });
218            if let Ok(line) = serde_json::to_string(&entry_json) {
219                let new_hash: [u8; 32] = Sha256::digest(line.as_bytes()).into();
220                let mut buf = line;
221                buf.push('\n');
222                if let Err(e) = std::fs::OpenOptions::new()
223                    .create(true)
224                    .append(true)
225                    .open(path)
226                    .and_then(|mut f| f.write_all(buf.as_bytes()))
227                {
228                    tracing::warn!("deletion log write failed: {e}");
229                }
230                *self.chain_head.write() = new_hash;
231                self.write_signed_head(&new_hash, path);
232            }
233        }
234
235        Ok(record)
236    }
237
238    pub fn rotate_key(&mut self, new_key_path: &Path) -> Result<String> {
239        let old_did = self.issuer_did.clone();
240        let new_signing_key =
241            super::security::SigningManager::new(new_key_path)?.into_signing_key();
242        let new_did = did::did_key_from_ed25519(&new_signing_key.verifying_key().to_bytes());
243
244        let mut rotation_payload = Vec::new();
245        let cbor = ciborium::Value::Map(vec![
246            (
247                ciborium::Value::Text("action".to_string()),
248                ciborium::Value::Text("key_rotation".to_string()),
249            ),
250            (
251                ciborium::Value::Text("old_did".to_string()),
252                ciborium::Value::Text(old_did.clone()),
253            ),
254            (
255                ciborium::Value::Text("new_did".to_string()),
256                ciborium::Value::Text(new_did.clone()),
257            ),
258        ]);
259        ciborium::into_writer(&cbor, &mut rotation_payload)
260            .map_err(|e| anyhow::anyhow!("CBOR encoding failed: {e}"))?;
261        let rotation_envelope = cose::sign_payload(&self.signing_key, &rotation_payload)?;
262
263        if let Some(ref path) = self.log_path {
264            let prev_hash = hex_encode(&*self.chain_head.read());
265            let entry_json = serde_json::json!({
266                "prev_hash": prev_hash,
267                "action": "key_rotation",
268                "old_did": old_did,
269                "new_did": new_did,
270                "cose_envelope": rotation_envelope,
271            });
272            if let Ok(line) = serde_json::to_string(&entry_json) {
273                let new_hash: [u8; 32] = Sha256::digest(line.as_bytes()).into();
274                let mut buf = line;
275                buf.push('\n');
276                if let Err(e) = std::fs::OpenOptions::new()
277                    .create(true)
278                    .append(true)
279                    .open(path)
280                    .and_then(|mut f| f.write_all(buf.as_bytes()))
281                {
282                    tracing::warn!("key rotation log write failed: {e}");
283                }
284                *self.chain_head.write() = new_hash;
285                self.write_signed_head(&new_hash, path);
286            }
287        }
288
289        self.signing_key = new_signing_key;
290        self.issuer_did = new_did.clone();
291        Ok(new_did)
292    }
293
294    pub fn verify_log_integrity(&self) -> Result<bool> {
295        let Some(ref path) = self.log_path else {
296            return Ok(true);
297        };
298        let Ok(file) = std::fs::File::open(path) else {
299            return Ok(true);
300        };
301        let mut expected_prev = [0u8; 32];
302        let mut computed_head = [0u8; 32];
303        let mut has_entries = false;
304        for line in std::io::BufReader::new(file).lines() {
305            let line = line.map_err(|e| anyhow::anyhow!("log read error: {e}"))?;
306            let obj: serde_json::Value =
307                serde_json::from_str(&line).map_err(|e| anyhow::anyhow!("log parse error: {e}"))?;
308            let prev = obj
309                .get("prev_hash")
310                .and_then(|v| v.as_str())
311                .ok_or_else(|| anyhow::anyhow!("missing prev_hash"))?;
312            let declared = hex_decode(prev)?;
313            if declared != expected_prev {
314                return Ok(false);
315            }
316            computed_head = Sha256::digest(line.as_bytes()).into();
317            expected_prev = computed_head;
318            has_entries = true;
319        }
320        if has_entries {
321            let head_path = path.with_extension("head");
322            if let Ok(signed_bytes) = std::fs::read(&head_path) {
323                let verifying_key = self.signing_key.verifying_key();
324                match cose::verify_and_extract(&verifying_key, &signed_bytes) {
325                    Ok(stored_head) => {
326                        if stored_head.as_slice() != computed_head {
327                            return Ok(false);
328                        }
329                    }
330                    Err(_) => return Ok(false),
331                }
332            }
333        }
334        Ok(true)
335    }
336
337    fn load_log(path: Option<&Path>) -> (fxhash::FxHashMap<String, ProvenanceRecord>, [u8; 32]) {
338        let mut map = fxhash::FxHashMap::default();
339        let mut head = [0u8; 32];
340        let Some(path) = path else {
341            return (map, head);
342        };
343        let Ok(file) = std::fs::File::open(path) else {
344            return (map, head);
345        };
346        for line in std::io::BufReader::new(file).lines() {
347            let Ok(line) = line else { continue };
348            head = Sha256::digest(line.as_bytes()).into();
349            if let Ok(entry) = serde_json::from_str::<LogEntry>(&line) {
350                map.insert(entry.fact_id, entry.record);
351            } else if let Ok(obj) = serde_json::from_str::<serde_json::Value>(&line) {
352                if obj.get("action").and_then(|a| a.as_str()) == Some("delete") {
353                    if let Some(fid) = obj.get("fact_id").and_then(|f| f.as_str()) {
354                        map.remove(fid);
355                    }
356                }
357            }
358        }
359        (map, head)
360    }
361
362    fn load_revocation_bitstring(log_path: Option<&Path>) -> Vec<u8> {
363        let Some(path) = log_path else {
364            return Vec::new();
365        };
366        let bitstring_path = path.with_extension("statuslist");
367        std::fs::read(bitstring_path).unwrap_or_default()
368    }
369
370    pub fn revoke_credential(&self, status_index: u64) -> Result<()> {
371        let byte_idx = (status_index / 8) as usize;
372        let bit_idx = (status_index % 8) as u8;
373        {
374            let mut bits = self.revocation_bitstring.write();
375            if bits.len() <= byte_idx {
376                bits.resize(byte_idx + 1, 0);
377            }
378            bits[byte_idx] |= 1 << bit_idx;
379        }
380        if let Some(ref path) = self.log_path {
381            let bitstring_path = path.with_extension("statuslist");
382            let bits = self.revocation_bitstring.read();
383            std::fs::write(bitstring_path, &*bits)
384                .map_err(|e| anyhow::anyhow!("failed to persist revocation bitstring: {e}"))?;
385        }
386        Ok(())
387    }
388
389    pub fn is_revoked(&self, status_index: u64) -> bool {
390        let byte_idx = (status_index / 8) as usize;
391        let bit_idx = (status_index % 8) as u8;
392        let bits = self.revocation_bitstring.read();
393        byte_idx < bits.len() && (bits[byte_idx] & (1 << bit_idx)) != 0
394    }
395
396    /// Create a full provenance record for a stored fact.
397    pub fn create_fact_provenance(
398        &self,
399        fact_id: &str,
400        content: &[u8],
401        source_uri: Option<&str>,
402    ) -> Result<ProvenanceRecord> {
403        let content_hash: [u8; 32] = Sha256::digest(content).into();
404        let mut record = ProvenanceRecord::now().with_source(source_uri.unwrap_or(""), content);
405        record.issuer_did = Some(self.issuer_did.clone());
406        record.sequence = self.next_sequence();
407
408        let cose_envelope = cose::sign_fact_claim(
409            &self.signing_key,
410            fact_id,
411            &content_hash,
412            source_uri,
413            record.timestamp_ms,
414        )?;
415        record.cose_envelope = Some(cose_envelope);
416
417        let credential = vc::create_fact_credential(
418            &self.issuer_did,
419            fact_id,
420            &content_hash,
421            0, // dimensions filled by caller
422            source_uri,
423            None,
424            record.sequence,
425        );
426        let signed_vc = vc::sign_credential(&self.signing_key, credential)?;
427        record.vc_json = Some(serde_json::to_string(&signed_vc)?);
428
429        #[cfg(feature = "provenance-scitt")]
430        if let Some(ref endpoint) = self.scitt_endpoint {
431            let claim = scitt::FactClaim {
432                fact_id: fact_id.to_string(),
433                content_hash: content_hash.to_vec(),
434                timestamp_ms: record.timestamp_ms,
435                source_uri: source_uri.map(String::from),
436                store_id: None,
437            };
438            let statement = scitt::create_signed_statement(&self.signing_key, &claim)?;
439            match scitt::submit_statement(endpoint, &statement) {
440                Ok(receipt) => record.scitt_receipt = Some(receipt.receipt_bytes),
441                Err(e) => tracing::warn!("SCITT submission failed: {e}"),
442            }
443        }
444
445        self.store_record(fact_id, record.clone());
446        Ok(record)
447    }
448
449    /// Create a full provenance record for a stored triple.
450    pub fn create_triple_provenance(
451        &self,
452        params: &TripleProvenanceParams<'_>,
453    ) -> Result<ProvenanceRecord> {
454        let content_hash: [u8; 32] = Sha256::digest(params.content).into();
455        let mut record = ProvenanceRecord::now();
456        record.source_uri = params.source_uri.map(String::from);
457        record.source_hash = Some(content_hash);
458        record.issuer_did = Some(self.issuer_did.clone());
459        record.sequence = self.next_sequence();
460
461        let cose_envelope = cose::sign_fact_claim(
462            &self.signing_key,
463            params.fact_id,
464            &content_hash,
465            params.source_uri,
466            record.timestamp_ms,
467        )?;
468        record.cose_envelope = Some(cose_envelope);
469
470        let credential = vc::create_fact_credential(
471            &self.issuer_did,
472            params.fact_id,
473            &content_hash,
474            params.dimensions,
475            params.source_uri,
476            Some((params.subject, params.relation, params.object)),
477            record.sequence,
478        );
479        let signed_vc = vc::sign_credential(&self.signing_key, credential)?;
480        record.vc_json = Some(serde_json::to_string(&signed_vc)?);
481
482        self.store_record(params.fact_id, record.clone());
483        Ok(record)
484    }
485
486    pub fn create_batch_provenance(
487        &self,
488        items: &[(&str, &[u8], Option<&str>)],
489    ) -> Result<Vec<ProvenanceRecord>> {
490        if items.is_empty() {
491            return Ok(Vec::new());
492        }
493        let leaf_hashes: Vec<[u8; 32]> = items
494            .iter()
495            .map(|(_, content, _)| Sha256::digest(content).into())
496            .collect();
497        let merkle_root = Self::merkle_root(&leaf_hashes);
498        let root_envelope = cose::sign_payload(&self.signing_key, &merkle_root)?;
499
500        let mut records = Vec::with_capacity(items.len());
501        for (i, (fact_id, content, source_uri)) in items.iter().enumerate() {
502            let content_hash = leaf_hashes[i];
503            let proof = Self::merkle_proof(&leaf_hashes, i);
504            let mut record = ProvenanceRecord::now().with_source(source_uri.unwrap_or(""), content);
505            record.issuer_did = Some(self.issuer_did.clone());
506            record.sequence = self.next_sequence();
507            record.cose_envelope = Some(root_envelope.clone());
508            record.merkle_proof = Some(proof);
509
510            let credential = vc::create_fact_credential(
511                &self.issuer_did,
512                fact_id,
513                &content_hash,
514                0,
515                *source_uri,
516                None,
517                record.sequence,
518            );
519            let signed_vc = vc::sign_credential(&self.signing_key, credential)?;
520            record.vc_json = Some(serde_json::to_string(&signed_vc)?);
521
522            self.store_record(fact_id, record.clone());
523            records.push(record);
524        }
525        Ok(records)
526    }
527
528    fn merkle_root(leaves: &[[u8; 32]]) -> [u8; 32] {
529        if leaves.is_empty() {
530            return [0u8; 32];
531        }
532        let mut level: Vec<[u8; 32]> = leaves.to_vec();
533        while level.len() > 1 {
534            let mut next = Vec::with_capacity(level.len().div_ceil(2));
535            for pair in level.chunks(2) {
536                if pair.len() == 2 {
537                    let mut hasher = Sha256::new();
538                    hasher.update(pair[0]);
539                    hasher.update(pair[1]);
540                    next.push(hasher.finalize().into());
541                } else {
542                    next.push(pair[0]);
543                }
544            }
545            level = next;
546        }
547        level[0]
548    }
549
550    fn merkle_proof(leaves: &[[u8; 32]], index: usize) -> Vec<(bool, [u8; 32])> {
551        let mut proof = Vec::new();
552        let mut level: Vec<[u8; 32]> = leaves.to_vec();
553        let mut idx = index;
554        while level.len() > 1 {
555            let sibling_idx = if idx.is_multiple_of(2) {
556                idx + 1
557            } else {
558                idx - 1
559            };
560            if sibling_idx < level.len() {
561                proof.push((idx % 2 == 1, level[sibling_idx]));
562            }
563            let mut next = Vec::with_capacity(level.len().div_ceil(2));
564            for pair in level.chunks(2) {
565                if pair.len() == 2 {
566                    let mut hasher = Sha256::new();
567                    hasher.update(pair[0]);
568                    hasher.update(pair[1]);
569                    next.push(hasher.finalize().into());
570                } else {
571                    next.push(pair[0]);
572                }
573            }
574            level = next;
575            idx /= 2;
576        }
577        proof
578    }
579
580    /// Create a signed manifest for the entire store.
581    pub fn create_store_manifest(
582        &self,
583        store_id: &str,
584        store_data: &[u8],
585        fact_count: usize,
586        dimensions: u32,
587        title: Option<&str>,
588    ) -> Result<StoreManifest> {
589        let store_hash: [u8; 32] = Sha256::digest(store_data).into();
590
591        let params = c2pa_manifest::ManifestParams {
592            store_id,
593            fact_count,
594            dimensions,
595            store_hash: &store_hash,
596            title,
597            ingredients: Vec::new(),
598        };
599
600        let manifest = c2pa_manifest::create_manifest(&self.signing_key, &params)?;
601        let cose_envelope = c2pa_manifest::sign_manifest(&self.signing_key, &manifest)?;
602
603        let manifest_label = format!("urn:hms:manifest:{store_id}");
604        let claim_json = serde_json::to_value(&manifest)?;
605        let assertions: Vec<(&str, serde_json::Value)> = manifest
606            .assertions
607            .iter()
608            .map(|a| (a.label.as_str(), a.data.clone()))
609            .collect();
610        let jumbf_box = jumbf::build_c2pa_manifest_box(
611            &manifest_label,
612            claim_json,
613            cose_envelope.clone(),
614            assertions,
615        );
616        let jumbf_bytes = jumbf_box.encode()?;
617
618        Ok(StoreManifest {
619            manifest_id: manifest_label,
620            issuer_did: Some(self.issuer_did.clone()),
621            created_ms: std::time::SystemTime::now()
622                .duration_since(std::time::UNIX_EPOCH)
623                .unwrap_or_default()
624                .as_millis() as u64,
625            fact_count,
626            store_hash,
627            cose_envelope: Some(cose_envelope),
628            jumbf_manifest: Some(jumbf_bytes),
629        })
630    }
631
632    fn resolve_cose_verifying_key(
633        record: &ProvenanceRecord,
634        fallback: &ed25519_dalek::VerifyingKey,
635    ) -> ed25519_dalek::VerifyingKey {
636        if let Some(ref envelope) = record.cose_envelope {
637            if let Ok(kid) = cose::extract_key_id(envelope) {
638                if kid.len() == 32 {
639                    if let Ok(key) = ed25519_dalek::VerifyingKey::from_bytes(
640                        kid.as_slice().try_into().unwrap_or(&[0u8; 32]),
641                    ) {
642                        return key;
643                    }
644                }
645            }
646        }
647        if let Some(ref issuer_did) = record.issuer_did {
648            if let Ok(pk_bytes) = did::ed25519_from_did_key(issuer_did) {
649                if let Ok(key) = ed25519_dalek::VerifyingKey::from_bytes(&pk_bytes) {
650                    return key;
651                }
652            }
653        }
654        *fallback
655    }
656
657    /// Authenticity check: verifies the record's signatures **and** requires the
658    /// key that produced them to be present in `trust`. A record whose embedded
659    /// key is not trusted verifies as `valid == false` even if its signature is
660    /// internally consistent. Use [`ProvenanceManager::self_trust`] to trust
661    /// only this manager's own key.
662    pub fn verify_fact_provenance(
663        &self,
664        record: &ProvenanceRecord,
665        trust: &TrustStore,
666    ) -> Result<VerificationResult> {
667        let mut result = self.verify_fact_provenance_self_consistency(record)?;
668        let fallback = self.signing_key.verifying_key();
669        let key = Self::resolve_cose_verifying_key(record, &fallback);
670        let trusted = trust.is_trusted(&key);
671        result.details.push(VerificationDetail {
672            check: "trust_anchor".to_string(),
673            passed: trusted,
674            message: if trusted {
675                None
676            } else {
677                Some("signing key is not in the trust store".to_string())
678            },
679        });
680        result.valid = result.details.iter().all(|d| d.passed) && !result.details.is_empty();
681        Ok(result)
682    }
683
684    /// Integrity-only check: verifies the record's signatures against the key
685    /// **embedded in the record itself**. This proves the record has not been
686    /// altered since signing, but says nothing about *who* signed it — a record
687    /// signed by any key passes. For authenticity use
688    /// [`ProvenanceManager::verify_fact_provenance`] with a trust anchor.
689    pub fn verify_fact_provenance_self_consistency(
690        &self,
691        record: &ProvenanceRecord,
692    ) -> Result<VerificationResult> {
693        let mut details = Vec::new();
694        let fallback = self.signing_key.verifying_key();
695        let verifying_key = Self::resolve_cose_verifying_key(record, &fallback);
696
697        if let Some(ref envelope) = record.cose_envelope {
698            match cose::verify_and_extract(&verifying_key, envelope) {
699                Ok(_) => details.push(VerificationDetail {
700                    check: "cose_signature".to_string(),
701                    passed: true,
702                    message: None,
703                }),
704                Err(e) => details.push(VerificationDetail {
705                    check: "cose_signature".to_string(),
706                    passed: false,
707                    message: Some(e.to_string()),
708                }),
709            }
710        }
711
712        if let Some(ref vc_json) = record.vc_json {
713            match serde_json::from_str::<vc::FactCredential>(vc_json) {
714                Ok(ref credential) => {
715                    match vc::verify_credential(credential) {
716                        Ok(()) => details.push(VerificationDetail {
717                            check: "vc_proof".to_string(),
718                            passed: true,
719                            message: None,
720                        }),
721                        Err(e) => details.push(VerificationDetail {
722                            check: "vc_proof".to_string(),
723                            passed: false,
724                            message: Some(e.to_string()),
725                        }),
726                    }
727                    let revoked = credential
728                        .credential_status
729                        .as_ref()
730                        .and_then(|s| s.status_list_index.parse::<u64>().ok())
731                        .is_some_and(|idx| self.is_revoked(idx));
732                    details.push(VerificationDetail {
733                        check: "revocation_status".to_string(),
734                        passed: !revoked,
735                        message: if revoked {
736                            Some("credential has been revoked".to_string())
737                        } else {
738                            None
739                        },
740                    });
741                }
742                Err(e) => details.push(VerificationDetail {
743                    check: "vc_parse".to_string(),
744                    passed: false,
745                    message: Some(e.to_string()),
746                }),
747            }
748        }
749
750        let all_passed = details.iter().all(|d| d.passed);
751
752        Ok(VerificationResult {
753            valid: all_passed && !details.is_empty(),
754            issuer_did: record.issuer_did.clone(),
755            timestamp_ms: record.timestamp_ms,
756            details,
757        })
758    }
759
760    pub fn signing_key(&self) -> &SigningKey {
761        &self.signing_key
762    }
763
764    /// This manager's current verifying (public) key.
765    pub fn verifying_key(&self) -> ed25519_dalek::VerifyingKey {
766        self.signing_key.verifying_key()
767    }
768
769    /// A trust store containing only this manager's own key. Pass it to the
770    /// verify methods to authenticate records this manager signed.
771    pub fn self_trust(&self) -> TrustStore {
772        TrustStore::trusting_key(&self.signing_key.verifying_key())
773    }
774
775    pub fn create_sigstore_bundle(
776        &self,
777        content: &[u8],
778        identity: Option<&str>,
779    ) -> Result<sigstore::SigstoreBundle> {
780        sigstore::create_local_bundle(&self.signing_key, content, identity)
781    }
782
783    /// Verify a Sigstore bundle against a caller-supplied trusted key. The key
784    /// embedded in the bundle is ignored: authenticity comes from `trusted_key`,
785    /// not from the bundle itself.
786    pub fn verify_sigstore_bundle(
787        &self,
788        bundle: &sigstore::SigstoreBundle,
789        content: &[u8],
790        trusted_key: &ed25519_dalek::VerifyingKey,
791    ) -> Result<()> {
792        sigstore::verify_bundle_with_key(bundle, content, trusted_key)
793    }
794
795    /// Create a CAWG ICA identity assertion. `referenced` is `(url, alg, raw_hash)` and
796    /// MUST include the hard binding. Returns the `cawg.identity` assertion JSON.
797    pub fn create_cawg_assertion(
798        &self,
799        referenced: &[(String, String, Vec<u8>)],
800        display_name: &str,
801    ) -> Result<serde_json::Value> {
802        cawg::create_identity_assertion_ica(&self.signing_key, referenced, display_name)
803    }
804
805    /// Verify a `cawg.identity` ICA assertion JSON and require its issuer to be
806    /// trusted, returning the embedded VC. `verify_cawg_ica` only proves the
807    /// assertion is self-consistent (signed by the key its own VC names); the
808    /// `trust` anchor establishes that the issuing agent is one the caller
809    /// accepts. Rejects an assertion whose issuer DID is not in `trust`.
810    pub fn verify_cawg_assertion(
811        &self,
812        assertion: &serde_json::Value,
813        trust: &TrustStore,
814    ) -> Result<serde_json::Value> {
815        let embedded = cawg::ica_embedded_bytes(assertion)?;
816        let vc = cawg::verify_cawg_ica(&embedded)?;
817        let issuer = vc
818            .get("issuer")
819            .and_then(|v| v.as_str())
820            .ok_or_else(|| anyhow::anyhow!("CAWG assertion VC has no issuer"))?;
821        let key_bytes = did::ed25519_from_did_jwk(issuer)?;
822        let key = ed25519_dalek::VerifyingKey::from_bytes(&key_bytes)
823            .map_err(|e| anyhow::anyhow!("invalid CAWG issuer key: {e}"))?;
824        if !trust.is_trusted(&key) {
825            return Err(anyhow::anyhow!("CAWG issuer is not trusted: {issuer}"));
826        }
827        Ok(vc)
828    }
829
830    pub fn keri_event_log(&self) -> &RwLock<keri::KeyEventLog> {
831        &self.kel
832    }
833
834    pub fn create_keri_interaction(
835        &self,
836        anchors: Vec<serde_json::Value>,
837    ) -> Result<keri::KeyEvent> {
838        self.kel
839            .write()
840            .interaction(&self.signing_key, anchors)
841            .cloned()
842    }
843
844    pub fn verify_keri_log(&self) -> Result<()> {
845        self.kel.read().verify()
846    }
847
848    /// Verify a store manifest's COSE envelope and optionally check the store hash.
849    fn resolve_manifest_verifying_key(
850        manifest: &StoreManifest,
851        fallback: &ed25519_dalek::VerifyingKey,
852    ) -> ed25519_dalek::VerifyingKey {
853        if let Some(ref envelope) = manifest.cose_envelope {
854            if let Ok(kid) = cose::extract_key_id(envelope) {
855                if let Ok(key) = ed25519_dalek::VerifyingKey::from_bytes(
856                    kid.as_slice().try_into().unwrap_or(&[0u8; 32]),
857                ) {
858                    return key;
859                }
860            }
861        }
862        if let Some(ref issuer_did) = manifest.issuer_did {
863            if let Ok(pk_bytes) = did::ed25519_from_did_key(issuer_did) {
864                if let Ok(key) = ed25519_dalek::VerifyingKey::from_bytes(&pk_bytes) {
865                    return key;
866                }
867            }
868        }
869        *fallback
870    }
871
872    /// Authenticity check for a store manifest: verifies its COSE signature and
873    /// optional store hash, and requires the signing key to be present in
874    /// `trust`. See [`ProvenanceManager::verify_fact_provenance`] for the
875    /// self-consistency vs. authenticity distinction.
876    pub fn verify_store_manifest(
877        &self,
878        manifest: &StoreManifest,
879        store_data: Option<&[u8]>,
880        trust: &TrustStore,
881    ) -> Result<VerificationResult> {
882        let mut result = self.verify_store_manifest_self_consistency(manifest, store_data)?;
883        let fallback = self.signing_key.verifying_key();
884        let key = Self::resolve_manifest_verifying_key(manifest, &fallback);
885        let trusted = trust.is_trusted(&key);
886        result.details.push(VerificationDetail {
887            check: "trust_anchor".to_string(),
888            passed: trusted,
889            message: if trusted {
890                None
891            } else {
892                Some("signing key is not in the trust store".to_string())
893            },
894        });
895        result.valid = result.details.iter().all(|d| d.passed) && !result.details.is_empty();
896        Ok(result)
897    }
898
899    /// Integrity-only check for a store manifest: verifies signatures against
900    /// the key embedded in the manifest. Does not establish authenticity.
901    pub fn verify_store_manifest_self_consistency(
902        &self,
903        manifest: &StoreManifest,
904        store_data: Option<&[u8]>,
905    ) -> Result<VerificationResult> {
906        let mut details = Vec::new();
907        let fallback = self.signing_key.verifying_key();
908        let verifying_key = Self::resolve_manifest_verifying_key(manifest, &fallback);
909
910        if let Some(ref envelope) = manifest.cose_envelope {
911            match cose::verify_and_extract(&verifying_key, envelope) {
912                Ok(payload) => {
913                    details.push(VerificationDetail {
914                        check: "manifest_cose_signature".to_string(),
915                        passed: true,
916                        message: None,
917                    });
918
919                    if let Ok(hms_manifest) =
920                        serde_json::from_slice::<c2pa_manifest::HmsManifest>(&payload)
921                    {
922                        if let Some(data) = store_data {
923                            let hash_ok = c2pa_manifest::validate_store_hash(&hms_manifest, data)
924                                .unwrap_or(false);
925                            details.push(VerificationDetail {
926                                check: "store_hash".to_string(),
927                                passed: hash_ok,
928                                message: if hash_ok {
929                                    None
930                                } else {
931                                    Some("store hash mismatch".to_string())
932                                },
933                            });
934                        }
935                    }
936                }
937                Err(e) => details.push(VerificationDetail {
938                    check: "manifest_cose_signature".to_string(),
939                    passed: false,
940                    message: Some(e.to_string()),
941                }),
942            }
943        }
944
945        let all_passed = details.iter().all(|d| d.passed);
946
947        Ok(VerificationResult {
948            valid: all_passed && !details.is_empty(),
949            issuer_did: manifest.issuer_did.clone(),
950            timestamp_ms: manifest.created_ms,
951            details,
952        })
953    }
954}
955
956/// Verify a provenance record without a ProvenanceManager instance.
957///
958/// Verifies the record's signatures and requires its signing identity —
959/// resolved from the COSE key_id, or the record's issuer DID for a vc-only
960/// record — to be present in `trust`. As with
961/// [`ProvenanceManager::verify_fact_provenance`], a record whose signing key is
962/// not trusted verifies as `valid == false`.
963pub fn verify_record(record: &ProvenanceRecord, trust: &TrustStore) -> Result<VerificationResult> {
964    let mut details = Vec::new();
965
966    if let Some(ref envelope) = record.cose_envelope {
967        let kid = cose::extract_key_id(envelope)?;
968        let pk: [u8; 32] = kid
969            .as_slice()
970            .try_into()
971            .map_err(|_| anyhow::anyhow!("COSE key_id is not 32 bytes"))?;
972        let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(&pk)
973            .map_err(|e| anyhow::anyhow!("invalid public key in COSE: {e}"))?;
974        match cose::verify_and_extract(&verifying_key, envelope) {
975            Ok(_) => details.push(VerificationDetail {
976                check: "cose_signature".to_string(),
977                passed: true,
978                message: None,
979            }),
980            Err(e) => details.push(VerificationDetail {
981                check: "cose_signature".to_string(),
982                passed: false,
983                message: Some(e.to_string()),
984            }),
985        }
986    }
987
988    if let Some(ref vc_json) = record.vc_json {
989        match serde_json::from_str::<vc::FactCredential>(vc_json) {
990            Ok(ref credential) => match vc::verify_credential(credential) {
991                Ok(()) => details.push(VerificationDetail {
992                    check: "vc_proof".to_string(),
993                    passed: true,
994                    message: None,
995                }),
996                Err(e) => details.push(VerificationDetail {
997                    check: "vc_proof".to_string(),
998                    passed: false,
999                    message: Some(e.to_string()),
1000                }),
1001            },
1002            Err(e) => details.push(VerificationDetail {
1003                check: "vc_parse".to_string(),
1004                passed: false,
1005                message: Some(e.to_string()),
1006            }),
1007        }
1008    }
1009
1010    // Trust anchoring applies to the whole record, not just its COSE envelope:
1011    // resolve the signing identity from the COSE key_id, falling back to the
1012    // record's issuer DID (so a vc-only record is anchored too), and require it
1013    // to be trusted. Without the fallback, a record carrying only a self-signed
1014    // VC would bypass the trust check entirely.
1015    if !details.is_empty() {
1016        let signer_key = record
1017            .cose_envelope
1018            .as_ref()
1019            .and_then(|e| cose::extract_key_id(e).ok())
1020            .and_then(|kid| <[u8; 32]>::try_from(kid.as_slice()).ok())
1021            .or_else(|| {
1022                record
1023                    .issuer_did
1024                    .as_deref()
1025                    .and_then(|d| did::ed25519_from_did_key(d).ok())
1026            })
1027            .and_then(|pk| ed25519_dalek::VerifyingKey::from_bytes(&pk).ok());
1028        let trusted = signer_key.as_ref().is_some_and(|k| trust.is_trusted(k));
1029        details.push(VerificationDetail {
1030            check: "trust_anchor".to_string(),
1031            passed: trusted,
1032            message: if trusted {
1033                None
1034            } else {
1035                Some("signing key is not in the trust store".to_string())
1036            },
1037        });
1038    }
1039
1040    let all_passed = details.iter().all(|d| d.passed);
1041    Ok(VerificationResult {
1042        valid: all_passed && !details.is_empty(),
1043        issuer_did: record.issuer_did.clone(),
1044        timestamp_ms: record.timestamp_ms,
1045        details,
1046    })
1047}
1048
1049fn hex_encode(bytes: &[u8]) -> String {
1050    bytes.iter().map(|b| format!("{b:02x}")).collect()
1051}
1052
1053fn hex_decode(hex: &str) -> Result<[u8; 32]> {
1054    if hex.len() != 64 {
1055        return Err(anyhow::anyhow!("invalid hex hash length"));
1056    }
1057    let mut out = [0u8; 32];
1058    for (i, chunk) in hex.as_bytes().chunks(2).enumerate() {
1059        out[i] = u8::from_str_radix(
1060            std::str::from_utf8(chunk).map_err(|e| anyhow::anyhow!("hex decode: {e}"))?,
1061            16,
1062        )
1063        .map_err(|e| anyhow::anyhow!("hex decode: {e}"))?;
1064    }
1065    Ok(out)
1066}
1067
1068#[cfg(test)]
1069mod tests {
1070    use super::*;
1071    use tempfile::tempdir;
1072
1073    #[test]
1074    fn full_fact_provenance_flow() {
1075        let dir = tempdir().unwrap();
1076        let mgr = ProvenanceManager::new(&dir.path().join("test.key"), Some(dir.path())).unwrap();
1077
1078        let record = mgr
1079            .create_fact_provenance(
1080                "fact-001",
1081                b"Paris is the capital of France",
1082                Some("https://example.com"),
1083            )
1084            .unwrap();
1085
1086        assert!(record.cose_envelope.is_some());
1087        assert!(record.vc_json.is_some());
1088        assert!(record.issuer_did.is_some());
1089        assert!(record.issuer_did.as_ref().unwrap().starts_with("did:key:z"));
1090
1091        let verification = mgr
1092            .verify_fact_provenance(&record, &mgr.self_trust())
1093            .unwrap();
1094        assert!(verification.valid);
1095        assert_eq!(verification.details.len(), 4);
1096
1097        assert!(mgr.get_record("fact-001").is_some());
1098        assert_eq!(mgr.record_count(), 1);
1099    }
1100
1101    #[test]
1102    fn full_triple_provenance_flow() {
1103        let dir = tempdir().unwrap();
1104        let mgr = ProvenanceManager::new(&dir.path().join("test.key"), Some(dir.path())).unwrap();
1105
1106        let record = mgr
1107            .create_triple_provenance(&TripleProvenanceParams {
1108                fact_id: "triple-001",
1109                content: b"paris capital_of france",
1110                dimensions: 16384,
1111                subject: "paris",
1112                relation: "capital_of",
1113                object: "france",
1114                source_uri: None,
1115            })
1116            .unwrap();
1117
1118        let vc: vc::FactCredential =
1119            serde_json::from_str(record.vc_json.as_ref().unwrap()).unwrap();
1120        assert_eq!(vc.credential_subject.subject_id.as_deref(), Some("paris"));
1121
1122        let verification = mgr
1123            .verify_fact_provenance(&record, &mgr.self_trust())
1124            .unwrap();
1125        assert!(verification.valid);
1126        assert!(mgr.get_record("triple-001").is_some());
1127    }
1128
1129    #[test]
1130    fn store_manifest_flow() {
1131        let dir = tempdir().unwrap();
1132        let mgr = ProvenanceManager::new(&dir.path().join("test.key"), Some(dir.path())).unwrap();
1133
1134        let store_data = b"serialized store contents here";
1135        let manifest = mgr
1136            .create_store_manifest("store-001", store_data, 42, 16384, Some("Test Store"))
1137            .unwrap();
1138
1139        assert!(manifest.cose_envelope.is_some());
1140
1141        let verification = mgr
1142            .verify_store_manifest(&manifest, Some(store_data), &mgr.self_trust())
1143            .unwrap();
1144        assert!(verification.valid);
1145        assert!(verification
1146            .details
1147            .iter()
1148            .any(|d| d.check == "store_hash" && d.passed));
1149
1150        let bad_verification = mgr
1151            .verify_store_manifest(&manifest, Some(b"wrong"), &mgr.self_trust())
1152            .unwrap();
1153        assert!(!bad_verification.valid);
1154    }
1155
1156    #[test]
1157    fn issuer_did_consistency() {
1158        let dir = tempdir().unwrap();
1159        let key_path = dir.path().join("test.key");
1160        let mgr1 = ProvenanceManager::new(&key_path, Some(dir.path())).unwrap();
1161        let mgr2 = ProvenanceManager::new(&key_path, Some(dir.path())).unwrap();
1162        assert_eq!(mgr1.issuer_did(), mgr2.issuer_did());
1163    }
1164
1165    #[test]
1166    fn records_persist_to_disk() {
1167        let dir = tempdir().unwrap();
1168        let key_path = dir.path().join("test.key");
1169
1170        {
1171            let mgr = ProvenanceManager::new(&key_path, Some(dir.path())).unwrap();
1172            mgr.create_fact_provenance("f1", b"content one", None)
1173                .unwrap();
1174            mgr.create_fact_provenance("f2", b"content two", None)
1175                .unwrap();
1176            assert_eq!(mgr.record_count(), 2);
1177        }
1178
1179        let mgr2 = ProvenanceManager::new(&key_path, Some(dir.path())).unwrap();
1180        assert_eq!(mgr2.record_count(), 2);
1181        assert!(mgr2.get_record("f1").is_some());
1182        assert!(mgr2.get_record("f2").is_some());
1183    }
1184
1185    #[test]
1186    fn standalone_verify_without_manager() {
1187        let dir = tempdir().unwrap();
1188        let mgr = ProvenanceManager::new(&dir.path().join("test.key"), Some(dir.path())).unwrap();
1189        let record = mgr
1190            .create_fact_provenance("standalone-001", b"verify without manager", None)
1191            .unwrap();
1192        let issuer = record.issuer_did.clone().unwrap();
1193        drop(mgr);
1194
1195        let trust = trust::TrustStore::trusting_did(&issuer).unwrap();
1196        let result = verify_record(&record, &trust).unwrap();
1197        assert!(result.valid);
1198        assert_eq!(result.details.len(), 3);
1199    }
1200
1201    #[test]
1202    fn hash_chain_integrity() {
1203        let dir = tempdir().unwrap();
1204        let mgr = ProvenanceManager::new(&dir.path().join("test.key"), Some(dir.path())).unwrap();
1205        mgr.create_fact_provenance("c1", b"first", None).unwrap();
1206        mgr.create_fact_provenance("c2", b"second", None).unwrap();
1207        mgr.create_fact_provenance("c3", b"third", None).unwrap();
1208        assert!(mgr.verify_log_integrity().unwrap());
1209    }
1210
1211    #[test]
1212    fn tampered_log_detected() {
1213        let dir = tempdir().unwrap();
1214        let mgr = ProvenanceManager::new(&dir.path().join("test.key"), Some(dir.path())).unwrap();
1215        mgr.create_fact_provenance("t1", b"one", None).unwrap();
1216        mgr.create_fact_provenance("t2", b"two", None).unwrap();
1217        drop(mgr);
1218
1219        let log_path = dir.path().join("provenance_log.jsonl");
1220        let contents = std::fs::read_to_string(&log_path).unwrap();
1221        let lines: Vec<&str> = contents.lines().collect();
1222        let tampered = lines[1].replace("\"t2\"", "\"TAMPERED\"");
1223        std::fs::write(&log_path, format!("{}\n{tampered}\n", lines[0])).unwrap();
1224
1225        let mgr2 = ProvenanceManager::new(&dir.path().join("test.key"), Some(dir.path())).unwrap();
1226        assert!(!mgr2.verify_log_integrity().unwrap());
1227    }
1228
1229    #[test]
1230    fn key_rotation_preserves_old_records() {
1231        let dir = tempdir().unwrap();
1232        let key_path = dir.path().join("orig.key");
1233        let mut mgr = ProvenanceManager::new(&key_path, Some(dir.path())).unwrap();
1234        let old_did = mgr.issuer_did().to_string();
1235
1236        let record = mgr
1237            .create_fact_provenance("before-rotate", b"old key", None)
1238            .unwrap();
1239
1240        let new_key_path = dir.path().join("rotated.key");
1241        let new_did = mgr.rotate_key(&new_key_path).unwrap();
1242        assert_ne!(old_did, new_did);
1243        assert_eq!(mgr.issuer_did(), new_did);
1244
1245        let post_record = mgr
1246            .create_fact_provenance("after-rotate", b"new key", None)
1247            .unwrap();
1248        assert_eq!(post_record.issuer_did.as_deref(), Some(new_did.as_str()));
1249
1250        let mut trust = trust::TrustStore::new();
1251        trust.trust_did(&old_did).unwrap();
1252        trust.trust_did(&new_did).unwrap();
1253
1254        let old_result = verify_record(&record, &trust).unwrap();
1255        assert!(old_result.valid);
1256
1257        let new_result = verify_record(&post_record, &trust).unwrap();
1258        assert!(new_result.valid);
1259
1260        assert!(mgr.verify_log_integrity().unwrap());
1261    }
1262
1263    #[test]
1264    fn batch_provenance_merkle() {
1265        let dir = tempdir().unwrap();
1266        let mgr = ProvenanceManager::new(&dir.path().join("test.key"), Some(dir.path())).unwrap();
1267
1268        let items: Vec<(&str, &[u8], Option<&str>)> = vec![
1269            ("b1", b"alpha", None),
1270            ("b2", b"beta", None),
1271            ("b3", b"gamma", Some("https://example.com")),
1272            ("b4", b"delta", None),
1273        ];
1274
1275        let records = mgr.create_batch_provenance(&items).unwrap();
1276        assert_eq!(records.len(), 4);
1277        assert_eq!(mgr.record_count(), 4);
1278
1279        let envelope = records[0].cose_envelope.as_ref().unwrap();
1280        for r in &records {
1281            assert_eq!(r.cose_envelope.as_ref().unwrap(), envelope);
1282            assert!(r.merkle_proof.is_some());
1283            assert!(r.vc_json.is_some());
1284        }
1285
1286        let trust = mgr.self_trust();
1287        for r in &records {
1288            let result = verify_record(r, &trust).unwrap();
1289            assert!(result
1290                .details
1291                .iter()
1292                .any(|d| d.check == "vc_proof" && d.passed));
1293        }
1294    }
1295
1296    #[test]
1297    fn credential_revocation() {
1298        let dir = tempdir().unwrap();
1299        let mgr = ProvenanceManager::new(&dir.path().join("test.key"), Some(dir.path())).unwrap();
1300
1301        let record = mgr
1302            .create_fact_provenance("fact-rev", b"revocable content", None)
1303            .unwrap();
1304
1305        let verification = mgr
1306            .verify_fact_provenance(&record, &mgr.self_trust())
1307            .unwrap();
1308        assert!(verification.valid);
1309        assert!(verification
1310            .details
1311            .iter()
1312            .any(|d| d.check == "revocation_status" && d.passed));
1313
1314        let vc: vc::FactCredential =
1315            serde_json::from_str(record.vc_json.as_ref().unwrap()).unwrap();
1316        let status_index: u64 = vc
1317            .credential_status
1318            .as_ref()
1319            .unwrap()
1320            .status_list_index
1321            .parse()
1322            .unwrap();
1323        mgr.revoke_credential(status_index).unwrap();
1324        assert!(mgr.is_revoked(status_index));
1325
1326        let verification = mgr
1327            .verify_fact_provenance(&record, &mgr.self_trust())
1328            .unwrap();
1329        assert!(!verification.valid);
1330        assert!(verification
1331            .details
1332            .iter()
1333            .any(|d| d.check == "revocation_status" && !d.passed));
1334    }
1335
1336    #[test]
1337    fn untrusted_key_rejected() {
1338        let dir = tempdir().unwrap();
1339        let mgr = ProvenanceManager::new(&dir.path().join("test.key"), Some(dir.path())).unwrap();
1340        let record = mgr
1341            .create_fact_provenance("fact-trust", b"authentic content", None)
1342            .unwrap();
1343
1344        // Signed by the manager and trusting the manager: authentic.
1345        let ok = mgr
1346            .verify_fact_provenance(&record, &mgr.self_trust())
1347            .unwrap();
1348        assert!(ok.valid);
1349
1350        // The record's signature is internally valid, but a trust store that
1351        // does not contain the signing key must reject it as non-authentic,
1352        // even though every signature check passes.
1353        let stranger = SigningKey::from_bytes(&rand::random());
1354        let foreign_trust = trust::TrustStore::trusting_key(&stranger.verifying_key());
1355        let rejected = mgr.verify_fact_provenance(&record, &foreign_trust).unwrap();
1356        assert!(!rejected.valid);
1357        assert!(rejected
1358            .details
1359            .iter()
1360            .any(|d| d.check == "trust_anchor" && !d.passed));
1361
1362        // Self-consistency, by contrast, still passes: the record is intact.
1363        let self_consistent = mgr
1364            .verify_fact_provenance_self_consistency(&record)
1365            .unwrap();
1366        assert!(self_consistent.valid);
1367    }
1368
1369    #[test]
1370    fn vc_only_record_requires_trust() {
1371        // A record carrying only a self-signed VC (no COSE envelope) must still
1372        // be trust-anchored via its issuer DID, not treated as authentic just
1373        // because the VC verifies against its own embedded issuer.
1374        let dir = tempdir().unwrap();
1375        let mgr = ProvenanceManager::new(&dir.path().join("test.key"), Some(dir.path())).unwrap();
1376        let record = mgr
1377            .create_fact_provenance("vc-only", b"content", None)
1378            .unwrap();
1379
1380        let mut vc_only = record.clone();
1381        vc_only.cose_envelope = None;
1382
1383        let ok = verify_record(&vc_only, &mgr.self_trust()).unwrap();
1384        assert!(ok.valid);
1385        assert!(ok
1386            .details
1387            .iter()
1388            .any(|d| d.check == "trust_anchor" && d.passed));
1389
1390        let stranger = SigningKey::from_bytes(&rand::random());
1391        let foreign = trust::TrustStore::trusting_key(&stranger.verifying_key());
1392        let rejected = verify_record(&vc_only, &foreign).unwrap();
1393        assert!(!rejected.valid);
1394        assert!(rejected
1395            .details
1396            .iter()
1397            .any(|d| d.check == "trust_anchor" && !d.passed));
1398    }
1399
1400    #[test]
1401    fn cawg_untrusted_issuer_rejected() {
1402        let dir = tempdir().unwrap();
1403        let mgr = ProvenanceManager::new(&dir.path().join("test.key"), Some(dir.path())).unwrap();
1404
1405        let referenced = vec![(
1406            "self#jumbf=c2pa.assertions/c2pa.hash.data".to_string(),
1407            "sha256".to_string(),
1408            vec![0x11u8; 32],
1409        )];
1410        let assertion = mgr.create_cawg_assertion(&referenced, "hms agent").unwrap();
1411
1412        // The manager's own CAWG issuer (did:jwk of its key) is trusted by self_trust.
1413        assert!(mgr
1414            .verify_cawg_assertion(&assertion, &mgr.self_trust())
1415            .is_ok());
1416
1417        // A trust store that does not contain the issuer must reject it, even
1418        // though the assertion is internally self-consistent.
1419        let stranger = SigningKey::from_bytes(&rand::random());
1420        let foreign = trust::TrustStore::trusting_key(&stranger.verifying_key());
1421        assert!(mgr.verify_cawg_assertion(&assertion, &foreign).is_err());
1422    }
1423}