Red Hat Enterprise Linux (RHEL) ships with several Federal Information Processing Standards (FIPS)-validated cryptography libraries, including OpenSSL. This allows applications that use these libraries to operate in FIPS mode, which means that the cryptographic techniques they use can are in compliance with the FIPS-140-2 standard. Any organization that works with the U.S. Federal government must comply with this standard.
By default, applications written in Go use cryptographic functions from the Go standard library, which is not FIPS-validated. However, the version of Go shipped in RHEL is based on upstream Go's dev.boringcrypto branch, which is modified to use BoringSSL for crypto primitives. Modifications made in the RHEL version replace BoringSSL with OpenSSL. These modifications allow applications written with RHEL's Go to use crypto functions from a FIPS-validated version of OpenSSL. This article will show you how to verify that your system, including your installation of the Go language, is capable of operating in FIPS mode.
How to get started
Begin by building your Go binary with the Go compiler shipped in RHEL. To do this quickly use the ubi8-minimal Universal Base Image (UBI) as your build environment and install the go-toolset
package.
To confirm that the Go compiler and built binary are FIPS-capable, run the command below, modified as appropriate for your environment (e.g, $BINARY
could be /usr/bin/go
):
$ go tool nm $BINARY | grep FIPS
If the output looks like the listing below, with references to named FIPS
functions, then the binary is FIPS-capable. (If it weren't FIPS-capable, the output would be empty.)
[root@rhel-8-4 ~]# go tool nm ./main | grep FIPS
401210 T _cgo_23e85cd750d7_Cfunc__goboringcrypto_FIPS_mode
5d8d80 d _g_FIPS_mode
4c2680 T crypto/internal/boring._Cfunc__goboringcrypto_FIPS_mode
5a27c0 D crypto/internal/boring._cgo_23e85cd750d7_Cfunc__goboringcrypto_FIPS_mode
Next, run the application in a container that includes a FIPS-compliant OpenSSL library. Again, you can use the ubi8-minimal image, which fulfills the requirement. If you're using another image, you can check to see if the OpenSSL installation is FIPS-capable with the following command:
$ openssl version
OpenSSL 1.1.1k FIPS 25 Mar 2021
Note: This can be disabled by enforcing pure Go with a build time flag.
As we noted above, the version of Go that ships with RHEL is based on the upstream Go's dev.boringcrypto branch, which is modified to use BoringSSL. You can verify this by looking at the source code either in the Go source RPM file or in Fedora's source code manager.
Grepping over the Go source code shows the first signs of the patched-in BoringSSL support:
$ grep -r boring /usr/lib/golang/src/crypto/ | head
/usr/lib/golang/src/crypto/aes/cipher.go:import "crypto/internal/boring"
/usr/lib/golang/src/crypto/aes/cipher.go: if boring.Enabled() {
The crypto/internal/boring package includes directives that use CGO to dynamically link against libdl
:
package boring
// #include "goboringcrypto.h"
// #cgo LDFLAGS: -ldl
import "C"
Linking against libdl
allows the use of the dl_open()
function, which allows for further loading of shared libraries. The dl_open()
function is called to load libcrypto, one of the shared libraries in OpenSSL:
static void*
_goboringcrypto_DLOPEN_OPENSSL(void)
{
if (handle)
{
return handle;
}
#if OPENSSL_VERSION_NUMBER < 0x10100000L
handle = dlopen("libcrypto.so.10", RTLD_NOW | RTLD_GLOBAL);
#else
handle = dlopen("libcrypto.so.1.1", RTLD_NOW | RTLD_GLOBAL);
#endif
return handle;
}
Because dl_open()
is used to load the OpenSSL libraries, rather than linking the actual Go binaries, libcrypto
will not appear in the output of ldd
when run on binaries built with RHEL Go (though libdl
will).
Although libdl
is linked, OpenSSL is not used by default. The crypto/internal/boring package will always load OpenSSL, but it will only use it if FIPS mode is enabled:
func init() {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
// Check if we can `dlopen` OpenSSL
if C._goboringcrypto_DLOPEN_OPENSSL() == C.NULL {
return
}
// Initialize the OpenSSL library.
C._goboringcrypto_OPENSSL_setup()
// Check to see if the system is running in FIPS mode, if so
// enable "boring" mode to call into OpenSSL for FIPS compliance.
if fipsModeEnabled() {
enableBoringFIPSMode()
}
sig.BoringCrypto()
}
The FIPS_mode_set()
and FIPS_mode()
functions are defined in OpenSSL, which is why libcrypto
is loaded even if it won't be used for crypto functions. OpenSSL is used to check if the system is in FIPS mode.
How to verify FIPS mode
Depending on how deep you want to go, there are a couple of different ways in which you can check for FIPS compliance.
Custom fips-detect tool
A tool called fips-detect is available to determine whether your system or container and your Golang binary are ready to run in FIPS mode. It accomplishes this by performing checks on the running system and the supplied binary to see if everything is in place to correctly run in FIPS mode.
Common tools
If you don't want to install a custom tool, you can use ldd
to show that Go binaries are linked against libdl
, and inspect the source code to determine if dl_open()
is used to load OpenSSL. There are a couple of options available, including go tool nm
or readelf -s
. However, keep in mind that no checks on the underlying system are performed with these common tools, and it may not be convincing enough to inspect the Go binaries alone.
If the binary is compiled on a standard Fedora 34 system, when you enter this go tool nm
command:
$ go tool nm ./main | grep -i dlopen_openssl
The output you'll get will be blank. If the binary is compiled on RHEL 8, however, this is the result:
$ go tool nm ./main | grep -i dlopen_openssl
[root@rhel-8-4 ~]# go tool nm ./main | grep -i dlopen_openssl
4018d0 T _cgo_fb383f177a95_Cfunc__goboringcrypto_DLOPEN_OPENSSL
From this output, it is clear that, when compiling with the RHEL Golang compiler, the binary is at least able to call into OpenSSL and enable FIPS mode.
To verify that the program runs in FIPS mode, use the LD_DEBUG=symbols
environment variable. This shows the various symbols the binary binds from shared libraries to determine whether the application actually calls into OpenSSL.
The binary runs in FIPS mode when executed with the OPENSSL_FORCE_FIPS_MODE=1
variable. In the following example, we use a custom Go binary that computes a hash using the SHA1 algorithm:
[root@rhel-8-4 ~]# env OPENSSL_FORCE_FIPS_MODE=1 ./main
5939: symbol=FIPS_mode; lookup in file=./main [0]
5939: symbol=FIPS_mode; lookup in file=/lib64/libssl.so.1.1 [0]
5939: symbol=FIPS_mode; lookup in file=/lib64/libcrypto.so.1.1 [0]
5939: symbol=SHA1_Init; lookup in file=./main [0]
5939: symbol=SHA1_Init; lookup in file=/lib64/libssl.so.1.1 [0]
5939: symbol=SHA1_Init; lookup in file=/lib64/libcrypto.so.1.1 [0]
5939: symbol=SHA1_Update; lookup in file=./main [0]
5939: symbol=SHA1_Update; lookup in file=/lib64/libssl.so.1.1 [0]
5939: symbol=SHA1_Update; lookup in file=/lib64/libcrypto.so.1.1 [0]
5939: symbol=SHA1_Final; lookup in file=./main [0]
5939: symbol=SHA1_Final; lookup in file=/lib64/libssl.so.1.1 [0]
5939: symbol=SHA1_Final; lookup in file=/lib64/libcrypto.so.1.1 [0]
SHA1: C8282111D0FAD11680B3775A36E68DC41E36F911
For comparison, this is the result when it runs without this variable:
[root@rhel-8-4 ~]# ./main
12957: symbol=FIPS_mode_set; lookup in file=/lib64/libcrypto.so.1.1 [0]
12957: symbol=dlsym; lookup in file=./main [0]
12957: symbol=dlsym; lookup in file=/lib64/libdl.so.2 [0]
12957: symbol=OPENSSL_init; lookup in file=/lib64/libcrypto.so.1.1 [0]
12957: symbol=FIPS_mode; lookup in file=/lib64/libcrypto.so.1.1 [0]
SHA1: C8282111D0FAD11680B3775A36E68DC41E36F911
The differences are:
- When the binary runs in non-FIPS mode, it uses the Golang standard crypto library, which uses its own routines (these are statically included in the binary) rather than calling into OpenSSL. This is why the dynamic linker doesn't bind OpenSSL symbols.
- OpenSSL is still loaded even when FIPS mode is disabled. It provides the functions used to check if FIPS mode is enabled or disabled. Only enabled FIPS mode uses OpenSSL for cryptographic functions.
FIPS mode
Earlier, we used the OPENSSL_FORCE_FIPS_MODE
environment variable to force the binary to behave as if FIPS mode were enabled.
The following is a snippet of the libcrypto.so
shared library constructor. It runs in the lib init phase before the dynamic linker transfers the flow control to main()
:
# define FIPS_MODE_SWITCH_FILE "/proc/sys/crypto/fips_enabled"
static void init_fips_mode(void)
{
char buf[2] = "0";
int fd;
if (secure_getenv("OPENSSL_FORCE_FIPS_MODE") != NULL) {
buf[0] = '1';
} else if ((fd = open(FIPS_MODE_SWITCH_FILE, O_RDONLY)) >= 0) {
while (read(fd, buf, sizeof(buf)) < 0 && errno == EINTR) ;
close(fd);
}
if (buf[0] != '1' && !FIPS_module_installed())
return;
FIPS_mode_set(1);
if (buf[0] != '1') {
/* drop down to non-FIPS mode if it is not requested */
FIPS_mode_set(0);
} else {
/* abort if selftest failed */
FIPS_selftest_check();
}
}
void __attribute__ ((constructor)) OPENSSL_init_library(void)
{
static int done = 0;
if (done)
return;
done = 1;
init_fips_mode();
}
This snippet comes from a patch included in the RHEL and Fedora versions of OpenSSL, so it only applies to RHEL and the UBI container image.
The shared library constructor shows that, to transparently enable FIPS mode, you must either define the OPENSSL_FORCE_FIPS_MODE
variable or ensure that the first byte of the /proc/sys/crypto/fips_enabled
file contains 1.
Containers will detect hosts that are in FIPS mode when /proc/sys/crypto/fips_enabled
appears with the host's value in all container PID namespaces.
Conclusion
Building Go applications on RHEL allows them to run in two different modes, default and FIPS. The default mode uses the Go standard library, and the FIPS mode uses a FIPS-validated version of OpenSSL. This provides developers an easy way to meet compliance requirements that mandate the use of FIPS-validated libraries, while also preserving consistency with applications built upstream.
Last updated: June 8, 2022