Skip to content

Commit 975d5f0

Browse files
committed
fipsutil: report which module is linked, not just the mode
Enabled() and Only() answer whether the process is in FIPS 140-3 mode. They cannot answer which cryptographic module it is running, and the two are independent: a process built without GOFIPS140 but run with GODEBUG=fips140=on performs the integrity self-check, the known-answer tests and the TLS restriction, yet does all of it with the toolchain's in-tree crypto packages rather than a CMVP-validated code set. Nothing observable distinguishes that from the real thing except the version, so a caller with a certificate to point at had no way to tell. Adds Version() (the module version, "latest" when no frozen module is linked), BuildVersion() (the resolved GOFIPS140 build setting) and Validated() (whether a frozen snapshot is linked at all). Also fixes Only() on Go 1.26+. It decided by parsing the GODEBUG environment variable, which misses a default baked in at build time by GOFIPS140 or a //go:debug directive. A binary with //go:debug fips140=only and no GODEBUG set reported Only() == false while genuinely being in only-mode. crypto/fips140.Enforced, added in Go 1.26, is the runtime's own answer; older toolchains keep the environment scan since they have nothing better. crypto/fips140.Version and .Enforced are both Go 1.26, so the version-gated halves are split out and the pre-1.26 build falls back to the recorded build setting. That fallback carries the snapshot suffix ("v1.0.0-c2097c7c") where Go 1.26 reports the formal version ("v1.0.0"); the doc comment says so.
1 parent 5244a2b commit 975d5f0

6 files changed

Lines changed: 214 additions & 21 deletions

File tree

fipsutil/fipsutil.go

Lines changed: 37 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,15 @@
11
//go:build go1.24
22

3+
// Package fipsutil reports how the running process relates to the FIPS 140-3
4+
// Go Cryptographic Module.
35
package fipsutil
46

57
import (
68
"crypto/fips140"
7-
"os"
8-
"strings"
9+
"runtime/debug"
910
"sync"
1011
)
1112

12-
var (
13-
only bool
14-
once sync.Once
15-
)
16-
1713
// Enabled reports whether the cryptography libraries are operating in FIPS
1814
// 140-3 mode.
1915
//
@@ -26,24 +22,45 @@ func Enabled() bool {
2622
return fips140.Enabled()
2723
}
2824

29-
// Only reports whether the cryptography libraries are operating in FIPS 140-3
30-
// "only" mode. When in this mode, using non-approved cryptography functions
31-
// will return errors or panic.
32-
func Only() bool {
33-
once.Do(func() {
34-
if !fips140.Enabled() {
25+
var (
26+
buildVersion string
27+
buildVersionOnce sync.Once
28+
)
29+
30+
// BuildVersion returns the GOFIPS140 setting recorded in the binary, such as
31+
// "v1.0.0-c2097c7c", or the empty string if it was built without one.
32+
//
33+
// This is the resolved version rather than the value passed to the go command:
34+
// GOFIPS140=v1.0.0 is read through $GOROOT/lib/fips140/v1.0.0.txt and records
35+
// as the exact snapshot whose checksum is fixed in the module's security
36+
// policy.
37+
func BuildVersion() string {
38+
buildVersionOnce.Do(func() {
39+
info, ok := debug.ReadBuildInfo()
40+
if !ok {
3541
return
3642
}
37-
38-
// Parse GODEBUG backwards as the last value is the correct one.
39-
settings := strings.Split(os.Getenv("GODEBUG"), ",")
40-
for i := len(settings) - 1; i >= 0; i-- {
41-
if settings[i] == "fips140=only" {
42-
only = true
43+
for _, s := range info.Settings {
44+
if s.Key == "GOFIPS140" {
45+
buildVersion = s.Value
4346
return
4447
}
4548
}
4649
})
4750

48-
return only
51+
return buildVersion
52+
}
53+
54+
// Validated reports whether the binary links a frozen module snapshot rather
55+
// than the toolchain's in-tree crypto packages.
56+
//
57+
// It says nothing about whether FIPS 140-3 mode is currently on; use [Enabled]
58+
// for that.
59+
func Validated() bool {
60+
switch BuildVersion() {
61+
case "", "off", "latest":
62+
return false
63+
default:
64+
return true
65+
}
4966
}

fipsutil/fipsutil_go126.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
//go:build go1.26
2+
3+
package fipsutil
4+
5+
import "crypto/fips140"
6+
7+
// Only reports whether the cryptography libraries are operating in FIPS 140-3
8+
// "only" mode. When in this mode, using non-approved cryptography functions
9+
// will return errors or panic.
10+
//
11+
// Note that the Go project documents "only" mode as a best-effort mode for
12+
// testing, assessment and debugging that is not intended for production use.
13+
func Only() bool {
14+
return fips140.Enforced()
15+
}
16+
17+
// Version returns the FIPS 140-3 Go Cryptographic Module version, such as
18+
// "v1.0.0", when the program was built against a frozen module with GOFIPS140,
19+
// and "latest" otherwise.
20+
//
21+
// Only a concrete version ties a running process to a CMVP certificate;
22+
// "latest" ties it to nothing.
23+
func Version() string {
24+
return fips140.Version()
25+
}

fipsutil/fipsutil_internal_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
//go:build go1.24
2+
3+
package fipsutil
4+
5+
import "testing"
6+
7+
// TestValidated exercises the classification Validated() applies. The build
8+
// setting it reads cannot be changed by a test, so the cached value is swapped
9+
// directly rather than the reading of it being mocked.
10+
func TestValidated(t *testing.T) {
11+
tests := []struct {
12+
buildVersion string
13+
want bool
14+
}{
15+
{"", false},
16+
{"off", false},
17+
{"latest", false},
18+
{"v1.0.0-c2097c7c", true},
19+
{"v1.26.0", true},
20+
}
21+
22+
// Run the sync.Once now so the assignments below are not overwritten by a
23+
// later first call.
24+
BuildVersion()
25+
26+
saved := buildVersion
27+
t.Cleanup(func() { buildVersion = saved })
28+
29+
for _, tt := range tests {
30+
t.Run(tt.buildVersion, func(t *testing.T) {
31+
buildVersion = tt.buildVersion
32+
if got := Validated(); got != tt.want {
33+
t.Errorf("Validated() with BuildVersion() = %q is %v, want %v", tt.buildVersion, got, tt.want)
34+
}
35+
})
36+
}
37+
}

fipsutil/fipsutil_other.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,25 @@ func Enabled() bool {
1717
func Only() bool {
1818
return false
1919
}
20+
21+
// Version returns the FIPS 140-3 Go Cryptographic Module version.
22+
//
23+
// On Go < 1.24 there is no such module, so it will always return "latest".
24+
func Version() string {
25+
return "latest"
26+
}
27+
28+
// BuildVersion returns the GOFIPS140 setting recorded in the binary.
29+
//
30+
// On Go < 1.24 GOFIPS140 does not exist, so it will always return "".
31+
func BuildVersion() string {
32+
return ""
33+
}
34+
35+
// Validated reports whether the binary links a frozen, validated module
36+
// snapshot.
37+
//
38+
// On Go < 1.24 it will always return false.
39+
func Validated() bool {
40+
return false
41+
}

fipsutil/fipsutil_pre126.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//go:build go1.24 && !go1.26
2+
3+
package fipsutil
4+
5+
import (
6+
"crypto/fips140"
7+
"os"
8+
"strings"
9+
"sync"
10+
)
11+
12+
var (
13+
only bool
14+
once sync.Once
15+
)
16+
17+
// Only reports whether the cryptography libraries are operating in FIPS 140-3
18+
// "only" mode. When in this mode, using non-approved cryptography functions
19+
// will return errors or panic.
20+
//
21+
// Before Go 1.26 the runtime does not expose this, so it is inferred from the
22+
// GODEBUG environment variable. That misses a default baked in at build time
23+
// by GOFIPS140 or a //go:debug directive; Go 1.26 and later ask the runtime
24+
// directly and do not have that gap.
25+
func Only() bool {
26+
once.Do(func() {
27+
if !fips140.Enabled() {
28+
return
29+
}
30+
31+
// Parse GODEBUG backwards as the last value is the correct one.
32+
settings := strings.Split(os.Getenv("GODEBUG"), ",")
33+
for i := len(settings) - 1; i >= 0; i-- {
34+
if settings[i] == "fips140=only" {
35+
only = true
36+
return
37+
}
38+
}
39+
})
40+
41+
return only
42+
}
43+
44+
// Version returns the FIPS 140-3 Go Cryptographic Module version when the
45+
// program was built against a frozen module with GOFIPS140, and "latest"
46+
// otherwise.
47+
//
48+
// crypto/fips140.Version was added in Go 1.26. Before that this reports the
49+
// resolved GOFIPS140 build setting instead, so it carries the snapshot suffix
50+
// ("v1.0.0-c2097c7c") where Go 1.26 and later report the formal version
51+
// ("v1.0.0"). Use [BuildVersion] when you want the resolved value on every
52+
// Go version.
53+
func Version() string {
54+
if v := BuildVersion(); v != "" && v != "off" {
55+
return v
56+
}
57+
58+
return "latest"
59+
}

fipsutil/fipsutil_test.go

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,41 @@
11
package fipsutil
22

3-
import "testing"
3+
import (
4+
"strings"
5+
"testing"
6+
)
47

58
func TestFipsUtil(t *testing.T) {
69
t.Log("fipsutil.Enabled() is", Enabled())
710
t.Log("fipsutil.Only() is", Only())
11+
t.Log("fipsutil.Version() is", Version())
12+
t.Log("fipsutil.BuildVersion() is", BuildVersion())
13+
t.Log("fipsutil.Validated() is", Validated())
14+
}
15+
16+
// TestInvariants asserts the relationships that must hold however the test
17+
// binary was built and run, so it passes under `go test`,
18+
// `GOFIPS140=v1.0.0 go test`, and any GODEBUG=fips140 override of either.
19+
func TestInvariants(t *testing.T) {
20+
if Only() && !Enabled() {
21+
t.Error("Only() implies Enabled()")
22+
}
23+
24+
if Validated() {
25+
// A frozen module was selected, so the version must be a concrete one
26+
// and must agree with the recorded build setting. Go 1.26 and later
27+
// report the formal version ("v1.0.0") while the build setting carries
28+
// the snapshot suffix ("v1.0.0-c2097c7c"), so this is a prefix match.
29+
if got := Version(); got == "latest" {
30+
t.Error(`Validated() is true but Version() is "latest"`)
31+
} else if !strings.HasPrefix(BuildVersion(), got) {
32+
t.Errorf("BuildVersion() = %q, want it to start with Version() = %q", BuildVersion(), got)
33+
}
34+
35+
return
36+
}
37+
38+
if got := Version(); got != "latest" {
39+
t.Errorf("Version() = %q, want %q when no frozen module is linked", got, "latest")
40+
}
841
}

0 commit comments

Comments
 (0)