libp2p / specs

Technical specifications for the libp2p networking stack
https://libp2p.io
1.58k stars 275 forks source link

Standardise Noise handshake for libp2p #195

Closed raulk closed 3 years ago

raulk commented 5 years ago

There have been isolated initiatives to implement a Noise handshake for libp2p, and it's time to standardise on a common approach for interoperability 🎉

Prior art

Some questions to answer


Editor note. I found the 25519 nomenclature a tad confusing. This clarifies the difference between X25519, Ed25519, Curve25519; excerpt:

  • "X25519" is the recommended Montgomery-X-coordinate DH function.
  • "Ed25519" is the recommended Edwards-coordinate signature system.
  • "Curve25519" is the underlying elliptic curve.
tomaka commented 5 years ago

(cc'ing @romanb)

To sum things up, there are two possibilities:

In other words, it's a choice to make: do we stop being generic over the format of the node's public key, or do we go against the design of the Noise protocol? I'm personally in favour of option 1.

Noise protocol instantiations. Do we support a single suite (e.g. Noise_*_25519_ChaChaPoly_SHA256) or multiple? How do we select a suite?

In rust-libp2p, the suite is in the protocol name. We have three protocol implemented: /noise/ix/25519/chachapoly/sha256/0.1.0, /noise/xx/25519/chachapoly/sha256/0.1.0, and /noise/ik/25519/chachapoly/sha256/0.1.0.

romanb commented 5 years ago

The rust-libp2p does the above, but they use IX, IK, KK, and generate a Curve25519 key on the spot to fill in for the static key.

rust-libp2p does not do that and it does not currently support KK, but XX. If and when a new static DH key is generated, as well as its lifetime, is left to the application. It just supports either to reuse an ed25519 signing keypair or to authenticate a separate static DH keypair via any libp2p identity keypair. I think in substrate, the static DH key is generated and signed with the node's (persistent) identity keypair when the node starts and persists in-memory for the lifetime of the process.

I'm not sure how two nodes with non-matching key types would conduct a Noise handshake;

The implementation in rust-libp2p should indeed currently support that, since the public identity keys are sent along as noise handshake payloads in addition to the static DH keys and the type of identity keys used is not part of the (static) handshake pattern name.

romanb commented 5 years ago

(cc'ing @romanb)

To sum things up, there are two possibilities:

  • Force the libp2p node key (the one in the PeerId) to be ed25519. If it's not, then Noise simply wouldn't be supported.

You make it sound like using Noise with an Ed25519 keypair is a triviality, but I don't think that is true. Ed25519 keypairs and Curve25519 keypairs for use with X25519 are not in one-to-one correspondence. An ed25519 keypair can be converted to a Curve25519 keypair but that cannot be reversed, hence the need to transmit the public identity key (in the handshake payloads) even when reusing an ed25519 keypair for x25519 in rust-libp2p.

  • Replace the static key with an ephemeral key signed with the actual static key. That's kind of what secio is doing, and what rust-libp2p is doing at the moment. But that's kind of against the design of Noise.

I think this is misleading and to my knowledge is neither what rust-libp2p does nor what substrate does. The static key is not replaced by an ephemeral key. Static DH keys in Noise are by definition the keys that are reused across multiple Noise handshake executions whereas ephemeral keys are by definition used in a single handshake. rust-libp2p-noise indeed only supports handshake patterns also involving static DH keys. The fact that a static DH keypair only persists for e.g. the lifetime of a process does not make it an ephemeral keypair as far as the Noise protocol is concerned. As I mentioned in https://github.com/libp2p/rust-libp2p/pull/1027 I don't agree that signing the static public DH keys with separate long-lived signature keys is "against the design of Noise", but a legitimate extension making use of the handshake payloads.

In other words, it's a choice to make: do we stop being generic over the format of the node's public key, or do we go against the design of the Noise protocol? I'm personally in favour of option 1.

Personally, I think it very desirable to have a Noise integration that works with any libp2p identity keypair. As I mentioned in https://github.com/libp2p/rust-libp2p/pull/1027, using handshakes from the Noise Signatures Extension Spec may be preferable to the current implementation in rust-libp2p in order to remove the indirection over static DH keys, though I think that it would preclude handshakes between nodes with different types of signature keypairs, as these types seem to be fixed for a particular such handshake pattern.

yusefnapora commented 5 years ago

Thanks for clarifying all those points @romanb. Also, as a side note, the PR notes on https://github.com/libp2p/rust-libp2p/pull/1027 are excellent, so thanks for that.

It does look like embedding a certificate in the handshake payload is suggested by the spec as a means of authenticating the static public key. It’s also very similar to what we’re doing for TLS 1.3 - we embed the identity key and a signature over the TLS session key into a certificate and send it in the handshake.

Also, it may not be desireable to use the identity key as the Noise static key, even if it is of a compatible key type. The security considerations section of the spec (linked above) says

Reusing a Noise static key pair outside of Noise would require extremely careful analysis to ensure the uses don't compromise each other, and security proofs are preserved

Which suggests that if the identity key is used as the Noise static key, it would be difficult to prove that it’s safe to also use for e.g secio.

raulk commented 5 years ago

Thanks a lot @tomaka @romanb @yusefnapora for contributing to the discussion here.

  1. The Noise Protocol Framework does indeed red flag reusing the static key elsewhere. Therefore, our option of constraining Noise to libp2p Ed25519 identity keys on the basis of reuse is frowned upon. So I'm +1 to discarding this solution.

  2. By default, the static key should be owned by the Noise layer. If the application possesses a compatible key they want to feed in, this must be an opt-in.

  3. The Noise layer could persist the key across restarts, or it could generate a new one with every restart.

  4. We should model a signature chain like we've done with TLS 1.3 to pass as message data. It isn't fresh in my head, but this could look like:

sign(sign(sign(libp2p_public_key, libp2p_key), noise_ephem_key), noise_static_key)
  1. The above is a dumbed down, incorrect version just for illustration purposes. We should definitely prefer using Noise Signatures, as @romanb suggests.

  2. We should consider Noise Pipes for secure 0-RTT data if we know the other party's static key from prior handshakes, and graceful fallback if we don't.

  3. I don't think Noise supports session resumption. Unless the PSK variants are catering for that, assuming our PSK is the ECDH key of our previous session. Any clues?

yusefnapora commented 5 years ago

I agree that we shouldn't try to use libp2p identity keys as Noise static keys. It seems simplest to just always use the libp2p identity key to sign the Noise static key, regardless of key type.

Regarding the Noise Signatures spec, it does look like basically what we want, but as @romanb mentions, the signature algorithm is fixed in the handshake type. So two peers with different identity key types would not be able to perform the handshake with each other.

Below is a rough outline that assumes that we're essentially codifying the rust-libp2p behavior, except that there's no special treatment of ed25519 keys. I can start writing it up next week, if there's no major objections, or retool it if there are.

Intro & Context

What is Noise, why do we want it, etc.

Supported ciphers & hashes

rust-libp2p always uses ChaChaPoly and SHA2-265.

Is there a good reason to also support AESGCM (hardware support, maybe)?

Supported Handshake Patterns

rust-libp2p currently supports IK, IX, and XX.

Noise Pipes describes a "compound protocol" using XX, IK, and XX+fallback to switch from a failed IK handshake to XX. This should enable 0-RTT handshakes once multiselect/2.0 is fully spec'd / implemented / deployed.

I propose we spec out support for XX, IK, and XX+fallback to enable the Noise Pipes 0-RTT pattern. We should specify the fallback behavior - e.g. if Bob fails to decrypt an inbound IK handshake from Alice, he initiates an XX+fallback handshake to Alice using the ephemeral key from Alice's failed IK message.

Should we also support IX?

Authenticating Noise static keys using libp2p identities

Describe and specify the transmission of the libp2p identity key and signature of Noise static key that is sent in the handshake payload.

Note that the lifetime of the Noise static keys is application-specific. libp2p will generate a static keypair when the Noise transport is initialized if none is provided.

Protocol / Handshake negotiation

How do we negotiate which supported handshake to use? To play nice with the rest of libp2p, we should probably just use multistream / multiselect. I think the current rust-libp2p protocol id naming convention is pretty good, e.g. /noise/ix/25519/chachapoly/sha256/0.1.0.

Message Framing

Noise messages have a maximum size of 65535 bytes, which makes it simple to delimit them on the wire. We can simply prefix all Noise messages with their length in bytes, encoded as a 16-bit int (network order). This is what rust-libp2p is doing & is recommended by the framework spec.

QUIC support

We should think about this some more :) nQUIC seems to only support the IK handshake, although they mention that it could be trivially altered to support XK as well. In either case, the responder's static key needs to be known in advance.

mxinden commented 3 years ago

I am closing here since the main goal - "Standardise Noise handshake for libp2p" - has been achieved with https://github.com/libp2p/specs/pull/202 and https://github.com/libp2p/specs/pull/260. I would suggest tracking any future improvements in separate Github issues.