Skip to content

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

Closed
raulk wants to merge 1 commit intomasterfrom
feat/noise-non-replayability-message-payload
Closed

noise-libp2p spec: rethink message data construction.#210
raulk wants to merge 1 commit intomasterfrom
feat/noise-non-replayability-message-payload

Conversation

@raulk
Copy link
Member

@raulk raulk commented Aug 24, 2019

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

@raulk raulk requested a review from yusefnapora August 24, 2019 21:56
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.

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.
@raulk raulk force-pushed the feat/noise-non-replayability-message-payload branch from 434a452 to 3b2ef9f Compare August 24, 2019 22:21
MUST rebuild the message payload with the new cryptographic material,
and resend it in the 3rd message.

To guard against replay attacks, these message payloads are signed with the
Copy link

@zmanian zmanian Aug 25, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't you be signing the HandshakeHash + the message payload to bind the signature to a specific channel.

Signing with an X25519 keys can get annoying. Signal protocol does it but X25519 to ed25519 is not provided by many implementations.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah easier to make the static keys Ed25519 if you want this.

@tarcieri
Copy link

@zmanian
Copy link

zmanian commented Aug 25, 2019

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.

Copy link
Contributor

@yusefnapora yusefnapora left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a good point that we're vulnerable to a MITM replay here. I also like the protobuf name changes and being explicit about when we're sending handshake messages.

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. Not that we should base the spec on a particular library, but it might be a common choice to make the remote ephemeral key private in other Noise implementations.

If we assume we do have access to the remote ephemeral key, we wouldn't necessarily need to use the ephemeral key to produce the signature. We could just include it in the data to be signed by the identity key, along with the static key. A MITM could replay the message, but they would need the original private ephemeral key to complete the handshake and wouldn't be able to substitute a new ephemeral key.

Signing the handshake hash (and possibly the entire handshake payload) as @zmanian suggests should work. The only downside is that we can't fully validate the connection until each side sends at least one transport message, since the handshake hash isn't available until after the handshake completes.

@tarcieri
Copy link

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

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

tarcieri commented Aug 26, 2019

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

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

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

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

raulk commented Aug 30, 2019

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

  • ❌ Some implementations may not expose the DH ephemeral key, so using that as signing material may not always be feasible.

  • ❌ Noise signatures are experimental and not supported in formally verified implementations. libp2p identity keys are also polymorphic, so that's probably a no-go.

  • ✅ Channel binding seems like a way forward, but it's not trivial because it requires additional data to be shared before the handshake is sound. However, I feel good about the following solution, which omits any additional round trips:

    The finaliser of the handshake (responder in IK, initiator in XX) generates the last Noise message and obtains the handshake hash. They sign it with the symmetric key and concatenate/pipeline it to the final Noise handshake message, length-prefixing it with a varint. The aggregate of these two payloads is written at once on the wire.

    <channel-binding-token>   ::= <varint-length-prefix> || <sign(noise-symmetric-key, noise-handshake-hash)>
    <aggregate-final-message> ::= <noise-last-message> || <channel-binding-token>
    

    The other party, on consuming the final handshake message, proceeds to consume the channel binding token. They derive the token locally, and assert that it matches the received one. If yes, both parties can be confident a MITM agent did not perturb the handshake. If no, they close the connection.

    This solution allows us to remove protobuf field 4 (data_sig), since any alteration would be caught by this procedure.

@raulk
Copy link
Member Author

raulk commented Aug 30, 2019

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

@zmanian
Copy link

zmanian commented Aug 30, 2019

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

raulk commented Aug 30, 2019

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

zmanian commented Aug 30, 2019

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

zmanian commented Aug 30, 2019

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

@raulk
Copy link
Member Author

raulk commented Aug 30, 2019

@zmanian how is that different to what I specified in #210 (comment)?

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

zmanian commented Sep 2, 2019

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

raulk commented Sep 2, 2019

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

zmanian commented Sep 2, 2019

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

zmanian commented Sep 3, 2019

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

raulk commented Sep 3, 2019

@zmanian yes, and that's precisely what I had suggested above. Maybe it's worth re-reading point 3 in this comment: #210 (comment).

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

@zmanian
Copy link

zmanian commented Sep 3, 2019

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

@raulk
Copy link
Member Author

raulk commented Dec 4, 2019

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

@raulk raulk closed this Dec 4, 2019
jxs pushed a commit to jxs/specs that referenced this pull request Aug 8, 2025
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.

5 participants