1use base64::engine::general_purpose::STANDARD;
5use base64::Engine;
6use chacha20poly1305::aead::{Aead, KeyInit};
7use chacha20poly1305::{ChaCha20Poly1305, Nonce};
8use ed25519_dalek::{Signature, Signer, SigningKey, Verifier, VerifyingKey};
9use hkdf::Hkdf;
10use rand::RngCore;
11use serde::{Deserialize, Serialize};
12use serde_json::Value;
13use sha2::{Digest, Sha256};
14use x25519_dalek::{PublicKey as X25519Public, StaticSecret as X25519Secret};
15
16use crate::canonicalize;
17
18const HKDF_INFO: &[u8] = b"tfbundle/wrap";
19
20#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
21pub struct EncryptedProofBundle {
22 pub bundle_version: String,
23 pub level: String,
24 pub ciphertext: String,
25 pub nonce: String,
26 pub wrapped_keys: Vec<WrappedKey>,
27 #[serde(skip_serializing_if = "Option::is_none", default)]
28 pub transparency_anchor: Option<Value>,
29 pub signature: SignatureEnvelope,
30}
31
32#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
33pub struct WrappedKey {
34 pub recipient: String,
35 #[serde(skip_serializing_if = "Option::is_none", default)]
36 pub recipient_key_id: Option<String>,
37 pub ephemeral_public: String,
38 pub wrapped: String,
39 pub wrap_nonce: String,
40}
41
42#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
43pub struct SignatureEnvelope {
44 pub algorithm: String,
45 pub signer: String,
46 pub signature: String,
47}
48
49#[derive(Clone, Debug)]
50pub struct BundleRecipient {
51 pub actor: String,
52 pub kem_public: [u8; 32],
53 pub key_id: Option<String>,
54}
55
56pub fn seal_bundle(
57 bundle: &Value,
58 recipients: &[BundleRecipient],
59 level: &str,
60 signer_priv: &[u8; 32],
61 signer: &str,
62) -> EncryptedProofBundle {
63 let mut rng = rand::thread_rng();
64 let mut data_key = [0u8; 32];
65 rng.fill_bytes(&mut data_key);
66 let mut nonce_bytes = [0u8; 12];
67 rng.fill_bytes(&mut nonce_bytes);
68 let cipher = ChaCha20Poly1305::new(&data_key.into());
69 let plaintext = canonicalize(bundle).unwrap_or_default();
70 let ciphertext = cipher
71 .encrypt(Nonce::from_slice(&nonce_bytes), plaintext.as_bytes())
72 .expect("seal");
73
74 let mut wrapped_keys = Vec::with_capacity(recipients.len());
75 for r in recipients {
76 let mut eph_seed = [0u8; 32];
77 rng.fill_bytes(&mut eph_seed);
78 let eph = X25519Secret::from(eph_seed);
79 let eph_pub = X25519Public::from(&eph);
80 let recipient_pub = X25519Public::from(r.kem_public);
81 let shared = eph.diffie_hellman(&recipient_pub);
82 let hk = Hkdf::<Sha256>::new(None, shared.as_bytes());
83 let mut wrap_key = [0u8; 32];
84 hk.expand(HKDF_INFO, &mut wrap_key).expect("hkdf");
85 let mut wrap_nonce = [0u8; 12];
86 rng.fill_bytes(&mut wrap_nonce);
87 let wrap_cipher = ChaCha20Poly1305::new(&wrap_key.into());
88 let wrapped = wrap_cipher
89 .encrypt(Nonce::from_slice(&wrap_nonce), data_key.as_ref())
90 .expect("wrap");
91 wrapped_keys.push(WrappedKey {
92 recipient: r.actor.clone(),
93 recipient_key_id: r.key_id.clone(),
94 ephemeral_public: STANDARD.encode(eph_pub.as_bytes()),
95 wrapped: STANDARD.encode(&wrapped),
96 wrap_nonce: STANDARD.encode(wrap_nonce),
97 });
98 }
99
100 let mut stub = EncryptedProofBundle {
101 bundle_version: "1".into(),
102 level: level.into(),
103 ciphertext: STANDARD.encode(&ciphertext),
104 nonce: STANDARD.encode(nonce_bytes),
105 wrapped_keys,
106 transparency_anchor: None,
107 signature: SignatureEnvelope {
108 algorithm: "ed25519".into(),
109 signer: signer.into(),
110 signature: String::new(),
111 },
112 };
113 let digest = encrypted_signing_bytes(&stub);
114 let signing = SigningKey::from_bytes(signer_priv);
115 let sig: Signature = signing.sign(&digest);
116 stub.signature.signature = STANDARD.encode(sig.to_bytes());
117 stub
118}
119
120pub fn open_bundle(
121 enc: &EncryptedProofBundle,
122 recipient_priv: &[u8; 32],
123 recipient_actor: &str,
124 signer_pub: Option<&[u8; 32]>,
125) -> Result<Value, String> {
126 let wrap = enc
127 .wrapped_keys
128 .iter()
129 .find(|w| w.recipient == recipient_actor)
130 .ok_or_else(|| format!("no wrapped key for recipient {}", recipient_actor))?;
131 let eph_pub_bytes = STANDARD
132 .decode(&wrap.ephemeral_public)
133 .map_err(|e| format!("ephemeral_public base64: {}", e))?;
134 let mut eph_pub_arr = [0u8; 32];
135 if eph_pub_bytes.len() != 32 {
136 return Err("ephemeral_public not 32 bytes".into());
137 }
138 eph_pub_arr.copy_from_slice(&eph_pub_bytes);
139 let recipient_secret = X25519Secret::from(*recipient_priv);
140 let shared = recipient_secret.diffie_hellman(&X25519Public::from(eph_pub_arr));
141 let hk = Hkdf::<Sha256>::new(None, shared.as_bytes());
142 let mut wrap_key = [0u8; 32];
143 hk.expand(HKDF_INFO, &mut wrap_key)
144 .map_err(|e| e.to_string())?;
145 let wrapped = STANDARD
146 .decode(&wrap.wrapped)
147 .map_err(|e| format!("wrapped base64: {}", e))?;
148 let wrap_nonce = STANDARD
149 .decode(&wrap.wrap_nonce)
150 .map_err(|e| format!("wrap_nonce base64: {}", e))?;
151 let data_key_bytes = ChaCha20Poly1305::new(&wrap_key.into())
152 .decrypt(Nonce::from_slice(&wrap_nonce), wrapped.as_ref())
153 .map_err(|e| format!("unwrap: {}", e))?;
154 let ciphertext = STANDARD
155 .decode(&enc.ciphertext)
156 .map_err(|e| format!("ciphertext base64: {}", e))?;
157 let nonce = STANDARD
158 .decode(&enc.nonce)
159 .map_err(|e| format!("nonce base64: {}", e))?;
160 let mut data_key_arr = [0u8; 32];
161 if data_key_bytes.len() != 32 {
162 return Err("data_key not 32 bytes".into());
163 }
164 data_key_arr.copy_from_slice(&data_key_bytes);
165 let plaintext = ChaCha20Poly1305::new(&data_key_arr.into())
166 .decrypt(Nonce::from_slice(&nonce), ciphertext.as_ref())
167 .map_err(|e| format!("decrypt: {}", e))?;
168
169 if let Some(pk) = signer_pub {
170 let digest = encrypted_signing_bytes(enc);
171 let sig_bytes = STANDARD
172 .decode(&enc.signature.signature)
173 .map_err(|e| format!("signature base64: {}", e))?;
174 let sig = Signature::from_slice(&sig_bytes).map_err(|e| format!("sig parse: {}", e))?;
175 let vk = VerifyingKey::from_bytes(pk).map_err(|e| format!("verifying key: {}", e))?;
176 if vk.verify(&digest, &sig).is_err() {
177 return Err("encrypted bundle signature did not verify".into());
178 }
179 }
180
181 let json: Value = serde_json::from_slice(&plaintext).map_err(|e| e.to_string())?;
182 Ok(json)
183}
184
185pub fn encrypted_signing_bytes(enc: &EncryptedProofBundle) -> [u8; 32] {
186 let mut value = serde_json::to_value(enc).unwrap_or(Value::Null);
187 if let Value::Object(map) = &mut value {
188 map.remove("signature");
189 }
190 let canonical = canonicalize(&value).unwrap_or_default();
191 Sha256::digest(canonical.as_bytes()).into()
192}
193
194pub fn build_rfc3161_request(data: &[u8]) -> Vec<u8> {
196 let digest: [u8; 32] = Sha256::digest(data).into();
197 let oid_sha256: [u8; 11] = [
198 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01,
199 ];
200 let alg_id = der_sequence(&[oid_sha256.to_vec(), vec![0x05, 0x00]]);
201 let hashed_message = der_octet_string(&digest);
202 let message_imprint = der_sequence(&[alg_id, hashed_message]);
203 let version = der_integer(&[0x01]);
204 let cert_req = vec![0x01, 0x01, 0xff];
205 der_sequence(&[version, message_imprint, cert_req])
206}
207
208fn der_sequence(parts: &[Vec<u8>]) -> Vec<u8> {
209 let body: Vec<u8> = parts.iter().flat_map(|p| p.clone()).collect();
210 let mut out = Vec::with_capacity(2 + body.len());
211 out.push(0x30);
212 out.extend_from_slice(&der_len(body.len()));
213 out.extend_from_slice(&body);
214 out
215}
216
217fn der_octet_string(bytes: &[u8]) -> Vec<u8> {
218 let mut out = Vec::with_capacity(2 + bytes.len());
219 out.push(0x04);
220 out.extend_from_slice(&der_len(bytes.len()));
221 out.extend_from_slice(bytes);
222 out
223}
224
225fn der_integer(bytes: &[u8]) -> Vec<u8> {
226 let mut start = 0usize;
227 while start < bytes.len() - 1 && bytes[start] == 0 {
228 start += 1;
229 }
230 let payload = &bytes[start..];
231 let needs_pad = payload[0] & 0x80 != 0;
232 let len = payload.len() + if needs_pad { 1 } else { 0 };
233 let mut out = Vec::with_capacity(2 + len);
234 out.push(0x02);
235 out.extend_from_slice(&der_len(len));
236 if needs_pad {
237 out.push(0x00);
238 }
239 out.extend_from_slice(payload);
240 out
241}
242
243fn der_len(n: usize) -> Vec<u8> {
244 if n < 0x80 {
245 return vec![n as u8];
246 }
247 let mut bytes = Vec::new();
248 let mut v = n;
249 while v > 0 {
250 bytes.insert(0, (v & 0xff) as u8);
251 v >>= 8;
252 }
253 let mut out = Vec::with_capacity(1 + bytes.len());
254 out.push(0x80 | bytes.len() as u8);
255 out.extend_from_slice(&bytes);
256 out
257}
258
259pub trait AnchorBackend: Send + Sync {
263 fn kind(&self) -> &'static str;
264 fn submit(&self, bundle_bytes: &[u8]) -> Result<Value, String>;
265 fn verify(&self, bundle_bytes: &[u8], inclusion_proof: &Value) -> bool;
266}
267
268#[cfg(feature = "http-anchors")]
275pub struct Rfc6962Anchor {
276 pub log_url: String,
277}
278
279#[cfg(feature = "http-anchors")]
280impl Rfc6962Anchor {
281 pub fn new(log_url: impl Into<String>) -> Self {
282 Self {
283 log_url: log_url.into(),
284 }
285 }
286}
287
288#[cfg(feature = "http-anchors")]
289impl AnchorBackend for Rfc6962Anchor {
290 fn kind(&self) -> &'static str {
291 "rfc6962"
292 }
293 fn submit(&self, bundle_bytes: &[u8]) -> Result<Value, String> {
294 let digest = Sha256::digest(bundle_bytes);
300 let cert_b64 = crate::crypto::b64encode(&digest);
301 let body = serde_json::json!({ "chain": [cert_b64] });
302 let url = format!("{}/ct/v1/add-chain", self.log_url.trim_end_matches('/'));
303 let resp = ureq::post(&url)
304 .set("content-type", "application/json")
305 .send_json(body)
306 .map_err(|e| format!("rfc6962 submit: {e}"))?;
307 let json: Value = resp
308 .into_json()
309 .map_err(|e| format!("rfc6962 response parse: {e}"))?;
310 Ok(serde_json::json!({
311 "kind": "rfc6962",
312 "log_url": self.log_url,
313 "sct": json,
314 "digest_hex": digest.iter().map(|b| format!("{:02x}", b)).collect::<String>(),
315 }))
316 }
317 fn verify(&self, bundle_bytes: &[u8], inclusion_proof: &Value) -> bool {
318 let digest = Sha256::digest(bundle_bytes);
319 let want: String = digest.iter().map(|b| format!("{:02x}", b)).collect();
320 inclusion_proof
321 .get("digest_hex")
322 .and_then(|v| v.as_str())
323 .map(|s| s == want)
324 .unwrap_or(false)
325 }
326}
327
328#[cfg(feature = "http-anchors")]
332pub struct SigstoreAnchor {
333 pub rekor_url: String,
334}
335
336#[cfg(feature = "http-anchors")]
337impl SigstoreAnchor {
338 pub fn new(rekor_url: impl Into<String>) -> Self {
339 Self {
340 rekor_url: rekor_url.into(),
341 }
342 }
343}
344
345#[cfg(feature = "http-anchors")]
346impl AnchorBackend for SigstoreAnchor {
347 fn kind(&self) -> &'static str {
348 "sigstore"
349 }
350 fn submit(&self, bundle_bytes: &[u8]) -> Result<Value, String> {
351 let digest = Sha256::digest(bundle_bytes);
352 let hex: String = digest.iter().map(|b| format!("{:02x}", b)).collect();
353 let body = serde_json::json!({
355 "apiVersion": "0.0.1",
356 "kind": "hashedrekord",
357 "spec": {
358 "data": { "hash": { "algorithm": "sha256", "value": hex } },
359 "signature": { "format": "x509" },
360 },
361 });
362 let url = format!(
363 "{}/api/v1/log/entries",
364 self.rekor_url.trim_end_matches('/')
365 );
366 let resp = ureq::post(&url)
367 .set("content-type", "application/json")
368 .send_json(body)
369 .map_err(|e| format!("sigstore submit: {e}"))?;
370 let json: Value = resp
371 .into_json()
372 .map_err(|e| format!("sigstore response parse: {e}"))?;
373 Ok(serde_json::json!({
374 "kind": "sigstore",
375 "rekor_url": self.rekor_url,
376 "entry": json,
377 "digest_hex": digest.iter().map(|b| format!("{:02x}", b)).collect::<String>(),
378 }))
379 }
380 fn verify(&self, bundle_bytes: &[u8], inclusion_proof: &Value) -> bool {
381 let digest = Sha256::digest(bundle_bytes);
382 let want: String = digest.iter().map(|b| format!("{:02x}", b)).collect();
383 inclusion_proof
384 .get("digest_hex")
385 .and_then(|v| v.as_str())
386 .map(|s| s == want)
387 .unwrap_or(false)
388 }
389}
390
391#[derive(Default)]
393pub struct MemoryAnchor {
394 entries: std::sync::Mutex<std::collections::HashMap<String, usize>>,
395}
396
397impl MemoryAnchor {
398 pub fn new() -> Self {
399 Self::default()
400 }
401
402 pub fn submit(&self, bundle_bytes: &[u8]) -> Value {
403 let digest = Sha256::digest(bundle_bytes);
404 let hex: String = digest.iter().map(|b| format!("{:02x}", b)).collect();
405 let mut entries = self.entries.lock().unwrap();
406 let seq = entries.len();
407 entries.insert(hex.clone(), seq);
408 serde_json::json!({ "kind": "memory", "digest": hex, "sequence_number": seq })
409 }
410
411 pub fn verify_inclusion(&self, bundle_bytes: &[u8], inclusion_proof: &Value) -> bool {
412 let digest = Sha256::digest(bundle_bytes);
413 let hex: String = digest.iter().map(|b| format!("{:02x}", b)).collect();
414 if inclusion_proof.get("digest").and_then(|v| v.as_str()) != Some(&hex) {
415 return false;
416 }
417 let seq = inclusion_proof
418 .get("sequence_number")
419 .and_then(|v| v.as_u64())
420 .map(|n| n as usize);
421 let entries = self.entries.lock().unwrap();
422 seq == entries.get(&hex).copied()
423 }
424}
425
426impl AnchorBackend for MemoryAnchor {
427 fn kind(&self) -> &'static str {
428 "memory"
429 }
430 fn submit(&self, bundle_bytes: &[u8]) -> Result<Value, String> {
431 Ok(MemoryAnchor::submit(self, bundle_bytes))
432 }
433 fn verify(&self, bundle_bytes: &[u8], inclusion_proof: &Value) -> bool {
434 MemoryAnchor::verify_inclusion(self, bundle_bytes, inclusion_proof)
435 }
436}