quantum_sign/transparency/
mod.rs1#![forbid(unsafe_code)]
3#![deny(missing_docs)]
4
5use ciborium::{de, ser};
6use serde::{Deserialize, Serialize};
8use std::fmt;
9
10use crate::crypto::{self as qs_crypto, DigestAlg};
11
12#[derive(Debug)]
14pub enum Error {
15 Http(String),
17 Cbor(String),
19 Verify(String),
21}
22
23impl fmt::Display for Error {
24 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25 match self {
26 Error::Http(e) => write!(f, "http: {e}"),
27 Error::Cbor(e) => write!(f, "cbor: {e}"),
28 Error::Verify(e) => write!(f, "verify: {e}"),
29 }
30 }
31}
32
33impl std::error::Error for Error {}
34
35#[derive(Clone, Debug, Serialize, Deserialize)]
37pub struct Sth {
38 pub v: u8,
40 pub log_id: String,
42 pub tree_size: u64,
44 #[serde(with = "serde_bytes")]
46 pub root_hash: Vec<u8>,
47 pub timestamp: i64,
49 pub sig_alg: String,
51 #[serde(with = "serde_bytes")]
53 pub sig: Vec<u8>,
54}
55
56#[derive(Clone, Debug, Serialize, Deserialize)]
58pub struct AppendReceipt {
59 #[serde(with = "serde_bytes")]
61 pub leaf_hash: Vec<u8>,
62 pub index: u64,
64 pub path: Vec<Vec<u8>>,
66 pub sth: Sth,
68}
69
70pub fn append(base_url: &str, leaf: [u8; 32]) -> Result<AppendReceipt, Error> {
72 #[derive(Serialize)]
73 struct Req<'a> {
74 #[serde(with = "serde_bytes")]
75 leaf_hash: &'a [u8],
76 }
77 let mut body = Vec::new();
78 let req = Req { leaf_hash: &leaf };
79 ser::into_writer(&req, &mut body).map_err(|e| Error::Cbor(e.to_string()))?;
80 let url = format!("{}/v1/append", base_url.trim_end_matches('/'));
81 let mut res = ureq::post(&url)
82 .content_type("application/cbor")
83 .send(body.as_slice())
84 .map_err(|e| Error::Http(e.to_string()))?;
85 let bytes = res
86 .body_mut()
87 .read_to_vec()
88 .map_err(|e| Error::Http(e.to_string()))?;
89 de::from_reader(bytes.as_slice()).map_err(|e| Error::Cbor(e.to_string()))
90}
91
92pub fn verify_inclusion(
94 r: &AppendReceipt,
95 leaf: [u8; 32],
96 log_pubkey_spki: &[u8],
97) -> Result<(), Error> {
98 if r.leaf_hash.as_slice() != leaf {
99 return Err(Error::Verify("leaf mismatch".into()));
100 }
101 if r.sth.root_hash.len() != 32 {
102 return Err(Error::Verify("root hash wrong length".into()));
103 }
104
105 if !verify_inclusion_simple(r.index as usize, leaf, &r.path, r.sth.root_hash.as_slice()) {
107 return Err(Error::Verify("inclusion proof invalid".into()));
108 }
109
110 if r.sth.sig_alg != "mldsa-87" {
112 return Err(Error::Verify("unsupported sth sig alg".into()));
113 }
114 let kid = qs_crypto::kid_from_spki_der(log_pubkey_spki);
115 if kid != r.sth.log_id {
116 return Err(Error::Verify("log_id mismatch".into()));
117 }
118 let signed = encode_sth_sig_input(&r.sth).map_err(Error::Verify)?;
119 let digest = sha512(&signed);
120 qs_crypto::verify_mldsa87_spki(log_pubkey_spki, &digest, DigestAlg::Sha512, &r.sth.sig, None)
121 .map_err(|e| Error::Verify(format!("sth sig: {e}")))?
122 ;
123 Ok(())
124}
125
126pub fn verify_consistency(old: &Sth, new_: &Sth, proof: &[[u8; 32]]) -> Result<(), Error> {
129 if old.tree_size > new_.tree_size {
130 return Err(Error::Verify("tree sizes inverted".into()));
131 }
132 if old.tree_size == new_.tree_size {
133 if old.root_hash == new_.root_hash && proof.is_empty() {
134 return Ok(());
135 } else {
136 return Err(Error::Verify("equal sizes but roots/proof mismatch".into()));
137 }
138 }
139
140 let mut m = (old.tree_size - 1) as u64;
142 let mut _n = (new_.tree_size - 1) as u64;
143
144 let mut i = 1usize;
146 while (m & 1) == 1 {
147 m >>= 1;
148 _n >>= 1;
149 }
150 if proof.is_empty() {
151 return Err(Error::Verify("empty consistency proof".into()));
152 }
153 let mut c_old = proof[0];
154 let mut c_new = proof[0];
155 while i < proof.len() {
156 let p = proof[i];
157 if (m & 1) == 1 {
158 c_old = sha256_concat(&p, &c_old);
160 c_new = sha256_concat(&p, &c_new);
161 i += 1;
162 while (m & 1) == 0 {
163 m >>= 1;
164 _n >>= 1;
165 }
166 } else {
167 c_new = sha256_concat(&c_new, &p);
169 i += 1;
170 }
171 m >>= 1;
172 _n >>= 1;
173 }
174
175 if c_old.as_slice() != old.root_hash.as_slice() {
176 return Err(Error::Verify("old root mismatch".into()));
177 }
178 if c_new.as_slice() != new_.root_hash.as_slice() {
179 return Err(Error::Verify("new root mismatch".into()));
180 }
181 Ok(())
182}
183
184pub fn encode_sth_sig_input(sth: &Sth) -> Result<Vec<u8>, String> {
186 #[derive(Serialize)]
187 struct S<'a> {
188 v: u8,
189 log_id: &'a str,
190 tree_size: u64,
191 #[serde(with = "serde_bytes")]
192 root_hash: &'a [u8],
193 timestamp: i64,
194 }
195 let s = S {
196 v: sth.v,
197 log_id: &sth.log_id,
198 tree_size: sth.tree_size,
199 root_hash: &sth.root_hash,
200 timestamp: sth.timestamp,
201 };
202 let mut out = Vec::new();
203 ser::into_writer(&s, &mut out).map_err(|e| e.to_string())?;
204 Ok(out)
205}
206
207pub fn sha512(data: &[u8]) -> [u8; 64] {
209 use sha2::{Digest, Sha512};
210 let d = Sha512::digest(data);
211 let mut out = [0u8; 64];
212 out.copy_from_slice(&d);
213 out
214}
215
216fn sha256_concat(left: &[u8], right: &[u8]) -> [u8; 32] {
217 use sha2::{Digest, Sha256};
218 let mut h = Sha256::new();
219 h.update(left);
220 h.update(right);
221 let d = h.finalize();
222 let mut out = [0u8; 32];
223 out.copy_from_slice(&d);
224 out
225}
226
227fn verify_inclusion_simple(index: usize, mut leaf: [u8; 32], path: &[Vec<u8>], root: &[u8]) -> bool {
228 let mut idx = index;
229 for sib in path {
230 if sib.len() != 32 { return false; }
231 if idx % 2 == 0 {
232 leaf = sha256_concat(&leaf, sib);
233 } else {
234 leaf = sha256_concat(sib, &leaf);
235 }
236 idx >>= 1;
237 }
238 leaf.as_slice() == root
239}