Skip to content

Commit 5d542ad

Browse files
committed
ssh: support rsa-sha2-256/512 for client authentication
CL 220037 had implemented support for host authentication using rsa-sha2-256/512, but not client public key authentication. OpenSSH disabled the SHA-1 based ssh-rsa by default in version 8.8 (after pre-announcing it in versions 8.2, 8.3, 8.4, 8.5, 8.6, and 8.7) although some distributions re-enable it. GitHub will start rejecting ssh-rsa for keys uploaded before November 2, 2021 on March 15, 2022. https://github.blog/2021-09-01-improving-git-protocol-security-github/ The server side already worked, as long as the client selected one of the SHA-2 algorithms, because the signature flowed freely to Verify. There was however nothing verifying that the signature algorithm matched the advertised one. The comment suggested the check was being performed, but it got lost back in CL 86190043. Not a security issue because the signature had to pass the callback's Verify method regardless, and both values were checked to be acceptable. Tested with OpenSSH 8.8 configured with "PubkeyAcceptedKeyTypes -ssh-rsa" and no application-side changes. The Signers returned by ssh/agent (when backed by an agent client) didn't actually implement AlgorithmSigner but ParameterizedSigner, an interface defined in an earlier version of CL 123955. Updates golang/go#49269 Fixes golang/go#39885 For golang/go#49952 Change-Id: I13b41db8041f1112a70f106c55f077b904b12cb8 Reviewed-on: https://go-review.googlesource.com/c/crypto/+/392394 Trust: Filippo Valsorda <[email protected]> Run-TryBot: Filippo Valsorda <[email protected]> TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Roland Shoemaker <[email protected]>
1 parent a577426 commit 5d542ad

File tree

7 files changed

+192
-35
lines changed

7 files changed

+192
-35
lines changed

ssh/agent/client.go

+15-9
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@ import (
2525
"math/big"
2626
"sync"
2727

28-
"crypto"
2928
"golang.org/x/crypto/ed25519"
3029
"golang.org/x/crypto/ssh"
3130
)
@@ -771,19 +770,26 @@ func (s *agentKeyringSigner) Sign(rand io.Reader, data []byte) (*ssh.Signature,
771770
return s.agent.Sign(s.pub, data)
772771
}
773772

774-
func (s *agentKeyringSigner) SignWithOpts(rand io.Reader, data []byte, opts crypto.SignerOpts) (*ssh.Signature, error) {
773+
func (s *agentKeyringSigner) SignWithAlgorithm(rand io.Reader, data []byte, algorithm string) (*ssh.Signature, error) {
774+
if algorithm == "" || algorithm == s.pub.Type() {
775+
return s.Sign(rand, data)
776+
}
777+
775778
var flags SignatureFlags
776-
if opts != nil {
777-
switch opts.HashFunc() {
778-
case crypto.SHA256:
779-
flags = SignatureFlagRsaSha256
780-
case crypto.SHA512:
781-
flags = SignatureFlagRsaSha512
782-
}
779+
switch algorithm {
780+
case ssh.KeyAlgoRSASHA256:
781+
flags = SignatureFlagRsaSha256
782+
case ssh.KeyAlgoRSASHA512:
783+
flags = SignatureFlagRsaSha512
784+
default:
785+
return nil, fmt.Errorf("agent: unsupported algorithm %q", algorithm)
783786
}
787+
784788
return s.agent.SignWithFlags(s.pub, data, flags)
785789
}
786790

791+
var _ ssh.AlgorithmSigner = &agentKeyringSigner{}
792+
787793
// Calls an extension method. It is up to the agent implementation as to whether or not
788794
// any particular extension is supported and may always return an error. Because the
789795
// type of the response is up to the implementation, this returns the bytes of the

ssh/client_auth.go

+95-21
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"errors"
1010
"fmt"
1111
"io"
12+
"strings"
1213
)
1314

1415
type authResult int
@@ -29,6 +30,33 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error {
2930
if err != nil {
3031
return err
3132
}
33+
// The server may choose to send a SSH_MSG_EXT_INFO at this point (if we
34+
// advertised willingness to receive one, which we always do) or not. See
35+
// RFC 8308, Section 2.4.
36+
extensions := make(map[string][]byte)
37+
if len(packet) > 0 && packet[0] == msgExtInfo {
38+
var extInfo extInfoMsg
39+
if err := Unmarshal(packet, &extInfo); err != nil {
40+
return err
41+
}
42+
payload := extInfo.Payload
43+
for i := uint32(0); i < extInfo.NumExtensions; i++ {
44+
name, rest, ok := parseString(payload)
45+
if !ok {
46+
return parseError(msgExtInfo)
47+
}
48+
value, rest, ok := parseString(rest)
49+
if !ok {
50+
return parseError(msgExtInfo)
51+
}
52+
extensions[string(name)] = value
53+
payload = rest
54+
}
55+
packet, err = c.transport.readPacket()
56+
if err != nil {
57+
return err
58+
}
59+
}
3260
var serviceAccept serviceAcceptMsg
3361
if err := Unmarshal(packet, &serviceAccept); err != nil {
3462
return err
@@ -41,7 +69,7 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error {
4169

4270
sessionID := c.transport.getSessionID()
4371
for auth := AuthMethod(new(noneAuth)); auth != nil; {
44-
ok, methods, err := auth.auth(sessionID, config.User, c.transport, config.Rand)
72+
ok, methods, err := auth.auth(sessionID, config.User, c.transport, config.Rand, extensions)
4573
if err != nil {
4674
return err
4775
}
@@ -93,7 +121,7 @@ type AuthMethod interface {
93121
// If authentication is not successful, a []string of alternative
94122
// method names is returned. If the slice is nil, it will be ignored
95123
// and the previous set of possible methods will be reused.
96-
auth(session []byte, user string, p packetConn, rand io.Reader) (authResult, []string, error)
124+
auth(session []byte, user string, p packetConn, rand io.Reader, extensions map[string][]byte) (authResult, []string, error)
97125

98126
// method returns the RFC 4252 method name.
99127
method() string
@@ -102,7 +130,7 @@ type AuthMethod interface {
102130
// "none" authentication, RFC 4252 section 5.2.
103131
type noneAuth int
104132

105-
func (n *noneAuth) auth(session []byte, user string, c packetConn, rand io.Reader) (authResult, []string, error) {
133+
func (n *noneAuth) auth(session []byte, user string, c packetConn, rand io.Reader, _ map[string][]byte) (authResult, []string, error) {
106134
if err := c.writePacket(Marshal(&userAuthRequestMsg{
107135
User: user,
108136
Service: serviceSSH,
@@ -122,7 +150,7 @@ func (n *noneAuth) method() string {
122150
// a function call, e.g. by prompting the user.
123151
type passwordCallback func() (password string, err error)
124152

125-
func (cb passwordCallback) auth(session []byte, user string, c packetConn, rand io.Reader) (authResult, []string, error) {
153+
func (cb passwordCallback) auth(session []byte, user string, c packetConn, rand io.Reader, _ map[string][]byte) (authResult, []string, error) {
126154
type passwordAuthMsg struct {
127155
User string `sshtype:"50"`
128156
Service string
@@ -189,7 +217,36 @@ func (cb publicKeyCallback) method() string {
189217
return "publickey"
190218
}
191219

192-
func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand io.Reader) (authResult, []string, error) {
220+
func pickSignatureAlgorithm(signer Signer, extensions map[string][]byte) (as AlgorithmSigner, algo string) {
221+
keyFormat := signer.PublicKey().Type()
222+
223+
// Like in sendKexInit, if the public key implements AlgorithmSigner we
224+
// assume it supports all algorithms, otherwise only the key format one.
225+
as, ok := signer.(AlgorithmSigner)
226+
if !ok {
227+
return algorithmSignerWrapper{signer}, keyFormat
228+
}
229+
230+
extPayload, ok := extensions["server-sig-algs"]
231+
if !ok {
232+
// If there is no "server-sig-algs" extension, fall back to the key
233+
// format algorithm.
234+
return as, keyFormat
235+
}
236+
237+
serverAlgos := strings.Split(string(extPayload), ",")
238+
keyAlgos := algorithmsForKeyFormat(keyFormat)
239+
algo, err := findCommon("public key signature algorithm", keyAlgos, serverAlgos)
240+
if err != nil {
241+
// If there is no overlap, try the key anyway with the key format
242+
// algorithm, to support servers that fail to list all supported
243+
// algorithms.
244+
return as, keyFormat
245+
}
246+
return as, algo
247+
}
248+
249+
func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand io.Reader, extensions map[string][]byte) (authResult, []string, error) {
193250
// Authentication is performed by sending an enquiry to test if a key is
194251
// acceptable to the remote. If the key is acceptable, the client will
195252
// attempt to authenticate with the valid key. If not the client will repeat
@@ -201,21 +258,24 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand
201258
}
202259
var methods []string
203260
for _, signer := range signers {
204-
ok, err := validateKey(signer.PublicKey(), user, c)
261+
pub := signer.PublicKey()
262+
as, algo := pickSignatureAlgorithm(signer, extensions)
263+
264+
ok, err := validateKey(pub, algo, user, c)
205265
if err != nil {
206266
return authFailure, nil, err
207267
}
208268
if !ok {
209269
continue
210270
}
211271

212-
pub := signer.PublicKey()
213272
pubKey := pub.Marshal()
214-
sign, err := signer.Sign(rand, buildDataSignedForAuth(session, userAuthRequestMsg{
273+
data := buildDataSignedForAuth(session, userAuthRequestMsg{
215274
User: user,
216275
Service: serviceSSH,
217276
Method: cb.method(),
218-
}, []byte(pub.Type()), pubKey))
277+
}, algo, pubKey)
278+
sign, err := as.SignWithAlgorithm(rand, data, underlyingAlgo(algo))
219279
if err != nil {
220280
return authFailure, nil, err
221281
}
@@ -229,7 +289,7 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand
229289
Service: serviceSSH,
230290
Method: cb.method(),
231291
HasSig: true,
232-
Algoname: pub.Type(),
292+
Algoname: algo,
233293
PubKey: pubKey,
234294
Sig: sig,
235295
}
@@ -266,26 +326,25 @@ func containsMethod(methods []string, method string) bool {
266326
}
267327

268328
// validateKey validates the key provided is acceptable to the server.
269-
func validateKey(key PublicKey, user string, c packetConn) (bool, error) {
329+
func validateKey(key PublicKey, algo string, user string, c packetConn) (bool, error) {
270330
pubKey := key.Marshal()
271331
msg := publickeyAuthMsg{
272332
User: user,
273333
Service: serviceSSH,
274334
Method: "publickey",
275335
HasSig: false,
276-
Algoname: key.Type(),
336+
Algoname: algo,
277337
PubKey: pubKey,
278338
}
279339
if err := c.writePacket(Marshal(&msg)); err != nil {
280340
return false, err
281341
}
282342

283-
return confirmKeyAck(key, c)
343+
return confirmKeyAck(key, algo, c)
284344
}
285345

286-
func confirmKeyAck(key PublicKey, c packetConn) (bool, error) {
346+
func confirmKeyAck(key PublicKey, algo string, c packetConn) (bool, error) {
287347
pubKey := key.Marshal()
288-
algoname := key.Type()
289348

290349
for {
291350
packet, err := c.readPacket()
@@ -302,14 +361,14 @@ func confirmKeyAck(key PublicKey, c packetConn) (bool, error) {
302361
if err := Unmarshal(packet, &msg); err != nil {
303362
return false, err
304363
}
305-
if msg.Algo != algoname || !bytes.Equal(msg.PubKey, pubKey) {
364+
if msg.Algo != algo || !bytes.Equal(msg.PubKey, pubKey) {
306365
return false, nil
307366
}
308367
return true, nil
309368
case msgUserAuthFailure:
310369
return false, nil
311370
default:
312-
return false, unexpectedMessageError(msgUserAuthSuccess, packet[0])
371+
return false, unexpectedMessageError(msgUserAuthPubKeyOk, packet[0])
313372
}
314373
}
315374
}
@@ -330,6 +389,7 @@ func PublicKeysCallback(getSigners func() (signers []Signer, err error)) AuthMet
330389
// along with a list of remaining authentication methods to try next and
331390
// an error if an unexpected response was received.
332391
func handleAuthResponse(c packetConn) (authResult, []string, error) {
392+
gotMsgExtInfo := false
333393
for {
334394
packet, err := c.readPacket()
335395
if err != nil {
@@ -341,6 +401,12 @@ func handleAuthResponse(c packetConn) (authResult, []string, error) {
341401
if err := handleBannerResponse(c, packet); err != nil {
342402
return authFailure, nil, err
343403
}
404+
case msgExtInfo:
405+
// Ignore post-authentication RFC 8308 extensions, once.
406+
if gotMsgExtInfo {
407+
return authFailure, nil, unexpectedMessageError(msgUserAuthSuccess, packet[0])
408+
}
409+
gotMsgExtInfo = true
344410
case msgUserAuthFailure:
345411
var msg userAuthFailureMsg
346412
if err := Unmarshal(packet, &msg); err != nil {
@@ -395,7 +461,7 @@ func (cb KeyboardInteractiveChallenge) method() string {
395461
return "keyboard-interactive"
396462
}
397463

398-
func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packetConn, rand io.Reader) (authResult, []string, error) {
464+
func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packetConn, rand io.Reader, _ map[string][]byte) (authResult, []string, error) {
399465
type initiateMsg struct {
400466
User string `sshtype:"50"`
401467
Service string
@@ -412,6 +478,7 @@ func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packe
412478
return authFailure, nil, err
413479
}
414480

481+
gotMsgExtInfo := false
415482
for {
416483
packet, err := c.readPacket()
417484
if err != nil {
@@ -425,6 +492,13 @@ func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packe
425492
return authFailure, nil, err
426493
}
427494
continue
495+
case msgExtInfo:
496+
// Ignore post-authentication RFC 8308 extensions, once.
497+
if gotMsgExtInfo {
498+
return authFailure, nil, unexpectedMessageError(msgUserAuthInfoRequest, packet[0])
499+
}
500+
gotMsgExtInfo = true
501+
continue
428502
case msgUserAuthInfoRequest:
429503
// OK
430504
case msgUserAuthFailure:
@@ -497,9 +571,9 @@ type retryableAuthMethod struct {
497571
maxTries int
498572
}
499573

500-
func (r *retryableAuthMethod) auth(session []byte, user string, c packetConn, rand io.Reader) (ok authResult, methods []string, err error) {
574+
func (r *retryableAuthMethod) auth(session []byte, user string, c packetConn, rand io.Reader, extensions map[string][]byte) (ok authResult, methods []string, err error) {
501575
for i := 0; r.maxTries <= 0 || i < r.maxTries; i++ {
502-
ok, methods, err = r.authMethod.auth(session, user, c, rand)
576+
ok, methods, err = r.authMethod.auth(session, user, c, rand, extensions)
503577
if ok != authFailure || err != nil { // either success, partial success or error terminate
504578
return ok, methods, err
505579
}
@@ -542,7 +616,7 @@ type gssAPIWithMICCallback struct {
542616
target string
543617
}
544618

545-
func (g *gssAPIWithMICCallback) auth(session []byte, user string, c packetConn, rand io.Reader) (authResult, []string, error) {
619+
func (g *gssAPIWithMICCallback) auth(session []byte, user string, c packetConn, rand io.Reader, _ map[string][]byte) (authResult, []string, error) {
546620
m := &userAuthRequestMsg{
547621
User: user,
548622
Service: serviceSSH,

ssh/client_auth_test.go

+53-1
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,63 @@ func tryAuthBothSides(t *testing.T, config *ClientConfig, gssAPIWithMICConfig *G
105105
return err, serverAuthErrors
106106
}
107107

108+
type loggingAlgorithmSigner struct {
109+
used []string
110+
AlgorithmSigner
111+
}
112+
113+
func (l *loggingAlgorithmSigner) Sign(rand io.Reader, data []byte) (*Signature, error) {
114+
l.used = append(l.used, "[Sign]")
115+
return l.AlgorithmSigner.Sign(rand, data)
116+
}
117+
118+
func (l *loggingAlgorithmSigner) SignWithAlgorithm(rand io.Reader, data []byte, algorithm string) (*Signature, error) {
119+
l.used = append(l.used, algorithm)
120+
return l.AlgorithmSigner.SignWithAlgorithm(rand, data, algorithm)
121+
}
122+
108123
func TestClientAuthPublicKey(t *testing.T) {
124+
signer := &loggingAlgorithmSigner{AlgorithmSigner: testSigners["rsa"].(AlgorithmSigner)}
109125
config := &ClientConfig{
110126
User: "testuser",
111127
Auth: []AuthMethod{
112-
PublicKeys(testSigners["rsa"]),
128+
PublicKeys(signer),
129+
},
130+
HostKeyCallback: InsecureIgnoreHostKey(),
131+
}
132+
if err := tryAuth(t, config); err != nil {
133+
t.Fatalf("unable to dial remote side: %s", err)
134+
}
135+
// Once the server implements the server-sig-algs extension, this will turn
136+
// into KeyAlgoRSASHA256.
137+
if len(signer.used) != 1 || signer.used[0] != KeyAlgoRSA {
138+
t.Errorf("unexpected Sign/SignWithAlgorithm calls: %q", signer.used)
139+
}
140+
}
141+
142+
// TestClientAuthNoSHA2 tests a ssh-rsa Signer that doesn't implement AlgorithmSigner.
143+
func TestClientAuthNoSHA2(t *testing.T) {
144+
config := &ClientConfig{
145+
User: "testuser",
146+
Auth: []AuthMethod{
147+
PublicKeys(&legacyRSASigner{testSigners["rsa"]}),
148+
},
149+
HostKeyCallback: InsecureIgnoreHostKey(),
150+
}
151+
if err := tryAuth(t, config); err != nil {
152+
t.Fatalf("unable to dial remote side: %s", err)
153+
}
154+
}
155+
156+
// TestClientAuthThirdKey checks that the third configured can succeed. If we
157+
// were to do three attempts for each key (rsa-sha2-256, rsa-sha2-512, ssh-rsa),
158+
// we'd hit the six maximum attempts before reaching it.
159+
func TestClientAuthThirdKey(t *testing.T) {
160+
config := &ClientConfig{
161+
User: "testuser",
162+
Auth: []AuthMethod{
163+
PublicKeys(testSigners["rsa-openssh-format"],
164+
testSigners["rsa-openssh-format"], testSigners["rsa"]),
113165
},
114166
HostKeyCallback: InsecureIgnoreHostKey(),
115167
}

0 commit comments

Comments
 (0)