diff --git a/ssh/cipher_test.go b/ssh/cipher_test.go index 8d9a81f14c..3b369c8afd 100644 --- a/ssh/cipher_test.go +++ b/ssh/cipher_test.go @@ -17,14 +17,9 @@ import ( ) func TestDefaultCiphersExist(t *testing.T) { - for _, cipherAlgo := range SupportedAlgorithms().Ciphers { + for _, cipherAlgo := range allAlgorithms().Ciphers { if _, ok := cipherModes[cipherAlgo]; !ok { - t.Errorf("supported cipher %q is unknown", cipherAlgo) - } - } - for _, cipherAlgo := range InsecureAlgorithms().Ciphers { - if _, ok := cipherModes[cipherAlgo]; !ok { - t.Errorf("preferred cipher %q is unknown", cipherAlgo) + t.Errorf("cipher %q is unknown", cipherAlgo) } } } diff --git a/ssh/client.go b/ssh/client.go index 33079789bc..d624e1dda4 100644 --- a/ssh/client.go +++ b/ssh/client.go @@ -12,6 +12,8 @@ import ( "os" "sync" "time" + + "golang.org/x/crypto/ssh/internal/fips" ) // Client implements a traditional SSH client that supports shells, @@ -71,6 +73,23 @@ func NewClient(c Conn, chans <-chan NewChannel, reqs <-chan *Request) *Client { func NewClientConn(c net.Conn, addr string, config *ClientConfig) (Conn, <-chan NewChannel, <-chan *Request, error) { fullConf := *config fullConf.SetDefaults() + if len(config.HostKeyAlgorithms) == 0 { + if fips.Enabled { + config.HostKeyAlgorithms = fipsHostKeyAlgos + } else { + config.HostKeyAlgorithms = preferredHostKeyAlgos + } + } else { + var hostKeyAlgos []string + supported := allAlgorithms().HostKeys + for _, h := range config.HostKeyAlgorithms { + // Ignore unsupported host key algorithms. + if contains(supported, h) { + hostKeyAlgos = append(hostKeyAlgos, h) + } + } + config.HostKeyAlgorithms = hostKeyAlgos + } if fullConf.HostKeyCallback == nil { c.Close() return nil, nil, nil, errors.New("ssh: must specify HostKeyCallback") diff --git a/ssh/client_auth.go b/ssh/client_auth.go index b86dde151d..e510fe0312 100644 --- a/ssh/client_auth.go +++ b/ssh/client_auth.go @@ -10,6 +10,8 @@ import ( "fmt" "io" "strings" + + "golang.org/x/crypto/ssh/internal/fips" ) type authResult int @@ -315,6 +317,18 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand origSignersLen := len(signers) for idx := 0; idx < len(signers); idx++ { signer := signers[idx] + if fips.Enabled { + if rsaKey, ok := signer.PublicKey().(*rsaPublicKey); ok { + if !isFIPSSupportedRSASize(rsaKey.N.BitLen()) { + continue + } + } + if ecdsaKey, ok := signer.PublicKey().(*ecdsaPublicKey); ok { + if !isFIPSSupportedECDSASize(ecdsaKey.Params().BitSize) { + continue + } + } + } pub := signer.PublicKey() as, algo, err := pickSignatureAlgorithm(signer, extensions) if err != nil && errSigAlgo == nil { diff --git a/ssh/client_auth_test.go b/ssh/client_auth_test.go index d08d865d0a..695c020f51 100644 --- a/ssh/client_auth_test.go +++ b/ssh/client_auth_test.go @@ -324,9 +324,7 @@ func TestMethodInvalidAlgorithm(t *testing.T) { } func TestClientHMAC(t *testing.T) { - supportedAlgos := SupportedAlgorithms() - insecureAlgos := InsecureAlgorithms() - supportedMACs := append(supportedAlgos.MACs, insecureAlgos.MACs...) + supportedMACs := allAlgorithms().MACs for _, mac := range supportedMACs { config := &ClientConfig{ User: "testuser", diff --git a/ssh/common.go b/ssh/common.go index 1cdd008934..d2d178748d 100644 --- a/ssh/common.go +++ b/ssh/common.go @@ -15,6 +15,8 @@ import ( _ "crypto/sha1" _ "crypto/sha256" _ "crypto/sha512" + + "golang.org/x/crypto/ssh/internal/fips" ) // These are string constants in the SSH protocol. @@ -84,6 +86,9 @@ var ( // package and which have security issues. insecureKexAlgos = []string{InsecureKeyExchangeDH14SHA1, InsecureKeyExchangeDH1SHA1, InsecureKeyExchangeDHGEXSHA1} + // fipsKexAlgos specifies FIPS approved key-exchange algorithms implemented + // by this package. + fipsKexAlgos = []string{KeyExchangeECDHP256, KeyExchangeECDHP384} // supportedCiphers specifies cipher algorithms implemented by this package // in preference order, excluding those with security issues. supportedCiphers = []string{ @@ -100,6 +105,12 @@ var ( InsecureCipherTripleDESCBC, InsecureCipherRC4256, InsecureCipherRC4128, InsecureCipherRC4, } + // fipsCiphers specifies FIPS approved cipher algorithms implemented by this + // package. + fipsCiphers = []string{ + CipherAES128GCM, CipherAES256GCM, + CipherAES128CTR, CipherAES192CTR, CipherAES256CTR, + } // supportedMACs specifies MAC algorithms implemented by this package in // preference order, excluding those with security issues. supportedMACs = []string{HMACSHA256ETM, HMACSHA512ETM, @@ -113,6 +124,11 @@ var ( // insecureMACs specifies MAC algorithms implemented by this // package and which have security issues. insecureMACs = []string{InsecureHMACSHA196, InsecureHMACSHA1} + // fipsMACs specifies FIPS approved MAC algorithms implemented by this + // package. + fipsMACs = []string{HMACSHA256ETM, HMACSHA512ETM, + HMACSHA256, HMACSHA512, + } // supportedHostKeyAlgos specifies the supported host-key algorithms (i.e. // methods of authenticating servers) implemented by this package in // preference order, excluding those with security issues. @@ -143,6 +159,15 @@ var ( insecureHostKeyAlgos = []string{KeyAlgoRSA, InsecureKeyAlgoDSA, CertAlgoRSAv01, InsecureCertAlgoDSAv01, } + // fipsHostKeyAlgos specifies FIPS approved host-key algorithms implemented + // by this package. + fipsHostKeyAlgos = []string{ + CertAlgoRSASHA256v01, CertAlgoRSASHA512v01, + CertAlgoECDSA256v01, CertAlgoECDSA384v01, + + KeyAlgoECDSA256, KeyAlgoECDSA384, + KeyAlgoRSASHA256, KeyAlgoRSASHA512, + } // supportedPubKeyAuthAlgos specifies the supported client public key // authentication algorithms. Note that this doesn't include certificate // types since those use the underlying algorithm. Order is irrelevant. @@ -166,6 +191,12 @@ var ( // insecurePubKeyAuthAlgos specifies client public key authentication // algorithms implemented by this package and which have security issues. insecurePubKeyAuthAlgos = []string{KeyAlgoRSA, InsecureKeyAlgoDSA} + // fipsPubKeyAuthAlgos specifies FIPS approved public key authentication + // algorithms implemented by this package. + fipsPubKeyAuthAlgos = []string{ + KeyAlgoECDSA256, KeyAlgoECDSA384, + KeyAlgoRSASHA256, KeyAlgoRSASHA512, + } ) // NegotiatedAlgorithms defines algorithms negotiated between client and server. @@ -186,10 +217,31 @@ type Algorithms struct { PublicKeyAuths []string } +// isFIPSSupportedRSASize returns true if the specified size (in bits) is +// supported for RSA keys in FIPS mode. +func isFIPSSupportedRSASize(size int) bool { + return size == 2048 || size == 3072 || size == 4096 +} + +// isFIPSSupportedECDSASize returns true if the specified size (in bits) is +// supported for ECDSA keys in FIPS mode. +func isFIPSSupportedECDSASize(size int) bool { + return size == 256 || size == 384 +} + // SupportedAlgorithms returns algorithms currently implemented by this package, // excluding those with security issues, which are returned by // InsecureAlgorithms. The algorithms listed here are in preference order. func SupportedAlgorithms() Algorithms { + if fips.Enabled { + return Algorithms{ + Ciphers: fipsCiphers, + MACs: fipsMACs, + KeyExchanges: fipsKexAlgos, + HostKeys: fipsHostKeyAlgos, + PublicKeyAuths: fipsPubKeyAuthAlgos, + } + } return Algorithms{ Ciphers: supportedCiphers, MACs: supportedMACs, @@ -202,6 +254,9 @@ func SupportedAlgorithms() Algorithms { // InsecureAlgorithms returns algorithms currently implemented by this package // and which have security issues. func InsecureAlgorithms() Algorithms { + if fips.Enabled { + return Algorithms{} + } return Algorithms{ KeyExchanges: insecureKexAlgos, Ciphers: insecureCiphers, @@ -211,6 +266,19 @@ func InsecureAlgorithms() Algorithms { } } +func allAlgorithms() Algorithms { + supported := SupportedAlgorithms() + insecure := InsecureAlgorithms() + + return Algorithms{ + KeyExchanges: append(supported.KeyExchanges, insecure.KeyExchanges...), + Ciphers: append(supported.Ciphers, insecure.Ciphers...), + MACs: append(supported.MACs, insecure.MACs...), + HostKeys: append(supported.HostKeys, insecure.HostKeys...), + PublicKeyAuths: append(supported.PublicKeyAuths, insecure.PublicKeyAuths...), + } +} + var supportedCompressions = []string{compressionNone} // hashFuncs keeps the mapping of supported signature algorithms to their @@ -397,28 +465,40 @@ type Config struct { // exported for testing: Configs passed to SSH functions are copied and have // default values set automatically. func (c *Config) SetDefaults() { + algos := allAlgorithms() if c.Rand == nil { c.Rand = rand.Reader } - if c.Ciphers == nil { - c.Ciphers = preferredCiphers - } - var ciphers []string - for _, c := range c.Ciphers { - if cipherModes[c] != nil { - // Ignore the cipher if we have no cipherModes definition. - ciphers = append(ciphers, c) + if len(c.Ciphers) == 0 { + if fips.Enabled { + c.Ciphers = fipsCiphers + } else { + c.Ciphers = preferredCiphers + } + } else { + var ciphers []string + for _, c := range c.Ciphers { + // Ignore unsupported ciphers. + if contains(algos.Ciphers, c) { + ciphers = append(ciphers, c) + } } + c.Ciphers = ciphers } - c.Ciphers = ciphers - if c.KeyExchanges == nil { - c.KeyExchanges = preferredKexAlgos + if len(c.KeyExchanges) == 0 { + if fips.Enabled { + c.KeyExchanges = fipsKexAlgos + } else { + c.KeyExchanges = preferredKexAlgos + } } var kexs []string + hasKexCurve25519SHA256 := contains(algos.KeyExchanges, KeyExchangeCurve25519SHA256) for _, k := range c.KeyExchanges { - if kexAlgoMap[k] != nil { - // Ignore the KEX if we have no kexAlgoMap definition. + // Ignore unsupported KEXs, we accept keyExchangeCurve25519SHA256LibSSH + // if KeyExchangeCurve25519SHA256 is supported. + if contains(algos.KeyExchanges, k) || (k == keyExchangeCurve25519SHA256LibSSH && hasKexCurve25519SHA256) { kexs = append(kexs, k) if k == KeyExchangeCurve25519SHA256 && !contains(c.KeyExchanges, keyExchangeCurve25519SHA256LibSSH) { kexs = append(kexs, keyExchangeCurve25519SHA256LibSSH) @@ -427,17 +507,22 @@ func (c *Config) SetDefaults() { } c.KeyExchanges = kexs - if c.MACs == nil { - c.MACs = preferredMACs - } - var macs []string - for _, m := range c.MACs { - if macModes[m] != nil { - // Ignore the MAC if we have no macModes definition. - macs = append(macs, m) + if len(c.MACs) == 0 { + if fips.Enabled { + c.MACs = fipsMACs + } else { + c.MACs = preferredMACs + } + } else { + var macs []string + for _, m := range c.MACs { + // Ignore unsupported MACs. + if contains(algos.MACs, m) { + macs = append(macs, m) + } } + c.MACs = macs } - c.MACs = macs if c.RekeyThreshold == 0 { // cipher specific default diff --git a/ssh/fipsonly/fipsonly.go b/ssh/fipsonly/fipsonly.go new file mode 100644 index 0000000000..5e2b2afc93 --- /dev/null +++ b/ssh/fipsonly/fipsonly.go @@ -0,0 +1,20 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//go:build boringcrypto + +// Package fipsonly restricts all SSH configuration to FIPS-approved settings. +// +// The effect is triggered by importing the package anywhere in a program, as in: +// +// import _ "golang.org/x/crypto/ssh/fipsonly" +// +// This package only exists when using Go compiled with GOEXPERIMENT=boringcrypto. +package fipsonly + +import "golang.org/x/crypto/ssh/internal/fips" + +func init() { + fips.Enabled = true +} diff --git a/ssh/internal/fips/fips.go b/ssh/internal/fips/fips.go new file mode 100644 index 0000000000..2af8a47b0b --- /dev/null +++ b/ssh/internal/fips/fips.go @@ -0,0 +1,10 @@ +// Copyright 2023 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// Package fips allows to restrict all SSH configuration to FIPS-approved +// settings. +package fips + +// Enabled defines the FIPS status. +var Enabled = false diff --git a/ssh/server.go b/ssh/server.go index bde460545f..2ed60112e0 100644 --- a/ssh/server.go +++ b/ssh/server.go @@ -11,6 +11,8 @@ import ( "io" "net" "strings" + + "golang.org/x/crypto/ssh/internal/fips" ) // The Permissions type holds fine-grained permissions that are @@ -134,10 +136,45 @@ type ServerConfig struct { GSSAPIWithMICConfig *GSSAPIWithMICConfig } -// AddHostKey adds a private key as a host key. If an existing host -// key exists with the same public key format, it is replaced. Each server -// config must have at least one host key. +// AddHostKey adds a private key as a host key. If an existing host key exists +// with the same public key format, it is replaced. If the key contains +// unsupported algorithms it is silently ignored. Each server config must have +// at least one host key. func (s *ServerConfig) AddHostKey(key Signer) { + if fips.Enabled { + if rsaKey, ok := key.PublicKey().(*rsaPublicKey); ok { + if !isFIPSSupportedRSASize(rsaKey.N.BitLen()) { + return + } + } + if ecdsaKey, ok := key.PublicKey().(*ecdsaPublicKey); ok { + if !isFIPSSupportedECDSASize(ecdsaKey.Params().BitSize) { + return + } + } + + keyFormat := key.PublicKey().Type() + supportedAlgos := allAlgorithms().HostKeys + + switch s := key.(type) { + case MultiAlgorithmSigner: + for _, algo := range algorithmsForKeyFormat(keyFormat) { + if contains(s.Algorithms(), underlyingAlgo(algo)) && !contains(supportedAlgos, algo) { + return + } + } + case AlgorithmSigner: + for _, algo := range algorithmsForKeyFormat(keyFormat) { + if !contains(supportedAlgos, algo) { + return + } + } + default: + if !contains(supportedAlgos, keyFormat) { + return + } + } + } for i, k := range s.hostKeys { if k.PublicKey().Type() == key.PublicKey().Type() { s.hostKeys[i] = key @@ -209,14 +246,21 @@ func NewServerConn(c net.Conn, config *ServerConfig) (*ServerConn, <-chan NewCha fullConf.MaxAuthTries = 6 } if len(fullConf.PublicKeyAuthAlgorithms) == 0 { - fullConf.PublicKeyAuthAlgorithms = preferredPubKeyAuthAlgos + if fips.Enabled { + fullConf.PublicKeyAuthAlgorithms = fipsPubKeyAuthAlgos + } else { + fullConf.PublicKeyAuthAlgorithms = preferredPubKeyAuthAlgos + } } else { + var pubKeyAlgos []string + supported := allAlgorithms().PublicKeyAuths for _, algo := range fullConf.PublicKeyAuthAlgorithms { - if !contains(SupportedAlgorithms().PublicKeyAuths, algo) && !contains(InsecureAlgorithms().PublicKeyAuths, algo) { - c.Close() - return nil, nil, nil, fmt.Errorf("ssh: unsupported public key authentication algorithm %s", algo) + // Ignore unsupported public key authentication algorithms. + if contains(supported, algo) { + pubKeyAlgos = append(pubKeyAlgos, algo) } } + fullConf.PublicKeyAuthAlgorithms = pubKeyAlgos } s := &connection{ @@ -611,6 +655,19 @@ userAuthLoop: if err != nil { return nil, err } + if fips.Enabled { + if rsaKey, ok := pubKey.(*rsaPublicKey); ok { + if !isFIPSSupportedRSASize(rsaKey.N.BitLen()) { + return nil, fmt.Errorf("unsupported RSA public key, size: %d not allowed", rsaKey.N.BitLen()) + } + } + if ecdsaKey, ok := pubKey.(*ecdsaPublicKey); ok { + if !isFIPSSupportedECDSASize(ecdsaKey.Params().BitSize) { + return nil, fmt.Errorf("unsupported ECDSA public key, size: %d not allowed", ecdsaKey.Params().BitSize) + } + } + + } candidate, ok := cache.get(s.user, pubKeyData) if !ok {