use crate::checkpoint::{CosignedCheckpoint, KeyId};
use crate::error::{Error, Result};
use base64::Engine;
use ed25519_dalek::{Verifier, VerifyingKey};
use sha2::{Digest, Sha256};
const ALG_ED25519: u8 = 0x01;
#[derive(Debug, Clone)]
pub struct LogConfig {
pub origin: String,
pub vkey: String,
pub url: Option<String>,
verifying_key: VerifyingKey,
key_id: KeyId,
key_name: String,
}
impl LogConfig {
pub fn new(origin: String, vkey: &str) -> Result<Self> {
let (key_name, key_id, verifying_key) = parse_vkey(vkey)?;
Ok(Self {
origin,
vkey: vkey.to_string(),
url: None,
verifying_key,
key_id,
key_name,
})
}
pub fn with_url(origin: String, vkey: &str, url: String) -> Result<Self> {
let mut config = Self::new(origin, vkey)?;
config.url = Some(url);
Ok(config)
}
pub fn public_key(&self) -> &VerifyingKey {
&self.verifying_key
}
pub fn key_name(&self) -> &str {
&self.key_name
}
}
pub struct CheckpointVerifier {
config: LogConfig,
}
impl CheckpointVerifier {
pub fn new(config: LogConfig) -> Self {
Self { config }
}
pub fn verify(&self, checkpoint: &CosignedCheckpoint) -> Result<()> {
if checkpoint.checkpoint.origin.as_str() != self.config.origin {
return Err(Error::Config(format!(
"origin mismatch: expected '{}', got '{}'",
self.config.origin,
checkpoint.checkpoint.origin.as_str()
)));
}
let log_sig = checkpoint
.signatures
.iter()
.find(|s| s.key_id == self.config.key_id || s.name.as_str() == self.config.key_name);
let log_sig = log_sig.ok_or_else(|| {
Error::Config(format!(
"no signature from log '{}' found",
self.config.key_name
))
})?;
let body = checkpoint.checkpoint.to_body();
self.config
.verifying_key
.verify(body.as_bytes(), &log_sig.signature)
.map_err(|e| Error::Signing(format!("signature verification failed: {}", e)))?;
Ok(())
}
}
fn parse_vkey(vkey: &str) -> Result<(String, KeyId, VerifyingKey)> {
let parts: Vec<&str> = vkey.trim().splitn(3, '+').collect();
if parts.len() != 3 {
return Err(Error::Config(format!(
"invalid vkey format: expected 'name+hash+base64', got '{}'",
vkey
)));
}
let name = parts[0].to_string();
let hash_hex = parts[1];
let key_base64 = parts[2];
if hash_hex.len() != 8 {
return Err(Error::Config(format!(
"invalid hash length: expected 8 hex chars, got {}",
hash_hex.len()
)));
}
let expected_hash =
u32::from_str_radix(hash_hex, 16).map_err(|_| Error::Config("invalid hash hex".into()))?;
let key_data = base64::engine::general_purpose::STANDARD
.decode(key_base64)
.map_err(|e| Error::Config(format!("invalid key base64: {}", e)))?;
if key_data.len() != 33 {
return Err(Error::Config(format!(
"invalid key length: expected 33, got {}",
key_data.len()
)));
}
if key_data[0] != ALG_ED25519 {
return Err(Error::Config(format!(
"unsupported algorithm: expected {}, got {}",
ALG_ED25519, key_data[0]
)));
}
let pubkey_bytes: [u8; 32] = key_data[1..33]
.try_into()
.map_err(|_| Error::Config("invalid pubkey length".into()))?;
let verifying_key = VerifyingKey::from_bytes(&pubkey_bytes)
.map_err(|e| Error::Config(format!("invalid public key: {}", e)))?;
let key_id = compute_key_id(&name, &verifying_key);
if key_id.as_u32() != expected_hash {
return Err(Error::Config(format!(
"key hash mismatch: expected {:08x}, computed {:08x}",
expected_hash,
key_id.as_u32()
)));
}
Ok((name, key_id, verifying_key))
}
fn compute_key_id(name: &str, key: &VerifyingKey) -> KeyId {
let mut hasher = Sha256::new();
hasher.update(name.as_bytes());
hasher.update(b"\n");
hasher.update([ALG_ED25519]);
hasher.update(key.as_bytes());
let hash = hasher.finalize();
KeyId::new([hash[0], hash[1], hash[2], hash[3]])
}
#[cfg(test)]
mod tests {
use super::*;
use crate::checkpoint::CheckpointSigner;
#[test]
fn test_parse_vkey_roundtrip() {
let signer = CheckpointSigner::generate("test.example.com");
let note_key = signer.to_note_key();
let parts: Vec<&str> = note_key.splitn(5, '+').collect();
assert_eq!(parts.len(), 5);
let name = parts[2];
let hash = parts[3];
let pubkey = signer.public_key();
let mut key_data = Vec::with_capacity(33);
key_data.push(ALG_ED25519);
key_data.extend_from_slice(pubkey.as_bytes());
let vkey = format!(
"{}+{}+{}",
name,
hash,
base64::engine::general_purpose::STANDARD.encode(&key_data)
);
let (parsed_name, parsed_id, parsed_key) = parse_vkey(&vkey).unwrap();
assert_eq!(parsed_name, name);
assert_eq!(parsed_id.as_u32(), signer.key_id().as_u32());
assert_eq!(parsed_key.as_bytes(), pubkey.as_bytes());
}
}