Skip to content

Commit 5c68cfd

Browse files
nathandfoxagl
authored andcommitted
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 <[email protected]>
1 parent c57d4a7 commit 5c68cfd

File tree

5 files changed

+200
-0
lines changed

5 files changed

+200
-0
lines changed

ssh/cipher.go

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,10 @@ var cipherModes = map[string]*streamCipherMode{
113113
// special case. If we add any more non-stream ciphers, we
114114
// should invest a cleaner way to do this.
115115
gcmCipherID: {16, 12, 0, nil},
116+
117+
// insecure cipher, see http://www.isg.rhul.ac.uk/~kp/SandPfinal.pdf
118+
// uncomment below to enable it.
119+
// aes128cbcID: {16, aes.BlockSize, 0, nil},
116120
}
117121

118122
// prefixLen is the length of the packet prefix that contains the packet length
@@ -342,3 +346,178 @@ func (c *gcmCipher) readPacket(seqNum uint32, r io.Reader) ([]byte, error) {
342346
plain = plain[1 : length-uint32(padding)]
343347
return plain, nil
344348
}
349+
350+
// cbcCipher implements aes128-cbc cipher defined in RFC 4253 section 6.1
351+
type cbcCipher struct {
352+
mac hash.Hash
353+
decrypter cipher.BlockMode
354+
encrypter cipher.BlockMode
355+
356+
// The following members are to avoid per-packet allocations.
357+
seqNumBytes [4]byte
358+
packetData []byte
359+
macResult []byte
360+
}
361+
362+
func newAESCBCCipher(iv, key, macKey []byte, algs directionAlgorithms) (packetCipher, error) {
363+
c, err := aes.NewCipher(key)
364+
if err != nil {
365+
return nil, err
366+
}
367+
return &cbcCipher{
368+
mac: macModes[algs.MAC].new(macKey),
369+
decrypter: cipher.NewCBCDecrypter(c, iv),
370+
encrypter: cipher.NewCBCEncrypter(c, iv),
371+
packetData: make([]byte, 1024),
372+
}, nil
373+
}
374+
375+
func maxUInt32(a, b int) uint32 {
376+
if a > b {
377+
return uint32(a)
378+
}
379+
return uint32(b)
380+
}
381+
382+
const (
383+
cbcMinPacketSizeMultiple = 8
384+
cbcMinPacketSize = 16
385+
cbcMinPaddingSize = 4
386+
)
387+
388+
func (c *cbcCipher) readPacket(seqNum uint32, r io.Reader) ([]byte, error) {
389+
blockSize := c.decrypter.BlockSize()
390+
391+
// Read the header, which will include some of the subsequent data in the
392+
// case of block ciphers - this is copied back to the payload later.
393+
// How many bytes of payload/padding will be read with this first read.
394+
firstBlockLength := (prefixLen + blockSize - 1) / blockSize * blockSize
395+
firstBlock := c.packetData[:firstBlockLength]
396+
if _, err := io.ReadFull(r, firstBlock); err != nil {
397+
return nil, err
398+
}
399+
400+
c.decrypter.CryptBlocks(firstBlock, firstBlock)
401+
length := binary.BigEndian.Uint32(firstBlock[:4])
402+
if length > maxPacket {
403+
return nil, errors.New("ssh: packet too large")
404+
}
405+
if length+4 < maxUInt32(cbcMinPacketSize, blockSize) {
406+
// The minimum size of a packet is 16 (or the cipher block size, whichever
407+
// is larger) bytes.
408+
return nil, errors.New("ssh: packet too small")
409+
}
410+
// The length of the packet (including the length field but not the MAC) must
411+
// be a multiple of the block size or 8, whichever is larger.
412+
if (length+4)%maxUInt32(cbcMinPacketSizeMultiple, blockSize) != 0 {
413+
return nil, errors.New("ssh: invalid packet length multiple")
414+
}
415+
416+
paddingLength := uint32(firstBlock[4])
417+
if paddingLength < cbcMinPaddingSize || length <= paddingLength+1 {
418+
return nil, errors.New("ssh: invalid packet length")
419+
}
420+
421+
var macSize uint32
422+
if c.mac != nil {
423+
macSize = uint32(c.mac.Size())
424+
}
425+
426+
// Positions within the c.packetData buffer:
427+
macStart := 4 + length
428+
paddingStart := macStart - paddingLength
429+
430+
// Entire packet size, starting before length, ending at end of mac.
431+
entirePacketSize := macStart + macSize
432+
433+
// Ensure c.packetData is large enough for the entire packet data.
434+
if uint32(cap(c.packetData)) < entirePacketSize {
435+
// Still need to upsize and copy, but this should be rare at runtime, only
436+
// on upsizing the packetData buffer.
437+
c.packetData = make([]byte, entirePacketSize)
438+
copy(c.packetData, firstBlock)
439+
} else {
440+
c.packetData = c.packetData[:entirePacketSize]
441+
}
442+
443+
if _, err := io.ReadFull(r, c.packetData[firstBlockLength:]); err != nil {
444+
return nil, err
445+
}
446+
447+
remainingCrypted := c.packetData[firstBlockLength:macStart]
448+
c.decrypter.CryptBlocks(remainingCrypted, remainingCrypted)
449+
450+
mac := c.packetData[macStart:]
451+
if c.mac != nil {
452+
c.mac.Reset()
453+
binary.BigEndian.PutUint32(c.seqNumBytes[:], seqNum)
454+
c.mac.Write(c.seqNumBytes[:])
455+
c.mac.Write(c.packetData[:macStart])
456+
c.macResult = c.mac.Sum(c.macResult[:0])
457+
if subtle.ConstantTimeCompare(c.macResult, mac) != 1 {
458+
return nil, errors.New("ssh: MAC failure")
459+
}
460+
}
461+
462+
return c.packetData[prefixLen:paddingStart], nil
463+
}
464+
465+
func (c *cbcCipher) writePacket(seqNum uint32, w io.Writer, rand io.Reader, packet []byte) error {
466+
effectiveBlockSize := maxUInt32(cbcMinPacketSizeMultiple, c.encrypter.BlockSize())
467+
468+
// Length of encrypted portion of the packet (header, payload, padding).
469+
// Enforce minimum padding and packet size.
470+
encLength := maxUInt32(prefixLen+len(packet)+cbcMinPaddingSize, cbcMinPaddingSize)
471+
// Enforce block size.
472+
encLength = (encLength + effectiveBlockSize - 1) / effectiveBlockSize * effectiveBlockSize
473+
474+
length := encLength - 4
475+
paddingLength := int(length) - (1 + len(packet))
476+
477+
var macSize uint32
478+
if c.mac != nil {
479+
macSize = uint32(c.mac.Size())
480+
}
481+
// Overall buffer contains: header, payload, padding, mac.
482+
// Space for the MAC is reserved in the capacity but not the slice length.
483+
bufferSize := encLength + macSize
484+
if uint32(cap(c.packetData)) < bufferSize {
485+
c.packetData = make([]byte, encLength, bufferSize)
486+
} else {
487+
c.packetData = c.packetData[:encLength]
488+
}
489+
490+
p := c.packetData
491+
492+
// Packet header.
493+
binary.BigEndian.PutUint32(p, length)
494+
p = p[4:]
495+
p[0] = byte(paddingLength)
496+
497+
// Payload.
498+
p = p[1:]
499+
copy(p, packet)
500+
501+
// Padding.
502+
p = p[len(packet):]
503+
if _, err := io.ReadFull(rand, p); err != nil {
504+
return err
505+
}
506+
507+
if c.mac != nil {
508+
c.mac.Reset()
509+
binary.BigEndian.PutUint32(c.seqNumBytes[:], seqNum)
510+
c.mac.Write(c.seqNumBytes[:])
511+
c.mac.Write(c.packetData)
512+
// The MAC is now appended into the capacity reserved for it earlier.
513+
c.packetData = c.mac.Sum(c.packetData)
514+
}
515+
516+
c.encrypter.CryptBlocks(c.packetData[:encLength], c.packetData[:encLength])
517+
518+
if _, err := w.Write(c.packetData); err != nil {
519+
return err
520+
}
521+
522+
return nil
523+
}

ssh/cipher_test.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package ssh
77
import (
88
"bytes"
99
"crypto"
10+
"crypto/aes"
1011
"crypto/rand"
1112
"testing"
1213
)
@@ -20,6 +21,10 @@ func TestDefaultCiphersExist(t *testing.T) {
2021
}
2122

2223
func TestPacketCiphers(t *testing.T) {
24+
// Still test aes128cbc cipher althought it's commented out.
25+
cipherModes[aes128cbcID] = &streamCipherMode{16, aes.BlockSize, 0, nil}
26+
defer delete(cipherModes, aes128cbcID)
27+
2328
for cipher := range cipherModes {
2429
kr := &kexResult{Hash: crypto.SHA1}
2530
algs := directionAlgorithms{

ssh/common.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,14 @@ func (c *Config) SetDefaults() {
206206
if c.Ciphers == nil {
207207
c.Ciphers = supportedCiphers
208208
}
209+
var ciphers []string
210+
for _, c := range c.Ciphers {
211+
if cipherModes[c] != nil {
212+
// reject the cipher if we have no cipherModes definition
213+
ciphers = append(ciphers, c)
214+
}
215+
}
216+
c.Ciphers = ciphers
209217

210218
if c.KeyExchanges == nil {
211219
c.KeyExchanges = supportedKexAlgos

ssh/test/session_test.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -280,6 +280,9 @@ func TestCiphers(t *testing.T) {
280280
var config ssh.Config
281281
config.SetDefaults()
282282
cipherOrder := config.Ciphers
283+
// This cipher will not be tested when commented out in cipher.go it will
284+
// fallback to the next available as per line 292.
285+
cipherOrder = append(cipherOrder, "aes128-cbc")
283286

284287
for _, ciph := range cipherOrder {
285288
server := newServer(t)

ssh/transport.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212

1313
const (
1414
gcmCipherID = "[email protected]"
15+
aes128cbcID = "aes128-cbc"
1516
)
1617

1718
// packetConn represents a transport that implements packet based
@@ -218,6 +219,10 @@ func newPacketCipher(d direction, algs directionAlgorithms, kex *kexResult) (pac
218219
return newGCMCipher(iv, key, macKey)
219220
}
220221

222+
if algs.Cipher == aes128cbcID {
223+
return newAESCBCCipher(iv, key, macKey, algs)
224+
}
225+
221226
c := &streamPacketCipher{
222227
mac: macModes[algs.MAC].new(macKey),
223228
}

0 commit comments

Comments
 (0)