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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Standardise Noise handshake for libp2p #195

Closed
raulk opened this issue Jul 26, 2019 · 7 comments
Closed

Standardise Noise handshake for libp2p #195

raulk opened this issue Jul 26, 2019 · 7 comments
Assignees

Comments

@raulk
Copy link
Member

raulk commented Jul 26, 2019

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

  • rust-libp2p-noise: experimental support for IX, IK, KK handshake patterns in rust-libp2p with protocol suite Noise_{IK/IX/KK}_25519_ChaChaPoly_SHA256.
  • proposal for a hack project to introduce the Noise IX handshake in go-libp2p.

Some questions to answer

  • Which handshake patterns we'll support? These are valid ones:

    • IX: 1 RTT to be sound, exposes the initiator's public static key in clear text.
    • IK: 1 RTT to be sound, encrypts initiator's public static key with the responder's static key, providing initiator privacy, but requires previous knowledge of the initiator's public key. Might be suitable to use against well-known bootstrap nodes.
    • XX: 1.5 RTT to be sound, tradeoff is that no static key is exposed in plaintext without requiring prior knowledge of the responder's public key.
  • libp2p key support. Noise relies on ECDH operations and does not support RSA keys.

    • We could maybe shim support for RSA keys with the NN handshake (only ephemeral keys, no static keys), and leveraging Noise message data to transmit a signature of the ephemeral key with our libp2p RSA key.
    • rust-libp2p does the above, but they use IX, IK, XX, and generates a Curve25519 key on the spot to fill in for the static key. (see Standardise Noise handshake for libp2p聽#195 (comment) for correction)
    • The Noise framework does not mention P-256 and secp256k1 curves explicitly, but they're used in practice. See the Bitcoin lightning network specs and implementation.
    • I'm not sure how two nodes with non-matching key types would conduct a Noise handshake; we might have to fallback to NN in this case, with a linked identity.
    • Nodes could, theoretically, hold multiple keys (one per type), and pre-negotiate which curve to use (see next point). However, this would yield multiple effective identities, causing problems with peer IDs and univocal identification.
  • Noise protocol instantiations. Do we support a single suite (e.g. Noise_*_25519_ChaChaPoly_SHA256) or multiple? How do we select a suite?

  • Multiaddr support. A few of us have debated enhancing multiaddrs to announce the secure channels supported by the peer.

    • If we go ahead, we'd need to define what level of specificity to apply to Noise components (just noise, noise handshake noise/ix, the full suite noise/ix/25519/chachapoly/sha256), and how that choice will impact negotiation.
    • Lots of tradeoffs to analyse here.
  • Integration with QUIC (which presumes TLS 1.3). See nQUIC for Noise+QUIC.


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

tomaka commented Jul 26, 2019

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

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
Copy link
Contributor

romanb commented Jul 26, 2019

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
Copy link
Contributor

romanb commented Jul 26, 2019

(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 libp2p/rust-libp2p#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 libp2p/rust-libp2p#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
Copy link
Contributor

Thanks for clarifying all those points @romanb. Also, as a side note, the PR notes on libp2p/rust-libp2p#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鈥檚 also very similar to what we鈥檙e 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鈥檚 safe to also use for e.g secio.

@raulk
Copy link
Member Author

raulk commented Aug 1, 2019

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
Copy link
Contributor

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.

  • specify protobuf schema for payload
  • specify when the payload is sent (during which parts of the handshake)
  • link to peer-id spec for key and signature encoding rules

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

mxinden commented Apr 6, 2021

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

@mxinden mxinden closed this as completed Apr 6, 2021
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

No branches or pull requests

5 participants