use std::collections::BTreeMap;
use ed25519_dalek::SigningKey;
use crate::log::{ConsistencyProof, TreeHead, empty_root};
use crate::transparency::{
AddCheckpoint, Error, KeyName, KeyType, Verified, Vkey, cosign, verify_line, vkey,
};
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct Reply {
pub status: u16,
pub content_type: Option<&'static str>,
pub body: Vec<u8>,
}
impl Reply {
fn status(status: u16) -> Self {
Self {
status,
content_type: None,
body: Vec::new(),
}
}
}
#[derive(Clone)]
pub struct TestWitness {
name: KeyName,
key: SigningKey,
logs: Vec<Vkey>,
heads: BTreeMap<KeyName, TreeHead>,
clock: u64,
}
impl core::fmt::Debug for TestWitness {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("TestWitness")
.field("vkey", &self.vkey().to_string())
.field("logs", &self.logs)
.field("heads", &self.heads)
.finish_non_exhaustive()
}
}
impl TestWitness {
pub fn new(name: &str, seed: [u8; 32]) -> Result<Self, Error> {
Ok(Self {
name: KeyName::new(name)?,
key: SigningKey::from_bytes(&seed),
logs: Vec::new(),
heads: BTreeMap::new(),
clock: 0,
})
}
#[must_use]
pub fn log(mut self, key: Vkey) -> Self {
self.logs.push(key);
self
}
#[must_use]
pub fn vkey(&self) -> Vkey {
vkey(
&self.name,
KeyType::CosignatureEd25519,
&self.key.verifying_key(),
)
}
#[must_use]
pub fn head(&self, origin: &KeyName) -> Option<TreeHead> {
self.heads.get(origin).copied()
}
pub fn post(&mut self, body: &[u8]) -> Reply {
let Ok(request) = AddCheckpoint::parse(body) else {
return Reply::status(400);
};
let signed = request.checkpoint();
let checkpoint = signed.checkpoint();
let origin = checkpoint.origin();
let keys: Vec<&Vkey> = self
.logs
.iter()
.filter(|key| key.name() == origin)
.collect();
if keys.is_empty() {
return Reply::status(404);
}
let text = signed.note().text();
let mut verified = 0usize;
for line in signed.signatures() {
let mut matching = keys.iter().filter(|key| key.signed(line));
if let Some(key) = matching.next() {
if matching.any(|other| other != key) {
return Reply::status(403);
}
match verify_line(key, line, text) {
Ok(Verified::Log { .. } | Verified::Cosigner(_)) => {
verified = verified.saturating_add(1)
}
Err(_) => return Reply::status(403),
}
}
}
if verified == 0 {
return Reply::status(403);
}
let head = checkpoint.head();
if head.size == 0 && head.root != empty_root() {
return Reply::status(422);
}
if request.old() > head.size {
return Reply::status(400);
}
let held = self.heads.get(origin).copied();
let held_size = held.map_or(0, |held| held.size);
if request.old() != held_size {
return Reply {
status: 409,
content_type: Some("text/x.tlog.size"),
body: format!("{held_size}\n").into_bytes(),
};
}
let consistent = match held {
None => request.proof().is_empty(),
Some(held) if held.size == 0 => request.proof().is_empty(),
Some(held) if held.size == head.size => {
request.proof().is_empty() && held.root == head.root
}
Some(held) => ConsistencyProof {
old_size: held.size,
new_size: head.size,
path: request.proof().to_vec(),
}
.verify(&held, &head)
.is_ok(),
};
if !consistent {
return Reply::status(422);
}
self.clock = self.clock.saturating_add(1);
self.heads.insert(origin.clone(), head);
let cosignature = cosign(&self.name, &self.key, self.clock, checkpoint);
Reply {
status: 200,
content_type: None,
body: cosignature.to_line().into_bytes(),
}
}
}