AES-CTR-DRBG in Go: Allocation-Free, Low-Latency, Deterministic Cryptographic Randomness

#computing #distributed systems #software

“The purpose of abstraction is not to be vague, but to create a new semantic level in which one can be absolutely precise.” — Edsger W. Dijkstra

Randomness occupies an unusual position in software systems. We rely on it for security, for fairness, for simulation, and yet we rarely scrutinize its behavior with the same rigor we apply to storage engines or replication protocols. We accept entropy as something that simply happens, mediated by an operating system call or a library abstraction, and move on.

That approach works—until it doesn’t.

When I began working on systems that required both cryptographic correctness and operational predictability, the implicit assumptions around randomness became a liability. Latency mattered. Allocation behavior mattered. Reproducibility mattered. And in certain environments, auditability mattered most of all. The standard tools did not fail outright, but they failed quietly, by obscuring behavior that ought to be explicit.

At its core, the problem is not randomness itself, but how cryptographic state evolves over time—and whether that evolution remains visible, bounded, and predictable.

This article is about addressing that gap.

Determinism Is Not the Enemy

In distributed systems, we accept a fundamental axiom: there is no way to guarantee complete knowledge of global state at any given moment. We design for tolerance, convergence, and recovery, not for omniscience; ref Parallel and Distributed System Design). Randomness, however, often escapes this discipline. It is treated as an oracle—invoked, trusted, and forgotten.

Cryptographic systems take a different view. They do not reject determinism; they constrain it. Given a fixed internal state and a fixed construction, output must follow. This is not a weakness but a requirement. It is precisely what allows us to reason about forward security, backtracking resistance, and compromise scenarios.

AES-CTR-DRBG sits squarely in this space. It provides a deterministic construction based on a well-understood primitive, AES, and defines explicit rules for how internal state evolves over time. Once you accept that premise, several consequences follow naturally. Hidden entropy sources become suspect. Implicit reseeding becomes problematic. Mutable configuration becomes dangerous.

The design space narrows considerably.

Deterministic Constructions and Cryptographic Honesty

Cryptography is often discussed in terms of strength, but strength alone is not sufficient. A system can employ strong primitives and still behave in ways that are difficult to reason about. When that happens, security becomes an emergent property rather than an explicit one.

Deterministic constructions reject that ambiguity.

An AES-CTR-DRBG does not attempt to surprise the reader. Given a key, a counter, and a defined update function, the output follows directly. There is no hidden mixing, no opportunistic entropy acquisition, and no conditional behavior based on environmental state. This predictability is not merely convenient; it is foundational. It allows one to reason about compromise without first reverse-engineering library behavior.

Once determinism is accepted as a requirement, architectural freedoms disappear. You can no longer “improve” security by quietly pulling in entropy. You can no longer treat reseeding as an implementation detail. You must decide, explicitly, when state changes and why.

This is not a limitation of AES-CTR-DRBG. It is a discipline imposed by cryptography itself.

These constraints do not arise in isolation. They emerge most clearly in regulated, multi-tenant, and distributed systems, where randomness is no longer an incidental utility but a component subject to scrutiny. In such environments, deterministic and auditable cryptographic randomness becomes a requirement rather than a preference. Reproducibility matters for forensic analysis. Isolation matters for per-tenant security. Predictable resource behavior matters when concurrency and throughput are no longer theoretical concerns.

The standard random primitives in Go do not address this space. They make no guarantees around deterministic output, explicit key management, or resource predictability under concurrency, nor do they target alignment with NIST SP 800-90A or FIPS 140-2 requirements. Third-party alternatives often complicate matters further, introducing additional dependencies or obscuring allocation behavior and latency characteristics behind opaque abstractions.

At scale, these gaps become operational risks. System PRNGs do not ensure reproducibility. Key lifecycle management and tenant isolation are left to the caller. Failure modes relevant to compliance and auditability remain implicit, even as entropy sources degrade or cryptographic assumptions are violated. What is missing is not another source of randomness, but a generator whose behavior remains explicit, bounded, and inspectable over time.

These constraints define the space for a deterministic random bit generator based on AES in counter mode (CTR), as specified in NIST SP 800-90A and aligned with FIPS 140-2. The implementation discussed here relies solely on Go’s standard library cryptography and targets allocation-free output and low, predictable latency, making it suitable for high-throughput and regulated environments where correctness and observability are inseparable.

Entropy, State, and the Shape of Time

Many practical failures in cryptographic systems arise not from broken primitives, but from blurred boundaries. Entropy acquisition is a boundary. Deterministic generation is another. When the two are conflated, responsibility becomes unclear.

Operating systems are good at collecting entropy. Applications are good at applying policy. A DRBG sits between the two, which is why many implementations quietly collapse the distinction.

I chose not to.

In this design, entropy enters the system at a clearly defined point and nowhere else. The generator consumes seed material and expands it deterministically. If reseeding occurs, it occurs because the caller has decided that new entropy is available and appropriate. This makes the system honest about its dependencies. It also makes it auditable. One can trace exactly when and how entropy influences output, which is a prerequisite for meaningful analysis.

This mirrors a lesson learned repeatedly in distributed systems: implicit coordination is coordination nonetheless. If you depend on entropy, say so.

It is helpful to visualize the generator not as a black box, but as a state machine evolving over time.

Imagine a simple diagram with three persistent elements: Key, Counter (V), and Limits. At initialization, these are set from seed material and configuration. Each request for output advances the counter deterministically, producing a block of output via AES encryption. The key remains fixed until a reseed event occurs. Limits monotonically decrease as output is generated.

Time, in this model, does not pass in seconds. It passes in blocks generated.

A reseed event resets the key and counter and restores limits. Absent a reseed, state evolution is linear, irreversible, and predictable. There are no side paths, no hidden refreshes, and no conditional jumps based on runtime conditions. The diagram contains no feedback loops other than the explicit reseed boundary.

This mental model is intentionally simple. It allows one to reason about compromise, exhaustion, and recovery without hand-waving.

Reseeding is often presented as a binary feature: enabled or disabled. In practice, it is a temporal question. How long may a generator safely operate on a given internal state? How much output may be derived before assumptions begin to erode?

FIPS-aligned guidance does not answer these questions abstractly; it answers them in terms of limits. Maximum bytes per key. Maximum requests between reseeds. Defined update functions. These limits exist not because AES is fragile, but because systems operate over time, and time introduces exposure.

In a deterministic generator, time is measured not in seconds but in output. Each block generated advances the internal counter. Each advancement is irreversible. By enforcing explicit limits, the generator treats time as a first-class dimension rather than an afterthought.

This perspective aligns closely with how we reason about versioning and state evolution in distributed systems. State does not merely exist; it progresses. If you do not bound that progression, you eventually lose the ability to reason about it.

Prediction resistance is frequently discussed as an absolute good. In reality, it is a trade. Enabling it increases resilience against certain compromise scenarios, but it also introduces new operational dependencies. Reseeding requires entropy. Entropy requires coordination with the environment. Coordination introduces latency and failure modes.

Rather than treating prediction resistance as a default, this design treats it as a conscious decision. When enabled, reseeding behavior is strict and enforced. When disabled, the generator behaves as a pure deterministic expander. Neither mode is inherently superior; each corresponds to a different threat model.

Cryptographic systems fail when they attempt to serve all threat models simultaneously. By forcing the caller to choose, the system avoids pretending that one size fits all.

Runtime Semantics and Predictability

Garbage collection is often discussed in terms of performance, but in latency-sensitive systems it also becomes a semantic concern. Allocation introduces nondeterminism: not in output, but in timing. When a generator allocates during its hot path, it cedes control of latency to the runtime.

The effects of these choices are observable in practice. Under serial workloads, small reads complete in tens of nanoseconds, largely reflecting fixed overhead rather than per-byte cost. Larger reads exhibit higher absolute latency, not because of inefficiency, but because AES-CTR scales linearly with the number of blocks processed. Encrypting a few blocks versus a few hundred necessarily occupies different regions of the CPU pipeline, even when the underlying primitive is highly optimized. What matters is that this behavior is predictable and stable under load, matching expectations derived from the algorithm and the memory hierarchy rather than fluctuating with runtime conditions.

Achieving zero allocations in the output path was, therefore, non-negotiable; read “MUST”.

What matters here is not raw speed, but control. A system that allocates implicitly hands part of its execution semantics to the runtime. In many contexts that is acceptable; in some, it is not. When randomness participates in protocol boundaries, scheduling decisions, or security-sensitive operations, latency variance becomes observable behavior. Treating allocation as a semantic choice forces the design to acknowledge that reality. Once allocation is removed from the generation path, timing becomes a property of the algorithm itself rather than a side effect of memory management. That shift restores a degree of predictability that is otherwise difficult to recover.

The decision to make this DRBG allocation-free during generation was not an optimization exercise; it was a correctness constraint. Once generation begins, no heap allocations occur. The caller owns the buffers. The internal state resides in fixed-size structures. The runtime remains uninvolved. Latency variance collapses. Throughput becomes predictable. The generator behaves the same under load as it does in isolation.

Concurrency often exposes design shortcuts. Shared mutable state invites contention; global locks invite collapse. In the context of deterministic randomness, concurrency introduces an additional risk: accidental nondeterminism caused by interleaving.

The solution follows a familiar distributed-systems pattern: partition the state. Each shard maintains its own independent DRBG state. Shards do not share counters, keys, or reseed thresholds. They share only configuration. This trades a small amount of memory for clarity and scalability. Concurrency improves throughput, not entropy.

Determinism remains intact.

Configuration is where many security failures quietly begin. Defaults accrete. Options multiply. Behavior changes without callers noticing. Over time, the configuration surface becomes an implicit API, understood only by convention.

Here, configuration is a contract.

All configuration occurs at construction time. Options are explicit. Once initialized, the generator does not change its behavior. There is no hidden reseeding, no dynamic mutation, no silent fallback. If prediction resistance is enabled, it is enabled because the caller said so.

A generator whose security posture can change mid-flight violates basic architectural discipline. A useful way to evaluate a DRBG is to ask a simple question: if an attacker learns the internal state at time t, what can they infer? With a deterministic construction, the answer is precise. Past output remains secure if the construction provides backtracking resistance. Future output becomes predictable unless new entropy is introduced. These are not surprising conclusions; they are the expected consequences of the model.

What matters is that the model makes these consequences explicit. By avoiding hidden entropy and implicit reseeding, the generator ensures that compromise scenarios are analyzable rather than speculative. An auditor does not need to guess whether the library refreshed itself “recently.” The code tells the story plainly.

In cryptography, clarity is often mistaken for weakness. In practice, it is the opposite.

Closing Thoughts

This AES-CTR-DRBG implementation exists because certain systems demand more from randomness than opacity and convenience. They demand predictability, auditability, and explicit control. They demand that design choices surface in code rather than hide behind defaults.

If you need a general-purpose source of entropy, the standard library remains an excellent choice.

If you need deterministic cryptographic randomness whose behavior you can reason about—whose latency you can predict and whose state evolution you can audit—then the design presented here follows inevitably from that requirement. Once randomness is treated as state evolving over time, rather than as an oracle invoked on demand, many familiar shortcuts stop making sense. What remains is a system whose behavior is explicit, bounded, and knowable. Good architectures do not happen by accident. They emerge when constraints are taken seriously and followed to their logical conclusions—even when those conclusions lead to different constructions under different constraints.

This design is not theoretical; the AES-CTR-DRBG implementation described here is used as the underlying randomness source in my NanoID library, where deterministic behavior and predictable performance are equally important.


Implementation: https://github.com/sixafter/aes-ctr-drbg