-
-
Notifications
You must be signed in to change notification settings - Fork 934
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
Padding is broken #1238
base: develop
Are you sure you want to change the base?
Padding is broken #1238
Conversation
Hi @Rob-Hague , it seems we're on the same path again. I've mentioned the broken padding some days ago in 865 as I had seen the same issues you describe. I also worked on a fix this week - here's my code - I think it follows pretty much the same path as yours, with a slightly different implementation. Just to drive the point home about how broken the padding is: the current BlockDecrypt() function is actually adding padding instead of removing it, which is only working because the SSH Session code actually never calls it with non-blocksized data (otherwise DecryptBlock() produces 100% garbage output). While padding can be entirely removed for now, I think it's better to leave the (corrected) functionality in place as it may be needed in the future. The SSH Stream will always use the block-aligned BlockEncrypt/BlockDecrypt functions, but the more generic Encrypt/Decrypt functions are needed for secondary usages such as certificate decryption. These functions should fully support padding/unpadding, and should have 100% standard behavior. We could add Constructors that don't take a CipherPadding arg (defaulting to null) as almost all usages in SSH.Net set it to null anyway. One other related issue is that some cipher modes should allow for non-blocksized data input/output, without padding, though this functionality is not needed for SSH.NET. Only CBC and ECB mandate a PKCS7 padding, while CTR/CFC/OFB do not (ie, you can encrypt 7 bytes and get 7 bytes back without padding). I've commited a change to CFB that allows that to happen, just as a proof of concept. |
I've considered this while I was doing #865 and I think this is not the case. The correct behavior is:
This usually means that you cannot use the same Cipher instance to mix calls to both APIs, which I think is perfectly fine.
This is the reason why the .Net APIs include a TransformFinalBlock, which adds the final padding and resets the IV back to the original value. Under .Net the Encrypt/Decrypt functions are deterministic, they don't preserve IV state. |
Doesn't that contradict https://github.com/zybexXL/SSH.NET/blob/ada6ccbcc20e1e26463fbe22004556c9f884da57/src/Renci.SshNet/Security/Cryptography/Ciphers/AesCipher.cs#L225-L232 ? It is using I agree with that code - the integration tests fail without it. |
It is a partial contradiction. I did that so we can use ECB and CBC (which normally require padding) to encrypt data without padding or to decrypt without removing it. This is mostly to pass some test cases which are not expecting correct padding functionality. Some of the test PrivateKey/certs have the padding included when they should not, for instance, or vice versa. As an aside - your (most excelent) AesCipherTest generator uses OpenSSH to generate the reference outputs, but it always calls it with the -nopad option. However, because you're piping the output to hd (hexdump) you're not seeing the STDERR errors that happen when using ECB and CBC with -nopad. OpenSSH warns that the output is wrong, and there are 16 missing bytes. The generated test can then only pass when calling Decrypt() with Padding.None, even though ECB/CBC require it. This is also why I added that code above, as TransformFinalBlock will always add padding. With -nopad : 0x100 bytes output, StdErr message "bad decrypt":
Without -nopad : 0x110 bytes output (padded), no error:
|
Many existing testcases are calling Encrypt/Decrypt under the expectation that this function ends up calling DecryptBlock/EncryptBlock. However after #865 this is no longer the case! So perhaps new/changed testcases are needed for DecryptBlock/EncryptBlock, which are still the main functions used during an SSH session. |
Calling
The script itself does not pipe to hd. It reads stderr and generates
I don't see such an expectation. var cbc = new AesCipher(key, new CbcCipherMode(iv), null);
cbc.EncryptBlock(input); // This will be ECB, not CBC. It is not correct to call EncryptBlock
// It is only correct to call
cbc.Encrypt(input); |
Yes, I added the change to call TransformBlock instead of TransformFinalBlock when padding is null so that it preserves state for the SSH stream mode. When Padding is not null, calling TransformFinalBlock will reset the IV.
Yes I did. The point is that the testcase expects a given output with -nopad when that is not a valid case for ECB/CBC. I believe an SSH server using AES-CBC will add correct PKCS7 padding; the reason SSH.Net still works when using null Padding is likely because the SSH message contains a
You're right, Session.cs calls Encrypt/Decrypt.
This is fixed in #865, you can safely call EncryptBlock/DecryptBlock and it will do the correct mode. |
Where have you read this? I don't think it is true. |
I didn't, I just think that it should because ECB and CBC mandate padding. Though it's fine to use them without padding, as long as both sides agree on the implementation. |
@Rob-Hague, off-topic: can I contact you directly? I have a JetBrains license (Resharper, ...) available for you to use, if you're interested. I contacted JetBrains and got three licenses (one for @WojciechNagorski, one for you and one for me). |
Sure, thanks very much. You can use an email of mine in the commit history |
Send me an email, and I'll respond with the license URL. |
Originally this was just an exercise to further increase coverage for
AesCipher
when using padding, but it turns out padding is not implemented correctly at all: it is not added on block-sized lengths; and not removed during decryption.So then this became a fix for that, with some unsatisfying logic in
BlockCipher.Decrypt
to accommodate existing usage where padding is specified but the plaintext is not actually padded.However, upon reviewing usages in the library and reviewing #865 I want to discuss removing
CipherPadding
altogether. Here some notes:CipherPadding
is not used for SSH packets: instead packets are padded within the protocol to block-length.PrivateKeyFile
, e.g.SSH.NET/src/Renci.SshNet/PrivateKeyFile.cs
Lines 217 to 227 in 826222f
It is used (incorrectly) to allow decrypting non-block-sized inputs in a block-by-block manner in
BlockCipher
. We could equally just callArray.Resize
to achieve the same effect.Encrypt
/Decrypt
. But a call to these functions with padding specified would be expected to provide padding i.e. to "finalize" the state. So there are effectively two modes leading to logic likehttps://github.com/zybexXL/SSH.NET/blob/ada6ccbcc20e1e26463fbe22004556c9f884da57/src/Renci.SshNet/Security/Cryptography/Ciphers/AesCipher.cs#L225-L232 even though we never actually use the
TransformFinalBlock
logic in the library.I think the options are:
CipherPadding
,PKCS7Padding
etc. They are broken and effectively unused (or rather, used uneffectively). It's hard to imagine anyone depending on them. We then do not need a condition in the cipher implementations which tries to decide whether we are in a "stream" mode or not.This would disallow encrypting non-block-sized lengths, which we do not do in the library anyway.
PKCS7Padding
which neither pads nor depads correctly.