diff --git a/infra/conf/transport_internet.go b/infra/conf/transport_internet.go index 57ef057b..d2cb35cd 100644 --- a/infra/conf/transport_internet.go +++ b/infra/conf/transport_internet.go @@ -1,6 +1,7 @@ package conf import ( + "encoding/base64" "encoding/json" "math" "net/url" @@ -338,18 +339,19 @@ func (c *TLSCertConfig) Build() (*tls.Certificate, error) { } 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. @@ -379,6 +381,18 @@ func (c *TLSConfig) Build() (proto.Message, error) { config.PreferServerCipherSuites = c.PreferServerCipherSuites config.Fingerprint = strings.ToLower(c.Fingerprint) 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 } @@ -432,17 +446,18 @@ func (c *XTLSCertConfig) Build() (*xtls.Certificate, error) { } 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. @@ -471,6 +486,18 @@ func (c *XTLSConfig) Build() (proto.Message, error) { config.CipherSuites = c.CipherSuites config.PreferServerCipherSuites = c.PreferServerCipherSuites 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 } diff --git a/main/commands/all/tls/certchainhash.go b/main/commands/all/tls/certchainhash.go new file mode 100644 index 00000000..3fbdb4c4 --- /dev/null +++ b/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 + 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 +} diff --git a/main/commands/all/tls/ping.go b/main/commands/all/tls/ping.go index 0f096cac..7d0ea0d6 100644 --- a/main/commands/all/tls/ping.go +++ b/main/commands/all/tls/ping.go @@ -1,12 +1,14 @@ package tls import ( - "crypto/tls" + gotls "crypto/tls" "crypto/x509" + "encoding/base64" "fmt" "net" "github.com/xtls/xray-core/main/commands/base" + . "github.com/xtls/xray-core/transport/internet/tls" ) // cmdPing is the tls ping command @@ -60,11 +62,13 @@ func executePing(cmd *base.Command, args []string) { if err != nil { base.Fatalf("Failed to dial tcp: %s", err) } - tlsConn := tls.Client(tcpConn, &tls.Config{ + tlsConn := gotls.Client(tcpConn, &gotls.Config{ InsecureSkipVerify: true, 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() if err != nil { @@ -83,11 +87,13 @@ func executePing(cmd *base.Command, args []string) { if err != nil { base.Fatalf("Failed to dial tcp: %s", err) } - tlsConn := tls.Client(tcpConn, &tls.Config{ + tlsConn := gotls.Client(tcpConn, &gotls.Config{ ServerName: domain, 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() if err != nil { @@ -110,3 +116,11 @@ func printCertificates(certs []*x509.Certificate) { 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 + } +} diff --git a/main/commands/all/tls/tls.go b/main/commands/all/tls/tls.go index 55824d94..bf653301 100644 --- a/main/commands/all/tls/tls.go +++ b/main/commands/all/tls/tls.go @@ -13,5 +13,6 @@ var CmdTLS = &base.Command{ Commands: []*base.Command{ cmdCert, cmdPing, + cmdCertChainHash, }, } diff --git a/testing/scenarios/tls_test.go b/testing/scenarios/tls_test.go index 83699f2d..9e4961b0 100644 --- a/testing/scenarios/tls_test.go +++ b/testing/scenarios/tls_test.go @@ -826,3 +826,106 @@ func TestGRPCMultiMode(t *testing.T) { 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) + } +} diff --git a/transport/internet/tls/config.go b/transport/internet/tls/config.go index e1d84959..e1c12882 100644 --- a/transport/internet/tls/config.go +++ b/transport/internet/tls/config.go @@ -1,8 +1,10 @@ package tls import ( + "crypto/hmac" "crypto/tls" "crypto/x509" + "encoding/base64" "strings" "sync" "time" @@ -254,6 +256,19 @@ func (c *Config) parseServerName() string { 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. func (c *Config) GetTLSConfig(opts ...Option) *tls.Config { root, err := c.getCertPool() @@ -277,6 +292,7 @@ func (c *Config) GetTLSConfig(opts ...Option) *tls.Config { InsecureSkipVerify: c.AllowInsecure, NextProtos: c.NextProtocol, SessionTicketsDisabled: !c.EnableSessionResumption, + VerifyPeerCertificate: c.verifyPeerCert, } for _, opt := range opts { diff --git a/transport/internet/tls/config.pb.go b/transport/internet/tls/config.pb.go index ee8a5800..f1ce3f52 100644 --- a/transport/internet/tls/config.pb.go +++ b/transport/internet/tls/config.pb.go @@ -198,6 +198,11 @@ 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. + //@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() { @@ -316,6 +321,13 @@ func (x *Config) GetRejectUnknownSni() bool { 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_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, 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, 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, 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, @@ -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, 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, - 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, 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, diff --git a/transport/internet/tls/config.proto b/transport/internet/tls/config.proto index cc2ae7cc..28266e20 100644 --- a/transport/internet/tls/config.proto +++ b/transport/internet/tls/config.proto @@ -69,4 +69,11 @@ message Config { string fingerprint = 11; 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; } diff --git a/transport/internet/tls/pin.go b/transport/internet/tls/pin.go new file mode 100644 index 00000000..a7b012b5 --- /dev/null +++ b/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 +} diff --git a/transport/internet/tls/pin_test.go b/transport/internet/tls/pin_test.go new file mode 100644 index 00000000..9607fe1f --- /dev/null +++ b/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) + }) +} diff --git a/transport/internet/xtls/config.go b/transport/internet/xtls/config.go index fd1f3471..c85dc250 100644 --- a/transport/internet/xtls/config.go +++ b/transport/internet/xtls/config.go @@ -1,7 +1,9 @@ package xtls import ( + "crypto/hmac" "crypto/x509" + "encoding/base64" "strings" "sync" "time" @@ -13,6 +15,7 @@ import ( "github.com/xtls/xray-core/common/platform/filesystem" "github.com/xtls/xray-core/common/protocol/tls/cert" "github.com/xtls/xray-core/transport/internet" + "github.com/xtls/xray-core/transport/internet/tls" ) var globalSessionCache = xtls.NewLRUClientSessionCache(128) @@ -244,6 +247,19 @@ func (c *Config) parseServerName() string { 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. func (c *Config) GetXTLSConfig(opts ...Option) *xtls.Config { root, err := c.getCertPool() @@ -267,6 +283,7 @@ func (c *Config) GetXTLSConfig(opts ...Option) *xtls.Config { InsecureSkipVerify: c.AllowInsecure, NextProtos: c.NextProtocol, SessionTicketsDisabled: !c.EnableSessionResumption, + VerifyPeerCertificate: c.verifyPeerCert, } for _, opt := range opts { diff --git a/transport/internet/xtls/config.pb.go b/transport/internet/xtls/config.pb.go index 5c984ffb..f78eaf61 100644 --- a/transport/internet/xtls/config.pb.go +++ b/transport/internet/xtls/config.pb.go @@ -196,6 +196,11 @@ type Config struct { // 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"` 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() { @@ -307,6 +312,13 @@ func (x *Config) GetRejectUnknownSni() bool { 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_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, 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, 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, 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, @@ -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, 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, - 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, 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, diff --git a/transport/internet/xtls/config.proto b/transport/internet/xtls/config.proto index 57410148..e2e57e55 100644 --- a/transport/internet/xtls/config.proto +++ b/transport/internet/xtls/config.proto @@ -65,6 +65,12 @@ message Config { // Whether the server selects its most preferred ciphersuite. bool prefer_server_cipher_suites = 10; - 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; }