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 #2157

Open
wants to merge 2 commits into
base: main
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
60 changes: 54 additions & 6 deletions paramiko/packet.py
Original file line number Diff line number Diff line change
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,20 @@ 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 = int.from_bytes(iv_counter_b, "big")
inc_iv_counter = iv_counter + 1
inc_iv_counter_b = inc_iv_counter.to_bytes(8, "big")
new_iv = iv[0:4] + inc_iv_counter_b
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 +442,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 +490,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 +509,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.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 +552,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 +675,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
123 changes: 108 additions & 15 deletions paramiko/transport.py
Original file line number Diff line number Diff line change
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",
)
Comment on lines +167 to 169

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question: Why are the new ciphers at the bottom? While I am not a cryptographer, I see these ciphers as recommended to use in SSH because they're extremely strong (sources: Security Stack Exchange, Mozilla SSH guidelines, ssh-audit hardening guidelines).

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because there might be some issues with the new cipher suites, consider putting it at the bottom.

_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,32 @@ 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 +2669,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 +2700,32 @@ 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 +2736,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