May 19, 2025·6 min read·engineeringgosecuritycryptographyaudits

Go's cryptography audit examines implementation boundaries

The May audit announcement distinguishes algorithm implementations from protocols and compliance. Its findings explain why internal API scope and cgo lifetime rules matter to cryptographic assurance.

A cryptographic implementation can compute the right result and still mishandle the lifetime of the object that owns its memory. Go's newly published audit results include a low-severity issue in the legacy Go+BoringCrypto integration where a finalizer could release memory needed by a C call. The mathematical operation and the language boundary required different kinds of review.

The May 19 announcement by Roland Shoemaker and Filippo Valsorda describes an independent Trail of Bits review commissioned by Google. Three engineers worked for a month on core cryptography implementations. Higher-level TLS and X.509 protocols were outside its scope. That boundary limits what the result says about a complete application, even when the application uses the standard library throughout.

I am strongly attached to Go's readable implementations. In cryptographic code, readability gives reviewers a better chance of establishing exactly which assumptions an operation needs. The interesting result here is the connection between a finding and the implementation rule that prevents the same class of mistake elsewhere.

A C pointer does not retain its Go owner

The public fix for ECDH lifetime management adds deferred runtime.KeepAlive calls for both private and public key wrappers. Its commit message identifies a possible use-after-free. The correction keeps the Go objects reachable until the operation finishes using the C state they own.

The tagged ECDH implementation makes the ownership relationship visible. A public key wrapper contains a C elliptic-curve point pointer. Its finalizer calls the C function that frees that point. The private key has a similar arrangement around its C key object. Garbage collection manages the wrappers, while finalizers release the separately allocated cryptographic objects.

Passing a field from one of those wrappers to C does not automatically express every required lifetime relationship to Go's collector. The wrapper can stop being reachable even though a foreign call still needs the resource that its finalizer releases. Correctness therefore requires an explicit use of the owner after the foreign operation, or an equivalent deferred reachability guarantee.

The fixed ECDH function retains both wrappers through its operation. It obtains the private key's group and scalar, allocates a temporary point, and invokes the C multiplication using the public point. The public wrapper's lifetime must extend across that multiplication because its finalizer owns the point's deallocation. Retaining only the raw pointer value would not express the wrapper's continued ownership.

The runtime's KeepAlive documentation describes the general rule independently of cryptography. Its example uses a finalizer that closes a file descriptor. A system call receiving the descriptor does not itself guarantee that the containing Go object remains reachable. KeepAlive prevents premature finalization through the required point in execution.

This is a narrow tool. The same documentation says it does not relax the rules for unsafe.Pointer. Keeping an owner alive cannot make an invalid pointer conversion valid or supply synchronization between concurrent accesses. It solves the reachability part of resource ownership, which must accompany the other correctness conditions at a foreign-language boundary.

Audit findings need their operational context

The announcement separates the single low-severity finding from five informational findings. It identifies the potentially exploitable memory-management issue as belonging to the legacy, opt-in Go+BoringCrypto integration. The Go team does not support that integration for external use. The fix is already in Go 1.24 at the time of this publication.

That distinction prevents two misleading interpretations. The result does not identify a default-path ECDH use-after-free in ordinary pure-Go builds. It also does not mean that memory-management concerns disappear whenever the top-level API uses Go types. The internal implementation's resource ownership determines whether foreign memory and finalizers enter the operation.

Several informational findings concern timing behavior. A timing difference must be interpreted according to whether its inputs are secret and how the operation uses them. A conversion applied only to public verification inputs has a different security significance from a secret-dependent branch during signing. Auditing requires tracing those uses, not simply locating any branch in cryptographic code.

The publication also discusses internal APIs that are appropriate only for a limited use case. An implementation can safely omit general-purpose behavior when callers cannot require that behavior and the restriction is clear. Reusing the same internal routine for a broader purpose would change the assumptions under which it was reviewed. That risk is one reason internal documentation matters even when an API is unexported.

Native randomness has an explicit structure

The Go 1.24.3 random-number implementation provides a concrete example of a constrained internal design. Outside FIPS mode, its Read delegates to the operating system's randomness. In FIPS mode, it uses a deterministic random bit generator and separately mixes operating-system bytes into each read as additional input.

The code comments identify the additional input as uncredited for FIPS entropy accounting. That is a distinction between the mechanism used to strengthen output and the accounting rules of a compliance regime. It would be inaccurate to reduce the design to a claim that compliant operation simply replaces operating-system randomness with a deterministic stream.

The implementation also handles requests in bounded pieces and reseeds when the generator requires it. Its control flow makes the handoff explicit. On a reseed request, it supplies fresh seed material and the additional input, then clears the latter before retrying generation. The state transition follows the specification references given in the comments.

This code helps explain why an audit needs to examine integration as well as the core generator. A generator's security depends on how it is seeded and called. An API specialized to one internal path cannot be assumed to meet the requirements of an arbitrary external consumer merely because it implements part of a named standard.

Complexity is an engineering constraint

Go's earlier cryptography principles place security and safe use ahead of flexibility and performance. They tie implementation security to reduced complexity, testing, code review, and readability. The principles also say that functionality requires enough maintenance resources to sustain its security over time.

That ordering explains decisions that can otherwise look unnecessarily restrictive. A general-purpose cryptographic API permits more combinations, which creates more opportunities for callers to choose incompatible parameters. A specialized API can remove combinations that the intended application does not need. The restriction only helps if the supported use remains explicit and the implementation does not later acquire callers with different assumptions.

I consider this a good fit for Go's wider design. A standard package should make a common secure operation understandable to the next maintainer. Extra flexibility has a continuing review cost, particularly when its meaning depends on cryptographic requirements that ordinary application developers cannot be expected to reconstruct at every call.

Protocol assurance remains separate

The Go 1.24 release notes introduced the native Go Cryptographic Module and a mode intended to facilitate FIPS 140-3 compliance. They describe approved algorithm implementations behind existing public standard-library packages. This arrangement can keep the public calling code stable while changing how the internal module participates in a build.

The notes also introduce crypto/mlkem and move key-derivation packages into the standard library. This expands the amount of cryptographic code available through ordinary distribution packages. It gives callers a more direct dependency path, but the availability of an implementation does not determine which protocol should use it. Algorithm selection still belongs to a protocol with defined interoperability and security requirements. The native module's integration preserves that separation between an implementation and its callers.

An implementation audit and a compliance process answer different questions. The audit can examine concrete code paths, identify misuse risks, and explain corrections. A compliance process evaluates a defined module under its own requirements. Neither automatically proves that an application authenticates the right peer or applies a correct authorization policy.

That is particularly relevant to the explicit exclusion of TLS and X.509 from this review. An application uses algorithms through protocols that impose additional parsing and identity rules. The announced findings provide evidence about the reviewed cryptographic implementations. The C lifetime correction makes one of those implementation assumptions explicit, and the audit's stated scope leaves protocol-level claims to separate evidence.