Skip to main content

quantica/
prehash.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Cédric Mesnil <cslashm@pm.me>
3
4//! Pre-hash selectors and `M′` message encoding for the external
5//! signature APIs (FIPS 204 §5.4 / FIPS 205 §10.2).
6//!
7//! Both ML-DSA (HashML-DSA) and SLH-DSA (HashSLH-DSA) wrap the message
8//! into `M′` for domain separation before calling the internal signer:
9//!
10//! - pure:    `M′ = 0x00 ‖ len(ctx) ‖ ctx ‖ M`
11//! - prehash: `M′ = 0x01 ‖ len(ctx) ‖ ctx ‖ OID(PH) ‖ PH(M)`
12//!
13//! `OID(PH)` is the full DER object identifier (tag + length + content,
14//! 11 bytes for every NIST CSOR hash/XOF) per FIPS 204 Algorithm 4. The
15//! pre-hash digest is computed from the shared `krypteia-tessera`
16//! primitives; SHAKE128 is squeezed to 256 bits and SHAKE256 to 512 bits.
17//!
18//! `M′` is built over public data (domain tag, context, OID, message or
19//! its digest) — no secret-dependent branch, index, or length. The only
20//! decision is the public `|ctx| ≤ 255` check, done by the callers.
21
22use alloc::vec::Vec;
23use tessera::{
24    Digest, Sha3_224, Sha3_256, Sha3_384, Sha3_512, Sha224, Sha256, Sha384, Sha512, Sha512_224, Sha512_256, Shake128,
25    Shake256, Xof,
26};
27
28/// Approved pre-hash function for HashML-DSA / HashSLH-DSA
29/// (FIPS 204 §5.4, FIPS 205 §10.2). Selects both the digest computed over
30/// the message and the DER OID embedded in `M′`.
31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
32pub enum PreHash {
33    /// SHA-256 (OID 2.16.840.1.101.3.4.2.1).
34    Sha256,
35    /// SHA-384 (OID 2.16.840.1.101.3.4.2.2).
36    Sha384,
37    /// SHA-512 (OID 2.16.840.1.101.3.4.2.3).
38    Sha512,
39    /// SHA-224 (OID 2.16.840.1.101.3.4.2.4).
40    Sha224,
41    /// SHA-512/224 (OID 2.16.840.1.101.3.4.2.5).
42    Sha512_224,
43    /// SHA-512/256 (OID 2.16.840.1.101.3.4.2.6).
44    Sha512_256,
45    /// SHA3-224 (OID 2.16.840.1.101.3.4.2.7).
46    Sha3_224,
47    /// SHA3-256 (OID 2.16.840.1.101.3.4.2.8).
48    Sha3_256,
49    /// SHA3-384 (OID 2.16.840.1.101.3.4.2.9).
50    Sha3_384,
51    /// SHA3-512 (OID 2.16.840.1.101.3.4.2.10).
52    Sha3_512,
53    /// SHAKE128 squeezed to 256 bits (OID 2.16.840.1.101.3.4.2.11).
54    Shake128,
55    /// SHAKE256 squeezed to 512 bits (OID 2.16.840.1.101.3.4.2.12).
56    Shake256,
57}
58
59impl PreHash {
60    /// DER encoding of the function's OID (tag `0x06`, length `0x09`, then
61    /// the nine content bytes), per FIPS 204 Algorithm 4 — verified against
62    /// the NIST.FIPS.204 primary source. The arcs differ only in the final
63    /// byte (the CSOR `hashAlgs` index).
64    pub(crate) fn oid(self) -> &'static [u8] {
65        // DER TLV `06 09 60 86 48 01 65 03 04 02 XX`; only the final CSOR
66        // `hashAlgs` index XX differs. Array-literal refs promote to 'static.
67        match self {
68            PreHash::Sha256 => &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01],
69            PreHash::Sha384 => &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x02],
70            PreHash::Sha512 => &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x03],
71            PreHash::Sha224 => &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x04],
72            PreHash::Sha512_224 => &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x05],
73            PreHash::Sha512_256 => &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x06],
74            PreHash::Sha3_224 => &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x07],
75            PreHash::Sha3_256 => &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x08],
76            PreHash::Sha3_384 => &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x09],
77            PreHash::Sha3_512 => &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x0A],
78            PreHash::Shake128 => &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x0B],
79            PreHash::Shake256 => &[0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x0C],
80        }
81    }
82
83    /// Compute `PH(M)`. Fixed-output hashes return their digest; SHAKE128
84    /// is squeezed to 256 bits and SHAKE256 to 512 bits (FIPS 204 §5.4).
85    pub(crate) fn hash(self, m: &[u8]) -> Vec<u8> {
86        fn fixed<H: Digest>(m: &[u8]) -> Vec<u8> {
87            let mut out = alloc::vec![0u8; H::OUTPUT_LEN];
88            H::digest(m, &mut out);
89            out
90        }
91        fn xof<X: Xof>(m: &[u8], out_len: usize) -> Vec<u8> {
92            let mut x = X::new();
93            x.update(m);
94            let mut out = alloc::vec![0u8; out_len];
95            x.squeeze(&mut out);
96            out
97        }
98        match self {
99            PreHash::Sha256 => fixed::<Sha256>(m),
100            PreHash::Sha384 => fixed::<Sha384>(m),
101            PreHash::Sha512 => fixed::<Sha512>(m),
102            PreHash::Sha224 => fixed::<Sha224>(m),
103            PreHash::Sha512_224 => fixed::<Sha512_224>(m),
104            PreHash::Sha512_256 => fixed::<Sha512_256>(m),
105            PreHash::Sha3_224 => fixed::<Sha3_224>(m),
106            PreHash::Sha3_256 => fixed::<Sha3_256>(m),
107            PreHash::Sha3_384 => fixed::<Sha3_384>(m),
108            PreHash::Sha3_512 => fixed::<Sha3_512>(m),
109            PreHash::Shake128 => xof::<Shake128>(m, 32),
110            PreHash::Shake256 => xof::<Shake256>(m, 64),
111        }
112    }
113}
114
115/// Pure external `M′ = 0x00 ‖ len(ctx) ‖ ctx ‖ M`
116/// (FIPS 204 Algorithm 2 / FIPS 205 Algorithm 22).
117///
118/// The caller must ensure `ctx.len() <= 255` (mapped to the caller's own
119/// `ContextTooLong` error) before calling.
120pub(crate) fn m_prime_pure(ctx: &[u8], msg: &[u8]) -> Vec<u8> {
121    let mut out = Vec::with_capacity(2 + ctx.len() + msg.len());
122    out.push(0x00);
123    out.push(ctx.len() as u8);
124    out.extend_from_slice(ctx);
125    out.extend_from_slice(msg);
126    out
127}
128
129/// Pre-hash external `M′ = 0x01 ‖ len(ctx) ‖ ctx ‖ OID(PH) ‖ PH(M)`
130/// (FIPS 204 Algorithm 4 / FIPS 205 Algorithm 23).
131///
132/// The caller must ensure `ctx.len() <= 255` before calling.
133pub(crate) fn m_prime_prehash(ctx: &[u8], ph: PreHash, msg: &[u8]) -> Vec<u8> {
134    let oid = ph.oid();
135    let digest = ph.hash(msg);
136    let mut out = Vec::with_capacity(2 + ctx.len() + oid.len() + digest.len());
137    out.push(0x01);
138    out.push(ctx.len() as u8);
139    out.extend_from_slice(ctx);
140    out.extend_from_slice(oid);
141    out.extend_from_slice(&digest);
142    out
143}
144
145#[cfg(test)]
146mod tests {
147    use super::*;
148
149    #[test]
150    fn oids_are_11_byte_der_tlv() {
151        for ph in [
152            PreHash::Sha256,
153            PreHash::Sha384,
154            PreHash::Sha512,
155            PreHash::Sha224,
156            PreHash::Sha512_224,
157            PreHash::Sha512_256,
158            PreHash::Sha3_224,
159            PreHash::Sha3_256,
160            PreHash::Sha3_384,
161            PreHash::Sha3_512,
162            PreHash::Shake128,
163            PreHash::Shake256,
164        ] {
165            let oid = ph.oid();
166            assert_eq!(oid.len(), 11);
167            assert_eq!(oid[0], 0x06); // DER OBJECT IDENTIFIER tag
168            assert_eq!(oid[1], 0x09); // length
169            assert_eq!(&oid[2..10], &[0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02]);
170        }
171        // Spot-check the FIPS 204 Algorithm 4 examples (verbatim).
172        assert_eq!(PreHash::Sha256.oid()[10], 0x01);
173        assert_eq!(PreHash::Sha512.oid()[10], 0x03);
174        assert_eq!(PreHash::Shake128.oid()[10], 0x0B);
175    }
176
177    #[test]
178    fn shake_prehash_output_lengths() {
179        assert_eq!(PreHash::Shake128.hash(b"x").len(), 32); // 256 bits
180        assert_eq!(PreHash::Shake256.hash(b"x").len(), 64); // 512 bits
181        assert_eq!(PreHash::Sha256.hash(b"x").len(), 32);
182        assert_eq!(PreHash::Sha512.hash(b"x").len(), 64);
183    }
184
185    #[test]
186    fn pure_m_prime_layout() {
187        let mp = m_prime_pure(b"ctx", b"msg");
188        assert_eq!(mp[0], 0x00);
189        assert_eq!(mp[1], 3);
190        assert_eq!(&mp[2..5], b"ctx");
191        assert_eq!(&mp[5..], b"msg");
192    }
193
194    #[test]
195    fn prehash_m_prime_layout() {
196        let mp = m_prime_prehash(b"", PreHash::Sha256, b"msg");
197        assert_eq!(mp[0], 0x01);
198        assert_eq!(mp[1], 0); // empty ctx
199        assert_eq!(&mp[2..13], PreHash::Sha256.oid());
200        assert_eq!(mp.len(), 2 + 11 + 32);
201    }
202
203    fn hexd(s: &str) -> Vec<u8> {
204        (0..s.len())
205            .step_by(2)
206            .map(|i| u8::from_str_radix(&s[i..i + 2], 16).unwrap())
207            .collect()
208    }
209
210    #[test]
211    fn prehash_m_prime_matches_fips_reference() {
212        // Independently-computed references for every PreHash (ctx empty,
213        // msg = b"abc"): `0x01 ‖ 0x00 ‖ OID(PH) ‖ PH("abc")`. Cross-checks the
214        // OID byte strings *and* the underlying digests in one shot.
215        let refs: &[(PreHash, &str)] = &[
216            (
217                PreHash::Sha384,
218                "01000609608648016503040202cb00753f45a35e8bb5a03d699ac65007272c32ab0eded1631a8b605a43ff5bed8086072ba1e7cc2358baeca134c825a7",
219            ),
220            (
221                PreHash::Shake256,
222                "0100060960864801650304020c483366601360a8771c6863080cc4114d8db44530f8f1e1ee4f94ea37e78b5739d5a15bef186a5386c75744c0527e1faa9f8726e462a12a4feb06bd8801e751e4",
223            ),
224            (
225                PreHash::Sha3_224,
226                "01000609608648016503040207e642824c3f8cf24ad09234ee7d3c766fc9a3a5168d0c94ad73b46fdf",
227            ),
228            (
229                PreHash::Sha3_256,
230                "010006096086480165030402083a985da74fe225b2045c172d6bd390bd855f086e3e9d525b46bfe24511431532",
231            ),
232            (
233                PreHash::Sha3_384,
234                "01000609608648016503040209ec01498288516fc926459f58e2c6ad8df9b473cb0fc08c2596da7cf0e49be4b298d88cea927ac7f539f1edf228376d25",
235            ),
236            (
237                PreHash::Sha3_512,
238                "0100060960864801650304020ab751850b1a57168a5693cd924b6b096e08f621827444f70d884f5d0240d2712e10e116e9192af3c91a7ec57647e3934057340b4cf408d5a56592f8274eec53f0",
239            ),
240            (
241                PreHash::Sha224,
242                "0100060960864801650304020423097d223405d8228642a477bda255b32aadbce4bda0b3f7e36c9da7",
243            ),
244            (
245                PreHash::Sha256,
246                "01000609608648016503040201ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
247            ),
248            (
249                PreHash::Sha512,
250                "01000609608648016503040203ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f",
251            ),
252            (
253                PreHash::Sha512_224,
254                "010006096086480165030402054634270f707b6a54daae7530460842e20e37ed265ceee9a43e8924aa",
255            ),
256            (
257                PreHash::Sha512_256,
258                "0100060960864801650304020653048e2681941ef99b2e29b76b4c7dabe4c2d0c634fc6d46e0e2f13107e7af23",
259            ),
260            (
261                PreHash::Shake128,
262                "0100060960864801650304020b5881092dd818bf5cf8a3ddb793fbcba74097d5c526a6d35f97b83351940f2cc8",
263            ),
264        ];
265        for (ph, want) in refs {
266            assert_eq!(m_prime_prehash(b"", *ph, b"abc"), hexd(want), "{ph:?} M' mismatch");
267        }
268    }
269}