1use rand::RngCore;
40use std::time::Duration;
41
42use crate::entropy::EntropySnapshot;
43use crate::error::{KkError, Result};
44use crate::kdf;
45use crate::kk_mix;
46use zeroize::Zeroize;
47
48pub const GENESIS_MAC: [u8; 32] = [0u8; 32];
50
51#[derive(Clone, Debug)]
67pub struct TemporalCommitment {
68 pub mac: [u8; 32],
69}
70
71impl TemporalCommitment {
72 pub fn to_bytes(&self) -> Vec<u8> {
73 self.mac.to_vec()
74 }
75
76 pub fn from_bytes(data: &[u8]) -> Result<Self> {
77 if data.len() < 32 {
78 return Err(KkError::InvalidPacket("commitment too short".into()));
79 }
80 let mut mac = [0u8; 32];
81 mac.copy_from_slice(&data[..32]);
82 Ok(Self { mac })
83 }
84}
85
86pub fn commit(
90 shared_secret: &[u8],
91 snapshot: &EntropySnapshot,
92 ciphertext: &[u8],
93) -> Result<TemporalCommitment> {
94 let mut commit_key = kdf::derive_commitment_key(shared_secret, snapshot)?;
95
96 let mut message = Vec::with_capacity(32 + 16 + ciphertext.len());
97 message.extend_from_slice(&snapshot.bytes);
98 message.extend_from_slice(&snapshot.timestamp_nanos.to_le_bytes());
99 message.extend_from_slice(ciphertext);
100
101 let mac_bytes = kk_mix::kk_mac(&commit_key, &message);
102 commit_key.zeroize();
103
104 Ok(TemporalCommitment { mac: mac_bytes })
105}
106
107pub fn verify(
109 shared_secret: &[u8],
110 snapshot: &EntropySnapshot,
111 ciphertext: &[u8],
112 commitment: &TemporalCommitment,
113) -> Result<()> {
114 let mut commit_key = kdf::derive_commitment_key(shared_secret, snapshot)?;
115
116 let mut message = Vec::with_capacity(32 + 16 + ciphertext.len());
117 message.extend_from_slice(&snapshot.bytes);
118 message.extend_from_slice(&snapshot.timestamp_nanos.to_le_bytes());
119 message.extend_from_slice(ciphertext);
120
121 let verified = kk_mix::kk_mac_verify(&commit_key, &message, &commitment.mac);
122 commit_key.zeroize();
123
124 if verified {
125 Ok(())
126 } else {
127 Err(KkError::CommitmentMismatch)
128 }
129}
130
131pub fn commit_aead(
143 shared_secret: &[u8],
144 snapshot: &EntropySnapshot,
145 ciphertext: &[u8],
146 aad: &[u8],
147) -> Result<TemporalCommitment> {
148 let mut commit_key = kdf::derive_commitment_key(shared_secret, snapshot)?;
149
150 let aad_len = aad.len() as u64;
151 let mut message = Vec::with_capacity(32 + 16 + 8 + aad.len() + ciphertext.len());
152 message.extend_from_slice(&snapshot.bytes);
153 message.extend_from_slice(&snapshot.timestamp_nanos.to_le_bytes());
154 message.extend_from_slice(&aad_len.to_le_bytes());
155 message.extend_from_slice(aad);
156 message.extend_from_slice(ciphertext);
157
158 let mac_bytes = kk_mix::kk_mac(&commit_key, &message);
159 commit_key.zeroize();
160
161 Ok(TemporalCommitment { mac: mac_bytes })
162}
163
164pub fn commit_aead_batch_8(
169 shared_secret: &[u8],
170 snapshots: [&EntropySnapshot; 8],
171 ciphertexts: [&[u8]; 8],
172 aads: [&[u8]; 8],
173) -> Result<[TemporalCommitment; 8]> {
174 let mut commit_keys: [Vec<u8>; 8] = core::array::from_fn(|i| {
176 kdf::derive_commitment_key(shared_secret, snapshots[i])
177 .expect("commitment key derivation should not fail")
178 });
179
180 let prefixes: [Vec<u8>; 8] = core::array::from_fn(|i| {
182 let aad_len = aads[i].len() as u64;
183 let mut prefix = Vec::with_capacity(48 + aads[i].len());
184 prefix.extend_from_slice(&snapshots[i].bytes);
185 prefix.extend_from_slice(&snapshots[i].timestamp_nanos.to_le_bytes());
186 prefix.extend_from_slice(&aad_len.to_le_bytes());
187 prefix.extend_from_slice(aads[i]);
188 prefix
189 });
190
191 let key_refs: [&[u8]; 8] = core::array::from_fn(|i| commit_keys[i].as_slice());
192 let prefix_refs: [&[u8]; 8] = core::array::from_fn(|i| prefixes[i].as_slice());
193
194 let macs = kk_mix::kk_mac_batch_8_multipart(key_refs, prefix_refs, ciphertexts);
195
196 for k in &mut commit_keys {
197 k.zeroize();
198 }
199
200 Ok(core::array::from_fn(|i| TemporalCommitment {
201 mac: macs[i],
202 }))
203}
204
205pub fn verify_aead(
207 shared_secret: &[u8],
208 snapshot: &EntropySnapshot,
209 ciphertext: &[u8],
210 aad: &[u8],
211 commitment: &TemporalCommitment,
212) -> Result<()> {
213 let mut commit_key = kdf::derive_commitment_key(shared_secret, snapshot)?;
214
215 let aad_len = aad.len() as u64;
216 let mut message = Vec::with_capacity(32 + 16 + 8 + aad.len() + ciphertext.len());
217 message.extend_from_slice(&snapshot.bytes);
218 message.extend_from_slice(&snapshot.timestamp_nanos.to_le_bytes());
219 message.extend_from_slice(&aad_len.to_le_bytes());
220 message.extend_from_slice(aad);
221 message.extend_from_slice(ciphertext);
222
223 let verified = kk_mix::kk_mac_verify(&commit_key, &message, &commitment.mac);
224 commit_key.zeroize();
225
226 if verified {
227 Ok(())
228 } else {
229 Err(KkError::CommitmentMismatch)
230 }
231}
232
233#[derive(Clone, Debug)]
270pub struct TemporalProof {
271 pub mac: [u8; 32],
273 pub nonce: [u8; 32],
275 pub prev_mac: [u8; 32],
277}
278
279impl TemporalProof {
280 pub const BYTES: usize = 96;
282
283 pub fn to_bytes(&self) -> Vec<u8> {
284 let mut out = Vec::with_capacity(Self::BYTES);
285 out.extend_from_slice(&self.mac);
286 out.extend_from_slice(&self.nonce);
287 out.extend_from_slice(&self.prev_mac);
288 out
289 }
290
291 pub fn from_bytes(data: &[u8]) -> Result<Self> {
292 if data.len() < Self::BYTES {
293 return Err(KkError::InvalidPacket(format!(
294 "temporal proof too short: need {}, got {}",
295 Self::BYTES,
296 data.len()
297 )));
298 }
299 let mut mac = [0u8; 32];
300 let mut nonce = [0u8; 32];
301 let mut prev_mac = [0u8; 32];
302 mac.copy_from_slice(&data[..32]);
303 nonce.copy_from_slice(&data[32..64]);
304 prev_mac.copy_from_slice(&data[64..96]);
305 Ok(Self {
306 mac,
307 nonce,
308 prev_mac,
309 })
310 }
311}
312
313pub fn generate_challenge() -> Result<[u8; 32]> {
322 let mut nonce = [0u8; 32];
323 rand::rngs::OsRng
324 .try_fill_bytes(&mut nonce)
325 .map_err(|e| KkError::EntropyFailure(format!("nonce generation: {e}")))?;
326 Ok(nonce)
327}
328
329pub fn commit_bound(
339 shared_secret: &[u8],
340 snapshot: &EntropySnapshot,
341 ciphertext: &[u8],
342 verifier_nonce: &[u8; 32],
343 prev_mac: &[u8; 32],
344) -> Result<TemporalProof> {
345 let mut commit_key = kdf::derive_commitment_key(shared_secret, snapshot)?;
346
347 let mut message = Vec::with_capacity(32 + 32 + 32 + 16 + ciphertext.len());
349 message.extend_from_slice(verifier_nonce);
350 message.extend_from_slice(prev_mac);
351 message.extend_from_slice(&snapshot.bytes);
352 message.extend_from_slice(&snapshot.timestamp_nanos.to_le_bytes());
353 message.extend_from_slice(ciphertext);
354
355 let mac_bytes = kk_mix::kk_mac_with_entropy(&commit_key, &message, &snapshot.bytes);
357 commit_key.zeroize();
358
359 Ok(TemporalProof {
360 mac: mac_bytes,
361 nonce: *verifier_nonce,
362 prev_mac: *prev_mac,
363 })
364}
365
366pub fn verify_bound(
385 shared_secret: &[u8],
386 snapshot: &EntropySnapshot,
387 ciphertext: &[u8],
388 proof: &TemporalProof,
389 expected_nonce: &[u8; 32],
390 max_drift: Duration,
391) -> Result<()> {
392 if proof.nonce != *expected_nonce {
394 return Err(KkError::StaleNonce);
395 }
396
397 let now_nanos = std::time::SystemTime::now()
399 .duration_since(std::time::UNIX_EPOCH)
400 .unwrap_or_default()
401 .as_nanos();
402
403 let drift = now_nanos.abs_diff(snapshot.timestamp_nanos);
404
405 if drift > max_drift.as_nanos() {
406 return Err(KkError::EpochDrift {
407 claimed_nanos: snapshot.timestamp_nanos,
408 drift_nanos: drift,
409 max_nanos: max_drift.as_nanos(),
410 });
411 }
412
413 let mut commit_key = kdf::derive_commitment_key(shared_secret, snapshot)?;
415
416 let mut message = Vec::with_capacity(32 + 32 + 32 + 16 + ciphertext.len());
417 message.extend_from_slice(&proof.nonce);
418 message.extend_from_slice(&proof.prev_mac);
419 message.extend_from_slice(&snapshot.bytes);
420 message.extend_from_slice(&snapshot.timestamp_nanos.to_le_bytes());
421 message.extend_from_slice(ciphertext);
422
423 let verified =
424 kk_mix::kk_mac_verify_with_entropy(&commit_key, &message, &proof.mac, &snapshot.bytes);
425 commit_key.zeroize();
426
427 if verified {
428 Ok(())
429 } else {
430 Err(KkError::CommitmentMismatch)
431 }
432}
433
434#[cfg(test)]
435mod tests {
436 use super::*;
437 use crate::entropy;
438
439 #[test]
442 fn valid_commitment_verifies() {
443 let secret = b"test-key";
444 let snap = entropy::gather().unwrap();
445 let ciphertext = b"some ciphertext bytes";
446
447 let commitment = commit(secret, &snap, ciphertext).unwrap();
448 verify(secret, &snap, ciphertext, &commitment).unwrap();
449 }
450
451 #[test]
452 fn tampered_ciphertext_fails() {
453 let secret = b"test-key";
454 let snap = entropy::gather().unwrap();
455 let ciphertext = b"original ciphertext";
456
457 let commitment = commit(secret, &snap, ciphertext).unwrap();
458
459 let tampered = b"tampered ciphertext";
460 let result = verify(secret, &snap, tampered, &commitment);
461 assert!(
462 result.is_err(),
463 "Tampered ciphertext must fail verification"
464 );
465 }
466
467 #[test]
468 fn wrong_key_fails() {
469 let snap = entropy::gather().unwrap();
470 let ciphertext = b"test data";
471
472 let commitment = commit(b"correct-key", &snap, ciphertext).unwrap();
473 let result = verify(b"wrong-key", &snap, ciphertext, &commitment);
474 assert!(
475 result.is_err(),
476 "Wrong shared secret must fail verification"
477 );
478 }
479
480 #[test]
483 fn bound_proof_verifies() {
484 let secret = b"test-key";
485 let snap = entropy::gather().unwrap();
486 let ciphertext = b"bound ciphertext";
487 let nonce = generate_challenge().unwrap();
488
489 let proof = commit_bound(secret, &snap, ciphertext, &nonce, &GENESIS_MAC).unwrap();
490 verify_bound(
491 secret,
492 &snap,
493 ciphertext,
494 &proof,
495 &nonce,
496 Duration::from_secs(5),
497 )
498 .unwrap();
499 }
500
501 #[test]
502 fn wrong_nonce_rejected() {
503 let secret = b"test-key";
504 let snap = entropy::gather().unwrap();
505 let ciphertext = b"nonce test";
506 let real_nonce = generate_challenge().unwrap();
507 let fake_nonce = generate_challenge().unwrap();
508
509 let proof = commit_bound(secret, &snap, ciphertext, &real_nonce, &GENESIS_MAC).unwrap();
510 let result = verify_bound(
511 secret,
512 &snap,
513 ciphertext,
514 &proof,
515 &fake_nonce,
516 Duration::from_secs(5),
517 );
518 assert!(
519 matches!(result, Err(KkError::StaleNonce)),
520 "Wrong nonce must be rejected as StaleNonce"
521 );
522 }
523
524 #[test]
525 fn tampered_ciphertext_fails_bound() {
526 let secret = b"test-key";
527 let snap = entropy::gather().unwrap();
528 let nonce = generate_challenge().unwrap();
529
530 let proof = commit_bound(secret, &snap, b"original", &nonce, &GENESIS_MAC).unwrap();
531 let result = verify_bound(
532 secret,
533 &snap,
534 b"tampered",
535 &proof,
536 &nonce,
537 Duration::from_secs(5),
538 );
539 assert!(
540 result.is_err(),
541 "Tampered ciphertext must fail bound verification"
542 );
543 }
544
545 #[test]
546 fn epoch_drift_rejected() {
547 let secret = b"test-key";
548 let ciphertext = b"epoch test";
549 let nonce = generate_challenge().unwrap();
550
551 let real_snap = entropy::gather().unwrap();
553 let old_snap = EntropySnapshot {
554 bytes: real_snap.bytes,
555 timestamp_nanos: 1_000_000_000_000_000_000, };
557
558 let proof = commit_bound(secret, &old_snap, ciphertext, &nonce, &GENESIS_MAC).unwrap();
559 let result = verify_bound(
560 secret,
561 &old_snap,
562 ciphertext,
563 &proof,
564 &nonce,
565 Duration::from_secs(5),
566 );
567 assert!(
568 matches!(result, Err(KkError::EpochDrift { .. })),
569 "Ancient timestamp must be rejected as EpochDrift"
570 );
571 }
572
573 #[test]
574 fn chain_ordering() {
575 let secret = b"chain-key";
576 let nonce1 = generate_challenge().unwrap();
577 let nonce2 = generate_challenge().unwrap();
578
579 let snap1 = entropy::gather().unwrap();
581 let ct1 = b"message one";
582 let proof1 = commit_bound(secret, &snap1, ct1, &nonce1, &GENESIS_MAC).unwrap();
583 verify_bound(
584 secret,
585 &snap1,
586 ct1,
587 &proof1,
588 &nonce1,
589 Duration::from_secs(5),
590 )
591 .unwrap();
592
593 let snap2 = entropy::gather().unwrap();
595 let ct2 = b"message two";
596 let proof2 = commit_bound(secret, &snap2, ct2, &nonce2, &proof1.mac).unwrap();
597 verify_bound(
598 secret,
599 &snap2,
600 ct2,
601 &proof2,
602 &nonce2,
603 Duration::from_secs(5),
604 )
605 .unwrap();
606
607 assert_eq!(
609 proof2.prev_mac, proof1.mac,
610 "Proof 2 must reference Proof 1's MAC"
611 );
612 assert_eq!(
613 proof1.prev_mac, GENESIS_MAC,
614 "Proof 1 must reference genesis"
615 );
616 }
617
618 #[test]
619 fn proof_serde_roundtrip() {
620 let secret = b"serde-key";
621 let snap = entropy::gather().unwrap();
622 let nonce = generate_challenge().unwrap();
623
624 let proof = commit_bound(secret, &snap, b"serde test", &nonce, &GENESIS_MAC).unwrap();
625 let bytes = proof.to_bytes();
626 assert_eq!(bytes.len(), TemporalProof::BYTES);
627
628 let restored = TemporalProof::from_bytes(&bytes).unwrap();
629 assert_eq!(restored.mac, proof.mac);
630 assert_eq!(restored.nonce, proof.nonce);
631 assert_eq!(restored.prev_mac, proof.prev_mac);
632 }
633
634 #[test]
635 fn wrong_prev_mac_fails() {
636 let secret = b"chain-key";
637 let snap = entropy::gather().unwrap();
638 let nonce = generate_challenge().unwrap();
639 let ciphertext = b"chain integrity";
640
641 let proof = commit_bound(secret, &snap, ciphertext, &nonce, &GENESIS_MAC).unwrap();
643
644 let mut forged = proof.clone();
646 forged.prev_mac = [0xFF; 32];
647
648 let result = verify_bound(
650 secret,
651 &snap,
652 ciphertext,
653 &forged,
654 &nonce,
655 Duration::from_secs(5),
656 );
657 assert!(
658 result.is_err(),
659 "Forged prev_mac must fail MAC verification"
660 );
661 }
662}