pqc_sig/types.rs
1//! Wire types for PQC signature public keys, secret keys, and signatures.
2//!
3//! These types are designed for serialization across WASM boundaries and for
4//! use in DID Documents (W3C Decentralized Identifiers).
5//!
6//! All byte arrays are encoded as base64url (no padding) strings in JSON.
7
8extern crate alloc;
9use alloc::{format, string::{String, ToString}, vec::Vec};
10
11use base64ct::{Base64Url, Encoding};
12use serde::{Deserialize, Serialize};
13use zeroize::{Zeroize, ZeroizeOnDrop};
14
15use crate::error::{SigError, SigResult};
16
17// ── Algorithm Identifiers ─────────────────────────────────────────────────────
18
19/// Identifies the post-quantum signature algorithm used.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
21#[serde(rename_all = "snake_case")]
22pub enum SigAlgorithm {
23 // FIPS 204 — ML-DSA (Module Lattice Digital Signature Algorithm)
24 /// ML-DSA-44 (NIST FIPS 204, Security Level 2)
25 MlDsa44,
26 /// ML-DSA-65 (NIST FIPS 204, Security Level 3) — recommended default
27 MlDsa65,
28 /// ML-DSA-87 (NIST FIPS 204, Security Level 5)
29 MlDsa87,
30
31 // FIPS 205 — SLH-DSA (Stateless Hash-Based Digital Signature Algorithm)
32 /// SLH-DSA-SHA2-128s (NIST FIPS 205, Security Level 1, small signatures)
33 SlhDsaSha2_128s,
34 /// SLH-DSA-SHA2-128f (NIST FIPS 205, Security Level 1, fast signing)
35 SlhDsaSha2_128f,
36 /// SLH-DSA-SHA2-192s (NIST FIPS 205, Security Level 3, small signatures)
37 SlhDsaSha2_192s,
38 /// SLH-DSA-SHA2-192f (NIST FIPS 205, Security Level 3, fast signing)
39 SlhDsaSha2_192f,
40 /// SLH-DSA-SHA2-256s (NIST FIPS 205, Security Level 5, small signatures)
41 SlhDsaSha2_256s,
42 /// SLH-DSA-SHA2-256f (NIST FIPS 205, Security Level 5, fast signing)
43 SlhDsaSha2_256f,
44 /// SLH-DSA-SHAKE-128s (NIST FIPS 205, Security Level 1, small signatures)
45 SlhDsaShake128s,
46 /// SLH-DSA-SHAKE-128f (NIST FIPS 205, Security Level 1, fast signing)
47 SlhDsaShake128f,
48 /// SLH-DSA-SHAKE-192s (NIST FIPS 205, Security Level 3, small signatures)
49 SlhDsaShake192s,
50 /// SLH-DSA-SHAKE-192f (NIST FIPS 205, Security Level 3, fast signing)
51 SlhDsaShake192f,
52 /// SLH-DSA-SHAKE-256s (NIST FIPS 205, Security Level 5, small signatures)
53 SlhDsaShake256s,
54 /// SLH-DSA-SHAKE-256f (NIST FIPS 205, Security Level 5, fast signing)
55 SlhDsaShake256f,
56
57 // FIPS 206 — FN-DSA (Falcon)
58 /// FN-DSA-512 (NIST FIPS 206, Security Level 1) — requires `fndsa` feature
59 FnDsa512,
60 /// FN-DSA-1024 (NIST FIPS 206, Security Level 5) — requires `fndsa` feature
61 FnDsa1024,
62}
63
64impl SigAlgorithm {
65 /// Returns the canonical string identifier for this algorithm.
66 pub fn as_str(&self) -> &'static str {
67 match self {
68 SigAlgorithm::MlDsa44 => "ML-DSA-44",
69 SigAlgorithm::MlDsa65 => "ML-DSA-65",
70 SigAlgorithm::MlDsa87 => "ML-DSA-87",
71 SigAlgorithm::SlhDsaSha2_128s => "SLH-DSA-SHA2-128s",
72 SigAlgorithm::SlhDsaSha2_128f => "SLH-DSA-SHA2-128f",
73 SigAlgorithm::SlhDsaSha2_192s => "SLH-DSA-SHA2-192s",
74 SigAlgorithm::SlhDsaSha2_192f => "SLH-DSA-SHA2-192f",
75 SigAlgorithm::SlhDsaSha2_256s => "SLH-DSA-SHA2-256s",
76 SigAlgorithm::SlhDsaSha2_256f => "SLH-DSA-SHA2-256f",
77 SigAlgorithm::SlhDsaShake128s => "SLH-DSA-SHAKE-128s",
78 SigAlgorithm::SlhDsaShake128f => "SLH-DSA-SHAKE-128f",
79 SigAlgorithm::SlhDsaShake192s => "SLH-DSA-SHAKE-192s",
80 SigAlgorithm::SlhDsaShake192f => "SLH-DSA-SHAKE-192f",
81 SigAlgorithm::SlhDsaShake256s => "SLH-DSA-SHAKE-256s",
82 SigAlgorithm::SlhDsaShake256f => "SLH-DSA-SHAKE-256f",
83 SigAlgorithm::FnDsa512 => "FN-DSA-512",
84 SigAlgorithm::FnDsa1024 => "FN-DSA-1024",
85 }
86 }
87
88 /// Returns the public key size in bytes for this algorithm.
89 pub fn public_key_size(&self) -> usize {
90 match self {
91 SigAlgorithm::MlDsa44 => 1312,
92 SigAlgorithm::MlDsa65 => 1952,
93 SigAlgorithm::MlDsa87 => 2592,
94 SigAlgorithm::SlhDsaSha2_128s => 32,
95 SigAlgorithm::SlhDsaSha2_128f => 32,
96 SigAlgorithm::SlhDsaSha2_192s => 48,
97 SigAlgorithm::SlhDsaSha2_192f => 48,
98 SigAlgorithm::SlhDsaSha2_256s => 64,
99 SigAlgorithm::SlhDsaSha2_256f => 64,
100 SigAlgorithm::SlhDsaShake128s => 32,
101 SigAlgorithm::SlhDsaShake128f => 32,
102 SigAlgorithm::SlhDsaShake192s => 48,
103 SigAlgorithm::SlhDsaShake192f => 48,
104 SigAlgorithm::SlhDsaShake256s => 64,
105 SigAlgorithm::SlhDsaShake256f => 64,
106 SigAlgorithm::FnDsa512 => 897,
107 SigAlgorithm::FnDsa1024 => 1793,
108 }
109 }
110
111 /// Returns the secret key size in bytes for this algorithm.
112 ///
113 /// For ML-DSA variants, this returns 32 (the seed size), which is the preferred
114 /// serialization per the `ml-dsa` crate. The full expanded key form (2560/4032/4896 bytes)
115 /// is deprecated in favor of the compact 32-byte seed.
116 ///
117 /// For FN-DSA variants, this is the `fn-dsa` crate's own encoded signing-key format
118 /// (1345/2369 bytes), not the raw 1281/2305-byte NIST secret-key size — implementation-defined,
119 /// like the ML-DSA seed above.
120 pub fn secret_key_size(&self) -> usize {
121 match self {
122 SigAlgorithm::MlDsa44 => 32,
123 SigAlgorithm::MlDsa65 => 32,
124 SigAlgorithm::MlDsa87 => 32,
125 SigAlgorithm::SlhDsaSha2_128s => 64,
126 SigAlgorithm::SlhDsaSha2_128f => 64,
127 SigAlgorithm::SlhDsaSha2_192s => 96,
128 SigAlgorithm::SlhDsaSha2_192f => 96,
129 SigAlgorithm::SlhDsaSha2_256s => 128,
130 SigAlgorithm::SlhDsaSha2_256f => 128,
131 SigAlgorithm::SlhDsaShake128s => 64,
132 SigAlgorithm::SlhDsaShake128f => 64,
133 SigAlgorithm::SlhDsaShake192s => 96,
134 SigAlgorithm::SlhDsaShake192f => 96,
135 SigAlgorithm::SlhDsaShake256s => 128,
136 SigAlgorithm::SlhDsaShake256f => 128,
137 SigAlgorithm::FnDsa512 => 1345,
138 SigAlgorithm::FnDsa1024 => 2369,
139 }
140 }
141
142 /// Returns the maximum signature size in bytes for this algorithm.
143 pub fn signature_size(&self) -> usize {
144 match self {
145 SigAlgorithm::MlDsa44 => 2420,
146 SigAlgorithm::MlDsa65 => 3309,
147 SigAlgorithm::MlDsa87 => 4627,
148 SigAlgorithm::SlhDsaSha2_128s => 7856,
149 SigAlgorithm::SlhDsaSha2_128f => 17088,
150 SigAlgorithm::SlhDsaSha2_192s => 16224,
151 SigAlgorithm::SlhDsaSha2_192f => 35664,
152 SigAlgorithm::SlhDsaSha2_256s => 29792,
153 SigAlgorithm::SlhDsaSha2_256f => 49856,
154 SigAlgorithm::SlhDsaShake128s => 7856,
155 SigAlgorithm::SlhDsaShake128f => 17088,
156 SigAlgorithm::SlhDsaShake192s => 16224,
157 SigAlgorithm::SlhDsaShake192f => 35664,
158 SigAlgorithm::SlhDsaShake256s => 29792,
159 SigAlgorithm::SlhDsaShake256f => 49856,
160 SigAlgorithm::FnDsa512 => 666,
161 SigAlgorithm::FnDsa1024 => 1280,
162 }
163 }
164
165 /// Returns the FIPS standard number for this algorithm.
166 pub fn fips_standard(&self) -> &'static str {
167 match self {
168 SigAlgorithm::MlDsa44 | SigAlgorithm::MlDsa65 | SigAlgorithm::MlDsa87 => "FIPS-204",
169 SigAlgorithm::SlhDsaSha2_128s
170 | SigAlgorithm::SlhDsaSha2_128f
171 | SigAlgorithm::SlhDsaSha2_192s
172 | SigAlgorithm::SlhDsaSha2_192f
173 | SigAlgorithm::SlhDsaSha2_256s
174 | SigAlgorithm::SlhDsaSha2_256f
175 | SigAlgorithm::SlhDsaShake128s
176 | SigAlgorithm::SlhDsaShake128f
177 | SigAlgorithm::SlhDsaShake192s
178 | SigAlgorithm::SlhDsaShake192f
179 | SigAlgorithm::SlhDsaShake256s
180 | SigAlgorithm::SlhDsaShake256f => "FIPS-205",
181 SigAlgorithm::FnDsa512 | SigAlgorithm::FnDsa1024 => "FIPS-206",
182 }
183 }
184
185 /// Returns the [multicodec](https://github.com/multiformats/multicodec) code for this
186 /// algorithm's public key.
187 ///
188 /// ML-DSA and SLH-DSA codes (`0x1210`–`0x122b`) are registered (draft status) in the
189 /// upstream multicodec table. FN-DSA (FIPS 206 / Falcon) has **no upstream-registered**
190 /// multicodec code as of this writing — see [`FN_DSA_PRIVATE_USE_BASE`] for the
191 /// provisional, 0x307-namespaced private-use codes this crate assigns instead, and its
192 /// doc comment for the full rationale, scope, and revisit trigger.
193 pub fn multicodec_code(&self) -> SigResult<u32> {
194 match self {
195 SigAlgorithm::MlDsa44 => Ok(0x1210),
196 SigAlgorithm::MlDsa65 => Ok(0x1211),
197 SigAlgorithm::MlDsa87 => Ok(0x1212),
198 SigAlgorithm::SlhDsaSha2_128s => Ok(0x1220),
199 SigAlgorithm::SlhDsaShake128s => Ok(0x1221),
200 SigAlgorithm::SlhDsaSha2_128f => Ok(0x1222),
201 SigAlgorithm::SlhDsaShake128f => Ok(0x1223),
202 SigAlgorithm::SlhDsaSha2_192s => Ok(0x1224),
203 SigAlgorithm::SlhDsaShake192s => Ok(0x1225),
204 SigAlgorithm::SlhDsaSha2_192f => Ok(0x1226),
205 SigAlgorithm::SlhDsaShake192f => Ok(0x1227),
206 SigAlgorithm::SlhDsaSha2_256s => Ok(0x1228),
207 SigAlgorithm::SlhDsaShake256s => Ok(0x1229),
208 SigAlgorithm::SlhDsaSha2_256f => Ok(0x122a),
209 SigAlgorithm::SlhDsaShake256f => Ok(0x122b),
210 // Provisional, 0x307-reserved private-use codes -- see
211 // `FN_DSA_PRIVATE_USE_BASE` doc comment below for full rationale.
212 SigAlgorithm::FnDsa512 => Ok(FN_DSA_PRIVATE_USE_BASE),
213 SigAlgorithm::FnDsa1024 => Ok(FN_DSA_PRIVATE_USE_BASE + 1),
214 }
215 }
216
217 /// Returns `true` if this algorithm's [`multicodec_code`](Self::multicodec_code) is one of
218 /// this crate's own provisional private-use reservations (currently just FN-DSA), rather
219 /// than an upstream-registered multiformats/multicodec table entry.
220 ///
221 /// Useful for callers who want to warn, log, or otherwise flag when a DID Document /
222 /// Multikey they're producing embeds a non-standard code that other multicodec
223 /// implementations will not recognize.
224 pub fn is_private_use_multicodec(&self) -> bool {
225 matches!(self, SigAlgorithm::FnDsa512 | SigAlgorithm::FnDsa1024)
226 }
227}
228
229/// Base of the **provisional, `0x307`-reserved private-use multicodec range** used for
230/// FN-DSA (FIPS 206 / Falcon), because [multiformats/multicodec] has no registered code for
231/// FN-DSA/Falcon as of this writing (checked against the live table during the P2/alpha-001
232/// implementation pass — no PR, issue, or merged entry exists for FN-DSA/Falcon at either
233/// `0x1213` (the next sequential slot after ML-DSA's `0x1210`-`0x1212` block) or elsewhere).
234///
235/// [multiformats/multicodec]: https://github.com/multiformats/multicodec
236///
237/// # Decision (per project owner: "reserve and don't block upstream")
238///
239/// Rather than leaving `to_multibase()`/`from_multibase()` permanently erroring for FN-DSA
240/// until an upstream PR lands (outside 0x307's control, no committed timeline), this crate
241/// provisionally reserves its own private-use block:
242///
243/// - **`0x307000`** — `FN-DSA-512` (`SigAlgorithm::FnDsa512`)
244/// - **`0x307001`** — `FN-DSA-1024` (`SigAlgorithm::FnDsa1024`)
245///
246/// `0x307000` is deliberately namespaced after this organization (`0x307`) rather than picked
247/// arbitrarily, so a code walking a DID Document that hits an unrecognized `0x307xxx`
248/// multicodec prefix has an immediate, greppable clue about its provenance. The
249/// `0x307000`-`0x3070ff` block (256 codes) is reserved for this purpose, of which only the
250/// first two are in use today — headroom for any future 0x307-authored algorithm that hits
251/// the same "real, no upstream multicodec yet" situation FN-DSA is in now.
252///
253/// # This is intentionally *not* a claim about the officially blessed "Private Use Area"
254///
255/// The multicodec table's own header comments describe a reserved application-specific /
256/// private-use block in the `0x300000`-`0x3fffff` range, and `0x307000` happens to fall
257/// inside it — which is a reasonable coincidence given `0x307`'s own numeric name, not proof
258/// this crate re-verified that exact boundary byte-for-byte against the live `table.csv`.
259/// **Before this crate's FN-DSA Multikey output is ever consumed by a system this
260/// organization does not control, re-confirm `0x307000`-`0x3070ff` against the current
261/// upstream table** (or complete the real registration PR, which supersedes this reservation
262/// entirely — see the revisit trigger below).
263///
264/// # Consequences and caveats
265///
266/// - **Interop scope: 0x307-controlled systems only.** Any multibase-encoded FN-DSA public
267/// key produced by [`SigPublicKey::to_multibase`] carries this provisional code. A
268/// generic/independent multicodec-table-driven decoder (one that doesn't know about
269/// `0x307000`/`0x307001` specifically) will not recognize it as FN-DSA — it isn't in the
270/// canonical table. This is fine for internal SAGP DID documents (the actual, current use
271/// case — see `docs/pqc/migration.md`'s FN-DSA-as-compact-signature design), but it is
272/// **not** a substitute for real interop with third-party multicodec/DID tooling.
273/// - **This is a breaking change, shipped as one.** `multicodec_code()` previously returned
274/// `Err(..)` for FN-DSA; `STABILITY.md` §2 lists "changing documented behavior... including
275/// error-vs-success outcomes" as breaking, explicitly and without a doc-comment-worded-it-as-
276/// provisional exception. It ships in a minor version bump alongside another, independently
277/// breaking change in this same release — see `CHANGELOG.md`'s migration note.
278/// - **Revisit trigger:** if/when multiformats/multicodec registers an official code for
279/// FN-DSA/Falcon, migrate `multicodec_code()` to that value in a follow-up **minor**
280/// release (changing an already-provisional, explicitly-flagged-non-final code is not the
281/// same kind of break as changing an already-stable one — but it does still change the
282/// wire bytes of anything encoded with `0x307000`/`0x307001` in the interim, so it must
283/// still carry a `CHANGELOG.md` migration note per `STABILITY.md`, and any 0x307-internal
284/// system that persisted FN-DSA Multikeys before that point needs a re-encode pass).
285pub const FN_DSA_PRIVATE_USE_BASE: u32 = 0x307000;
286
287/// Encode a multicodec code as an unsigned varint (LEB128, per the
288/// [multiformats unsigned-varint spec](https://github.com/multiformats/unsigned-varint)).
289fn encode_varint(mut code: u32) -> Vec<u8> {
290 let mut out = Vec::with_capacity(2);
291 loop {
292 let mut byte = (code & 0x7f) as u8;
293 code >>= 7;
294 if code != 0 {
295 byte |= 0x80;
296 }
297 out.push(byte);
298 if code == 0 {
299 break;
300 }
301 }
302 out
303}
304
305/// Decode an unsigned varint from the start of `bytes`, returning the decoded value and the
306/// number of bytes consumed.
307fn decode_varint(bytes: &[u8]) -> SigResult<(u32, usize)> {
308 let mut value: u32 = 0;
309 for (i, &byte) in bytes.iter().enumerate() {
310 if i >= 5 {
311 return Err(SigError::InvalidPublicKey("multicodec varint prefix too long".into()));
312 }
313 value |= ((byte & 0x7f) as u32) << (7 * i);
314 if byte & 0x80 == 0 {
315 return Ok((value, i + 1));
316 }
317 }
318 Err(SigError::InvalidPublicKey("truncated multicodec varint prefix".into()))
319}
320
321impl core::fmt::Display for SigAlgorithm {
322 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
323 write!(f, "{}", self.as_str())
324 }
325}
326
327// ── Public Key ────────────────────────────────────────────────────────────────
328
329/// A post-quantum public key with its algorithm tag.
330///
331/// Public keys are safe to distribute and do not require zeroization.
332#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
333pub struct SigPublicKey {
334 /// The algorithm this key belongs to.
335 pub algorithm: SigAlgorithm,
336 /// Raw public key bytes.
337 #[serde(
338 serialize_with = "serialize_bytes_base64url",
339 deserialize_with = "deserialize_bytes_base64url"
340 )]
341 pub bytes: Vec<u8>,
342}
343
344impl SigPublicKey {
345 /// Create a new public key from raw bytes.
346 pub fn new(algorithm: SigAlgorithm, bytes: Vec<u8>) -> Self {
347 Self { algorithm, bytes }
348 }
349
350 /// Returns the raw bytes of the public key.
351 pub fn as_bytes(&self) -> &[u8] {
352 &self.bytes
353 }
354
355 /// Encode the public key bytes as base64url (no padding).
356 pub fn to_base64url(&self) -> String {
357 Base64Url::encode_string(&self.bytes)
358 }
359
360 /// Decode a public key from base64url bytes.
361 pub fn from_base64url(algorithm: SigAlgorithm, encoded: &str) -> SigResult<Self> {
362 let bytes = Base64Url::decode_vec(encoded)
363 .map_err(|e| SigError::Base64Decode(e.to_string()))?;
364 Ok(Self { algorithm, bytes })
365 }
366
367 /// Encode as a [W3C Multikey](https://www.w3.org/TR/controller-document/#multikey):
368 /// a multicodec-prefixed key, multibase-encoded as base58btc with a 'z' prefix.
369 ///
370 /// Works for all 17 algorithms this crate supports, including FN-DSA — which uses a
371 /// provisional, `0x307`-reserved private-use multicodec code rather than an
372 /// upstream-registered one; see [`FN_DSA_PRIVATE_USE_BASE`] for the full rationale and
373 /// interop scope. In practice this only returns `Err` for malformed/mismatched input, not
374 /// for any currently-defined [`SigAlgorithm`] variant.
375 pub fn to_multibase(&self) -> SigResult<String> {
376 let code = self.algorithm.multicodec_code()?;
377 let mut prefixed = encode_varint(code);
378 prefixed.extend_from_slice(&self.bytes);
379
380 let mut out = String::with_capacity(prefixed.len() * 2);
381 out.push('z');
382 out.push_str(&bs58::encode(&prefixed).into_string());
383 Ok(out)
384 }
385
386 /// Decode from a [W3C Multikey](https://www.w3.org/TR/controller-document/#multikey)
387 /// (base58btc with 'z' prefix). Verifies the embedded multicodec code matches `algorithm`.
388 pub fn from_multibase(algorithm: SigAlgorithm, multibase: &str) -> SigResult<Self> {
389 let expected_code = algorithm.multicodec_code()?;
390
391 let stripped = multibase.strip_prefix('z').ok_or_else(|| {
392 SigError::InvalidPublicKey("multibase must start with 'z' (base58btc)".into())
393 })?;
394 let prefixed = bs58::decode(stripped)
395 .into_vec()
396 .map_err(|e| SigError::InvalidPublicKey(format!("base58 decode: {}", e)))?;
397
398 let (code, prefix_len) = decode_varint(&prefixed)?;
399 if code != expected_code {
400 return Err(SigError::InvalidPublicKey(format!(
401 "multicodec code 0x{:x} does not match expected code 0x{:x} for {}",
402 code, expected_code, algorithm.as_str()
403 )));
404 }
405
406 let bytes = prefixed[prefix_len..].to_vec();
407 Ok(Self { algorithm, bytes })
408 }
409
410 /// Serialize to JSON string.
411 pub fn to_json(&self) -> SigResult<String> {
412 serde_json::to_string(self)
413 .map_err(|e| SigError::Serialization(e.to_string()))
414 }
415
416 /// Deserialize from JSON string.
417 pub fn from_json(json: &str) -> SigResult<Self> {
418 serde_json::from_str(json)
419 .map_err(|e| SigError::Serialization(e.to_string()))
420 }
421}
422
423// ── Secret Key ────────────────────────────────────────────────────────────────
424
425/// A post-quantum secret key with its algorithm tag. Zeroized on drop.
426#[derive(Clone, Zeroize, ZeroizeOnDrop)]
427pub struct SigSecretKey {
428 /// The algorithm this key belongs to.
429 /// Skipped during zeroization — `SigAlgorithm` is a plain enum with no secret data.
430 #[zeroize(skip)]
431 pub algorithm: SigAlgorithm,
432 /// Raw secret key bytes (zeroized on drop).
433 pub bytes: Vec<u8>,
434}
435
436impl core::fmt::Debug for SigSecretKey {
437 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
438 write!(f, "SigSecretKey {{ algorithm: {:?}, bytes: [REDACTED {} bytes] }}", self.algorithm, self.bytes.len())
439 }
440}
441
442impl SigSecretKey {
443 /// Create a new secret key from raw bytes.
444 pub fn new(algorithm: SigAlgorithm, bytes: Vec<u8>) -> Self {
445 Self { algorithm, bytes }
446 }
447
448 /// Returns the raw bytes of the secret key.
449 pub fn as_bytes(&self) -> &[u8] {
450 &self.bytes
451 }
452
453 /// Encode the secret key bytes as base64url (no padding).
454 pub fn to_base64url(&self) -> String {
455 Base64Url::encode_string(&self.bytes)
456 }
457
458 /// Decode a secret key from base64url bytes.
459 pub fn from_base64url(algorithm: SigAlgorithm, encoded: &str) -> SigResult<Self> {
460 let bytes = Base64Url::decode_vec(encoded)
461 .map_err(|e| SigError::Base64Decode(e.to_string()))?;
462 Ok(Self { algorithm, bytes })
463 }
464}
465
466// ── Signature ─────────────────────────────────────────────────────────────────
467
468/// A post-quantum signature with its algorithm tag.
469#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
470pub struct Signature {
471 /// The algorithm used to produce this signature.
472 pub algorithm: SigAlgorithm,
473 /// Raw signature bytes.
474 #[serde(
475 serialize_with = "serialize_bytes_base64url",
476 deserialize_with = "deserialize_bytes_base64url"
477 )]
478 pub bytes: Vec<u8>,
479}
480
481impl Signature {
482 /// Create a new signature from raw bytes.
483 pub fn new(algorithm: SigAlgorithm, bytes: Vec<u8>) -> Self {
484 Self { algorithm, bytes }
485 }
486
487 /// Returns the raw bytes of the signature.
488 pub fn as_bytes(&self) -> &[u8] {
489 &self.bytes
490 }
491
492 /// Encode the signature bytes as base64url (no padding).
493 pub fn to_base64url(&self) -> String {
494 Base64Url::encode_string(&self.bytes)
495 }
496
497 /// Decode a signature from base64url bytes.
498 pub fn from_base64url(algorithm: SigAlgorithm, encoded: &str) -> SigResult<Self> {
499 let bytes = Base64Url::decode_vec(encoded)
500 .map_err(|e| SigError::Base64Decode(e.to_string()))?;
501 Ok(Self { algorithm, bytes })
502 }
503
504 /// Serialize to JSON string (for WASM boundary crossing).
505 pub fn to_json(&self) -> SigResult<String> {
506 serde_json::to_string(self)
507 .map_err(|e| SigError::Serialization(e.to_string()))
508 }
509
510 /// Deserialize from JSON string.
511 pub fn from_json(json: &str) -> SigResult<Self> {
512 serde_json::from_str(json)
513 .map_err(|e| SigError::Serialization(e.to_string()))
514 }
515}
516
517// ── Signed Message Envelope ───────────────────────────────────────────────────
518
519/// A signed message envelope — bundles the message, signature, and public key together.
520///
521/// Useful for self-contained attestation payloads that can be verified without
522/// any external context.
523#[derive(Debug, Clone, Serialize, Deserialize)]
524pub struct SignedMessage {
525 /// The original message bytes (base64url encoded in JSON).
526 #[serde(
527 serialize_with = "serialize_bytes_base64url",
528 deserialize_with = "deserialize_bytes_base64url"
529 )]
530 pub message: Vec<u8>,
531 /// The signature over the message.
532 pub signature: Signature,
533 /// The public key that produced the signature.
534 pub public_key: SigPublicKey,
535 /// Algorithm identifier string (e.g. "ML-DSA-65").
536 pub algorithm: String,
537}
538
539impl SignedMessage {
540 /// Create a new signed message envelope.
541 pub fn new(message: Vec<u8>, signature: Signature, public_key: SigPublicKey) -> Self {
542 let algorithm = signature.algorithm.as_str().to_string();
543 Self { message, signature, public_key, algorithm }
544 }
545
546 /// Serialize to JSON string.
547 pub fn to_json(&self) -> SigResult<String> {
548 serde_json::to_string(self)
549 .map_err(|e| SigError::Serialization(e.to_string()))
550 }
551
552 /// Deserialize from JSON string.
553 pub fn from_json(json: &str) -> SigResult<Self> {
554 serde_json::from_str(json)
555 .map_err(|e| SigError::Serialization(e.to_string()))
556 }
557}
558
559// ── Serde Helpers ─────────────────────────────────────────────────────────────
560
561pub(crate) fn serialize_bytes_base64url<S>(bytes: &[u8], serializer: S) -> Result<S::Ok, S::Error>
562where
563 S: serde::Serializer,
564{
565 serializer.serialize_str(&Base64Url::encode_string(bytes))
566}
567
568pub(crate) fn deserialize_bytes_base64url<'de, D>(deserializer: D) -> Result<Vec<u8>, D::Error>
569where
570 D: serde::Deserializer<'de>,
571{
572 let s = String::deserialize(deserializer)?;
573 Base64Url::decode_vec(&s).map_err(serde::de::Error::custom)
574}