quantum-sign 0.1.7

Quantum-Sign: post-quantum signatures, format, policy, and CLI in one crate
Documentation
#![forbid(unsafe_code)]

//! Canonical `.qsig` detached signature container encoding.
//! Uses a fixed-position CBOR array to avoid map-ordering pitfalls.

use ciborium::{de, ser};
use serde::{Deserialize, Serialize};
use serde_bytes::ByteBuf;
use serde_tuple::{Deserialize_tuple, Serialize_tuple};
use std::io;
use sha2::{Digest, Sha256};

/// Errors returned by encoding or decoding a `.qsig` structure.
#[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),
}

/// Additional metadata about the signer (key identifier + optional opaque claims blob).
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Signer {
    pub kid: String,
    #[serde(with = "serde_bytes")]
    #[serde(default)]
    pub claims: Vec<u8>,
}

/// Transparency log proof bundle (RFC 6962-style).
#[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>,
}

/// Canonical `.qsig` representation. All fields have a fixed position.
#[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 {
    /// Serialize to canonical CBOR bytes.
    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)
    }

    /// Deserialize from CBOR bytes.
    pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
        de::from_reader(bytes).map_err(|e| Error::Decode(e.to_string()))
    }

    /// Convenience constructor from raw field values.
    #[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),
        }
    }

    /// Access optional timestamp token as bytes.
    pub fn timestamp(&self) -> Option<&[u8]> {
        self.tst.as_ref().map(ByteBuf::as_ref)
    }

    /// Iterate over co-signature blobs as raw byte slices.
    pub fn co_signatures(&self) -> impl Iterator<Item = &[u8]> {
        self.co_sig.iter().map(ByteBuf::as_ref)
    }

    /// Encode the canonical "core" view used for transparency leaf hashing.
    /// This view clears any attached timestamp (`tst`) and transparency log (`tlog`)
    /// so updates to these fields do not affect the log leaf.
    pub fn encode_core_view(&self) -> Result<Vec<u8>, Error> {
        let mut core = self.clone();
        core.tst = None;
        core.tlog = None;
        core.encode_to_vec()
    }

    /// Compute SHA-256 over the canonical core view encoding.
    pub fn core_leaf_hash_sha256(&self) -> Result<[u8; 32], Error> {
        let core = self.encode_core_view()?;
        let mut h = Sha256::new();
        h.update(&core);
        let digest = h.finalize();
        let mut out = [0u8; 32];
        out.copy_from_slice(&digest);
        Ok(out)
    }
}