libp2p / specs

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

noise-libp2p spec: rethink message data construction. #210

Closed raulk closed 4 years ago

raulk commented 5 years ago

The current message data construction is not secure enough, as it is vulnerable to replay attacks by a MITM agent that records previous handshakes, and transplants old user data from those handshakes into new ones, as long as the static key hasn't changed.

We fix this vulnerability by introducing a signature over the whole message payload against the ephemeral session key. This "seals" the payload so that it's only valid for that exchange.

Also, this PR simplifies protobuf field naming.

Finally, we formalise in which Noise messages of IK and XX the message payload is to be shared, to guarantee secrecy, integrity and authentication.

CC @noot @ansermino @wildmolasses @burdges @kirushik @zmanian

tarcieri commented 5 years ago

Have you taken a look at https://github.com/noiseprotocol/noise_sig_spec/blob/master/output/noise_sig.pdf ?

zmanian commented 5 years ago

So this has been largely a never ending back and forth about how to include the application layer identity key in the handshake.

I am pretty firmly in the sign the channel binding token camp vs the underutilized noise signature extensions.

tarcieri commented 5 years ago

This solution might be hard to implement in some cases. rust-libp2p uses a Noise crate called snow; I skimmed the docs for the HandshakeState struct, and it doesn't expose the remote ephemeral key. [...] Signing the handshake hash (and possibly the entire handshake payload)

You don't need to verify the remote ephemeral key directly, or do anything more than sign the handshake hash. It's explicitly designed for this purpose. See:

https://noiseprotocol.org/noise.html#channel-binding

yusefnapora commented 5 years ago

Thanks @tarcieri, that makes sense. It sounds like the channel binding token is the simplest way to solve this.

Can you help me clarify my thinking about this? I'm worried that I'm missing something.

As I understand it, the first IK message is vulnerable to replay because "there's no ephemeral contribution from the recipient" (from payload security).

But there is an ephemeral contribution from the sender, which seems like it should prevent @raulk's scenario. A MITM could extract the handshake payload from an initial IK message, but if they transplant it into a new handshake message with a different ephemeral keypair, the recipient won't be able to decrypt it and will abort.

Is that right, or have I been thinking about this wrong?

tarcieri commented 5 years ago

As I understand it, the first IK message is vulnerable to replay because "there's no ephemeral contribution from the recipient" (from payload security).

The handshake hash is computed when the handshake is fully completed, and authenticates the entire message sequence of the handshake. From the table in the aforementioned payload security section:

                          Source         Destination

IK
  <- s
  ...
  -> e, es, s, ss           1                2
  <- e, ee, se              2                4
  ->                        2                5
  <-                        2                5

This means at the end of the handshake you have the following security properties, with channel binding to a digital signature key:

Source (2.): Sender authentication resistant to key-compromise impersonation (KCI) Destination (5.): Encryption to a known recipient, strong forward secrecy.

If there are any discrepancies in the transcript between either side, the handshake hash won't match.

yusefnapora commented 5 years ago

Thanks again @tarcieri!

I realized that my thinking about this attack scenario was flawed. I was imagining a MITM extracting the encrypted payload and transplanting it directly into another message, which I don't think is directly possible thanks to the senders ephemeral key contributing to the encryption.

However, an active attacker could initiate an XX handshake using any ephemeral key, and the responder will send their handshake payload encrypted with DH's using only the key provided by the attacker. Once the cleartext payload has been obtained, they can stick it into any handshake message and it will be accepted if the responder's static key is unchanged.

@raulk what do you think about the channel binding solution? I was hoping to figure something else out to avoid requiring an exchange of transport messages before the connection is sound... IMO if we're going to require that, we might as well just send all the libp2p data in the first transport message instead of using handshake payloads at all.

tarcieri commented 5 years ago

@yusefnapora there's an inherent tradeoff between 0-RTT and a replay defense.

There are a few solutions, either exchanging an ephemeral key in advance ("pre-keys"), or fancy new research like puncturable encryption.

I'd suggest just completing the 1-RTT handshake to begin with, and once you have that working, investigating various options for 0-RTT.

yusefnapora commented 5 years ago

Gah, I just re-read my last comment and am second guessing myself yet again!

To use the cleartext payload obtained from a speculative XX handshake, the attacker would also need the private static Noise key, or the signature of the static in the payload would be invalid.

This seems like a good time to get a cup of tea and think for a bit 😄

raulk commented 5 years ago

After a chat with @yusefnapora, I suggest this way forward:

raulk commented 5 years ago

@zmanian @tarcieri @noot @ansermino @wildmolasses – WDYT?

zmanian commented 5 years ago

I am confused by what you mean by sign(noise-symmetric-key, noise-handshake-hash)

This could either mean eddsa_sign(privatekey, noise-symmetric-key || noise-handshake-hash or it could mean hmac(noise-symmetric-key, noise-handshake-hash)

In either of these constructions, the noise-symmetric-key is redundant because the noise-handshake-hash commits to the symmetric key.

raulk commented 5 years ago

it could mean hmac(noise-symmetric-key, noise-handshake-hash)

Yeah I guess a MAC would suffice since the we're already authenticating a hash.

In either of these constructions, the noise-symmetric-key is redundant because the noise-handshake-hash commits to the symmetric key.

Is it? If we don't authenticate the handshake hash with our symmetric key, how do we protect against a MITM?

Unless we encrypt the handshake hash. Since both ends will have already derived the symmetric key, this is feasible.

zmanian commented 5 years ago

What I would reccomend is you transmit the signature of the handshake hash but not handshake hash itself.

The receiver then verifies the signature against their instance of handshake hash and the public key.

if the signature doesn't verify, drop the connection.

zmanian commented 5 years ago

you don't need send the hashshake hash because the receiver already has it if the channel is secure.

raulk commented 5 years ago

@zmanian how is that different to what I specified in https://github.com/libp2p/specs/pull/210#issuecomment-526663594?

<sign(noise-symmetric-key, noise-handshake-hash)> is precisely the signature of the noise-handshake-hash with the noise-symmetric-key.

Forgive me for lack of precision (not a cryptographer, and glad that we have @tarcieri here!), but sign would be the EdDSA signature with the 25519 symmetric key we've derived from the handshake.

zmanian commented 5 years ago

I think the most helpful thing I can do here is make a pull request to your branch that would bring the suggested spec in line with my thoughts.

raulk commented 5 years ago

@zmanian the diff of this PR no longer matches the discussion we've had here. I'm gonna close this PR and submit a new one capturing the above. You can then suggest your changes on the incoming one.

zmanian commented 5 years ago
  1. NoiseSignedHandshakePayload should only contain 1 signature field. The public key for for this signature is provided in NoiseHandshakePayload.

  2. The HandshakeHash MUST never be sent over the wire

  3. All data in NoiseHandshakePayload is concatenated with HandshakeHash prior to signing.

The important thing to remember is the sender and reciever will only generate an identical HandshakeHash if a secure channel and nonreplayable channel exists between the sender and receiver.

If the signature in NoiseSignedHandshakePayload fails, the connection should be immediately dropped.

zmanian commented 5 years ago

My suggestion is that earliest we send a signature is after the handshake has been completed. If we want to send the rest of the Payload in earlier handshake message, this seems fine to me.

raulk commented 5 years ago

@zmanian yes, and that's precisely what I had suggested above. Maybe it's worth re-reading point 3 in this comment: https://github.com/libp2p/specs/pull/210#issuecomment-526663594.

We might be going around in circles now; it will all become clearer once I post the spec update reflecting the discussion.

zmanian commented 5 years ago

I think we are in alignment as well. I just wanted to document it.

raulk commented 4 years ago

Superseded by #234. Thanks for your input here, it's been carried over to that PR.