mirror of https://lore.kernel.org/lkml/
 help / color / mirror / Atom feed
From: "Jérémy Jean" <jeremy.jean@oss.cyber.gouv.fr>
To: "Jason A. Donenfeld" <Jason@zx2c4.com>
Cc: wireguard@lists.zx2c4.com, netdev@vger.kernel.org,
	Andrew Lunn <andrew+netdev@lunn.ch>,
	"David S. Miller" <davem@davemloft.net>,
	Eric Dumazet <edumazet@kernel.org>,
	Jakub Kicinski <kuba@kernel.org>, Paolo Abeni <pabeni@redhat.com>,
	linux-kernel@vger.kernel.org, stable@vger.kernel.org
Subject: Re: [PATCH net] wireguard: noise: reject responses for replaced initiations
Date: Tue, 06 Oct 2026 14:40:36 +0200	[thread overview]
Message-ID: <fbeae2e157e335504157b6a6683fb505@oss.cyber.gouv.fr> (raw)
In-Reply-To: <asSo7kefA3IQFpQp@zx2c4.com>

On 2026-10-06 09:53, Jason A. Donenfeld wrote:
> On Mon, Oct 05, 2026 at 11:18:33PM +0200, Jason A. Donenfeld wrote:
>> Hi,
>> 
>> On Mon, Oct 05, 2026 at 08:35:55PM +0000, Jérémy Jean wrote:
>> > WireGuard can accept an old handshake response after starting a new
>> > handshake. This reinstalls old keys and resets transport counters and
>> > replay state, enabling nonce reuse, replay and packet forgery. This
>> > breaks confidentiality and integrity guarantees.
>> >
>> > Compare ephemeral secrets under the write lock to reject responses for
>> > replaced initiations.
>> >
>> > Fixes: e7096c131e51 ("net: WireGuard secure network tunnel")
>> > Cc: stable@vger.kernel.org
>> > Assisted-by: LLM
>> > Signed-off-by: Jérémy Jean <Jeremy.Jean@oss.cyber.gouv.fr>
>> > ---
>> >  drivers/net/wireguard/noise.c | 8 +++++---
>> >  1 file changed, 5 insertions(+), 3 deletions(-)
>> >
>> > diff --git a/drivers/net/wireguard/noise.c b/drivers/net/wireguard/noise.c
>> > index 9c0a09bf6c95..88cc9acc7dc7 100644
>> > --- a/drivers/net/wireguard/noise.c
>> > +++ b/drivers/net/wireguard/noise.c
>> > @@ -784,10 +784,12 @@ wg_noise_handshake_consume_response(struct message_handshake_response *src,
>> >
>> >  	/* Success! Copy everything to peer */
>> >  	down_write(&handshake->lock);
>> > -	/* It's important to check that the state is still the same, while we
>> > -	 * have an exclusive lock.
>> > +	/* Check that this is still the initiation we authenticated against,
>> > +	 * while we have an exclusive lock.
>> >  	 */
>> > -	if (handshake->state != state) {
>> > +	if (handshake->state != state ||
>> > +	    crypto_memneq(handshake->ephemeral_private, ephemeral_private,
>> > +			  NOISE_PUBLIC_KEY_LEN)) {
>> >  		up_write(&handshake->lock);
>> >  		goto fail;
>> >  	}
>> 
>> Could you describe the flow that you think causes a bug? Trying
>> to recreate your mental model. Something like this?
>> 
>> == thread 1 ==
>> 
>> 	down_read(&handshake->lock);
>> 	state = handshake->state;
>> 	memcpy(hash, handshake->hash, NOISE_HASH_LEN);
>> 	memcpy(chaining_key, handshake->chaining_key, NOISE_HASH_LEN);
>> 	memcpy(ephemeral_private, handshake->ephemeral_private,
>> 	       NOISE_PUBLIC_KEY_LEN);
>> 	memcpy(preshared_key, handshake->preshared_key,
>> 	       NOISE_SYMMETRIC_KEY_LEN);
>> 	up_read(&handshake->lock);
>> 
>> 	if (state != HANDSHAKE_CREATED_INITIATION)
>> 		goto fail;
>> 
>> 	/* e */
>> 	message_ephemeral(e, src->unencrypted_ephemeral, chaining_key, hash);
>> 
>> 	/* ee */
>> 	if (!mix_dh(chaining_key, NULL, ephemeral_private, e))
>> 		goto fail;
>> 
>> 	/* se */
>> 	if (!mix_dh(chaining_key, NULL, wg->static_identity.static_private, 
>> e))
>> 		goto fail;
>> 
>> 	/* psk */
>> 	mix_psk(chaining_key, hash, key, preshared_key);
>> 
>> 	/* {} */
>> 	if (!message_decrypt(NULL, src->encrypted_nothing,
>> 			     sizeof(src->encrypted_nothing), key, hash))
>> 		goto fail;
>> 
>> == thread 2 ==
>> 
>> 	down_read(&handshake->static_identity->lock);
>> 	down_write(&handshake->lock);
>> 
>> 	if (unlikely(!handshake->static_identity->has_identity))
>> 		goto out;
>> 
>> 	dst->header.type = cpu_to_le32(MESSAGE_HANDSHAKE_INITIATION);
>> 
>> 	handshake_init(handshake->chaining_key, handshake->hash,
>> 		       handshake->remote_static);
>> 
>> 	/* e */
>> 	curve25519_generate_secret(handshake->ephemeral_private);
>> 	if (!curve25519_generate_public(dst->unencrypted_ephemeral,
>> 					handshake->ephemeral_private))
>> 		goto out;
>> 	message_ephemeral(dst->unencrypted_ephemeral,
>> 			  dst->unencrypted_ephemeral, handshake->chaining_key,
>> 			  handshake->hash);
>> 
>> 	/* es */
>> 	if (!mix_dh(handshake->chaining_key, key, 
>> handshake->ephemeral_private,
>> 		    handshake->remote_static))
>> 		goto out;
>> 
>> 	/* s */
>> 	message_encrypt(dst->encrypted_static,
>> 			handshake->static_identity->static_public,
>> 			NOISE_PUBLIC_KEY_LEN, key, handshake->hash);
>> 
>> 	/* ss */
>> 	if (!mix_precomputed_dh(handshake->chaining_key, key,
>> 				handshake->precomputed_static_static))
>> 		goto out;
>> 
>> 	/* {t} */
>> 	tai64n_now(timestamp);
>> 	message_encrypt(dst->encrypted_timestamp, timestamp,
>> 			NOISE_TIMESTAMP_LEN, key, handshake->hash);
>> 
>> 	dst->sender_index = wg_index_hashtable_insert(
>> 		handshake->entry.peer->device->index_hashtable,
>> 		&handshake->entry);
>> 
>> 	handshake->state = HANDSHAKE_CREATED_INITIATION;
>> 	ret = true;
>> 
>> out:
>> 	up_write(&handshake->lock);
>> 	up_read(&handshake->static_identity->lock);
>> 
>> == thread 1 ==
>> 
>> 	down_write(&handshake->lock);
>> 	/* It's important to check that the state is still the same, while we
>> 	 * have an exclusive lock.
>> 	 */
>> 	if (handshake->state != state) {
>> 		up_write(&handshake->lock);
>> 		goto fail;
>> 	}
>> 	memcpy(handshake->remote_ephemeral, e, NOISE_PUBLIC_KEY_LEN);
>> 	memcpy(handshake->hash, hash, NOISE_HASH_LEN);
>> 	memcpy(handshake->chaining_key, chaining_key, NOISE_HASH_LEN);
>> 	handshake->remote_index = src->sender_index;
>> 	handshake->state = HANDSHAKE_CONSUMED_RESPONSE;
>> 	up_write(&handshake->lock);
>> 	ret_peer = peer;
>> 	goto out;
>> 
>> And now begin_session is called on the older completed handshake 
>> rather
>> than the half-completed newer handshake?
>> 
>> Or did you see some other flow?
> 
> Okay I think I worked something plausible out:
> 
> - two threads begin processing the same response
> - the first succeeds. the second gets halfway, when it blocks on taking
>   a lock that a queued initiation has taken
> - the queued initiation does its thing and resets the state
> - the second thread resumes and completes the old handshake and
>   reinstalls keys
> 
> I'll continue analyzing real world feasibility, but in all cases, thank
> you for the patch.
> 
> Jason

Hello Jason,

Thanks for your quick reply. Sorry that I could not answer in time.
Your analysis is indeed the scenario that I had in mind, yet, the
second thread does not really have to block, I'd say it can just
be slow?

As for real world feasibility and impacts, I must admit that
I have no idea.

Regards,
Jérémy

  reply	other threads:[~2026-10-06 12:40 UTC|newest]

Thread overview: 6+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-05 20:35 Jérémy Jean
2026-10-05 20:43 ` netdev-bot+sinfo
2026-10-05 21:18 ` Jason A. Donenfeld
2026-10-06  7:53   ` Jason A. Donenfeld
2026-10-06 12:40     ` Jérémy Jean [this message]
2026-10-06 15:18       ` Jason A. Donenfeld

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=fbeae2e157e335504157b6a6683fb505@oss.cyber.gouv.fr \
    --to=jeremy.jean@oss.cyber.gouv.fr \
    --cc=Jason@zx2c4.com \
    --cc=andrew+netdev@lunn.ch \
    --cc=davem@davemloft.net \
    --cc=edumazet@kernel.org \
    --cc=kuba@kernel.org \
    --cc=linux-kernel@vger.kernel.org \
    --cc=netdev@vger.kernel.org \
    --cc=pabeni@redhat.com \
    --cc=stable@vger.kernel.org \
    --cc=wireguard@lists.zx2c4.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox

all inboxes | Powered by JetHome®