From be0d0d09dfb35e5c2207db1b0540f6f1347a7268 Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Tue, 29 Mar 2022 13:03:13 +0200 Subject: [PATCH] ssh: add support for extension negotiation (rfc 8308) This is a rebase of the following PR https://github.com/golang/crypto/pull/197 with some changes and improvements: - added support for client certificate authentication - removed read loop from server handshake - adapted extInfoMsg to upstream changes Signed-off-by: Nicola Murino --- ssh/client_auth_test.go | 4 +--- ssh/common.go | 33 ++++++++++++++++++++++++++++++++ ssh/handshake.go | 42 ++++++++++++++++++++++++++++++++++++++++- ssh/server.go | 32 ++++++++++++++++++++++++++++++- 4 files changed, 106 insertions(+), 5 deletions(-) diff --git a/ssh/client_auth_test.go b/ssh/client_auth_test.go index a6bedbfcaa..35b62e3311 100644 --- a/ssh/client_auth_test.go +++ b/ssh/client_auth_test.go @@ -132,9 +132,7 @@ func TestClientAuthPublicKey(t *testing.T) { 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 { + if len(signer.used) != 1 || signer.used[0] != KeyAlgoRSASHA256 { t.Errorf("unexpected Sign/SignWithAlgorithm calls: %q", signer.used) } } diff --git a/ssh/common.go b/ssh/common.go index 00221b577f..13d8db25e6 100644 --- a/ssh/common.go +++ b/ssh/common.go @@ -24,6 +24,18 @@ const ( serviceSSH = "ssh-connection" ) +// These are string constants related to extensions and extension negotiation +const ( + extInfoServer = "ext-info-s" + extInfoClient = "ext-info-c" + ExtServerSigAlgs = "server-sig-algs" +) + +// defaultExtensions lists extensions enabled by default. +var defaultExtensions = []string{ + ExtServerSigAlgs, +} + // supportedCiphers lists ciphers we support but might not recommend. var supportedCiphers = []string{ "aes128-ctr", "aes192-ctr", "aes256-ctr", @@ -95,6 +107,15 @@ var preferredMACs = []string{ var supportedCompressions = []string{compressionNone} +// supportedServerSigAlgs defines the algorithms supported for pubkey authentication +// in no particular order. +var supportedServerSigAlgs = []string{KeyAlgoRSASHA256, + KeyAlgoRSASHA512, KeyAlgoRSA, + KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521, + KeyAlgoSKECDSA256, KeyAlgoED25519, KeyAlgoSKED25519, + KeyAlgoDSA, +} + // hashFuncs keeps the mapping of supported signature algorithms to their // respective hashes needed for signing and verification. var hashFuncs = map[string]crypto.Hash{ @@ -187,6 +208,10 @@ func findAgreedAlgorithms(isClient bool, clientKexInit, serverKexInit *kexInitMs result.kex, err = findCommon("key exchange", clientKexInit.KexAlgos, serverKexInit.KexAlgos) if err != nil { return + } else if result.kex == extInfoClient || result.kex == extInfoServer { + // According to RFC8308 section 2.2 if either the client or server extension signal + // is chosen as the kex algorithm the parties must disconnect. + return result, fmt.Errorf("ssh: invalid kex algorithm chosen: %s", result.kex) } result.hostKey, err = findCommon("host key", clientKexInit.ServerHostKeyAlgos, serverKexInit.ServerHostKeyAlgos) @@ -264,6 +289,10 @@ type Config struct { // The allowed MAC algorithms. If unspecified then a sensible default // is used. MACs []string + + // A list of enabled extensions. If unspecified then a sensible + // default is used + Extensions []string } // SetDefaults sets sensible values for unset fields in config. This is @@ -293,6 +322,10 @@ func (c *Config) SetDefaults() { c.MACs = preferredMACs } + if c.Extensions == nil { + c.Extensions = defaultExtensions + } + if c.RekeyThreshold == 0 { // cipher specific default } else if c.RekeyThreshold < minRekeyThreshold { diff --git a/ssh/handshake.go b/ssh/handshake.go index 653dc4d2cf..7f075e98c9 100644 --- a/ssh/handshake.go +++ b/ssh/handshake.go @@ -11,6 +11,7 @@ import ( "io" "log" "net" + "strings" "sync" ) @@ -94,6 +95,12 @@ type handshakeTransport struct { // The session ID or nil if first kex did not complete yet. sessionID []byte + + // True if the other side has signaled support for extensions. + extInfoSupported bool + // True if the first ext info message has been sent immediately following + // SSH_MSG_NEWKEYS, false otherwise. + extInfoSent bool } type pendingKex struct { @@ -474,6 +481,9 @@ func (t *handshakeTransport) sendKexInit() error { msg.ServerHostKeyAlgos = append(msg.ServerHostKeyAlgos, keyFormat) } } + if contains(t.config.Extensions, ExtServerSigAlgs) { + msg.KexAlgos = append(msg.KexAlgos, extInfoServer) + } } else { msg.ServerHostKeyAlgos = t.hostKeyAlgorithms @@ -483,7 +493,7 @@ func (t *handshakeTransport) sendKexInit() error { if firstKeyExchange := t.sessionID == nil; firstKeyExchange { msg.KexAlgos = make([]string, 0, len(t.config.KeyExchanges)+1) msg.KexAlgos = append(msg.KexAlgos, t.config.KeyExchanges...) - msg.KexAlgos = append(msg.KexAlgos, "ext-info-c") + msg.KexAlgos = append(msg.KexAlgos, extInfoClient) } } @@ -632,6 +642,36 @@ func (t *handshakeTransport) enterKeyExchange(otherInitPacket []byte) error { return unexpectedMessageError(msgNewKeys, packet[0]) } + if !isClient { + // We're on the server side, see if the client sent the extension signal + t.extInfoSupported = contains(clientInit.KexAlgos, extInfoClient) + if t.extInfoSupported && !t.extInfoSent && len(t.config.Extensions) > 0 { + // The other side supports ext info, an ext info message hasn't been sent this session, + // and we have at least one extension enabled, so send an SSH_MSG_EXT_INFO message. + extensions := map[string][]byte{} + if contains(t.config.Extensions, ExtServerSigAlgs) { + // We're the server, the client supports SSH_MSG_EXT_INFO and server-sig-algs + // is enabled. Prepare the server-sig-algos extension message to send. + extensions[ExtServerSigAlgs] = []byte(strings.Join(supportedServerSigAlgs, ",")) + } + var payload []byte + for k, v := range extensions { + payload = appendInt(payload, len(k)) + payload = append(payload, k...) + payload = appendInt(payload, len(v)) + payload = append(payload, v...) + } + extInfo := extInfoMsg{ + NumExtensions: uint32(len(extensions)), + Payload: payload, + } + if err := t.conn.writePacket(Marshal(&extInfo)); err != nil { + return err + } + t.extInfoSent = true + } + } + return nil } diff --git a/ssh/server.go b/ssh/server.go index c8f804ebfd..06f8ebed5b 100644 --- a/ssh/server.go +++ b/ssh/server.go @@ -274,11 +274,40 @@ func (s *connection) serverHandshake(config *ServerConfig) (*Permissions, error) // We just did the key change, so the session ID is established. s.sessionID = s.transport.getSessionID() + // the client could send a SSH_MSG_EXT_INFO before SSH_MSG_SERVICE_REQUEST var packet []byte if packet, err = s.transport.readPacket(); err != nil { return nil, err } + if contains(s.transport.config.Extensions, ExtServerSigAlgs) && len(packet) > 0 && packet[0] == msgExtInfo { + // read SSH_MSG_EXT_INFO + var extInfo extInfoMsg + extensions := make(map[string][]byte) + if err := Unmarshal(packet, &extInfo); err != nil { + return nil, err + } + payload := extInfo.Payload + for i := uint32(0); i < extInfo.NumExtensions; i++ { + name, rest, ok := parseString(payload) + if !ok { + return nil, parseError(msgExtInfo) + } + value, rest, ok := parseString(rest) + if !ok { + return nil, parseError(msgExtInfo) + } + extensions[string(name)] = value + payload = rest + } + + // read the next packet + packet = nil + if packet, err = s.transport.readPacket(); err != nil { + return nil, err + } + } + var serviceRequest serviceRequestMsg if err = Unmarshal(packet, &serviceRequest); err != nil { return nil, err @@ -304,7 +333,8 @@ func (s *connection) serverHandshake(config *ServerConfig) (*Permissions, error) func isAcceptableAlgo(algo string) bool { switch algo { case KeyAlgoRSA, KeyAlgoRSASHA256, KeyAlgoRSASHA512, KeyAlgoDSA, KeyAlgoECDSA256, KeyAlgoECDSA384, KeyAlgoECDSA521, KeyAlgoSKECDSA256, KeyAlgoED25519, KeyAlgoSKED25519, - CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01, CertAlgoSKECDSA256v01, CertAlgoED25519v01, CertAlgoSKED25519v01: + CertAlgoRSAv01, CertAlgoDSAv01, CertAlgoECDSA256v01, CertAlgoECDSA384v01, CertAlgoECDSA521v01, CertAlgoSKECDSA256v01, CertAlgoED25519v01, CertAlgoSKED25519v01, + CertAlgoRSASHA256v01, CertAlgoRSASHA512v01: return true } return false