MITM: Allow using local received SNI in the outgoing serverName & verifyPeerCertInNames

https://github.com/XTLS/Xray-core/issues/4348#issuecomment-2637370175

Local received SNI was sent by browser/app.

In freedom RAW's `tlsSettings`, set `"serverName": "fromMitm"` to forward it to the real website.

In freedom RAW's `tlsSettings`, set `"verifyPeerCertInNames": ["fromMitm"]` to use all possible names to verify the certificate.
This commit is contained in:
RPRX 2025-02-06 07:37:30 +00:00 committed by GitHub
parent 9b7841178a
commit c6a31f457c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 150 additions and 85 deletions

View file

@ -9,6 +9,7 @@ import (
"crypto/x509"
"encoding/base64"
"os"
"slices"
"strings"
"sync"
"time"
@ -277,10 +278,35 @@ func (c *Config) parseServerName() string {
return c.ServerName
}
func (c *Config) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if c.PinnedPeerCertificateChainSha256 != nil {
func (r *RandCarrier) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
if r.VerifyPeerCertInNames != nil {
if len(r.VerifyPeerCertInNames) > 0 {
certs := make([]*x509.Certificate, len(rawCerts))
for i, asn1Data := range rawCerts {
certs[i], _ = x509.ParseCertificate(asn1Data)
}
opts := x509.VerifyOptions{
Roots: r.RootCAs,
CurrentTime: time.Now(),
Intermediates: x509.NewCertPool(),
}
for _, cert := range certs[1:] {
opts.Intermediates.AddCert(cert)
}
for _, opts.DNSName = range r.VerifyPeerCertInNames {
if _, err := certs[0].Verify(opts); err == nil {
return nil
}
}
}
if r.PinnedPeerCertificateChainSha256 == nil {
errors.New("peer cert is invalid.")
}
}
if r.PinnedPeerCertificateChainSha256 != nil {
hashValue := GenerateCertChainHash(rawCerts)
for _, v := range c.PinnedPeerCertificateChainSha256 {
for _, v := range r.PinnedPeerCertificateChainSha256 {
if hmac.Equal(hashValue, v) {
return nil
}
@ -288,11 +314,11 @@ func (c *Config) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509.Cert
return errors.New("peer cert is unrecognized: ", base64.StdEncoding.EncodeToString(hashValue))
}
if c.PinnedPeerCertificatePublicKeySha256 != nil {
if r.PinnedPeerCertificatePublicKeySha256 != nil {
for _, v := range verifiedChains {
for _, cert := range v {
publicHash := GenerateCertPublicKeyHash(cert)
for _, c := range c.PinnedPeerCertificatePublicKeySha256 {
for _, c := range r.PinnedPeerCertificatePublicKeySha256 {
if hmac.Equal(publicHash, c) {
return nil
}
@ -305,7 +331,10 @@ func (c *Config) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509.Cert
}
type RandCarrier struct {
ServerNameToVerify string
RootCAs *x509.CertPool
VerifyPeerCertInNames []string
PinnedPeerCertificateChainSha256 [][]byte
PinnedPeerCertificatePublicKeySha256 [][]byte
}
func (r *RandCarrier) Read(p []byte) (n int, err error) {
@ -329,16 +358,25 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
}
}
randCarrier := &RandCarrier{
RootCAs: root,
VerifyPeerCertInNames: slices.Clone(c.VerifyPeerCertInNames),
PinnedPeerCertificateChainSha256: c.PinnedPeerCertificateChainSha256,
PinnedPeerCertificatePublicKeySha256: c.PinnedPeerCertificatePublicKeySha256,
}
config := &tls.Config{
Rand: &RandCarrier{
ServerNameToVerify: c.ServerNameToVerify,
},
Rand: randCarrier,
ClientSessionCache: globalSessionCache,
RootCAs: root,
InsecureSkipVerify: c.AllowInsecure,
NextProtos: c.NextProtocol,
NextProtos: slices.Clone(c.NextProtocol),
SessionTicketsDisabled: !c.EnableSessionResumption,
VerifyPeerCertificate: c.verifyPeerCert,
VerifyPeerCertificate: randCarrier.verifyPeerCert,
}
if len(c.VerifyPeerCertInNames) > 0 {
config.InsecureSkipVerify = true
} else {
randCarrier.VerifyPeerCertInNames = nil
}
for _, opt := range opts {

View file

@ -202,20 +202,21 @@ type Config struct {
// TLS Client Hello fingerprint (uTLS).
Fingerprint string `protobuf:"bytes,11,opt,name=fingerprint,proto3" json:"fingerprint,omitempty"`
RejectUnknownSni bool `protobuf:"varint,12,opt,name=reject_unknown_sni,json=rejectUnknownSni,proto3" json:"reject_unknown_sni,omitempty"`
// @Document A pinned certificate chain sha256 hash.
// @Document If the server's hash does not match this value, the connection will be aborted.
// @Document This value replace allow_insecure.
// @Document Some certificate chain sha256 hashes.
// @Document After normal validation or allow_insecure, if the server's cert chain hash does not match any of these values, the connection will be aborted.
// @Critical
PinnedPeerCertificateChainSha256 [][]byte `protobuf:"bytes,13,rep,name=pinned_peer_certificate_chain_sha256,json=pinnedPeerCertificateChainSha256,proto3" json:"pinned_peer_certificate_chain_sha256,omitempty"`
// @Document A pinned certificate public key sha256 hash.
// @Document If the server's public key hash does not match this value, the connection will be aborted.
// @Document This value replace allow_insecure.
// @Document Some certificate public key sha256 hashes.
// @Document After normal validation (required), if the verified cert's public key hash does not match any of these values, the connection will be aborted.
// @Critical
PinnedPeerCertificatePublicKeySha256 [][]byte `protobuf:"bytes,14,rep,name=pinned_peer_certificate_public_key_sha256,json=pinnedPeerCertificatePublicKeySha256,proto3" json:"pinned_peer_certificate_public_key_sha256,omitempty"`
MasterKeyLog string `protobuf:"bytes,15,opt,name=master_key_log,json=masterKeyLog,proto3" json:"master_key_log,omitempty"`
// Lists of string as CurvePreferences values.
CurvePreferences []string `protobuf:"bytes,16,rep,name=curve_preferences,json=curvePreferences,proto3" json:"curve_preferences,omitempty"`
ServerNameToVerify string `protobuf:"bytes,17,opt,name=server_name_to_verify,json=serverNameToVerify,proto3" json:"server_name_to_verify,omitempty"`
CurvePreferences []string `protobuf:"bytes,16,rep,name=curve_preferences,json=curvePreferences,proto3" json:"curve_preferences,omitempty"`
// @Document Replaces server_name to verify the peer cert.
// @Document After allow_insecure (automatically), if the server's cert can't be verified by any of these names, pinned_peer_certificate_chain_sha256 will be tried.
// @Critical
VerifyPeerCertInNames []string `protobuf:"bytes,17,rep,name=verify_peer_cert_in_names,json=verifyPeerCertInNames,proto3" json:"verify_peer_cert_in_names,omitempty"`
}
func (x *Config) Reset() {
@ -353,11 +354,11 @@ func (x *Config) GetCurvePreferences() []string {
return nil
}
func (x *Config) GetServerNameToVerify() string {
func (x *Config) GetVerifyPeerCertInNames() []string {
if x != nil {
return x.ServerNameToVerify
return x.VerifyPeerCertInNames
}
return ""
return nil
}
var File_transport_internet_tls_config_proto protoreflect.FileDescriptor
@ -391,7 +392,7 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{
0x4e, 0x43, 0x49, 0x50, 0x48, 0x45, 0x52, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x14, 0x0a,
0x10, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, 0x5f, 0x56, 0x45, 0x52, 0x49, 0x46,
0x59, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59,
0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0x93, 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e,
0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0x9a, 0x06, 0x0a, 0x06, 0x43, 0x6f, 0x6e,
0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x6e, 0x73,
0x65, 0x63, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d, 0x61, 0x6c, 0x6c,
0x6f, 0x77, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x12, 0x4a, 0x0a, 0x0b, 0x63, 0x65,
@ -437,18 +438,19 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{
0x52, 0x0c, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x4b, 0x65, 0x79, 0x4c, 0x6f, 0x67, 0x12, 0x2b,
0x0a, 0x11, 0x63, 0x75, 0x72, 0x76, 0x65, 0x5f, 0x70, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e,
0x63, 0x65, 0x73, 0x18, 0x10, 0x20, 0x03, 0x28, 0x09, 0x52, 0x10, 0x63, 0x75, 0x72, 0x76, 0x65,
0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x31, 0x0a, 0x15, 0x73,
0x65, 0x72, 0x76, 0x65, 0x72, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x5f, 0x74, 0x6f, 0x5f, 0x76, 0x65,
0x72, 0x69, 0x66, 0x79, 0x18, 0x11, 0x20, 0x01, 0x28, 0x09, 0x52, 0x12, 0x73, 0x65, 0x72, 0x76,
0x65, 0x72, 0x4e, 0x61, 0x6d, 0x65, 0x54, 0x6f, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x42, 0x73,
0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73,
0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, 0x6c,
0x73, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f,
0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79, 0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x74,
0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65,
0x74, 0x2f, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58, 0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61,
0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e,
0x54, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
0x50, 0x72, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x38, 0x0a, 0x19, 0x76,
0x65, 0x72, 0x69, 0x66, 0x79, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x63, 0x65, 0x72, 0x74, 0x5f,
0x69, 0x6e, 0x5f, 0x6e, 0x61, 0x6d, 0x65, 0x73, 0x18, 0x11, 0x20, 0x03, 0x28, 0x09, 0x52, 0x15,
0x76, 0x65, 0x72, 0x69, 0x66, 0x79, 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x49, 0x6e,
0x4e, 0x61, 0x6d, 0x65, 0x73, 0x42, 0x73, 0x0a, 0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61,
0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65,
0x72, 0x6e, 0x65, 0x74, 0x2e, 0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68,
0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78, 0x74, 0x6c, 0x73, 0x2f, 0x78, 0x72, 0x61, 0x79,
0x2d, 0x63, 0x6f, 0x72, 0x65, 0x2f, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2f,
0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2f, 0x74, 0x6c, 0x73, 0xaa, 0x02, 0x1b, 0x58,
0x72, 0x61, 0x79, 0x2e, 0x54, 0x72, 0x61, 0x6e, 0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x49, 0x6e,
0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x54, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
}
var (

View file

@ -69,16 +69,14 @@ message Config {
bool reject_unknown_sni = 12;
/* @Document A pinned certificate chain sha256 hash.
@Document If the server's hash does not match this value, the connection will be aborted.
@Document This value replace allow_insecure.
/* @Document Some certificate chain sha256 hashes.
@Document After normal validation or allow_insecure, if the server's cert chain hash does not match any of these values, the connection will be aborted.
@Critical
*/
repeated bytes pinned_peer_certificate_chain_sha256 = 13;
/* @Document A pinned certificate public key sha256 hash.
@Document If the server's public key hash does not match this value, the connection will be aborted.
@Document This value replace allow_insecure.
/* @Document Some certificate public key sha256 hashes.
@Document After normal validation (required), if the verified cert's public key hash does not match any of these values, the connection will be aborted.
@Critical
*/
repeated bytes pinned_peer_certificate_public_key_sha256 = 14;
@ -88,5 +86,9 @@ message Config {
// Lists of string as CurvePreferences values.
repeated string curve_preferences = 16;
string server_name_to_verify = 17;
/* @Document Replaces server_name to verify the peer cert.
@Document After allow_insecure (automatically), if the server's cert can't be verified by any of these names, pinned_peer_certificate_chain_sha256 will be tried.
@Critical
*/
repeated string verify_peer_cert_in_names = 17;
}

View file

@ -16,6 +16,7 @@ type Interface interface {
net.Conn
HandshakeContext(ctx context.Context) error
VerifyHostname(host string) error
HandshakeContextServerName(ctx context.Context) string
NegotiatedProtocol() string
}
@ -43,15 +44,11 @@ func (c *Conn) WriteMultiBuffer(mb buf.MultiBuffer) error {
return err
}
func (c *Conn) HandshakeAddressContext(ctx context.Context) net.Address {
func (c *Conn) HandshakeContextServerName(ctx context.Context) string {
if err := c.HandshakeContext(ctx); err != nil {
return nil
return ""
}
state := c.ConnectionState()
if state.ServerName == "" {
return nil
}
return net.ParseAddress(state.ServerName)
return c.ConnectionState().ServerName
}
func (c *Conn) NegotiatedProtocol() string {
@ -85,15 +82,11 @@ func (c *UConn) Close() error {
return c.Conn.Close()
}
func (c *UConn) HandshakeAddressContext(ctx context.Context) net.Address {
func (c *UConn) HandshakeContextServerName(ctx context.Context) string {
if err := c.HandshakeContext(ctx); err != nil {
return nil
return ""
}
state := c.ConnectionState()
if state.ServerName == "" {
return nil
}
return net.ParseAddress(state.ServerName)
return c.ConnectionState().ServerName
}
// WebsocketHandshake basically calls UConn.Handshake inside it but it will only send
@ -134,17 +127,13 @@ func UClient(c net.Conn, config *tls.Config, fingerprint *utls.ClientHelloID) ne
}
func copyConfig(c *tls.Config) *utls.Config {
serverNameToVerify := ""
if r, ok := c.Rand.(*RandCarrier); ok {
serverNameToVerify = r.ServerNameToVerify
}
return &utls.Config{
RootCAs: c.RootCAs,
ServerName: c.ServerName,
InsecureSkipVerify: c.InsecureSkipVerify,
VerifyPeerCertificate: c.VerifyPeerCertificate,
KeyLogWriter: c.KeyLogWriter,
InsecureServerNameToVerify: serverNameToVerify,
Rand: c.Rand,
RootCAs: c.RootCAs,
ServerName: c.ServerName,
InsecureSkipVerify: c.InsecureSkipVerify,
VerifyPeerCertificate: c.VerifyPeerCertificate,
KeyLogWriter: c.KeyLogWriter,
}
}