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
Adds support for custom transport protection algorithms #97
Adds support for custom transport protection algorithms #97
Conversation
@@ -12,6 +12,10 @@ | |||
// | |||
//===----------------------------------------------------------------------===// | |||
|
|||
enum Constants { | |||
public enum Constants { |
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.
Not sure this is the best place, maybe we need to type Defaults
or smth?
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.
I agree with you, I think Defaults
would be better.
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.
Decided to change this a bit, I will rename constant to bundledTransportProtectionSchemes
, I quite like this idea from Joannis's PR, wdyt?
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.
LGTM.
Sources/NIOSSH/Constants.swift
Outdated
static let version = "SSH-2.0-SwiftNIOSSH_1.0" | ||
|
||
public static let defaultTransportProtectionSchemes: [NIOSSHTransportProtection.Type] = [ |
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.
I made it public so it would be possible both using only custom provided algs and adding new ones in addition to defaults one, but we probably can make it a bit nicer...
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.
I'm not sure that there's a better alternative than this.
@@ -79,7 +79,7 @@ extension AESGCMTransportProtection: NIOSSHTransportProtection { | |||
// unencrypted! | |||
} | |||
|
|||
func decryptAndVerifyRemainingPacket(_ source: inout ByteBuffer) throws -> ByteBuffer { | |||
func decryptAndVerifyRemainingPacket(_ source: inout ByteBuffer, sequence: UInt32) throws -> ByteBuffer { |
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.
CTR
requires packet sequence number for MAC verification, same for encryption, and it seems that those sequences start when binary protocol starts...
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.
Can we make it clear that AESGCM
doesn't need it, by using sequence _: UInt32
here and below?
Sources/NIOSSH/Connection State Machine/SSHConnectionStateMachine.swift
Outdated
Show resolved
Hide resolved
f9170d0
to
48ae0bb
Compare
8e8a4f3
to
8bf1a9a
Compare
8bf1a9a
to
d056ef3
Compare
d056ef3
to
1e7e46d
Compare
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.
Looks like a solid PR to me. I don't have anything to add to the code, but I don't think #50 can be closed after this PR. This PR only covers transport protection.
Agreed, but at least it should be referenced there, thank you! |
@@ -12,6 +12,10 @@ | |||
// | |||
//===----------------------------------------------------------------------===// | |||
|
|||
enum Constants { | |||
public enum Constants { |
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.
I agree with you, I think Defaults
would be better.
Sources/NIOSSH/Constants.swift
Outdated
static let version = "SSH-2.0-SwiftNIOSSH_1.0" | ||
|
||
public static let defaultTransportProtectionSchemes: [NIOSSHTransportProtection.Type] = [ |
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.
I'm not sure that there's a better alternative than this.
Sources/NIOSSH/SSHPacketParser.swift
Outdated
self.sequence = 0 | ||
} else { | ||
self.sequence += 1 | ||
} |
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.
This can be more succinctly spelled self.sequence &+= 1
.
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.
done, plus renamed to sequenceNumber
as per Joannis Orlandos's PR suggestion
} else { | ||
self.sequence += 1 | ||
} | ||
} |
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.
Same note here.
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.
done
@@ -79,7 +79,7 @@ extension AESGCMTransportProtection: NIOSSHTransportProtection { | |||
// unencrypted! | |||
} | |||
|
|||
func decryptAndVerifyRemainingPacket(_ source: inout ByteBuffer) throws -> ByteBuffer { | |||
func decryptAndVerifyRemainingPacket(_ source: inout ByteBuffer, sequence: UInt32) throws -> ByteBuffer { |
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.
Can we make it clear that AESGCM
doesn't need it, by using sequence _: UInt32
here and below?
0d7cf3a
to
7dba735
Compare
7dba735
to
89de739
Compare
89de739
to
a4d3621
Compare
@@ -60,11 +60,11 @@ struct SSHConnectionStateMachine { | |||
/// The state of this state machine. | |||
private var state: State | |||
|
|||
private static let defaultTransportProtectionSchemes: [NIOSSHTransportProtection.Type] = [ | |||
public static let bundledTransportProtectionSchemes: [NIOSSHTransportProtection.Type] = [ |
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.
I decided to make this public so clients can have an ability to specify a fully custom set of algorithms (i.e. exclude bundled). But if they need to add custom, they will be able to specify bundled
+ custom
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.
Ah, NIOSSHConnectionStateMachine
is not public, moved to SSHConnectionRole
6bfb945
to
a7e248c
Compare
|
||
/// Encrypt an entire outbound packet | ||
func encryptPacket(_ packet: NIOSSHEncryptablePayload, to outboundBuffer: inout ByteBuffer) throws | ||
func encryptPacket(_ packet: NIOSSHEncryptablePayload, sequenceNumber: UInt32, to outboundBuffer: inout ByteBuffer) throws |
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.
One thing I haven't solved yet, is how to use NIOSSHEncryptablePayload
to write tests for custom transport implementations. It uses internal type (SSHMessage), so there is no way to initialize it. I see three options:
- Make it a protocol
- Make it an enum (`SSHMessage|ByteBuffer)
- Make
encryptPacket
similar to decrypt, where we will be passing aninout
buffer with message already written there expecting client to encrypt it inplace
wdyt?
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.
(right now I'm in favour of 3 tbh)
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.
I think many implementation will begin with:
let packetLengthIndex = outboundBuffer.writerIndex
let packetLengthLength = MemoryLayout<UInt32>.size
let packetPaddingIndex = outboundBuffer.writerIndex + packetLengthLength
let packetPaddingLength = MemoryLayout<UInt8>.size
outboundBuffer.moveWriterIndex(forwardBy: packetLengthLength + packetPaddingLength)
maybe we should abstract in serializer?
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.
I suspect (3) is the right approach, yes. We can also abstract the write pattern in the serializer as well. Can I suggest that we do that as a separate PR though to avoid this one growing further?
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.
agreed 👍
@@ -87,10 +87,10 @@ protocol NIOSSHTransportProtection: AnyObject { | |||
/// length, the padding, or the MAC), and update source to indicate the consumed bytes. | |||
/// It must also perform any integrity checking that | |||
/// is required and throw if the integrity check fails. | |||
func decryptAndVerifyRemainingPacket(_ source: inout ByteBuffer) throws -> ByteBuffer | |||
func decryptAndVerifyRemainingPacket(_ source: inout ByteBuffer, sequenceNumber: UInt32) throws -> ByteBuffer |
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.
Actually, two additional questions before we make this API public:
- Why do we return a
ByteBuffer
here instead of in-place mutation? - There is a bit of a lost symmetry here, in plaintext mode padding and packet length are consumed by parser, but in ciphertext mode we have to do it manually, should this be handled by parser instead? And decryptors should only decrypt data in place and leave it? (Maybe we should drop mac bytes here?)
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.
I think parser can handle mac bytes, we have macBytes
property, we can use it to remove last bytes from the packet
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.
- As the doc comment says: it is expected to update the buffer to indicate what it consumed, and return the decrypted buffer. We avoid in-place mutation because we do not want to guarantee that all encryption algorithms have the exact same size input and output.
- The absence of symmetry is because in the input case we need to be able to decrypt just the length so we know how much further data to read, and we wanted to minimise the number of times we need to do that operation. I don't think the parser can do it for us because the actual length is affected by the protection scheme.
We could pass the MAC separately, but I'm not sure we gain much do we?
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.
let me think about this further, I'll revisit this together with refactoring encryptPacket
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.
Just for my clarification, did this work get done?
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.
I opened a draft here:
artemredkin#1
but so far I only refactored encryptPacket
, we probably should have two separate PRs for encryptPacket
and decryptAndVerifyRemainingPacket
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.
Ok great, so you'd like to have this PR merged without further changes to this and we'll revisit in a follow-on PR?
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.
yes! otherwise it's already quite large
Sources/NIOSSH/Role.swift
Outdated
@@ -14,6 +14,10 @@ | |||
|
|||
/// The role of a given party in an SSH connection. | |||
public enum SSHConnectionRole { | |||
public static let bundledTransportProtectionSchemes: [NIOSSHTransportProtection.Type] = [ |
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.
I don't think this makes sense on SSHConnectionRole
. This should go on config objects. We can avoid some duplication by using an internal central location.
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.
moved to Constants
@@ -87,10 +87,10 @@ protocol NIOSSHTransportProtection: AnyObject { | |||
/// length, the padding, or the MAC), and update source to indicate the consumed bytes. | |||
/// It must also perform any integrity checking that | |||
/// is required and throw if the integrity check fails. | |||
func decryptAndVerifyRemainingPacket(_ source: inout ByteBuffer) throws -> ByteBuffer | |||
func decryptAndVerifyRemainingPacket(_ source: inout ByteBuffer, sequenceNumber: UInt32) throws -> ByteBuffer |
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.
- As the doc comment says: it is expected to update the buffer to indicate what it consumed, and return the decrypted buffer. We avoid in-place mutation because we do not want to guarantee that all encryption algorithms have the exact same size input and output.
- The absence of symmetry is because in the input case we need to be able to decrypt just the length so we know how much further data to read, and we wanted to minimise the number of times we need to do that operation. I don't think the parser can do it for us because the actual length is affected by the protection scheme.
We could pass the MAC separately, but I'm not sure we gain much do we?
|
||
/// Encrypt an entire outbound packet | ||
func encryptPacket(_ packet: NIOSSHEncryptablePayload, to outboundBuffer: inout ByteBuffer) throws | ||
func encryptPacket(_ packet: NIOSSHEncryptablePayload, sequenceNumber: UInt32, to outboundBuffer: inout ByteBuffer) throws |
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.
I suspect (3) is the right approach, yes. We can also abstract the write pattern in the serializer as well. Can I suggest that we do that as a separate PR though to avoid this one growing further?
9a27935
to
e312215
Compare
e312215
to
2ced9ad
Compare
2ced9ad
to
0ac56ea
Compare
0ac56ea
to
0987147
Compare
43d91e4
to
c776967
Compare
Motivation: In some cases AES GCM might not be supported by a server/client, and clients might require custom algorithms to be implemented in their projects. Modifications: - Makes NIOSSHTransportProtection, NIOSSHSessionKeys and ExpectedKeySizes public - Makes transport algorithms list configurable - Adds sequence number of incoming and outgoing messages to parser and serializer - Tests
c776967
to
01e5a89
Compare
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.
Great, let's ship this. Thanks so much for the work!
Motivation:
In some cases AES GCM might not be supported by a server/client, and
clients might require custom algorithms to be implemented in their
projects.
Modifications:
serializer