#![forbid(unsafe_code)]
use ciborium::{de, ser};
use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;
use serde_tuple::{Deserialize_tuple, Serialize_tuple};
use std::io;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("CBOR encode error: {0}")]
Encode(String),
#[error("CBOR decode error: {0}")]
Decode(String),
#[error("I/O error: {0}")]
Io(#[from] io::Error),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Signer {
pub kid: String,
#[serde(with = "serde_bytes")]
#[serde(default)]
pub claims: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Transparency {
#[serde(with = "serde_bytes")]
pub root: Vec<u8>,
#[serde(with = "serde_bytes")]
pub proof: Vec<u8>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize_tuple, Deserialize_tuple)]
pub struct QSig {
pub version: u8,
pub alg: String,
#[serde(with = "serde_bytes")]
pub digest: Vec<u8>,
#[serde(with = "serde_bytes")]
pub sig: Vec<u8>,
pub co_sig: Vec<ByteBuf>,
pub tst: Option<ByteBuf>,
pub tlog: Option<Transparency>,
pub signer: Signer,
pub time_unix: i64,
pub domain_sep: String,
#[serde(default)]
pub meta: Option<ByteBuf>,
}
impl QSig {
pub fn encode_to_vec(&self) -> Result<Vec<u8>, Error> {
let mut buf = Vec::new();
ser::into_writer(self, &mut buf).map_err(|e| Error::Encode(e.to_string()))?;
Ok(buf)
}
pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
de::from_reader(bytes).map_err(|e| Error::Decode(e.to_string()))
}
#[allow(clippy::too_many_arguments)]
pub fn new(
version: u8,
alg: String,
digest: Vec<u8>,
sig: Vec<u8>,
co_sig: Vec<Vec<u8>>,
tst: Option<Vec<u8>>,
tlog: Option<Transparency>,
signer: Signer,
time_unix: i64,
domain_sep: String,
meta: Option<Vec<u8>>,
) -> Self {
Self {
version,
alg,
digest,
sig,
co_sig: co_sig.into_iter().map(ByteBuf::from).collect(),
tst: tst.map(ByteBuf::from),
tlog,
signer,
time_unix,
domain_sep,
meta: meta.map(ByteBuf::from),
}
}
pub fn timestamp(&self) -> Option<&[u8]> {
self.tst.as_ref().map(ByteBuf::as_ref)
}
pub fn co_signatures(&self) -> impl Iterator<Item = &[u8]> {
self.co_sig.iter().map(ByteBuf::as_ref)
}
}
#[cfg(test)]
mod tests {
use super::*;
use sha2::{Digest, Sha256};
fn sample_qsig() -> QSig {
let signer = Signer {
kid: "kid-example".into(),
claims: vec![0x01, 0x02],
};
QSig {
version: 1,
alg: "mldsa-87".into(),
digest: vec![0xAA; 32],
sig: vec![0xBB; 24],
co_sig: vec![ByteBuf::from(vec![0xCC; 10])],
tst: Some(ByteBuf::from(vec![0xDD; 8])),
tlog: Some(Transparency {
root: vec![0xEE; 32],
proof: vec![0xFF; 64],
}),
signer,
time_unix: 1_700_000_000,
domain_sep: "quantum-sign-v1".into(),
meta: Some(ByteBuf::from(vec![0x11, 0x22])),
}
}
#[test]
fn round_trip() {
let qsig = sample_qsig();
let encoded = qsig.encode_to_vec().expect("encode");
let decoded = QSig::decode(&encoded).expect("decode");
assert_eq!(qsig, decoded);
assert_eq!(decoded.timestamp().unwrap(), &[0xDD; 8]);
assert_eq!(decoded.co_signatures().count(), 1);
}
#[test]
fn golden_sha256_digest() {
const GOLDEN_DIGEST: &str =
"877519a8e00897c7214f9393bea0462f13eee190bdef1b952c432bc121560374";
let qsig = sample_qsig();
let encoded = qsig.encode_to_vec().expect("encode");
let digest = Sha256::digest(&encoded);
let digest_hex: String = digest.iter().map(|b| format!("{:02x}", b)).collect();
assert_eq!(digest_hex, GOLDEN_DIGEST);
}
}