Skip to main content

arcana/drbg/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Cédric Mesnil <cslashm@pm.me>
3
4//! Deterministic Random Bit Generators (NIST SP 800-90A Rev.1).
5//!
6//! The three approved DRBG mechanisms of SP 800-90A §10:
7//!
8//! | Mechanism   | Section    | Backend                   | Type                                        |
9//! |-------------|------------|---------------------------|---------------------------------------------|
10//! | Hash_DRBG   | §10.1.1    | SHA-2 ([`tessera::Digest`])| [`crate::drbg::hash::HashDrbg`]`<H>`                      |
11//! | HMAC_DRBG   | §10.1.2    | HMAC-SHA-2                | [`crate::drbg::hmac::HmacDrbg`]`<H>`                      |
12//! | CTR_DRBG    | §10.2.1    | AES-128/192/256           | [`crate::drbg::ctr::CtrDrbg`]                            |
13//!
14//! # Deterministic by design
15//!
16//! These generators are *deterministic functions of the caller-supplied
17//! entropy input* — they never pull OS entropy themselves. This matches
18//! arcana's "caller-provided randomness" posture (cf. ECDSA / RSA blinding)
19//! and is exactly how the NIST CAVP DRBGVS vectors drive the constructions:
20//! the harness feeds `EntropyInput`, `Nonce`, `PersonalizationString`,
21//! optional `AdditionalInput`, and (for reseed / prediction resistance)
22//! `EntropyInputReseed` / `EntropyInputPR`, and checks the returned bits.
23//!
24//! The caller is responsible for supplying entropy from a source with the
25//! required min-entropy (SP 800-90A §8.6.3): at least `security_strength`
26//! bits in the instantiate/reseed entropy input, and a nonce of at least
27//! `security_strength/2` bits (§8.6.7). arcana does not validate the
28//! *quality* of the entropy, only its *length* against the seedlen /
29//! security-strength tables of §10.
30//!
31//! # Side-channel posture
32//!
33//! Per `arcana/doc/sca/countermeasures/drbg.rst` (planned):
34//!
35//! | Concern                                     | Status      | Note                                                           |
36//! |---------------------------------------------|-------------|----------------------------------------------------------------|
37//! | Internal state (`V`, `C`, `Key`) is secret  | handled     | never branched on; all state transforms are data-oblivious     |
38//! | Hash_DRBG / HMAC_DRBG on SHA-2              | inherited   | data-oblivious SHA-2 / HMAC (no secret-dependent branch/index) |
39//! | Hash_DRBG mod-2^seedlen addition            | handled     | branch-free byte adder ([`crate::drbg::util::ct_add_into`])                 |
40//! | CTR increment / counter arithmetic          | handled     | branch-free ([`crate::drbg::util::ct_increment`])                           |
41//! | CTR_DRBG AES core                           | **T1-A gap**| table-based AES leaks S-box index on shared-L1 CPUs (see below) |
42//! | HMAC-SHA-2 carry-based DPA (Belenky 2023/3) | **T2-D gap**| the underlying HMAC leak; DRBG key is DPA-extractable          |
43//!
44//! ## CTR_DRBG AES leak — pre-existing T1-A gap, NOT introduced here
45//!
46//! [`crate::drbg::ctr::CtrDrbg`] calls the existing [`crate::cipher::aes`] core, which
47//! is table-based (S-box + T-tables). On CPUs with a shared L1 data cache
48//! this leaks the S-box access pattern, i.e. the DRBG `Key`. This is the
49//! documented `T1-A` roadmap item (fixsliced AES, Adomnicai-Peyrin TCHES
50//! 2021/1); the DRBG inherits it and does not fix or worsen it. All DRBG
51//! *own* arithmetic (df, counter increment, XORs) is branch-free and
52//! index-oblivious, so the only residual leak on the CTR path is the
53//! shared AES core. `[CESTI: CTR_DRBG inherits the AES T1-A cache leak —
54//! point to the fixsliced-AES roadmap item.]`
55//!
56//! The Hash_DRBG / HMAC_DRBG paths inherit only the (branch-free) SHA-2
57//! compression and, for HMAC, the carry-based DPA of `T2-D`; they carry no
58//! table lookup on secret data.
59
60pub mod ctr;
61pub mod hash;
62pub mod hmac;
63pub mod util;
64
65#[cfg(test)]
66mod test_util {
67    /// Decode a hex string (ignoring internal whitespace / line breaks used
68    /// to wrap long CAVP vectors) into a byte vector.
69    pub fn hx(s: &str) -> Vec<u8> {
70        let clean: Vec<u8> = s.bytes().filter(|b| !b.is_ascii_whitespace()).collect();
71        assert!(clean.len() % 2 == 0, "odd hex length");
72        clean
73            .chunks(2)
74            .map(|c| {
75                let hi = (c[0] as char).to_digit(16).unwrap() as u8;
76                let lo = (c[1] as char).to_digit(16).unwrap() as u8;
77                (hi << 4) | lo
78            })
79            .collect()
80    }
81}
82
83/// Errors returned by the DRBG mechanisms (SP 800-90A §9).
84///
85/// Each mechanism re-exports this single enum; the variants map onto the
86/// `ERROR_FLAG` / length-check conditions of §10 and the reseed-count
87/// exhaustion of §9.3.3.
88#[derive(Clone, Copy, Debug, PartialEq, Eq)]
89pub enum Error {
90    /// The instantiate/reseed entropy input is shorter than the mechanism's
91    /// minimum (`security_strength` bits, SP 800-90A §8.6.3 and the tables
92    /// of §10). Also raised, for CTR_DRBG *without* a derivation function,
93    /// when the entropy input length is not exactly `seedlen` (§10.2.1.3.1).
94    InsufficientEntropy,
95    /// A single `generate` request asked for more than `max_number_of_bits_per_request`
96    /// (SP 800-90A §10, tables 2/3: 2^19 bits = 65 536 octets).
97    RequestTooLarge,
98    /// The `additional_input` supplied to `generate`/`reseed` exceeds
99    /// `max_additional_input_length` (2^35 bits; enforced here as a byte
100    /// bound to keep the length fields in range).
101    AdditionalInputTooLong,
102    /// The personalization string exceeds `max_personalization_string_length`
103    /// (SP 800-90A §10, 2^35 bits).
104    PersonalizationTooLong,
105    /// The reseed counter reached `reseed_interval`: a reseed is required
106    /// before the next `generate` (SP 800-90A §9.3.3, step 1 → status
107    /// "Reseed required"). The caller must call [`reseed`](hash::HashDrbg::reseed).
108    ReseedRequired,
109    /// A CTR_DRBG *no-df* key length that is not one of the AES key sizes,
110    /// or a personalization/additional-input length that does not match
111    /// `seedlen` in no-df mode (SP 800-90A §10.2.1.3.1 / §10.2.1.5.1).
112    InvalidLength,
113}