holographic_memory/core/provenance/
cose.rs1use anyhow::{anyhow, Result};
5use coset::iana::Algorithm;
6use coset::{CborSerializable, CoseSign1, CoseSign1Builder, HeaderBuilder};
7use ed25519_dalek::{Signer, SigningKey, Verifier, VerifyingKey};
8
9pub fn sign_payload(signing_key: &SigningKey, payload: &[u8]) -> Result<Vec<u8>> {
11 let protected = HeaderBuilder::new()
12 .algorithm(Algorithm::EdDSA)
13 .key_id(signing_key.verifying_key().to_bytes().to_vec())
14 .build();
15
16 let sign1 = CoseSign1Builder::new()
17 .protected(protected)
18 .payload(payload.to_vec())
19 .create_signature(b"", |data| signing_key.sign(data).to_bytes().to_vec())
20 .build();
21
22 sign1
23 .to_vec()
24 .map_err(|e| anyhow!("COSE serialization failed: {e}"))
25}
26
27pub fn verify_and_extract(verifying_key: &VerifyingKey, cose_bytes: &[u8]) -> Result<Vec<u8>> {
29 let sign1 = CoseSign1::from_slice(cose_bytes)
30 .map_err(|e| anyhow!("COSE deserialization failed: {e}"))?;
31
32 let payload = sign1
33 .payload
34 .as_ref()
35 .ok_or_else(|| anyhow!("COSE envelope has no payload"))?;
36
37 sign1
38 .verify_signature(b"", |sig, data| {
39 let signature = ed25519_dalek::Signature::from_slice(sig)
40 .map_err(|e| anyhow!("invalid signature bytes: {e}"))?;
41 verifying_key
42 .verify(data, &signature)
43 .map_err(|e| anyhow!("signature verification failed: {e}"))
44 })
45 .map_err(|e| anyhow!("COSE verification failed: {e}"))?;
46
47 Ok(payload.clone())
48}
49
50pub fn extract_key_id(cose_bytes: &[u8]) -> Result<Vec<u8>> {
52 let sign1 = CoseSign1::from_slice(cose_bytes)
53 .map_err(|e| anyhow!("COSE deserialization failed: {e}"))?;
54 let kid = sign1.protected.header.key_id.clone();
55 if kid.is_empty() {
56 return Err(anyhow!("no key ID in COSE envelope"));
57 }
58 Ok(kid)
59}
60
61pub fn sign_fact_claim(
64 signing_key: &SigningKey,
65 fact_id: &str,
66 content_hash: &[u8; 32],
67 source_uri: Option<&str>,
68 timestamp_ms: u64,
69) -> Result<Vec<u8>> {
70 let mut claim = ciborium::Value::Map(vec![
71 (
72 ciborium::Value::Text("id".to_string()),
73 ciborium::Value::Text(fact_id.to_string()),
74 ),
75 (
76 ciborium::Value::Text("content_hash".to_string()),
77 ciborium::Value::Bytes(content_hash.to_vec()),
78 ),
79 (
80 ciborium::Value::Text("timestamp_ms".to_string()),
81 ciborium::Value::Integer(timestamp_ms.into()),
82 ),
83 ]);
84 if let Some(uri) = source_uri {
85 if let ciborium::Value::Map(ref mut map) = claim {
86 map.push((
87 ciborium::Value::Text("source_uri".to_string()),
88 ciborium::Value::Text(uri.to_string()),
89 ));
90 }
91 }
92 let mut payload = Vec::new();
93 ciborium::into_writer(&claim, &mut payload)
94 .map_err(|e| anyhow!("CBOR encoding failed: {e}"))?;
95
96 sign_payload(signing_key, &payload)
97}
98
99#[cfg(test)]
100mod tests {
101 use super::*;
102
103 fn test_keypair() -> SigningKey {
104 SigningKey::from_bytes(&rand::random())
105 }
106
107 #[test]
108 fn sign_verify_roundtrip() {
109 let key = test_keypair();
110 let payload = b"test payload data";
111
112 let envelope = sign_payload(&key, payload).unwrap();
113 let recovered = verify_and_extract(&key.verifying_key(), &envelope).unwrap();
114 assert_eq!(recovered, payload);
115 }
116
117 #[test]
118 fn tampered_payload_rejected() {
119 let key = test_keypair();
120 let envelope = sign_payload(&key, b"original").unwrap();
121
122 let wrong_key = test_keypair();
123 assert!(verify_and_extract(&wrong_key.verifying_key(), &envelope).is_err());
124 }
125
126 #[test]
127 fn key_id_extraction() {
128 let key = test_keypair();
129 let envelope = sign_payload(&key, b"data").unwrap();
130 let kid = extract_key_id(&envelope).unwrap();
131 assert_eq!(kid, key.verifying_key().to_bytes().to_vec());
132 }
133
134 #[test]
135 fn fact_claim_roundtrip() {
136 let key = test_keypair();
137 let hash = [0xABu8; 32];
138 let envelope = sign_fact_claim(
139 &key,
140 "fact-001",
141 &hash,
142 Some("https://example.com"),
143 1719878400000,
144 )
145 .unwrap();
146 let payload = verify_and_extract(&key.verifying_key(), &envelope).unwrap();
147
148 let decoded: ciborium::Value = ciborium::from_reader(payload.as_slice()).unwrap();
149 if let ciborium::Value::Map(entries) = decoded {
150 let id_val = entries
151 .iter()
152 .find(|(k, _)| k == &ciborium::Value::Text("id".to_string()))
153 .unwrap();
154 assert_eq!(id_val.1, ciborium::Value::Text("fact-001".to_string()));
155 } else {
156 panic!("expected CBOR map");
157 }
158 }
159
160 fn unhex(s: &str) -> Vec<u8> {
161 (0..s.len())
162 .step_by(2)
163 .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
164 .collect()
165 }
166
167 #[test]
168 fn verifies_cogmem_cross_conformance_vector() {
169 let pk = unhex("4b5f52db17ebdeb555101922e89beac9b43e864086b02e4529951d7f491f0cfa");
173 let cose = unhex("845838a3012703706170706c69636174696f6e2f63626f720458204b5f52db17ebdeb555101922e89beac9b43e864086b02e4529951d7f491f0cfaa05887a6636973736c6469643a6b65793a74657374686d656d6f7279496468766563746f722d316a6d656d6f7279547970656472756c65656576656e7467637265617465646d73746174656d656e74486173685820000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f6b74696d657374616d704d731b0000019070bdb80058402fc837df236fa9d4d3a8bad98148ea020b4ea7bbb717d57819675eaee65264b280f5ec8d22f21dc9531f91fe4571305f84f05396cfde33ac897eac52a4937e09");
174 let expected = unhex("a6636973736c6469643a6b65793a74657374686d656d6f7279496468766563746f722d316a6d656d6f7279547970656472756c65656576656e7467637265617465646d73746174656d656e74486173685820000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f6b74696d657374616d704d731b0000019070bdb800");
175 let vk = VerifyingKey::from_bytes(&pk.try_into().unwrap()).unwrap();
176 let payload = verify_and_extract(&vk, &cose).unwrap();
177 assert_eq!(payload, expected);
178 }
179}