8000 #12184 Add support in conch.ssh to ETM HMACs. by adiroiban · Pull Request #12186 · twisted/twisted · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
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

#12184 Add support in conch.ssh to ETM HMACs. #12186

Draft
wants to merge 3 commits into
base: trunk
Choose a base branch
from
Draft

Conversation

adiroiban
Copy link
Member
@adiroiban adiroiban commented May 29, 2024

Scope and purpose

Fixes #12184

It adds for now hmac-sha2-256-etm@openssh.com and hmac-sha2-512-etm@openssh.com

I don't think that we need hmac-sha1-etm@openssh.com or hmac-md5-etm@openssh.com.
They can be added later.

How to test

ssh server

You can use OpenSSH ssh client to connect using only ETM HMAC

ssh -oMACs=hmac-sha2-256-etm@openssh.com -oPort=5022 user@localhost

Copy link
codspeed-hq bot commented May 29, 2024

CodSpeed Performance Report

Merging #12186 will not alter performance

Comparing 12184-ssh-etm (2d405b1) with trunk (c465c46)

Summary

✅ 2 untouched benchmarks

@@ -214,7 +216,7 @@ def _getMAC(
result.key = key
return result

def encrypt(self, blocks):
def _encrypt(self, blocks):
Copy link
Member Author

Choose a reason for hiding this comment

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

SSHCiphers is kind of a public API, so for the final merge we might need to revert this change.

But I think that this API should be private

@@ -257,6 +259,49 @@ def makeMAC(self, seqid, data):
data = struct.pack(">L", seqid) + data
return hmac.HMAC(self.outMAC.key, data, self.outMAC[0]).digest()

def createPacket(self, payload: bytes, outgoingPacketSequence: int) -> bytes:
Copy link
Member Author
@adiroiban adiroiban May 29, 2024

Choose a reason for hiding this comment

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

I moved this in ciphers as I feel that it can help reduce the API for SSHCiphers.

But I am ok to move it back to SSHTransportBase

This is used for sending a packet, and in this way, you don't need to expose the isETM info the the transport.

But for receiving data the code is still in SSHTransportBase

+ payload
+ randbytes.secureRandom(lenPad)

wirePacket = self.currentEncryptions.createPacket(
Copy link
Member Author

Choose a reason for hiding this comment

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

This might be moved out of SSHCiphers and have a private helper in SSHTransportBase

# Not enough data for a block
return

try:
if self.currentEncryptions.inMACType.endswith(b"-etm@openssh.com"):
Copy link
Member Author

Choose a reason for hiding this comment

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

Maybe we can have something like:

Suggested change
if self.currentEncryptions.inMACType.endswith(b"-etm@openssh.com"):
if self.currentEncryptions.isETM():

return self.inMACType.endswith(b"-etm@openssh.com")


class _InvalidPacket(Exception):
Copy link
Member Author

Choose a reason for hiding this comment

The 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.

"""
@return True if outgoing MAC is ETM.
"""
return self.outMACType.endswith(b"-etm@openssh.com")
Copy link
Member Author

Choose a reason for hiding this comment

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

Feedback from @glyph

Get this value cached.

We can pre-compute them when we set the key.

Copy link
Member
@glyph glyph left a comment

Choose a reason for hiding this comment

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

I definitely would like to get ETM and AESGCM support, but crypto code breaks my brain, so it might take a few tries to do a full review. In the meanwhile here is some initial feedback.

@@ -279,6 +281,24 @@ def verify(self, seqid, data, mac):
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.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
@return True if outgoing MAC is ETM.
@return: True if outgoing MAC is Encrypt-Then-MAC, otherwise False.


def isInMACETM(self) -> bool:
"""
@return True if incoming MAC is ETM.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
@return True if incoming MAC is ETM.
@return: True if incoming MAC is Encrypt-Then-MAC, otherwise False


class _InvalidPacket(Exception):
"""
Used internally when an invalid packet was received.
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
Used internally when an invalid packet was received.
An invalid packet was received.

# 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
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
lenPad = lenPad + bs
lenPad += bs

"""
@return True if outgoing MAC is ETM.
"""
return self.outMACType.endswith(b"-etm@openssh.com")
Copy link
Member

Choose a reason for hiding this comment

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

Is outMACType intended to be a mutable attribute? Perhaps this could be computed in __init__?

Copy link
Member Author

Choose a reason for hiding this comment

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

True. Not at __init__ , but at setKeys(). I will do that. Thanks

+ payload
+ randbytes.secureRandom(lenPad)

# Create the unencrypted packet.
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn't this packet construction live in the currentEncryptions attribute? The only reference here to self other than self.currentEncryptions is to self.outgoingPacketSequence, so maybe the thing to do here is return self.currentEncryptions.makePacket(payload, self.outgoingPacketSequence) ?

(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.)

Copy link
Member Author

Choose a reason for hiding this comment

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

Shouldn't this packet construction live in the currentEncryptions attribute?

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
return self.currentEncryptions.makePacket(payload, self.outgoingPacketSequence) looks better

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

Comment on lines +731 to +734
if self.currentEncryptions.isInMACETM():
payload = self._getPacketETM()
else:
payload = self._getPacketMTE()
Copy link
Member

Choose a reason for hiding this comment

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

I think it would be a lot easier to read if these ...MTE / ...ETM methods were encapsulated within their own object, so there were a protocol for the operation, then a MTE implementation and an ETM implementation, then a call to self._packetGetter.getPacket() (or something) rather than switching to one of two mostly-similar methods.

Copy link
Member

Choose a reason for hiding this comment

The 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
if self.currentEncryptions.isInMACETM():
payload = self._getPacketETM()
else:
payload = self._getPacketMTE()
payload = self._getPacketETM() if self.currentEncryptions.isInMACETM() else self._getPacketMTE()

Copy link
Member Author

Choose a reason for hiding this comment

The 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 if / else is not ok for the long term.

I went with these simple methods inside the SSHTranport as I don't feel that is that much code to move into an object.

Comment on lines +736 to +741
except _InvalidPacket as error:
self.sendDisconnect(
DISCONNECT_PROTOCOL_ERROR,
error.args[0].encode("ascii"),
)
return
Copy link
Member

Choose a reason for hiding this comment

The 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. payload: bytes | None | Literal[Invalid.packet] and then if packet is Invalid.packet: ?

Copy link
Member Author

Choose a reason for hiding this comment

The 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 self._log.failure(error.args[0]).

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

try:
payload = self.incomingCompression.decompress(payload)
except Exception:
self._log.failure("Error decompressing payload")
Copy link
Member

Choose a reason for hiding this comment

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

love to see the new logging system used as intended :)

Copy link
Member Author

Choose a reason for hiding this comment

The 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.

@adiroiban
Copy link
Member Author

Many thanks @glyph for the review.

It's very useful to get a second opinion.

I think that the major comment here is self.currentEncryptions.makePacket(payload, self.outgoingPacketSequence)

See my inline answer.

I kept makePacket (write on wire) inside SSHTranport for 2 main reasons:

  • Don't add SSH transport binary packet logic to SSHCiphers
  • It hard to add a similar getPacket (for from write) to SSHCiphers

So my preference is the current refactoring, with the code hosted in SSHTransportBase.


Should we consider the methods of the SSHCipher private ?

They are public, but I fell that they should be private and end users should not mess with that code.

Copy link
Member
@glyph glyph left a comment

Choose a reason for hiding this comment

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

Tests are failing and I see plenty of feedback that hasn't been responded to or addressed, so I don't think that this ought to be in the review queue.

If you have a specific question for me feel free to @ me and just ask (I'm not sure I see one that needs an answer any more?), I'll do my best to reply; no need to request additional reviews when the code is still in more of a draft state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add support for Encrypt-then-MAC MAC algorithms
3 participants
0