quantum_sign/format/
mod.rs

1#![forbid(unsafe_code)]
2
3//! Canonical `.qsig` detached signature container encoding.
4//! Uses a fixed-position CBOR array to avoid map-ordering pitfalls.
5
6use ciborium::{de, ser};
7use serde::{Deserialize, Serialize};
8use serde_bytes::ByteBuf;
9use serde_tuple::{Deserialize_tuple, Serialize_tuple};
10use std::io;
11use sha2::{Digest, Sha256};
12
13/// Errors returned by encoding or decoding a `.qsig` structure.
14#[derive(Debug, thiserror::Error)]
15pub enum Error {
16    #[error("CBOR encode error: {0}")]
17    Encode(String),
18    #[error("CBOR decode error: {0}")]
19    Decode(String),
20    #[error("I/O error: {0}")]
21    Io(#[from] io::Error),
22}
23
24/// Additional metadata about the signer (key identifier + optional opaque claims blob).
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
26pub struct Signer {
27    pub kid: String,
28    #[serde(with = "serde_bytes")]
29    #[serde(default)]
30    pub claims: Vec<u8>,
31}
32
33/// Transparency log proof bundle (RFC 6962-style).
34#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct Transparency {
36    #[serde(with = "serde_bytes")]
37    pub root: Vec<u8>,
38    #[serde(with = "serde_bytes")]
39    pub proof: Vec<u8>,
40}
41
42/// Canonical `.qsig` representation. All fields have a fixed position.
43#[derive(Debug, Clone, PartialEq, Eq, Serialize_tuple, Deserialize_tuple)]
44pub struct QSig {
45    pub version: u8,
46    pub alg: String,
47    #[serde(with = "serde_bytes")]
48    pub digest: Vec<u8>,
49    #[serde(with = "serde_bytes")]
50    pub sig: Vec<u8>,
51    pub co_sig: Vec<ByteBuf>,
52    pub tst: Option<ByteBuf>,
53    pub tlog: Option<Transparency>,
54    pub signer: Signer,
55    pub time_unix: i64,
56    pub domain_sep: String,
57    #[serde(default)]
58    pub meta: Option<ByteBuf>,
59}
60
61impl QSig {
62    /// Serialize to canonical CBOR bytes.
63    pub fn encode_to_vec(&self) -> Result<Vec<u8>, Error> {
64        let mut buf = Vec::new();
65        ser::into_writer(self, &mut buf).map_err(|e| Error::Encode(e.to_string()))?;
66        Ok(buf)
67    }
68
69    /// Deserialize from CBOR bytes.
70    pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
71        de::from_reader(bytes).map_err(|e| Error::Decode(e.to_string()))
72    }
73
74    /// Convenience constructor from raw field values.
75    #[allow(clippy::too_many_arguments)]
76    pub fn new(
77        version: u8,
78        alg: String,
79        digest: Vec<u8>,
80        sig: Vec<u8>,
81        co_sig: Vec<Vec<u8>>,
82        tst: Option<Vec<u8>>,
83        tlog: Option<Transparency>,
84        signer: Signer,
85        time_unix: i64,
86        domain_sep: String,
87        meta: Option<Vec<u8>>,
88    ) -> Self {
89        Self {
90            version,
91            alg,
92            digest,
93            sig,
94            co_sig: co_sig.into_iter().map(ByteBuf::from).collect(),
95            tst: tst.map(ByteBuf::from),
96            tlog,
97            signer,
98            time_unix,
99            domain_sep,
100            meta: meta.map(ByteBuf::from),
101        }
102    }
103
104    /// Access optional timestamp token as bytes.
105    pub fn timestamp(&self) -> Option<&[u8]> {
106        self.tst.as_ref().map(ByteBuf::as_ref)
107    }
108
109    /// Iterate over co-signature blobs as raw byte slices.
110    pub fn co_signatures(&self) -> impl Iterator<Item = &[u8]> {
111        self.co_sig.iter().map(ByteBuf::as_ref)
112    }
113
114    /// Encode the canonical "core" view used for transparency leaf hashing.
115    /// This view clears any attached timestamp (`tst`) and transparency log (`tlog`)
116    /// so updates to these fields do not affect the log leaf.
117    pub fn encode_core_view(&self) -> Result<Vec<u8>, Error> {
118        let mut core = self.clone();
119        core.tst = None;
120        core.tlog = None;
121        core.encode_to_vec()
122    }
123
124    /// Compute SHA-256 over the canonical core view encoding.
125    pub fn core_leaf_hash_sha256(&self) -> Result<[u8; 32], Error> {
126        let core = self.encode_core_view()?;
127        let mut h = Sha256::new();
128        h.update(&core);
129        let digest = h.finalize();
130        let mut out = [0u8; 32];
131        out.copy_from_slice(&digest);
132        Ok(out)
133    }
134}