diff --git a/common/net/tls.go b/common/net/tls.go deleted file mode 100644 index 77c0d7ca3..000000000 --- a/common/net/tls.go +++ /dev/null @@ -1,65 +0,0 @@ -package net - -import ( - "crypto/rand" - "crypto/rsa" - "crypto/sha256" - "crypto/tls" - "crypto/x509" - "encoding/hex" - "encoding/pem" - "fmt" - "math/big" -) - -type Path interface { - Resolve(path string) string -} - -func ParseCert(certificate, privateKey string, path Path) (tls.Certificate, error) { - if certificate == "" && privateKey == "" { - var err error - certificate, privateKey, _, err = NewRandomTLSKeyPair() - if err != nil { - return tls.Certificate{}, err - } - } - cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey)) - if painTextErr == nil { - return cert, nil - } - - certificate = path.Resolve(certificate) - privateKey = path.Resolve(privateKey) - cert, loadErr := tls.LoadX509KeyPair(certificate, privateKey) - if loadErr != nil { - return tls.Certificate{}, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) - } - return cert, nil -} - -func NewRandomTLSKeyPair() (certificate string, privateKey string, fingerprint string, err error) { - key, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - return - } - template := x509.Certificate{SerialNumber: big.NewInt(1)} - certDER, err := x509.CreateCertificate( - rand.Reader, - &template, - &template, - &key.PublicKey, - key) - if err != nil { - return - } - cert, err := x509.ParseCertificate(certDER) - if err != nil { - return - } - hash := sha256.Sum256(cert.Raw) - fingerprint = hex.EncodeToString(hash[:]) - privateKey = string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})) - certificate = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})) - return -} diff --git a/component/ca/config.go b/component/ca/config.go index 7ff353343..d9899dfa6 100644 --- a/component/ca/config.go +++ b/component/ca/config.go @@ -1,17 +1,13 @@ package ca import ( - "bytes" - "crypto/sha256" "crypto/tls" "crypto/x509" _ "embed" - "encoding/hex" "errors" "fmt" "os" "strconv" - "strings" "sync" C "github.com/metacubex/mihomo/constant" @@ -81,36 +77,6 @@ func getCertPool() *x509.CertPool { return globalCertPool } -func verifyFingerprint(fingerprint *[32]byte) func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { - return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { - // ssl pining - for i := range rawCerts { - rawCert := rawCerts[i] - cert, err := x509.ParseCertificate(rawCert) - if err == nil { - hash := sha256.Sum256(cert.Raw) - if bytes.Equal(fingerprint[:], hash[:]) { - return nil - } - } - } - return errNotMatch - } -} - -func convertFingerprint(fingerprint string) (*[32]byte, error) { - fingerprint = strings.TrimSpace(strings.Replace(fingerprint, ":", "", -1)) - fpByte, err := hex.DecodeString(fingerprint) - if err != nil { - return nil, err - } - - if len(fpByte) != 32 { - return nil, fmt.Errorf("fingerprint string length error,need sha256 fingerprint") - } - return (*[32]byte)(fpByte), nil -} - func GetCertPool(customCA string, customCAString string) (*x509.CertPool, error) { var certificate []byte var err error @@ -133,14 +99,6 @@ func GetCertPool(customCA string, customCAString string) (*x509.CertPool, error) } } -func NewFingerprintVerifier(fingerprint string) (func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error, error) { - fingerprintBytes, err := convertFingerprint(fingerprint) - if err != nil { - return nil, err - } - return verifyFingerprint(fingerprintBytes), nil -} - // GetTLSConfig specified fingerprint, customCA and customCAString func GetTLSConfig(tlsConfig *tls.Config, fingerprint string, customCA string, customCAString string) (_ *tls.Config, err error) { if tlsConfig == nil { diff --git a/component/ca/fingerprint.go b/component/ca/fingerprint.go new file mode 100644 index 000000000..43833756b --- /dev/null +++ b/component/ca/fingerprint.go @@ -0,0 +1,40 @@ +package ca + +import ( + "bytes" + "crypto/sha256" + "crypto/x509" + "encoding/hex" + "fmt" + "strings" +) + +// NewFingerprintVerifier returns a function that verifies whether a certificate's SHA-256 fingerprint matches the given one. +func NewFingerprintVerifier(fingerprint string) (func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error, error) { + fingerprint = strings.TrimSpace(strings.Replace(fingerprint, ":", "", -1)) + fpByte, err := hex.DecodeString(fingerprint) + if err != nil { + return nil, err + } + + if len(fpByte) != 32 { + return nil, fmt.Errorf("fingerprint string length error,need sha256 fingerprint") + } + + return func(rawCerts [][]byte, verifiedChains [][]*x509.Certificate) error { + // ssl pining + for _, rawCert := range rawCerts { + hash := sha256.Sum256(rawCert) + if bytes.Equal(fpByte, hash[:]) { + return nil + } + } + return errNotMatch + }, nil +} + +// CalculateFingerprint computes the SHA-256 fingerprint of the given DER-encoded certificate and returns it as a hex string. +func CalculateFingerprint(certDER []byte) string { + hash := sha256.Sum256(certDER) + return hex.EncodeToString(hash[:]) +} diff --git a/component/ca/keypair.go b/component/ca/keypair.go new file mode 100644 index 000000000..fd279bfcf --- /dev/null +++ b/component/ca/keypair.go @@ -0,0 +1,92 @@ +package ca + +import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "encoding/pem" + "fmt" + "math/big" +) + +type Path interface { + Resolve(path string) string +} + +// LoadTLSKeyPair loads a TLS key pair from the provided certificate and private key data or file paths, supporting fallback resolution. +// Returns a tls.Certificate and an error, where the error indicates issues during parsing or file loading. +// If both certificate and privateKey are empty, generates a random TLS RSA key pair. +// Accepts a Path interface for resolving file paths when necessary. +func LoadTLSKeyPair(certificate, privateKey string, path Path) (tls.Certificate, error) { + if certificate == "" && privateKey == "" { + var err error + certificate, privateKey, _, err = NewRandomTLSKeyPair(KeyPairTypeRSA) + if err != nil { + return tls.Certificate{}, err + } + } + cert, painTextErr := tls.X509KeyPair([]byte(certificate), []byte(privateKey)) + if painTextErr == nil { + return cert, nil + } + if path == nil { + return tls.Certificate{}, painTextErr + } + + certificate = path.Resolve(certificate) + privateKey = path.Resolve(privateKey) + cert, loadErr := tls.LoadX509KeyPair(certificate, privateKey) + if loadErr != nil { + return tls.Certificate{}, fmt.Errorf("parse certificate failed, maybe format error:%s, or path error: %s", painTextErr.Error(), loadErr.Error()) + } + return cert, nil +} + +type KeyPairType string + +const ( + KeyPairTypeRSA KeyPairType = "rsa" + KeyPairTypeP256 KeyPairType = "p256" + KeyPairTypeP384 KeyPairType = "p384" + KeyPairTypeEd25519 KeyPairType = "ed25519" +) + +// NewRandomTLSKeyPair generates a random TLS key pair based on the specified KeyPairType and returns it with a SHA256 fingerprint. +// Note: Most browsers do not support KeyPairTypeEd25519 type of certificate, and utls.UConn will also reject this type of certificate. +func NewRandomTLSKeyPair(keyPairType KeyPairType) (certificate string, privateKey string, fingerprint string, err error) { + var key crypto.Signer + switch keyPairType { + case KeyPairTypeRSA: + key, err = rsa.GenerateKey(rand.Reader, 2048) + case KeyPairTypeP256: + key, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + case KeyPairTypeP384: + key, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader) + case KeyPairTypeEd25519: + _, key, err = ed25519.GenerateKey(rand.Reader) + default: // fallback to KeyPairTypeRSA + key, err = rsa.GenerateKey(rand.Reader, 2048) + } + if err != nil { + return + } + + template := x509.Certificate{SerialNumber: big.NewInt(1)} + certDER, err := x509.CreateCertificate(rand.Reader, &template, &template, key.Public(), key) + if err != nil { + return + } + privBytes, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return + } + fingerprint = CalculateFingerprint(certDER) + privateKey = string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: privBytes})) + certificate = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certDER})) + return +} diff --git a/hub/route/server.go b/hub/route/server.go index 3c3e5ca47..2ccd8596d 100644 --- a/hub/route/server.go +++ b/hub/route/server.go @@ -15,8 +15,8 @@ import ( "time" "github.com/metacubex/mihomo/adapter/inbound" - CN "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/utils" + "github.com/metacubex/mihomo/component/ca" C "github.com/metacubex/mihomo/constant" "github.com/metacubex/mihomo/log" "github.com/metacubex/mihomo/tunnel/statistic" @@ -186,7 +186,7 @@ func startTLS(cfg *Config) { // handle tlsAddr if len(cfg.TLSAddr) > 0 { - c, err := CN.ParseCert(cfg.Certificate, cfg.PrivateKey, C.Path) + c, err := ca.LoadTLSKeyPair(cfg.Certificate, cfg.PrivateKey, C.Path) if err != nil { log.Errorln("External controller tls listen error: %s", err) return diff --git a/listener/anytls/server.go b/listener/anytls/server.go index 2293f7c92..f06aafd2f 100644 --- a/listener/anytls/server.go +++ b/listener/anytls/server.go @@ -12,7 +12,7 @@ import ( "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/common/atomic" "github.com/metacubex/mihomo/common/buf" - N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/component/ca" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/sing" @@ -43,7 +43,7 @@ func New(config LC.AnyTLSServer, tunnel C.Tunnel, additions ...inbound.Addition) tlsConfig := &tls.Config{} if config.Certificate != "" && config.PrivateKey != "" { - cert, err := N.ParseCert(config.Certificate, config.PrivateKey, C.Path) + cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) if err != nil { return nil, err } diff --git a/listener/http/server.go b/listener/http/server.go index 3c1eacdd5..52483081e 100644 --- a/listener/http/server.go +++ b/listener/http/server.go @@ -6,7 +6,7 @@ import ( "net" "github.com/metacubex/mihomo/adapter/inbound" - N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/component/ca" C "github.com/metacubex/mihomo/constant" authStore "github.com/metacubex/mihomo/listener/auth" LC "github.com/metacubex/mihomo/listener/config" @@ -68,7 +68,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { - cert, err := N.ParseCert(config.Certificate, config.PrivateKey, C.Path) + cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) if err != nil { return nil, err } diff --git a/listener/inbound/common_test.go b/listener/inbound/common_test.go index 528cbbf9c..7c5718dc0 100644 --- a/listener/inbound/common_test.go +++ b/listener/inbound/common_test.go @@ -30,7 +30,7 @@ var httpPath = "/inbound_test" var httpData = make([]byte, 10240) var remoteAddr = netip.MustParseAddr("1.2.3.4") var userUUID = utils.NewUUIDV4().String() -var tlsCertificate, tlsPrivateKey, tlsFingerprint, _ = N.NewRandomTLSKeyPair() +var tlsCertificate, tlsPrivateKey, tlsFingerprint, _ = ca.NewRandomTLSKeyPair(ca.KeyPairTypeP256) var tlsConfigCert, _ = tls.X509KeyPair([]byte(tlsCertificate), []byte(tlsPrivateKey)) var tlsConfig = &tls.Config{Certificates: []tls.Certificate{tlsConfigCert}, NextProtos: []string{"h2", "http/1.1"}} var tlsClientConfig, _ = ca.GetTLSConfig(nil, tlsFingerprint, "", "") diff --git a/listener/mixed/mixed.go b/listener/mixed/mixed.go index 0ffdb02a3..6893bb5a1 100644 --- a/listener/mixed/mixed.go +++ b/listener/mixed/mixed.go @@ -8,6 +8,7 @@ import ( "github.com/metacubex/mihomo/adapter/inbound" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/auth" + "github.com/metacubex/mihomo/component/ca" C "github.com/metacubex/mihomo/constant" authStore "github.com/metacubex/mihomo/listener/auth" LC "github.com/metacubex/mihomo/listener/config" @@ -63,7 +64,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { - cert, err := N.ParseCert(config.Certificate, config.PrivateKey, C.Path) + cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) if err != nil { return nil, err } diff --git a/listener/sing_hysteria2/server.go b/listener/sing_hysteria2/server.go index dd51896bd..92bd13979 100644 --- a/listener/sing_hysteria2/server.go +++ b/listener/sing_hysteria2/server.go @@ -13,8 +13,8 @@ import ( "github.com/metacubex/mihomo/adapter/inbound" "github.com/metacubex/mihomo/adapter/outbound" - CN "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/sockopt" + "github.com/metacubex/mihomo/component/ca" tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" @@ -56,7 +56,7 @@ func New(config LC.Hysteria2Server, tunnel C.Tunnel, additions ...inbound.Additi sl = &Listener{false, config, nil, nil} - cert, err := CN.ParseCert(config.Certificate, config.PrivateKey, C.Path) + cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) if err != nil { return nil, err } diff --git a/listener/sing_vless/server.go b/listener/sing_vless/server.go index c16bc97bf..b2eeb37bf 100644 --- a/listener/sing_vless/server.go +++ b/listener/sing_vless/server.go @@ -11,7 +11,7 @@ import ( "unsafe" "github.com/metacubex/mihomo/adapter/inbound" - N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/component/ca" tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" @@ -87,7 +87,7 @@ func New(config LC.VlessServer, tunnel C.Tunnel, additions ...inbound.Addition) var httpHandler http.Handler if config.Certificate != "" && config.PrivateKey != "" { - cert, err := N.ParseCert(config.Certificate, config.PrivateKey, C.Path) + cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) if err != nil { return nil, err } diff --git a/listener/sing_vmess/server.go b/listener/sing_vmess/server.go index dc80e8cb8..657ed2fbd 100644 --- a/listener/sing_vmess/server.go +++ b/listener/sing_vmess/server.go @@ -10,7 +10,7 @@ import ( "strings" "github.com/metacubex/mihomo/adapter/inbound" - N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/component/ca" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/reality" @@ -80,7 +80,7 @@ func New(config LC.VmessServer, tunnel C.Tunnel, additions ...inbound.Addition) var httpHandler http.Handler if config.Certificate != "" && config.PrivateKey != "" { - cert, err := N.ParseCert(config.Certificate, config.PrivateKey, C.Path) + cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) if err != nil { return nil, err } diff --git a/listener/socks/tcp.go b/listener/socks/tcp.go index 9e42a6250..ab4086a38 100644 --- a/listener/socks/tcp.go +++ b/listener/socks/tcp.go @@ -9,6 +9,7 @@ import ( "github.com/metacubex/mihomo/adapter/inbound" N "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/component/auth" + "github.com/metacubex/mihomo/component/ca" C "github.com/metacubex/mihomo/constant" authStore "github.com/metacubex/mihomo/listener/auth" LC "github.com/metacubex/mihomo/listener/config" @@ -62,7 +63,7 @@ func NewWithConfig(config LC.AuthServer, tunnel C.Tunnel, additions ...inbound.A var realityBuilder *reality.Builder if config.Certificate != "" && config.PrivateKey != "" { - cert, err := N.ParseCert(config.Certificate, config.PrivateKey, C.Path) + cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) if err != nil { return nil, err } diff --git a/listener/trojan/server.go b/listener/trojan/server.go index 299575a56..f239cb3b9 100644 --- a/listener/trojan/server.go +++ b/listener/trojan/server.go @@ -9,7 +9,7 @@ import ( "strings" "github.com/metacubex/mihomo/adapter/inbound" - N "github.com/metacubex/mihomo/common/net" + "github.com/metacubex/mihomo/component/ca" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" "github.com/metacubex/mihomo/listener/reality" @@ -74,7 +74,7 @@ func New(config LC.TrojanServer, tunnel C.Tunnel, additions ...inbound.Addition) var httpHandler http.Handler if config.Certificate != "" && config.PrivateKey != "" { - cert, err := N.ParseCert(config.Certificate, config.PrivateKey, C.Path) + cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) if err != nil { return nil, err } diff --git a/listener/tuic/server.go b/listener/tuic/server.go index 7d54b9053..75b39a5bf 100644 --- a/listener/tuic/server.go +++ b/listener/tuic/server.go @@ -7,8 +7,8 @@ import ( "time" "github.com/metacubex/mihomo/adapter/inbound" - CN "github.com/metacubex/mihomo/common/net" "github.com/metacubex/mihomo/common/sockopt" + "github.com/metacubex/mihomo/component/ca" tlsC "github.com/metacubex/mihomo/component/tls" C "github.com/metacubex/mihomo/constant" LC "github.com/metacubex/mihomo/listener/config" @@ -48,7 +48,7 @@ func New(config LC.TuicServer, tunnel C.Tunnel, additions ...inbound.Addition) ( return nil, err } - cert, err := CN.ParseCert(config.Certificate, config.PrivateKey, C.Path) + cert, err := ca.LoadTLSKeyPair(config.Certificate, config.PrivateKey, C.Path) if err != nil { return nil, err }