]> Cypherpunks repositories - gostls13.git/commitdiff
exp/ssh: Add support for (most) of the ciphers from RFC4253, RFC4344 and RFC4345.
authorJohn Beisley <huin@google.com>
Fri, 18 Nov 2011 17:56:57 +0000 (12:56 -0500)
committerAdam Langley <agl@golang.org>
Fri, 18 Nov 2011 17:56:57 +0000 (12:56 -0500)
R=dave, agl, taruti, rsc, r
CC=golang-dev
https://golang.org/cl/5342057

src/pkg/exp/ssh/Makefile
src/pkg/exp/ssh/cipher.go [new file with mode: 0644]
src/pkg/exp/ssh/cipher_test.go [new file with mode: 0644]
src/pkg/exp/ssh/client.go
src/pkg/exp/ssh/common.go
src/pkg/exp/ssh/messages.go
src/pkg/exp/ssh/server.go
src/pkg/exp/ssh/transport.go

index 5c288320fb8eb94e7f7438849c11934f8832a4c3..1b75d5aacda11c9e1fd0d0795c7da286542cdd46 100644 (file)
@@ -7,6 +7,7 @@ include ../../../Make.inc
 TARG=exp/ssh
 GOFILES=\
        channel.go\
+       cipher.go\
        client.go\
        client_auth.go\
        common.go\
diff --git a/src/pkg/exp/ssh/cipher.go b/src/pkg/exp/ssh/cipher.go
new file mode 100644 (file)
index 0000000..de4926d
--- /dev/null
@@ -0,0 +1,88 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package ssh
+
+import (
+       "crypto/aes"
+       "crypto/cipher"
+       "crypto/rc4"
+)
+
+// streamDump is used to dump the initial keystream for stream ciphers. It is a
+// a write-only buffer, and not intended for reading so do not require a mutex.
+var streamDump [512]byte
+
+// noneCipher implements cipher.Stream and provides no encryption. It is used
+// by the transport before the first key-exchange.
+type noneCipher struct{}
+
+func (c noneCipher) XORKeyStream(dst, src []byte) {
+       copy(dst, src)
+}
+
+func newAESCTR(key, iv []byte) (cipher.Stream, error) {
+       c, err := aes.NewCipher(key)
+       if err != nil {
+               return nil, err
+       }
+       return cipher.NewCTR(c, iv), nil
+}
+
+func newRC4(key, iv []byte) (cipher.Stream, error) {
+       return rc4.NewCipher(key)
+}
+
+type cipherMode struct {
+       keySize  int
+       ivSize   int
+       skip     int
+       createFn func(key, iv []byte) (cipher.Stream, error)
+}
+
+func (c *cipherMode) createCipher(key, iv []byte) (cipher.Stream, error) {
+       if len(key) < c.keySize {
+               panic("ssh: key length too small for cipher")
+       }
+       if len(iv) < c.ivSize {
+               panic("ssh: iv too small for cipher")
+       }
+
+       stream, err := c.createFn(key[:c.keySize], iv[:c.ivSize])
+       if err != nil {
+               return nil, err
+       }
+
+       for remainingToDump := c.skip; remainingToDump > 0; {
+               dumpThisTime := remainingToDump
+               if dumpThisTime > len(streamDump) {
+                       dumpThisTime = len(streamDump)
+               }
+               stream.XORKeyStream(streamDump[:dumpThisTime], streamDump[:dumpThisTime])
+               remainingToDump -= dumpThisTime
+       }
+
+       return stream, nil
+}
+
+// Specifies a default set of ciphers and a preference order. This is based on
+// OpenSSH's default client preference order, minus algorithms that are not
+// implemented.
+var DefaultCipherOrder = []string{
+       "aes128-ctr", "aes192-ctr", "aes256-ctr",
+       "arcfour256", "arcfour128",
+}
+
+var cipherModes = map[string]*cipherMode{
+       // Ciphers from RFC4344, which introduced many CTR-based ciphers. Algorithms
+       // are defined in the order specified in the RFC.
+       "aes128-ctr": &cipherMode{16, aes.BlockSize, 0, newAESCTR},
+       "aes192-ctr": &cipherMode{24, aes.BlockSize, 0, newAESCTR},
+       "aes256-ctr": &cipherMode{32, aes.BlockSize, 0, newAESCTR},
+
+       // Ciphers from RFC4345, which introduces security-improved arcfour ciphers.
+       // They are defined in the order specified in the RFC.
+       "arcfour128": &cipherMode{16, 0, 1536, newRC4},
+       "arcfour256": &cipherMode{32, 0, 1536, newRC4},
+}
diff --git a/src/pkg/exp/ssh/cipher_test.go b/src/pkg/exp/ssh/cipher_test.go
new file mode 100644 (file)
index 0000000..ea27bd8
--- /dev/null
@@ -0,0 +1,62 @@
+// Copyright 2011 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package ssh
+
+import (
+       "bytes"
+       "testing"
+)
+
+// TestCipherReversal tests that each cipher factory produces ciphers that can
+// encrypt and decrypt some data successfully.
+func TestCipherReversal(t *testing.T) {
+       testData := []byte("abcdefghijklmnopqrstuvwxyz012345")
+       testKey := []byte("AbCdEfGhIjKlMnOpQrStUvWxYz012345")
+       testIv := []byte("sdflkjhsadflkjhasdflkjhsadfklhsa")
+
+       cryptBuffer := make([]byte, 32)
+
+       for name, cipherMode := range cipherModes {
+               encrypter, err := cipherMode.createCipher(testKey, testIv)
+               if err != nil {
+                       t.Errorf("failed to create encrypter for %q: %s", name, err)
+                       continue
+               }
+               decrypter, err := cipherMode.createCipher(testKey, testIv)
+               if err != nil {
+                       t.Errorf("failed to create decrypter for %q: %s", name, err)
+                       continue
+               }
+
+               copy(cryptBuffer, testData)
+
+               encrypter.XORKeyStream(cryptBuffer, cryptBuffer)
+               if name == "none" {
+                       if !bytes.Equal(cryptBuffer, testData) {
+                               t.Errorf("encryption made change with 'none' cipher")
+                               continue
+                       }
+               } else {
+                       if bytes.Equal(cryptBuffer, testData) {
+                               t.Errorf("encryption made no change with %q", name)
+                               continue
+                       }
+               }
+
+               decrypter.XORKeyStream(cryptBuffer, cryptBuffer)
+               if !bytes.Equal(cryptBuffer, testData) {
+                       t.Errorf("decrypted bytes not equal to input with %q", name)
+                       continue
+               }
+       }
+}
+
+func TestDefaultCiphersExist(t *testing.T) {
+       for _, cipherAlgo := range DefaultCipherOrder {
+               if _, ok := cipherModes[cipherAlgo]; !ok {
+                       t.Errorf("default cipher %q is unknown", cipherAlgo)
+               }
+       }
+}
index 0ea48437b6ad1517d9b6c20153a64d2e4cf64ea1..24569ad9389bd65d6c026a208606660ecd75d032 100644 (file)
@@ -60,8 +60,8 @@ func (c *ClientConn) handshake() error {
        clientKexInit := kexInitMsg{
                KexAlgos:                supportedKexAlgos,
                ServerHostKeyAlgos:      supportedHostKeyAlgos,
-               CiphersClientServer:     supportedCiphers,
-               CiphersServerClient:     supportedCiphers,
+               CiphersClientServer:     c.config.Crypto.ciphers(),
+               CiphersServerClient:     c.config.Crypto.ciphers(),
                MACsClientServer:        supportedMACs,
                MACsServerClient:        supportedMACs,
                CompressionClientServer: supportedCompressions,
@@ -301,6 +301,9 @@ type ClientConfig struct {
        // A slice of ClientAuth methods. Only the first instance 
        // of a particular RFC 4252 method will be used during authentication.
        Auth []ClientAuth
+
+       // Cryptographic-related configuration.
+       Crypto CryptoConfig
 }
 
 func (c *ClientConfig) rand() io.Reader {
index cc720558fc4e5297bff2b1e5843593856bb13ae9..01c55219d47578f2133c43663b5052eb762b503c 100644 (file)
@@ -16,7 +16,6 @@ import (
 const (
        kexAlgoDH14SHA1 = "diffie-hellman-group14-sha1"
        hostAlgoRSA     = "ssh-rsa"
-       cipherAES128CTR = "aes128-ctr"
        macSHA196       = "hmac-sha1-96"
        compressionNone = "none"
        serviceUserAuth = "ssh-userauth"
@@ -25,7 +24,6 @@ const (
 
 var supportedKexAlgos = []string{kexAlgoDH14SHA1}
 var supportedHostKeyAlgos = []string{hostAlgoRSA}
-var supportedCiphers = []string{cipherAES128CTR}
 var supportedMACs = []string{macSHA196}
 var supportedCompressions = []string{compressionNone}
 
@@ -130,6 +128,20 @@ func findAgreedAlgorithms(transport *transport, clientKexInit, serverKexInit *ke
        return
 }
 
+// Cryptographic configuration common to both ServerConfig and ClientConfig.
+type CryptoConfig struct {
+       // The allowed cipher algorithms. If unspecified then DefaultCipherOrder is
+       // used.
+       Ciphers []string
+}
+
+func (c *CryptoConfig) ciphers() []string {
+       if c.Ciphers == nil {
+               return DefaultCipherOrder
+       }
+       return c.Ciphers
+}
+
 // serialize a signed slice according to RFC 4254 6.6.
 func serializeSignature(algoname string, sig []byte) []byte {
        length := stringLength([]byte(algoname))
index 169a8bf6b811c3261ba327d113b938eaa127d374..cebb5609db38711e01f8992923e126b4bb9374f5 100644 (file)
@@ -448,8 +448,6 @@ func parseUint32(in []byte) (out uint32, rest []byte, ok bool) {
        return
 }
 
-const maxPacketSize = 36000
-
 func nameListLength(namelist []string) int {
        length := 4 /* uint32 length prefix */
        for i, name := range namelist {
index 55dd5b0e0290ae46a631b19c26ad1053bdb3076f..428a747e1e0c89b424d0349d5f6ccde744d2c503 100644 (file)
@@ -40,6 +40,9 @@ type ServerConfig struct {
        // key authentication. It must return true iff the given public key is
        // valid for the given user.
        PubKeyCallback func(user, algo string, pubkey []byte) bool
+
+       // Cryptographic-related configuration.
+       Crypto CryptoConfig
 }
 
 func (c *ServerConfig) rand() io.Reader {
@@ -257,8 +260,8 @@ func (s *ServerConn) Handshake() error {
        serverKexInit := kexInitMsg{
                KexAlgos:                supportedKexAlgos,
                ServerHostKeyAlgos:      supportedHostKeyAlgos,
-               CiphersClientServer:     supportedCiphers,
-               CiphersServerClient:     supportedCiphers,
+               CiphersClientServer:     s.config.Crypto.ciphers(),
+               CiphersServerClient:     s.config.Crypto.ciphers(),
                MACsClientServer:        supportedMACs,
                MACsServerClient:        supportedMACs,
                CompressionClientServer: supportedCompressions,
@@ -323,7 +326,9 @@ func (s *ServerConn) Handshake() error {
        if packet[0] != msgNewKeys {
                return UnexpectedMessageError{msgNewKeys, packet[0]}
        }
-       s.transport.reader.setupKeys(clientKeys, K, H, H, hashFunc)
+       if err = s.transport.reader.setupKeys(clientKeys, K, H, H, hashFunc); err != nil {
+               return err
+       }
        if packet, err = s.readPacket(); err != nil {
                return err
        }
index 579a9d82de914604a95e918d098a3d9e9fe8c18c..b8cb2c319d85e119495cee900a81993a4a5ffc73 100644 (file)
@@ -7,7 +7,6 @@ package ssh
 import (
        "bufio"
        "crypto"
-       "crypto/aes"
        "crypto/cipher"
        "crypto/hmac"
        "crypto/subtle"
@@ -19,7 +18,10 @@ import (
 )
 
 const (
-       paddingMultiple = 16 // TODO(dfc) does this need to be configurable?
+       packetSizeMultiple = 16 // TODO(huin) this should be determined by the cipher.
+       minPacketSize      = 16
+       maxPacketSize      = 36000
+       minPaddingSize     = 4 // TODO(huin) should this be configurable?
 )
 
 // filteredConn reduces the set of methods exposed when embeddeding
@@ -61,8 +63,7 @@ type reader struct {
 type writer struct {
        *sync.Mutex // protects writer.Writer from concurrent writes
        *bufio.Writer
-       paddingMultiple int
-       rand            io.Reader
+       rand io.Reader
        common
 }
 
@@ -82,14 +83,11 @@ type common struct {
 func (r *reader) readOnePacket() ([]byte, error) {
        var lengthBytes = make([]byte, 5)
        var macSize uint32
-
        if _, err := io.ReadFull(r, lengthBytes); err != nil {
                return nil, err
        }
 
-       if r.cipher != nil {
-               r.cipher.XORKeyStream(lengthBytes, lengthBytes)
-       }
+       r.cipher.XORKeyStream(lengthBytes, lengthBytes)
 
        if r.mac != nil {
                r.mac.Reset()
@@ -153,9 +151,9 @@ func (w *writer) writePacket(packet []byte) error {
        w.Mutex.Lock()
        defer w.Mutex.Unlock()
 
-       paddingLength := paddingMultiple - (5+len(packet))%paddingMultiple
+       paddingLength := packetSizeMultiple - (5+len(packet))%packetSizeMultiple
        if paddingLength < 4 {
-               paddingLength += paddingMultiple
+               paddingLength += packetSizeMultiple
        }
 
        length := len(packet) + 1 + paddingLength
@@ -188,11 +186,9 @@ func (w *writer) writePacket(packet []byte) error {
 
        // TODO(dfc) lengthBytes, packet and padding should be
        // subslices of a single buffer
-       if w.cipher != nil {
-               w.cipher.XORKeyStream(lengthBytes, lengthBytes)
-               w.cipher.XORKeyStream(packet, packet)
-               w.cipher.XORKeyStream(padding, padding)
-       }
+       w.cipher.XORKeyStream(lengthBytes, lengthBytes)
+       w.cipher.XORKeyStream(packet, packet)
+       w.cipher.XORKeyStream(padding, padding)
 
        if _, err := w.Write(lengthBytes); err != nil {
                return err
@@ -227,11 +223,17 @@ func newTransport(conn net.Conn, rand io.Reader) *transport {
        return &transport{
                reader: reader{
                        Reader: bufio.NewReader(conn),
+                       common: common{
+                               cipher: noneCipher{},
+                       },
                },
                writer: writer{
                        Writer: bufio.NewWriter(conn),
                        rand:   rand,
                        Mutex:  new(sync.Mutex),
+                       common: common{
+                               cipher: noneCipher{},
+                       },
                },
                filteredConn: conn,
        }
@@ -249,29 +251,32 @@ var (
        clientKeys = direction{[]byte{'A'}, []byte{'C'}, []byte{'E'}}
 )
 
-// setupKeys sets the cipher and MAC keys from K, H and sessionId, as
+// setupKeys sets the cipher and MAC keys from kex.K, kex.H and sessionId, as
 // described in RFC 4253, section 6.4. direction should either be serverKeys
 // (to setup server->client keys) or clientKeys (for client->server keys).
 func (c *common) setupKeys(d direction, K, H, sessionId []byte, hashFunc crypto.Hash) error {
-       h := hashFunc.New()
+       cipherMode := cipherModes[c.cipherAlgo]
 
-       blockSize := 16
-       keySize := 16
        macKeySize := 20
 
-       iv := make([]byte, blockSize)
-       key := make([]byte, keySize)
+       iv := make([]byte, cipherMode.ivSize)
+       key := make([]byte, cipherMode.keySize)
        macKey := make([]byte, macKeySize)
+
+       h := hashFunc.New()
        generateKeyMaterial(iv, d.ivTag, K, H, sessionId, h)
        generateKeyMaterial(key, d.keyTag, K, H, sessionId, h)
        generateKeyMaterial(macKey, d.macKeyTag, K, H, sessionId, h)
 
        c.mac = truncatingMAC{12, hmac.NewSHA1(macKey)}
-       aes, err := aes.NewCipher(key)
+
+       cipher, err := cipherMode.createCipher(key, iv)
        if err != nil {
                return err
        }
-       c.cipher = cipher.NewCTR(aes, iv)
+
+       c.cipher = cipher
+
        return nil
 }