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

Noise protocol framework implementation #142

Open
wants to merge 35 commits into
base: main
Choose a base branch
from
Open

Conversation

ethindp
Copy link
Collaborator

@ethindp ethindp commented Nov 18, 2024

This PR is the one where the Noise implementation will be tracked (see discussion #96 for more). It is reasonably expected that this PR will take quite a while to implement. As much as I would like to grant people network encryption immediately, this is not something that we can rush, lest we make critical mistakes that we cannot come back from.

This initial set of commit contains:

  • A drop-in public domain implementation of the Noise protocol framework, as defined by it's specification, minus:
    • Support for pre-shared keys;
    • Support for the fallback modifier or protocol switching;
    • Rekeying support; and
    • Only one ciphersuite is supported;
  • A couple updates to some deps and deps for the aforementioned implementation; and
  • Locking of the extra submodule at it's latest commit as of the time of writing.

The last two changes are ancillary at best though probably wise to do.

During the implementation phase, some questions need to be answered, as follows:

  1. What should the default configuration be (handshake pattern)? My research strongly advises against the NN pattern due to the lack of any kind of authentication. Any future transition away from that kind of default risks incompatibilities and other problems.
  2. What do we expose via the API? Ideally the API should handhold as much as possible; we do not want the end-user to be able to interact much with the underlying state machine as this could risk massive security failures if they do something wrong. At the same time, we wish to give users the ability to have some control (i.e., they need to load and export key pairs for example). Currently, the only change I have added is a boolean to turn off encryption during setup_client/setup_server/setup_local_server; the idea is that the transition should be decently smooth and only require at most an additional few lines to generate the initial key pairs (assuming we go with the XX pattern or similar) and then loading them.
  3. Do we figure out some kind of PKI equivalent, or leave this up to the end-user? Theoretically the use of static key pairs allows for mutual authentication of both client and server, which could enable far stronger protections against things such as ban evasion, but this question is an ancillary one and not necessarily relevant to this PR and could be discussed later, as doing any kind of PKI has it's own pros and cons.
  4. Do we figure out rekeying? If so, what is the criteria for that? Do we do it automatically, manually, or some hybrid combination?
  5. Is it worth implementing pre-shared key (PSK) support? It has some significant advantages but puts extra requirements on the shoulders of the end-user.
  6. Do we implement support for encryption of unreliable packets? This introduces significant overhead and issues that will need to be considered carefully, such as tracking those messages that have been encrypted or not to prevent replay attacks, out-of-order handshakes and so on.
  7. Do we perhaps at some future date seek a full security audit of this portion of the code? This is just a question that is more a curiosity at this point, though something that would definitely raise the confidence of anybody who uses it by a significant margin. However, security audits are something I am uncertain about, both in cost and who we would approach, so we can table this for later.

Internally, the general idea is that the overall execution of the cryptographic subsystem would work as follows:

  1. The user sets up a client or server, and loads the static key pair that they previously generated, if we choose to go that route. They MUST NOT need to specify the handshake pattern to use.
  2. When a connection is established and that event is received, the handshake is performed automatically without any input from the angelscript side of things; that is, the rest of the code needn't know or care that it's underlying packets are being encrypted.

At this point, the server or client could perform channel binding if it wishes to by retrieving the handshake hash. That in turn would allow the receiving party to authenticate the receiving party using that hash via some application-defined method such as signatures, passwords, etc., because the token is unique to each session and cannot be used for any other. This is entirely optional however.

So, yeah, lots to consider. I've also added in some other internal bits to the network class that shouldn't actually affect it in any way yet. I would've submitted a much fuller PR but I am uncertain about implementation details, hence me opening it early and raising these considerations. However, the base infrastructure is present, and I think this PR is ready for a collaborative development process now that the most complicated portion is reasonably complete.

@ethindp
Copy link
Collaborator Author

ethindp commented Nov 18, 2024

Update: so question 6 is partially solved, at least for ENet (for other protocols running over unreliable transports this is a case-by-case basis problem): the handshake needn't be performed via an unreliable stream.

@ethindp
Copy link
Collaborator Author

ethindp commented Dec 6, 2024

Okay, so I've done a lot of thinking, and with the current networking setup the implementation of the actual handshake will be trivial. The remaining question mainly has to do with the handshake pattern. Since the opening of this PR the noise implementation has undergone some significant overhauling, fixing several bugs and moving the handshake state parameters into a configuration struct so that we can store that and then create a new state on the fly whenever we need it.

The patterns I'm zooming in on are the KK, KN, XN, or XX patterns. I believe these might be the most suitable because:

  1. The KK and XX patterns provide mutual authentication of all clients and the server; and
  2. The KN and XN patterns provide authentication of the server, but not the client.

As a reminder, the letters in the pattern describe, for the server and client (in that order), the authentication levels:

  • N means no authentication is performed; the connection is only encrypted.
  • K means that the public key is known to the remote party via some out-of-band mechanism
  • X means that the keys are transmitted during the handshake.

If we use the process of elimination here, the KK or KN patterns make the most sense, since it can be reasonably assumed that a developer who's making a networked application has control over both the code of the client and server applications. Thus, the developer knows both keys on both ends. The downside to the KK pattern is that, to be effective, the developer would need to add a key generation step for each client installation, and figure out how to permanently store it, since an ephemeral key defeats the purpose of "knowing" anything. This however would be a one-time thing and wouldn't need to be done again, and key pairs are only 64 bytes (32 bytes for the private key and 32 bytes for the public key) so are prime candidates for encoding via something like hex or base64. The KN pattern might however be the simplest to start with: the developer can assume that the client cannot be trusted, as is probably a good idea anyway, but can also authenticate the server. Should that fail, then we know that either the connection has been tampered with or the server's key pair has changed. However, the authentication failure doesn't present itself in an obvious manner (it causes an encryption/decryption failure), but I question whether this will actually be an issue, since if we do encounter this problem we can safely assume that a critical error has occurred, and immediately abort.

The advantage to the XN or XX patterns is that, although the keys are known ahead-of-time, they are transmitted during the handshake. That in turn allows game developers to perform their own form of mutual authentication as they see fit, but places the burden of actually doing this onto them, and I'd like this to be as low-overhead as possible. (Mutual authentication can take any form that the developer wants, and usually boils down to "is there a reason to reject this public key?".) Obviously, this would enable many other possibilities as well, but better to start simple and focus on extending this as time goes on.

I would appreciate any input/thoughts/opinions. I'm doing my best to not over-complicate this entire process while at the same time being thoughtful about it. My idea is that, if you want this to be used, you need only do two extra things at most: (1) set up some key generation method in your app as a one-time thing, and (2) add, after setup_{client|server|local_server}, a call to network::load_key_pair, passing in two or three arguments (depending on whether this is a server or client), those being the local private key, the local public key, and the remote party's public key. Everything else will be taken care of automatically, and an exception will be thrown if something goes wrong. Of course, the setup_* methods will take a boolean to turn this off, so if you determine that you don't need it, you need only add true to the end of a call to those functions to opt out of it.

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

Successfully merging this pull request may close these issues.

2 participants