1#![forbid(unsafe_code)]
2
3use ciborium::{de, ser};
7use serde::{Deserialize, Serialize};
8use serde_bytes::ByteBuf;
9use serde_tuple::{Deserialize_tuple, Serialize_tuple};
10use std::io;
11
12#[derive(Debug, thiserror::Error)]
14pub enum Error {
15 #[error("CBOR encode error: {0}")]
16 Encode(String),
17 #[error("CBOR decode error: {0}")]
18 Decode(String),
19 #[error("I/O error: {0}")]
20 Io(#[from] io::Error),
21}
22
23#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
25pub struct Signer {
26 pub kid: String,
27 #[serde(with = "serde_bytes")]
28 #[serde(default)]
29 pub claims: Vec<u8>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
34pub struct Transparency {
35 #[serde(with = "serde_bytes")]
36 pub root: Vec<u8>,
37 #[serde(with = "serde_bytes")]
38 pub proof: Vec<u8>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq, Serialize_tuple, Deserialize_tuple)]
43pub struct QSig {
44 pub version: u8,
45 pub alg: String,
46 #[serde(with = "serde_bytes")]
47 pub digest: Vec<u8>,
48 #[serde(with = "serde_bytes")]
49 pub sig: Vec<u8>,
50 pub co_sig: Vec<ByteBuf>,
51 pub tst: Option<ByteBuf>,
52 pub tlog: Option<Transparency>,
53 pub signer: Signer,
54 pub time_unix: i64,
55 pub domain_sep: String,
56 #[serde(default)]
57 pub meta: Option<ByteBuf>,
58}
59
60impl QSig {
61 pub fn encode_to_vec(&self) -> Result<Vec<u8>, Error> {
63 let mut buf = Vec::new();
64 ser::into_writer(self, &mut buf).map_err(|e| Error::Encode(e.to_string()))?;
65 Ok(buf)
66 }
67
68 pub fn decode(bytes: &[u8]) -> Result<Self, Error> {
70 de::from_reader(bytes).map_err(|e| Error::Decode(e.to_string()))
71 }
72
73 #[allow(clippy::too_many_arguments)]
75 pub fn new(
76 version: u8,
77 alg: String,
78 digest: Vec<u8>,
79 sig: Vec<u8>,
80 co_sig: Vec<Vec<u8>>,
81 tst: Option<Vec<u8>>,
82 tlog: Option<Transparency>,
83 signer: Signer,
84 time_unix: i64,
85 domain_sep: String,
86 meta: Option<Vec<u8>>,
87 ) -> Self {
88 Self {
89 version,
90 alg,
91 digest,
92 sig,
93 co_sig: co_sig.into_iter().map(ByteBuf::from).collect(),
94 tst: tst.map(ByteBuf::from),
95 tlog,
96 signer,
97 time_unix,
98 domain_sep,
99 meta: meta.map(ByteBuf::from),
100 }
101 }
102
103 pub fn timestamp(&self) -> Option<&[u8]> {
105 self.tst.as_ref().map(ByteBuf::as_ref)
106 }
107
108 pub fn co_signatures(&self) -> impl Iterator<Item = &[u8]> {
110 self.co_sig.iter().map(ByteBuf::as_ref)
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use sha2::{Digest, Sha256};
118
119 fn sample_qsig() -> QSig {
120 let signer = Signer {
121 kid: "kid-example".into(),
122 claims: vec![0x01, 0x02],
123 };
124 QSig {
125 version: 1,
126 alg: "mldsa-87".into(),
127 digest: vec![0xAA; 32],
128 sig: vec![0xBB; 24],
129 co_sig: vec![ByteBuf::from(vec![0xCC; 10])],
130 tst: Some(ByteBuf::from(vec![0xDD; 8])),
131 tlog: Some(Transparency {
132 root: vec![0xEE; 32],
133 proof: vec![0xFF; 64],
134 }),
135 signer,
136 time_unix: 1_700_000_000,
137 domain_sep: "quantum-sign-v1".into(),
138 meta: Some(ByteBuf::from(vec![0x11, 0x22])),
139 }
140 }
141
142 #[test]
143 fn round_trip() {
144 let qsig = sample_qsig();
145 let encoded = qsig.encode_to_vec().expect("encode");
146 let decoded = QSig::decode(&encoded).expect("decode");
147 assert_eq!(qsig, decoded);
148 assert_eq!(decoded.timestamp().unwrap(), &[0xDD; 8]);
149 assert_eq!(decoded.co_signatures().count(), 1);
150 }
151
152 #[test]
153 fn golden_sha256_digest() {
154 const GOLDEN_DIGEST: &str =
155 "877519a8e00897c7214f9393bea0462f13eee190bdef1b952c432bc121560374";
156
157 let qsig = sample_qsig();
158 let encoded = qsig.encode_to_vec().expect("encode");
159 let digest = Sha256::digest(&encoded);
160 let digest_hex: String = digest.iter().map(|b| format!("{:02x}", b)).collect();
161 assert_eq!(digest_hex, GOLDEN_DIGEST);
162 }
163}