FIPS compliant crypto in golang

2019/12/15

Tags: go

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.

Updated: 2023-08-07. To add a note of microsoft/go.

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: https://github.com/igor-kupczynski/fips-echo-server/compare/main...boringcrypto.

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)
-----------------------------------------------------------------------------------------------------------------------------
SSLv2
 -
SSLv3
 -
TLSv1
 -
TLSv1.1
 -
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
TLSv1.3
 -

 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 convenient.

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]

Microsoft go fork #

Since 2022 Microsoft provides its own fork of go microsoft/go with the stated goal:

This repository produces a modified version of Go that can be used to build FIPS 140-2 compliant applications. Our goal is to share this implementation with others in the Go community who have the same requirement, and to merge this capability into upstream Go as soon as possible. See eng/doc/fips for more information about this feature and the history of FIPS 140-2 compliance in Go.

The binaries produced by this repository are also intended for general use within Microsoft instead of the official binary distribution of Go.

We call this repository a fork even though it isn’t a traditional Git fork. Its branches do not share Git ancestry with the Go repository. However, the repository serves the same purpose as a Git fork: maintaining a modified version of the Go source code over time.

It’s interesting because it uses different libraries, and supports Windows:

On Linux, the fork uses OpenSSL through the golang-fips/openssl module in Go 1.21+ and the go-crypto-openssl module in earlier versions. On Windows, CNG, using go-crypto-winnative. Similar to BoringSSL, certain OpenSSL and CNG versions are FIPS 140-2 certified.

I personally haven’t evaluated this fork, but it’s there and it seems useful if you want to rely on different library than boringcrypto or if your application lives on Windows.

Boringcrypto in go 1.18 and earlier #

Update: This section is likely irrelevant unless you want to upgrade your old go 1.18 build.

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 us-docker.pkg.dev/google.com/api-project-999119582588/go-boringcrypto/golang:1.17.2b7 and should need no other modifications. (…)

Caveat

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
+FROM us-docker.pkg.dev/google.com/api-project-999119582588/go-boringcrypto/golang:1.18.6b7

See the full diff: https://github.com/igor-kupczynski/fips-echo-server/compare/main-1.18...boringcrypto-1.18.

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: https://github.com/igor-kupczynski/fips-echo-server/compare/boringcrypto-1.18...boringcrypto

diff --git a/Dockerfile b/Dockerfile
index ccb2648..5209f6f 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,5 +1,5 @@
# Start with an official image
-FROM us-docker.pkg.dev/google.com/api-project-999119582588/go-boringcrypto/golang:1.18.6b7
+FROM golang:1.19.1

# Bundle the project source in the container
RUN mkdir -p /app
@@ -7,7 +7,7 @@ ADD . /app
WORKDIR /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, Google, Microsoft, and RedHat provide go toolchains backed by FIPS validated SSL libraries. We can leverage them to make our go app FIPS compliant.

>> Home