crypto/ssh: add support for aes128-cbc cipher. The aes128cbc cipher is commented out in cipher.go on purpose, anyone wants to use the cipher needs to uncomment line 119 in cipher.go Fixes #4274. Change-Id: I4bbc88ab884bda821c5f155dcf495bb7235c8605 Reviewed-on: https://go-review.googlesource.com/8396 Reviewed-by: Adam Langley <agl@golang.org>
diff --git a/ssh/cipher.go b/ssh/cipher.go index 642696b..b173183 100644 --- a/ssh/cipher.go +++ b/ssh/cipher.go
@@ -113,6 +113,10 @@ // special case. If we add any more non-stream ciphers, we // should invest a cleaner way to do this. gcmCipherID: {16, 12, 0, nil}, + + // insecure cipher, see http://www.isg.rhul.ac.uk/~kp/SandPfinal.pdf + // uncomment below to enable it. + // aes128cbcID: {16, aes.BlockSize, 0, nil}, } // prefixLen is the length of the packet prefix that contains the packet length @@ -342,3 +346,178 @@ plain = plain[1 : length-uint32(padding)] return plain, nil } + +// cbcCipher implements aes128-cbc cipher defined in RFC 4253 section 6.1 +type cbcCipher struct { + mac hash.Hash + decrypter cipher.BlockMode + encrypter cipher.BlockMode + + // The following members are to avoid per-packet allocations. + seqNumBytes [4]byte + packetData []byte + macResult []byte +} + +func newAESCBCCipher(iv, key, macKey []byte, algs directionAlgorithms) (packetCipher, error) { + c, err := aes.NewCipher(key) + if err != nil { + return nil, err + } + return &cbcCipher{ + mac: macModes[algs.MAC].new(macKey), + decrypter: cipher.NewCBCDecrypter(c, iv), + encrypter: cipher.NewCBCEncrypter(c, iv), + packetData: make([]byte, 1024), + }, nil +} + +func maxUInt32(a, b int) uint32 { + if a > b { + return uint32(a) + } + return uint32(b) +} + +const ( + cbcMinPacketSizeMultiple = 8 + cbcMinPacketSize = 16 + cbcMinPaddingSize = 4 +) + +func (c *cbcCipher) readPacket(seqNum uint32, r io.Reader) ([]byte, error) { + blockSize := c.decrypter.BlockSize() + + // Read the header, which will include some of the subsequent data in the + // case of block ciphers - this is copied back to the payload later. + // How many bytes of payload/padding will be read with this first read. + firstBlockLength := (prefixLen + blockSize - 1) / blockSize * blockSize + firstBlock := c.packetData[:firstBlockLength] + if _, err := io.ReadFull(r, firstBlock); err != nil { + return nil, err + } + + c.decrypter.CryptBlocks(firstBlock, firstBlock) + length := binary.BigEndian.Uint32(firstBlock[:4]) + if length > maxPacket { + return nil, errors.New("ssh: packet too large") + } + if length+4 < maxUInt32(cbcMinPacketSize, blockSize) { + // The minimum size of a packet is 16 (or the cipher block size, whichever + // is larger) bytes. + return nil, errors.New("ssh: packet too small") + } + // The length of the packet (including the length field but not the MAC) must + // be a multiple of the block size or 8, whichever is larger. + if (length+4)%maxUInt32(cbcMinPacketSizeMultiple, blockSize) != 0 { + return nil, errors.New("ssh: invalid packet length multiple") + } + + paddingLength := uint32(firstBlock[4]) + if paddingLength < cbcMinPaddingSize || length <= paddingLength+1 { + return nil, errors.New("ssh: invalid packet length") + } + + var macSize uint32 + if c.mac != nil { + macSize = uint32(c.mac.Size()) + } + + // Positions within the c.packetData buffer: + macStart := 4 + length + paddingStart := macStart - paddingLength + + // Entire packet size, starting before length, ending at end of mac. + entirePacketSize := macStart + macSize + + // Ensure c.packetData is large enough for the entire packet data. + if uint32(cap(c.packetData)) < entirePacketSize { + // Still need to upsize and copy, but this should be rare at runtime, only + // on upsizing the packetData buffer. + c.packetData = make([]byte, entirePacketSize) + copy(c.packetData, firstBlock) + } else { + c.packetData = c.packetData[:entirePacketSize] + } + + if _, err := io.ReadFull(r, c.packetData[firstBlockLength:]); err != nil { + return nil, err + } + + remainingCrypted := c.packetData[firstBlockLength:macStart] + c.decrypter.CryptBlocks(remainingCrypted, remainingCrypted) + + mac := c.packetData[macStart:] + if c.mac != nil { + c.mac.Reset() + binary.BigEndian.PutUint32(c.seqNumBytes[:], seqNum) + c.mac.Write(c.seqNumBytes[:]) + c.mac.Write(c.packetData[:macStart]) + c.macResult = c.mac.Sum(c.macResult[:0]) + if subtle.ConstantTimeCompare(c.macResult, mac) != 1 { + return nil, errors.New("ssh: MAC failure") + } + } + + return c.packetData[prefixLen:paddingStart], nil +} + +func (c *cbcCipher) writePacket(seqNum uint32, w io.Writer, rand io.Reader, packet []byte) error { + effectiveBlockSize := maxUInt32(cbcMinPacketSizeMultiple, c.encrypter.BlockSize()) + + // Length of encrypted portion of the packet (header, payload, padding). + // Enforce minimum padding and packet size. + encLength := maxUInt32(prefixLen+len(packet)+cbcMinPaddingSize, cbcMinPaddingSize) + // Enforce block size. + encLength = (encLength + effectiveBlockSize - 1) / effectiveBlockSize * effectiveBlockSize + + length := encLength - 4 + paddingLength := int(length) - (1 + len(packet)) + + var macSize uint32 + if c.mac != nil { + macSize = uint32(c.mac.Size()) + } + // Overall buffer contains: header, payload, padding, mac. + // Space for the MAC is reserved in the capacity but not the slice length. + bufferSize := encLength + macSize + if uint32(cap(c.packetData)) < bufferSize { + c.packetData = make([]byte, encLength, bufferSize) + } else { + c.packetData = c.packetData[:encLength] + } + + p := c.packetData + + // Packet header. + binary.BigEndian.PutUint32(p, length) + p = p[4:] + p[0] = byte(paddingLength) + + // Payload. + p = p[1:] + copy(p, packet) + + // Padding. + p = p[len(packet):] + if _, err := io.ReadFull(rand, p); err != nil { + return err + } + + if c.mac != nil { + c.mac.Reset() + binary.BigEndian.PutUint32(c.seqNumBytes[:], seqNum) + c.mac.Write(c.seqNumBytes[:]) + c.mac.Write(c.packetData) + // The MAC is now appended into the capacity reserved for it earlier. + c.packetData = c.mac.Sum(c.packetData) + } + + c.encrypter.CryptBlocks(c.packetData[:encLength], c.packetData[:encLength]) + + if _, err := w.Write(c.packetData); err != nil { + return err + } + + return nil +}
diff --git a/ssh/cipher_test.go b/ssh/cipher_test.go index e279af0..2fb75d0 100644 --- a/ssh/cipher_test.go +++ b/ssh/cipher_test.go
@@ -7,6 +7,7 @@ import ( "bytes" "crypto" + "crypto/aes" "crypto/rand" "testing" ) @@ -20,6 +21,10 @@ } func TestPacketCiphers(t *testing.T) { + // Still test aes128cbc cipher althought it's commented out. + cipherModes[aes128cbcID] = &streamCipherMode{16, aes.BlockSize, 0, nil} + defer delete(cipherModes, aes128cbcID) + for cipher := range cipherModes { kr := &kexResult{Hash: crypto.SHA1} algs := directionAlgorithms{
diff --git a/ssh/common.go b/ssh/common.go index 2fd7fd9..b03a3df 100644 --- a/ssh/common.go +++ b/ssh/common.go
@@ -206,6 +206,14 @@ if c.Ciphers == nil { c.Ciphers = supportedCiphers } + var ciphers []string + for _, c := range c.Ciphers { + if cipherModes[c] != nil { + // reject the cipher if we have no cipherModes definition + ciphers = append(ciphers, c) + } + } + c.Ciphers = ciphers if c.KeyExchanges == nil { c.KeyExchanges = supportedKexAlgos
diff --git a/ssh/test/session_test.go b/ssh/test/session_test.go index 846f580..fbd1044 100644 --- a/ssh/test/session_test.go +++ b/ssh/test/session_test.go
@@ -280,6 +280,9 @@ var config ssh.Config config.SetDefaults() cipherOrder := config.Ciphers + // This cipher will not be tested when commented out in cipher.go it will + // fallback to the next available as per line 292. + cipherOrder = append(cipherOrder, "aes128-cbc") for _, ciph := range cipherOrder { server := newServer(t)
diff --git a/ssh/transport.go b/ssh/transport.go index 0f19478..8351d37 100644 --- a/ssh/transport.go +++ b/ssh/transport.go
@@ -12,6 +12,7 @@ const ( gcmCipherID = "aes128-gcm@openssh.com" + aes128cbcID = "aes128-cbc" ) // packetConn represents a transport that implements packet based @@ -218,6 +219,10 @@ return newGCMCipher(iv, key, macKey) } + if algs.Cipher == aes128cbcID { + return newAESCBCCipher(iv, key, macKey, algs) + } + c := &streamPacketCipher{ mac: macModes[algs.MAC].new(macKey), }