Previously, the server selected the Diffie-Hellman group based solely on the MaxBits value provided by the client. This resulted in suboptimal modulus selection, often ignoring the client's PreferredBits or selecting a larger-than-necessary group. This change implements a "best fit" selection algorithm similar to OpenSSH's choose_dh logic. It attempts to find the smallest available group larger than or equal to the client's PreferredBits, falling back to the largest available group within the accepted range if no group above the preference is available. Additionally, this commit caches the parsed Oakley groups using sync.OnceValue, avoiding repeated big.Int parsing on every handshake while keeping the cost out of package initialization. This issue was found during a security audit by NCC Group Cryptography Services, sponsored by Teleport, and was assessed and is being fixed as a non-security bug. Change-Id: Idfa81bbcf354a7fb7b541cb4bbeb6e4a0181398a Reviewed-on: https://go-review.googlesource.com/c/crypto/+/782424 Auto-Submit: Nicola Murino <nicola.murino@gmail.com> Reviewed-by: Filippo Valsorda <filippo@golang.org> Reviewed-by: Mark Freeman <markfreeman@google.com> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: David Chase <drchase@google.com>
865 lines
24 KiB
Go
865 lines
24 KiB
Go
// Copyright 2013 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 ssh
|
|
|
|
import (
|
|
"crypto"
|
|
"crypto/ecdsa"
|
|
"crypto/elliptic"
|
|
"crypto/fips140"
|
|
"crypto/rand"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"math/big"
|
|
"slices"
|
|
"sync"
|
|
|
|
"golang.org/x/crypto/curve25519"
|
|
)
|
|
|
|
const (
|
|
// This is the group called diffie-hellman-group1-sha1 in RFC 4253 and
|
|
// Oakley Group 2 in RFC 2409.
|
|
oakleyGroup2 = "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE65381FFFFFFFFFFFFFFFF"
|
|
// This is the group called diffie-hellman-group14-sha1 in RFC 4253 and
|
|
// Oakley Group 14 in RFC 3526.
|
|
oakleyGroup14 = "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF"
|
|
// This is the group called diffie-hellman-group15-sha512 in RFC 8268 and
|
|
// Oakley Group 15 in RFC 3526.
|
|
oakleyGroup15 = "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A93AD2CAFFFFFFFFFFFFFFFF"
|
|
// This is the group called diffie-hellman-group16-sha512 in RFC 8268 and
|
|
// Oakley Group 16 in RFC 3526.
|
|
oakleyGroup16 = "FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AAAC42DAD33170D04507A33A85521ABDF1CBA64ECFB850458DBEF0A8AEA71575D060C7DB3970F85A6E1E4C7ABF5AE8CDB0933D71E8C94E04A25619DCEE3D2261AD2EE6BF12FFA06D98A0864D87602733EC86A64521F2B18177B200CBBE117577A615D6C770988C0BAD946E208E24FA074E5AB3143DB5BFCE0FD108E4B82D120A92108011A723C12A787E6D788719A10BDBA5B2699C327186AF4E23C1A946834B6150BDA2583E9CA2AD44CE8DBBBC2DB04DE8EF92E8EFC141FBECAA6287C59474E6BC05D99B2964FA090C3A2233BA186515BE7ED1F612970CEE2D7AFB81BDD762170481CD0069127D5B05AA993B4EA988D8FDDC186FFB7DC90A6C08F4DF435C934063199FFFFFFFFFFFFFFFF"
|
|
)
|
|
|
|
// kexResult captures the outcome of a key exchange.
|
|
type kexResult struct {
|
|
// Session hash. See also RFC 4253, section 8.
|
|
H []byte
|
|
|
|
// Shared secret. See also RFC 4253, section 8.
|
|
K []byte
|
|
|
|
// Host key as hashed into H.
|
|
HostKey []byte
|
|
|
|
// Signature of H.
|
|
Signature []byte
|
|
|
|
// A cryptographic hash function that matches the security
|
|
// level of the key exchange algorithm. It is used for
|
|
// calculating H, and for deriving keys from H and K.
|
|
Hash crypto.Hash
|
|
|
|
// The session ID, which is the first H computed. This is used
|
|
// to derive key material inside the transport.
|
|
SessionID []byte
|
|
}
|
|
|
|
// handshakeMagics contains data that is always included in the
|
|
// session hash.
|
|
type handshakeMagics struct {
|
|
clientVersion, serverVersion []byte
|
|
clientKexInit, serverKexInit []byte
|
|
}
|
|
|
|
func (m *handshakeMagics) write(w io.Writer) {
|
|
writeString(w, m.clientVersion)
|
|
writeString(w, m.serverVersion)
|
|
writeString(w, m.clientKexInit)
|
|
writeString(w, m.serverKexInit)
|
|
}
|
|
|
|
// kexAlgorithm abstracts different key exchange algorithms.
|
|
type kexAlgorithm interface {
|
|
// Server runs server-side key agreement, signing the result
|
|
// with a hostkey. algo is the negotiated algorithm, and may
|
|
// be a certificate type.
|
|
Server(p packetConn, rand io.Reader, magics *handshakeMagics, s AlgorithmSigner, algo string) (*kexResult, error)
|
|
|
|
// Client runs the client-side key agreement. Caller is
|
|
// responsible for verifying the host key signature.
|
|
Client(p packetConn, rand io.Reader, magics *handshakeMagics) (*kexResult, error)
|
|
}
|
|
|
|
// dhGroup is a multiplicative group suitable for implementing Diffie-Hellman key agreement.
|
|
type dhGroup struct {
|
|
g, p, pMinus1 *big.Int
|
|
hashFunc crypto.Hash
|
|
}
|
|
|
|
func (group *dhGroup) diffieHellman(theirPublic, myPrivate *big.Int) (*big.Int, error) {
|
|
if theirPublic.Cmp(bigOne) <= 0 || theirPublic.Cmp(group.pMinus1) >= 0 {
|
|
return nil, errors.New("ssh: DH parameter out of bounds")
|
|
}
|
|
return new(big.Int).Exp(theirPublic, myPrivate, group.p), nil
|
|
}
|
|
|
|
func (group *dhGroup) Client(c packetConn, randSource io.Reader, magics *handshakeMagics) (*kexResult, error) {
|
|
var x *big.Int
|
|
for {
|
|
var err error
|
|
if x, err = rand.Int(randSource, group.pMinus1); err != nil {
|
|
return nil, err
|
|
}
|
|
if x.Sign() > 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
X := new(big.Int).Exp(group.g, x, group.p)
|
|
kexDHInit := kexDHInitMsg{
|
|
X: X,
|
|
}
|
|
if err := c.writePacket(Marshal(&kexDHInit)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
packet, err := c.readPacket()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var kexDHReply kexDHReplyMsg
|
|
if err = Unmarshal(packet, &kexDHReply); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
ki, err := group.diffieHellman(kexDHReply.Y, x)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
h := group.hashFunc.New()
|
|
magics.write(h)
|
|
writeString(h, kexDHReply.HostKey)
|
|
writeInt(h, X)
|
|
writeInt(h, kexDHReply.Y)
|
|
K := make([]byte, intLength(ki))
|
|
marshalInt(K, ki)
|
|
h.Write(K)
|
|
|
|
return &kexResult{
|
|
H: h.Sum(nil),
|
|
K: K,
|
|
HostKey: kexDHReply.HostKey,
|
|
Signature: kexDHReply.Signature,
|
|
Hash: group.hashFunc,
|
|
}, nil
|
|
}
|
|
|
|
func (group *dhGroup) Server(c packetConn, randSource io.Reader, magics *handshakeMagics, priv AlgorithmSigner, algo string) (result *kexResult, err error) {
|
|
packet, err := c.readPacket()
|
|
if err != nil {
|
|
return
|
|
}
|
|
var kexDHInit kexDHInitMsg
|
|
if err = Unmarshal(packet, &kexDHInit); err != nil {
|
|
return
|
|
}
|
|
|
|
var y *big.Int
|
|
for {
|
|
if y, err = rand.Int(randSource, group.pMinus1); err != nil {
|
|
return
|
|
}
|
|
if y.Sign() > 0 {
|
|
break
|
|
}
|
|
}
|
|
|
|
Y := new(big.Int).Exp(group.g, y, group.p)
|
|
ki, err := group.diffieHellman(kexDHInit.X, y)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hostKeyBytes := priv.PublicKey().Marshal()
|
|
|
|
h := group.hashFunc.New()
|
|
magics.write(h)
|
|
writeString(h, hostKeyBytes)
|
|
writeInt(h, kexDHInit.X)
|
|
writeInt(h, Y)
|
|
|
|
K := make([]byte, intLength(ki))
|
|
marshalInt(K, ki)
|
|
h.Write(K)
|
|
|
|
H := h.Sum(nil)
|
|
|
|
// H is already a hash, but the hostkey signing will apply its
|
|
// own key-specific hash algorithm.
|
|
sig, err := signAndMarshal(priv, randSource, H, algo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
kexDHReply := kexDHReplyMsg{
|
|
HostKey: hostKeyBytes,
|
|
Y: Y,
|
|
Signature: sig,
|
|
}
|
|
packet = Marshal(&kexDHReply)
|
|
|
|
err = c.writePacket(packet)
|
|
return &kexResult{
|
|
H: H,
|
|
K: K,
|
|
HostKey: hostKeyBytes,
|
|
Signature: sig,
|
|
Hash: group.hashFunc,
|
|
}, err
|
|
}
|
|
|
|
// ecdh performs Elliptic Curve Diffie-Hellman key exchange as
|
|
// described in RFC 5656, section 4.
|
|
type ecdh struct {
|
|
curve elliptic.Curve
|
|
}
|
|
|
|
func (kex *ecdh) Client(c packetConn, rand io.Reader, magics *handshakeMagics) (*kexResult, error) {
|
|
ephKey, err := ecdsa.GenerateKey(kex.curve, rand)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
kexInit := kexECDHInitMsg{
|
|
ClientPubKey: elliptic.Marshal(kex.curve, ephKey.PublicKey.X, ephKey.PublicKey.Y),
|
|
}
|
|
|
|
serialized := Marshal(&kexInit)
|
|
if err := c.writePacket(serialized); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
packet, err := c.readPacket()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var reply kexECDHReplyMsg
|
|
if err = Unmarshal(packet, &reply); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
x, y, err := unmarshalECKey(kex.curve, reply.EphemeralPubKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// generate shared secret
|
|
secret, _ := kex.curve.ScalarMult(x, y, ephKey.D.Bytes())
|
|
|
|
h := ecHash(kex.curve).New()
|
|
magics.write(h)
|
|
writeString(h, reply.HostKey)
|
|
writeString(h, kexInit.ClientPubKey)
|
|
writeString(h, reply.EphemeralPubKey)
|
|
K := make([]byte, intLength(secret))
|
|
marshalInt(K, secret)
|
|
h.Write(K)
|
|
|
|
return &kexResult{
|
|
H: h.Sum(nil),
|
|
K: K,
|
|
HostKey: reply.HostKey,
|
|
Signature: reply.Signature,
|
|
Hash: ecHash(kex.curve),
|
|
}, nil
|
|
}
|
|
|
|
// unmarshalECKey parses and checks an EC key.
|
|
func unmarshalECKey(curve elliptic.Curve, pubkey []byte) (x, y *big.Int, err error) {
|
|
x, y = elliptic.Unmarshal(curve, pubkey)
|
|
if x == nil {
|
|
return nil, nil, errors.New("ssh: elliptic.Unmarshal failure")
|
|
}
|
|
if !validateECPublicKey(curve, x, y) {
|
|
return nil, nil, errors.New("ssh: public key not on curve")
|
|
}
|
|
return x, y, nil
|
|
}
|
|
|
|
// validateECPublicKey checks that the point is a valid public key for
|
|
// the given curve. See [SEC1], 3.2.2
|
|
func validateECPublicKey(curve elliptic.Curve, x, y *big.Int) bool {
|
|
if x.Sign() == 0 && y.Sign() == 0 {
|
|
return false
|
|
}
|
|
|
|
if x.Cmp(curve.Params().P) >= 0 {
|
|
return false
|
|
}
|
|
|
|
if y.Cmp(curve.Params().P) >= 0 {
|
|
return false
|
|
}
|
|
|
|
if !curve.IsOnCurve(x, y) {
|
|
return false
|
|
}
|
|
|
|
// We don't check if N * PubKey == 0, since
|
|
//
|
|
// - the NIST curves have cofactor = 1, so this is implicit.
|
|
// (We don't foresee an implementation that supports non NIST
|
|
// curves)
|
|
//
|
|
// - for ephemeral keys, we don't need to worry about small
|
|
// subgroup attacks.
|
|
return true
|
|
}
|
|
|
|
func (kex *ecdh) Server(c packetConn, rand io.Reader, magics *handshakeMagics, priv AlgorithmSigner, algo string) (result *kexResult, err error) {
|
|
packet, err := c.readPacket()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var kexECDHInit kexECDHInitMsg
|
|
if err = Unmarshal(packet, &kexECDHInit); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
clientX, clientY, err := unmarshalECKey(kex.curve, kexECDHInit.ClientPubKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// We could cache this key across multiple users/multiple
|
|
// connection attempts, but the benefit is small. OpenSSH
|
|
// generates a new key for each incoming connection.
|
|
ephKey, err := ecdsa.GenerateKey(kex.curve, rand)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
hostKeyBytes := priv.PublicKey().Marshal()
|
|
|
|
serializedEphKey := elliptic.Marshal(kex.curve, ephKey.PublicKey.X, ephKey.PublicKey.Y)
|
|
|
|
// generate shared secret
|
|
secret, _ := kex.curve.ScalarMult(clientX, clientY, ephKey.D.Bytes())
|
|
|
|
h := ecHash(kex.curve).New()
|
|
magics.write(h)
|
|
writeString(h, hostKeyBytes)
|
|
writeString(h, kexECDHInit.ClientPubKey)
|
|
writeString(h, serializedEphKey)
|
|
|
|
K := make([]byte, intLength(secret))
|
|
marshalInt(K, secret)
|
|
h.Write(K)
|
|
|
|
H := h.Sum(nil)
|
|
|
|
// H is already a hash, but the hostkey signing will apply its
|
|
// own key-specific hash algorithm.
|
|
sig, err := signAndMarshal(priv, rand, H, algo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
reply := kexECDHReplyMsg{
|
|
EphemeralPubKey: serializedEphKey,
|
|
HostKey: hostKeyBytes,
|
|
Signature: sig,
|
|
}
|
|
|
|
serialized := Marshal(&reply)
|
|
if err := c.writePacket(serialized); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &kexResult{
|
|
H: H,
|
|
K: K,
|
|
HostKey: reply.HostKey,
|
|
Signature: sig,
|
|
Hash: ecHash(kex.curve),
|
|
}, nil
|
|
}
|
|
|
|
// ecHash returns the hash to match the given elliptic curve, see RFC
|
|
// 5656, section 6.2.1
|
|
func ecHash(curve elliptic.Curve) crypto.Hash {
|
|
bitSize := curve.Params().BitSize
|
|
switch {
|
|
case bitSize <= 256:
|
|
return crypto.SHA256
|
|
case bitSize <= 384:
|
|
return crypto.SHA384
|
|
}
|
|
return crypto.SHA512
|
|
}
|
|
|
|
// kexAlgoMap defines the supported KEXs. KEXs not included are not supported
|
|
// and will not be negotiated, even if explicitly configured. When FIPS mode is
|
|
// enabled, only FIPS-approved algorithms are included.
|
|
var kexAlgoMap = map[string]kexAlgorithm{}
|
|
|
|
func init() {
|
|
// mlkem768x25519-sha256 we'll work with fips140=on but not fips140=only
|
|
// until Go 1.26.
|
|
kexAlgoMap[KeyExchangeMLKEM768X25519] = &mlkem768WithCurve25519sha256{}
|
|
kexAlgoMap[KeyExchangeECDHP521] = &ecdh{elliptic.P521()}
|
|
kexAlgoMap[KeyExchangeECDHP384] = &ecdh{elliptic.P384()}
|
|
kexAlgoMap[KeyExchangeECDHP256] = &ecdh{elliptic.P256()}
|
|
|
|
if fips140.Enabled() {
|
|
defaultKexAlgos = slices.DeleteFunc(defaultKexAlgos, func(algo string) bool {
|
|
_, ok := kexAlgoMap[algo]
|
|
return !ok
|
|
})
|
|
return
|
|
}
|
|
|
|
p, _ := new(big.Int).SetString(oakleyGroup2, 16)
|
|
kexAlgoMap[InsecureKeyExchangeDH1SHA1] = &dhGroup{
|
|
g: new(big.Int).SetInt64(2),
|
|
p: p,
|
|
pMinus1: new(big.Int).Sub(p, bigOne),
|
|
hashFunc: crypto.SHA1,
|
|
}
|
|
|
|
p, _ = new(big.Int).SetString(oakleyGroup14, 16)
|
|
group14 := &dhGroup{
|
|
g: new(big.Int).SetInt64(2),
|
|
p: p,
|
|
pMinus1: new(big.Int).Sub(p, bigOne),
|
|
}
|
|
|
|
kexAlgoMap[InsecureKeyExchangeDH14SHA1] = &dhGroup{
|
|
g: group14.g, p: group14.p, pMinus1: group14.pMinus1,
|
|
hashFunc: crypto.SHA1,
|
|
}
|
|
kexAlgoMap[KeyExchangeDH14SHA256] = &dhGroup{
|
|
g: group14.g, p: group14.p, pMinus1: group14.pMinus1,
|
|
hashFunc: crypto.SHA256,
|
|
}
|
|
|
|
p, _ = new(big.Int).SetString(oakleyGroup16, 16)
|
|
|
|
kexAlgoMap[KeyExchangeDH16SHA512] = &dhGroup{
|
|
g: new(big.Int).SetInt64(2),
|
|
p: p,
|
|
pMinus1: new(big.Int).Sub(p, bigOne),
|
|
hashFunc: crypto.SHA512,
|
|
}
|
|
|
|
kexAlgoMap[KeyExchangeCurve25519] = &curve25519sha256{}
|
|
kexAlgoMap[keyExchangeCurve25519LibSSH] = &curve25519sha256{}
|
|
kexAlgoMap[InsecureKeyExchangeDHGEXSHA1] = &dhGEXSHA{hashFunc: crypto.SHA1}
|
|
kexAlgoMap[KeyExchangeDHGEXSHA256] = &dhGEXSHA{hashFunc: crypto.SHA256}
|
|
}
|
|
|
|
// curve25519sha256 implements the curve25519-sha256 (formerly known as
|
|
// curve25519-sha256@libssh.org) key exchange method, as described in RFC 8731.
|
|
type curve25519sha256 struct{}
|
|
|
|
type curve25519KeyPair struct {
|
|
priv [32]byte
|
|
pub [32]byte
|
|
}
|
|
|
|
func (kp *curve25519KeyPair) generate(rand io.Reader) error {
|
|
if _, err := io.ReadFull(rand, kp.priv[:]); err != nil {
|
|
return err
|
|
}
|
|
p, err := curve25519.X25519(kp.priv[:], curve25519.Basepoint)
|
|
if err != nil {
|
|
return fmt.Errorf("curve25519: %w", err)
|
|
}
|
|
if len(p) != 32 {
|
|
return fmt.Errorf("curve25519: internal error: X25519 returned %d bytes, expected 32", len(p))
|
|
}
|
|
copy(kp.pub[:], p)
|
|
return nil
|
|
}
|
|
|
|
func (kex *curve25519sha256) Client(c packetConn, rand io.Reader, magics *handshakeMagics) (*kexResult, error) {
|
|
var kp curve25519KeyPair
|
|
if err := kp.generate(rand); err != nil {
|
|
return nil, err
|
|
}
|
|
if err := c.writePacket(Marshal(&kexECDHInitMsg{kp.pub[:]})); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
packet, err := c.readPacket()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var reply kexECDHReplyMsg
|
|
if err = Unmarshal(packet, &reply); err != nil {
|
|
return nil, err
|
|
}
|
|
if len(reply.EphemeralPubKey) != 32 {
|
|
return nil, errors.New("ssh: peer's curve25519 public value has wrong length")
|
|
}
|
|
|
|
secret, err := curve25519.X25519(kp.priv[:], reply.EphemeralPubKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ssh: peer's curve25519 public value is not valid: %w", err)
|
|
}
|
|
|
|
h := crypto.SHA256.New()
|
|
magics.write(h)
|
|
writeString(h, reply.HostKey)
|
|
writeString(h, kp.pub[:])
|
|
writeString(h, reply.EphemeralPubKey)
|
|
|
|
ki := new(big.Int).SetBytes(secret[:])
|
|
K := make([]byte, intLength(ki))
|
|
marshalInt(K, ki)
|
|
h.Write(K)
|
|
|
|
return &kexResult{
|
|
H: h.Sum(nil),
|
|
K: K,
|
|
HostKey: reply.HostKey,
|
|
Signature: reply.Signature,
|
|
Hash: crypto.SHA256,
|
|
}, nil
|
|
}
|
|
|
|
func (kex *curve25519sha256) Server(c packetConn, rand io.Reader, magics *handshakeMagics, priv AlgorithmSigner, algo string) (result *kexResult, err error) {
|
|
packet, err := c.readPacket()
|
|
if err != nil {
|
|
return
|
|
}
|
|
var kexInit kexECDHInitMsg
|
|
if err = Unmarshal(packet, &kexInit); err != nil {
|
|
return
|
|
}
|
|
|
|
if len(kexInit.ClientPubKey) != 32 {
|
|
return nil, errors.New("ssh: peer's curve25519 public value has wrong length")
|
|
}
|
|
|
|
var kp curve25519KeyPair
|
|
if err := kp.generate(rand); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
secret, err := curve25519.X25519(kp.priv[:], kexInit.ClientPubKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ssh: peer's curve25519 public value is not valid: %w", err)
|
|
}
|
|
|
|
hostKeyBytes := priv.PublicKey().Marshal()
|
|
|
|
h := crypto.SHA256.New()
|
|
magics.write(h)
|
|
writeString(h, hostKeyBytes)
|
|
writeString(h, kexInit.ClientPubKey)
|
|
writeString(h, kp.pub[:])
|
|
|
|
ki := new(big.Int).SetBytes(secret[:])
|
|
K := make([]byte, intLength(ki))
|
|
marshalInt(K, ki)
|
|
h.Write(K)
|
|
|
|
H := h.Sum(nil)
|
|
|
|
sig, err := signAndMarshal(priv, rand, H, algo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
reply := kexECDHReplyMsg{
|
|
EphemeralPubKey: kp.pub[:],
|
|
HostKey: hostKeyBytes,
|
|
Signature: sig,
|
|
}
|
|
if err := c.writePacket(Marshal(&reply)); err != nil {
|
|
return nil, err
|
|
}
|
|
return &kexResult{
|
|
H: H,
|
|
K: K,
|
|
HostKey: hostKeyBytes,
|
|
Signature: sig,
|
|
Hash: crypto.SHA256,
|
|
}, nil
|
|
}
|
|
|
|
// dhGEXSHA implements the diffie-hellman-group-exchange-sha1 and
|
|
// diffie-hellman-group-exchange-sha256 key agreement protocols,
|
|
// as described in RFC 4419
|
|
type dhGEXSHA struct {
|
|
hashFunc crypto.Hash
|
|
}
|
|
|
|
const (
|
|
dhGroupExchangeMinimumBits = 2048
|
|
dhGroupExchangePreferredBits = 2048
|
|
dhGroupExchangeMaximumBits = 8192
|
|
)
|
|
|
|
func (gex *dhGEXSHA) Client(c packetConn, randSource io.Reader, magics *handshakeMagics) (*kexResult, error) {
|
|
// Send GexRequest
|
|
kexDHGexRequest := kexDHGexRequestMsg{
|
|
MinBits: dhGroupExchangeMinimumBits,
|
|
PreferredBits: dhGroupExchangePreferredBits,
|
|
MaxBits: dhGroupExchangeMaximumBits,
|
|
}
|
|
if err := c.writePacket(Marshal(&kexDHGexRequest)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Receive GexGroup
|
|
packet, err := c.readPacket()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var msg kexDHGexGroupMsg
|
|
if err = Unmarshal(packet, &msg); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// reject if p's bit length < dhGroupExchangeMinimumBits or > dhGroupExchangeMaximumBits
|
|
if msg.P.BitLen() < dhGroupExchangeMinimumBits || msg.P.BitLen() > dhGroupExchangeMaximumBits {
|
|
return nil, fmt.Errorf("ssh: server-generated gex p is out of range (%d bits)", msg.P.BitLen())
|
|
}
|
|
|
|
// Check if g is safe by verifying that 1 < g < p-1
|
|
pMinusOne := new(big.Int).Sub(msg.P, bigOne)
|
|
if msg.G.Cmp(bigOne) <= 0 || msg.G.Cmp(pMinusOne) >= 0 {
|
|
return nil, fmt.Errorf("ssh: server provided gex g is not safe")
|
|
}
|
|
|
|
// Send GexInit
|
|
pHalf := new(big.Int).Rsh(msg.P, 1)
|
|
x, err := rand.Int(randSource, pHalf)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
X := new(big.Int).Exp(msg.G, x, msg.P)
|
|
kexDHGexInit := kexDHGexInitMsg{
|
|
X: X,
|
|
}
|
|
if err := c.writePacket(Marshal(&kexDHGexInit)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Receive GexReply
|
|
packet, err = c.readPacket()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var kexDHGexReply kexDHGexReplyMsg
|
|
if err = Unmarshal(packet, &kexDHGexReply); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
if kexDHGexReply.Y.Cmp(bigOne) <= 0 || kexDHGexReply.Y.Cmp(pMinusOne) >= 0 {
|
|
return nil, errors.New("ssh: DH parameter out of bounds")
|
|
}
|
|
kInt := new(big.Int).Exp(kexDHGexReply.Y, x, msg.P)
|
|
|
|
// Check if k is safe by verifying that k > 1 and k < p - 1
|
|
if kInt.Cmp(bigOne) <= 0 || kInt.Cmp(pMinusOne) >= 0 {
|
|
return nil, fmt.Errorf("ssh: derived k is not safe")
|
|
}
|
|
|
|
h := gex.hashFunc.New()
|
|
magics.write(h)
|
|
writeString(h, kexDHGexReply.HostKey)
|
|
binary.Write(h, binary.BigEndian, uint32(dhGroupExchangeMinimumBits))
|
|
binary.Write(h, binary.BigEndian, uint32(dhGroupExchangePreferredBits))
|
|
binary.Write(h, binary.BigEndian, uint32(dhGroupExchangeMaximumBits))
|
|
writeInt(h, msg.P)
|
|
writeInt(h, msg.G)
|
|
writeInt(h, X)
|
|
writeInt(h, kexDHGexReply.Y)
|
|
K := make([]byte, intLength(kInt))
|
|
marshalInt(K, kInt)
|
|
h.Write(K)
|
|
|
|
return &kexResult{
|
|
H: h.Sum(nil),
|
|
K: K,
|
|
HostKey: kexDHGexReply.HostKey,
|
|
Signature: kexDHGexReply.Signature,
|
|
Hash: gex.hashFunc,
|
|
}, nil
|
|
}
|
|
|
|
// Server half implementation of the Diffie Hellman Key Exchange with SHA1 and SHA256.
|
|
func (gex *dhGEXSHA) Server(c packetConn, randSource io.Reader, magics *handshakeMagics, priv AlgorithmSigner, algo string) (result *kexResult, err error) {
|
|
// Receive GexRequest
|
|
packet, err := c.readPacket()
|
|
if err != nil {
|
|
return
|
|
}
|
|
var kexDHGexRequest kexDHGexRequestMsg
|
|
if err = Unmarshal(packet, &kexDHGexRequest); err != nil {
|
|
return
|
|
}
|
|
// We check that the request received is valid and that the MaxBits
|
|
// requested are at least equal to our supported minimum. This is the same
|
|
// check done in OpenSSH:
|
|
// https://github.com/openssh/openssh-portable/blob/80a2f64b/kexgexs.c#L94
|
|
//
|
|
// Furthermore, we also check that the required MinBits are less than or
|
|
// equal to 4096 because we can use up to Oakley Group 16.
|
|
if kexDHGexRequest.MaxBits < kexDHGexRequest.MinBits || kexDHGexRequest.PreferredBits < kexDHGexRequest.MinBits ||
|
|
kexDHGexRequest.MaxBits < kexDHGexRequest.PreferredBits || kexDHGexRequest.MaxBits < dhGroupExchangeMinimumBits ||
|
|
kexDHGexRequest.MinBits > 4096 {
|
|
return nil, fmt.Errorf("ssh: DH GEX request out of range, min: %d, max: %d, preferred: %d", kexDHGexRequest.MinBits,
|
|
kexDHGexRequest.MaxBits, kexDHGexRequest.PreferredBits)
|
|
}
|
|
|
|
p, err := chooseDH(kexDHGexRequest)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
g := big.NewInt(2)
|
|
msg := &kexDHGexGroupMsg{
|
|
P: p,
|
|
G: g,
|
|
}
|
|
if err := c.writePacket(Marshal(msg)); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Receive GexInit
|
|
packet, err = c.readPacket()
|
|
if err != nil {
|
|
return
|
|
}
|
|
var kexDHGexInit kexDHGexInitMsg
|
|
if err = Unmarshal(packet, &kexDHGexInit); err != nil {
|
|
return
|
|
}
|
|
|
|
pHalf := new(big.Int).Rsh(p, 1)
|
|
|
|
y, err := rand.Int(randSource, pHalf)
|
|
if err != nil {
|
|
return
|
|
}
|
|
Y := new(big.Int).Exp(g, y, p)
|
|
|
|
pMinusOne := new(big.Int).Sub(p, bigOne)
|
|
if kexDHGexInit.X.Cmp(bigOne) <= 0 || kexDHGexInit.X.Cmp(pMinusOne) >= 0 {
|
|
return nil, errors.New("ssh: DH parameter out of bounds")
|
|
}
|
|
kInt := new(big.Int).Exp(kexDHGexInit.X, y, p)
|
|
|
|
hostKeyBytes := priv.PublicKey().Marshal()
|
|
|
|
h := gex.hashFunc.New()
|
|
magics.write(h)
|
|
writeString(h, hostKeyBytes)
|
|
binary.Write(h, binary.BigEndian, kexDHGexRequest.MinBits)
|
|
binary.Write(h, binary.BigEndian, kexDHGexRequest.PreferredBits)
|
|
binary.Write(h, binary.BigEndian, kexDHGexRequest.MaxBits)
|
|
writeInt(h, p)
|
|
writeInt(h, g)
|
|
writeInt(h, kexDHGexInit.X)
|
|
writeInt(h, Y)
|
|
|
|
K := make([]byte, intLength(kInt))
|
|
marshalInt(K, kInt)
|
|
h.Write(K)
|
|
|
|
H := h.Sum(nil)
|
|
|
|
// H is already a hash, but the hostkey signing will apply its
|
|
// own key-specific hash algorithm.
|
|
sig, err := signAndMarshal(priv, randSource, H, algo)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
kexDHGexReply := kexDHGexReplyMsg{
|
|
HostKey: hostKeyBytes,
|
|
Y: Y,
|
|
Signature: sig,
|
|
}
|
|
packet = Marshal(&kexDHGexReply)
|
|
|
|
err = c.writePacket(packet)
|
|
|
|
return &kexResult{
|
|
H: H,
|
|
K: K,
|
|
HostKey: hostKeyBytes,
|
|
Signature: sig,
|
|
Hash: gex.hashFunc,
|
|
}, err
|
|
}
|
|
|
|
type dhKEXGroup struct {
|
|
size int
|
|
p *big.Int
|
|
}
|
|
|
|
// supportedDHKEXGroups returns the DH groups the server is willing to offer
|
|
// for diffie-hellman-group-exchange-* key exchanges. The list is built lazily
|
|
// on first use to keep the hex-to-big.Int parse out of package initialization.
|
|
var supportedDHKEXGroups = sync.OnceValue(func() []dhKEXGroup {
|
|
specs := []struct {
|
|
size int
|
|
hex string
|
|
}{
|
|
{2048, oakleyGroup14},
|
|
{3072, oakleyGroup15},
|
|
{4096, oakleyGroup16},
|
|
}
|
|
out := make([]dhKEXGroup, 0, len(specs))
|
|
for _, s := range specs {
|
|
p, _ := new(big.Int).SetString(s.hex, 16)
|
|
out = append(out, dhKEXGroup{size: s.size, p: p})
|
|
}
|
|
return out
|
|
})
|
|
|
|
// chooseDH picks a DH group for the given client request, mirroring the
|
|
// algorithm used by OpenSSH's choose_dh in dh.c: prefer the smallest known
|
|
// group larger than or equal to the client's PreferredBits, and otherwise pick
|
|
// the largest group within the accepted [MinBits, MaxBits] range.
|
|
func chooseDH(req kexDHGexRequestMsg) (*big.Int, error) {
|
|
var best *big.Int
|
|
bestSize := 0
|
|
wantBits := int(req.PreferredBits)
|
|
|
|
for _, group := range supportedDHKEXGroups() {
|
|
if uint32(group.size) < req.MinBits || uint32(group.size) > req.MaxBits {
|
|
continue
|
|
}
|
|
|
|
if bestSize == 0 {
|
|
best = group.p
|
|
bestSize = group.size
|
|
continue
|
|
}
|
|
|
|
closerFromAbove := group.size >= wantBits && group.size < bestSize
|
|
closerFromBelow := group.size > bestSize && bestSize < wantBits
|
|
|
|
if closerFromAbove || closerFromBelow {
|
|
best = group.p
|
|
bestSize = group.size
|
|
}
|
|
}
|
|
|
|
if bestSize == 0 {
|
|
return nil, fmt.Errorf("ssh: no suitable DH group found for request min: %d, preferred: %d, max: %d",
|
|
req.MinBits, req.PreferredBits, req.MaxBits)
|
|
}
|
|
|
|
return best, nil
|
|
}
|