treeship_core/merkle/
checkpoint.rs1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct Checkpoint {
13 pub index: u64,
14 pub root: String,
16 pub tree_size: usize,
17 pub height: usize,
18 pub signed_at: String,
20 pub signer: String,
22 pub public_key: String,
24 pub signature: String,
26 #[serde(default, skip_serializing_if = "Option::is_none")]
28 pub algorithm: Option<String>,
29 #[serde(default, skip_serializing_if = "Option::is_none")]
31 pub zk_proof: Option<ChainProofSummary>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ChainProofSummary {
37 pub image_id: String,
38 pub all_signatures_valid: bool,
39 pub chain_intact: bool,
40 pub approval_nonces_matched: bool,
41 pub artifact_count: u64,
42 pub public_key_digest: String,
43 pub proved_at: String,
44}
45
46#[derive(Debug)]
48pub enum CheckpointError {
49 EmptyTree,
50 Signing(SignerError),
51}
52
53impl std::fmt::Display for CheckpointError {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 match self {
56 Self::EmptyTree => write!(f, "cannot checkpoint an empty tree"),
57 Self::Signing(e) => write!(f, "checkpoint signing failed: {}", e),
58 }
59 }
60}
61
62impl std::error::Error for CheckpointError {}
63impl From<SignerError> for CheckpointError {
64 fn from(e: SignerError) -> Self {
65 Self::Signing(e)
66 }
67}
68
69impl Checkpoint {
70 pub fn create(
74 index: u64,
75 tree: &MerkleTree,
76 signer: &dyn Signer,
77 ) -> Result<Self, CheckpointError> {
78 let root_bytes = tree.root().ok_or(CheckpointError::EmptyTree)?;
79 let root = format!("sha256:{}", hex::encode(root_bytes));
80
81 let secs = std::time::SystemTime::now()
82 .duration_since(std::time::UNIX_EPOCH)
83 .unwrap_or_default()
84 .as_secs();
85 let signed_at = unix_to_rfc3339(secs);
86
87 let canonical = format!("{}|{}|{}|{}|{}|{}", index, root, tree.len(), tree.height(), signer.key_id(), signed_at);
88 let sig_bytes = signer.sign(canonical.as_bytes())?;
89 let signature = URL_SAFE_NO_PAD.encode(&sig_bytes);
90 let public_key = URL_SAFE_NO_PAD.encode(signer.public_key_bytes());
91
92 Ok(Self {
93 index,
94 root,
95 tree_size: tree.len(),
96 height: tree.height(),
97 signed_at,
98 signer: signer.key_id().to_string(),
99 public_key,
100 signature,
101 algorithm: Some(super::tree::MERKLE_ALGORITHM_V2.to_string()),
102 zk_proof: None,
103 })
104 }
105
106 pub fn verify(&self) -> bool {
109 let pub_bytes = match URL_SAFE_NO_PAD.decode(&self.public_key) {
110 Ok(b) => b,
111 Err(_) => return false,
112 };
113 let pub_array: [u8; 32] = match pub_bytes.as_slice().try_into() {
114 Ok(a) => a,
115 Err(_) => return false,
116 };
117 let vk = match VerifyingKey::from_bytes(&pub_array) {
118 Ok(k) => k,
119 Err(_) => return false,
120 };
121
122 let canonical = format!("{}|{}|{}|{}|{}|{}", self.index, self.root, self.tree_size, self.height, self.signer, self.signed_at);
123
124 let sig_bytes = match URL_SAFE_NO_PAD.decode(&self.signature) {
125 Ok(b) => b,
126 Err(_) => return false,
127 };
128 let sig_array: [u8; 64] = match sig_bytes.as_slice().try_into() {
129 Ok(a) => a,
130 Err(_) => return false,
131 };
132 let sig = Signature::from_bytes(&sig_array);
133
134 vk.verify(canonical.as_bytes(), &sig).is_ok()
135 }
136}