From ee41a25c63fb5b74abf2213abb6dee3751e6ac4a Mon Sep 17 00:00:00 2001 From: Han-Wen Nienhuys Date: Wed, 10 Jan 2018 12:42:30 +0100 Subject: [PATCH] ssh: support chacha20-poly1305 cipher chacha20-poly1305 is an AEAD which performs well without hardware support. It is recommended as a replacement for the aging arcfour128 and arcfour256 ciphers. Fixes golang/go#9489 Change-Id: I5d5a4620a435e65997f0ba7e683a34c29d9a396b Reviewed-on: https://go-review.googlesource.com/87077 Run-TryBot: Han-Wen Nienhuys TryBot-Result: Gobot Gobot Reviewed-by: Adam Langley Reviewed-by: Brad Fitzpatrick --- ssh/cipher.go | 150 +++++++++++++++++++++++++++++++++++++++++++++-- ssh/common.go | 1 + ssh/transport.go | 13 ++-- 3 files changed, 153 insertions(+), 11 deletions(-) diff --git a/ssh/cipher.go b/ssh/cipher.go index e67c5e0..9944139 100644 --- a/ssh/cipher.go +++ b/ssh/cipher.go @@ -16,6 +16,9 @@ import ( "hash" "io" "io/ioutil" + + "golang.org/x/crypto/internal/chacha20" + "golang.org/x/crypto/poly1305" ) const ( @@ -111,10 +114,10 @@ var cipherModes = map[string]*streamCipherMode{ // RFC4345 introduces improved versions of Arcfour. "arcfour": {16, 0, 0, newRC4}, - // AES-GCM is not a stream cipher, so it is constructed with a - // special case. If we add any more non-stream ciphers, we - // should invest a cleaner way to do this. - gcmCipherID: {16, 12, 0, nil}, + // AEAD ciphers are special cased. If we add any more non-stream + // ciphers, we should create a cleaner way to do this. + gcmCipherID: {16, 12, 0, nil}, + chacha20Poly1305ID: {64, 0, 0, nil}, // CBC mode is insecure and so is not included in the default config. // (See http://www.isg.rhul.ac.uk/~kp/SandPfinal.pdf). If absolutely @@ -627,3 +630,142 @@ func (c *cbcCipher) writePacket(seqNum uint32, w io.Writer, rand io.Reader, pack return nil } + +const chacha20Poly1305ID = "chacha20-poly1305@openssh.com" + +// chacha20Poly1305Cipher implements the chacha20-poly1305@openssh.com +// AEAD, which is described here: +// +// https://tools.ietf.org/html/draft-josefsson-ssh-chacha20-poly1305-openssh-00 +// +// the methods here also implement padding, which RFC4253 Section 6 +// also requires of stream ciphers. +type chacha20Poly1305Cipher struct { + lengthKey [32]byte + contentKey [32]byte + buf []byte +} + +func newChaCha20Cipher(key []byte) (packetCipher, error) { + if len(key) != 64 { + panic("key length") + } + + c := &chacha20Poly1305Cipher{ + buf: make([]byte, 256), + } + + copy(c.contentKey[:], key[:32]) + copy(c.lengthKey[:], key[32:]) + return c, nil +} + +// The Poly1305 key is obtained by encrypting 32 0-bytes. +var chacha20PolyKeyInput [32]byte + +func (c *chacha20Poly1305Cipher) readPacket(seqNum uint32, r io.Reader) ([]byte, error) { + var counter [16]byte + binary.BigEndian.PutUint64(counter[8:], uint64(seqNum)) + + var polyKey [32]byte + chacha20.XORKeyStream(polyKey[:], chacha20PolyKeyInput[:], &counter, &c.contentKey) + + encryptedLength := c.buf[:4] + if _, err := r.Read(encryptedLength); err != nil { + return nil, err + } + + var lenBytes [4]byte + chacha20.XORKeyStream(lenBytes[:], encryptedLength, &counter, &c.lengthKey) + + length := binary.BigEndian.Uint32(lenBytes[:]) + if length > maxPacket { + return nil, errors.New("ssh: invalid packet length, packet too large") + } + + contentEnd := 4 + length + packetEnd := contentEnd + poly1305.TagSize + if uint32(cap(c.buf)) < packetEnd { + c.buf = make([]byte, packetEnd) + copy(c.buf[:], encryptedLength) + } else { + c.buf = c.buf[:packetEnd] + } + + if _, err := r.Read(c.buf[4:packetEnd]); err != nil { + return nil, err + } + + var mac [poly1305.TagSize]byte + copy(mac[:], c.buf[contentEnd:packetEnd]) + + if !poly1305.Verify(&mac, c.buf[:contentEnd], &polyKey) { + return nil, errors.New("ssh: MAC failure") + } + + counter[0] = 1 + + plain := c.buf[4:contentEnd] + chacha20.XORKeyStream(plain, plain, &counter, &c.contentKey) + + padding := plain[0] + if padding < 4 { + // padding is a byte, so it automatically satisfies + // the maximum size, which is 255. + return nil, fmt.Errorf("ssh: illegal padding %d", padding) + } + + if int(padding)+1 >= len(plain) { + return nil, fmt.Errorf("ssh: padding %d too large", padding) + } + + plain = plain[1 : len(plain)-int(padding)] + return plain, nil +} + +func (c *chacha20Poly1305Cipher) writePacket(seqNum uint32, w io.Writer, rand io.Reader, payload []byte) error { + var counter [16]byte + binary.BigEndian.PutUint64(counter[8:], uint64(seqNum)) + + var polyKey [32]byte + chacha20.XORKeyStream(polyKey[:], chacha20PolyKeyInput[:], &counter, &c.contentKey) + + // There is no blocksize, so fall back to multiple of 8 byte + // padding, as described in RFC 4253, Sec 6. + const packetSizeMultiple = 8 + + padding := packetSizeMultiple - (1+len(payload))%packetSizeMultiple + if padding < 4 { + padding += packetSizeMultiple + } + + // size (4 bytes), padding (1), payload, padding, tag. + totalLength := 4 + 1 + len(payload) + padding + poly1305.TagSize + if cap(c.buf) < totalLength { + c.buf = make([]byte, totalLength) + } else { + c.buf = c.buf[:totalLength] + } + + binary.BigEndian.PutUint32(c.buf, uint32(1+len(payload)+padding)) + chacha20.XORKeyStream(c.buf, c.buf[:4], &counter, &c.lengthKey) + c.buf[4] = byte(padding) + copy(c.buf[5:], payload) + packetEnd := 5 + len(payload) + padding + if _, err := io.ReadFull(rand, c.buf[5+len(payload):packetEnd]); err != nil { + return err + } + + counter[0] = 1 + chacha20.XORKeyStream(c.buf[4:], c.buf[4:packetEnd], &counter, &c.contentKey) + + var mac [poly1305.TagSize]byte + poly1305.Sum(&mac, c.buf[:packetEnd], &polyKey) + + copy(c.buf[packetEnd:], mac[:]) + + if _, err := w.Write(c.buf); err != nil { + return err + } + return nil +} diff --git a/ssh/common.go b/ssh/common.go index 135b4ed..77f9c5b 100644 --- a/ssh/common.go +++ b/ssh/common.go @@ -28,6 +28,7 @@ const ( var supportedCiphers = []string{ "aes128-ctr", "aes192-ctr", "aes256-ctr", "aes128-gcm@openssh.com", + chacha20Poly1305ID, "arcfour256", "arcfour128", } diff --git a/ssh/transport.go b/ssh/transport.go index 82da0d7..5a56753 100644 --- a/ssh/transport.go +++ b/ssh/transport.go @@ -254,15 +254,14 @@ func generateKeys(d direction, algs directionAlgorithms, kex *kexResult) (iv, ke func newPacketCipher(d direction, algs directionAlgorithms, kex *kexResult) (packetCipher, error) { iv, key, macKey := generateKeys(d, algs, kex) - if algs.Cipher == gcmCipherID { + switch algs.Cipher { + case chacha20Poly1305ID: + return newChaCha20Cipher(key) + case gcmCipherID: return newGCMCipher(iv, key) - } - - if algs.Cipher == aes128cbcID { + case aes128cbcID: return newAESCBCCipher(iv, key, macKey, algs) - } - - if algs.Cipher == tripledescbcID { + case tripledescbcID: return newTripleDESCBCCipher(iv, key, macKey, algs) }