Skip to content

Commit 4d39909

Browse files
committed
ssh: fix diffie-hellman-group-exchange g and K bounds checks
The previous code if gex.g.Cmp(one) != 1 && gex.g.Cmp(pMinusOne) != -1 { deobfuscates to the classic mistake if g <= 1 && g >= p - 1 { which is never true. What the code actually intended to do is if gex.g.Cmp(one) != 1 || gex.g.Cmp(pMinusOne) != -1 { or more readably and consistently with the diffieHellman method if gex.g.Cmp(one) <= 0 || gex.g.Cmp(pMinusOne) >= 0 { Now, is this a security issue? The incorrect checks apply to g and k, but not what we call Y and the spec calls f. RFC 4419 says: Either side MUST NOT send or accept e or f values that are not in the range [1, p-1]. If this condition is violated, the key exchange fails. To prevent confinement attacks, they MUST accept the shared secret K only if 1 < K < p - 1. Note that RFC 8268, Section 4 updates the equivalent RFC 4253 statement (although not the RFC 4419 one) about e and f, but we are already doing the correct full check there. DH Public Key values MUST be checked and both conditions: 1 < e < p-1 1 < f < p-1 MUST be true. Values not within these bounds MUST NOT be sent or accepted by either side. If either one of these conditions is violated, then the key exchange fails. This simple check ensures that: o The remote peer behaves properly. o The local system is not forced into the two-element subgroup. The check on K seems like a proxy for checking a number of ways to fix the DH output (for example by manipulating g) to one of 0, 1, or -1. This should not be meaningful to security for two reasons: - all parameters end up in the "transcript" hash that will get signed by the server's host key, and if the attacker controls the host key's signature, they have the ability to MitM without resorting to confinement attacks - the client secret is ephemeral, so leaking bits of it by forcing it into small sub-groups does not gain the attacker anything, as the secret does not get reused Indeed, this is the same explanation of why it's ok not to check that p is indeed a (safe) prime, which even OpenSSH omits. Building an equivalent attack by manipulating p instead of g is left as an exercise to the reader. For the future, this is a case study in why we should not add complexity even when it looks easy enough to do. CL 174257 added the diffie-hellman-group-exchange kex. That introduced a data race (arguably a security issue), which was fixed in CL 222078. Then it was too slow, which led to CL 252337 that removed the primalty check, which required a full analysis of whether it's safe to skip it, and checking against other implementations. Now we find there's a bug and we have to do another security analysis that not even the RFC bothered to do in order to decide if it's a security issue. My decision in golang/go#17230 (comment) does not look like the right one in hindsight. While at it, clean up the code some - drop useless bit size bounds logic in the server stub that get ignored by the rest of the function - make p and g local variables instead of method fields, since they are not persistent state (this was originally a data race which was fixed in CL 222078 by making Client not a pointer receiver) Updates golang/go#17230 Change-Id: I4b1c68537109f627ccd75ec381dcfab57ce1768c Reviewed-on: https://go-review.googlesource.com/c/crypto/+/392015 Trust: Filippo Valsorda <[email protected]> Run-TryBot: Filippo Valsorda <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent e4b3678 commit 4d39909

File tree

1 file changed

+30
-62
lines changed

1 file changed

+30
-62
lines changed

ssh/kex.go

Lines changed: 30 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -563,7 +563,6 @@ func (kex *curve25519sha256) Server(c packetConn, rand io.Reader, magics *handsh
563563
// diffie-hellman-group-exchange-sha256 key agreement protocols,
564564
// as described in RFC 4419
565565
type dhGEXSHA struct {
566-
g, p *big.Int
567566
hashFunc crypto.Hash
568567
}
569568

@@ -573,14 +572,7 @@ const (
573572
dhGroupExchangeMaximumBits = 8192
574573
)
575574

576-
func (gex *dhGEXSHA) diffieHellman(theirPublic, myPrivate *big.Int) (*big.Int, error) {
577-
if theirPublic.Sign() <= 0 || theirPublic.Cmp(gex.p) >= 0 {
578-
return nil, fmt.Errorf("ssh: DH parameter out of bounds")
579-
}
580-
return new(big.Int).Exp(theirPublic, myPrivate, gex.p), nil
581-
}
582-
583-
func (gex dhGEXSHA) Client(c packetConn, randSource io.Reader, magics *handshakeMagics) (*kexResult, error) {
575+
func (gex *dhGEXSHA) Client(c packetConn, randSource io.Reader, magics *handshakeMagics) (*kexResult, error) {
584576
// Send GexRequest
585577
kexDHGexRequest := kexDHGexRequestMsg{
586578
MinBits: dhGroupExchangeMinimumBits,
@@ -597,35 +589,29 @@ func (gex dhGEXSHA) Client(c packetConn, randSource io.Reader, magics *handshake
597589
return nil, err
598590
}
599591

600-
var kexDHGexGroup kexDHGexGroupMsg
601-
if err = Unmarshal(packet, &kexDHGexGroup); err != nil {
592+
var msg kexDHGexGroupMsg
593+
if err = Unmarshal(packet, &msg); err != nil {
602594
return nil, err
603595
}
604596

605597
// reject if p's bit length < dhGroupExchangeMinimumBits or > dhGroupExchangeMaximumBits
606-
if kexDHGexGroup.P.BitLen() < dhGroupExchangeMinimumBits || kexDHGexGroup.P.BitLen() > dhGroupExchangeMaximumBits {
607-
return nil, fmt.Errorf("ssh: server-generated gex p is out of range (%d bits)", kexDHGexGroup.P.BitLen())
598+
if msg.P.BitLen() < dhGroupExchangeMinimumBits || msg.P.BitLen() > dhGroupExchangeMaximumBits {
599+
return nil, fmt.Errorf("ssh: server-generated gex p is out of range (%d bits)", msg.P.BitLen())
608600
}
609601

610-
gex.p = kexDHGexGroup.P
611-
gex.g = kexDHGexGroup.G
612-
613-
// Check if g is safe by verifing that g > 1 and g < p - 1
614-
one := big.NewInt(1)
615-
var pMinusOne = &big.Int{}
616-
pMinusOne.Sub(gex.p, one)
617-
if gex.g.Cmp(one) != 1 && gex.g.Cmp(pMinusOne) != -1 {
602+
// Check if g is safe by verifying that 1 < g < p-1
603+
pMinusOne := new(big.Int).Sub(msg.P, bigOne)
604+
if msg.G.Cmp(bigOne) <= 0 || msg.G.Cmp(pMinusOne) >= 0 {
618605
return nil, fmt.Errorf("ssh: server provided gex g is not safe")
619606
}
620607

621608
// Send GexInit
622-
var pHalf = &big.Int{}
623-
pHalf.Rsh(gex.p, 1)
609+
pHalf := new(big.Int).Rsh(msg.P, 1)
624610
x, err := rand.Int(randSource, pHalf)
625611
if err != nil {
626612
return nil, err
627613
}
628-
X := new(big.Int).Exp(gex.g, x, gex.p)
614+
X := new(big.Int).Exp(msg.G, x, msg.P)
629615
kexDHGexInit := kexDHGexInitMsg{
630616
X: X,
631617
}
@@ -644,13 +630,13 @@ func (gex dhGEXSHA) Client(c packetConn, randSource io.Reader, magics *handshake
644630
return nil, err
645631
}
646632

647-
kInt, err := gex.diffieHellman(kexDHGexReply.Y, x)
648-
if err != nil {
649-
return nil, err
633+
if kexDHGexReply.Y.Cmp(bigOne) <= 0 || kexDHGexReply.Y.Cmp(pMinusOne) >= 0 {
634+
return nil, errors.New("ssh: DH parameter out of bounds")
650635
}
636+
kInt := new(big.Int).Exp(kexDHGexReply.Y, x, msg.P)
651637

652-
// Check if k is safe by verifing that k > 1 and k < p - 1
653-
if kInt.Cmp(one) != 1 && kInt.Cmp(pMinusOne) != -1 {
638+
// Check if k is safe by verifying that k > 1 and k < p - 1
639+
if kInt.Cmp(bigOne) <= 0 || kInt.Cmp(pMinusOne) >= 0 {
654640
return nil, fmt.Errorf("ssh: derived k is not safe")
655641
}
656642

@@ -660,8 +646,8 @@ func (gex dhGEXSHA) Client(c packetConn, randSource io.Reader, magics *handshake
660646
binary.Write(h, binary.BigEndian, uint32(dhGroupExchangeMinimumBits))
661647
binary.Write(h, binary.BigEndian, uint32(dhGroupExchangePreferredBits))
662648
binary.Write(h, binary.BigEndian, uint32(dhGroupExchangeMaximumBits))
663-
writeInt(h, gex.p)
664-
writeInt(h, gex.g)
649+
writeInt(h, msg.P)
650+
writeInt(h, msg.G)
665651
writeInt(h, X)
666652
writeInt(h, kexDHGexReply.Y)
667653
K := make([]byte, intLength(kInt))
@@ -691,35 +677,17 @@ func (gex dhGEXSHA) Server(c packetConn, randSource io.Reader, magics *handshake
691677
return
692678
}
693679

694-
// smoosh the user's preferred size into our own limits
695-
if kexDHGexRequest.PreferedBits > dhGroupExchangeMaximumBits {
696-
kexDHGexRequest.PreferedBits = dhGroupExchangeMaximumBits
697-
}
698-
if kexDHGexRequest.PreferedBits < dhGroupExchangeMinimumBits {
699-
kexDHGexRequest.PreferedBits = dhGroupExchangeMinimumBits
700-
}
701-
// fix min/max if they're inconsistent. technically, we could just pout
702-
// and hang up, but there's no harm in giving them the benefit of the
703-
// doubt and just picking a bitsize for them.
704-
if kexDHGexRequest.MinBits > kexDHGexRequest.PreferedBits {
705-
kexDHGexRequest.MinBits = kexDHGexRequest.PreferedBits
706-
}
707-
if kexDHGexRequest.MaxBits < kexDHGexRequest.PreferedBits {
708-
kexDHGexRequest.MaxBits = kexDHGexRequest.PreferedBits
709-
}
710-
711680
// Send GexGroup
712681
// This is the group called diffie-hellman-group14-sha1 in RFC
713682
// 4253 and Oakley Group 14 in RFC 3526.
714683
p, _ := new(big.Int).SetString("FFFFFFFFFFFFFFFFC90FDAA22168C234C4C6628B80DC1CD129024E088A67CC74020BBEA63B139B22514A08798E3404DDEF9519B3CD3A431B302B0A6DF25F14374FE1356D6D51C245E485B576625E7EC6F44C42E9A637ED6B0BFF5CB6F406B7EDEE386BFB5A899FA5AE9F24117C4B1FE649286651ECE45B3DC2007CB8A163BF0598DA48361C55D39A69163FA8FD24CF5F83655D23DCA3AD961C62F356208552BB9ED529077096966D670C354E4ABC9804F1746C08CA18217C32905E462E36CE3BE39E772C180E86039B2783A2EC07A28FB5C55DF06F4C52C9DE2BCBF6955817183995497CEA956AE515D2261898FA051015728E5A8AACAA68FFFFFFFFFFFFFFFF", 16)
715-
gex.p = p
716-
gex.g = big.NewInt(2)
684+
g := big.NewInt(2)
717685

718-
kexDHGexGroup := kexDHGexGroupMsg{
719-
P: gex.p,
720-
G: gex.g,
686+
msg := &kexDHGexGroupMsg{
687+
P: p,
688+
G: g,
721689
}
722-
if err := c.writePacket(Marshal(&kexDHGexGroup)); err != nil {
690+
if err := c.writePacket(Marshal(msg)); err != nil {
723691
return nil, err
724692
}
725693

@@ -733,19 +701,19 @@ func (gex dhGEXSHA) Server(c packetConn, randSource io.Reader, magics *handshake
733701
return
734702
}
735703

736-
var pHalf = &big.Int{}
737-
pHalf.Rsh(gex.p, 1)
704+
pHalf := new(big.Int).Rsh(p, 1)
738705

739706
y, err := rand.Int(randSource, pHalf)
740707
if err != nil {
741708
return
742709
}
710+
Y := new(big.Int).Exp(g, y, p)
743711

744-
Y := new(big.Int).Exp(gex.g, y, gex.p)
745-
kInt, err := gex.diffieHellman(kexDHGexInit.X, y)
746-
if err != nil {
747-
return nil, err
712+
pMinusOne := new(big.Int).Sub(p, bigOne)
713+
if kexDHGexInit.X.Cmp(bigOne) <= 0 || kexDHGexInit.X.Cmp(pMinusOne) >= 0 {
714+
return nil, errors.New("ssh: DH parameter out of bounds")
748715
}
716+
kInt := new(big.Int).Exp(kexDHGexInit.X, y, p)
749717

750718
hostKeyBytes := priv.PublicKey().Marshal()
751719

@@ -755,8 +723,8 @@ func (gex dhGEXSHA) Server(c packetConn, randSource io.Reader, magics *handshake
755723
binary.Write(h, binary.BigEndian, uint32(dhGroupExchangeMinimumBits))
756724
binary.Write(h, binary.BigEndian, uint32(dhGroupExchangePreferredBits))
757725
binary.Write(h, binary.BigEndian, uint32(dhGroupExchangeMaximumBits))
758-
writeInt(h, gex.p)
759-
writeInt(h, gex.g)
726+
writeInt(h, p)
727+
writeInt(h, g)
760728
writeInt(h, kexDHGexInit.X)
761729
writeInt(h, Y)
762730

0 commit comments

Comments
 (0)