Skip to content

Commit

Permalink
ssh: support rsa-sha2-256/512 for client authentication
Browse files Browse the repository at this point in the history
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 <filippo@golang.org>
Run-TryBot: Filippo Valsorda <filippo@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Roland Shoemaker <roland@golang.org>
  • Loading branch information
FiloSottile committed Mar 14, 2022
1 parent 0a64037 commit 56f1c49
Show file tree
Hide file tree
Showing 7 changed files with 192 additions and 35 deletions.
24 changes: 15 additions & 9 deletions ssh/agent/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ import (
"math/big"
"sync"

"crypto"
"golang.org/x/crypto/ed25519"
"golang.org/x/crypto/ssh"
)
Expand Down Expand Up @@ -771,19 +770,26 @@ func (s *agentKeyringSigner) Sign(rand io.Reader, data []byte) (*ssh.Signature,
return s.agent.Sign(s.pub, data)
}

func (s *agentKeyringSigner) SignWithOpts(rand io.Reader, data []byte, opts crypto.SignerOpts) (*ssh.Signature, error) {
func (s *agentKeyringSigner) SignWithAlgorithm(rand io.Reader, data []byte, algorithm string) (*ssh.Signature, error) {
if algorithm == "" || algorithm == s.pub.Type() {
return s.Sign(rand, data)
}

var flags SignatureFlags
if opts != nil {
switch opts.HashFunc() {
case crypto.SHA256:
flags = SignatureFlagRsaSha256
case crypto.SHA512:
flags = SignatureFlagRsaSha512
}
switch algorithm {
case ssh.KeyAlgoRSASHA256:
flags = SignatureFlagRsaSha256
case ssh.KeyAlgoRSASHA512:
flags = SignatureFlagRsaSha512
default:
return nil, fmt.Errorf("agent: unsupported algorithm %q", algorithm)
}

return s.agent.SignWithFlags(s.pub, data, flags)
}

var _ ssh.AlgorithmSigner = &agentKeyringSigner{}

// Calls an extension method. It is up to the agent implementation as to whether or not
// any particular extension is supported and may always return an error. Because the
// type of the response is up to the implementation, this returns the bytes of the
Expand Down
116 changes: 95 additions & 21 deletions ssh/client_auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"errors"
"fmt"
"io"
"strings"
)

type authResult int
Expand All @@ -29,6 +30,33 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error {
if err != nil {
return err
}
// The server may choose to send a SSH_MSG_EXT_INFO at this point (if we
// advertised willingness to receive one, which we always do) or not. See
// RFC 8308, Section 2.4.
extensions := make(map[string][]byte)
if len(packet) > 0 && packet[0] == msgExtInfo {
var extInfo extInfoMsg
if err := Unmarshal(packet, &extInfo); err != nil {
return err
}
payload := extInfo.Payload
for i := uint32(0); i < extInfo.NumExtensions; i++ {
name, rest, ok := parseString(payload)
if !ok {
return parseError(msgExtInfo)
}
value, rest, ok := parseString(rest)
if !ok {
return parseError(msgExtInfo)
}
extensions[string(name)] = value
payload = rest
}
packet, err = c.transport.readPacket()
if err != nil {
return err
}
}
var serviceAccept serviceAcceptMsg
if err := Unmarshal(packet, &serviceAccept); err != nil {
return err
Expand All @@ -41,7 +69,7 @@ func (c *connection) clientAuthenticate(config *ClientConfig) error {

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

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

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

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

func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand io.Reader) (authResult, []string, error) {
func pickSignatureAlgorithm(signer Signer, extensions map[string][]byte) (as AlgorithmSigner, algo string) {
keyFormat := signer.PublicKey().Type()

// Like in sendKexInit, if the public key implements AlgorithmSigner we
// assume it supports all algorithms, otherwise only the key format one.
as, ok := signer.(AlgorithmSigner)
if !ok {
return algorithmSignerWrapper{signer}, keyFormat
}

extPayload, ok := extensions["server-sig-algs"]
if !ok {
// If there is no "server-sig-algs" extension, fall back to the key
// format algorithm.
return as, keyFormat
}

serverAlgos := strings.Split(string(extPayload), ",")
keyAlgos := algorithmsForKeyFormat(keyFormat)
algo, err := findCommon("public key signature algorithm", keyAlgos, serverAlgos)
if err != nil {
// If there is no overlap, try the key anyway with the key format
// algorithm, to support servers that fail to list all supported
// algorithms.
return as, keyFormat
}
return as, algo
}

func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand io.Reader, extensions map[string][]byte) (authResult, []string, error) {
// Authentication is performed by sending an enquiry to test if a key is
// acceptable to the remote. If the key is acceptable, the client will
// attempt to authenticate with the valid key. If not the client will repeat
Expand All @@ -201,21 +258,24 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand
}
var methods []string
for _, signer := range signers {
ok, err := validateKey(signer.PublicKey(), user, c)
pub := signer.PublicKey()
as, algo := pickSignatureAlgorithm(signer, extensions)

ok, err := validateKey(pub, algo, user, c)
if err != nil {
return authFailure, nil, err
}
if !ok {
continue
}

pub := signer.PublicKey()
pubKey := pub.Marshal()
sign, err := signer.Sign(rand, buildDataSignedForAuth(session, userAuthRequestMsg{
data := buildDataSignedForAuth(session, userAuthRequestMsg{
User: user,
Service: serviceSSH,
Method: cb.method(),
}, []byte(pub.Type()), pubKey))
}, algo, pubKey)
sign, err := as.SignWithAlgorithm(rand, data, underlyingAlgo(algo))
if err != nil {
return authFailure, nil, err
}
Expand All @@ -229,7 +289,7 @@ func (cb publicKeyCallback) auth(session []byte, user string, c packetConn, rand
Service: serviceSSH,
Method: cb.method(),
HasSig: true,
Algoname: pub.Type(),
Algoname: algo,
PubKey: pubKey,
Sig: sig,
}
Expand Down Expand Up @@ -266,26 +326,25 @@ func containsMethod(methods []string, method string) bool {
}

// validateKey validates the key provided is acceptable to the server.
func validateKey(key PublicKey, user string, c packetConn) (bool, error) {
func validateKey(key PublicKey, algo string, user string, c packetConn) (bool, error) {
pubKey := key.Marshal()
msg := publickeyAuthMsg{
User: user,
Service: serviceSSH,
Method: "publickey",
HasSig: false,
Algoname: key.Type(),
Algoname: algo,
PubKey: pubKey,
}
if err := c.writePacket(Marshal(&msg)); err != nil {
return false, err
}

return confirmKeyAck(key, c)
return confirmKeyAck(key, algo, c)
}

func confirmKeyAck(key PublicKey, c packetConn) (bool, error) {
func confirmKeyAck(key PublicKey, algo string, c packetConn) (bool, error) {
pubKey := key.Marshal()
algoname := key.Type()

for {
packet, err := c.readPacket()
Expand All @@ -302,14 +361,14 @@ func confirmKeyAck(key PublicKey, c packetConn) (bool, error) {
if err := Unmarshal(packet, &msg); err != nil {
return false, err
}
if msg.Algo != algoname || !bytes.Equal(msg.PubKey, pubKey) {
if msg.Algo != algo || !bytes.Equal(msg.PubKey, pubKey) {
return false, nil
}
return true, nil
case msgUserAuthFailure:
return false, nil
default:
return false, unexpectedMessageError(msgUserAuthSuccess, packet[0])
return false, unexpectedMessageError(msgUserAuthPubKeyOk, packet[0])
}
}
}
Expand All @@ -330,6 +389,7 @@ func PublicKeysCallback(getSigners func() (signers []Signer, err error)) AuthMet
// along with a list of remaining authentication methods to try next and
// an error if an unexpected response was received.
func handleAuthResponse(c packetConn) (authResult, []string, error) {
gotMsgExtInfo := false
for {
packet, err := c.readPacket()
if err != nil {
Expand All @@ -341,6 +401,12 @@ func handleAuthResponse(c packetConn) (authResult, []string, error) {
if err := handleBannerResponse(c, packet); err != nil {
return authFailure, nil, err
}
case msgExtInfo:
// Ignore post-authentication RFC 8308 extensions, once.
if gotMsgExtInfo {
return authFailure, nil, unexpectedMessageError(msgUserAuthSuccess, packet[0])
}
gotMsgExtInfo = true
case msgUserAuthFailure:
var msg userAuthFailureMsg
if err := Unmarshal(packet, &msg); err != nil {
Expand Down Expand Up @@ -395,7 +461,7 @@ func (cb KeyboardInteractiveChallenge) method() string {
return "keyboard-interactive"
}

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

gotMsgExtInfo := false
for {
packet, err := c.readPacket()
if err != nil {
Expand All @@ -425,6 +492,13 @@ func (cb KeyboardInteractiveChallenge) auth(session []byte, user string, c packe
return authFailure, nil, err
}
continue
case msgExtInfo:
// Ignore post-authentication RFC 8308 extensions, once.
if gotMsgExtInfo {
return authFailure, nil, unexpectedMessageError(msgUserAuthInfoRequest, packet[0])
}
gotMsgExtInfo = true
continue
case msgUserAuthInfoRequest:
// OK
case msgUserAuthFailure:
Expand Down Expand Up @@ -497,9 +571,9 @@ type retryableAuthMethod struct {
maxTries int
}

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

func (g *gssAPIWithMICCallback) auth(session []byte, user string, c packetConn, rand io.Reader) (authResult, []string, error) {
func (g *gssAPIWithMICCallback) auth(session []byte, user string, c packetConn, rand io.Reader, _ map[string][]byte) (authResult, []string, error) {
m := &userAuthRequestMsg{
User: user,
Service: serviceSSH,
Expand Down
54 changes: 53 additions & 1 deletion ssh/client_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,63 @@ func tryAuthBothSides(t *testing.T, config *ClientConfig, gssAPIWithMICConfig *G
return err, serverAuthErrors
}

type loggingAlgorithmSigner struct {
used []string
AlgorithmSigner
}

func (l *loggingAlgorithmSigner) Sign(rand io.Reader, data []byte) (*Signature, error) {
l.used = append(l.used, "[Sign]")
return l.AlgorithmSigner.Sign(rand, data)
}

func (l *loggingAlgorithmSigner) SignWithAlgorithm(rand io.Reader, data []byte, algorithm string) (*Signature, error) {
l.used = append(l.used, algorithm)
return l.AlgorithmSigner.SignWithAlgorithm(rand, data, algorithm)
}

func TestClientAuthPublicKey(t *testing.T) {
signer := &loggingAlgorithmSigner{AlgorithmSigner: testSigners["rsa"].(AlgorithmSigner)}
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
PublicKeys(testSigners["rsa"]),
PublicKeys(signer),
},
HostKeyCallback: InsecureIgnoreHostKey(),
}
if err := tryAuth(t, config); err != nil {
t.Fatalf("unable to dial remote side: %s", err)
}
// Once the server implements the server-sig-algs extension, this will turn
// into KeyAlgoRSASHA256.
if len(signer.used) != 1 || signer.used[0] != KeyAlgoRSA {
t.Errorf("unexpected Sign/SignWithAlgorithm calls: %q", signer.used)
}
}

// TestClientAuthNoSHA2 tests a ssh-rsa Signer that doesn't implement AlgorithmSigner.
func TestClientAuthNoSHA2(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
PublicKeys(&legacyRSASigner{testSigners["rsa"]}),
},
HostKeyCallback: InsecureIgnoreHostKey(),
}
if err := tryAuth(t, config); err != nil {
t.Fatalf("unable to dial remote side: %s", err)
}
}

// TestClientAuthThirdKey checks that the third configured can succeed. If we
// were to do three attempts for each key (rsa-sha2-256, rsa-sha2-512, ssh-rsa),
// we'd hit the six maximum attempts before reaching it.
func TestClientAuthThirdKey(t *testing.T) {
config := &ClientConfig{
User: "testuser",
Auth: []AuthMethod{
PublicKeys(testSigners["rsa-openssh-format"],
testSigners["rsa-openssh-format"], testSigners["rsa"]),
},
HostKeyCallback: InsecureIgnoreHostKey(),
}
Expand Down
Loading

0 comments on commit 56f1c49

Please sign in to comment.