Skip to content

Commit b01312b

Browse files
committed
Add reality ML-DSA-65 verification
- Add `mldsa65_verify` option to REALITY outbound configuration. - Implement ML-DSA-65 signature verification in `VerifyPeerCertificate` to verify certificate's extra extensions. - Add Trace logging for the ML-DSA-65 verification process. - Add unit tests to verify the ML-DSA-65 signature logic. - Follow Xray-core PR #4915 implementation.
1 parent 3367bde commit b01312b

9 files changed

Lines changed: 413 additions & 196 deletions

File tree

common/tls/reality_client.go

Lines changed: 69 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,20 @@ import (
3838
aTLS "github.com/sagernet/sing/common/tls"
3939

4040
utls "github.com/metacubex/utls"
41+
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
4142
"golang.org/x/crypto/hkdf"
4243
"golang.org/x/net/http2"
4344
)
4445

4546
var _ ConfigCompat = (*RealityClientConfig)(nil)
4647

4748
type RealityClientConfig struct {
48-
ctx context.Context
49-
uClient *UTLSClientConfig
50-
publicKey []byte
51-
shortID [8]byte
49+
ctx context.Context
50+
logger logger.ContextLogger
51+
uClient *UTLSClientConfig
52+
publicKey []byte
53+
shortID [8]byte
54+
mldsa65Verify []byte
5255
}
5356

5457
func NewRealityClient(ctx context.Context, logger logger.ContextLogger, serverAddress string, options option.OutboundTLSOptions) (Config, error) {
@@ -84,7 +87,18 @@ func newRealityClient(ctx context.Context, logger logger.ContextLogger, serverAd
8487
return nil, E.New("invalid short_id")
8588
}
8689

87-
var config Config = &RealityClientConfig{ctx, uClient.(*UTLSClientConfig), publicKey, shortID}
90+
var mldsa65Verify []byte
91+
if options.Reality.Mldsa65Verify != "" {
92+
mldsa65Verify, err = base64.RawURLEncoding.DecodeString(options.Reality.Mldsa65Verify)
93+
if err != nil {
94+
return nil, E.Cause(err, "decode mldsa65_verify")
95+
}
96+
if len(mldsa65Verify) != 1952 {
97+
return nil, E.New("invalid mldsa65_verify")
98+
}
99+
}
100+
101+
var config Config = &RealityClientConfig{ctx, logger, uClient.(*UTLSClientConfig), publicKey, shortID, mldsa65Verify}
88102
if options.KernelRx || options.KernelTx {
89103
if !C.IsLinux {
90104
return nil, E.New("kTLS is only supported on Linux")
@@ -133,7 +147,9 @@ func (e *RealityClientConfig) Client(conn net.Conn) (Conn, error) {
133147

134148
func (e *RealityClientConfig) ClientHandshake(ctx context.Context, conn net.Conn) (aTLS.Conn, error) {
135149
verifier := &realityVerifier{
136-
serverName: e.uClient.ServerName(),
150+
serverName: e.uClient.ServerName(),
151+
mldsa65Verify: e.mldsa65Verify,
152+
logger: e.logger,
137153
}
138154
uConfig := e.uClient.config.Clone()
139155
uConfig.InsecureSkipVerify = true
@@ -268,17 +284,21 @@ func realityClientFallback(ctx context.Context, uConn net.Conn, serverName strin
268284
func (e *RealityClientConfig) Clone() Config {
269285
return &RealityClientConfig{
270286
e.ctx,
287+
e.logger,
271288
e.uClient.Clone().(*UTLSClientConfig),
272289
e.publicKey,
273290
e.shortID,
291+
e.mldsa65Verify,
274292
}
275293
}
276294

277295
type realityVerifier struct {
278296
*utls.UConn
279-
serverName string
280-
authKey []byte
281-
verified bool
297+
serverName string
298+
authKey []byte
299+
verified bool
300+
mldsa65Verify []byte
301+
logger logger.ContextLogger
282302
}
283303

284304
func (c *realityVerifier) VerifyPeerCertificate(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
@@ -288,8 +308,46 @@ func (c *realityVerifier) VerifyPeerCertificate(rawCerts [][]byte, verifiedChain
288308
h := hmac.New(sha512.New, c.authKey)
289309
h.Write(pub)
290310
if bytes.Equal(h.Sum(nil), certs[0].Signature) {
291-
c.verified = true
292-
return nil
311+
if len(c.mldsa65Verify) > 0 {
312+
if c.logger != nil {
313+
c.logger.Trace("REALITY: verifying certificate with ML-DSA-65")
314+
}
315+
if debug.Enabled {
316+
fmt.Printf("REALITY: verifying certificate with ML-DSA-65\n")
317+
}
318+
if len(certs[0].Extensions) > 0 {
319+
h.Write(c.HandshakeState.Hello.Raw)
320+
h.Write(c.HandshakeState.ServerHello.Raw)
321+
verify, err := mldsa65.Scheme().UnmarshalBinaryPublicKey(c.mldsa65Verify)
322+
if err != nil {
323+
if c.logger != nil {
324+
c.logger.Trace("REALITY: failed to unmarshal ML-DSA-65 public key")
325+
}
326+
return E.Cause(err, "unmarshal ML-DSA-65 public key")
327+
}
328+
if mldsa65.Verify(verify.(*mldsa65.PublicKey), h.Sum(nil), nil, certs[0].Extensions[0].Value) {
329+
if c.logger != nil {
330+
c.logger.Trace("REALITY: ML-DSA-65 verification succeeded")
331+
}
332+
if debug.Enabled {
333+
fmt.Printf("REALITY: ML-DSA-65 verification succeeded\n")
334+
}
335+
c.verified = true
336+
return nil
337+
} else {
338+
if c.logger != nil {
339+
c.logger.Trace("REALITY: ML-DSA-65 verification failed")
340+
}
341+
}
342+
} else {
343+
if c.logger != nil {
344+
c.logger.Trace("REALITY: certificate has no extensions for ML-DSA-65 signature")
345+
}
346+
}
347+
} else {
348+
c.verified = true
349+
return nil
350+
}
293351
}
294352
}
295353
opts := x509.VerifyOptions{

common/tls/reality_mldsa_test.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
//go:build with_utls
2+
3+
package tls
4+
5+
import (
6+
"crypto/ed25519"
7+
"crypto/hmac"
8+
"crypto/sha512"
9+
"crypto/x509"
10+
"crypto/x509/pkix"
11+
"reflect"
12+
"testing"
13+
"unsafe"
14+
15+
"github.com/cloudflare/circl/sign/mldsa/mldsa65"
16+
utls "github.com/metacubex/utls"
17+
"github.com/stretchr/testify/require"
18+
)
19+
20+
func TestMLDSA65Verifier(t *testing.T) {
21+
pub, priv, err := mldsa65.GenerateKey(nil)
22+
require.NoError(t, err)
23+
pubBinary, _ := pub.MarshalBinary()
24+
25+
// Mock AuthKey and peer cert public key
26+
authKey := make([]byte, 32)
27+
peerPubKey, _, _ := ed25519.GenerateKey(nil)
28+
29+
helloRaw := make([]byte, 100)
30+
serverHelloRaw := make([]byte, 100)
31+
32+
h := hmac.New(sha512.New, authKey)
33+
h.Write(peerPubKey)
34+
certSignature := h.Sum(nil)
35+
36+
h.Write(helloRaw)
37+
h.Write(serverHelloRaw)
38+
mldsaMsg := h.Sum(nil)
39+
40+
scheme := mldsa65.Scheme()
41+
mldsaSig := scheme.Sign(priv, mldsaMsg, nil)
42+
43+
cert := &x509.Certificate{
44+
PublicKey: peerPubKey,
45+
Signature: certSignature,
46+
Extensions: []pkix.Extension{
47+
{
48+
Id: []int{1, 2, 3}, // Dummy OID
49+
Value: mldsaSig,
50+
},
51+
},
52+
}
53+
54+
verifier := &realityVerifier{
55+
authKey: authKey,
56+
mldsa65Verify: pubBinary,
57+
UConn: &utls.UConn{
58+
Conn: &utls.Conn{},
59+
},
60+
}
61+
62+
// Set peerCertificates using unsafe as sing-box does
63+
p, _ := reflect.TypeOf(verifier.Conn).Elem().FieldByName("peerCertificates")
64+
*(*([]*x509.Certificate))(unsafe.Pointer(uintptr(unsafe.Pointer(verifier.Conn)) + p.Offset)) = []*x509.Certificate{cert}
65+
66+
// Use reflection to set Hello and ServerHello since types are not easily nameable
67+
hType := reflect.TypeOf(verifier.HandshakeState.Hello).Elem()
68+
hVal := reflect.New(hType)
69+
hVal.Elem().FieldByName("Raw").SetBytes(helloRaw)
70+
reflect.ValueOf(&verifier.HandshakeState).Elem().FieldByName("Hello").Set(hVal)
71+
72+
shType := reflect.TypeOf(verifier.HandshakeState.ServerHello).Elem()
73+
shVal := reflect.New(shType)
74+
shVal.Elem().FieldByName("Raw").SetBytes(serverHelloRaw)
75+
reflect.ValueOf(&verifier.HandshakeState).Elem().FieldByName("ServerHello").Set(shVal)
76+
77+
err = verifier.VerifyPeerCertificate(nil, nil)
78+
require.NoError(t, err)
79+
require.True(t, verifier.verified)
80+
}

docs/configuration/shared/tls.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ icon: material/new-box
88
:material-plus: [handshake_timeout](#handshake_timeout)
99
:material-plus: [spoof](#spoof)
1010
:material-plus: [spoof_method](#spoof_method)
11+
:material-plus: [reality.mldsa65_verify](#mldsa65_verify)
1112
:material-delete-clock: [acme](#acme-fields)
1213

1314
!!! quote "Changes in sing-box 1.13.0"
@@ -151,7 +152,8 @@ icon: material/new-box
151152
"reality": {
152153
"enabled": false,
153154
"public_key": "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0",
154-
"short_id": "0123456789abcdef"
155+
"short_id": "0123456789abcdef",
156+
"mldsa65_verify": ""
155157
}
156158
}
157159
```
@@ -799,3 +801,11 @@ A hexadecimal string with zero to eight digits.
799801
The maximum time difference between the server and the client.
800802

801803
Check disabled if empty.
804+
805+
#### mldsa65_verify
806+
807+
!!! question "Since sing-box 1.14.0"
808+
809+
==Client only==
810+
811+
A 1952 bytes ML-DSA-65 public key in base64 format, used to verify the additional post-quantum signature in the first certificate extension.

docs/configuration/shared/tls.zh.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ icon: material/new-box
88
:material-plus: [handshake_timeout](#handshake_timeout)
99
:material-plus: [spoof](#spoof)
1010
:material-plus: [spoof_method](#spoof_method)
11+
:material-plus: [reality.mldsa65_verify](#mldsa65_verify)
1112
:material-delete-clock: [acme](#acme-字段)
1213

1314
!!! quote "sing-box 1.13.0 中的更改"
@@ -151,7 +152,8 @@ icon: material/new-box
151152
"reality": {
152153
"enabled": false,
153154
"public_key": "jNXHt1yRo0vDuchQlIP6Z0ZvjT3KtzVI-T4E7RoLJS0",
154-
"short_id": "0123456789abcdef"
155+
"short_id": "0123456789abcdef",
156+
"mldsa65_verify": ""
155157
}
156158
}
157159
```
@@ -786,3 +788,11 @@ ACME DNS01 验证字段。如果配置,将禁用其他验证方法。
786788
服务器和客户端之间的最大时间差。
787789

788790
如果为空则禁用检查。
791+
792+
#### mldsa65_verify
793+
794+
!!! question "自 sing-box 1.14.0 起"
795+
796+
==仅客户端==
797+
798+
Base64 格式的 1952 字节 ML-DSA-65 公钥,用于验证证书第一个扩展中的额外后量子签名。

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ require (
7373
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
7474
github.com/andybalholm/brotli v1.1.0 // indirect
7575
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
76+
github.com/cloudflare/circl v1.6.3 // indirect
7677
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
7778
github.com/coreos/go-oidc/v3 v3.17.0 // indirect
7879
github.com/database64128/netx-go v0.1.1 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
2424
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
2525
github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
2626
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
27+
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
28+
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
2729
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
2830
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
2931
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=

option/tls.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,8 @@ type OutboundUTLSOptions struct {
241241
}
242242

243243
type OutboundRealityOptions struct {
244-
Enabled bool `json:"enabled,omitempty"`
245-
PublicKey string `json:"public_key,omitempty"`
246-
ShortID string `json:"short_id,omitempty"`
244+
Enabled bool `json:"enabled,omitempty"`
245+
PublicKey string `json:"public_key,omitempty"`
246+
ShortID string `json:"short_id,omitempty"`
247+
Mldsa65Verify string `json:"mldsa65_verify,omitempty"`
247248
}

0 commit comments

Comments
 (0)