Description
Go version
go version go1.22.5 linux/amd64
Output of go env
in your module/workspace:
O111MODULE=''
GOARCH='amd64'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/nix/store/16g3awzjv3341s27hkkkv1vk5dw4206m-go-1.22.5/share/go'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/nix/store/16g3awzjv3341s27hkkkv1vk5dw4206m-go-1.22.5/share/go/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.22.5'
GCCGO='gccgo'
GOAMD64='v1'
AR='ar'
CC='gcc'
CXX='g++'
CGO_ENABLED='0'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -m64 -fno-caret-diagnostics -Qunused-arguments -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build2148692384=/tmp/go-build -gno-record-gcc-switches'
What did you do?
The following Go program starts a simple HTTPS server on port :4443
using a self-signed RSA certificate and writes a CPU profile to cpu.pprof
: https://go.dev/play/p/6DzPHSlU1r2
Compile this program using the following go build
commands on a linux/amd64 machine:
GOEXPERIMENT=boringcrypto CGO_ENABLED=0 go build -o fips-test main.go
GOEXPERIMENT=boringcrypto CGO_ENABLED=1 go build -o fips-test main.go
What did you see happen?
Both commands produce a valid binary without any error. Inspecting both binaries with rsc.io/goversion
results in:
-
goversion -crypto -m fips-test fips-test go1.22.5 X:boringcrypto (standard crypto) path command-line-arguments build -buildmode=exe build -compiler=gc build CGO_ENABLED=0 build GOARCH=amd64 build GOEXPERIMENT=boringcrypto build GOOS=linux build GOAMD64=v1
-
goversion -crypto -m fips-test fips-test go1.22.5 X:boringcrypto (boring crypto) path command-line-arguments build -buildmode=exe build -compiler=gc build CGO_ENABLED=1 build CGO_CFLAGS= build CGO_CPPFLAGS= build CGO_CXXFLAGS= build CGO_LDFLAGS= build GOARCH=amd64 build GOEXPERIMENT=boringcrypto build GOOS=linux build GOAMD64=v1
Looking at the cpu.pprof
files also shows that the boringcrypto C implementation is only selected when CGO_ENABLED=1
.
go tool pprof cpu.pprof
File: fips-test
Type: cpu
Time: Jul 25, 2024 at 4:33pm (CEST)
Duration: 13.37s, Total samples = 50ms ( 0.37%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top 10
Showing nodes accounting for 50ms, 100% of 50ms total
Showing top 10 nodes out of 15
flat flat% sum% cum cum%
20ms 40.00% 40.00% 40ms 80.00% crypto/internal/bigmod.(*Nat).montgomeryMul
20ms 40.00% 80.00% 20ms 40.00% crypto/internal/bigmod.addMulVVW2048
10ms 20.00% 100% 10ms 20.00% crypto/internal/bigmod.(*Nat).shiftIn
0 0% 100% 40ms 80.00% crypto/internal/bigmod.(*Nat).Exp
0 0% 100% 10ms 20.00% crypto/internal/bigmod.(*Nat).Mod
0 0% 100% 50ms 100% crypto/rsa.(*PrivateKey).Sign
0 0% 100% 50ms 100% crypto/rsa.SignPSS
0 0% 100% 50ms 100% crypto/rsa.decrypt
0 0% 100% 50ms 100% crypto/rsa.signPSSWithSalt
0 0% 100% 50ms 100% crypto/tls.(*Conn).HandshakeContext
File: fips-test
Type: cpu
Time: Jul 25, 2024 at 4:40pm (CEST)
Duration: 12.57s, Total samples = 50ms ( 0.4%)
Entering interactive mode (type "help" for commands, "o" for options)
(pprof) top 10
Showing nodes accounting for 50ms, 100% of 50ms total
Showing top 10 nodes out of 42
flat flat% sum% cum cum%
30ms 60.00% 60.00% 30ms 60.00% runtime.cgocall
10ms 20.00% 80.00% 10ms 20.00% runtime.memclrNoHeapPointers
10ms 20.00% 100% 10ms 20.00% runtime/internal/syscall.Syscall6
0 0% 100% 10ms 20.00% bufio.(*Writer).Flush
0 0% 100% 30ms 60.00% crypto/internal/boring.(*PrivateKeyRSA).withKey
0 0% 100% 30ms 60.00% crypto/internal/boring.SignRSAPSS
0 0% 100% 30ms 60.00% crypto/internal/boring.SignRSAPSS.func1
0 0% 100% 30ms 60.00% crypto/internal/boring.SignRSAPSS.func1.2
0 0% 100% 30ms 60.00% crypto/internal/boring._Cfunc__goboringcrypto_RSA_sign_pss_mgf1
0 0% 100% 30ms 60.00% crypto/rsa.(*PrivateKey).Sign
Importing crypto/tls/fipsonly
works in both cases and the Go TLS stack actually rejects TLS 1.3 connections (TLS 1.3 has to be disabled for boringcrypto). When obserbing the TLS stack behavior, it seems that the binary uses boringcrypto
when CGO_ENABLED=0
even though it actually uses the standard library crypto implementations.
What did you expect to see?
This seems like a "footgun" in the sense that people might try to build a binary that uses a FIPS 140-2 certified module to meet compliance requirements but accidentally have set CGO_ENABLED=0
in their build environment.
While it might seem obvious that GOEXPERIMENT=boringcrypto
demands CGO_ENABLED=1
, I was not able to find any documentation for this. Also there are blog posts not mentioning this in any way. For example: https://medium.com/cyberark-engineering/navigating-fips-compliance-for-go-applications-libraries-integration-and-security-42ac87eec40b
I'm aware that building Go binaries with boringcrypto as crypto backend is not officially supported. However, requiring CGO_ENABLED=1
does not seem to have any downside.