Skip to main content

quantum_shield/
types.rs

1//! Wire types: [`Envelope`] and [`HybridSignature`].
2
3use crate::constants::*;
4use crate::error::{Error, Result};
5use crate::wire::{read_header, take, write_header};
6use alloc::boxed::Box;
7use alloc::vec::Vec;
8
9/// An encrypted message: hybrid KEM ciphertext plus AEAD-protected payload.
10///
11/// Wire layout (`QSE2`):
12///
13/// ```text
14/// magic[4] | version u8 | suite u8 | epk_x25519[32] | ct_mlkem[1568] | nonce[12] | aead_ct[..]
15/// ```
16///
17/// Everything before `aead_ct` is authenticated as AEAD associated data, so
18/// no header field can be modified without failing decryption.
19#[derive(Clone, Debug, PartialEq, Eq)]
20pub struct Envelope {
21    /// Sender's ephemeral X25519 public key.
22    pub(crate) epk_x25519: [u8; X25519_PK_LEN],
23    /// ML-KEM-1024 ciphertext.
24    pub(crate) ct_mlkem: Box<[u8; MLKEM1024_CT_LEN]>,
25    /// AES-256-GCM nonce.
26    pub(crate) nonce: [u8; NONCE_LEN],
27    /// AES-256-GCM ciphertext (plaintext length + 16-byte tag).
28    pub(crate) ciphertext: Vec<u8>,
29}
30
31impl Envelope {
32    /// Serialize to the v2 envelope format.
33    pub fn to_bytes(&self) -> Vec<u8> {
34        let mut out = Vec::with_capacity(ENVELOPE_AAD_LEN + self.ciphertext.len());
35        self.write_aad(&mut out);
36        out.extend_from_slice(&self.ciphertext);
37        out
38    }
39
40    /// Write the authenticated prefix (header through nonce) to `out`.
41    pub(crate) fn write_aad(&self, out: &mut Vec<u8>) {
42        let start = out.len();
43        write_header(out, MAGIC_ENVELOPE);
44        out.extend_from_slice(&self.epk_x25519);
45        out.extend_from_slice(self.ct_mlkem.as_ref());
46        out.extend_from_slice(&self.nonce);
47        debug_assert_eq!(out.len() - start, ENVELOPE_AAD_LEN);
48    }
49
50    /// Parse a v2 envelope.
51    ///
52    /// # Errors
53    ///
54    /// Returns [`Error::InvalidEnvelope`] on malformed input,
55    /// [`Error::LegacyV1Artifact`] for 0.1.x JSON artifacts, and
56    /// version/suite errors for unknown formats.
57    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
58        let mut rest = read_header(bytes, MAGIC_ENVELOPE, Error::InvalidEnvelope)?;
59        let epk_x25519 = take(&mut rest, Error::InvalidEnvelope)?;
60        let ct_mlkem: [u8; MLKEM1024_CT_LEN] = take(&mut rest, Error::InvalidEnvelope)?;
61        let nonce = take(&mut rest, Error::InvalidEnvelope)?;
62        if rest.len() < TAG_LEN {
63            return Err(Error::InvalidEnvelope);
64        }
65        Ok(Self {
66            epk_x25519,
67            ct_mlkem: Box::new(ct_mlkem),
68            nonce,
69            ciphertext: rest.to_vec(),
70        })
71    }
72}
73
74/// A hybrid signature: Ed25519 and ML-DSA-87, both always present.
75///
76/// Wire layout (`QSS2`, fixed 4697 bytes):
77///
78/// ```text
79/// magic[4] | version u8 | suite u8 | ed25519_sig[64] | mldsa_sig[4627]
80/// ```
81///
82/// Verification requires **both** components to be valid; there is no way to
83/// strip the post-quantum signature and still verify.
84#[derive(Clone, Debug, PartialEq, Eq)]
85pub struct HybridSignature {
86    /// Ed25519 signature over the framed message.
87    pub(crate) ed25519: [u8; ED25519_SIG_LEN],
88    /// ML-DSA-87 signature over the framed message.
89    pub(crate) mldsa: Box<[u8; MLDSA87_SIG_LEN]>,
90}
91
92impl HybridSignature {
93    /// Serialize to the v2 signature format (fixed 4697 bytes).
94    pub fn to_bytes(&self) -> Vec<u8> {
95        let mut out = Vec::with_capacity(SIGNATURE_LEN);
96        write_header(&mut out, MAGIC_SIGNATURE);
97        out.extend_from_slice(&self.ed25519);
98        out.extend_from_slice(self.mldsa.as_ref());
99        debug_assert_eq!(out.len(), SIGNATURE_LEN);
100        out
101    }
102
103    /// Parse a v2 signature.
104    ///
105    /// # Errors
106    ///
107    /// Returns [`Error::InvalidSignature`] on malformed input,
108    /// [`Error::LegacyV1Artifact`] for 0.1.x JSON artifacts, and
109    /// version/suite errors for unknown formats.
110    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
111        let mut rest = read_header(bytes, MAGIC_SIGNATURE, Error::InvalidSignature)?;
112        let ed25519 = take(&mut rest, Error::InvalidSignature)?;
113        let mldsa: [u8; MLDSA87_SIG_LEN] = take(&mut rest, Error::InvalidSignature)?;
114        if !rest.is_empty() {
115            return Err(Error::InvalidSignature);
116        }
117        Ok(Self {
118            ed25519,
119            mldsa: Box::new(mldsa),
120        })
121    }
122}
123
124#[cfg(test)]
125mod tests {
126    use super::*;
127
128    fn dummy_envelope() -> Envelope {
129        Envelope {
130            epk_x25519: [1; X25519_PK_LEN],
131            ct_mlkem: Box::new([2; MLKEM1024_CT_LEN]),
132            nonce: [3; NONCE_LEN],
133            ciphertext: vec![4; 40],
134        }
135    }
136
137    #[test]
138    fn envelope_roundtrip() {
139        let env = dummy_envelope();
140        let bytes = env.to_bytes();
141        assert_eq!(bytes.len(), ENVELOPE_AAD_LEN + 40);
142        assert_eq!(Envelope::from_bytes(&bytes).unwrap(), env);
143    }
144
145    #[test]
146    fn envelope_rejects_truncation() {
147        let bytes = dummy_envelope().to_bytes();
148        // Anything shorter than AAD + tag must fail.
149        for len in [
150            0,
151            5,
152            HEADER_LEN,
153            ENVELOPE_AAD_LEN,
154            ENVELOPE_AAD_LEN + TAG_LEN - 1,
155        ] {
156            assert!(Envelope::from_bytes(&bytes[..len]).is_err(), "len={len}");
157        }
158    }
159
160    #[test]
161    fn signature_roundtrip() {
162        let sig = HybridSignature {
163            ed25519: [5; ED25519_SIG_LEN],
164            mldsa: Box::new([6; MLDSA87_SIG_LEN]),
165        };
166        let bytes = sig.to_bytes();
167        assert_eq!(bytes.len(), SIGNATURE_LEN);
168        assert_eq!(HybridSignature::from_bytes(&bytes).unwrap(), sig);
169    }
170
171    #[test]
172    fn signature_rejects_wrong_length() {
173        let sig = HybridSignature {
174            ed25519: [5; ED25519_SIG_LEN],
175            mldsa: Box::new([6; MLDSA87_SIG_LEN]),
176        };
177        let bytes = sig.to_bytes();
178        assert_eq!(
179            HybridSignature::from_bytes(&bytes[..bytes.len() - 1]).unwrap_err(),
180            Error::InvalidSignature
181        );
182        let mut long = bytes.clone();
183        long.push(0);
184        assert_eq!(
185            HybridSignature::from_bytes(&long).unwrap_err(),
186            Error::InvalidSignature
187        );
188    }
189}