mirror of
https://github.com/andwati/zitadel.git
synced 2026-10-18 05:52:51 +00:00
# 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
182 lines
4.2 KiB
Go
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")
|
|
}
|