Skip to content

rand: use the OS CSPRNG for uuid_v4, uuid_v7, UUIDSession and ulid#27152

Open
davlgd wants to merge 4 commits into
vlang:masterfrom
davlgd:davlgd-uuid-csprng
Open

rand: use the OS CSPRNG for uuid_v4, uuid_v7, UUIDSession and ulid#27152
davlgd wants to merge 4 commits into
vlang:masterfrom
davlgd:davlgd-uuid-csprng

Conversation

@davlgd
Copy link
Copy Markdown
Contributor

@davlgd davlgd commented May 13, 2026

The high-level identifier generators in vlib/rand/ (uuid_v4, uuid_v7, UUIDSession.next, ulid and ulid_at_millisecond) currently draw their random bits from default_rng, which is a WyRandRNG — a fast non-cryptographic PRNG seeded from the current time. This PR routes those bits through crypto.rand.read instead, i.e. through the OS CSPRNG (getrandom on Linux, getentropy on macOS/BSD, BCryptGenRandom on Windows), without changing the public API or the produced format.

Importing crypto.rand from vlib/rand/ would normally introduce an import cycle (crypto.rand used to depend on math.big and encoding.binary, both of which transitively depend on rand via test files or sync). The first commit breaks that cycle by trimming crypto.rand's own dependencies — see Commits below.

Motivation

These identifiers are routinely used as session IDs, request IDs, file boundaries, primary keys and cache keys (see e.g. vlib/veb/auth/auth.v, vlib/veb/request_id, vlib/net/websocket, vlib/net/http/request.v, vlib/v/pref/pref.v). In those contexts, unpredictability matters: a non-CSPRNG default lets an attacker who observes a few IDs reconstruct the PRNG state and predict subsequent ones.

Each of the relevant specifications already points in this direction:

  • UUIDv4 — RFC 4122 (2005) defines v4 in §4.4 and points to §4.5 for random-number sourcing, which recommends a "cryptographic quality random number" and cites RFC 1750 / RFC 4086 ("Randomness Requirements for Security") for the underlying guidance.
  • UUIDv7 — RFC 9562 (the RFC that defines v7) inherits the same guidance for the rand_a and rand_b fields.
  • ULID — ulid/spec is the most explicit: "If no cryptographically secure random number generator is available, ULIDs should not be generated."

The major ecosystems all default to a CSPRNG.

Commits

The PR is three commits, bisect-clean (each one builds and passes its relevant tests on its own):

  1. crypto.rand: split int_big into crypto.rand.bigint sub-module — moves int_big() (the only function in crypto.rand that needs math.big) into a new crypto.rand.bigint sub-module, and inlines the single encoding.binary.big_endian_u64 call so crypto.rand no longer depends on encoding.binary either. Breaking change: callers of crypto.rand.int_big need to switch to crypto.rand.bigint.int_big. Inside vlib that's a single testdata file (vlib/v/gen/c/testdata/struct_field_result_init.vv), updated in the same commit. crypto.rand.int_u64 stays where it is.
  2. rand: use OS CSPRNG for uuid_v4, uuid_v7 and UUIDSession — the actual switch for the three UUID-family functions.
  3. rand: use OS CSPRNG for ulid and ulid_at_millisecond — same for ulid. On the C backend the convenience functions now go through the CSPRNG; on the JS backend they keep the previous behaviour byte-for-byte.

Changes per file

vlib/crypto/rand/utils.v

  • Drop import math.big and import encoding.binary.
  • Inline the one binary.big_endian_u64 call (3 lines).
  • Remove int_big() (moved to the new sub-module).

vlib/crypto/rand/bigint/bigint.v

  • New module crypto.rand.bigint. Contains pub fn int_big(n big.Integer) !big.Integer. Imports crypto.rand and math.big.

vlib/crypto/rand/bigint/bigint_test.v

  • The previous utils_test.v content, renamed and re-pointed to bigint.int_big.

vlib/v/gen/c/testdata/struct_field_result_init.vv

  • One-line update: rand.int_big(result)bigint.int_big(result).

vlib/rand/rand.c.v

  • Add a private helper csprng_u64_pair() returning 16 bytes of OS CSPRNG output as two big-endian u64s. If crypto.rand.read returns a ReadError (OS entropy source unavailable), the helper panics rather than falling back to a non-cryptographic generator, which would defeat the guarantee.
  • Switch uuid_v4, uuid_v7 and UUIDSession.next to consume that helper instead of default_rng.u64() / default_rng.u16().
  • Extract a pure formatter internal_ulid_format(time, rand_a, rand_b) from internal_ulid_at_millisecond. The PRNG-based method is unchanged (it's a one-line wrapper around the formatter), and the new module-level convenience ulid() / ulid_at_millisecond() reuse the same formatter with csprng_u64_pair() as the random source.

vlib/rand/rand.v

  • Remove the convenience pub fn ulid() and pub fn ulid_at_millisecond() that used to delegate to default_rng. They move to the backend-specific files so that each backend can pick its own entropy source. The method forms (mut rng PRNG) ulid() / (mut rng PRNG) ulid_at_millisecond() stay where they are — they are explicit opt-in and untouched.

vlib/rand/rand.js.v

  • Add pub fn ulid() and pub fn ulid_at_millisecond() that delegate to default_rng. This preserves the current JS-backend behaviour byte-for-byte. Bringing a CSPRNG to the JS backend (via crypto.getRandomValues or crypto.randomBytes) is a separate question and is intentionally out of scope.

API and behaviour

  • Public API is unchanged. All function signatures (fn () string) and produced formats (36-char dashed UUID, 26-char Crockford Base32 ULID) are identical.
  • JS backend behaviour is unchanged. ULID on the JS backend continues to go through default_rng exactly as before.
  • Method APIs are unchanged. (mut rng PRNG) ulid() still draws from the user-provided PRNG. The opt-in path is preserved.

A future optimisation would be to amortise the syscall via a userspace entropy buffer (as other implementations do — one syscall every 128 UUIDs). That is intentionally not part of this PR: it raises its own questions (thread-safety, zeroising consumed bytes, fork handling) and deserves a dedicated discussion. The current change is the smallest one that brings V in line with the spec recommendations.

Additional notes

  • panic on CSPRNG failure. Changing uuid_v4 to return !string would break a lot of call sites, including code that uses it as a function pointer of type fn () string (e.g. veb/request_id). crypto.rand.read already exposes an unrecoverable ReadError when the OS entropy source fails; the UUID/ULID layer simply elevates that to a panic rather than silently degrading to a non-cryptographic source, which would defeat the whole point.
  • No drive-by changes. The two pre-existing v vet warnings in rand.v (i32_in_range, f64cp doc comments) are left untouched.

Comment thread vlib/crypto/rand/utils.v
@davlgd davlgd force-pushed the davlgd-uuid-csprng branch from da6626f to 1a71e6b Compare May 13, 2026 15:17
@davlgd davlgd force-pushed the davlgd-uuid-csprng branch from 1a71e6b to 4da5994 Compare May 13, 2026 15:18
@davlgd davlgd requested a review from JalonSolov May 13, 2026 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants