Skip to main content

arcana/drbg/
hmac.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Cédric Mesnil <cslashm@pm.me>
3
4//! HMAC_DRBG — SP 800-90A Rev.1 §10.1.2.
5//!
6//! Generic over any [`tessera::Digest`], driving [`tessera::hmac`](fn@tessera::hmac); used in
7//! practice over HMAC-SHA-256 / -384 / -512. Both the state size and the
8//! output block equal the digest's output length `outlen`.
9//!
10//! # Parameters (SP 800-90A §10.1, Table 2)
11//!
12//! | Hash        | security_strength | outlen (octets) |
13//! |-------------|-------------------|-----------------|
14//! | SHA-1       | 128               | 20              |
15//! | SHA-224     | 192               | 28              |
16//! | SHA-256     | 256               | 32              |
17//! | SHA-384     | 256               | 48              |
18//! | SHA-512     | 256               | 64              |
19//!
20//! `reseed_interval = 2^48`, `max_number_of_bits_per_request = 2^19`.
21//!
22//! # Side-channel posture
23//!
24//! `(K, V)` are secret. The construction is *branch-free by shape*: the
25//! only value-dependent control flow is the `provided_data.is_empty()`
26//! test of the Update (§10.1.2.2), which is on the (public) presence of
27//! additional input, not on secret content. HMAC-SHA-2 inherits the
28//! carry-based DPA of `T2-D` (see [`crate::drbg`]); no table lookup on
29//! secret data.
30
31use crate::drbg::Error;
32use tessera::{Digest, hmac_multi};
33
34/// `reseed_interval` (SP 800-90A §10.1, Table 2): 2^48.
35const RESEED_INTERVAL: u64 = 1 << 48;
36/// `max_number_of_bits_per_request` = 2^19 bits = 65 536 octets.
37const MAX_REQUEST_OCTETS: usize = 1 << 16;
38/// Practical cap on optional input lengths.
39const MAX_OPT_OCTETS: usize = u32::MAX as usize;
40
41/// Largest digest output among the crate's digests (SHA-512 → 64 octets).
42const MAX_OUTPUT: usize = 64;
43
44/// HMAC_DRBG internal state (SP 800-90A §10.1.2.1): `(K, V)` each `outlen`
45/// octets, plus the `reseed_counter`. `K` and `V` are secret.
46pub struct HmacDrbg<H: Digest> {
47    k: [u8; MAX_OUTPUT],
48    v: [u8; MAX_OUTPUT],
49    reseed_counter: u64,
50    _marker: core::marker::PhantomData<H>,
51}
52
53impl<H: Digest> HmacDrbg<H> {
54    /// The HMAC_DRBG Update function (SP 800-90A §10.1.2.2).
55    ///
56    /// ```text
57    ///   K = HMAC(K, V || 0x00 || provided_data)
58    ///   V = HMAC(K, V)
59    ///   if provided_data == Null: return
60    ///   K = HMAC(K, V || 0x01 || provided_data)
61    ///   V = HMAC(K, V)
62    /// ```
63    /// `provided_parts` are the (possibly several) chunks of `provided_data`
64    /// (e.g. `entropy || additional_input`), concatenated logically.
65    fn update(&mut self, provided_parts: &[&[u8]]) {
66        let outlen = H::OUTPUT_LEN;
67        let has_data = provided_parts.iter().any(|p| !p.is_empty());
68
69        // K = HMAC(K, V || 0x00 || provided_data)
70        let mut newk = [0u8; MAX_OUTPUT];
71        {
72            let mut parts: [&[u8]; 8] = [&[]; 8];
73            let n = assemble(&mut parts, &self.v[..outlen], 0x00, provided_parts);
74            hmac_multi::<H>(&self.k[..outlen], &parts[..n], &mut newk[..outlen]);
75        }
76        self.k[..outlen].copy_from_slice(&newk[..outlen]);
77        // V = HMAC(K, V)
78        let mut newv = [0u8; MAX_OUTPUT];
79        hmac_multi::<H>(&self.k[..outlen], &[&self.v[..outlen]], &mut newv[..outlen]);
80        self.v[..outlen].copy_from_slice(&newv[..outlen]);
81
82        if !has_data {
83            return;
84        }
85
86        // K = HMAC(K, V || 0x01 || provided_data)
87        {
88            let mut parts: [&[u8]; 8] = [&[]; 8];
89            let n = assemble(&mut parts, &self.v[..outlen], 0x01, provided_parts);
90            hmac_multi::<H>(&self.k[..outlen], &parts[..n], &mut newk[..outlen]);
91        }
92        self.k[..outlen].copy_from_slice(&newk[..outlen]);
93        // V = HMAC(K, V)
94        hmac_multi::<H>(&self.k[..outlen], &[&self.v[..outlen]], &mut newv[..outlen]);
95        self.v[..outlen].copy_from_slice(&newv[..outlen]);
96    }
97
98    /// Instantiate an HMAC_DRBG (SP 800-90A §10.1.2.3).
99    ///
100    /// ```text
101    ///   seed_material = entropy_input || nonce || personalization_string
102    ///   K = 0x00 00 … 00   (outlen octets)
103    ///   V = 0x01 01 … 01   (outlen octets)
104    ///   (K, V) = Update(seed_material, K, V)
105    ///   reseed_counter = 1
106    /// ```
107    pub fn instantiate(entropy_input: &[u8], nonce: &[u8], personalization: &[u8]) -> Result<Self, Error> {
108        if entropy_input.len() * 8 < security_strength::<H>() {
109            return Err(Error::InsufficientEntropy);
110        }
111        if personalization.len() > MAX_OPT_OCTETS {
112            return Err(Error::PersonalizationTooLong);
113        }
114        let outlen = H::OUTPUT_LEN;
115        let mut s = HmacDrbg::<H> {
116            k: [0x00u8; MAX_OUTPUT],
117            v: [0x01u8; MAX_OUTPUT],
118            reseed_counter: 1,
119            _marker: core::marker::PhantomData,
120        };
121        // Ensure the tail beyond outlen is not accidentally used.
122        for i in 0..outlen {
123            s.k[i] = 0x00;
124            s.v[i] = 0x01;
125        }
126        s.update(&[entropy_input, nonce, personalization]);
127        s.reseed_counter = 1;
128        Ok(s)
129    }
130
131    /// Reseed an HMAC_DRBG (SP 800-90A §10.1.2.4).
132    ///
133    /// ```text
134    ///   seed_material = entropy_input || additional_input
135    ///   (K, V) = Update(seed_material, K, V)
136    ///   reseed_counter = 1
137    /// ```
138    pub fn reseed(&mut self, entropy_input: &[u8], additional_input: &[u8]) -> Result<(), Error> {
139        if entropy_input.len() * 8 < security_strength::<H>() {
140            return Err(Error::InsufficientEntropy);
141        }
142        if additional_input.len() > MAX_OPT_OCTETS {
143            return Err(Error::AdditionalInputTooLong);
144        }
145        self.update(&[entropy_input, additional_input]);
146        self.reseed_counter = 1;
147        Ok(())
148    }
149
150    /// Generate pseudorandom bits (SP 800-90A §10.1.2.5).
151    ///
152    /// ```text
153    ///   if reseed_counter > reseed_interval: return "Reseed required"
154    ///   if additional_input != Null: (K, V) = Update(additional_input, K, V)
155    ///   temp = ""
156    ///   while len(temp) < requested_bits:
157    ///       V = HMAC(K, V); temp = temp || V
158    ///   returned_bits = leftmost requested_bits of temp
159    ///   (K, V) = Update(additional_input, K, V)
160    ///   reseed_counter += 1
161    /// ```
162    pub fn generate(&mut self, out: &mut [u8], additional_input: &[u8]) -> Result<(), Error> {
163        if out.len() > MAX_REQUEST_OCTETS {
164            return Err(Error::RequestTooLarge);
165        }
166        if additional_input.len() > MAX_OPT_OCTETS {
167            return Err(Error::AdditionalInputTooLong);
168        }
169        if self.reseed_counter > RESEED_INTERVAL {
170            return Err(Error::ReseedRequired);
171        }
172        let outlen = H::OUTPUT_LEN;
173
174        // Step 2: fold in additional_input when present.
175        if !additional_input.is_empty() {
176            self.update(&[additional_input]);
177        }
178
179        // Step 3-4: V = HMAC(K, V); temp = temp || V ; …
180        let mut off = 0;
181        let mut newv = [0u8; MAX_OUTPUT];
182        while off < out.len() {
183            hmac_multi::<H>(&self.k[..outlen], &[&self.v[..outlen]], &mut newv[..outlen]);
184            self.v[..outlen].copy_from_slice(&newv[..outlen]);
185            let take = core::cmp::min(outlen, out.len() - off);
186            out[off..off + take].copy_from_slice(&self.v[..take]);
187            off += take;
188        }
189
190        // Step 6: (K, V) = Update(additional_input, K, V).
191        self.update(&[additional_input]);
192        // Step 7: reseed_counter += 1.
193        self.reseed_counter += 1;
194        Ok(())
195    }
196}
197
198/// Assemble `[V, tag, provided_parts…]` into `parts`, returning the count.
199/// Empty `provided_parts` chunks are skipped (they are no-ops for HMAC and
200/// keep the multi-part call minimal). The `tag` byte (`0x00`/`0x01`) is
201/// held in a small module-static table so the borrow lives long enough.
202fn assemble<'a>(parts: &mut [&'a [u8]; 8], v: &'a [u8], tag: u8, provided: &[&'a [u8]]) -> usize {
203    parts[0] = v;
204    parts[1] = TAG_BYTES[tag as usize];
205    let mut n = 2;
206    for p in provided {
207        if !p.is_empty() {
208            parts[n] = p;
209            n += 1;
210        }
211    }
212    n
213}
214
215/// Single-byte constants for the Update domain separator (`0x00` / `0x01`).
216static TAG_BYTES: [&[u8]; 2] = [&[0x00u8], &[0x01u8]];
217
218/// `security_strength` in bits for the digest (SP 800-90A §10.1, Table 2).
219const fn security_strength<H: Digest>() -> usize {
220    match H::OUTPUT_LEN {
221        20 => 128, // SHA-1
222        28 => 192, // SHA-224
223        _ => 256,  // SHA-256 / 384 / 512
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use crate::drbg::test_util::hx;
231    use tessera::{Sha256, Sha384, Sha512};
232
233    // Official NIST CAVP DRBGVS vectors (CAVS 14.3, "drbgtestvectors").
234    // Same two-generate DRBGVS procedure as Hash_DRBG.
235
236    // drbgvectors_no_reseed / HMAC_DRBG.txt — [SHA-256], Pers=0, AddIn=0, COUNT=0.
237    #[test]
238    fn hmac_sha256_no_reseed_pers0_add0() {
239        let entropy = hx("ca851911349384bffe89de1cbdc46e6831e44d34a4fb935ee285dd14b71a7488");
240        let nonce = hx("659ba96c601dc69fc902940805ec0ca8");
241        let mut drbg = HmacDrbg::<Sha256>::instantiate(&entropy, &nonce, &[]).unwrap();
242        let mut out = [0u8; 128];
243        drbg.generate(&mut out, &[]).unwrap();
244        drbg.generate(&mut out, &[]).unwrap();
245        assert_eq!(
246            out[..],
247            hx(
248                "e528e9abf2dece54d47c7e75e5fe302149f817ea9fb4bee6f4199697d04d5b89d54fbb978a15b5c443c9ec21036d2460\
249                b6f73ebad0dc2aba6e624abf07745bc107694bb7547bb0995f70de25d6b29e2d3011bb19d27676c07162c8b5ccde06689\
250                61df86803482cb37ed6d5c0bb8d50cf1f50d476aa0458bdaba806f48be9dcb8"
251            )[..]
252        );
253    }
254
255    // drbgvectors_pr_true / HMAC_DRBG.txt — [SHA-256], Pers=0, AddIn=0, COUNT=0.
256    // PredictionResistance=True: each generate is preceded by a reseed with
257    // the corresponding EntropyInputPR (SP 800-90A §9.3.1 with PR).
258    #[test]
259    fn hmac_sha256_pr_true_pers0_add0() {
260        let entropy = hx("9969e54b4703ff31785b879a7e5c0eae0d3e309559e9fe96b0676d49d591ea4d");
261        let nonce = hx("07d20d46d064757d3023cac2376127ab");
262        let pr0 = hx("c60f2999100f738c10f74792676a3fc4a262d13721798046e29a295181569f54");
263        let pr1 = hx("c11d4524c9071bd3096015fcf7bc24a607f22fa065c937658a2a77a8699089f4");
264        let mut drbg = HmacDrbg::<Sha256>::instantiate(&entropy, &nonce, &[]).unwrap();
265        let mut out = [0u8; 128];
266        // Generate #1 (PR): reseed(pr0) then generate.
267        drbg.reseed(&pr0, &[]).unwrap();
268        drbg.generate(&mut out, &[]).unwrap();
269        // Generate #2 (PR): reseed(pr1) then generate → ReturnedBits.
270        drbg.reseed(&pr1, &[]).unwrap();
271        drbg.generate(&mut out, &[]).unwrap();
272        assert_eq!(
273            out[..],
274            hx(
275                "abc015856094803a938dffd20da94843870ef935b82cfec17706b8f551b8385044235dd44b599f94b39be78dd476e0cf\
276                11309c995a7334e0a78b37bc9586235086fa3b637ba91cf8fb65efa22a589c137531aa7b2d4e2607aac27292b01c698e6\
277                e01ae679eb87c01a89c7422d4372d6d754ababb4bf896fcb1cd09d692d0283f"
278            )[..]
279        );
280    }
281
282    // drbgvectors_pr_true / HMAC_DRBG.txt — [SHA-256], Pers=256, AddIn=256, COUNT=0.
283    // With PR the additional input is supplied on the *reseed*+*generate* pair.
284    #[test]
285    fn hmac_sha256_pr_true_pers_add() {
286        let entropy = hx("4294671d493dc085b5184607d7de2ff2b6aceb734a1b026f6cfee7c5a90f03da");
287        let nonce = hx("d071544e599235d5eb38b64b551d2a6e");
288        let pers = hx("63bc769ae1d95a98bde870e4db7776297041d37c8a5c688d4e024b78d83f4d78");
289        let add0 = hx("28848becd3f47696f124f4b14853a456156f69be583a7d4682cff8d44b39e1d3");
290        let add1 = hx("8bfce0b7132661c3cd78175d83926f643e36f7608eec2c5dac3ddcbacc8c2182");
291        let pr0 = hx("db9b4790b62336fbb9a684b82947065393eeef8f57bd2477141ad17e776dac34");
292        let pr1 = hx("4a9abe80f6f522f29878bedf8245b27940a76471006fb4a4110beb4decb6c341");
293        let mut drbg = HmacDrbg::<Sha256>::instantiate(&entropy, &nonce, &pers).unwrap();
294        let mut out = [0u8; 128];
295        // For PR, the additional_input is applied on the reseed (with the PR
296        // entropy) preceding each generate, and the generate itself uses no
297        // further additional input (DRBGVS PR-true semantics).
298        drbg.reseed(&pr0, &add0).unwrap();
299        drbg.generate(&mut out, &[]).unwrap();
300        drbg.reseed(&pr1, &add1).unwrap();
301        drbg.generate(&mut out, &[]).unwrap();
302        assert_eq!(
303            out[..],
304            hx(
305                "e580dc969194b2b18a97478aef9d1a72390aff14562747bf080d741527a6655ce7fc135325b457483a9f9c70f91165a8\
306                11cf4524b50d51199a0df3bd60d12abac27d0bf6618e6b114e05420352e23f3603dfe8a225dc19b3d1fff1dc245dc6b1d\
307                f24c741744bec3f9437dbbf222df84881a457a589e7815ef132f686b760f012"
308            )[..]
309        );
310    }
311
312    // --- Per-width coverage: HMAC-SHA-384 / -SHA-512. ---
313    // no_reseed two-generate procedure; ReturnedBits = SECOND generate.
314
315    // drbgvectors_no_reseed / HMAC_DRBG.txt — [SHA-384], Pers=0, AddIn=0, COUNT=0.
316    #[test]
317    fn hmac_sha384_no_reseed_pers0_add0() {
318        let entropy = hx("a1dc2dfeda4f3a1124e0e75ebfbe5f98cac11018221dda3fdcf8f9125d68447a");
319        let nonce = hx("bae5ea27166540515268a493a96b5187");
320        let mut drbg = HmacDrbg::<Sha384>::instantiate(&entropy, &nonce, &[]).unwrap();
321        let mut out = [0u8; 192];
322        drbg.generate(&mut out, &[]).unwrap();
323        drbg.generate(&mut out, &[]).unwrap();
324        assert_eq!(
325            out[..],
326            hx(
327                "228293e59b1e4545a4ff9f232616fc5108a1128debd0f7c20ace837ca105cbf24c0dac1f9847dafd0d0500721ffad3c6\
328                84a992d110a549a264d14a8911c50be8cd6a7e8fac783ad95b24f64fd8cc4c8b649eac2b15b363e30df79541a6b8a1ca\
329                ac238949b46643694c85e1d5fcbcd9aaae6260acee660b8a79bea48e079ceb6a5eaf4993a82c3f1b758d7c53e3094eea\
330                c63dc255be6dcdcc2b51e5ca45d2b20684a5a8fa5806b96f8461ebf51bc515a7dd8c5475c0e70f2fd0faf7869a99ab6c"
331            )[..]
332        );
333    }
334
335    // drbgvectors_no_reseed / HMAC_DRBG.txt — [SHA-384], Pers=256, AddIn=256, COUNT=0.
336    #[test]
337    fn hmac_sha384_no_reseed_pers_add() {
338        let entropy = hx("c2feb900032f2cca98d3f60536f563d8ac9af5fb2e90dba36c371c0a1c58cf5e");
339        let nonce = hx("4a60f2be0fa13b8266b715be8aad128c");
340        let pers = hx("8e6f9be0c692648072d19c750804b10e2ec313c8013abd363de7a467787859f2");
341        let add0 = hx("72f54ba3f8e71ad69a040bb8493283acfc8815f17dbcea220ecd68372a2dffae");
342        let add1 = hx("adce8157ef60482841dd2ac5ac512bf7649120c1dba81ea75f2a70b7512bb6f3");
343        let mut drbg = HmacDrbg::<Sha384>::instantiate(&entropy, &nonce, &pers).unwrap();
344        let mut out = [0u8; 192];
345        drbg.generate(&mut out, &add0).unwrap();
346        drbg.generate(&mut out, &add1).unwrap();
347        assert_eq!(
348            out[..],
349            hx(
350                "e76e4326ac69ddbc6b2408c529b05a96425c65cc65671601191238e9434d2a0147f3a25ce9b6818774f5263c92459bca\
351                421d2b492f9a9c2971359baaa1426d6e2c36d8924f39d02ee2fb5502c4e0b206dbe9aeeacd508abe6c055d547b5f9f35\
352                de4fdc9c05a2c63ad699a3a7e265598b8f40a8a295d7376b88c49af9edc790b8a5ee221e19877616678e2a5135d7b375\
353                6109200439d9ec8bfe0cc5f3c334ca9c022ab9192d5d554dc7ae76af1dc06d814427f46a7cfa2dcc62f4777d07ebde7d"
354            )[..]
355        );
356    }
357
358    // drbgvectors_no_reseed / HMAC_DRBG.txt — [SHA-512], Pers=0, AddIn=0, COUNT=0.
359    #[test]
360    fn hmac_sha512_no_reseed_pers0_add0() {
361        let entropy = hx("35049f389a33c0ecb1293238fd951f8ffd517dfde06041d32945b3e26914ba15");
362        let nonce = hx("f7328760be6168e6aa9fb54784989a11");
363        let mut drbg = HmacDrbg::<Sha512>::instantiate(&entropy, &nonce, &[]).unwrap();
364        let mut out = [0u8; 256];
365        drbg.generate(&mut out, &[]).unwrap();
366        drbg.generate(&mut out, &[]).unwrap();
367        assert_eq!(
368            out[..],
369            hx(
370                "e76491b0260aacfded01ad39fbf1a66a88284caa5123368a2ad9330ee48335e3c9c9ba90e6cbc9429962d60c1a6661ed\
371                cfaa31d972b8264b9d4562cf18494128a092c17a8da6f3113e8a7edfcd4427082bd390675e9662408144971717303d8d\
372                c352c9e8b95e7f35fa2ac9f549b292bc7c4bc7f01ee0a577859ef6e82d79ef23892d167c140d22aac32b64ccdfeee273\
373                0528a38763b24227f91ac3ffe47fb11538e435307e77481802b0f613f370ffb0dbeab774fe1efbb1a80d01154a9459e7\
374                3ad361108bbc86b0914f095136cbe634555ce0bb263618dc5c367291ce0825518987154fe9ecb052b3f0a256fcc30cc1\
375                4572531c9628973639beda456f2bddf6"
376            )[..]
377        );
378    }
379
380    // drbgvectors_no_reseed / HMAC_DRBG.txt — [SHA-512], Pers=256, AddIn=256, COUNT=0.
381    #[test]
382    fn hmac_sha512_no_reseed_pers_add() {
383        let entropy = hx("e97a4631d0a08d549cde8af9a1aae058e3e9585575a726c76a27bc62bed18a4b");
384        let nonce = hx("227221d5fe5a5db9810f9afe56a3ee78");
385        let pers = hx("94084b11d55e0f9c2ef577741753af66ad7a25b28524b50ea970105c3545e97d");
386        let add0 = hx("24c81d4773938371b906cf4801957ac22f87432b9c8a84bc5ac04ad5b1cc3f57");
387        let add1 = hx("c8c878451e2b76577c36393ca253888c1038885bbfdacd8539615a611e2ac00b");
388        let mut drbg = HmacDrbg::<Sha512>::instantiate(&entropy, &nonce, &pers).unwrap();
389        let mut out = [0u8; 256];
390        drbg.generate(&mut out, &add0).unwrap();
391        drbg.generate(&mut out, &add1).unwrap();
392        assert_eq!(
393            out[..],
394            hx(
395                "761422dea283262998c0ffffefc77de2d395c818b9cf1ac2bcd1153235e0d8b63199c51e195135a75f1f87b454484ecc\
396                560c532c7ba5923c9490a423c177453459d81efc38ce2939226043cb733062eae303a009b48ee0cf3c7e40abe2b57a70\
397                a6062c669a9fbff20b4c94b4ecbc5f744a80d7be8134359581d441da921737b1329470b214f3e679fb7ad48baf046bac\
398                59a36b5770806cdef28cc4a8fd0e049b924c3c9216e00ba63c2ff771d66b7520dd33a85382a84b622717e594e447c919\
399                926a5b2e94d490ee626da9df587fed674067917963fd51d383e55730c17a124555e2e46e1395c9920d07dae4d67ffee5\
400                c759b6a326eec6d7b3ba6dee012e4807"
401            )[..]
402        );
403    }
404
405    #[test]
406    fn hmac_sha256_rejects_short_entropy() {
407        let short = [0u8; 16];
408        assert_eq!(
409            HmacDrbg::<Sha256>::instantiate(&short, &[0u8; 16], &[]).err(),
410            Some(Error::InsufficientEntropy)
411        );
412    }
413}