From b58de20ce4bbbd2acede91976229ca549b4e0c06 Mon Sep 17 00:00:00 2001 From: Alireza Ahmadi Date: Mon, 29 Jun 2026 21:27:20 +0200 Subject: [PATCH] fix certificate conflict #1153 --- cmd/migration/1_5.go | 117 ++++++++++++++++++++++++++++++++++++++++++ cmd/migration/main.go | 9 ++++ service/tls.go | 6 ++- util/cert.go | 12 +++++ util/outJson.go | 6 ++- 5 files changed, 147 insertions(+), 3 deletions(-) create mode 100644 cmd/migration/1_5.go diff --git a/cmd/migration/1_5.go b/cmd/migration/1_5.go new file mode 100644 index 0000000..b611500 --- /dev/null +++ b/cmd/migration/1_5.go @@ -0,0 +1,117 @@ +package migration + +import ( + "encoding/json" + + "github.com/alireza0/s-ui/util" + + "gorm.io/gorm" +) + +func to1_5_1(tx *gorm.DB) error { + type tlsRow struct { + Id uint + Server []byte + Client []byte + } + var tlsRows []tlsRow + if err := tx.Raw("SELECT id, server, client FROM tls").Scan(&tlsRows).Error; err != nil { + return err + } + + pinByTlsId := make(map[uint]string, len(tlsRows)) + for _, row := range tlsRows { + var server map[string]interface{} + if len(row.Server) > 0 { + _ = json.Unmarshal(row.Server, &server) + } + + pin := "" + isReality := false + if r, ok := server["reality"].(map[string]interface{}); ok { + isReality, _ = r["enabled"].(bool) + } + // Only self-signed certificates are pinned. + if !isReality { + if certPEM := util.CertPEMFromTLS(server); util.CertIsSelfSigned(certPEM) { + pin = util.CertPublicKeySha256(certPEM) + } + } + pinByTlsId[row.Id] = pin + + var client map[string]interface{} + if len(row.Client) > 0 { + if err := json.Unmarshal(row.Client, &client); err != nil { + continue + } + } + if client == nil { + client = map[string]interface{}{} + } + if applyTlsPin(client, pin) { + newClient, err := json.MarshalIndent(client, "", " ") + if err != nil { + return err + } + if err := tx.Exec("UPDATE tls SET client = ? WHERE id = ?", newClient, row.Id).Error; err != nil { + return err + } + } + } + + type inboundRow struct { + Id uint + TlsId uint + OutJson []byte + } + var inbounds []inboundRow + if err := tx.Raw("SELECT id, tls_id, out_json FROM inbounds WHERE tls_id > 0").Scan(&inbounds).Error; err != nil { + return err + } + for _, in := range inbounds { + if len(in.OutJson) == 0 { + continue + } + var out map[string]interface{} + if err := json.Unmarshal(in.OutJson, &out); err != nil { + continue + } + tlsM, ok := out["tls"].(map[string]interface{}) + if !ok { + continue + } + if applyTlsPin(tlsM, pinByTlsId[in.TlsId]) { + out["tls"] = tlsM + newOut, err := json.MarshalIndent(out, "", " ") + if err != nil { + return err + } + if err := tx.Exec("UPDATE inbounds SET out_json = ? WHERE id = ?", newOut, in.Id).Error; err != nil { + return err + } + } + } + return nil +} + +func applyTlsPin(tls map[string]interface{}, pin string) bool { + changed := false + if pin != "" { + if cur, _ := tls["certificate_public_key_sha256"].([]interface{}); len(cur) != 1 || cur[0] != pin { + tls["certificate_public_key_sha256"] = []string{pin} + changed = true + } + if _, ok := tls["certificate"]; ok { + delete(tls, "certificate") + changed = true + } + if _, ok := tls["certificate_path"]; ok { + delete(tls, "certificate_path") + changed = true + } + } else if _, ok := tls["certificate_public_key_sha256"]; ok { + delete(tls, "certificate_public_key_sha256") + changed = true + } + return changed +} diff --git a/cmd/migration/main.go b/cmd/migration/main.go index 3e70ca2..0b67e17 100644 --- a/cmd/migration/main.go +++ b/cmd/migration/main.go @@ -74,6 +74,15 @@ func MigrateDb() { } } + // Before 1.5.1: back-fill self-signed TLS public-key pins and rewrite OutJson + if dbVersion < "1.5.1" { + err = to1_5_1(tx) + if err != nil { + log.Fatal("Migration to 1.5.1 failed: ", err) + return + } + } + // Set version err = tx.Exec("UPDATE settings SET value = ? WHERE key = ?", currentVersion, "version").Error if err != nil { diff --git a/service/tls.go b/service/tls.go index 7e91ade..a39ed55 100644 --- a/service/tls.go +++ b/service/tls.go @@ -118,7 +118,9 @@ func setCertFingerprint(t *model.Tls) { isReality, _ = r["enabled"].(bool) } if !isReality { - pin = util.CertPublicKeySha256(util.CertPEMFromTLS(server)) + if certPEM := util.CertPEMFromTLS(server); util.CertIsSelfSigned(certPEM) { + pin = util.CertPublicKeySha256(certPEM) + } } var client map[string]interface{} @@ -133,6 +135,8 @@ func setCertFingerprint(t *model.Tls) { if pin != "" { client["certificate_public_key_sha256"] = []string{pin} + delete(client, "certificate") + delete(client, "certificate_path") } else { delete(client, "certificate_public_key_sha256") } diff --git a/util/cert.go b/util/cert.go index f1e09d3..2e5a558 100644 --- a/util/cert.go +++ b/util/cert.go @@ -56,6 +56,18 @@ func parseLeafCert(pemData string) *x509.Certificate { } } +// CertIsSelfSigned reports whether the leaf certificate in pemData is +// self-signed, i.e. its signature verifies against its own public key. Only +// self-signed certificates should be pinned via certificate_public_key_sha256; +// CA-signed certificates are validated normally. +func CertIsSelfSigned(pemData string) bool { + cert := parseLeafCert(pemData) + if cert == nil { + return false + } + return cert.CheckSignature(cert.SignatureAlgorithm, cert.RawTBSCertificate, cert.Signature) == nil +} + // CertPublicKeySha256 returns the base64-encoded SHA256 of the certificate's // SubjectPublicKeyInfo (sing-box `certificate_public_key_sha256` / link pinSHA256). func CertPublicKeySha256(pemData string) string { diff --git a/util/outJson.go b/util/outJson.go index 5063d46..0eadcb4 100644 --- a/util/outJson.go +++ b/util/outJson.go @@ -101,8 +101,10 @@ func addTls(out *map[string]interface{}, tls *model.Tls) { if maxVersion, ok := tlsServer["max_version"]; ok { tlsConfig["max_version"] = maxVersion } - if certificate, ok := tlsServer["certificate"]; ok { - tlsConfig["certificate"] = certificate + if _, pinned := tlsConfig["certificate_public_key_sha256"]; !pinned { + if certificate, ok := tlsServer["certificate"]; ok { + tlsConfig["certificate"] = certificate + } } if cipherSuites, ok := tlsServer["cipher_suites"]; ok { tlsConfig["cipher_suites"] = cipherSuites