auths_verifier/tlog/
checkpoint.rs1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5
6use super::error::TransparencyError;
7use super::types::{LogOrigin, MerkleHash};
8use crate::core::{EcdsaP256PublicKey, EcdsaP256Signature, Ed25519PublicKey, Ed25519Signature};
9
10#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
28#[allow(missing_docs)]
29pub struct Checkpoint {
30 pub origin: LogOrigin,
31 pub size: u64,
32 pub root: MerkleHash,
33 pub timestamp: DateTime<Utc>,
34}
35
36impl Checkpoint {
37 pub fn to_note_body(&self) -> String {
39 format!(
40 "{}\n{}\n{}\n",
41 self.origin,
42 self.size,
43 self.root.to_base64()
44 )
45 }
46
47 pub fn from_note_body(body: &str, timestamp: DateTime<Utc>) -> Result<Self, TransparencyError> {
49 let lines: Vec<&str> = body.lines().collect();
50 if lines.len() < 3 {
51 return Err(TransparencyError::InvalidNote(
52 "checkpoint body must have at least 3 lines".into(),
53 ));
54 }
55 let origin = LogOrigin::new(lines[0])?;
56 let size: u64 = lines[1]
57 .parse()
58 .map_err(|e: std::num::ParseIntError| TransparencyError::InvalidNote(e.to_string()))?;
59 let root = MerkleHash::from_base64(lines[2])?;
60 Ok(Self {
61 origin,
62 size,
63 root,
64 timestamp,
65 })
66 }
67}
68
69#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
87#[allow(missing_docs)]
88pub struct SignedCheckpoint {
89 pub checkpoint: Checkpoint,
90 pub log_signature: Ed25519Signature,
91 pub log_public_key: Ed25519PublicKey,
92 #[serde(default, skip_serializing_if = "Vec::is_empty")]
93 pub witnesses: Vec<WitnessCosignature>,
94 #[serde(default, skip_serializing_if = "Option::is_none")]
97 pub ecdsa_checkpoint_signature: Option<EcdsaP256Signature>,
98 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub ecdsa_checkpoint_key: Option<EcdsaP256PublicKey>,
101}
102
103impl SignedCheckpoint {
104 pub fn verify_log_signature(
127 &self,
128 pinned_log_key: &Ed25519PublicKey,
129 ) -> Result<(), TransparencyError> {
130 use subtle::ConstantTimeEq;
131 let key_is_pinned: bool = self
132 .log_public_key
133 .as_bytes()
134 .ct_eq(pinned_log_key.as_bytes())
135 .into();
136 if !key_is_pinned {
137 return Err(TransparencyError::InvalidCheckpointSignature);
138 }
139 let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(pinned_log_key.as_bytes())
140 .map_err(|_| TransparencyError::InvalidCheckpointSignature)?;
141 let signature = ed25519_dalek::Signature::from_bytes(self.log_signature.as_bytes());
142 verifying_key
143 .verify_strict(self.checkpoint.to_note_body().as_bytes(), &signature)
144 .map_err(|_| TransparencyError::InvalidCheckpointSignature)
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
153#[allow(missing_docs)]
154pub struct WitnessCosignature {
155 pub witness_name: String,
156 pub witness_public_key: Ed25519PublicKey,
157 pub signature: Ed25519Signature,
158 pub timestamp: DateTime<Utc>,
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164
165 #[test]
166 fn checkpoint_note_body_roundtrip() {
167 let ts = chrono::DateTime::parse_from_rfc3339("2025-06-01T00:00:00Z")
168 .unwrap()
169 .with_timezone(&Utc);
170 let cp = Checkpoint {
171 origin: LogOrigin::new("auths.dev/log").unwrap(),
172 size: 42,
173 root: MerkleHash::from_bytes([0xab; 32]),
174 timestamp: ts,
175 };
176 let body = cp.to_note_body();
177 let parsed = Checkpoint::from_note_body(&body, ts).unwrap();
178 assert_eq!(cp.origin, parsed.origin);
179 assert_eq!(cp.size, parsed.size);
180 assert_eq!(cp.root, parsed.root);
181 }
182
183 fn signed_by(signing_key: &ed25519_dalek::SigningKey) -> SignedCheckpoint {
184 use ed25519_dalek::Signer;
185 let ts = chrono::DateTime::parse_from_rfc3339("2025-06-01T00:00:00Z")
186 .unwrap()
187 .with_timezone(&Utc);
188 let checkpoint = Checkpoint {
189 origin: LogOrigin::new("auths.dev/log").unwrap(),
190 size: 42,
191 root: MerkleHash::from_bytes([0xab; 32]),
192 timestamp: ts,
193 };
194 let signature = signing_key.sign(checkpoint.to_note_body().as_bytes());
195 SignedCheckpoint {
196 checkpoint,
197 log_signature: Ed25519Signature::from_bytes(signature.to_bytes()),
198 log_public_key: Ed25519PublicKey::from_bytes(signing_key.verifying_key().to_bytes()),
199 witnesses: vec![],
200 ecdsa_checkpoint_signature: None,
201 ecdsa_checkpoint_key: None,
202 }
203 }
204
205 #[test]
206 fn log_signature_verifies_under_the_pinned_key() {
207 let signing_key = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
208 let signed = signed_by(&signing_key);
209 let pinned = Ed25519PublicKey::from_bytes(signing_key.verifying_key().to_bytes());
210 signed
211 .verify_log_signature(&pinned)
212 .expect("operator-signed checkpoint verifies under its pinned key");
213 }
214
215 #[test]
216 fn log_signature_rejects_a_different_pinned_operator() {
217 let signing_key = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
218 let signed = signed_by(&signing_key);
219 let other = ed25519_dalek::SigningKey::from_bytes(&[9u8; 32]);
220 let pinned = Ed25519PublicKey::from_bytes(other.verifying_key().to_bytes());
221 assert!(
222 signed.verify_log_signature(&pinned).is_err(),
223 "a checkpoint from a different operator must fail the pinned-key check"
224 );
225 }
226
227 #[test]
228 fn log_signature_rejects_a_forged_signature() {
229 let signing_key = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
230 let mut signed = signed_by(&signing_key);
231 signed.log_signature = Ed25519Signature::from_bytes([0x42; 64]);
233 let pinned = Ed25519PublicKey::from_bytes(signing_key.verifying_key().to_bytes());
234 assert!(
235 signed.verify_log_signature(&pinned).is_err(),
236 "a forged checkpoint signature must fail closed"
237 );
238 }
239
240 #[test]
241 fn log_signature_rejects_a_tampered_checkpoint_body() {
242 let signing_key = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
243 let mut signed = signed_by(&signing_key);
244 signed.checkpoint.root = MerkleHash::from_bytes([0xcd; 32]);
246 let pinned = Ed25519PublicKey::from_bytes(signing_key.verifying_key().to_bytes());
247 assert!(
248 signed.verify_log_signature(&pinned).is_err(),
249 "a rewritten checkpoint body must not verify under the old signature"
250 );
251 }
252
253 #[test]
254 fn checkpoint_json_roundtrip() {
255 let ts = chrono::DateTime::parse_from_rfc3339("2025-06-01T00:00:00Z")
256 .unwrap()
257 .with_timezone(&Utc);
258 let cp = Checkpoint {
259 origin: LogOrigin::new("auths.dev/log").unwrap(),
260 size: 100,
261 root: MerkleHash::from_bytes([0x01; 32]),
262 timestamp: ts,
263 };
264 let json = serde_json::to_string(&cp).unwrap();
265 let back: Checkpoint = serde_json::from_str(&json).unwrap();
266 assert_eq!(cp, back);
267 }
268}