Skip to main content

treeship_core/merkle/
checkpoint.rs

1use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
2use ed25519_dalek::{Signature, Verifier, VerifyingKey};
3use serde::{Deserialize, Serialize};
4
5use crate::attestation::{Signer, SignerError};
6use crate::statements::unix_to_rfc3339;
7
8use super::tree::MerkleTree;
9
10/// A signed snapshot of the Merkle tree at a point in time.
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Checkpoint {
13    pub index: u64,
14    /// Root hash in `sha256:<hex>` format.
15    pub root: String,
16    pub tree_size: usize,
17    pub height: usize,
18    /// RFC 3339 timestamp.
19    pub signed_at: String,
20    /// Key ID of the signer.
21    pub signer: String,
22    /// Base64url-encoded public key bytes.
23    pub public_key: String,
24    /// Base64url-encoded Ed25519 signature of the canonical form.
25    pub signature: String,
26}
27
28/// Errors from checkpoint creation.
29#[derive(Debug)]
30pub enum CheckpointError {
31    EmptyTree,
32    Signing(SignerError),
33}
34
35impl std::fmt::Display for CheckpointError {
36    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
37        match self {
38            Self::EmptyTree => write!(f, "cannot checkpoint an empty tree"),
39            Self::Signing(e) => write!(f, "checkpoint signing failed: {}", e),
40        }
41    }
42}
43
44impl std::error::Error for CheckpointError {}
45impl From<SignerError> for CheckpointError {
46    fn from(e: SignerError) -> Self {
47        Self::Signing(e)
48    }
49}
50
51impl Checkpoint {
52    /// Create a signed checkpoint from the current tree state.
53    ///
54    /// The canonical form for signing is: `{root}|{tree_size}|{signed_at}`
55    pub fn create(
56        index: u64,
57        tree: &MerkleTree,
58        signer: &dyn Signer,
59    ) -> Result<Self, CheckpointError> {
60        let root_bytes = tree.root().ok_or(CheckpointError::EmptyTree)?;
61        let root = format!("sha256:{}", hex::encode(root_bytes));
62
63        let secs = std::time::SystemTime::now()
64            .duration_since(std::time::UNIX_EPOCH)
65            .unwrap_or_default()
66            .as_secs();
67        let signed_at = unix_to_rfc3339(secs);
68
69        let canonical = format!("{}|{}|{}", root, tree.len(), signed_at);
70        let sig_bytes = signer.sign(canonical.as_bytes())?;
71        let signature = URL_SAFE_NO_PAD.encode(&sig_bytes);
72        let public_key = URL_SAFE_NO_PAD.encode(signer.public_key_bytes());
73
74        Ok(Self {
75            index,
76            root,
77            tree_size: tree.len(),
78            height: tree.height(),
79            signed_at,
80            signer: signer.key_id().to_string(),
81            public_key,
82            signature,
83        })
84    }
85
86    /// Verify the checkpoint signature. Returns `false` on any failure
87    /// (bad encoding, wrong key size, invalid signature). Never panics.
88    pub fn verify(&self) -> bool {
89        let pub_bytes = match URL_SAFE_NO_PAD.decode(&self.public_key) {
90            Ok(b) => b,
91            Err(_) => return false,
92        };
93        let pub_array: [u8; 32] = match pub_bytes.as_slice().try_into() {
94            Ok(a) => a,
95            Err(_) => return false,
96        };
97        let vk = match VerifyingKey::from_bytes(&pub_array) {
98            Ok(k) => k,
99            Err(_) => return false,
100        };
101
102        let canonical = format!("{}|{}|{}", self.root, self.tree_size, self.signed_at);
103
104        let sig_bytes = match URL_SAFE_NO_PAD.decode(&self.signature) {
105            Ok(b) => b,
106            Err(_) => return false,
107        };
108        let sig_array: [u8; 64] = match sig_bytes.as_slice().try_into() {
109            Ok(a) => a,
110            Err(_) => return false,
111        };
112        let sig = Signature::from_bytes(&sig_array);
113
114        vk.verify(canonical.as_bytes(), &sig).is_ok()
115    }
116}