FIPS compliant crypto in golang


Tags: golang

Updated: 2022-09-29. To reflect that BoringCrypto is now part of the main branch.

Updated: 2022-11-20. To add a section on TLS 1.3 and FIPS.

Updated: 2023-01-07. To add a section on automatic configuration at compile time: Force FIPS-compliant crypto at compile time.

If you work with US government entities or corporations in regulated markets the subject of FIPS compliance may come up. FIPS 140-2 is a set of cryptographic standards that your application may need to adhere to.

FIPS in go #

It takes a lot to be FIPS verified or FIPS compliant (learn more), but for us the bottom line is that our app must use FIPS verified crypto libraries. This is a challenge with go, where the native crypto is not FIPS friendly.

There are no plans to change it either:

Go’s crypto is not FIPS 140 validated and I’m afraid that there is no possibility of that happening in the future either. I think Ian’s suggestion of using cgo to call out to an existing, certified library is probably your best bet. However, we would not be interested in patches to add hook points all over the Go library, so you would need to carry that work yourself.

Reimplementing crypto with cgo call outs doesn’t seem like a fun activity. Are we out of luck? Rewrite in Java? Luckily there are two options for us:

Let’s discuss them.

Boring gets interesting #

BoringSSL is a fork of OpenSSL that is designed to meet Google’s needs.

Critically, it has a FIPS 140-2 verified version.

BoringSSL is used internally in google’s monorepo. Cloudflare is another prominent user.

dev.boringcrypto is a go repo branch. It is maintained alongside the mainline go (quote from its readme):

We have been working inside Google on a fork of Go that uses BoringCrypto (the core of BoringSSL) for various crypto primitives, in furtherance of some work related to FIPS 140-2. We have heard that some external users of Go would be interested in this code as well, so I intend to create a new branch dev.boringcrypto that will hold patches to make Go use BoringCrypto.

Unlike typical dev branches, we do not intend any eventual merge of this code into the master branch. Instead we intend to maintain in that branch the latest release plus BoringCrypto patches. In this sense it is a bit like dev.typealias holding go1.8+type alias patches.

This sounds good, but in an OSS fashion there are no guarantees:

To be clear, we are not making any statements or representations about the suitability of this code in relation to the FIPS 140-2 standard. Interested users will have to evaluate for themselves whether the code is useful for their own purposes.

Since go 1.19 BoringSSL based crypto is part of the main branch: golang/go#51940.

The dev.boringcrypto branch started out as a bit of an experiment, back in the Go 1.8 time frame. It is clearly here to stay as something that we maintain alongside the main distribution.

Maintaining a whole separate branch is cumbersome, requiring frequent conflict resolution during merges and being just generally painful.

It would be far less upkeep if we kept the boringcrypto code in the main branch behind a GOEXPERIMENT, same as we do for GOEXPERIMENT=fieldtrack. We should do that.

It means that the build process is different for go 1.19+.

Updated the Build process and Boring Example sections to highlight this.

Build process #

Pass GOEXPERIMENT=boringcrypto to the go tool during build time. As simple as that.

Code changes #

No changes to the code are needed (maybe with an exception of filtering out unsupported ciphers, etc. but this is a configuration concern).

Performance impact #

The foreign function interface between golang and c is not free. The library performs worse than the build-in crypto. See golang/go#21525.

In general there is about a 200ns overhead to calling into BoringCrypto via cgo for a particular call. So for example aes.BenchmarkEncrypt (testing encryption of a single 16-byte block) went from 13ns to 209ns, or +1500%. That we can’t do much about except hope that bulk operations call into cgo once instead of once per 16 bytes.

Note that the benchmarks were done in 2017, so the things might’ve improved (but then vanilla tls/crypto improved as well). Also note the detailed benchmark results, not all ciphers performed as poorly as the highlight. Some where on the same level or faster. If you don’t care about every nanosecond of performance or your code is not on the critical path then this is good enough. Otherwise benchmark your usecase to see if the performance hit is acceptable.

Usage Example #

I’ve created a simple echo server in go to show what changes are needed to switch to boringcrypto.

It is as simple as passing the extra flag GOEXPERIMENT=boringcrypto to the build:

# Build a binary and assert that it uses boringcrypto instead of the native golang crypto
RUN GOEXPERIMENT=boringcrypto go build . && \
    go tool nm fips-echo-server > tags.txt && \
    grep '_Cfunc__goboringcrypto_' tags.txt 1> /dev/null

See the full diff:

Note on verification: Use go tool nm fips-echo-server > tags.txt and grep '_Cfunc__goboringcrypto_' tags.txt to verify if the symbol _Cfunc__goboringcrypto_ is present in the binary. As per the comment above this verifies that the binary uses BoringCrypto.

There are some other changes in the branch, e.g. to add new option -fipsMode, which resets the ciphersuite to a FIPS compliant one. You may want to do something else, like still allow user configured ciphers as long as they are a subset of FIPS compliant ones, etc. No “core” app changes required.

testssl reports this:

 Testing server's cipher preferences

Hexcode  Cipher Suite Name (OpenSSL)       KeyExch.   Encryption  Bits     Cipher Suite Name (IANA/RFC)
TLSv1.2 (server order)
 xc02f   ECDHE-RSA-AES128-GCM-SHA256       ECDH 521   AESGCM      128      TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
 xc030   ECDHE-RSA-AES256-GCM-SHA384       ECDH 521   AESGCM      256      TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
 x9c     AES128-GCM-SHA256                 RSA        AESGCM      128      TLS_RSA_WITH_AES_128_GCM_SHA256
 x9d     AES256-GCM-SHA384                 RSA        AESGCM      256      TLS_RSA_WITH_AES_256_GCM_SHA384

 Has server cipher order?     yes (OK)
 Negotiated protocol          TLSv1.2
 Negotiated cipher            ECDHE-RSA-AES128-GCM-SHA256, 521 bit ECDH (P-521)

Force FIPS-compliant crypto at compile time #

Instead of manually setting FIPS-complaint ciphers and TLS versions you add a following file to your project:

//go:build boringcrypto
// +build boringcrypto

package main

import _ "crypto/tls/fipsonly"

This forces TLSv1.2 and FIPS ciphers regardless of the runtime settings. If you don’t need FIPS/non-FIPS selection at runtime this is simpler and more convinient.

See "crypto/tls/fipsonly" package for details.

Boringcrypto, FIPS, and TLS 1.3 #

FIPS requires at least TLS 1.2, and encourages TLS 1.3. [src]. Starting from 2024, TLS 1.3 will be required.

NIST Special Publication (SP) 800-52, Rev. 2, provides guidance for the selection and configuration of TLS protocol implementations while making effective use of Federal Information Processing Standards (FIPS) and NIST-recommended cryptographic algorithms. It requires that TLS 1.2 configured with FIPS-based cipher suites be supported by all government TLS servers and clients. This Special Publication also provides guidance on certificates and TLS extensions that impact security. Support for TLS 1.3 is strongly recommended.

Agencies shall support TLS 1.3 by January 1, 2024. After this date, servers shall support TLS 1.3 for both government-only and citizen or business-facing applications.

However, google disables 1.3 in their FIPS mode by default:

The situation around FIPS 140 and TLS 1.3 is a little complex. However, Google is not in a position to give opinions on the interpretation of NIST rules and we recommend that you retain the services of an NVLAP lab for that.

I suspect this is because goboring internally uses boringcrypto. Boringcrypto TLS 1.3 cipher list is not configurable, and contains non-FIPS cipher TLS_CHACHA20_POLY1305_SHA256:

[Jul 27, 2022] @\visweshn92: Could you confirm the possibility of removing TLS_CHACHA20_POLY1305_SHA256 in the FIPS build? Or allowing a way to configure the TLS 1.3 ciphers?

[Jul 27, 2022] @\PiotrSikora: BoringSSL recently added SSL_CTX_set_compliance_policy(ssl_compliance_policy_fips_202205), which does exactly that, but it won’t be available in FIPS builds until BoringCrypto fips-20220613 is validated, which might take up to a year.

Src: [envoyproxy/envoy#19548]

go 1.18 and earlier #

Build Process #

Google maintains docker images with patched go toolchain, similar to golang:x.y.z.

A Dockerfile that starts with FROM golang:1.17.2 can switch to FROM and should need no other modifications. (…)


BoringCrypto is used for a given build only in limited circumstances:

  • The build must be GOOS=linux, GOARCH=amd64.
  • The build must have cgo enabled.
  • The android build tag must not be specified.
  • The cmd_go_bootstrap build tag must not be specified.
  • The version string reported by runtime.Version does not indicate that BoringCrypto was actually used for the build. For example, linux/386 and non-cgo linux/amd64 binaries will report a version of go1.8.3b2 but not be using BoringCrypto.

To check whether a given binary is using BoringCrypto, run go tool nm on it and check that it has symbols named *_Cfunc__goboringcrypto_*.

Usage example #

The boringssl based toolchain is basically a drop in replacement. All we need to do is to use different go image toolchain:

--- a/Dockerfile  (revision 3209b023ad9173f1deebcc5ff91b4c315510299e)
+++ b/Dockerfile  (date 1664401645292)
@@ -1,15 +1,16 @@
-FROM golang:1.18.6

See the full diff:

Upgrading from 1.18 to 1.19 #

To see the difference in boringcrypto usage between go 1.18 and go 1.19 check this diff:

diff --git a/Dockerfile b/Dockerfile
index ccb2648..5209f6f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
 # Start with an official image
+FROM golang:1.19.1
 # Bundle the project source in the container
 RUN mkdir -p /app
@@ -7,7 +7,7 @@ ADD . /app
 # Build a binary and assert that it uses boringcrypto instead of the native golang crypto
-RUN go build . && \
+RUN GOEXPERIMENT=boringcrypto go build . && \
     go tool nm fips-echo-server > tags.txt && \
     grep '_Cfunc__goboringcrypto_' tags.txt 1> /dev/null

The differences are:

  1. Switch to the standard image.
  2. Pass the GOEXPERIMENT=boringcrypto flag.

RedHat go toolchain #

Update: This section was written in 2019 and can be outdated at this point.

RedHat offers an alternative.

We are excited to announce that we plan to ship go-toolset with a new feature that allows Go to bypass the standard library cryptographic routines and instead call into a FIPS 140-2 validated cryptographic library.

I think they leverage some of the dev.boringcrypto patches internally:

This new feature builds on top of pre-existing upstream work (which instead calls into BoringSSL) …

The major differences seem to be:

  1. RedHat offers support contracts and FIPS mode for RHEL, so they are committed to supporting their FIPS complaint go toolchain.
  2. FIPS mode can be enabled at runtime.
  3. Older go version — 1.11 at the time of writing, they promise to upgrade to 1.12 in 2019. dev.boringcrypto is being kept up-to-date against master (but there are no guarantees it will be like that in the future).

Summary #

The native go crypto is not FIPS compliant, nor it will be in a foreseeable future. Luckily, both Google and RedHat provide go toolchains backed by FIPS validated SSL libraries. We can leverage them to make our go app FIPS compliant.

>> Home