From 5c1b8bfc3105e5f07ee5708fed3250de9069dfef Mon Sep 17 00:00:00 2001 From: wwqgtxx Date: Thu, 16 Jul 2026 09:00:54 +0800 Subject: [PATCH] chore: authenticate JLS ServerHello using its wire encoding JLS authenticates the serialized ServerHello with only the random field zeroed. The previous marshalForJLS implementation reordered extensions to match rustls-jls's internal struct order, even when that order differed from the bytes sent on the wire. That canonical order is not defined by TLS or the JLS specification and couples this package to a rustls-jls implementation detail. Use the normal marshal output when sending and the original wire bytes when receiving, preserving extension order and leaving the TLS transcript untouched. This intentionally removes compatibility with rustls-jls clients that re-encode ServerHello extensions before JLS verification. --- go.mod | 2 +- go.sum | 4 +- transport/jls/utls.go | 94 +++++-------------------------------------- 3 files changed, 12 insertions(+), 88 deletions(-) diff --git a/go.mod b/go.mod index cffb067a..cf588985 100644 --- a/go.mod +++ b/go.mod @@ -24,7 +24,7 @@ require ( github.com/metacubex/gopacket v1.1.20-0.20230608035415-7e2f98a3e759 github.com/metacubex/http v0.1.6 github.com/metacubex/jls-quic-go v0.0.0-20260712113821-d34e6f9b4c7f - github.com/metacubex/jls-tls v0.0.0-20260712171131-15870a03ac51 + github.com/metacubex/jls-tls v0.0.0-20260716010121-e13c4e54a728 github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 github.com/metacubex/mhurl v0.1.0 github.com/metacubex/mlkem v0.1.0 diff --git a/go.sum b/go.sum index b219457e..b9fd2733 100644 --- a/go.sum +++ b/go.sum @@ -133,8 +133,8 @@ github.com/metacubex/http v0.1.6 h1:xvXuvXMCMxCWMF5nEJF4yiKvXL+p2atWMzs37e80m1I= github.com/metacubex/http v0.1.6/go.mod h1:Nxx0zZAo2AhRfanyL+fmmK6ACMtVsfpwIl1aFAik2Eg= github.com/metacubex/jls-quic-go v0.0.0-20260712113821-d34e6f9b4c7f h1:ywMFrpfRTU6oiRYhn2PGefm/RWdtdwsD13sDFmrJ6+s= github.com/metacubex/jls-quic-go v0.0.0-20260712113821-d34e6f9b4c7f/go.mod h1:fXVJbX1dv67OpH+jGaecl7DYRj6KDLwOya8OSUPBIxo= -github.com/metacubex/jls-tls v0.0.0-20260712171131-15870a03ac51 h1:xTFfQ+fybBXCl8mkp6ojGp+tCrfIa2ZEqSrx+4unTVM= -github.com/metacubex/jls-tls v0.0.0-20260712171131-15870a03ac51/go.mod h1:mmqs889W/TqPlfNRDa2UyJvRiLyiTJIEnWHkcj3SKB8= +github.com/metacubex/jls-tls v0.0.0-20260716010121-e13c4e54a728 h1:4IbvO5xjKMJLs5GmLl0fYI+lDcJxAoZ3hJtaqq2/YC0= +github.com/metacubex/jls-tls v0.0.0-20260716010121-e13c4e54a728/go.mod h1:mmqs889W/TqPlfNRDa2UyJvRiLyiTJIEnWHkcj3SKB8= github.com/metacubex/jsonv2 v0.0.0-20260518173308-f4597c22f1df h1:S0vBzqjXok24VopstOgPd1JdgglW9tXehrqvwpQWbQ8= github.com/metacubex/jsonv2 v0.0.0-20260518173308-f4597c22f1df/go.mod h1:F4sVXat6QjPXkNsKRDyyG3BhSkxPFFnRPEIwmmyCgbg= github.com/metacubex/kcp-go v0.0.0-20260105040817-550693377604 h1:hJwCVlE3ojViC35MGHB+FBr8TuIf3BUFn2EQ1VIamsI= diff --git a/transport/jls/utls.go b/transport/jls/utls.go index 88db4a52..8006d1eb 100644 --- a/transport/jls/utls.go +++ b/transport/jls/utls.go @@ -18,20 +18,16 @@ import ( "github.com/metacubex/http" "github.com/metacubex/randv2" utls "github.com/metacubex/utls" - "golang.org/x/crypto/cryptobyte" ) const ( - jlsClientHelloType = 1 - jlsServerHelloType = 2 - jlsHandshakeHeaderLen = 4 - jlsHelloLegacyVersionLen = 2 - jlsHelloRandomLen = 32 - jlsHelloRandomOffset = jlsHandshakeHeaderLen + jlsHelloLegacyVersionLen - jlsRandomSeedLen = jlsHelloRandomLen / 2 - jlsExtensionPreSharedKey = 41 - jlsExtensionSupportedVers = 43 - jlsExtensionKeyShare = 51 + jlsClientHelloType = 1 + jlsServerHelloType = 2 + jlsHandshakeHeaderLen = 4 + jlsHelloLegacyVersionLen = 2 + jlsHelloRandomLen = 32 + jlsHelloRandomOffset = jlsHandshakeHeaderLen + jlsHelloLegacyVersionLen + jlsRandomSeedLen = jlsHelloRandomLen / 2 ) func newUTLSClient(ctx context.Context, conn net.Conn, config *ClientConfig) (net.Conn, bool, error) { @@ -254,80 +250,8 @@ func jlsServerHelloAuthData(raw []byte) ([]byte, error) { if err != nil { return nil, err } - - s := cryptobyte.String(msg) - var messageType, compressionMethod uint8 - var legacyVersion, cipherSuite uint16 - var body, sessionID, extensions cryptobyte.String - var random []byte - if !s.ReadUint8(&messageType) || - !s.ReadUint24LengthPrefixed(&body) || - !s.Empty() || - !body.ReadUint16(&legacyVersion) || - !body.ReadBytes(&random, jlsHelloRandomLen) || - !body.ReadUint8LengthPrefixed(&sessionID) || - !body.ReadUint16(&cipherSuite) || - !body.ReadUint8(&compressionMethod) || - !body.ReadUint16LengthPrefixed(&extensions) || - !body.Empty() { - return nil, errors.New("jls: invalid uTLS server hello") - } - - type extension struct { - typeID uint16 - wire []byte - } - extensionBytes := extensions - parsed := make([]extension, 0, 3) - for len(extensions) > 0 { - remaining := extensions - var typeID uint16 - var data cryptobyte.String - if !extensions.ReadUint16(&typeID) || !extensions.ReadUint16LengthPrefixed(&data) { - return nil, errors.New("jls: invalid uTLS server hello extensions") - } - wireLen := len(remaining) - len(extensions) - parsed = append(parsed, extension{typeID: typeID, wire: remaining[:wireLen]}) - } - - // rustls decodes ServerHello extensions into fields and serializes them in - // this order when it calculates the JLS random. The wire order is irrelevant - // to TLS but not to JLS, whose authentication input must match byte for byte. - canonicalTypes := [...]uint16{ - jlsExtensionKeyShare, - jlsExtensionPreSharedKey, - jlsExtensionSupportedVers, - } - canonical := make(map[uint16][]byte, len(canonicalTypes)) - for _, ext := range parsed { - for _, typeID := range canonicalTypes { - if ext.typeID == typeID { - canonical[typeID] = ext.wire - break - } - } - } - - // Reinsert the canonical extensions at their first original position. Their - // encoded bytes and all unrelated extensions remain untouched, and the total - // length does not change. - extensionOffset := len(msg) - len(extensionBytes) - result := append([]byte(nil), msg[:extensionOffset]...) - canonicalWritten := false - for _, ext := range parsed { - if _, ok := canonical[ext.typeID]; ok { - if !canonicalWritten { - for _, typeID := range canonicalTypes { - result = append(result, canonical[typeID]...) - } - canonicalWritten = true - } - continue - } - result = append(result, ext.wire...) - } - zeroJLSBytes(result[jlsHelloRandomOffset : jlsHelloRandomOffset+jlsHelloRandomLen]) - return result, nil + zeroJLSBytes(msg[jlsHelloRandomOffset : jlsHelloRandomOffset+jlsHelloRandomLen]) + return msg, nil } func cloneJLSHello(raw []byte, messageType byte) ([]byte, error) {