Skip to content

Commit

Permalink
ssh: add support for extension negotiation (rfc 8308)
Browse files Browse the repository at this point in the history
This is a rebase of the following PR

#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 <[email protected]>
  • Loading branch information
drakkan committed Mar 30, 2022
1 parent 2c7772b commit 7fe3443
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 5 deletions.
4 changes: 1 addition & 3 deletions ssh/client_auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
Expand Down
33 changes: 33 additions & 0 deletions ssh/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -89,6 +101,15 @@ var supportedMACs = []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{
Expand Down Expand Up @@ -180,6 +201,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)
Expand Down Expand Up @@ -257,6 +282,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
Expand Down Expand Up @@ -286,6 +315,10 @@ func (c *Config) SetDefaults() {
c.MACs = supportedMACs
}

if c.Extensions == nil {
c.Extensions = defaultExtensions
}

if c.RekeyThreshold == 0 {
// cipher specific default
} else if c.RekeyThreshold < minRekeyThreshold {
Expand Down
37 changes: 36 additions & 1 deletion ssh/handshake.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"io"
"log"
"net"
"strings"
"sync"
)

Expand Down Expand Up @@ -94,6 +95,10 @@ type handshakeTransport struct {

// The session ID or nil if first kex did not complete yet.
sessionID []byte

// True if the first ext info message has been sent immediately following
// SSH_MSG_NEWKEYS, false otherwise.
extInfoSent bool
}

type pendingKex struct {
Expand Down Expand Up @@ -474,6 +479,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

Expand All @@ -483,7 +491,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)
}
}

Expand Down Expand Up @@ -632,6 +640,33 @@ 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
if !t.extInfoSent && contains(clientInit.KexAlgos, extInfoClient) && contains(t.config.Extensions, ExtServerSigAlgs) {
// 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{}
// 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
}

Expand Down
33 changes: 32 additions & 1 deletion ssh/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,11 +256,41 @@ 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
}

// be permissive and don't add contains(s.transport.config.Extensions, ExtServerSigAlgs)
if 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
Expand All @@ -286,7 +316,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
Expand Down

0 comments on commit 7fe3443

Please sign in to comment.