feat: client TLS cert SHA256 pinning (pinSHA256)

This commit is contained in:
Toby 2023-08-23 15:53:22 -07:00
parent b12bd74ac7
commit 3c3c2a51a8
6 changed files with 54 additions and 15 deletions

View File

@ -1,7 +1,9 @@
package cmd package cmd
import ( import (
"crypto/sha256"
"crypto/x509" "crypto/x509"
"encoding/hex"
"errors" "errors"
"net" "net"
"net/url" "net/url"
@ -70,9 +72,10 @@ type clientConfigObfs struct {
} }
type clientConfigTLS struct { type clientConfigTLS struct {
SNI string `mapstructure:"sni"` SNI string `mapstructure:"sni"`
Insecure bool `mapstructure:"insecure"` Insecure bool `mapstructure:"insecure"`
CA string `mapstructure:"ca"` PinSHA256 string `mapstructure:"pinSHA256"`
CA string `mapstructure:"ca"`
} }
type clientConfigQUIC struct { type clientConfigQUIC struct {
@ -174,6 +177,20 @@ func (c *clientConfig) fillTLSConfig(hyConfig *client.Config) error {
hyConfig.TLSConfig.ServerName = c.TLS.SNI hyConfig.TLSConfig.ServerName = c.TLS.SNI
} }
hyConfig.TLSConfig.InsecureSkipVerify = c.TLS.Insecure hyConfig.TLSConfig.InsecureSkipVerify = c.TLS.Insecure
if c.TLS.PinSHA256 != "" {
nHash := normalizeCertHash(c.TLS.PinSHA256)
hyConfig.TLSConfig.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
for _, cert := range rawCerts {
hash := sha256.Sum256(cert)
hashHex := hex.EncodeToString(hash[:])
if hashHex == nHash {
return nil
}
}
// No match
return errors.New("no certificate matches the pinned hash")
}
}
if c.TLS.CA != "" { if c.TLS.CA != "" {
ca, err := os.ReadFile(c.TLS.CA) ca, err := os.ReadFile(c.TLS.CA)
if err != nil { if err != nil {
@ -233,6 +250,7 @@ func (c *clientConfig) fillFastOpen(hyConfig *client.Config) error {
// - obfuscation password // - obfuscation password
// - TLS SNI // - TLS SNI
// - TLS insecure // - TLS insecure
// - TLS pinned SHA256 hash (normalized)
func (c *clientConfig) URI() string { func (c *clientConfig) URI() string {
q := url.Values{} q := url.Values{}
switch strings.ToLower(c.Obfs.Type) { switch strings.ToLower(c.Obfs.Type) {
@ -246,6 +264,9 @@ func (c *clientConfig) URI() string {
if c.TLS.Insecure { if c.TLS.Insecure {
q.Set("insecure", "1") q.Set("insecure", "1")
} }
if c.TLS.PinSHA256 != "" {
q.Set("pinSHA256", normalizeCertHash(c.TLS.PinSHA256))
}
var user *url.Userinfo var user *url.Userinfo
if c.Auth != "" { if c.Auth != "" {
// We need to handle the special case of user:pass pairs // We need to handle the special case of user:pass pairs
@ -297,6 +318,9 @@ func (c *clientConfig) parseURI() bool {
if insecure, err := strconv.ParseBool(q.Get("insecure")); err == nil { if insecure, err := strconv.ParseBool(q.Get("insecure")); err == nil {
c.TLS.Insecure = insecure c.TLS.Insecure = insecure
} }
if pinSHA256 := q.Get("pinSHA256"); pinSHA256 != "" {
c.TLS.PinSHA256 = pinSHA256
}
return true return true
} }

View File

@ -27,9 +27,10 @@ func TestClientConfig(t *testing.T) {
}, },
}, },
TLS: clientConfigTLS{ TLS: clientConfigTLS{
SNI: "another.example.com", SNI: "another.example.com",
Insecure: true, Insecure: true,
CA: "custom_ca.crt", PinSHA256: "114515DEADBEEF",
CA: "custom_ca.crt",
}, },
QUIC: clientConfigQUIC{ QUIC: clientConfigQUIC{
InitStreamReceiveWindow: 1145141, InitStreamReceiveWindow: 1145141,
@ -105,7 +106,7 @@ func TestClientConfigURI(t *testing.T) {
}, },
}, },
{ {
uri: "hysteria2://noauth.com/?insecure=1&obfs=salamander&obfs-password=66ccff&sni=crap.cc", uri: "hysteria2://noauth.com/?insecure=1&obfs=salamander&obfs-password=66ccff&pinSHA256=deadbeef&sni=crap.cc",
uriOK: true, uriOK: true,
config: &clientConfig{ config: &clientConfig{
Server: "noauth.com", Server: "noauth.com",
@ -117,8 +118,9 @@ func TestClientConfigURI(t *testing.T) {
}, },
}, },
TLS: clientConfigTLS{ TLS: clientConfigTLS{
SNI: "crap.cc", SNI: "crap.cc",
Insecure: true, Insecure: true,
PinSHA256: "deadbeef",
}, },
}, },
}, },

View File

@ -10,6 +10,7 @@ obfs:
tls: tls:
sni: another.example.com sni: another.example.com
insecure: true insecure: true
pinSHA256: 114515DEADBEEF
ca: custom_ca.crt ca: custom_ca.crt
quic: quic:

View File

@ -5,6 +5,7 @@ import (
"io" "io"
"net/http" "net/http"
"os" "os"
"strings"
"github.com/apernet/hysteria/extras/utils" "github.com/apernet/hysteria/extras/utils"
"github.com/mdp/qrterminal/v3" "github.com/mdp/qrterminal/v3"
@ -108,3 +109,12 @@ func (l *geoipLoader) Load() *geoip2.Reader {
} }
return l.db return l.db
} }
// normalizeCertHash normalizes a certificate hash string.
// It converts all characters to lowercase and removes possible separators such as ":" and "-".
func normalizeCertHash(hash string) string {
r := strings.ToLower(hash)
r = strings.ReplaceAll(r, ":", "")
r = strings.ReplaceAll(r, "-", "")
return r
}

View File

@ -63,9 +63,10 @@ func (c *clientImpl) connect() error {
} }
// Convert config to TLS config & QUIC config // Convert config to TLS config & QUIC config
tlsConfig := &tls.Config{ tlsConfig := &tls.Config{
ServerName: c.config.TLSConfig.ServerName, ServerName: c.config.TLSConfig.ServerName,
InsecureSkipVerify: c.config.TLSConfig.InsecureSkipVerify, InsecureSkipVerify: c.config.TLSConfig.InsecureSkipVerify,
RootCAs: c.config.TLSConfig.RootCAs, VerifyPeerCertificate: c.config.TLSConfig.VerifyPeerCertificate,
RootCAs: c.config.TLSConfig.RootCAs,
} }
quicConfig := &quic.Config{ quicConfig := &quic.Config{
InitialStreamReceiveWindow: c.config.QUICConfig.InitialStreamReceiveWindow, InitialStreamReceiveWindow: c.config.QUICConfig.InitialStreamReceiveWindow,

View File

@ -88,9 +88,10 @@ func (f *udpConnFactory) New(addr net.Addr) (net.PacketConn, error) {
// TLSConfig contains the TLS configuration fields that we want to expose to the user. // TLSConfig contains the TLS configuration fields that we want to expose to the user.
type TLSConfig struct { type TLSConfig struct {
ServerName string ServerName string
InsecureSkipVerify bool InsecureSkipVerify bool
RootCAs *x509.CertPool VerifyPeerCertificate func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error
RootCAs *x509.CertPool
} }
// QUICConfig contains the QUIC configuration fields that we want to expose to the user. // QUICConfig contains the QUIC configuration fields that we want to expose to the user.