From 98ee790771cea366e1e314c27d6d1b0ac96ba051 Mon Sep 17 00:00:00 2001 From: DHR60 Date: Thu, 8 Jan 2026 17:38:21 +0800 Subject: [PATCH] Add Cert SHA-256 pinning support --- v2rayN/ServiceLib/Handler/ConfigHandler.cs | 1 + v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs | 5 ++ .../Manager/ActionPrecheckManager.cs | 4 +- v2rayN/ServiceLib/Manager/CertPemManager.cs | 18 +++++++ v2rayN/ServiceLib/Models/ProfileItem.cs | 1 + v2rayN/ServiceLib/Models/V2rayConfig.cs | 1 + v2rayN/ServiceLib/Resx/ResUI.Designer.cs | 20 +++++++- v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx | 6 +++ v2rayN/ServiceLib/Resx/ResUI.fr.resx | 12 +++-- v2rayN/ServiceLib/Resx/ResUI.hu.resx | 8 +++- v2rayN/ServiceLib/Resx/ResUI.resx | 8 +++- v2rayN/ServiceLib/Resx/ResUI.ru.resx | 8 +++- v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx | 10 +++- v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx | 8 +++- .../CoreConfig/V2ray/V2rayOutboundService.cs | 4 ++ .../ViewModels/AddServerViewModel.cs | 48 ++++++++++++++++--- .../Views/AddServerWindow.axaml | 17 +++++++ .../Views/AddServerWindow.axaml.cs | 1 + v2rayN/v2rayN/Views/AddServerWindow.xaml | 20 ++++++++ v2rayN/v2rayN/Views/AddServerWindow.xaml.cs | 1 + 20 files changed, 184 insertions(+), 17 deletions(-) diff --git a/v2rayN/ServiceLib/Handler/ConfigHandler.cs b/v2rayN/ServiceLib/Handler/ConfigHandler.cs index 3718a73f..5b34e9bf 100644 --- a/v2rayN/ServiceLib/Handler/ConfigHandler.cs +++ b/v2rayN/ServiceLib/Handler/ConfigHandler.cs @@ -253,6 +253,7 @@ public static class ConfigHandler item.Extra = profileItem.Extra; item.MuxEnabled = profileItem.MuxEnabled; item.Cert = profileItem.Cert; + item.CertSha = profileItem.CertSha; item.EchConfigList = profileItem.EchConfigList; item.EchForceQuery = profileItem.EchForceQuery; } diff --git a/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs b/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs index 34f30505..8bd3c3de 100644 --- a/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs +++ b/v2rayN/ServiceLib/Handler/Fmt/BaseFmt.cs @@ -74,6 +74,10 @@ public class BaseFmt { dicQuery.Add("ech", Utils.UrlEncode(item.EchConfigList)); } + if (item.CertSha.IsNotEmpty()) + { + dicQuery.Add("pcs", Utils.UrlEncode(item.CertSha)); + } dicQuery.Add("type", item.Network.IsNotEmpty() ? item.Network : nameof(ETransport.tcp)); @@ -214,6 +218,7 @@ public class BaseFmt item.SpiderX = GetQueryDecoded(query, "spx"); item.Mldsa65Verify = GetQueryDecoded(query, "pqv"); item.EchConfigList = GetQueryDecoded(query, "ech"); + item.CertSha = GetQueryDecoded(query, "pcs"); if (_allowInsecureArray.Any(k => GetQueryDecoded(query, k) == "1")) { diff --git a/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs b/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs index 73a1020a..50fc4961 100644 --- a/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs +++ b/v2rayN/ServiceLib/Manager/ActionPrecheckManager.cs @@ -168,7 +168,9 @@ public class ActionPrecheckManager if (item.StreamSecurity == Global.StreamSecurity) { // check certificate validity - if ((!item.Cert.IsNullOrEmpty()) && (CertPemManager.ParsePemChain(item.Cert).Count == 0)) + if (!item.Cert.IsNullOrEmpty() + && (CertPemManager.ParsePemChain(item.Cert).Count == 0) + && !item.CertSha.IsNullOrEmpty()) { errors.Add(string.Format(ResUI.InvalidProperty, "TLS Certificate")); } diff --git a/v2rayN/ServiceLib/Manager/CertPemManager.cs b/v2rayN/ServiceLib/Manager/CertPemManager.cs index 247b5842..dfefc746 100644 --- a/v2rayN/ServiceLib/Manager/CertPemManager.cs +++ b/v2rayN/ServiceLib/Manager/CertPemManager.cs @@ -416,4 +416,22 @@ public class CertPemManager return string.Concat(pemList); } + + public static string GetCertSha256Thumbprint(string pemCert, bool includeColon = false) + { + try + { + var cert = X509Certificate2.CreateFromPem(pemCert); + var thumbprint = cert.GetCertHashString(HashAlgorithmName.SHA256); + if (includeColon) + { + return string.Join(":", thumbprint.Chunk(2).Select(c => new string(c))); + } + return thumbprint; + } + catch + { + return string.Empty; + } + } } diff --git a/v2rayN/ServiceLib/Models/ProfileItem.cs b/v2rayN/ServiceLib/Models/ProfileItem.cs index fa7b16ba..b5424265 100644 --- a/v2rayN/ServiceLib/Models/ProfileItem.cs +++ b/v2rayN/ServiceLib/Models/ProfileItem.cs @@ -161,6 +161,7 @@ public class ProfileItem : ReactiveObject public string Extra { get; set; } public bool? MuxEnabled { get; set; } public string Cert { get; set; } + public string CertSha { get; set; } public string EchConfigList { get; set; } public string EchForceQuery { get; set; } } diff --git a/v2rayN/ServiceLib/Models/V2rayConfig.cs b/v2rayN/ServiceLib/Models/V2rayConfig.cs index 2d9c1d2d..9593b54e 100644 --- a/v2rayN/ServiceLib/Models/V2rayConfig.cs +++ b/v2rayN/ServiceLib/Models/V2rayConfig.cs @@ -355,6 +355,7 @@ public class TlsSettings4Ray public string? spiderX { get; set; } public string? mldsa65Verify { get; set; } public List? certificates { get; set; } + public string? pinnedPeerCertSha256 { get; set; } public bool? disableSystemRoot { get; set; } public string? echConfigList { get; set; } public string? echForceQuery { get; set; } diff --git a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs index d7c7b43e..8adf0472 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.Designer.cs +++ b/v2rayN/ServiceLib/Resx/ResUI.Designer.cs @@ -2617,7 +2617,7 @@ namespace ServiceLib.Resx { } /// - /// 查找类似 Server Certificate (PEM format, optional) + /// 查找类似 Pinned certificate (fill in either one) ///When specified, the certificate will be pinned, and "Allow Insecure" will be disabled. /// ///The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA. 的本地化字符串。 @@ -2628,6 +2628,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Certificate fingerprint (SHA-256) 的本地化字符串。 + /// + public static string TbCertSha256Tips { + get { + return ResourceManager.GetString("TbCertSha256Tips", resourceCulture); + } + } + /// /// 查找类似 Clear system proxy 的本地化字符串。 /// @@ -2889,6 +2898,15 @@ namespace ServiceLib.Resx { } } + /// + /// 查找类似 Full certificate (chain), PEM format 的本地化字符串。 + /// + public static string TbFullCertTips { + get { + return ResourceManager.GetString("TbFullCertTips", resourceCulture); + } + } + /// /// 查找类似 This feature is intended for advanced users and those with special requirements. Once enabled, it will ignore the Core's basic settings, DNS settings, and routing settings. You must ensure that the system proxy port, traffic statistics, and other related configurations are set correctly — everything will be configured by you. 的本地化字符串。 /// diff --git a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx index 47bae402..1c9cd81d 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx @@ -1647,4 +1647,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if EchForceQuery + + Full certificate (chain), PEM format + + + Certificate fingerprint (SHA-256) + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.fr.resx b/v2rayN/ServiceLib/Resx/ResUI.fr.resx index a2c95286..1531421d 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.fr.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.fr.resx @@ -1606,10 +1606,10 @@ Certificate Pinning - Certificat serveur (format PEM, facultatif) -Si le certificat est défini, il est fixé et l’option « Ignorer la vérification » est désactivée. + Pinned certificate (fill in either one) +When specified, the certificate will be pinned, and "Allow Insecure" will be disabled. -Si un certificat auto-signé est utilisé ou si le système contient une CA non fiable ou malveillante, l’action « Obtenir le certificat » peut échouer. +The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA. Obtenir le certificat @@ -1644,4 +1644,10 @@ Si un certificat auto-signé est utilisé ou si le système contient une CA non EchForceQuery + + Full certificate (chain), PEM format + + + Certificate fingerprint (SHA-256) + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.hu.resx b/v2rayN/ServiceLib/Resx/ResUI.hu.resx index 9a1235da..c7a852c9 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.hu.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.hu.resx @@ -1609,7 +1609,7 @@ Certificate Pinning - Server Certificate (PEM format, optional) + Pinned certificate (fill in either one) When specified, the certificate will be pinned, and "Allow Insecure" will be disabled. The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA. @@ -1647,4 +1647,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if EchForceQuery + + Full certificate (chain), PEM format + + + Certificate fingerprint (SHA-256) + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.resx b/v2rayN/ServiceLib/Resx/ResUI.resx index 569798da..b07980ab 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.resx @@ -1609,7 +1609,7 @@ Certificate Pinning - Server Certificate (PEM format, optional) + Pinned certificate (fill in either one) When specified, the certificate will be pinned, and "Allow Insecure" will be disabled. The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA. @@ -1647,4 +1647,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if EchForceQuery + + Full certificate (chain), PEM format + + + Certificate fingerprint (SHA-256) + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.ru.resx b/v2rayN/ServiceLib/Resx/ResUI.ru.resx index 6e1ee37c..16267356 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.ru.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.ru.resx @@ -1609,7 +1609,7 @@ Certificate Pinning - Server Certificate (PEM format, optional) + Pinned certificate (fill in either one) When specified, the certificate will be pinned, and "Allow Insecure" will be disabled. The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA. @@ -1647,4 +1647,10 @@ The "Get Certificate" action may fail if a self-signed certificate is used or if EchForceQuery + + Full certificate (chain), PEM format + + + Certificate fingerprint (SHA-256) + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx index d37cd62c..33b531ab 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx @@ -1606,10 +1606,10 @@ 固定证书 - 服务器证书(PEM 格式,可选) + 固定证书(二选一填写即可) 当指定此证书后,将固定该证书,并禁用“跳过证书验证”选项。 -“获取证书”操作可能失败,原因可能是使用了自签证书,或系统中存在不受信任或恶意的 CA。 +“获取证书”操作可能失败,原因包括使用了自签名证书,或系统中存在不受信任甚至恶意的 CA。 获取证书 @@ -1644,4 +1644,10 @@ EchForceQuery + + 完整证书(链),PEM 格式 + + + 证书指纹(SHA-256) + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx index 89edc2f3..b04e73a4 100644 --- a/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx +++ b/v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx @@ -1606,7 +1606,7 @@ 憑證綁定 - 伺服器憑證(PEM 格式,可選) + 固定憑證(二選一填寫即可) 若已指定,憑證將會被綁定,並且「跳過憑證驗證」將被停用。 若使用自簽憑證,或系統中存在不受信任或惡意的 CA,「取得憑證」動作可能會失敗。 @@ -1644,4 +1644,10 @@ EchForceQuery + + Full certificate (chain), PEM format + + + Certificate fingerprint (SHA-256) + \ No newline at end of file diff --git a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs index d4285a09..274431b3 100644 --- a/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs +++ b/v2rayN/ServiceLib/Services/CoreConfig/V2ray/V2rayOutboundService.cs @@ -301,6 +301,10 @@ public partial class CoreConfigV2rayService tlsSettings.disableSystemRoot = true; tlsSettings.allowInsecure = false; } + else if (!node.CertSha.IsNullOrEmpty()) + { + tlsSettings.pinnedPeerCertSha256 = node.CertSha; + } streamSettings.tlsSettings = tlsSettings; } diff --git a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs index 804287c5..9e39a938 100644 --- a/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs +++ b/v2rayN/ServiceLib/ViewModels/AddServerViewModel.cs @@ -14,6 +14,9 @@ public class AddServerViewModel : MyReactiveObject [Reactive] public string CertTip { get; set; } + [Reactive] + public string CertSha { get; set; } + public ReactiveCommand FetchCertCmd { get; } public ReactiveCommand FetchCertChainCmd { get; } public ReactiveCommand SaveCmd { get; } @@ -39,6 +42,12 @@ public class AddServerViewModel : MyReactiveObject this.WhenAnyValue(x => x.Cert) .Subscribe(_ => UpdateCertTip()); + this.WhenAnyValue(x => x.CertSha) + .Subscribe(_ => UpdateCertTip()); + + this.WhenAnyValue(x => x.Cert) + .Subscribe(_ => UpdateCertSha()); + if (profileItem.IndexId.IsNullOrEmpty()) { profileItem.Network = Global.DefaultNetwork; @@ -97,7 +106,8 @@ public class AddServerViewModel : MyReactiveObject } } SelectedSource.CoreType = CoreType.IsNullOrEmpty() ? null : (ECoreType)Enum.Parse(typeof(ECoreType), CoreType); - SelectedSource.Cert = Cert.IsNullOrEmpty() ? null : Cert; + SelectedSource.Cert = Cert.IsNullOrEmpty() ? string.Empty : Cert; + SelectedSource.CertSha = CertSha.IsNullOrEmpty() ? string.Empty : CertSha; if (await ConfigHandler.AddServer(_config, SelectedSource) == 0) { @@ -113,10 +123,36 @@ public class AddServerViewModel : MyReactiveObject private void UpdateCertTip(string? errorMessage = null) { CertTip = errorMessage.IsNullOrEmpty() - ? (Cert.IsNullOrEmpty() ? ResUI.CertNotSet : ResUI.CertSet) + ? ((Cert.IsNullOrEmpty() && CertSha.IsNullOrEmpty()) ? ResUI.CertNotSet : ResUI.CertSet) : errorMessage; } + private void UpdateCertSha() + { + if (Cert.IsNullOrEmpty()) + { + return; + } + + var certList = CertPemManager.ParsePemChain(Cert); + if (certList.Count == 0) + { + return; + } + + List shaList = new(); + foreach (var cert in certList) + { + var sha = CertPemManager.GetCertSha256Thumbprint(cert); + if (sha.IsNullOrEmpty()) + { + return; + } + shaList.Add(sha); + } + CertSha = string.Join('~', shaList); + } + private async Task FetchCert() { if (SelectedSource.StreamSecurity != Global.StreamSecurity) @@ -142,8 +178,8 @@ public class AddServerViewModel : MyReactiveObject { domain += $":{SelectedSource.Port}"; } - string certError; - (Cert, certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName); + + (Cert, var certError) = await CertPemManager.Instance.GetCertPemAsync(domain, serverName); UpdateCertTip(certError); } @@ -172,8 +208,8 @@ public class AddServerViewModel : MyReactiveObject { domain += $":{SelectedSource.Port}"; } - string certError; - (var certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName); + + var (certs, certError) = await CertPemManager.Instance.GetCertChainPemAsync(domain, serverName); Cert = CertPemManager.ConcatenatePemChain(certs); UpdateCertTip(certError); } diff --git a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml index e7a45a77..19c62868 100644 --- a/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml +++ b/v2rayN/v2rayN.Desktop/Views/AddServerWindow.axaml @@ -841,6 +841,23 @@ Margin="{StaticResource Margin4}" Content="{x:Static resx:ResUI.TbFetchCertChain}" /> + + + this.Bind(ViewModel, vm => vm.SelectedSource.AllowInsecure, v => v.cmbAllowInsecure.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint.SelectedValue).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Alpn, v => v.cmbAlpn.SelectedValue).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.CertSha, v => v.txtCertSha256Pinning.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CertTip, v => v.labCertPinning.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.EchConfigList, v => v.txtEchConfigList.Text).DisposeWith(disposables); diff --git a/v2rayN/v2rayN/Views/AddServerWindow.xaml b/v2rayN/v2rayN/Views/AddServerWindow.xaml index 980496c8..1f70489b 100644 --- a/v2rayN/v2rayN/Views/AddServerWindow.xaml +++ b/v2rayN/v2rayN/Views/AddServerWindow.xaml @@ -1072,6 +1072,26 @@ Content="{x:Static resx:ResUI.TbFetchCertChain}" Style="{StaticResource DefButton}" /> + + + vm.SelectedSource.AllowInsecure, v => v.cmbAllowInsecure.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Fingerprint, v => v.cmbFingerprint.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.SelectedSource.Alpn, v => v.cmbAlpn.Text).DisposeWith(disposables); + this.Bind(ViewModel, vm => vm.CertSha, v => v.txtCertSha256Pinning.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.CertTip, v => v.labCertPinning.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables); this.Bind(ViewModel, vm => vm.Cert, v => v.txtCert.Text).DisposeWith(disposables);