Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add aes128-gcm and aes256-gcm support #2367

Open
wants to merge 1 commit into
base: 2.12
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
65 changes: 59 additions & 6 deletions paramiko/packet.py
Expand Up @@ -114,6 +114,12 @@ def __init__(self, socket):
self.__etm_out = False
self.__etm_in = False

# aead cipher use
self.__aead_out = False
self.__aead_in = False
self.__iv_out = None
self.__iv_in = None

# lock around outbound writes (packet computation)
self.__write_lock = threading.RLock()

Expand Down Expand Up @@ -145,6 +151,8 @@ def set_outbound_cipher(
mac_key,
sdctr=False,
etm=False,
aead=False,
iv_out=None,
):
"""
Switch outbound data cipher.
Expand All @@ -159,6 +167,8 @@ def set_outbound_cipher(
self.__sent_bytes = 0
self.__sent_packets = 0
self.__etm_out = etm
self.__aead_out = aead
self.__iv_out = iv_out
# wait until the reset happens in both directions before clearing
# rekey flag
self.__init_count |= 1
Expand All @@ -174,6 +184,8 @@ def set_inbound_cipher(
mac_size,
mac_key,
etm=False,
aead=False,
iv_in=None,
):
"""
Switch inbound data cipher.
Expand All @@ -189,6 +201,8 @@ def set_inbound_cipher(
self.__received_bytes_overflow = 0
self.__received_packets_overflow = 0
self.__etm_in = etm
self.__aead_in = aead
self.__iv_in = iv_in
# wait until the reset happens in both directions before clearing
# rekey flag
self.__init_count |= 2
Expand Down Expand Up @@ -385,6 +399,25 @@ def readline(self, timeout):
buf = buf[:-1]
return u(buf)

def _inc_iv_counter(self, iv):
# refer https://www.rfc-editor.org/rfc/rfc5647.html#section-7.1
iv_counter_b = iv[4:]
iv_counter = 0
for b in iv_counter_b:
iv_counter = (iv_counter << 8) | ord(b)
inc_iv_counter = iv_counter + 1
inc_iv_counter_b = ''
while inc_iv_counter > 0:
inc_iv_counter_b = chr(inc_iv_counter & 0xFF) + inc_iv_counter_b
inc_iv_counter >>= 8
new_iv = iv[0:4] + inc_iv_counter_b.rjust(4, '\x00')
self._log(
DEBUG,
"old-iv_count[%s], new-iv_count[%s]"
% (iv_counter, inc_iv_counter),
)
return new_iv

def send_message(self, data):
"""
Write a block of data using the current cipher, as an SSH block.
Expand Down Expand Up @@ -414,12 +447,18 @@ def send_message(self, data):
out = packet[0:4] + self.__block_engine_out.update(
packet[4:]
)
elif self.__aead_out:
# packet length is used to associated_data
out = packet[0:4] + self.__block_engine_out.encrypt(
self.__iv_out, packet[4:], packet[0:4]
)
self.__iv_out = self._inc_iv_counter(self.__iv_out)
else:
out = self.__block_engine_out.update(packet)
else:
out = packet
# + mac
if self.__block_engine_out is not None:
# + mac, aead no need hmac
if self.__block_engine_out is not None and not self.__aead_out:
packed = struct.pack(">I", self.__sequence_number_out)
payload = packed + (out if self.__etm_out else packet)
out += compute_hmac(
Expand Down Expand Up @@ -456,7 +495,9 @@ def read_message(self):
:raises: `.SSHException` -- if the packet is mangled
:raises: `.NeedRekeyException` -- if the transport should rekey
"""
self._log(DEBUG, "read message from sock")
header = self.read_all(self.__block_size_in, check_rekey=True)
self._log(DEBUG, "raw data length[%s]" % len(header))
if self.__etm_in:
packet_size = struct.unpack(">I", header[:4])[0]
remaining = packet_size - self.__block_size_in + 4
Expand All @@ -473,14 +514,26 @@ def read_message(self):
raise SSHException("Mismatched MAC")
header = packet

if self.__block_engine_in is not None:
if self.__aead_in:
packet_size = struct.unpack(">I", header[:4])[0]
aad = header[:4]
remaining = (
packet_size - self.__block_size_in + 4 + self.__mac_size_in
)
packet = header[4:] + self.read_all(remaining, check_rekey=False)
self._log(DEBUG, "len(aad)=%s, aad->%s" % (len(aad), aad.encode("hex")))
header = self.__block_engine_in.decrypt(self.__iv_in, packet, aad)

self.__iv_in = self._inc_iv_counter(self.__iv_in)

if self.__block_engine_in is not None and not self.__aead_in:
header = self.__block_engine_in.update(header)
if self.__dump_packets:
self._log(DEBUG, util.format_binary(header, "IN: "))

# When ETM is in play, we've already read the packet size & decrypted
# everything, so just set the packet back to the header we obtained.
if self.__etm_in:
if self.__etm_in or self.__aead_in:
packet = header
# Otherwise, use the older non-ETM logic
else:
Expand All @@ -504,7 +557,7 @@ def read_message(self):
if self.__dump_packets:
self._log(DEBUG, util.format_binary(packet, "IN: "))

if self.__mac_size_in > 0 and not self.__etm_in:
if self.__mac_size_in > 0 and not self.__etm_in and not self.__aead_in:
mac = post_packet[: self.__mac_size_in]
mac_payload = (
struct.pack(">II", self.__sequence_number_in, packet_size)
Expand Down Expand Up @@ -627,7 +680,7 @@ def _build_packet(self, payload):
bsize = self.__block_size_out
# do not include payload length in computations for padding in EtM mode
# (payload length won't be encrypted)
addlen = 4 if self.__etm_out else 8
addlen = 4 if self.__etm_out or self.__aead_out else 8
padding = 3 + bsize - ((len(payload) + addlen) % bsize)
packet = struct.pack(">IB", len(payload) + padding + 1, padding)
packet += payload
Expand Down
121 changes: 106 additions & 15 deletions paramiko/transport.py
Expand Up @@ -31,7 +31,12 @@
from hashlib import md5, sha1, sha256, sha512

from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.ciphers import algorithms, Cipher, modes
from cryptography.hazmat.primitives.ciphers import (
algorithms,
Cipher,
modes,
aead,
)

import paramiko
from paramiko import util
Expand Down Expand Up @@ -159,6 +164,8 @@ class Transport(threading.Thread, ClosingContextManager):
"aes192-cbc",
"aes256-cbc",
"3des-cbc",
"aes128-gcm@openssh.com",
"aes256-gcm@openssh.com",
)
_preferred_macs = (
"hmac-sha2-256",
Expand Down Expand Up @@ -217,44 +224,66 @@ class Transport(threading.Thread, ClosingContextManager):
"class": algorithms.AES,
"mode": modes.CTR,
"block-size": 16,
"iv-size": 16,
"key-size": 16,
},
"aes192-ctr": {
"class": algorithms.AES,
"mode": modes.CTR,
"block-size": 16,
"iv-size": 16,
"key-size": 24,
},
"aes256-ctr": {
"class": algorithms.AES,
"mode": modes.CTR,
"block-size": 16,
"iv-size": 16,
"key-size": 32,
},
"aes128-cbc": {
"class": algorithms.AES,
"mode": modes.CBC,
"block-size": 16,
"iv-size": 16,
"key-size": 16,
},
"aes192-cbc": {
"class": algorithms.AES,
"mode": modes.CBC,
"block-size": 16,
"iv-size": 16,
"key-size": 24,
},
"aes256-cbc": {
"class": algorithms.AES,
"mode": modes.CBC,
"block-size": 16,
"iv-size": 16,
"key-size": 32,
},
"3des-cbc": {
"class": algorithms.TripleDES,
"mode": modes.CBC,
"block-size": 8,
"iv-size": 8,
"key-size": 24,
},
# aead cipher
"aes128-gcm@openssh.com": {
"class": aead.AESGCM,
"block-size": 16,
"iv-size": 12,
"key-size": 16,
"is_aead": True,
},
"aes256-gcm@openssh.com": {
"class": aead.AESGCM,
"block-size": 16,
"iv-size": 12,
"key-size": 32,
"is_aead": True,
},
}

_mac_info = {
Expand Down Expand Up @@ -1990,6 +2019,10 @@ def _get_cipher(self, name, key, iv, operation):
else:
return cipher.decryptor()

def _get_aead_cipher(self, name, key):
aead_cipher = self._cipher_info[name]["class"](key)
return aead_cipher

def _set_forward_agent_handler(self, handler):
if handler is None:

Expand Down Expand Up @@ -2601,18 +2634,31 @@ def _activate_inbound(self):
inbound traffic"""
block_size = self._cipher_info[self.remote_cipher]["block-size"]
if self.server_mode:
IV_in = self._compute_key("A", block_size)
IV_in = self._compute_key(
"A", self._cipher_info[self.remote_cipher]["iv-size"]
)
key_in = self._compute_key(
"C", self._cipher_info[self.remote_cipher]["key-size"]
)
else:
IV_in = self._compute_key("B", block_size)
IV_in = self._compute_key(
"B", self._cipher_info[self.remote_cipher]["iv-size"]
)
key_in = self._compute_key(
"D", self._cipher_info[self.remote_cipher]["key-size"]
)
engine = self._get_cipher(
self.remote_cipher, key_in, IV_in, self._DECRYPT
is_aead = (
True
if self._cipher_info[self.remote_cipher].get("is_aead")
else False
)

if is_aead:
engine = self._get_aead_cipher(self.remote_cipher, key_in)
else:
engine = self._get_cipher(
self.remote_cipher, key_in, IV_in, self._DECRYPT
)
etm = "etm@openssh.com" in self.remote_mac
mac_size = self._mac_info[self.remote_mac]["size"]
mac_engine = self._mac_info[self.remote_mac]["class"]
Expand All @@ -2622,9 +2668,22 @@ def _activate_inbound(self):
mac_key = self._compute_key("E", mac_engine().digest_size)
else:
mac_key = self._compute_key("F", mac_engine().digest_size)
self.packetizer.set_inbound_cipher(
engine, block_size, mac_engine, mac_size, mac_key, etm=etm
)
if is_aead:
self._log(DEBUG, "use aead-cipher, so set mac to None")
self.packetizer.set_inbound_cipher(
engine,
block_size,
None,
16,
bytes(),
etm=False,
aead=is_aead,
iv_in=IV_in,
)
else:
self.packetizer.set_inbound_cipher(
engine, block_size, mac_engine, mac_size, mac_key, etm=etm
)
compress_in = self._compression_info[self.remote_compression][1]
if compress_in is not None and (
self.remote_compression != "zlib@openssh.com" or self.authenticated
Expand All @@ -2640,18 +2699,31 @@ def _activate_outbound(self):
self._send_message(m)
block_size = self._cipher_info[self.local_cipher]["block-size"]
if self.server_mode:
IV_out = self._compute_key("B", block_size)
IV_out = self._compute_key(
"B", self._cipher_info[self.local_cipher]["iv-size"]
)
key_out = self._compute_key(
"D", self._cipher_info[self.local_cipher]["key-size"]
)
else:
IV_out = self._compute_key("A", block_size)
IV_out = self._compute_key(
"A", self._cipher_info[self.local_cipher]["iv-size"]
)
key_out = self._compute_key(
"C", self._cipher_info[self.local_cipher]["key-size"]
)
engine = self._get_cipher(
self.local_cipher, key_out, IV_out, self._ENCRYPT
is_aead = (
True
if self._cipher_info[self.local_cipher].get("is_aead")
else False
)

if is_aead:
engine = self._get_aead_cipher(self.local_cipher, key_out)
else:
engine = self._get_cipher(
self.local_cipher, key_out, IV_out, self._ENCRYPT
)
etm = "etm@openssh.com" in self.local_mac
mac_size = self._mac_info[self.local_mac]["size"]
mac_engine = self._mac_info[self.local_mac]["class"]
Expand All @@ -2662,9 +2734,28 @@ def _activate_outbound(self):
else:
mac_key = self._compute_key("E", mac_engine().digest_size)
sdctr = self.local_cipher.endswith("-ctr")
self.packetizer.set_outbound_cipher(
engine, block_size, mac_engine, mac_size, mac_key, sdctr, etm=etm
)
if is_aead:
self.packetizer.set_outbound_cipher(
engine,
block_size,
None,
16,
bytes(),
sdctr,
etm=False,
aead=is_aead,
iv_out=IV_out,
)
else:
self.packetizer.set_outbound_cipher(
engine,
block_size,
mac_engine,
mac_size,
mac_key,
sdctr,
etm=etm,
)
compress_out = self._compression_info[self.local_compression][0]
if compress_out is not None and (
self.local_compression != "zlib@openssh.com" or self.authenticated
Expand Down