Blog
May 20, 2020 Marie H.

FIPS 140-2 Compliance in Go Applications

FIPS 140-2 Compliance in Go Applications

Photo by <a href="https://unsplash.com/@jakubzerdzicki?utm_source=cloudista&utm_medium=referral" target="_blank" rel="noopener">Jakub Żerdzicki</a> on <a href="https://unsplash.com/?utm_source=cloudista&utm_medium=referral" target="_blank" rel="noopener">Unsplash</a>

FIPS 140-2 Compliance in Go Applications

FIPS 140-2 compliance is a hard requirement for IBM's key encryption services. If you're building anything that handles cryptographic material for US federal agencies or regulated industries, you're eventually going to hit this requirement. Here's what it actually means in practice and how Go handles it — including the parts the docs gloss over.

What FIPS 140-2 Actually Means

FIPS 140-2 (Federal Information Processing Standard) defines requirements for cryptographic modules used in US government systems. "Compliance" means two distinct things, and conflating them will get you in trouble:

Algorithm compliance: using only FIPS-approved algorithms — AES, SHA-2, RSA, ECDSA, HMAC. Not using MD5 for anything security-related, not using SHA-1 for digital signatures, not using RC4, 3DES (deprecated), or custom crypto.

Module validation: using a cryptographic module that has actually been submitted to NIST and validated — this is a formal multi-year process. The software module gets a certificate number. When someone asks if your software is "FIPS validated," they mean the second thing.

Most engineers encounter only the first requirement. The second is an organizational/procurement concern, but you need to be aware of it because sometimes you're asked to use a validated module in your software.

Go's standard crypto package is not FIPS validated out of the box. The BoringCrypto path gets you there.

Go's Crypto Package: What's FIPS-Approved

The following are FIPS-approved and available in Go's standard library:

  • AES (crypto/aes) — AES-128, AES-192, AES-256
  • AES-GCM (crypto/cipher with NewGCM) — for authenticated encryption
  • SHA-2 family (crypto/sha256, crypto/sha512) — SHA-224, SHA-256, SHA-384, SHA-512
  • RSA (crypto/rsa) — for key sizes ≥2048 bits
  • ECDSA (crypto/ecdsa) with P-256, P-384, P-521 curves
  • HMAC (crypto/hmac) with SHA-2
  • TLS 1.2+ with approved cipher suites

What you must not use for anything security-sensitive:

  • crypto/md5 — MD5 is explicitly prohibited for security use
  • crypto/sha1 — SHA-1 is deprecated for digital signatures (still allowed for some non-security uses, but avoid it)
  • crypto/rc4 — RC4 is prohibited
  • math/rand — this is not a cryptographically secure RNG, ever

The most common violation I see in code reviews: using math/rand to generate nonces or IDs. Use crypto/rand exclusively for anything security-relevant.

// WRONG — do not use this for security purposes
import "math/rand"
nonce := make([]byte, 12)
rand.Read(nonce) // this is deterministic and predictable

// CORRECT
import "crypto/rand"
nonce := make([]byte, 12)
if _, err := rand.Read(nonce); err != nil {
    return fmt.Errorf("generating nonce: %w", err)
}

Building Go with BoringCrypto

Google maintains a fork of Go that replaces the standard crypto implementations with BoringSSL's FIPS-validated implementation. Red Hat's Go builds for RHEL also include this. This is the path to using a FIPS-validated module.

GOEXPERIMENT=boringcrypto go build ./...

You can verify that BoringCrypto is linked into your binary:

go tool nm ./keyservice | grep _Cfunc__goboringcrypto

If you see symbols like _Cfunc__goboringcrypto_AES_encrypt, BoringCrypto is present.

This requires CGO to be enabled (CGO_ENABLED=1). If you're building fully static Go binaries, this complicates things — BoringCrypto links against BoringSSL which is a C library. You can still build a "mostly static" binary:

CGO_ENABLED=1 GOEXPERIMENT=boringcrypto \
  go build -ldflags '-linkmode external -extldflags "-static"' ./...

Be aware that not all Linux base images have the necessary C libraries. We build on RHEL and deploy on RHEL UBI minimal images, which works cleanly.

Verifying FIPS Mode at Runtime

At service startup I check that we're actually running in FIPS mode and fail loud if not:

//go:build boringcrypto

package main

import "crypto/internal/boring"

func checkFIPS() {
    if !boring.Enabled() {
        panic("FIPS mode required but BoringCrypto is not enabled")
    }
    log.Println("BoringCrypto enabled — FIPS mode active")
}

The boring.Enabled() function is only available in BoringCrypto builds. The //go:build boringcrypto constraint means this file is only compiled when you pass GOEXPERIMENT=boringcrypto. Without the constraint you'd get a compile error on standard builds.

For a non-BoringCrypto build (dev environment), have a stub:

//go:build !boringcrypto

package main

func checkFIPS() {
    log.Println("WARNING: BoringCrypto not enabled, FIPS mode inactive")
}

Common Pitfalls

TLS cipher suites: Go's TLS 1.2 supports non-FIPS cipher suites by default. With BoringCrypto enabled, non-FIPS suites are automatically excluded. Without it, you need to set them explicitly:

config := &tls.Config{
    MinVersion: tls.VersionTLS12,
    CipherSuites: []uint16{
        tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
        tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
        tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
        tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
    },
}

Key sizes: RSA keys must be at least 2048 bits. I've seen test code using 1024-bit keys to speed up tests — those keys are not FIPS-approved and if that test code ever leaks into production you have a problem. Use 2048 minimum, 4096 for anything that protects long-lived secrets.

AES-GCM nonce reuse: GCM is catastrophically broken if you ever reuse a nonce with the same key. Use a random 12-byte nonce from crypto/rand for each encryption operation. With AES-256, the birthday bound for a random nonce is 2^32 encryptions — if you're encrypting more than that with a single key, rotate the key.

func encryptAESGCM(key, plaintext, additionalData []byte) ([]byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, fmt.Errorf("creating cipher: %w", err)
    }

    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return nil, fmt.Errorf("creating GCM: %w", err)
    }

    nonce := make([]byte, gcm.NonceSize())
    if _, err := rand.Read(nonce); err != nil {
        return nil, fmt.Errorf("generating nonce: %w", err)
    }

    // Prepend nonce to ciphertext for storage
    ciphertext := gcm.Seal(nonce, nonce, plaintext, additionalData)
    return ciphertext, nil
}

func decryptAESGCM(key, ciphertext, additionalData []byte) ([]byte, error) {
    block, err := aes.NewCipher(key)
    if err != nil {
        return nil, fmt.Errorf("creating cipher: %w", err)
    }

    gcm, err := cipher.NewGCM(block)
    if err != nil {
        return nil, fmt.Errorf("creating GCM: %w", err)
    }

    nonceSize := gcm.NonceSize()
    if len(ciphertext) < nonceSize {
        return nil, fmt.Errorf("ciphertext too short")
    }

    nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
    return gcm.Open(nil, nonce, ciphertext, additionalData)
}

The additionalData parameter: always use it. Additional authenticated data (AAD) binds the ciphertext to its context. If you're encrypting a record with an ID, include the record ID as AAD. This prevents an attacker from taking a valid ciphertext and replaying it in a different context even if they can't decrypt it.

Dependency Auditing

Your FIPS compliance is only as good as your dependency tree. Third-party libraries may use non-FIPS algorithms internally. Go's BoringCrypto build replaces the standard crypto implementations, but if a library uses a pure-Go crypto implementation instead of the standard library, BoringCrypto doesn't help you.

Grep for prohibited imports across your vendor directory:

grep -r '"crypto/md5"' vendor/
grep -r '"crypto/rc4"' vendor/
grep -r '"crypto/sha1"' vendor/
grep -r '"math/rand"' vendor/

Some results will be legitimate (e.g., crypto/md5 used for checksums, not security). Review them. For actual security use, they're violations.

Updated March 2026: Go 1.21 introduced GODEBUG=fips140=on as an officially supported mechanism to enable FIPS mode, moving away from the GOEXPERIMENT=boringcrypto approach. As of Go 1.24, the crypto/fips140 package provides a stable API for checking and enforcing FIPS mode — fips140.Enabled() replaces the internal boring.Enabled() call shown here. The GOEXPERIMENT=boringcrypto path still works but the GODEBUG approach is now preferred and doesn't require special build tags. Red Hat and IBM both ship Go toolchains with FIPS support built in for their enterprise platforms. If you're on a recent Go version, consult the crypto/fips140 package documentation rather than relying on the internal boring package.

The bottom line on FIPS: treat it as a continuous concern, not a one-time audit. Every dependency update is a potential regression. Automate the checks.