fips: correct the SSH algorithm policy for both FIPS backends
What this MR does
LabKit supplies the SSH algorithm sets for FIPS 140-3 builds. This MR corrects four defects in those sets. It changes the fips package in v1 and the fips/sshalgo package in v2.
Both packages now give the same result on the same backend.
Closes #139 (closed)
Public key authentication
DefaultAlgorithms always returned an empty list of public key algorithms. ssh.Config.SetDefaults does not set that field, so the filter received nothing.
A caller that assigns the empty list leaves ssh.NewServerConn to use its own list. That list contains ssh-rsa and ssh-dss. SP 800-131A permits SHA-1 signature verification only for legacy data. FIPS 186-5 removes DSA.
The list now comes from ssh.SupportedAlgorithms, which excludes both algorithms.
Host key algorithms
A server ignores ssh.Config.HostKeyAlgorithms. The SSH library calculates the host key algorithms from each host key. For an RSA key the result contains ssh-rsa, which signs with SHA-1.
Only the caller holds the signer, so only the caller can correct this. Wrap each host key with the new HostKeySigner function before you call AddHostKey:
signer, err := sshalgo.HostKeySigner(hostKey)
if err != nil {
return err
}
config.AddHostKey(signer)HostKeySigner restricts an RSA key to SHA-2. It passes ECDSA and Ed25519 keys through. It refuses DSA keys. Without the fips build tag it returns the key unchanged.
Three policy tiers
One flat exclusion list held three different reasons to refuse an algorithm. The list now has three tiers.
| Tier | Offered | Contents |
|---|---|---|
| 1 | Never | ChaCha20-Poly1305; X25519 and its alias; security-key public key variants |
| 2 | Native Go module only | ML-KEM768/X25519 |
| 3 | By default | HMAC-SHA1, HMAC-SHA1-96, DH-group14-SHA1, and finite-field DH |
WithoutDeprecated() removes Tier 3:
algorithms := sshalgo.DefaultAlgorithms(sshalgo.WithoutDeprecated())Tier 2 restores post-quantum key exchange on the native module. ML-KEM-768 gives an approved shared secret, so the X25519 part is permitted additional keying material under SP 800-227 §4.6.2. BoringCrypto keeps the exclusion, because the golang-fips toolchain refuses X25519 while OpenSSL is active.
Tier 3 holds algorithms that FIPS 140-3 still approves. SP 800-131A deprecates the SHA-1 algorithms only until 31 December 2030. The finite-field groups are approved safe primes, but x/crypto computes them in pure Go, outside every validated boundary. To refuse these algorithms is a hardening choice, not a compliance requirement, so the default keeps old clients operational.
Compatibility
Every change is behind the fips build tag. A non-FIPS build is unaffected.
A FIPS build regains hmac-sha1, hmac-sha1-96 and diffie-hellman-group14-sha1. It loses chacha20-poly1305@openssh.com, which is not an approved security function.
v1 and v2 now produce identical sets. A consumer that moves from v1 to v2 sees no change.
Tests
The algorithm sets are advisory, so the tests negotiate a real SSH handshake and assert on ssh.NegotiatedAlgorithms. This is the first handshake-level coverage in either package.
CI also gains two corrections. golangci-lint never inspected either fips package, because every file sits behind the fips tag. The test-fips job never activated BoringCrypto, because a container has no /proc/sys/crypto/fips_enabled.
Each change is verified in linux/amd64 containers, in four configurations:
| Configuration | Backend |
|---|---|
| no build tag | none |
-tags fips, no backend |
none |
-tags fips, GOEXPERIMENT=boringcrypto, GOLANG_FIPS=1 |
boringcrypto |
-tags fips, GOFIPS140=v1.0.0 |
native-go |
All four pass ./scripts/test.sh with -race. golangci-lint reports no issues in either module. The go directive does not change in either module.
Review note
v2/fips/sshalgo/COMPLIANCE.md maps each algorithm to the governing NIST document. One citation in it is not yet verified: the CMVP hybrid-KEM process document (IG D.S Scenario 2 and IG 2.4.A). The text came from a search index, not from the PDF. Please do not cite that line externally until someone confirms it.
Size
Danger reports that this MR is too big. It is not split, for two reasons.
Roughly two thirds of the insertions are tests. Most of the rest is duplicated, because v1 and v2 are deliberate mirrors of one policy. Production code is about 365 lines per module.
A split by module is possible, and it matches how the earlier FIPS work landed. It would delay the property that answers the gitlab-shell question, because v1 and v2 only produce identical sets when both parts merge. A reviewer who prefers the split should say so.
Review fixes
Two findings from the first review round are addressed.
The lint change had inverted the blind spot. Go build constraints evaluate !fips as false when the tag is set, so //go:build !fips files became invisible instead. Each module is now linted once per tag set, and the tag is out of the config file. A deliberate error on each side of the tag is now reported.
HostKeySigner failed open for an RSA signer that does not implement ssh.AlgorithmSigner. Such a server advertises only ssh-rsa. It signs with SHA-1 and it refuses the clients that ask for SHA-2, so the call left the server worse off than no call at all. It now returns an error.