Skip to main content

auths_transparency/
checkpoint.rs

1use auths_verifier::{Ed25519PublicKey, Ed25519Signature};
2use chrono::{DateTime, Utc};
3use serde::{Deserialize, Serialize};
4
5use crate::types::{LogOrigin, MerkleHash};
6
7/// An unsigned transparency log checkpoint.
8///
9/// Args:
10/// * `origin` — Log origin string (e.g., "auths.dev/log").
11/// * `size` — Number of entries in the log at this checkpoint.
12/// * `root` — Merkle root hash of the log at this size.
13/// * `timestamp` — When the checkpoint was created.
14///
15/// Usage:
16/// ```ignore
17/// let cp = Checkpoint {
18///     origin: LogOrigin::new("auths.dev/log")?,
19///     size: 42,
20///     root: merkle_root,
21///     timestamp: Utc::now(),
22/// };
23/// ```
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
25#[allow(missing_docs)]
26pub struct Checkpoint {
27    pub origin: LogOrigin,
28    pub size: u64,
29    pub root: MerkleHash,
30    pub timestamp: DateTime<Utc>,
31}
32
33impl Checkpoint {
34    /// Serialize to the C2SP checkpoint body format (three lines: origin, size, base64 hash).
35    pub fn to_note_body(&self) -> String {
36        format!(
37            "{}\n{}\n{}\n",
38            self.origin,
39            self.size,
40            self.root.to_base64()
41        )
42    }
43
44    /// Parse from C2SP checkpoint body lines.
45    pub fn from_note_body(
46        body: &str,
47        timestamp: DateTime<Utc>,
48    ) -> Result<Self, crate::error::TransparencyError> {
49        let lines: Vec<&str> = body.lines().collect();
50        if lines.len() < 3 {
51            return Err(crate::error::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].parse().map_err(|e: std::num::ParseIntError| {
57            crate::error::TransparencyError::InvalidNote(e.to_string())
58        })?;
59        let root = MerkleHash::from_base64(lines[2])?;
60        Ok(Self {
61            origin,
62            size,
63            root,
64            timestamp,
65        })
66    }
67}
68
69/// A checkpoint signed by the log operator (and optionally witnesses).
70///
71/// Args:
72/// * `checkpoint` — The unsigned checkpoint data.
73/// * `log_signature` — Ed25519 signature from the log's signing key.
74/// * `log_public_key` — The log operator's public key.
75/// * `witnesses` — Optional witness cosignatures.
76///
77/// Usage:
78/// ```ignore
79/// let signed = SignedCheckpoint {
80///     checkpoint,
81///     log_signature: sig,
82///     log_public_key: log_pk,
83///     witnesses: vec![],
84/// };
85/// ```
86#[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    /// ECDSA P-256 checkpoint signature (DER-encoded). Present when the log
95    /// uses ECDSA instead of Ed25519 (e.g., Rekor production shard).
96    #[serde(default, skip_serializing_if = "Option::is_none")]
97    pub ecdsa_checkpoint_signature: Option<auths_verifier::EcdsaP256Signature>,
98    /// ECDSA P-256 public key for checkpoint verification (PKIX DER).
99    #[serde(default, skip_serializing_if = "Option::is_none")]
100    pub ecdsa_checkpoint_key: Option<auths_verifier::EcdsaP256PublicKey>,
101}
102
103/// A witness cosignature on a checkpoint.
104///
105/// Witnesses independently verify the checkpoint and add their signature
106/// to increase trust in the log's consistency claims.
107#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
108#[allow(missing_docs)]
109pub struct WitnessCosignature {
110    pub witness_name: String,
111    pub witness_public_key: Ed25519PublicKey,
112    pub signature: Ed25519Signature,
113    pub timestamp: DateTime<Utc>,
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn checkpoint_note_body_roundtrip() {
122        let ts = chrono::DateTime::parse_from_rfc3339("2025-06-01T00:00:00Z")
123            .unwrap()
124            .with_timezone(&Utc);
125        let cp = Checkpoint {
126            origin: LogOrigin::new("auths.dev/log").unwrap(),
127            size: 42,
128            root: MerkleHash::from_bytes([0xab; 32]),
129            timestamp: ts,
130        };
131        let body = cp.to_note_body();
132        let parsed = Checkpoint::from_note_body(&body, ts).unwrap();
133        assert_eq!(cp.origin, parsed.origin);
134        assert_eq!(cp.size, parsed.size);
135        assert_eq!(cp.root, parsed.root);
136    }
137
138    #[test]
139    fn checkpoint_json_roundtrip() {
140        let ts = chrono::DateTime::parse_from_rfc3339("2025-06-01T00:00:00Z")
141            .unwrap()
142            .with_timezone(&Utc);
143        let cp = Checkpoint {
144            origin: LogOrigin::new("auths.dev/log").unwrap(),
145            size: 100,
146            root: MerkleHash::from_bytes([0x01; 32]),
147            timestamp: ts,
148        };
149        let json = serde_json::to_string(&cp).unwrap();
150        let back: Checkpoint = serde_json::from_str(&json).unwrap();
151        assert_eq!(cp, back);
152    }
153}