fix certificate conflict #1153
Some checks are pending
Release S-UI / build-frontend (push) Waiting to run
Release S-UI / build-386 (push) Blocked by required conditions
Release S-UI / build-amd64 (push) Blocked by required conditions
Release S-UI / build-armv5 (push) Blocked by required conditions
Release S-UI / build-armv6 (push) Blocked by required conditions
Release S-UI / build-armv7 (push) Blocked by required conditions
Release S-UI / build-arm64 (push) Blocked by required conditions
Release S-UI / build-s390x (push) Blocked by required conditions
Build S-UI for Windows / build-frontend (push) Waiting to run
Build S-UI for Windows / build-windows-amd64 (push) Blocked by required conditions
Build S-UI for Windows / build-windows-arm64 (push) Blocked by required conditions

This commit is contained in:
Alireza Ahmadi 2026-06-29 21:27:20 +02:00
parent 3b81e7656f
commit b58de20ce4
5 changed files with 147 additions and 3 deletions

117
cmd/migration/1_5.go Normal file
View file

@ -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
}

View file

@ -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 {

View file

@ -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")
}

View file

@ -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 {

View file

@ -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