#![forbid(unsafe_code)]
#![deny(missing_docs)]
use ciborium::{de, ser};
use serde::{Deserialize, Serialize};
use std::fmt;
use crate::crypto::{self as qs_crypto, DigestAlg};
#[derive(Debug)]
pub enum Error {
Http(String),
Cbor(String),
Verify(String),
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Http(e) => write!(f, "http: {e}"),
Error::Cbor(e) => write!(f, "cbor: {e}"),
Error::Verify(e) => write!(f, "verify: {e}"),
}
}
}
impl std::error::Error for Error {}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Sth {
pub v: u8,
pub log_id: String,
pub tree_size: u64,
#[serde(with = "serde_bytes")]
pub root_hash: Vec<u8>,
pub timestamp: i64,
pub sig_alg: String,
#[serde(with = "serde_bytes")]
pub sig: Vec<u8>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct AppendReceipt {
#[serde(with = "serde_bytes")]
pub leaf_hash: Vec<u8>,
pub index: u64,
pub path: Vec<Vec<u8>>,
pub sth: Sth,
}
pub fn append(base_url: &str, leaf: [u8; 32]) -> Result<AppendReceipt, Error> {
#[derive(Serialize)]
struct Req<'a> {
#[serde(with = "serde_bytes")]
leaf_hash: &'a [u8],
}
let mut body = Vec::new();
let req = Req { leaf_hash: &leaf };
ser::into_writer(&req, &mut body).map_err(|e| Error::Cbor(e.to_string()))?;
let url = format!("{}/v1/append", base_url.trim_end_matches('/'));
let mut res = ureq::post(&url)
.content_type("application/cbor")
.send(body.as_slice())
.map_err(|e| Error::Http(e.to_string()))?;
let bytes = res
.body_mut()
.read_to_vec()
.map_err(|e| Error::Http(e.to_string()))?;
de::from_reader(bytes.as_slice()).map_err(|e| Error::Cbor(e.to_string()))
}
pub fn verify_inclusion(
r: &AppendReceipt,
leaf: [u8; 32],
log_pubkey_spki: &[u8],
) -> Result<(), Error> {
if r.leaf_hash.as_slice() != leaf {
return Err(Error::Verify("leaf mismatch".into()));
}
if r.sth.root_hash.len() != 32 {
return Err(Error::Verify("root hash wrong length".into()));
}
if !verify_inclusion_simple(r.index as usize, leaf, &r.path, r.sth.root_hash.as_slice()) {
return Err(Error::Verify("inclusion proof invalid".into()));
}
if r.sth.sig_alg != "mldsa-87" {
return Err(Error::Verify("unsupported sth sig alg".into()));
}
let kid = qs_crypto::kid_from_spki_der(log_pubkey_spki);
if kid != r.sth.log_id {
return Err(Error::Verify("log_id mismatch".into()));
}
let signed = encode_sth_sig_input(&r.sth).map_err(Error::Verify)?;
let digest = sha512(&signed);
qs_crypto::verify_mldsa87_spki(log_pubkey_spki, &digest, DigestAlg::Sha512, &r.sth.sig, None)
.map_err(|e| Error::Verify(format!("sth sig: {e}")))?
;
Ok(())
}
pub fn verify_consistency(old: &Sth, new_: &Sth, proof: &[[u8; 32]]) -> Result<(), Error> {
if old.tree_size > new_.tree_size {
return Err(Error::Verify("tree sizes inverted".into()));
}
if old.tree_size == new_.tree_size {
if old.root_hash == new_.root_hash && proof.is_empty() {
return Ok(());
} else {
return Err(Error::Verify("equal sizes but roots/proof mismatch".into()));
}
}
let mut m = (old.tree_size - 1) as u64;
let mut _n = (new_.tree_size - 1) as u64;
let mut i = 1usize;
while (m & 1) == 1 {
m >>= 1;
_n >>= 1;
}
if proof.is_empty() {
return Err(Error::Verify("empty consistency proof".into()));
}
let mut c_old = proof[0];
let mut c_new = proof[0];
while i < proof.len() {
let p = proof[i];
if (m & 1) == 1 {
c_old = sha256_concat(&p, &c_old);
c_new = sha256_concat(&p, &c_new);
i += 1;
while (m & 1) == 0 {
m >>= 1;
_n >>= 1;
}
} else {
c_new = sha256_concat(&c_new, &p);
i += 1;
}
m >>= 1;
_n >>= 1;
}
if c_old.as_slice() != old.root_hash.as_slice() {
return Err(Error::Verify("old root mismatch".into()));
}
if c_new.as_slice() != new_.root_hash.as_slice() {
return Err(Error::Verify("new root mismatch".into()));
}
Ok(())
}
pub fn encode_sth_sig_input(sth: &Sth) -> Result<Vec<u8>, String> {
#[derive(Serialize)]
struct S<'a> {
v: u8,
log_id: &'a str,
tree_size: u64,
#[serde(with = "serde_bytes")]
root_hash: &'a [u8],
timestamp: i64,
}
let s = S {
v: sth.v,
log_id: &sth.log_id,
tree_size: sth.tree_size,
root_hash: &sth.root_hash,
timestamp: sth.timestamp,
};
let mut out = Vec::new();
ser::into_writer(&s, &mut out).map_err(|e| e.to_string())?;
Ok(out)
}
pub fn sha512(data: &[u8]) -> [u8; 64] {
use sha2::{Digest, Sha512};
let d = Sha512::digest(data);
let mut out = [0u8; 64];
out.copy_from_slice(&d);
out
}
fn sha256_concat(left: &[u8], right: &[u8]) -> [u8; 32] {
use sha2::{Digest, Sha256};
let mut h = Sha256::new();
h.update(left);
h.update(right);
let d = h.finalize();
let mut out = [0u8; 32];
out.copy_from_slice(&d);
out
}
fn verify_inclusion_simple(index: usize, mut leaf: [u8; 32], path: &[Vec<u8>], root: &[u8]) -> bool {
let mut idx = index;
for sib in path {
if sib.len() != 32 { return false; }
if idx % 2 == 0 {
leaf = sha256_concat(&leaf, sib);
} else {
leaf = sha256_concat(sib, &leaf);
}
idx >>= 1;
}
leaf.as_slice() == root
}