diff --git a/ssh/cipher.go b/ssh/cipher.go index 34d3917c4f..7cacd22042 100644 --- a/ssh/cipher.go +++ b/ssh/cipher.go @@ -135,6 +135,7 @@ const prefixLen = 5 type streamPacketCipher struct { mac hash.Hash cipher cipher.Stream + etm bool // The following members are to avoid per-packet allocations. prefix [prefixLen]byte @@ -150,7 +151,15 @@ func (s *streamPacketCipher) readPacket(seqNum uint32, r io.Reader) ([]byte, err return nil, err } - s.cipher.XORKeyStream(s.prefix[:], s.prefix[:]) + var encryptedPaddingLength []byte + if s.mac != nil && s.etm { + encryptedPaddingLength = make([]byte, 1) + copy(encryptedPaddingLength[:], s.prefix[4:5]) + s.cipher.XORKeyStream(s.prefix[4:5], s.prefix[4:5]) + } else { + s.cipher.XORKeyStream(s.prefix[:], s.prefix[:]) + } + length := binary.BigEndian.Uint32(s.prefix[0:4]) paddingLength := uint32(s.prefix[4]) @@ -159,7 +168,13 @@ func (s *streamPacketCipher) readPacket(seqNum uint32, r io.Reader) ([]byte, err s.mac.Reset() binary.BigEndian.PutUint32(s.seqNumBytes[:], seqNum) s.mac.Write(s.seqNumBytes[:]) - s.mac.Write(s.prefix[:]) + if s.etm { + s.mac.Write(s.prefix[:4]) + s.mac.Write(encryptedPaddingLength) + + } else { + s.mac.Write(s.prefix[:]) + } macSize = uint32(s.mac.Size()) } @@ -184,10 +199,17 @@ func (s *streamPacketCipher) readPacket(seqNum uint32, r io.Reader) ([]byte, err } mac := s.packetData[length-1:] data := s.packetData[:length-1] + + if s.mac != nil && s.etm { + s.mac.Write(data) + } + s.cipher.XORKeyStream(data, data) if s.mac != nil { - s.mac.Write(data) + if !s.etm { + s.mac.Write(data) + } s.macResult = s.mac.Sum(s.macResult[:0]) if subtle.ConstantTimeCompare(s.macResult, mac) != 1 { return nil, errors.New("ssh: MAC failure") @@ -203,7 +225,13 @@ func (s *streamPacketCipher) writePacket(seqNum uint32, w io.Writer, rand io.Rea return errors.New("ssh: packet too large") } - paddingLength := packetSizeMultiple - (prefixLen+len(packet))%packetSizeMultiple + aadlen := 0 + if s.mac != nil && s.etm { + // packet length is not encrypted for EtM modes + aadlen = 4 + } + + paddingLength := packetSizeMultiple - (prefixLen+len(packet)-aadlen)%packetSizeMultiple if paddingLength < 4 { paddingLength += packetSizeMultiple } @@ -216,7 +244,14 @@ func (s *streamPacketCipher) writePacket(seqNum uint32, w io.Writer, rand io.Rea return err } - if s.mac != nil { + if s.mac != nil && !s.etm { + // After key exchange, the 'mac' for the selected MAC + // algorithm will be computed before encryption from the concatenation + // of packet data: + // mac = MAC(key, sequence_number || unencrypted_packet) + // where unencrypted_packet is the entire packet without 'mac' (the + // length fields, 'payload' and 'random padding'), and sequence_number + // is an implicit packet sequence number represented as uint32. s.mac.Reset() binary.BigEndian.PutUint32(s.seqNumBytes[:], seqNum) s.mac.Write(s.seqNumBytes[:]) @@ -225,10 +260,36 @@ func (s *streamPacketCipher) writePacket(seqNum uint32, w io.Writer, rand io.Rea s.mac.Write(padding) } - s.cipher.XORKeyStream(s.prefix[:], s.prefix[:]) + if s.mac != nil && s.etm { + // Specifically, the "-etm" MAC algorithms modify the transport protocol + // to calculate the MAC over the packet ciphertext and to send the packet + // length unencrypted. This is necessary for the transport to obtain the + // length of the packet and location of the MAC tag so that it may be + // verified without decrypting unauthenticated data. + // As such, the MAC covers: + // mac = MAC(key, sequence_number || packet_length || encrypted_packet) + // where "packet_length" is encoded as a uint32. + s.mac.Reset() + binary.BigEndian.PutUint32(s.seqNumBytes[:], seqNum) + s.mac.Write(s.seqNumBytes[:]) + s.cipher.XORKeyStream(s.prefix[4:5], s.prefix[4:5]) + s.mac.Write(s.prefix[:]) + } else { + s.cipher.XORKeyStream(s.prefix[:], s.prefix[:]) + } + s.cipher.XORKeyStream(packet, packet) s.cipher.XORKeyStream(padding, padding) + if s.mac != nil && s.etm { + // "encrypted_packet" contains: + // byte padding_length + // byte[n1] payload; n1 = packet_length - padding_length - 1 + // byte[n2] random padding; n2 = padding_length + s.mac.Write(packet) + s.mac.Write(padding) + } + if _, err := w.Write(s.prefix[:]); err != nil { return err } diff --git a/ssh/cipher_test.go b/ssh/cipher_test.go index eced8d851c..fbc432d729 100644 --- a/ssh/cipher_test.go +++ b/ssh/cipher_test.go @@ -63,6 +63,45 @@ func TestPacketCiphers(t *testing.T) { } } +func TestPacketMacs(t *testing.T) { + for mac := range macModes { + kr := &kexResult{Hash: crypto.SHA1} + algs := directionAlgorithms{ + Cipher: "aes256-ctr", + MAC: mac, + Compression: "none", + } + client, err := newPacketCipher(clientKeys, algs, kr) + if err != nil { + t.Errorf("newPacketCipher(client, %q): %v", mac, err) + continue + } + server, err := newPacketCipher(clientKeys, algs, kr) + if err != nil { + t.Errorf("newPacketCipher(client, %q): %v", mac, err) + continue + } + + want := "bla bla" + input := []byte(want) + buf := &bytes.Buffer{} + if err := client.writePacket(0, buf, rand.Reader, input); err != nil { + t.Errorf("writePacket(%q): %v", mac, err) + continue + } + + packet, err := server.readPacket(0, buf) + if err != nil { + t.Errorf("readPacket(%q): %v", mac, err) + continue + } + + if string(packet) != want { + t.Errorf("roundtrip(%q): got %q, want %q", mac, packet, want) + } + } +} + func TestCBCOracleCounterMeasure(t *testing.T) { cipherModes[aes128cbcID] = &streamCipherMode{16, aes.BlockSize, 0, nil} defer delete(cipherModes, aes128cbcID) diff --git a/ssh/common.go b/ssh/common.go index 2c72ab544b..f20173a629 100644 --- a/ssh/common.go +++ b/ssh/common.go @@ -56,7 +56,7 @@ var supportedHostKeyAlgos = []string{ // This is based on RFC 4253, section 6.4, but with hmac-md5 variants removed // because they have reached the end of their useful life. var supportedMACs = []string{ - "hmac-sha2-256", "hmac-sha1", "hmac-sha1-96", + "hmac-sha2-256-etm@openssh.com", "hmac-sha2-256", "hmac-sha1", "hmac-sha1-96", } var supportedCompressions = []string{compressionNone} diff --git a/ssh/mac.go b/ssh/mac.go index 07744ad671..c07a06285e 100644 --- a/ssh/mac.go +++ b/ssh/mac.go @@ -15,6 +15,7 @@ import ( type macMode struct { keySize int + etm bool new func(key []byte) hash.Hash } @@ -45,13 +46,16 @@ func (t truncatingMAC) Size() int { func (t truncatingMAC) BlockSize() int { return t.hmac.BlockSize() } var macModes = map[string]*macMode{ - "hmac-sha2-256": {32, func(key []byte) hash.Hash { + "hmac-sha2-256-etm@openssh.com": {32, true, func(key []byte) hash.Hash { return hmac.New(sha256.New, key) }}, - "hmac-sha1": {20, func(key []byte) hash.Hash { + "hmac-sha2-256": {32, false, func(key []byte) hash.Hash { + return hmac.New(sha256.New, key) + }}, + "hmac-sha1": {20, false, func(key []byte) hash.Hash { return hmac.New(sha1.New, key) }}, - "hmac-sha1-96": {20, func(key []byte) hash.Hash { + "hmac-sha1-96": {20, false, func(key []byte) hash.Hash { return truncatingMAC{12, hmac.New(sha1.New, key)} }}, } diff --git a/ssh/test/session_test.go b/ssh/test/session_test.go index fc7e4715ba..a57437fc8d 100644 --- a/ssh/test/session_test.go +++ b/ssh/test/session_test.go @@ -320,6 +320,29 @@ func TestMACs(t *testing.T) { } } +func TestMACsEtM(t *testing.T) { + var config ssh.Config + config.SetDefaults() + macOrder := []string{} + for _, mac := range config.MACs { + if len(mac) > 16 && mac[len(mac)-16:] == "-etm@openssh.com" { + macOrder = append(macOrder, mac) + } + } + + for _, mac := range macOrder { + server := newServer(t) + defer server.Shutdown() + conf := clientConfig() + conf.MACs = []string{mac} + if conn, err := server.TryDial(conf); err == nil { + conn.Close() + } else { + t.Fatalf("failed for MAC %q", mac) + } + } +} + func TestKeyExchanges(t *testing.T) { var config ssh.Config config.SetDefaults() diff --git a/ssh/transport.go b/ssh/transport.go index fd199324dd..087ba008c0 100644 --- a/ssh/transport.go +++ b/ssh/transport.go @@ -238,6 +238,7 @@ func newPacketCipher(d direction, algs directionAlgorithms, kex *kexResult) (pac c := &streamPacketCipher{ mac: macModes[algs.MAC].new(macKey), + etm: macModes[algs.MAC].etm, } c.macResult = make([]byte, c.mac.Size())