pub mod handlers;
mod proof;
mod state;
mod verifier;
#[cfg(test)]
mod litewitness_test;
pub use proof::{verify_consistency, ConsistencyProof};
pub use state::WitnessStateStore;
pub use verifier::{CheckpointVerifier, LogConfig};
use crate::checkpoint::{CheckpointSignature, CheckpointSigner, CosignedCheckpoint};
use crate::error::{Error, Result};
use ed25519_dalek::Signer;
use sea_orm::DatabaseConnection;
use sigstore_types::Sha256Hash;
use std::sync::Arc;
pub struct Witness {
signer: Arc<CheckpointSigner>,
state_store: WitnessStateStore,
logs: Vec<LogConfig>,
}
impl Witness {
pub fn new(
signer: Arc<CheckpointSigner>,
conn: Arc<DatabaseConnection>,
logs: Vec<LogConfig>,
) -> Self {
Self {
signer,
state_store: WitnessStateStore::new(conn),
logs,
}
}
pub fn name(&self) -> &str {
self.signer.name().as_str()
}
pub async fn add_checkpoint(
&self,
request: AddCheckpointRequest,
) -> std::result::Result<CheckpointSignature, WitnessError> {
let checkpoint = CosignedCheckpoint::from_text(&request.checkpoint)
.map_err(|e| WitnessError::BadRequest(format!("invalid checkpoint: {}", e)))?;
let origin = checkpoint.checkpoint.origin.as_str();
let log_config = self
.logs
.iter()
.find(|l| l.origin == origin)
.ok_or(WitnessError::UnknownLog(origin.to_string()))?;
let verifier = CheckpointVerifier::new(log_config.clone());
verifier
.verify(&checkpoint)
.map_err(|e| WitnessError::InvalidSignature(e.to_string()))?;
let new_size = checkpoint.checkpoint.size.value();
let new_root = checkpoint.checkpoint.root_hash;
if request.old_size > new_size {
return Err(WitnessError::BadRequest(format!(
"old_size ({}) > checkpoint size ({})",
request.old_size, new_size
)));
}
let state = self
.state_store
.get_or_init(origin)
.await
.map_err(|e| WitnessError::Internal(format!("failed to get state: {}", e)))?;
if request.old_size != state.size {
return Err(WitnessError::Conflict(state.size));
}
if state.size > 0 {
if new_size < state.size {
return Err(WitnessError::BadRequest(format!(
"checkpoint size ({}) < witnessed size ({})",
new_size, state.size
)));
}
if new_size == state.size {
if new_root != state.root_hash {
return Err(WitnessError::InvalidProof(
"same size but different roots - split view detected".to_string(),
));
}
} else {
verify_consistency(
state.size,
new_size,
&state.root_hash,
&new_root,
&request.proof,
)
.map_err(|e| WitnessError::InvalidProof(e.to_string()))?;
}
} else if !request.proof.is_empty() {
return Err(WitnessError::InvalidProof(
"non-empty proof for empty tree".to_string(),
));
}
let body = checkpoint.checkpoint.to_body();
let signature = self.signer.signing_key_ref().sign(body.as_bytes());
let cosig = CheckpointSignature {
name: self.signer.name().clone(),
key_id: self.signer.key_id().clone(),
signature,
};
self.state_store
.update(origin, new_size, new_root, &request.checkpoint)
.await
.map_err(|e| WitnessError::Internal(format!("failed to update state: {}", e)))?;
Ok(cosig)
}
pub async fn get_state(&self, origin: &str) -> Result<Option<WitnessedState>> {
self.state_store.get(origin).await
}
}
#[derive(Debug, Clone)]
pub struct AddCheckpointRequest {
pub old_size: u64,
pub proof: ConsistencyProof,
pub checkpoint: String,
}
impl AddCheckpointRequest {
pub fn from_ascii(body: &str) -> Result<Self> {
let mut lines = body.lines();
let old_line = lines
.next()
.ok_or_else(|| Error::InvalidEntry("missing old size line".into()))?;
let old_size = if let Some(rest) = old_line.strip_prefix("old ") {
rest.trim()
.parse::<u64>()
.map_err(|e| Error::InvalidEntry(format!("invalid old size: {}", e)))?
} else {
return Err(Error::InvalidEntry(format!(
"expected 'old <size>', got '{}'",
old_line
)));
};
let mut proof_hashes = Vec::new();
for line in lines.by_ref() {
if line.is_empty() {
break;
}
let hash_bytes = base64::engine::general_purpose::STANDARD
.decode(line)
.map_err(|e| Error::InvalidEntry(format!("invalid proof hash base64: {}", e)))?;
if hash_bytes.len() != 32 {
return Err(Error::InvalidEntry(format!(
"proof hash must be 32 bytes, got {}",
hash_bytes.len()
)));
}
let hash = Sha256Hash::try_from_slice(&hash_bytes)
.map_err(|e| Error::InvalidEntry(format!("invalid hash: {}", e)))?;
proof_hashes.push(hash);
}
let checkpoint: String = lines.collect::<Vec<_>>().join("\n");
if checkpoint.is_empty() {
return Err(Error::InvalidEntry("missing checkpoint".into()));
}
Ok(Self {
old_size,
proof: ConsistencyProof::new(proof_hashes),
checkpoint,
})
}
}
#[derive(Debug, Clone)]
pub struct WitnessedState {
pub origin: String,
pub size: u64,
pub root_hash: Sha256Hash,
}
#[derive(Debug, thiserror::Error)]
pub enum WitnessError {
#[error("conflict: witnessed size is {0}")]
Conflict(u64),
#[error("unknown log: {0}")]
UnknownLog(String),
#[error("invalid signature: {0}")]
InvalidSignature(String),
#[error("invalid proof: {0}")]
InvalidProof(String),
#[error("bad request: {0}")]
BadRequest(String),
#[error("internal error: {0}")]
Internal(String),
}
impl WitnessError {
pub fn status_code(&self) -> u16 {
match self {
WitnessError::Conflict(_) => 409,
WitnessError::UnknownLog(_) => 404,
WitnessError::InvalidSignature(_) => 403,
WitnessError::InvalidProof(_) => 422,
WitnessError::BadRequest(_) => 400,
WitnessError::Internal(_) => 500,
}
}
}
use base64::Engine;