Ver Fonte

Verify peer cert function for better man in the middle prevention (#746)

* verify peer cert function for better man in the middle prevention

* publish cert chain hash generation algorithm

* added calculation of certificate hash as separate command and tlsping, use base64 to represent fingerprint to align with jsonPb

* apply coding style

* added test case for pinned certificates

* refactored cert pin

* pinned cert test

* added json loading of the PinnedPeerCertificateChainSha256

* removed tool to prepare for v5

* Add server cert pinning for Xtls

Change command "xray tls certChainHash" to xray style

Co-authored-by: Shelikhoo <[email protected]>
yuhan6665 há 4 anos atrás
pai
commit
acb81ebe3d

+ 50 - 23
infra/conf/transport_internet.go

@@ -1,6 +1,7 @@
 package conf
 package conf
 
 
 import (
 import (
+	"encoding/base64"
 	"encoding/json"
 	"encoding/json"
 	"math"
 	"math"
 	"net/url"
 	"net/url"
@@ -338,18 +339,19 @@ func (c *TLSCertConfig) Build() (*tls.Certificate, error) {
 }
 }
 
 
 type TLSConfig struct {
 type TLSConfig struct {
-	Insecure                 bool             `json:"allowInsecure"`
-	Certs                    []*TLSCertConfig `json:"certificates"`
-	ServerName               string           `json:"serverName"`
-	ALPN                     *StringList      `json:"alpn"`
-	EnableSessionResumption  bool             `json:"enableSessionResumption"`
-	DisableSystemRoot        bool             `json:"disableSystemRoot"`
-	MinVersion               string           `json:"minVersion"`
-	MaxVersion               string           `json:"maxVersion"`
-	CipherSuites             string           `json:"cipherSuites"`
-	PreferServerCipherSuites bool             `json:"preferServerCipherSuites"`
-	Fingerprint              string           `json:"fingerprint"`
-	RejectUnknownSNI         bool             `json:"rejectUnknownSni"`
+	Insecure                         bool             `json:"allowInsecure"`
+	Certs                            []*TLSCertConfig `json:"certificates"`
+	ServerName                       string           `json:"serverName"`
+	ALPN                             *StringList      `json:"alpn"`
+	EnableSessionResumption          bool             `json:"enableSessionResumption"`
+	DisableSystemRoot                bool             `json:"disableSystemRoot"`
+	MinVersion                       string           `json:"minVersion"`
+	MaxVersion                       string           `json:"maxVersion"`
+	CipherSuites                     string           `json:"cipherSuites"`
+	PreferServerCipherSuites         bool             `json:"preferServerCipherSuites"`
+	Fingerprint                      string           `json:"fingerprint"`
+	RejectUnknownSNI                 bool             `json:"rejectUnknownSni"`
+	PinnedPeerCertificateChainSha256 *[]string        `json:"pinnedPeerCertificateChainSha256"`
 }
 }
 
 
 // Build implements Buildable.
 // Build implements Buildable.
@@ -379,6 +381,18 @@ func (c *TLSConfig) Build() (proto.Message, error) {
 	config.PreferServerCipherSuites = c.PreferServerCipherSuites
 	config.PreferServerCipherSuites = c.PreferServerCipherSuites
 	config.Fingerprint = strings.ToLower(c.Fingerprint)
 	config.Fingerprint = strings.ToLower(c.Fingerprint)
 	config.RejectUnknownSni = c.RejectUnknownSNI
 	config.RejectUnknownSni = c.RejectUnknownSNI
+
+	if c.PinnedPeerCertificateChainSha256 != nil {
+		config.PinnedPeerCertificateChainSha256 = [][]byte{}
+		for _, v := range *c.PinnedPeerCertificateChainSha256 {
+			hashValue, err := base64.StdEncoding.DecodeString(v)
+			if err != nil {
+				return nil, err
+			}
+			config.PinnedPeerCertificateChainSha256 = append(config.PinnedPeerCertificateChainSha256, hashValue)
+		}
+	}
+
 	return config, nil
 	return config, nil
 }
 }
 
 
@@ -432,17 +446,18 @@ func (c *XTLSCertConfig) Build() (*xtls.Certificate, error) {
 }
 }
 
 
 type XTLSConfig struct {
 type XTLSConfig struct {
-	Insecure                 bool              `json:"allowInsecure"`
-	Certs                    []*XTLSCertConfig `json:"certificates"`
-	ServerName               string            `json:"serverName"`
-	ALPN                     *StringList       `json:"alpn"`
-	EnableSessionResumption  bool              `json:"enableSessionResumption"`
-	DisableSystemRoot        bool              `json:"disableSystemRoot"`
-	MinVersion               string            `json:"minVersion"`
-	MaxVersion               string            `json:"maxVersion"`
-	CipherSuites             string            `json:"cipherSuites"`
-	PreferServerCipherSuites bool              `json:"preferServerCipherSuites"`
-	RejectUnknownSNI         bool              `json:"rejectUnknownSni"`
+	Insecure                         bool              `json:"allowInsecure"`
+	Certs                            []*XTLSCertConfig `json:"certificates"`
+	ServerName                       string            `json:"serverName"`
+	ALPN                             *StringList       `json:"alpn"`
+	EnableSessionResumption          bool              `json:"enableSessionResumption"`
+	DisableSystemRoot                bool              `json:"disableSystemRoot"`
+	MinVersion                       string            `json:"minVersion"`
+	MaxVersion                       string            `json:"maxVersion"`
+	CipherSuites                     string            `json:"cipherSuites"`
+	PreferServerCipherSuites         bool              `json:"preferServerCipherSuites"`
+	RejectUnknownSNI                 bool              `json:"rejectUnknownSni"`
+	PinnedPeerCertificateChainSha256 *[]string         `json:"pinnedPeerCertificateChainSha256"`
 }
 }
 
 
 // Build implements Buildable.
 // Build implements Buildable.
@@ -471,6 +486,18 @@ func (c *XTLSConfig) Build() (proto.Message, error) {
 	config.CipherSuites = c.CipherSuites
 	config.CipherSuites = c.CipherSuites
 	config.PreferServerCipherSuites = c.PreferServerCipherSuites
 	config.PreferServerCipherSuites = c.PreferServerCipherSuites
 	config.RejectUnknownSni = c.RejectUnknownSNI
 	config.RejectUnknownSni = c.RejectUnknownSNI
+
+	if c.PinnedPeerCertificateChainSha256 != nil {
+		config.PinnedPeerCertificateChainSha256 = [][]byte{}
+		for _, v := range *c.PinnedPeerCertificateChainSha256 {
+			hashValue, err := base64.StdEncoding.DecodeString(v)
+			if err != nil {
+				return nil, err
+			}
+			config.PinnedPeerCertificateChainSha256 = append(config.PinnedPeerCertificateChainSha256, hashValue)
+		}
+	}
+
 	return config, nil
 	return config, nil
 }
 }
 
 

+ 41 - 0
main/commands/all/tls/certchainhash.go

@@ -0,0 +1,41 @@
+package tls
+
+import (
+	"flag"
+	"fmt"
+	"io/ioutil"
+
+	"github.com/xtls/xray-core/main/commands/base"
+	"github.com/xtls/xray-core/transport/internet/tls"
+)
+
+var cmdCertChainHash = &base.Command{
+	UsageLine: "{{.Exec}} certChainHash",
+	Short:     "Calculate TLS certificates hash.",
+	Long: `
+	xray tls certChainHash --cert <cert.pem>
+	Calculate TLS certificate chain hash.
+	`,
+}
+
+func init() {
+	cmdCertChainHash.Run = executeCertChainHash // break init loop
+}
+
+var input = cmdCertChainHash.Flag.String("cert", "fullchain.pem", "The file path of the certificates chain")
+
+func executeCertChainHash(cmd *base.Command, args []string) {
+	fs := flag.NewFlagSet("certChainHash", flag.ContinueOnError)
+	if err := fs.Parse(args); err != nil {
+		fmt.Println(err)
+		return
+	}
+	certContent, err := ioutil.ReadFile(*input)
+	if err != nil {
+		fmt.Println(err)
+		return
+	}
+	certChainHashB64 := tls.CalculatePEMCertChainSHA256Hash(certContent)
+	fmt.Println(certChainHashB64)
+	return
+}

+ 21 - 7
main/commands/all/tls/ping.go

@@ -1,12 +1,14 @@
 package tls
 package tls
 
 
 import (
 import (
-	"crypto/tls"
+	gotls "crypto/tls"
 	"crypto/x509"
 	"crypto/x509"
+	"encoding/base64"
 	"fmt"
 	"fmt"
 	"net"
 	"net"
 
 
 	"github.com/xtls/xray-core/main/commands/base"
 	"github.com/xtls/xray-core/main/commands/base"
+	. "github.com/xtls/xray-core/transport/internet/tls"
 )
 )
 
 
 // cmdPing is the tls ping command
 // cmdPing is the tls ping command
@@ -60,11 +62,13 @@ func executePing(cmd *base.Command, args []string) {
 		if err != nil {
 		if err != nil {
 			base.Fatalf("Failed to dial tcp: %s", err)
 			base.Fatalf("Failed to dial tcp: %s", err)
 		}
 		}
-		tlsConn := tls.Client(tcpConn, &tls.Config{
+		tlsConn := gotls.Client(tcpConn, &gotls.Config{
 			InsecureSkipVerify: true,
 			InsecureSkipVerify: true,
 			NextProtos:         []string{"http/1.1"},
 			NextProtos:         []string{"http/1.1"},
-			MaxVersion:         tls.VersionTLS12,
-			MinVersion:         tls.VersionTLS12,
+			MaxVersion:         gotls.VersionTLS12,
+			MinVersion:         gotls.VersionTLS12,
+			// Do not release tool before v5's refactor
+			// VerifyPeerCertificate: showCert(),
 		})
 		})
 		err = tlsConn.Handshake()
 		err = tlsConn.Handshake()
 		if err != nil {
 		if err != nil {
@@ -83,11 +87,13 @@ func executePing(cmd *base.Command, args []string) {
 		if err != nil {
 		if err != nil {
 			base.Fatalf("Failed to dial tcp: %s", err)
 			base.Fatalf("Failed to dial tcp: %s", err)
 		}
 		}
-		tlsConn := tls.Client(tcpConn, &tls.Config{
+		tlsConn := gotls.Client(tcpConn, &gotls.Config{
 			ServerName: domain,
 			ServerName: domain,
 			NextProtos: []string{"http/1.1"},
 			NextProtos: []string{"http/1.1"},
-			MaxVersion: tls.VersionTLS12,
-			MinVersion: tls.VersionTLS12,
+			MaxVersion: gotls.VersionTLS12,
+			MinVersion: gotls.VersionTLS12,
+			// Do not release tool before v5's refactor
+			// VerifyPeerCertificate: showCert(),
 		})
 		})
 		err = tlsConn.Handshake()
 		err = tlsConn.Handshake()
 		if err != nil {
 		if err != nil {
@@ -110,3 +116,11 @@ func printCertificates(certs []*x509.Certificate) {
 		fmt.Println("Allowed domains: ", cert.DNSNames)
 		fmt.Println("Allowed domains: ", cert.DNSNames)
 	}
 	}
 }
 }
+
+func showCert() func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+	return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+		hash := GenerateCertChainHash(rawCerts)
+		fmt.Println("Certificate Chain Hash: ", base64.StdEncoding.EncodeToString(hash))
+		return nil
+	}
+}

+ 1 - 0
main/commands/all/tls/tls.go

@@ -13,5 +13,6 @@ var CmdTLS = &base.Command{
 	Commands: []*base.Command{
 	Commands: []*base.Command{
 		cmdCert,
 		cmdCert,
 		cmdPing,
 		cmdPing,
+		cmdCertChainHash,
 	},
 	},
 }
 }

+ 103 - 0
testing/scenarios/tls_test.go

@@ -826,3 +826,106 @@ func TestGRPCMultiMode(t *testing.T) {
 		t.Error(err)
 		t.Error(err)
 	}
 	}
 }
 }
+
+func TestSimpleTLSConnectionPinned(t *testing.T) {
+	tcpServer := tcp.Server{
+		MsgProcessor: xor,
+	}
+	dest, err := tcpServer.Start()
+	common.Must(err)
+	defer tcpServer.Close()
+	certificateDer := cert.MustGenerate(nil)
+	certificate := tls.ParseCertificate(certificateDer)
+	certHash := tls.GenerateCertChainHash([][]byte{certificateDer.Certificate})
+	userID := protocol.NewID(uuid.New())
+	serverPort := tcp.PickPort()
+	serverConfig := &core.Config{
+		Inbound: []*core.InboundHandlerConfig{
+			{
+				ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
+					PortRange: net.SinglePortRange(serverPort),
+					Listen:    net.NewIPOrDomain(net.LocalHostIP),
+					StreamSettings: &internet.StreamConfig{
+						SecurityType: serial.GetMessageType(&tls.Config{}),
+						SecuritySettings: []*serial.TypedMessage{
+							serial.ToTypedMessage(&tls.Config{
+								Certificate: []*tls.Certificate{certificate},
+							}),
+						},
+					},
+				}),
+				ProxySettings: serial.ToTypedMessage(&inbound.Config{
+					User: []*protocol.User{
+						{
+							Account: serial.ToTypedMessage(&vmess.Account{
+								Id: userID.String(),
+							}),
+						},
+					},
+				}),
+			},
+		},
+		Outbound: []*core.OutboundHandlerConfig{
+			{
+				ProxySettings: serial.ToTypedMessage(&freedom.Config{}),
+			},
+		},
+	}
+
+	clientPort := tcp.PickPort()
+	clientConfig := &core.Config{
+		Inbound: []*core.InboundHandlerConfig{
+			{
+				ReceiverSettings: serial.ToTypedMessage(&proxyman.ReceiverConfig{
+					PortRange: net.SinglePortRange(clientPort),
+					Listen:    net.NewIPOrDomain(net.LocalHostIP),
+				}),
+				ProxySettings: serial.ToTypedMessage(&dokodemo.Config{
+					Address: net.NewIPOrDomain(dest.Address),
+					Port:    uint32(dest.Port),
+					NetworkList: &net.NetworkList{
+						Network: []net.Network{net.Network_TCP},
+					},
+				}),
+			},
+		},
+		Outbound: []*core.OutboundHandlerConfig{
+			{
+				ProxySettings: serial.ToTypedMessage(&outbound.Config{
+					Receiver: []*protocol.ServerEndpoint{
+						{
+							Address: net.NewIPOrDomain(net.LocalHostIP),
+							Port:    uint32(serverPort),
+							User: []*protocol.User{
+								{
+									Account: serial.ToTypedMessage(&vmess.Account{
+										Id: userID.String(),
+									}),
+								},
+							},
+						},
+					},
+				}),
+				SenderSettings: serial.ToTypedMessage(&proxyman.SenderConfig{
+					StreamSettings: &internet.StreamConfig{
+						SecurityType: serial.GetMessageType(&tls.Config{}),
+						SecuritySettings: []*serial.TypedMessage{
+							serial.ToTypedMessage(&tls.Config{
+								AllowInsecure:                    true,
+								PinnedPeerCertificateChainSha256: [][]byte{certHash},
+							}),
+						},
+					},
+				}),
+			},
+		},
+	}
+
+	servers, err := InitializeServerConfigs(serverConfig, clientConfig)
+	common.Must(err)
+	defer CloseAllServers(servers)
+
+	if err := testTCPConn(clientPort, 1024, time.Second*2)(); err != nil {
+		t.Fatal(err)
+	}
+}

+ 16 - 0
transport/internet/tls/config.go

@@ -1,8 +1,10 @@
 package tls
 package tls
 
 
 import (
 import (
+	"crypto/hmac"
 	"crypto/tls"
 	"crypto/tls"
 	"crypto/x509"
 	"crypto/x509"
+	"encoding/base64"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
 	"time"
 	"time"
@@ -254,6 +256,19 @@ func (c *Config) parseServerName() string {
 	return c.ServerName
 	return c.ServerName
 }
 }
 
 
+func (c *Config) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+	if c.PinnedPeerCertificateChainSha256 != nil {
+		hashValue := GenerateCertChainHash(rawCerts)
+		for _, v := range c.PinnedPeerCertificateChainSha256 {
+			if hmac.Equal(hashValue, v) {
+				return nil
+			}
+		}
+		return newError("peer cert is unrecognized: ", base64.StdEncoding.EncodeToString(hashValue))
+	}
+	return nil
+}
+
 // GetTLSConfig converts this Config into tls.Config.
 // GetTLSConfig converts this Config into tls.Config.
 func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
 func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
 	root, err := c.getCertPool()
 	root, err := c.getCertPool()
@@ -277,6 +292,7 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config {
 		InsecureSkipVerify:     c.AllowInsecure,
 		InsecureSkipVerify:     c.AllowInsecure,
 		NextProtos:             c.NextProtocol,
 		NextProtos:             c.NextProtocol,
 		SessionTicketsDisabled: !c.EnableSessionResumption,
 		SessionTicketsDisabled: !c.EnableSessionResumption,
+		VerifyPeerCertificate:  c.verifyPeerCert,
 	}
 	}
 
 
 	for _, opt := range opts {
 	for _, opt := range opts {

+ 19 - 2
transport/internet/tls/config.pb.go

@@ -198,6 +198,11 @@ type Config struct {
 	// TLS Client Hello fingerprint (uTLS).
 	// TLS Client Hello fingerprint (uTLS).
 	Fingerprint      string `protobuf:"bytes,11,opt,name=fingerprint,proto3" json:"fingerprint,omitempty"`
 	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"`
 	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.
+	//@Critical
+	PinnedPeerCertificateChainSha256 [][]byte `protobuf:"bytes,13,rep,name=pinned_peer_certificate_chain_sha256,json=pinnedPeerCertificateChainSha256,proto3" json:"pinned_peer_certificate_chain_sha256,omitempty"`
 }
 }
 
 
 func (x *Config) Reset() {
 func (x *Config) Reset() {
@@ -316,6 +321,13 @@ func (x *Config) GetRejectUnknownSni() bool {
 	return false
 	return false
 }
 }
 
 
+func (x *Config) GetPinnedPeerCertificateChainSha256() [][]byte {
+	if x != nil {
+		return x.PinnedPeerCertificateChainSha256
+	}
+	return nil
+}
+
 var File_transport_internet_tls_config_proto protoreflect.FileDescriptor
 var File_transport_internet_tls_config_proto protoreflect.FileDescriptor
 
 
 var file_transport_internet_tls_config_proto_rawDesc = []byte{
 var file_transport_internet_tls_config_proto_rawDesc = []byte{
@@ -345,7 +357,7 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{
 	0x43, 0x49, 0x50, 0x48, 0x45, 0x52, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10,
 	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,
 	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,
 	0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x41, 0x55, 0x54, 0x48, 0x4f, 0x52, 0x49, 0x54, 0x59, 0x5f,
-	0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xa3, 0x04, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66,
+	0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xf3, 0x04, 0x0a, 0x06, 0x43, 0x6f, 0x6e, 0x66,
 	0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69, 0x6e, 0x73, 0x65,
 	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,
 	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, 0x72,
 	0x77, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x12, 0x4a, 0x0a, 0x0b, 0x63, 0x65, 0x72,
@@ -379,7 +391,12 @@ var file_transport_internet_tls_config_proto_rawDesc = []byte{
 	0x28, 0x09, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x12,
 	0x28, 0x09, 0x52, 0x0b, 0x66, 0x69, 0x6e, 0x67, 0x65, 0x72, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x12,
 	0x2c, 0x0a, 0x12, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77,
 	0x2c, 0x0a, 0x12, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x6e, 0x6b, 0x6e, 0x6f, 0x77,
 	0x6e, 0x5f, 0x73, 0x6e, 0x69, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x65, 0x6a,
 	0x6e, 0x5f, 0x73, 0x6e, 0x69, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72, 0x65, 0x6a,
-	0x65, 0x63, 0x74, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x53, 0x6e, 0x69, 0x42, 0x73, 0x0a,
+	0x65, 0x63, 0x74, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x53, 0x6e, 0x69, 0x12, 0x4e, 0x0a,
+	0x24, 0x70, 0x69, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x63, 0x65, 0x72,
+	0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e, 0x5f, 0x73,
+	0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x20, 0x70, 0x69, 0x6e,
+	0x6e, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61,
+	0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, 0x42, 0x73, 0x0a,
 	0x1f, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e, 0x73, 0x70,
 	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,
 	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,
 	0x50, 0x01, 0x5a, 0x30, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x78,

+ 7 - 0
transport/internet/tls/config.proto

@@ -69,4 +69,11 @@ message Config {
   string fingerprint = 11;
   string fingerprint = 11;
 
 
   bool reject_unknown_sni = 12;
   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.
+     @Critical
+  */
+  repeated bytes pinned_peer_certificate_chain_sha256 = 13;
 }
 }

+ 36 - 0
transport/internet/tls/pin.go

@@ -0,0 +1,36 @@
+package tls
+
+import (
+	"crypto/sha256"
+	"encoding/base64"
+	"encoding/pem"
+)
+
+func CalculatePEMCertChainSHA256Hash(certContent []byte) string {
+	var certChain [][]byte
+	for {
+		block, remain := pem.Decode(certContent)
+		if block == nil {
+			break
+		}
+		certChain = append(certChain, block.Bytes)
+		certContent = remain
+	}
+	certChainHash := GenerateCertChainHash(certChain)
+	certChainHashB64 := base64.StdEncoding.EncodeToString(certChainHash)
+	return certChainHashB64
+}
+
+func GenerateCertChainHash(rawCerts [][]byte) []byte {
+	var hashValue []byte
+	for _, certValue := range rawCerts {
+		out := sha256.Sum256(certValue)
+		if hashValue == nil {
+			hashValue = out[:]
+		} else {
+			newHashValue := sha256.Sum256(append(hashValue, out[:]...))
+			hashValue = newHashValue[:]
+		}
+	}
+	return hashValue
+}

+ 110 - 0
transport/internet/tls/pin_test.go

@@ -0,0 +1,110 @@
+package tls
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestCalculateCertHash(t *testing.T) {
+	/* This is used to make sure that the hash signature generated is consistent
+	   Do NOT change this test to suit your modification.
+	*/
+	const CertBundle = `
+-----BEGIN CERTIFICATE-----
+MIIFJjCCBA6gAwIBAgISBL8FgUdEcVYEjdMkTZPgC3ocMA0GCSqGSIb3DQEBCwUA
+MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD
+EwJSMzAeFw0yMTAzMjkwMTM2MzlaFw0yMTA2MjcwMTM2MzlaMBsxGTAXBgNVBAMT
+EHNlY3VyZS5ra2Rldi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+AQDOF/j+s7rHaDMXdhYjffoOFjNZb7n3sCuvubI3qOcgJmr1WPlCEry50KoY8FaB
+IF2HstMIZceN4NoUK7mr3WAvsQTA47uBfuhp+XQmAQW0T/fYD+XbvxtCENEin+xm
+JsvTKZLTKbE08E964J4H+1sBmueP6rvy2Wt95z0XkqoQiikpmLE87WdltQcATvVX
+qqrL64hV0nN4Hdi2Bv1cQ92aR7lZGj8jiQRtTj8y5Ah3Gk3fPoao+yI7gnzembqo
+fddePzz/u8iEuvYAsIYZKn9bbS7rkYoJazL2/xiDZR7usn0SomzmM6lGXDD3FF4b
+lyTkLYwgFVgbGWoz1+eOHD5BAgMBAAGjggJLMIICRzAOBgNVHQ8BAf8EBAMCBaAw
+HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYD
+VR0OBBYEFOPRdApL8XENLXDuU3oPisykGyp+MB8GA1UdIwQYMBaAFBQusxe3WFbL
+rlAJQOYfr52LFMLGMFUGCCsGAQUFBwEBBEkwRzAhBggrBgEFBQcwAYYVaHR0cDov
+L3IzLm8ubGVuY3Iub3JnMCIGCCsGAQUFBzAChhZodHRwOi8vcjMuaS5sZW5jci5v
+cmcvMBsGA1UdEQQUMBKCEHNlY3VyZS5ra2Rldi5vcmcwTAYDVR0gBEUwQzAIBgZn
+gQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5s
+ZXRzZW5jcnlwdC5vcmcwggEEBgorBgEEAdZ5AgQCBIH1BIHyAPAAdQD2XJQv0Xcw
+IhRUGAgwlFaO400TGTO/3wwvIAvMTvFk4wAAAXh71yBGAAAEAwBGMEQCIDmziDOn
+ehPY2KoAFX8fPWiCm4EBTbGJXBWF1LCotPJBAiBLSCg+krXvbyoABnTm8knv0hbG
+/ZOk8LV6qpw9VoQwGwB3AG9Tdqwx8DEZ2JkApFEV/3cVHBHZAsEAKQaNsgiaN9kT
+AAABeHvXIIAAAAQDAEgwRgIhAOkeKc52wR3n5QWZfa3zbbicMMSQrTGbQ+1fHNs7
+SsRvAiEAqbflDx1nZRsTA22FfNYfgF6v5Z3/svjiTleWSQad4WswDQYJKoZIhvcN
+AQELBQADggEBAEj8tg+Agf5sNBM9CvjeXbA0fkpGDaQzXEkwefAea5vPgKoGiWSN
+pHDkyr0i7+mqa7cMXhmmo20V0/+QDv0nrsCw8pgJuviC3GvK6agT6WfkXM2djJuo
+osPeXOL9KEF/ATv0EyM5tr9TIoRSSYQoRhuqEdg3Dz9Ii8GXR5HhbYr6Cd7gwNHS
+kYeivKDmgv31GHb4twPSE9LZ/U+56lNVvSbJ4csupIF3GnxnxrFSmijYNOPoM6mj
+tzY45d4mjPs0fKCFKSsVM6YT0tX4NwIKsOaeQg30WLtRyDwYm6ma/a/UUUS0FloZ
+2/p85glOgzownfoRjzTbqHu8ewtMd6Apc0E=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEZTCCA02gAwIBAgIQQAF1BIMUpMghjISpDBbN3zANBgkqhkiG9w0BAQsFADA/
+MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
+DkRTVCBSb290IENBIFgzMB4XDTIwMTAwNzE5MjE0MFoXDTIxMDkyOTE5MjE0MFow
+MjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxCzAJBgNVBAMT
+AlIzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAuwIVKMz2oJTTDxLs
+jVWSw/iC8ZmmekKIp10mqrUrucVMsa+Oa/l1yKPXD0eUFFU1V4yeqKI5GfWCPEKp
+Tm71O8Mu243AsFzzWTjn7c9p8FoLG77AlCQlh/o3cbMT5xys4Zvv2+Q7RVJFlqnB
+U840yFLuta7tj95gcOKlVKu2bQ6XpUA0ayvTvGbrZjR8+muLj1cpmfgwF126cm/7
+gcWt0oZYPRfH5wm78Sv3htzB2nFd1EbjzK0lwYi8YGd1ZrPxGPeiXOZT/zqItkel
+/xMY6pgJdz+dU/nPAeX1pnAXFK9jpP+Zs5Od3FOnBv5IhR2haa4ldbsTzFID9e1R
+oYvbFQIDAQABo4IBaDCCAWQwEgYDVR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8E
+BAMCAYYwSwYIKwYBBQUHAQEEPzA9MDsGCCsGAQUFBzAChi9odHRwOi8vYXBwcy5p
+ZGVudHJ1c3QuY29tL3Jvb3RzL2RzdHJvb3RjYXgzLnA3YzAfBgNVHSMEGDAWgBTE
+p7Gkeyxx+tvhS5B1/8QVYIWJEDBUBgNVHSAETTBLMAgGBmeBDAECATA/BgsrBgEE
+AYLfEwEBATAwMC4GCCsGAQUFBwIBFiJodHRwOi8vY3BzLnJvb3QteDEubGV0c2Vu
+Y3J5cHQub3JnMDwGA1UdHwQ1MDMwMaAvoC2GK2h0dHA6Ly9jcmwuaWRlbnRydXN0
+LmNvbS9EU1RST09UQ0FYM0NSTC5jcmwwHQYDVR0OBBYEFBQusxe3WFbLrlAJQOYf
+r52LFMLGMB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjANBgkqhkiG9w0B
+AQsFAAOCAQEA2UzgyfWEiDcx27sT4rP8i2tiEmxYt0l+PAK3qB8oYevO4C5z70kH
+ejWEHx2taPDY/laBL21/WKZuNTYQHHPD5b1tXgHXbnL7KqC401dk5VvCadTQsvd8
+S8MXjohyc9z9/G2948kLjmE6Flh9dDYrVYA9x2O+hEPGOaEOa1eePynBgPayvUfL
+qjBstzLhWVQLGAkXXmNs+5ZnPBxzDJOLxhF2JIbeQAcH5H0tZrUlo5ZYyOqA7s9p
+O5b85o3AM/OJ+CktFBQtfvBhcJVd9wvlwPsk+uyOy2HI7mNxKKgsBTt375teA2Tw
+UdHkhVNcsAKX1H7GNNLOEADksd86wuoXvg==
+-----END CERTIFICATE-----
+`
+	t.Run("bundle", func(t *testing.T) {
+		hash := CalculatePEMCertChainSHA256Hash([]byte(CertBundle))
+		assert.Equal(t, "WF65fBkgltadMnXryOMZ6TEYeV4d5Q0uu4SGXGZ0RjI=", hash)
+	})
+	const Single = `-----BEGIN CERTIFICATE-----
+MIIFJjCCBA6gAwIBAgISBL8FgUdEcVYEjdMkTZPgC3ocMA0GCSqGSIb3DQEBCwUA
+MDIxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MQswCQYDVQQD
+EwJSMzAeFw0yMTAzMjkwMTM2MzlaFw0yMTA2MjcwMTM2MzlaMBsxGTAXBgNVBAMT
+EHNlY3VyZS5ra2Rldi5vcmcwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
+AQDOF/j+s7rHaDMXdhYjffoOFjNZb7n3sCuvubI3qOcgJmr1WPlCEry50KoY8FaB
+IF2HstMIZceN4NoUK7mr3WAvsQTA47uBfuhp+XQmAQW0T/fYD+XbvxtCENEin+xm
+JsvTKZLTKbE08E964J4H+1sBmueP6rvy2Wt95z0XkqoQiikpmLE87WdltQcATvVX
+qqrL64hV0nN4Hdi2Bv1cQ92aR7lZGj8jiQRtTj8y5Ah3Gk3fPoao+yI7gnzembqo
+fddePzz/u8iEuvYAsIYZKn9bbS7rkYoJazL2/xiDZR7usn0SomzmM6lGXDD3FF4b
+lyTkLYwgFVgbGWoz1+eOHD5BAgMBAAGjggJLMIICRzAOBgNVHQ8BAf8EBAMCBaAw
+HQYDVR0lBBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMAwGA1UdEwEB/wQCMAAwHQYD
+VR0OBBYEFOPRdApL8XENLXDuU3oPisykGyp+MB8GA1UdIwQYMBaAFBQusxe3WFbL
+rlAJQOYfr52LFMLGMFUGCCsGAQUFBwEBBEkwRzAhBggrBgEFBQcwAYYVaHR0cDov
+L3IzLm8ubGVuY3Iub3JnMCIGCCsGAQUFBzAChhZodHRwOi8vcjMuaS5sZW5jci5v
+cmcvMBsGA1UdEQQUMBKCEHNlY3VyZS5ra2Rldi5vcmcwTAYDVR0gBEUwQzAIBgZn
+gQwBAgEwNwYLKwYBBAGC3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5s
+ZXRzZW5jcnlwdC5vcmcwggEEBgorBgEEAdZ5AgQCBIH1BIHyAPAAdQD2XJQv0Xcw
+IhRUGAgwlFaO400TGTO/3wwvIAvMTvFk4wAAAXh71yBGAAAEAwBGMEQCIDmziDOn
+ehPY2KoAFX8fPWiCm4EBTbGJXBWF1LCotPJBAiBLSCg+krXvbyoABnTm8knv0hbG
+/ZOk8LV6qpw9VoQwGwB3AG9Tdqwx8DEZ2JkApFEV/3cVHBHZAsEAKQaNsgiaN9kT
+AAABeHvXIIAAAAQDAEgwRgIhAOkeKc52wR3n5QWZfa3zbbicMMSQrTGbQ+1fHNs7
+SsRvAiEAqbflDx1nZRsTA22FfNYfgF6v5Z3/svjiTleWSQad4WswDQYJKoZIhvcN
+AQELBQADggEBAEj8tg+Agf5sNBM9CvjeXbA0fkpGDaQzXEkwefAea5vPgKoGiWSN
+pHDkyr0i7+mqa7cMXhmmo20V0/+QDv0nrsCw8pgJuviC3GvK6agT6WfkXM2djJuo
+osPeXOL9KEF/ATv0EyM5tr9TIoRSSYQoRhuqEdg3Dz9Ii8GXR5HhbYr6Cd7gwNHS
+kYeivKDmgv31GHb4twPSE9LZ/U+56lNVvSbJ4csupIF3GnxnxrFSmijYNOPoM6mj
+tzY45d4mjPs0fKCFKSsVM6YT0tX4NwIKsOaeQg30WLtRyDwYm6ma/a/UUUS0FloZ
+2/p85glOgzownfoRjzTbqHu8ewtMd6Apc0E=
+-----END CERTIFICATE-----
+`
+	t.Run("single", func(t *testing.T) {
+		hash := CalculatePEMCertChainSHA256Hash([]byte(Single))
+		assert.Equal(t, "FW3SVMCL6um2wVltOdgJ3DpI82aredw83YoCblkMkVM=", hash)
+	})
+}

+ 17 - 0
transport/internet/xtls/config.go

@@ -1,7 +1,9 @@
 package xtls
 package xtls
 
 
 import (
 import (
+	"crypto/hmac"
 	"crypto/x509"
 	"crypto/x509"
+	"encoding/base64"
 	"strings"
 	"strings"
 	"sync"
 	"sync"
 	"time"
 	"time"
@@ -13,6 +15,7 @@ import (
 	"github.com/xtls/xray-core/common/platform/filesystem"
 	"github.com/xtls/xray-core/common/platform/filesystem"
 	"github.com/xtls/xray-core/common/protocol/tls/cert"
 	"github.com/xtls/xray-core/common/protocol/tls/cert"
 	"github.com/xtls/xray-core/transport/internet"
 	"github.com/xtls/xray-core/transport/internet"
+	"github.com/xtls/xray-core/transport/internet/tls"
 )
 )
 
 
 var globalSessionCache = xtls.NewLRUClientSessionCache(128)
 var globalSessionCache = xtls.NewLRUClientSessionCache(128)
@@ -244,6 +247,19 @@ func (c *Config) parseServerName() string {
 	return c.ServerName
 	return c.ServerName
 }
 }
 
 
+func (c *Config) verifyPeerCert(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error {
+	if c.PinnedPeerCertificateChainSha256 != nil {
+		hashValue := tls.GenerateCertChainHash(rawCerts)
+		for _, v := range c.PinnedPeerCertificateChainSha256 {
+			if hmac.Equal(hashValue, v) {
+				return nil
+			}
+		}
+		return newError("peer cert is unrecognized: ", base64.StdEncoding.EncodeToString(hashValue))
+	}
+	return nil
+}
+
 // GetXTLSConfig converts this Config into xtls.Config.
 // GetXTLSConfig converts this Config into xtls.Config.
 func (c *Config) GetXTLSConfig(opts ...Option) *xtls.Config {
 func (c *Config) GetXTLSConfig(opts ...Option) *xtls.Config {
 	root, err := c.getCertPool()
 	root, err := c.getCertPool()
@@ -267,6 +283,7 @@ func (c *Config) GetXTLSConfig(opts ...Option) *xtls.Config {
 		InsecureSkipVerify:     c.AllowInsecure,
 		InsecureSkipVerify:     c.AllowInsecure,
 		NextProtos:             c.NextProtocol,
 		NextProtos:             c.NextProtocol,
 		SessionTicketsDisabled: !c.EnableSessionResumption,
 		SessionTicketsDisabled: !c.EnableSessionResumption,
+		VerifyPeerCertificate:  c.verifyPeerCert,
 	}
 	}
 
 
 	for _, opt := range opts {
 	for _, opt := range opts {

+ 19 - 2
transport/internet/xtls/config.pb.go

@@ -196,6 +196,11 @@ type Config struct {
 	// Whether the server selects its most preferred ciphersuite.
 	// Whether the server selects its most preferred ciphersuite.
 	PreferServerCipherSuites bool `protobuf:"varint,10,opt,name=prefer_server_cipher_suites,json=preferServerCipherSuites,proto3" json:"prefer_server_cipher_suites,omitempty"`
 	PreferServerCipherSuites bool `protobuf:"varint,10,opt,name=prefer_server_cipher_suites,json=preferServerCipherSuites,proto3" json:"prefer_server_cipher_suites,omitempty"`
 	RejectUnknownSni         bool `protobuf:"varint,12,opt,name=reject_unknown_sni,json=rejectUnknownSni,proto3" json:"reject_unknown_sni,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.
+	//@Critical
+	PinnedPeerCertificateChainSha256 [][]byte `protobuf:"bytes,13,rep,name=pinned_peer_certificate_chain_sha256,json=pinnedPeerCertificateChainSha256,proto3" json:"pinned_peer_certificate_chain_sha256,omitempty"`
 }
 }
 
 
 func (x *Config) Reset() {
 func (x *Config) Reset() {
@@ -307,6 +312,13 @@ func (x *Config) GetRejectUnknownSni() bool {
 	return false
 	return false
 }
 }
 
 
+func (x *Config) GetPinnedPeerCertificateChainSha256() [][]byte {
+	if x != nil {
+		return x.PinnedPeerCertificateChainSha256
+	}
+	return nil
+}
+
 var File_transport_internet_xtls_config_proto protoreflect.FileDescriptor
 var File_transport_internet_xtls_config_proto protoreflect.FileDescriptor
 
 
 var file_transport_internet_xtls_config_proto_rawDesc = []byte{
 var file_transport_internet_xtls_config_proto_rawDesc = []byte{
@@ -336,7 +348,7 @@ var file_transport_internet_xtls_config_proto_rawDesc = []byte{
 	0x0c, 0x45, 0x4e, 0x43, 0x49, 0x50, 0x48, 0x45, 0x52, 0x4d, 0x45, 0x4e, 0x54, 0x10, 0x00, 0x12,
 	0x0c, 0x45, 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,
 	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,
 	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, 0x82, 0x04, 0x0a, 0x06, 0x43,
+	0x54, 0x59, 0x5f, 0x49, 0x53, 0x53, 0x55, 0x45, 0x10, 0x02, 0x22, 0xd2, 0x04, 0x0a, 0x06, 0x43,
 	0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x25, 0x0a, 0x0e, 0x61, 0x6c, 0x6c, 0x6f, 0x77, 0x5f, 0x69,
 	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,
 	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, 0x4b, 0x0a, 0x0b,
 	0x6c, 0x6c, 0x6f, 0x77, 0x49, 0x6e, 0x73, 0x65, 0x63, 0x75, 0x72, 0x65, 0x12, 0x4b, 0x0a, 0x0b,
@@ -368,7 +380,12 @@ var file_transport_internet_xtls_config_proto_rawDesc = []byte{
 	0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65,
 	0x65, 0x72, 0x76, 0x65, 0x72, 0x43, 0x69, 0x70, 0x68, 0x65, 0x72, 0x53, 0x75, 0x69, 0x74, 0x65,
 	0x73, 0x12, 0x2c, 0x0a, 0x12, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x6e, 0x6b, 0x6e,
 	0x73, 0x12, 0x2c, 0x0a, 0x12, 0x72, 0x65, 0x6a, 0x65, 0x63, 0x74, 0x5f, 0x75, 0x6e, 0x6b, 0x6e,
 	0x6f, 0x77, 0x6e, 0x5f, 0x73, 0x6e, 0x69, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72,
 	0x6f, 0x77, 0x6e, 0x5f, 0x73, 0x6e, 0x69, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x72,
-	0x65, 0x6a, 0x65, 0x63, 0x74, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x53, 0x6e, 0x69, 0x42,
+	0x65, 0x6a, 0x65, 0x63, 0x74, 0x55, 0x6e, 0x6b, 0x6e, 0x6f, 0x77, 0x6e, 0x53, 0x6e, 0x69, 0x12,
+	0x4e, 0x0a, 0x24, 0x70, 0x69, 0x6e, 0x6e, 0x65, 0x64, 0x5f, 0x70, 0x65, 0x65, 0x72, 0x5f, 0x63,
+	0x65, 0x72, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x65, 0x5f, 0x63, 0x68, 0x61, 0x69, 0x6e,
+	0x5f, 0x73, 0x68, 0x61, 0x32, 0x35, 0x36, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x20, 0x70,
+	0x69, 0x6e, 0x6e, 0x65, 0x64, 0x50, 0x65, 0x65, 0x72, 0x43, 0x65, 0x72, 0x74, 0x69, 0x66, 0x69,
+	0x63, 0x61, 0x74, 0x65, 0x43, 0x68, 0x61, 0x69, 0x6e, 0x53, 0x68, 0x61, 0x32, 0x35, 0x36, 0x42,
 	0x76, 0x0a, 0x20, 0x63, 0x6f, 0x6d, 0x2e, 0x78, 0x72, 0x61, 0x79, 0x2e, 0x74, 0x72, 0x61, 0x6e,
 	0x76, 0x0a, 0x20, 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, 0x78,
 	0x73, 0x70, 0x6f, 0x72, 0x74, 0x2e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x65, 0x74, 0x2e, 0x78,
 	0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,
 	0x74, 0x6c, 0x73, 0x50, 0x01, 0x5a, 0x31, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f,

+ 7 - 1
transport/internet/xtls/config.proto

@@ -65,6 +65,12 @@ message Config {
   // Whether the server selects its most preferred ciphersuite.
   // Whether the server selects its most preferred ciphersuite.
   bool prefer_server_cipher_suites = 10;
   bool prefer_server_cipher_suites = 10;
 
 
-
   bool reject_unknown_sni = 12;
   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.
+     @Critical
+  */
+  repeated bytes pinned_peer_certificate_chain_sha256 = 13;
 }
 }