From 6176cc473c775967810331a221d1434b62540585 Mon Sep 17 00:00:00 2001 From: sinku Date: Thu, 6 Aug 2026 15:29:29 -0400 Subject: [PATCH 1/3] fix: preserve KeyShareKeys on repeated ApplyPreset --- u_parrots.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/u_parrots.go b/u_parrots.go index 9b6c5b61b8..4ae91ff2b0 100644 --- a/u_parrots.go +++ b/u_parrots.go @@ -3039,7 +3039,10 @@ func (uconn *UConn) ApplyPreset(p *ClientHelloSpec) error { uconn.HandshakeState.Hello = privateHello.getPublicPtr() if clientKeySharePrivate != nil { uconn.HandshakeState.State13.KeyShareKeys = clientKeySharePrivate.ToPublic() - } else { + } else if uconn.HandshakeState.State13.KeyShareKeys == nil { + // When BuildHandshakeStateWithoutSession is called before + // BuildHandshakeState, the spec is reused and KeyShareExtension data is + // already populated. uconn.HandshakeState.State13.KeyShareKeys = &KeySharePrivateKeys{} } uconn.echCtx = ech From 293ee127bc10c1f80daa48ffc533577bc504ee40 Mon Sep 17 00:00:00 2001 From: sinku Date: Thu, 6 Aug 2026 15:30:08 -0400 Subject: [PATCH 2/3] fix: keep private keys for secondary keyshares --- key_schedule.go | 13 +++++++++++++ u_parrots.go | 15 +++++++++++++++ u_public.go | 21 ++++++++++++--------- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/key_schedule.go b/key_schedule.go index 9656369524..4865b646c3 100644 --- a/key_schedule.go +++ b/key_schedule.go @@ -56,6 +56,13 @@ type keySharePrivateKeys struct { mlkem crypto.Decapsulator mlkemEcdhe *ecdh.PrivateKey // [uTLS] seperate ecdhe key for pq keyshare in line with Chrome, instead of reusing ecdhe key like stdlib + + // [uTLS] ecdheFallback stores private keys for additional (non-preferred) + // key shares sent in the ClientHello. When a spec advertises multiple + // classical key shares (e.g. X25519 + P256 in Firefox profiles), only the + // first is stored in ecdhe. If the server selects a different curve, + // clientSharedSecret falls back to this map. + ecdheFallback map[CurveID]*ecdh.PrivateKey } // A keyExchange implements a TLS 1.3 KEM. @@ -167,6 +174,12 @@ func (ke *ecdhKeyExchange) clientSharedSecretChooseEcdhe(priv *keySharePrivateKe return nil, errors.New("tls: missing separate hybrid ECDHE private key") } ecdhe = priv.mlkemEcdhe + } else if ecdhe == nil || ecdhe.Curve() != ke.curve { + // The server selected a curve other than the preferred key share. + // Use the matching secondary key if one was generated. + if fallback, ok := priv.ecdheFallback[ke.id]; ok { + ecdhe = fallback + } } if ecdhe == nil { return nil, errors.New("tls: missing ECDHE private key") diff --git a/u_parrots.go b/u_parrots.go index 4ae91ff2b0..757859087e 100644 --- a/u_parrots.go +++ b/u_parrots.go @@ -3206,6 +3206,13 @@ func (uconn *UConn) ApplyPreset(p *ClientHelloSpec) error { // only do this once for the first non-grease curve uconn.HandshakeState.State13.KeyShareKeys.Ecdhe = reusedKey preferredCurveIsSet = true + } else { + // Store the reused key as a fallback for this + // non-preferred curve + if uconn.HandshakeState.State13.KeyShareKeys.EcdheFallback == nil { + uconn.HandshakeState.State13.KeyShareKeys.EcdheFallback = make(map[CurveID]*ecdh.PrivateKey) + } + uconn.HandshakeState.State13.KeyShareKeys.EcdheFallback[curveID] = reusedKey } continue } @@ -3221,6 +3228,14 @@ func (uconn *UConn) ApplyPreset(p *ClientHelloSpec) error { // only do this once for the first non-grease curve uconn.HandshakeState.State13.KeyShareKeys.Ecdhe = ecdheKey preferredCurveIsSet = true + } else { + // Store secondary key shares so the correct private key + // is available if the server selects a non-preferred + // curve + if uconn.HandshakeState.State13.KeyShareKeys.EcdheFallback == nil { + uconn.HandshakeState.State13.KeyShareKeys.EcdheFallback = make(map[CurveID]*ecdh.PrivateKey) + } + uconn.HandshakeState.State13.KeyShareKeys.EcdheFallback[curveID] = ecdheKey } } } diff --git a/u_public.go b/u_public.go index f19cf91ed1..f560db550b 100644 --- a/u_public.go +++ b/u_public.go @@ -925,9 +925,10 @@ func (kpk *kemPrivateKey) ToPublic() *KemPrivateKey { type KeySharePrivateKeys struct { // CurveID CurveID - Ecdhe *ecdh.PrivateKey - Mlkem crypto.Decapsulator - MlkemEcdhe *ecdh.PrivateKey + Ecdhe *ecdh.PrivateKey + Mlkem crypto.Decapsulator + MlkemEcdhe *ecdh.PrivateKey + EcdheFallback map[CurveID]*ecdh.PrivateKey // [uTLS] secondary key shares for non-preferred curves } func (ksp *KeySharePrivateKeys) ToPrivate() *keySharePrivateKeys { @@ -936,9 +937,10 @@ func (ksp *KeySharePrivateKeys) ToPrivate() *keySharePrivateKeys { } return &keySharePrivateKeys{ // curveID: ksp.CurveID, - ecdhe: ksp.Ecdhe, - mlkem: ksp.Mlkem, - mlkemEcdhe: ksp.MlkemEcdhe, + ecdhe: ksp.Ecdhe, + mlkem: ksp.Mlkem, + mlkemEcdhe: ksp.MlkemEcdhe, + ecdheFallback: ksp.EcdheFallback, } } @@ -948,8 +950,9 @@ func (ksp *keySharePrivateKeys) ToPublic() *KeySharePrivateKeys { } return &KeySharePrivateKeys{ // CurveID: ksp.curveID, - Ecdhe: ksp.ecdhe, - Mlkem: ksp.mlkem, - MlkemEcdhe: ksp.mlkemEcdhe, + Ecdhe: ksp.ecdhe, + Mlkem: ksp.mlkem, + MlkemEcdhe: ksp.mlkemEcdhe, + EcdheFallback: ksp.ecdheFallback, } } From cd25377899c80f476ce675ffbe1520f0def55b31 Mon Sep 17 00:00:00 2001 From: sinku Date: Thu, 24 Sep 2026 11:56:40 -0400 Subject: [PATCH 3/3] test: server selects secondary key share --- u_parrots_test.go | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/u_parrots_test.go b/u_parrots_test.go index c600cd3ad2..38485e1b43 100644 --- a/u_parrots_test.go +++ b/u_parrots_test.go @@ -286,3 +286,40 @@ func TestApplyPresetDoesNotMutateOriginalSpec(t *testing.T) { } } } + +func TestSecondaryKeyShareSelectedByServer(t *testing.T) { + // These fingerprints offer key_share: X25519, P-256. + for _, helloID := range []ClientHelloID{HelloFirefox_105, HelloFirefox_120, HelloFirefox_148} { + t.Run(helloID.Str(), func(t *testing.T) { + serverConfig := testConfig.Clone() + serverConfig.CurvePreferences = []CurveID{CurveP256} + + clientConfig := testConfig.Clone() + clientConfig.ServerName = "example.com" + clientConfig.InsecureSkipVerify = true + + clientConn, serverConn := localPipe(t) + server := Server(serverConn, serverConfig) + client := UClient(clientConn, clientConfig, helloID) + defer server.Close() + defer client.Close() + + serverErr := make(chan error, 1) + go func() { serverErr <- server.Handshake() }() + if err := client.Handshake(); err != nil { + t.Fatalf("client handshake: %v", err) + } + if err := <-serverErr; err != nil { + t.Fatalf("server handshake: %v", err) + } + + cs := client.ConnectionState() + if cs.CurveID != CurveP256 { + t.Fatalf("negotiated %v, want P-256", cs.CurveID) + } + if cs.HelloRetryRequest { + t.Fatal("unexpected HelloRetryRequest; the offered P-256 share should have been used") + } + }) + } +}