Closed raulk closed 4 years ago
Have you taken a look at https://github.com/noiseprotocol/noise_sig_spec/blob/master/output/noise_sig.pdf ?
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.
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:
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?
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.
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.
@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.
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 😄
After a chat with @yusefnapora, I suggest this way forward:
✅ 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.
@zmanian @tarcieri @noot @ansermino @wildmolasses – WDYT?
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.
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.
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.
you don't need send the hashshake hash because the receiver already has it if the channel is secure.
@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.
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.
@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.
NoiseSignedHandshakePayload should only contain 1 signature field. The public key for for this signature is provided in NoiseHandshakePayload.
The HandshakeHash MUST never be sent over the wire
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.
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.
@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.
I think we are in alignment as well. I just wanted to document it.
Superseded by #234. Thanks for your input here, it's been carried over to that PR.
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