1use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11
12use crate::error::{SignatifError, SignatifResult};
13use crate::graph::SignatureVerifier;
14use crate::jcs;
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct TimeAttestation {
19 pub authority: String,
21 pub artifact_hash: String,
23 pub attested_at: DateTime<Utc>,
25 pub external_anchor: Vec<u8>,
28 pub signature: Vec<u8>,
30}
31
32impl TimeAttestation {
33 pub fn signing_bytes(&self) -> SignatifResult<Vec<u8>> {
39 let v = serde_json::json!({
40 "authority": self.authority,
41 "artifact_hash": self.artifact_hash,
42 "attested_at": self.attested_at.to_rfc3339(),
43 "external_anchor": hex::encode(&self.external_anchor),
44 });
45 Ok(jcs::canonicalize(&v)?.into_bytes())
46 }
47
48 pub fn verify(
56 &self,
57 authority_key: &[u8],
58 verifier: &dyn SignatureVerifier,
59 ) -> SignatifResult<()> {
60 if self.external_anchor.is_empty() {
61 return Err(SignatifError::BadSignature {
62 context: "time attestation lacks an external anchor".into(),
63 });
64 }
65 let msg = self.signing_bytes()?;
66 if !verifier.verify(authority_key, &msg, &self.signature) {
67 return Err(SignatifError::BadSignature {
68 context: format!("time authority {}", self.authority),
69 });
70 }
71 Ok(())
72 }
73
74 pub fn freshness(
77 &self,
78 now: DateTime<Utc>,
79 window: chrono::Duration,
80 grace: chrono::Duration,
81 ) -> TimeFreshness {
82 let age = now.signed_duration_since(self.attested_at);
83 if age <= window {
84 TimeFreshness::Fresh
85 } else if age <= window + grace {
86 TimeFreshness::Stale
87 } else {
88 TimeFreshness::Expired
89 }
90 }
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum TimeFreshness {
96 Fresh,
98 Stale,
100 Expired,
102}
103
104#[cfg(test)]
105mod tests {
106 use super::*;
107 use ed25519_dalek::Signer;
108
109 fn generate_key() -> ed25519_dalek::SigningKey {
110 use rand_core::RngCore;
111 let mut seed = [0u8; 32];
112 rand_core::OsRng.fill_bytes(&mut seed);
113 ed25519_dalek::SigningKey::from_bytes(&seed)
114 }
115
116 struct Ed25519Verifier;
117
118 impl SignatureVerifier for Ed25519Verifier {
119 fn verify(&self, pk: &[u8], msg: &[u8], sig: &[u8]) -> bool {
120 use ed25519_dalek::Signature;
121 use ed25519_dalek::Verifier;
122 let Ok(vk) = ed25519_dalek::VerifyingKey::from_bytes(pk.try_into().unwrap()) else {
123 return false;
124 };
125 let Ok(signature) = Signature::from_slice(sig) else {
126 return false;
127 };
128 vk.verify(msg, &signature).is_ok()
129 }
130 }
131
132 fn attestation() -> (TimeAttestation, ed25519_dalek::SigningKey) {
133 let sk = generate_key();
134 let mut a = TimeAttestation {
135 authority: "time-authority-1".into(),
136 artifact_hash: hex::encode([1u8; 32]),
137 attested_at: Utc::now(),
138 external_anchor: b"ots-proof".to_vec(),
139 signature: vec![],
140 };
141 a.signature = sk.sign(&a.signing_bytes().unwrap()).to_bytes().to_vec();
142 (a, sk)
143 }
144
145 #[test]
146 fn verifies_with_external_anchor() {
147 let (a, sk) = attestation();
148 assert!(
149 a.verify(sk.verifying_key().as_bytes(), &Ed25519Verifier)
150 .is_ok()
151 );
152 let mut stripped = a.clone();
153 stripped.external_anchor = vec![];
154 assert!(
155 stripped
156 .verify(sk.verifying_key().as_bytes(), &Ed25519Verifier)
157 .is_err()
158 );
159 }
160
161 #[test]
162 fn freshness_ladder() {
163 let (mut a, _) = attestation();
164 let window = chrono::Duration::minutes(5);
165 let grace = chrono::Duration::minutes(5);
166 a.attested_at = Utc::now() - chrono::Duration::minutes(1);
167 assert_eq!(a.freshness(Utc::now(), window, grace), TimeFreshness::Fresh);
168 a.attested_at = Utc::now() - chrono::Duration::minutes(8);
169 assert_eq!(a.freshness(Utc::now(), window, grace), TimeFreshness::Stale);
170 a.attested_at = Utc::now() - chrono::Duration::minutes(30);
171 assert_eq!(
172 a.freshness(Utc::now(), window, grace),
173 TimeFreshness::Expired
174 );
175 }
176}