Skip to main content

confium_signatif/
time.rs

1//! The time dimension: external time anchoring (SIGNATIF ยง8.8).
2//!
3//! The time dimension is attested by a **time key** โ€” a co-signature
4//! from a time authority recording that the artifact hash existed at a
5//! stated time, anchored to an external, irrefutable time source
6//! (OpenTimestamps commitments in Confium). The signer's self-asserted
7//! timestamp is never the sole evidence.
8
9use chrono::{DateTime, Utc};
10use serde::{Deserialize, Serialize};
11
12use crate::error::{SignatifError, SignatifResult};
13use crate::graph::SignatureVerifier;
14use crate::jcs;
15
16/// A time authority's attestation over an artifact.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct TimeAttestation {
19    /// The time authority's identifier.
20    pub authority: String,
21    /// The artifact's canonical payload hash (hex).
22    pub artifact_hash: String,
23    /// When the authority attests the artifact existed.
24    pub attested_at: DateTime<Utc>,
25    /// The external anchor: serialized OpenTimestamps proof (or
26    /// equivalent) binding the hash to an irrefutable time source.
27    pub external_anchor: Vec<u8>,
28    /// The time authority's signature over the attestation body.
29    pub signature: Vec<u8>,
30}
31
32impl TimeAttestation {
33    /// The canonical signing bytes.
34    ///
35    /// # Errors
36    ///
37    /// Propagates canonicalization errors.
38    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    /// Verify the attestation: the authority's signature, and that an
49    /// external anchor is present (the anchor itself is verified
50    /// against the time source by the OTS layer).
51    ///
52    /// # Errors
53    ///
54    /// Signature or anchor errors.
55    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    /// Freshness against a window: fresh inside `window`, stale
75    /// (downgrade) inside `window + grace`, rejected beyond.
76    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/// Freshness outcome for the pipeline.
94#[derive(Debug, Clone, Copy, PartialEq, Eq)]
95pub enum TimeFreshness {
96    /// Inside the window.
97    Fresh,
98    /// Inside the grace period โ€” downgrade.
99    Stale,
100    /// Beyond grace โ€” reject.
101    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}