acme_proxy_core/random.rs
1//! Random values from the system CSPRNG.
2//!
3//! One definition of an idiom that had been written out at every call site
4//! that needed it: fill a buffer from `ring::rand::SystemRandom` and panic if
5//! the OS cannot supply the bytes. The comments at those sites cross-referenced
6//! each other by name ("the same trade-off `authz::generate_token` makes"),
7//! which is the shape a hoist is owed.
8//!
9//! Two sites deliberately stay outside this module, both because their failure
10//! handling differs rather than their randomness:
11//! `signer::local_ca`'s serial generator returns a `Result`, and the
12//! job runner's retry jitter falls back to an unjittered delay rather than
13//! panicking.
14
15use base64::prelude::*;
16use ring::rand::{SecureRandom, SystemRandom};
17
18/// Bytes, in a [`random_token`], before encoding: 256 bits, the size every
19/// non-guessable value in this tree is minted at.
20const TOKEN_BYTES: usize = 32;
21
22/// `N` bytes from the system CSPRNG.
23///
24/// An unavailable system RNG is unrecoverable and threading the error out
25/// would only move the panic, so this panics.
26#[must_use]
27pub fn random_bytes<const N: usize>() -> [u8; N] {
28 let mut bytes = [0u8; N];
29 SystemRandom::new()
30 .fill(&mut bytes)
31 .expect("system RNG unavailable");
32 bytes
33}
34
35/// A fresh high-entropy token: [`TOKEN_BYTES`] random bytes, base64url-encoded
36/// without padding, which is 43 characters.
37///
38/// That encoding is not cosmetic. It is what RFC 8555 §8.1 requires of a
39/// challenge token and §6.5.1 of a `Replay-Nonce`, and it is header-safe and
40/// URL-safe everywhere else the value is carried.
41#[must_use]
42pub fn random_token() -> String {
43 BASE64_URL_SAFE_NO_PAD.encode(random_bytes::<TOKEN_BYTES>())
44}
45
46#[cfg(test)]
47mod tests {
48 use super::*;
49
50 #[test]
51 fn random_bytes_fills_the_whole_buffer() {
52 // Two draws of the same width differing is the only thing that can
53 // tell a filled buffer from a zeroed one that was never touched.
54 assert_ne!(random_bytes::<32>(), [0u8; 32]);
55 assert_ne!(random_bytes::<32>(), random_bytes::<32>());
56 assert_eq!(random_bytes::<16>().len(), 16);
57 }
58
59 #[test]
60 fn random_token_is_43_base64url_characters_over_32_bytes() {
61 let token = random_token();
62
63 assert_eq!(token.len(), 43, "43 characters encode 32 bytes unpadded");
64 assert!(
65 token
66 .bytes()
67 .all(|b| b.is_ascii_alphanumeric() || b == b'-' || b == b'_'),
68 "the base64url alphabet only: {token}"
69 );
70 assert_eq!(
71 BASE64_URL_SAFE_NO_PAD.decode(&token).unwrap().len(),
72 TOKEN_BYTES
73 );
74 }
75
76 #[test]
77 fn random_token_does_not_repeat() {
78 assert_ne!(random_token(), random_token());
79 }
80}