Skip to main content

arcana/drbg/
util.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 Cédric Mesnil <cslashm@pm.me>
3
4//! Shared, data-oblivious arithmetic for the SP 800-90A DRBGs.
5//!
6//! Big-endian byte arithmetic used by Hash_DRBG (§10.1.1) and CTR_DRBG
7//! (§10.2.1): the mod-`2^(8·len)` addition of the Hashgen / Update steps
8//! and the counter increment of the block-cipher CTR loop. Both are the
9//! canonical carry-propagating schoolbook adders, written branch-free so
10//! that they never leak the (secret) internal state.
11//!
12//! # Side-channel posture
13//!
14//! The addends here are the DRBG internal state (`V`, the CTR counter),
15//! which are secret. The routines therefore run in constant time with
16//! respect to the *contents* of the buffers: the loop bounds depend only
17//! on the (public) buffer lengths, and the carry is propagated with plain
18//! `u16` arithmetic — no comparison, table lookup, or early exit on a
19//! data-dependent value. This is the discipline required by
20//! `~/.claude/CLAUDE.md` §2 and CLAUDE.md §3.1.
21
22/// `acc = (acc + addend) mod 2^(8·acc.len())`, big-endian, in place.
23///
24/// Data-oblivious carry-propagating add. `addend` may be shorter than
25/// `acc`; it is right-aligned (its LSBs line up with `acc`'s LSBs), which
26/// is the shape needed by Hash_DRBG's `V = (V + H) mod 2^seedlen` and
27/// `V = (V + C + reseed_counter + …) mod 2^seedlen` (SP 800-90A §10.1.1.4
28/// step 6). Bytes of `addend` beyond `acc`'s length would overflow the
29/// modulus and are ignored (they must be zero for a well-formed call).
30pub fn ct_add_into(acc: &mut [u8], addend: &[u8]) {
31    let mut carry: u16 = 0;
32    let alen = acc.len();
33    let blen = addend.len();
34    for i in 0..alen {
35        // Right-aligned index into `addend`; when `addend` is shorter than
36        // `acc`, the high bytes contribute 0. The index arithmetic is on
37        // public lengths only.
38        let a = acc[alen - 1 - i] as u16;
39        let b = if i < blen { addend[blen - 1 - i] as u16 } else { 0 };
40        let s = a + b + carry;
41        acc[alen - 1 - i] = (s & 0xff) as u8;
42        carry = s >> 8;
43    }
44    // Overflow beyond acc.len() is discarded == reduction mod 2^(8·len).
45}
46
47/// `acc = (acc + n) mod 2^(8·acc.len())` for a small integer `n`,
48/// big-endian, in place. Used by Hash_DRBG to fold in the
49/// `reseed_counter` (SP 800-90A §10.1.1.4 step 6).
50pub fn ct_add_u64(acc: &mut [u8], n: u64) {
51    let addend = n.to_be_bytes();
52    ct_add_into(acc, &addend);
53}
54
55/// `V = (V + 1) mod 2^(8·V.len())`, big-endian, in place — the CTR_DRBG
56/// counter increment (SP 800-90A §10.2.1.2, the `V = (V + 1) mod 2^blocklen`
57/// of the Update, and the generate loop of §10.2.1.5.2).
58///
59/// Constant time in `V`'s contents: the carry ripples through every byte
60/// regardless of value (no early break once `carry == 0`), so the timing
61/// does not reveal how many trailing `0xff` bytes `V` had.
62pub fn ct_increment(v: &mut [u8]) {
63    let mut carry: u16 = 1;
64    for i in (0..v.len()).rev() {
65        let s = v[i] as u16 + carry;
66        v[i] = (s & 0xff) as u8;
67        carry = s >> 8;
68    }
69}