use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::error::TransparencyError;
use super::types::{LogOrigin, MerkleHash};
use crate::core::{EcdsaP256PublicKey, EcdsaP256Signature, Ed25519PublicKey, Ed25519Signature};
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[allow(missing_docs)]
pub struct Checkpoint {
pub origin: LogOrigin,
pub size: u64,
pub root: MerkleHash,
pub timestamp: DateTime<Utc>,
}
impl Checkpoint {
pub fn to_note_body(&self) -> String {
format!(
"{}\n{}\n{}\n",
self.origin,
self.size,
self.root.to_base64()
)
}
pub fn from_note_body(body: &str, timestamp: DateTime<Utc>) -> Result<Self, TransparencyError> {
let lines: Vec<&str> = body.lines().collect();
if lines.len() < 3 {
return Err(TransparencyError::InvalidNote(
"checkpoint body must have at least 3 lines".into(),
));
}
let origin = LogOrigin::new(lines[0])?;
let size: u64 = lines[1]
.parse()
.map_err(|e: std::num::ParseIntError| TransparencyError::InvalidNote(e.to_string()))?;
let root = MerkleHash::from_base64(lines[2])?;
Ok(Self {
origin,
size,
root,
timestamp,
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[allow(missing_docs)]
pub struct SignedCheckpoint {
pub checkpoint: Checkpoint,
pub log_signature: Ed25519Signature,
pub log_public_key: Ed25519PublicKey,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub witnesses: Vec<WitnessCosignature>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ecdsa_checkpoint_signature: Option<EcdsaP256Signature>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ecdsa_checkpoint_key: Option<EcdsaP256PublicKey>,
}
impl SignedCheckpoint {
pub fn verify_log_signature(
&self,
pinned_log_key: &Ed25519PublicKey,
) -> Result<(), TransparencyError> {
use subtle::ConstantTimeEq;
let key_is_pinned: bool = self
.log_public_key
.as_bytes()
.ct_eq(pinned_log_key.as_bytes())
.into();
if !key_is_pinned {
return Err(TransparencyError::InvalidCheckpointSignature);
}
let verifying_key = ed25519_dalek::VerifyingKey::from_bytes(pinned_log_key.as_bytes())
.map_err(|_| TransparencyError::InvalidCheckpointSignature)?;
let signature = ed25519_dalek::Signature::from_bytes(self.log_signature.as_bytes());
verifying_key
.verify_strict(self.checkpoint.to_note_body().as_bytes(), &signature)
.map_err(|_| TransparencyError::InvalidCheckpointSignature)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[allow(missing_docs)]
pub struct WitnessCosignature {
pub witness_name: String,
pub witness_public_key: Ed25519PublicKey,
pub signature: Ed25519Signature,
pub timestamp: DateTime<Utc>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn checkpoint_note_body_roundtrip() {
let ts = chrono::DateTime::parse_from_rfc3339("2025-06-01T00:00:00Z")
.unwrap()
.with_timezone(&Utc);
let cp = Checkpoint {
origin: LogOrigin::new("auths.dev/log").unwrap(),
size: 42,
root: MerkleHash::from_bytes([0xab; 32]),
timestamp: ts,
};
let body = cp.to_note_body();
let parsed = Checkpoint::from_note_body(&body, ts).unwrap();
assert_eq!(cp.origin, parsed.origin);
assert_eq!(cp.size, parsed.size);
assert_eq!(cp.root, parsed.root);
}
fn signed_by(signing_key: &ed25519_dalek::SigningKey) -> SignedCheckpoint {
use ed25519_dalek::Signer;
let ts = chrono::DateTime::parse_from_rfc3339("2025-06-01T00:00:00Z")
.unwrap()
.with_timezone(&Utc);
let checkpoint = Checkpoint {
origin: LogOrigin::new("auths.dev/log").unwrap(),
size: 42,
root: MerkleHash::from_bytes([0xab; 32]),
timestamp: ts,
};
let signature = signing_key.sign(checkpoint.to_note_body().as_bytes());
SignedCheckpoint {
checkpoint,
log_signature: Ed25519Signature::from_bytes(signature.to_bytes()),
log_public_key: Ed25519PublicKey::from_bytes(signing_key.verifying_key().to_bytes()),
witnesses: vec![],
ecdsa_checkpoint_signature: None,
ecdsa_checkpoint_key: None,
}
}
#[test]
fn log_signature_verifies_under_the_pinned_key() {
let signing_key = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
let signed = signed_by(&signing_key);
let pinned = Ed25519PublicKey::from_bytes(signing_key.verifying_key().to_bytes());
signed
.verify_log_signature(&pinned)
.expect("operator-signed checkpoint verifies under its pinned key");
}
#[test]
fn log_signature_rejects_a_different_pinned_operator() {
let signing_key = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
let signed = signed_by(&signing_key);
let other = ed25519_dalek::SigningKey::from_bytes(&[9u8; 32]);
let pinned = Ed25519PublicKey::from_bytes(other.verifying_key().to_bytes());
assert!(
signed.verify_log_signature(&pinned).is_err(),
"a checkpoint from a different operator must fail the pinned-key check"
);
}
#[test]
fn log_signature_rejects_a_forged_signature() {
let signing_key = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
let mut signed = signed_by(&signing_key);
signed.log_signature = Ed25519Signature::from_bytes([0x42; 64]);
let pinned = Ed25519PublicKey::from_bytes(signing_key.verifying_key().to_bytes());
assert!(
signed.verify_log_signature(&pinned).is_err(),
"a forged checkpoint signature must fail closed"
);
}
#[test]
fn log_signature_rejects_a_tampered_checkpoint_body() {
let signing_key = ed25519_dalek::SigningKey::from_bytes(&[7u8; 32]);
let mut signed = signed_by(&signing_key);
signed.checkpoint.root = MerkleHash::from_bytes([0xcd; 32]);
let pinned = Ed25519PublicKey::from_bytes(signing_key.verifying_key().to_bytes());
assert!(
signed.verify_log_signature(&pinned).is_err(),
"a rewritten checkpoint body must not verify under the old signature"
);
}
#[test]
fn checkpoint_json_roundtrip() {
let ts = chrono::DateTime::parse_from_rfc3339("2025-06-01T00:00:00Z")
.unwrap()
.with_timezone(&Utc);
let cp = Checkpoint {
origin: LogOrigin::new("auths.dev/log").unwrap(),
size: 100,
root: MerkleHash::from_bytes([0x01; 32]),
timestamp: ts,
};
let json = serde_json::to_string(&cp).unwrap();
let back: Checkpoint = serde_json::from_str(&json).unwrap();
assert_eq!(cp, back);
}
}