Files
zitadel/internal/crypto/fips140_test.go
T
Tim MöhlmannandGitHub e94d4c3986 feat(crypto): FIPS 140-3 compliant build and runtime checks (#12233)
# Which Problems Are Solved

Enable FIPS 140-3 compliant build.

# How the Problems Are Solved

- Add runtime config validation, if the FIPS flag is enabled fail the
application when a non-compliant hasher is used, or throw a warning when
a legacy verifier is used
- Add a build matrix for FIPS certified build:
  - Go binary is built with `GOFIPS140=certified`
- Login container uses a separate base:
[ubi9](https://catalog.redhat.com/en/software/containers/ubi9/ubi/615bcf606feffc5384e8452e)
from redhat which provides a FIPS certified OpenSSL (used by NodeJS TLS
stack)
- Non-FIPS images where already pushed to both Github Container Registry
and Google Artifact Repository (GAR). Fips images are only pushed to the
GAR.
- Tag versions are suffixed `-fips`. So on release the following images
will be additionally available:

```
europe-docker.pkg.dev/zitadel-common/zitadel-repo/zitadel-login:vX.Y.Z-fips
europe-docker.pkg.dev/zitadel-common/zitadel-repo/zitadel:vX.Y.Z-fips-debug
europe-docker.pkg.dev/zitadel-common/zitadel-repo/zitadel:vX.Y.Z-fips
```
  
# Other changes

- Bumb Go toolchain. At least v1.25.10 is required for a
GOFIPS140=certified setting.
 
# Additional Context

- Closes https://github.com/zitadel/zitadel/issues/4335
- Build [test
run](https://github.com/zitadel/zitadel/actions/runs/27253916052)
pushing FIPS and non-FIPS images
2026-06-16 10:16:49 +02:00

182 lines
4.2 KiB
Go

package crypto
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func withFIPS140Enabled(t *testing.T) {
t.Helper()
prev := fips140Mode
fips140Mode = func() bool { return true }
t.Cleanup(func() { fips140Mode = prev })
}
func TestHashName_IsFIPSCompliant(t *testing.T) {
tests := []struct {
name HashName
want bool
}{
{HashNamePBKDF2, true},
{HashNameArgon2, false},
{HashNameArgon2i, false},
{HashNameArgon2id, false},
{HashNameBcrypt, false},
{HashNameMd5, false},
{HashNameMd5Plain, false},
{HashNameMd5Salted, false},
{HashNamePHPass, false},
{HashNameSha2, false},
{HashNameScrypt, false},
{HashNameDrupal7, false},
}
for _, tt := range tests {
t.Run(string(tt.name), func(t *testing.T) {
assert.Equal(t, tt.want, tt.name.IsFIPSCompliant())
})
}
}
func TestHashMode_IsFIPSCompliant(t *testing.T) {
tests := []struct {
mode HashMode
want bool
}{
{HashModeSHA256, true},
{HashModeSHA384, true},
{HashModeSHA512, true},
{HashModeSHA1, false},
{HashModeSHA224, false},
}
for _, tt := range tests {
t.Run(string(tt.mode), func(t *testing.T) {
assert.Equal(t, tt.want, tt.mode.IsFIPSCompliant())
})
}
}
func TestNonFIPSVerifiersConfigured(t *testing.T) {
got := nonFIPSVerifiersConfigured([]HashName{HashNamePBKDF2, HashNameBcrypt, HashNameMd5})
assert.Equal(t, []HashName{HashNameBcrypt, HashNameMd5}, got)
assert.Equal(t, []HashName{HashNameSha2}, nonFIPSVerifiersConfigured([]HashName{HashNamePBKDF2, HashNameSha2}))
assert.Nil(t, nonFIPSVerifiersConfigured([]HashName{HashNamePBKDF2}))
}
func TestValidateFIPSPBKDF2Hasher(t *testing.T) {
tests := []struct {
name string
hasher HasherConfig
wantErr string
}{
{
name: "rounds below minimum",
hasher: HasherConfig{
Algorithm: HashNamePBKDF2,
Params: map[string]any{
"Rounds": 999,
"Hash": HashModeSHA256,
},
},
wantErr: "iteration count 999 is below the FIPS minimum",
},
{
name: "rounds at minimum",
hasher: HasherConfig{
Algorithm: HashNamePBKDF2,
Params: map[string]any{
"Rounds": 1000,
"Hash": HashModeSHA256,
},
},
},
{
name: "sha1 hash mode",
hasher: HasherConfig{
Algorithm: HashNamePBKDF2,
Params: map[string]any{
"Rounds": 10000,
"Hash": HashModeSHA1,
},
},
wantErr: "hash mode \"sha1\" is not FIPS 140-3 compliant",
},
{
name: "sha224 hash mode",
hasher: HasherConfig{
Algorithm: HashNamePBKDF2,
Params: map[string]any{
"Rounds": 10000,
"Hash": HashModeSHA224,
},
},
wantErr: "hash mode \"sha224\" is not FIPS 140-3 compliant",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := validateFIPSPBKDF2Hasher(tt.hasher)
if tt.wantErr == "" {
require.NoError(t, err)
return
}
require.Error(t, err)
assert.Contains(t, err.Error(), tt.wantErr)
})
}
}
func TestHashConfig_validateFIPS140(t *testing.T) {
withFIPS140Enabled(t)
t.Run("bcrypt hasher fails", func(t *testing.T) {
cfg := &HashConfig{
Hasher: HasherConfig{
Algorithm: HashNameBcrypt,
Params: map[string]any{"Cost": 12},
},
Limits: HashLimitsConfig{
Bcrypt: BcryptLimitsConfig{MinCost: 10, MaxCost: 16},
},
}
err := cfg.validateFIPS140()
require.Error(t, err)
assert.Contains(t, err.Error(), "uncertified cryptographic state")
assert.Contains(t, err.Error(), "bcrypt")
})
t.Run("pbkdf2 compliant passes", func(t *testing.T) {
cfg := &HashConfig{
Hasher: HasherConfig{
Algorithm: HashNamePBKDF2,
Params: map[string]any{
"Rounds": 290000,
"Hash": HashModeSHA256,
},
},
Limits: HashLimitsConfig{
PBKDF2: PBKDF2LimitsConfig{MinRounds: 1000, MaxRounds: 10000000},
},
}
require.NoError(t, cfg.validateFIPS140())
})
}
func TestHashConfig_NewHasher_FIPSBcryptFails(t *testing.T) {
withFIPS140Enabled(t)
cfg := &HashConfig{
Hasher: HasherConfig{
Algorithm: HashNameBcrypt,
Params: map[string]any{"Cost": 12},
},
Limits: HashLimitsConfig{
Bcrypt: BcryptLimitsConfig{MinCost: 10, MaxCost: 16},
},
}
_, err := cfg.NewHasher()
require.Error(t, err)
assert.Contains(t, err.Error(), "uncertified cryptographic state")
}