use auths_crypto::{CurveType, TypedSeed, TypedSignerKey};
use auths_verifier::evidence_pack::TransparencyInclusion;
use auths_verifier::{Ed25519PublicKey, Ed25519Signature};
use chrono::{DateTime, Utc};
use crate::checkpoint::{Checkpoint, SignedCheckpoint};
use crate::error::TransparencyError;
use crate::merkle::{compute_root, prove_inclusion};
use crate::proof::InclusionProof;
use crate::store::TileStore;
use crate::tile::{TILE_WIDTH, leaf_tile, tile_count, tile_path};
use crate::types::{LogOrigin, MerkleHash};
pub struct LogSigningKey {
signer: TypedSignerKey,
public_key: Ed25519PublicKey,
}
impl LogSigningKey {
pub fn generate() -> Result<Self, TransparencyError> {
use ring::rand::SecureRandom;
let rng = ring::rand::SystemRandom::new();
let mut seed = [0u8; 32];
rng.fill(&mut seed)
.map_err(|_| TransparencyError::SigningKey("OS randomness unavailable".into()))?;
let signer = TypedSignerKey::from_seed(TypedSeed::Ed25519(seed))
.map_err(|e| TransparencyError::SigningKey(e.to_string()))?;
Self::from_signer(signer)
}
pub fn from_pkcs8_der(der: &[u8]) -> Result<Self, TransparencyError> {
let signer = TypedSignerKey::from_pkcs8(der)
.map_err(|e| TransparencyError::SigningKey(e.to_string()))?;
Self::from_signer(signer)
}
fn from_signer(signer: TypedSignerKey) -> Result<Self, TransparencyError> {
if signer.curve() != CurveType::Ed25519 {
return Err(TransparencyError::SigningKey(
"checkpoint signing key must be Ed25519 (C2SP signed-note pins the curve)".into(),
));
}
let public_key = Ed25519PublicKey::try_from_slice(signer.public_key())
.map_err(|e| TransparencyError::SigningKey(e.to_string()))?;
Ok(Self { signer, public_key })
}
pub fn to_pkcs8_der(&self) -> Result<Vec<u8>, TransparencyError> {
Ok(self
.signer
.to_pkcs8()
.map_err(|e| TransparencyError::SigningKey(e.to_string()))?
.as_ref()
.to_vec())
}
pub fn public_key(&self) -> Ed25519PublicKey {
self.public_key
}
}
#[derive(Debug, Clone)]
pub struct AppendedLeaf {
pub index: u64,
pub signed_checkpoint: SignedCheckpoint,
}
pub struct LogWriter<S: TileStore> {
store: S,
key: LogSigningKey,
origin: LogOrigin,
}
impl<S: TileStore> LogWriter<S> {
pub fn new(store: S, key: LogSigningKey, origin: LogOrigin) -> Self {
Self { store, key, origin }
}
pub async fn append(
&self,
leaf_hash: MerkleHash,
now: DateTime<Utc>,
) -> Result<AppendedLeaf, TransparencyError> {
let mut leaves = match self.read_state().await? {
Some((_, leaves)) => leaves,
None => Vec::new(),
};
let index = leaves.len() as u64;
leaves.push(leaf_hash);
let (tile_index, offset) = leaf_tile(index);
let width = offset + 1;
let start = usize::try_from(tile_index * TILE_WIDTH)
.map_err(|_| TransparencyError::StoreError("tile index out of range".into()))?;
let mut data = Vec::with_capacity((width as usize) * 32);
for leaf in &leaves[start..start + width as usize] {
data.extend_from_slice(leaf.as_bytes());
}
let path = tile_path(0, tile_index, width % TILE_WIDTH)?;
self.store.write_tile(&path, &data).await?;
let checkpoint = Checkpoint {
origin: self.origin.clone(),
size: leaves.len() as u64,
root: compute_root(&leaves),
timestamp: now,
};
let signed_checkpoint = self.sign(checkpoint)?;
let bytes = serde_json::to_vec(&signed_checkpoint)
.map_err(|e| TransparencyError::StoreError(e.to_string()))?;
self.store.write_checkpoint(&bytes).await?;
Ok(AppendedLeaf {
index,
signed_checkpoint,
})
}
pub async fn prove(
&self,
leaf_hash: &MerkleHash,
) -> Result<TransparencyInclusion, TransparencyError> {
let Some((signed_checkpoint, leaves)) = self.read_state().await? else {
return Err(TransparencyError::InvalidProof(
"the log is empty — nothing has been appended".into(),
));
};
let index = leaves
.iter()
.position(|leaf| leaf == leaf_hash)
.ok_or_else(|| {
TransparencyError::InvalidProof("leaf is not in the log — append it first".into())
})? as u64;
let inclusion_proof = InclusionProof {
index,
size: signed_checkpoint.checkpoint.size,
root: signed_checkpoint.checkpoint.root,
hashes: prove_inclusion(&leaves, index)?,
};
inclusion_proof.verify(leaf_hash)?;
Ok(TransparencyInclusion {
leaf_hash: *leaf_hash,
inclusion_proof,
signed_checkpoint,
consistency_proof: None,
})
}
async fn read_state(
&self,
) -> Result<Option<(SignedCheckpoint, Vec<MerkleHash>)>, TransparencyError> {
let Some(bytes) = self.store.read_checkpoint().await? else {
return Ok(None);
};
let signed: SignedCheckpoint = serde_json::from_slice(&bytes)
.map_err(|e| TransparencyError::StoreError(format!("checkpoint parse: {e}")))?;
if signed.checkpoint.origin != self.origin {
return Err(TransparencyError::InvalidOrigin(format!(
"log belongs to origin '{}', not '{}'",
signed.checkpoint.origin, self.origin
)));
}
let leaves = self.read_leaves(signed.checkpoint.size).await?;
let recomputed = compute_root(&leaves);
if recomputed != signed.checkpoint.root {
return Err(TransparencyError::RootMismatch {
expected: signed.checkpoint.root.to_string(),
actual: recomputed.to_string(),
});
}
Ok(Some((signed, leaves)))
}
async fn read_leaves(&self, size: u64) -> Result<Vec<MerkleHash>, TransparencyError> {
let (full_tiles, partial_width) = tile_count(size);
let mut leaves = Vec::with_capacity(size as usize);
for tile_index in 0..full_tiles {
let path = tile_path(0, tile_index, 0)?;
let data = self.store.read_tile(&path).await?;
parse_leaf_tile(&data, TILE_WIDTH, &path, &mut leaves)?;
}
if partial_width > 0 {
let path = tile_path(0, full_tiles, partial_width)?;
let data = self.store.read_tile(&path).await?;
parse_leaf_tile(&data, partial_width, &path, &mut leaves)?;
}
Ok(leaves)
}
fn sign(&self, checkpoint: Checkpoint) -> Result<SignedCheckpoint, TransparencyError> {
let body = checkpoint.to_note_body();
let signature = self
.key
.signer
.sign(body.as_bytes())
.map_err(|e| TransparencyError::SigningKey(e.to_string()))?;
let log_signature = Ed25519Signature::try_from_slice(&signature)
.map_err(|e| TransparencyError::SigningKey(e.to_string()))?;
Ok(SignedCheckpoint {
checkpoint,
log_signature,
log_public_key: self.key.public_key,
witnesses: Vec::new(),
ecdsa_checkpoint_signature: None,
ecdsa_checkpoint_key: None,
})
}
}
fn parse_leaf_tile(
data: &[u8],
width: u64,
path: &str,
out: &mut Vec<MerkleHash>,
) -> Result<(), TransparencyError> {
let expected = (width as usize) * 32;
if data.len() != expected {
return Err(TransparencyError::StoreError(format!(
"tile {path}: expected {expected} bytes ({width} hashes), got {}",
data.len()
)));
}
for chunk in data.chunks_exact(32) {
let mut bytes = [0u8; 32];
bytes.copy_from_slice(chunk);
out.push(MerkleHash::from_bytes(bytes));
}
Ok(())
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
use crate::fs_store::FsTileStore;
use crate::merkle::hash_leaf;
use auths_verifier::evidence_pack::verify_transparency_inclusion;
fn fixed_now() -> DateTime<Utc> {
DateTime::parse_from_rfc3339("2026-06-12T00:00:00Z")
.unwrap()
.with_timezone(&Utc)
}
fn writer_in(dir: &std::path::Path) -> LogWriter<FsTileStore> {
LogWriter::new(
FsTileStore::new(dir.to_path_buf()),
LogSigningKey::generate().unwrap(),
LogOrigin::new("test.example/log").unwrap(),
)
}
#[tokio::test]
async fn append_then_prove_roundtrips_through_the_verifier() {
let dir = tempfile::tempdir().unwrap();
let writer = writer_in(dir.path());
let digests = ["sha256:aa", "sha256:bb", "sha256:cc"];
for (i, d) in digests.iter().enumerate() {
let appended = writer
.append(hash_leaf(d.as_bytes()), fixed_now())
.await
.unwrap();
assert_eq!(appended.index, i as u64);
assert_eq!(appended.signed_checkpoint.checkpoint.size, i as u64 + 1);
}
for d in digests {
let leaf = hash_leaf(d.as_bytes());
let inclusion = writer.prove(&leaf).await.unwrap();
assert_eq!(inclusion.signed_checkpoint.checkpoint.size, 3);
verify_transparency_inclusion(&inclusion)
.expect("the verifier the browser/CLI runs must accept the writer's evidence");
}
}
#[tokio::test]
async fn checkpoint_signature_verifies_against_the_log_public_key() {
let dir = tempfile::tempdir().unwrap();
let writer = writer_in(dir.path());
let appended = writer
.append(hash_leaf(b"sha256:aa"), fixed_now())
.await
.unwrap();
let signed = &appended.signed_checkpoint;
let body = signed.checkpoint.to_note_body();
let key = ring::signature::UnparsedPublicKey::new(
&ring::signature::ED25519,
signed.log_public_key.as_bytes(),
);
key.verify(body.as_bytes(), signed.log_signature.as_bytes())
.expect("checkpoint must be signed over the C2SP note body");
}
#[tokio::test]
async fn prove_unknown_leaf_fails() {
let dir = tempfile::tempdir().unwrap();
let writer = writer_in(dir.path());
writer
.append(hash_leaf(b"sha256:aa"), fixed_now())
.await
.unwrap();
let stranger = hash_leaf(b"sha256:never-appended");
assert!(writer.prove(&stranger).await.is_err());
}
#[tokio::test]
async fn origin_mismatch_fails_closed() {
let dir = tempfile::tempdir().unwrap();
let writer = writer_in(dir.path());
writer
.append(hash_leaf(b"sha256:aa"), fixed_now())
.await
.unwrap();
let imposter = LogWriter::new(
FsTileStore::new(dir.path().to_path_buf()),
LogSigningKey::generate().unwrap(),
LogOrigin::new("other.example/log").unwrap(),
);
assert!(matches!(
imposter.append(hash_leaf(b"sha256:bb"), fixed_now()).await,
Err(TransparencyError::InvalidOrigin(_))
));
}
#[tokio::test]
async fn tampered_tile_fails_closed_on_next_operation() {
let dir = tempfile::tempdir().unwrap();
let writer = writer_in(dir.path());
let leaf = hash_leaf(b"sha256:aa");
writer.append(leaf, fixed_now()).await.unwrap();
writer
.append(hash_leaf(b"sha256:bb"), fixed_now())
.await
.unwrap();
let tile = dir.path().join("tile/0/000.p/2");
let mut bytes = std::fs::read(&tile).unwrap();
bytes[0] ^= 0xff;
std::fs::write(&tile, bytes).unwrap();
assert!(matches!(
writer.prove(&leaf).await,
Err(TransparencyError::RootMismatch { .. })
));
}
#[tokio::test]
async fn key_roundtrips_through_pkcs8() {
let key = LogSigningKey::generate().unwrap();
let der = key.to_pkcs8_der().unwrap();
let reloaded = LogSigningKey::from_pkcs8_der(&der).unwrap();
assert_eq!(key.public_key(), reloaded.public_key());
}
#[tokio::test]
async fn appends_roll_over_a_full_tile() {
let dir = tempfile::tempdir().unwrap();
let writer = writer_in(dir.path());
let count = TILE_WIDTH + 3;
for i in 0..count {
writer
.append(
hash_leaf(format!("sha256:{i:064x}").as_bytes()),
fixed_now(),
)
.await
.unwrap();
}
assert!(dir.path().join("tile/0/000").exists());
assert!(dir.path().join("tile/0/001.p/3").exists());
let first = hash_leaf(format!("sha256:{:064x}", 0).as_bytes());
let inclusion = writer.prove(&first).await.unwrap();
assert_eq!(inclusion.signed_checkpoint.checkpoint.size, count);
verify_transparency_inclusion(&inclusion).expect("proof across tiles verifies");
}
}