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.
This commit is contained in:
wwqgtxx committed 2026-07-16 09:04:31 +08:00
1 parent e3c1ea06a4
commit 5c1b8bfc31
3 files changed
+12 -88

No files matched your search

+1 -1
View File
@@ -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
+2 -2
View File
@@ -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=
+9 -85
View File
@@ -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) {