-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
#12184 Add support in conch.ssh to ETM HMACs. #12186
base: trunk
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
twisted.conch.ssh.transport.SSHTransportBase now supports the | ||
`hmac-sha2-256-etm@openssh.com` and `hmac-sha2-512-etm@openssh.com` HMAC | ||
algorithms. |
Original file line number | Diff line number | Diff line change | ||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -32,7 +32,7 @@ | |||||||||||
from twisted.internet import defer, protocol | ||||||||||||
from twisted.logger import Logger | ||||||||||||
from twisted.python import randbytes | ||||||||||||
from twisted.python.compat import iterbytes, networkString | ||||||||||||
from twisted.python.compat import iterbytes | ||||||||||||
|
||||||||||||
# This import is needed if SHA256 hashing is used. | ||||||||||||
# from twisted.python.compat import nativeString | ||||||||||||
|
@@ -117,6 +117,8 @@ | |||||||||||
b"none": (None, 0, modes.CBC), | ||||||||||||
} | ||||||||||||
macMap = { | ||||||||||||
b"hmac-sha2-256-etm@openssh.com": sha256, | ||||||||||||
b"hmac-sha2-512-etm@openssh.com": sha512, | ||||||||||||
b"hmac-sha2-512": sha512, | ||||||||||||
b"hmac-sha2-384": sha384, | ||||||||||||
b"hmac-sha2-256": sha256, | ||||||||||||
|
@@ -279,6 +281,24 @@ | |||||||||||
outer = hmac.HMAC(self.inMAC.key, data, self.inMAC[0]).digest() | ||||||||||||
return hmac.compare_digest(mac, outer) | ||||||||||||
|
||||||||||||
def isOutMACETM(self) -> bool: | ||||||||||||
""" | ||||||||||||
@return True if outgoing MAC is ETM. | ||||||||||||
""" | ||||||||||||
return self.outMACType.endswith(b"-etm@openssh.com") | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. True. Not at |
||||||||||||
|
||||||||||||
def isInMACETM(self) -> bool: | ||||||||||||
""" | ||||||||||||
@return True if incoming MAC is ETM. | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
""" | ||||||||||||
return self.inMACType.endswith(b"-etm@openssh.com") | ||||||||||||
|
||||||||||||
|
||||||||||||
class _InvalidPacket(Exception): | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This exception is used to share more code between ETM and MTE code. |
||||||||||||
""" | ||||||||||||
Used internally when an invalid packet was received. | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
""" | ||||||||||||
|
||||||||||||
|
||||||||||||
def _getSupportedCiphers(): | ||||||||||||
""" | ||||||||||||
|
@@ -454,9 +474,11 @@ | |||||||||||
# List ordered by preference. | ||||||||||||
supportedCiphers = _getSupportedCiphers() | ||||||||||||
supportedMACs = [ | ||||||||||||
b"hmac-sha2-512", | ||||||||||||
b"hmac-sha2-384", | ||||||||||||
b"hmac-sha2-256-etm@openssh.com", | ||||||||||||
b"hmac-sha2-512-etm@openssh.com", | ||||||||||||
b"hmac-sha2-256", | ||||||||||||
b"hmac-sha2-384", | ||||||||||||
b"hmac-sha2-512", | ||||||||||||
b"hmac-sha1", | ||||||||||||
b"hmac-md5", | ||||||||||||
# `none`, | ||||||||||||
|
@@ -637,83 +659,186 @@ | |||||||||||
payload = self.outgoingCompression.compress( | ||||||||||||
payload | ||||||||||||
) + self.outgoingCompression.flush(2) | ||||||||||||
|
||||||||||||
wirePacket = self._createPacket(payload) | ||||||||||||
|
||||||||||||
self.transport.write(wirePacket) | ||||||||||||
self.outgoingPacketSequence += 1 | ||||||||||||
|
||||||||||||
def _createPacket(self, payload: bytes) -> bytes: | ||||||||||||
""" | ||||||||||||
Return the SSH packet at it should be sent to the peer over the wire. | ||||||||||||
|
||||||||||||
`payload` is the actual data. | ||||||||||||
""" | ||||||||||||
# Start by computing the required padding. | ||||||||||||
# | ||||||||||||
# Extra 1 byte for the padding length | ||||||||||||
payloadSize = len(payload) + 1 | ||||||||||||
isETM = self.currentEncryptions.isOutMACETM() | ||||||||||||
|
||||||||||||
if not isETM: | ||||||||||||
# For not ETM, the packet length is also included. | ||||||||||||
payloadSize += 4 | ||||||||||||
|
||||||||||||
bs = self.currentEncryptions.encBlockSize | ||||||||||||
# 4 for the packet length and 1 for the padding length | ||||||||||||
totalSize = 5 + len(payload) | ||||||||||||
lenPad = bs - (totalSize % bs) | ||||||||||||
lenPad = bs - (payloadSize % bs) | ||||||||||||
if lenPad < 4: | ||||||||||||
lenPad = lenPad + bs | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||
packet = ( | ||||||||||||
struct.pack("!LB", totalSize + lenPad - 4, lenPad) | ||||||||||||
+ payload | ||||||||||||
+ randbytes.secureRandom(lenPad) | ||||||||||||
|
||||||||||||
# Create the unencrypted packet. | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldn't this packet construction live in the (This is definitely a big improvement to at least get stuff into a functional shape, but pushing it a little further would put the responsibility in the right place.) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Good question. This was my first refactoring. And this is why I wanted an initial review for this draft PR. Take a look at the first commit from the PR and let me know if you think that My initial refactoring was to move everything into SSHCiphers... but then, this means that SSHCiphers, beside actual cipher stuff it also need to know thinkgs about SSH transport and binary packet format. This is why in the second version I moved the code to transport. In the end, I think that is best to keep this createPacket code into SSHTransport and not into SSHCipher |
||||||||||||
payloadPacket = ( | ||||||||||||
struct.pack("!B", lenPad) + payload + randbytes.secureRandom(lenPad) | ||||||||||||
) | ||||||||||||
|
||||||||||||
if isETM: | ||||||||||||
# For Encrypt-then-MAC the total size is not encrypted. | ||||||||||||
encryptedPayload = self.currentEncryptions.encrypt(payloadPacket) | ||||||||||||
totalSizePacket = struct.pack("!L", len(encryptedPayload)) | ||||||||||||
macTarget = totalSizePacket + encryptedPayload | ||||||||||||
macPacket = self.currentEncryptions.makeMAC( | ||||||||||||
self.outgoingPacketSequence, macTarget | ||||||||||||
) | ||||||||||||
return macTarget + macPacket | ||||||||||||
|
||||||||||||
# For non ETM, we subtract the 4 bytes added to compute the | ||||||||||||
# padding size. | ||||||||||||
# But we also encrypt the package length. | ||||||||||||
totalSizePacket = struct.pack("!L", payloadSize + lenPad - 4) | ||||||||||||
encryptedPacket = self.currentEncryptions.encrypt( | ||||||||||||
totalSizePacket + payloadPacket | ||||||||||||
) | ||||||||||||
macTarget = totalSizePacket + payloadPacket | ||||||||||||
|
||||||||||||
macPacket = self.currentEncryptions.makeMAC( | ||||||||||||
self.outgoingPacketSequence, macTarget | ||||||||||||
) | ||||||||||||
encPacket = self.currentEncryptions.encrypt( | ||||||||||||
packet | ||||||||||||
) + self.currentEncryptions.makeMAC(self.outgoingPacketSequence, packet) | ||||||||||||
self.transport.write(encPacket) | ||||||||||||
self.outgoingPacketSequence += 1 | ||||||||||||
|
||||||||||||
def getPacket(self): | ||||||||||||
return encryptedPacket + macPacket | ||||||||||||
|
||||||||||||
def getPacket(self) -> bytes | None: | ||||||||||||
""" | ||||||||||||
Try to return a decrypted, authenticated, and decompressed packet | ||||||||||||
out of the buffer. If there is not enough data, return None. | ||||||||||||
|
||||||||||||
@rtype: L{str} or L{None} | ||||||||||||
@return: The decoded packet, if any. | ||||||||||||
@return: The decoded packet, if any. C{None} is returned if no packet was received yet. | ||||||||||||
""" | ||||||||||||
bs = self.currentEncryptions.decBlockSize | ||||||||||||
ms = self.currentEncryptions.verifyDigestSize | ||||||||||||
if len(self.buf) < bs: | ||||||||||||
if len(self.buf) < self.currentEncryptions.decBlockSize: | ||||||||||||
# Not enough data for a block | ||||||||||||
return | ||||||||||||
|
||||||||||||
try: | ||||||||||||
if self.currentEncryptions.isInMACETM(): | ||||||||||||
payload = self._getPacketETM() | ||||||||||||
else: | ||||||||||||
payload = self._getPacketMTE() | ||||||||||||
Comment on lines
+731
to
+734
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it would be a lot easier to read if these There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If you are going to make this a conditional though, there should still only be one assignment:
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks. I will see what we can do... in the future, we will also have an AES-GCM implementation. so inline I went with these simple methods inside the SSHTranport as I don't feel that is that much code to move into an object. |
||||||||||||
|
||||||||||||
except _InvalidPacket as error: | ||||||||||||
self.sendDisconnect( | ||||||||||||
DISCONNECT_PROTOCOL_ERROR, | ||||||||||||
error.args[0].encode("ascii"), | ||||||||||||
) | ||||||||||||
return | ||||||||||||
Comment on lines
+736
to
+741
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we are always discarding the traceback from this new exception, it might be better expressed as an addition to the union; less stack-walking that way, more explicit that it's a possibility. i.e. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't keep the traceback, but we extract the error message. I think that what is missing here, is This should make it similar to the other error exception handling. I prefer to use the exception here as I would like to have the error message, and I feel that raising an exceptoin is the "standard" way of signaling an invalid packet while also having an error details. From my experience, the whole log calls from the SSH transport are not well designed. For my production code, I ignore them and I have various custom hooks to add logs. Improving the logs would be a different tasks for conch.ssh |
||||||||||||
|
||||||||||||
if payload is None: | ||||||||||||
# No packet received yet. | ||||||||||||
return | ||||||||||||
|
||||||||||||
if self.incomingCompression: | ||||||||||||
try: | ||||||||||||
payload = self.incomingCompression.decompress(payload) | ||||||||||||
except Exception: | ||||||||||||
self._log.failure("Error decompressing payload") | ||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. love to see the new logging system used as intended :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As far as I know, this code is already in trunk. I don't have much exprience with Twisted log system. I remember that at some point I tried to document the new logging system, how to use and how to write test for it, but it wasn't a succesfull PR. |
||||||||||||
self.sendDisconnect(DISCONNECT_COMPRESSION_ERROR, b"compression error") | ||||||||||||
return | ||||||||||||
|
||||||||||||
self.incomingPacketSequence += 1 | ||||||||||||
return payload | ||||||||||||
|
||||||||||||
def _getPacketMTE(self) -> bytes | None: | ||||||||||||
""" | ||||||||||||
Handle decoding a packet when mac then encrypt. | ||||||||||||
|
||||||||||||
@return: None if no packet was yet received. | ||||||||||||
""" | ||||||||||||
bs = self.currentEncryptions.decBlockSize | ||||||||||||
ms = self.currentEncryptions.verifyDigestSize | ||||||||||||
|
||||||||||||
if not hasattr(self, "first"): | ||||||||||||
first = self.currentEncryptions.decrypt(self.buf[:bs]) | ||||||||||||
else: | ||||||||||||
first = self.first | ||||||||||||
del self.first | ||||||||||||
packetLen, paddingLen = struct.unpack("!LB", first[:5]) | ||||||||||||
if packetLen > 1048576: # 1024 ** 2 | ||||||||||||
self.sendDisconnect( | ||||||||||||
DISCONNECT_PROTOCOL_ERROR, | ||||||||||||
networkString(f"bad packet length {packetLen}"), | ||||||||||||
) | ||||||||||||
return | ||||||||||||
raise _InvalidPacket(f"bad packet length {packetLen}") | ||||||||||||
|
||||||||||||
if len(self.buf) < packetLen + 4 + ms: | ||||||||||||
# Not enough data for a packet | ||||||||||||
self.first = first | ||||||||||||
return | ||||||||||||
if (packetLen + 4) % bs != 0: | ||||||||||||
self.sendDisconnect( | ||||||||||||
DISCONNECT_PROTOCOL_ERROR, | ||||||||||||
networkString( | ||||||||||||
"bad packet mod (%i%%%i == %i)" | ||||||||||||
% (packetLen + 4, bs, (packetLen + 4) % bs) | ||||||||||||
), | ||||||||||||
raise _InvalidPacket( | ||||||||||||
"bad packet mod ({}%{} == {})".format( | ||||||||||||
packetLen + 4, bs, (packetLen + 4) % bs | ||||||||||||
) | ||||||||||||
) | ||||||||||||
return | ||||||||||||
|
||||||||||||
encData, self.buf = self.buf[: 4 + packetLen], self.buf[4 + packetLen :] | ||||||||||||
packet = first + self.currentEncryptions.decrypt(encData[bs:]) | ||||||||||||
if len(packet) != 4 + packetLen: | ||||||||||||
self.sendDisconnect(DISCONNECT_PROTOCOL_ERROR, b"bad decryption") | ||||||||||||
return | ||||||||||||
raise _InvalidPacket("bad decryption") | ||||||||||||
|
||||||||||||
if ms: | ||||||||||||
macData, self.buf = self.buf[:ms], self.buf[ms:] | ||||||||||||
if not self.currentEncryptions.verify( | ||||||||||||
self.incomingPacketSequence, packet, macData | ||||||||||||
): | ||||||||||||
self.sendDisconnect(DISCONNECT_MAC_ERROR, b"bad MAC") | ||||||||||||
return | ||||||||||||
raise _InvalidPacket("bad MAC for MTE") | ||||||||||||
|
||||||||||||
payload = packet[5:-paddingLen] | ||||||||||||
if self.incomingCompression: | ||||||||||||
try: | ||||||||||||
payload = self.incomingCompression.decompress(payload) | ||||||||||||
except Exception: | ||||||||||||
# Tolerate any errors in decompression | ||||||||||||
self._log.failure("Error decompressing payload") | ||||||||||||
self.sendDisconnect(DISCONNECT_COMPRESSION_ERROR, b"compression error") | ||||||||||||
return | ||||||||||||
self.incomingPacketSequence += 1 | ||||||||||||
return payload | ||||||||||||
|
||||||||||||
def _getPacketETM(self) -> bytes | None: | ||||||||||||
""" | ||||||||||||
Handle decoding a packet when encrypt then MAC is used. | ||||||||||||
|
||||||||||||
@return: None if no packet was yet received. | ||||||||||||
""" | ||||||||||||
bs = self.currentEncryptions.decBlockSize | ||||||||||||
ms = self.currentEncryptions.verifyDigestSize | ||||||||||||
|
||||||||||||
packetLen = struct.unpack("!L", self.buf[:4])[0] | ||||||||||||
|
||||||||||||
if packetLen > 1048576: # 1024 ** 2 | ||||||||||||
raise _InvalidPacket(f"bad packet length {packetLen}") | ||||||||||||
|
||||||||||||
if packetLen % bs != 0: | ||||||||||||
raise _InvalidPacket( | ||||||||||||
"bad packet mod ({}%{} == {})".format(packetLen, bs, packetLen % bs) | ||||||||||||
) | ||||||||||||
|
||||||||||||
if len(self.buf) < packetLen + ms: | ||||||||||||
# Not enough data for a packet | ||||||||||||
return | ||||||||||||
|
||||||||||||
# Consume the encrypted data from the buffer. | ||||||||||||
# | ||||||||||||
# The first 4 bytes are the package lenght field. | ||||||||||||
encData, self.buf = self.buf[: 4 + packetLen], self.buf[4 + packetLen :] | ||||||||||||
packet = self.currentEncryptions.decrypt(encData[4:]) | ||||||||||||
if len(packet) != packetLen: | ||||||||||||
raise _InvalidPacket("bad decryption") | ||||||||||||
|
||||||||||||
# Consume the MAC data from the buffer. | ||||||||||||
macData, self.buf = self.buf[:ms], self.buf[ms:] | ||||||||||||
if not self.currentEncryptions.verify( | ||||||||||||
self.incomingPacketSequence, encData, macData | ||||||||||||
): | ||||||||||||
raise _InvalidPacket("bad MAC for ETM") | ||||||||||||
|
||||||||||||
paddingLen = struct.unpack("!B", packet[:1])[0] | ||||||||||||
payload = packet[1:-paddingLen] | ||||||||||||
return payload | ||||||||||||
|
||||||||||||
def _unsupportedVersionReceived(self, remoteVersion): | ||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.