Skip to content

Commit 1990850

Browse files
authored
Optimize Cert Pinning (2dust#8282)
1 parent e6cb146 commit 1990850

14 files changed

Lines changed: 140 additions & 64 deletions

File tree

v2rayN/ServiceLib/Manager/CertPemManager.cs

Lines changed: 82 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,7 @@ public class CertPemManager
202202
/// <summary>
203203
/// Get certificate in PEM format from a server with CA pinning validation
204204
/// </summary>
205-
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, int timeout = 10)
205+
public async Task<(string?, string?)> GetCertPemAsync(string target, string serverName, int timeout = 4)
206206
{
207207
try
208208
{
@@ -216,7 +216,13 @@ public class CertPemManager
216216

217217
using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate);
218218

219-
await ssl.AuthenticateAsClientAsync(serverName);
219+
var sslOptions = new SslClientAuthenticationOptions
220+
{
221+
TargetHost = serverName,
222+
RemoteCertificateValidationCallback = ValidateServerCertificate
223+
};
224+
225+
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
220226

221227
var remote = ssl.RemoteCertificate;
222228
if (remote == null)
@@ -242,7 +248,7 @@ public class CertPemManager
242248
/// <summary>
243249
/// Get certificate chain in PEM format from a server with CA pinning validation
244250
/// </summary>
245-
public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName, int timeout = 10)
251+
public async Task<(List<string>, string?)> GetCertChainPemAsync(string target, string serverName, int timeout = 4)
246252
{
247253
var pemList = new List<string>();
248254
try
@@ -257,7 +263,13 @@ public class CertPemManager
257263

258264
using var ssl = new SslStream(client.GetStream(), false, ValidateServerCertificate);
259265

260-
await ssl.AuthenticateAsClientAsync(serverName);
266+
var sslOptions = new SslClientAuthenticationOptions
267+
{
268+
TargetHost = serverName,
269+
RemoteCertificateValidationCallback = ValidateServerCertificate
270+
};
271+
272+
await ssl.AuthenticateAsClientAsync(sslOptions, cts.Token);
261273

262274
if (ssl.RemoteCertificate is not X509Certificate2 certChain)
263275
{
@@ -330,10 +342,74 @@ private bool ValidateServerCertificate(
330342
return TrustedCaThumbprints.Contains(rootThumbprint);
331343
}
332344

333-
public string ExportCertToPem(X509Certificate2 cert)
345+
public static string ExportCertToPem(X509Certificate2 cert)
334346
{
335347
var der = cert.Export(X509ContentType.Cert);
336-
var b64 = Convert.ToBase64String(der, Base64FormattingOptions.InsertLineBreaks);
348+
var b64 = Convert.ToBase64String(der);
337349
return $"-----BEGIN CERTIFICATE-----\n{b64}\n-----END CERTIFICATE-----\n";
338350
}
351+
352+
/// <summary>
353+
/// Parse concatenated PEM certificates string into a list of individual certificates
354+
/// Normalizes format: removes line breaks from base64 content for better compatibility
355+
/// </summary>
356+
/// <param name="pemChain">Concatenated PEM certificates string (supports both \r\n and \n line endings)</param>
357+
/// <returns>List of individual PEM certificate strings with normalized format</returns>
358+
public static List<string> ParsePemChain(string pemChain)
359+
{
360+
var certs = new List<string>();
361+
if (string.IsNullOrWhiteSpace(pemChain))
362+
{
363+
return certs;
364+
}
365+
366+
// Normalize line endings (CRLF -> LF) at the beginning
367+
pemChain = pemChain.Replace("\r\n", "\n").Replace("\r", "\n");
368+
369+
const string beginMarker = "-----BEGIN CERTIFICATE-----";
370+
const string endMarker = "-----END CERTIFICATE-----";
371+
372+
var index = 0;
373+
while (index < pemChain.Length)
374+
{
375+
var beginIndex = pemChain.IndexOf(beginMarker, index, StringComparison.Ordinal);
376+
if (beginIndex == -1)
377+
break;
378+
379+
var endIndex = pemChain.IndexOf(endMarker, beginIndex, StringComparison.Ordinal);
380+
if (endIndex == -1)
381+
break;
382+
383+
// Extract certificate content
384+
var base64Start = beginIndex + beginMarker.Length;
385+
var base64Content = pemChain.Substring(base64Start, endIndex - base64Start);
386+
387+
// Remove all whitespace from base64 content
388+
base64Content = new string(base64Content.Where(c => !char.IsWhiteSpace(c)).ToArray());
389+
390+
// Reconstruct with clean format: BEGIN marker + base64 (no line breaks) + END marker
391+
var normalizedCert = $"{beginMarker}\n{base64Content}\n{endMarker}\n";
392+
certs.Add(normalizedCert);
393+
394+
// Move to next certificate
395+
index = endIndex + endMarker.Length;
396+
}
397+
398+
return certs;
399+
}
400+
401+
/// <summary>
402+
/// Concatenate a list of PEM certificates into a single string
403+
/// </summary>
404+
/// <param name="pemList">List of individual PEM certificate strings</param>
405+
/// <returns>Concatenated PEM certificates string</returns>
406+
public static string ConcatenatePemChain(IEnumerable<string> pemList)
407+
{
408+
if (pemList == null)
409+
{
410+
return string.Empty;
411+
}
412+
413+
return string.Concat(pemList);
414+
}
339415
}

v2rayN/ServiceLib/Resx/ResUI.Designer.cs

Lines changed: 5 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

v2rayN/ServiceLib/Resx/ResUI.fa-Ir.resx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1606,8 +1606,10 @@
16061606
<value>Certificate Pinning</value>
16071607
</data>
16081608
<data name="TbCertPinningTips" xml:space="preserve">
1609-
<value>Server certificate (PEM format, optional). Entering a certificate will pin it.
1610-
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value>
1609+
<value>Server Certificate (PEM format, optional)
1610+
When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
1611+
1612+
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
16111613
</data>
16121614
<data name="TbFetchCert" xml:space="preserve">
16131615
<value>Fetch Certificate</value>

v2rayN/ServiceLib/Resx/ResUI.fr.resx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1603,8 +1603,10 @@
16031603
<value>Certificate Pinning</value>
16041604
</data>
16051605
<data name="TbCertPinningTips" xml:space="preserve">
1606-
<value>Certificat serveur (PEM, optionnel). L’ajout d’un certificat le fixe.
1607-
Ne pas utiliser « Obtenir le certificat » si « Autoriser non sécurisé » est activé.</value>
1606+
<value>Server Certificate (PEM format, optional)
1607+
When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
1608+
1609+
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
16081610
</data>
16091611
<data name="TbFetchCert" xml:space="preserve">
16101612
<value>Obtenir le certificat</value>
@@ -1627,4 +1629,4 @@ Ne pas utiliser « Obtenir le certificat » si « Autoriser non sécurisé » es
16271629
<data name="TbSettingsCustomSystemProxyScriptPath" xml:space="preserve">
16281630
<value>Chemin script proxy système personnalisé</value>
16291631
</data>
1630-
</root>
1632+
</root>

v2rayN/ServiceLib/Resx/ResUI.hu.resx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1606,8 +1606,10 @@
16061606
<value>Certificate Pinning</value>
16071607
</data>
16081608
<data name="TbCertPinningTips" xml:space="preserve">
1609-
<value>Server certificate (PEM format, optional). Entering a certificate will pin it.
1610-
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value>
1609+
<value>Server Certificate (PEM format, optional)
1610+
When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
1611+
1612+
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
16111613
</data>
16121614
<data name="TbFetchCert" xml:space="preserve">
16131615
<value>Fetch Certificate</value>

v2rayN/ServiceLib/Resx/ResUI.resx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1606,8 +1606,10 @@
16061606
<value>Certificate Pinning</value>
16071607
</data>
16081608
<data name="TbCertPinningTips" xml:space="preserve">
1609-
<value>Server certificate (PEM format, optional). Entering a certificate will pin it.
1610-
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value>
1609+
<value>Server Certificate (PEM format, optional)
1610+
When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
1611+
1612+
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
16111613
</data>
16121614
<data name="TbFetchCert" xml:space="preserve">
16131615
<value>Fetch Certificate</value>

v2rayN/ServiceLib/Resx/ResUI.ru.resx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1606,8 +1606,10 @@
16061606
<value>Certificate Pinning</value>
16071607
</data>
16081608
<data name="TbCertPinningTips" xml:space="preserve">
1609-
<value>Server certificate (PEM format, optional). Entering a certificate will pin it.
1610-
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value>
1609+
<value>Server Certificate (PEM format, optional)
1610+
When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
1611+
1612+
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
16111613
</data>
16121614
<data name="TbFetchCert" xml:space="preserve">
16131615
<value>Fetch Certificate</value>

v2rayN/ServiceLib/Resx/ResUI.zh-Hans.resx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1603,8 +1603,10 @@
16031603
<value>固定证书</value>
16041604
</data>
16051605
<data name="TbCertPinningTips" xml:space="preserve">
1606-
<value>服务器证书(PEM 格式,可选)。填入后将固定该证书。
1607-
启用“跳过证书验证”时,请勿使用 '获取证书'。</value>
1606+
<value>服务器证书(PEM 格式,可选)
1607+
当指定此证书后,将固定该证书,并禁用“跳过证书验证”选项。
1608+
1609+
“获取证书”操作可能失败,原因可能是使用了自签证书,或系统中存在不受信任或恶意的 CA。</value>
16081610
</data>
16091611
<data name="TbFetchCert" xml:space="preserve">
16101612
<value>获取证书</value>

v2rayN/ServiceLib/Resx/ResUI.zh-Hant.resx

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1603,8 +1603,10 @@
16031603
<value>Certificate Pinning</value>
16041604
</data>
16051605
<data name="TbCertPinningTips" xml:space="preserve">
1606-
<value>Server certificate (PEM format, optional). Entering a certificate will pin it.
1607-
Do not use the "Fetch Certificate" button when "Allow Insecure" is enabled.</value>
1606+
<value>Server Certificate (PEM format, optional)
1607+
When specified, the certificate will be pinned, and "Allow Insecure" will be disabled.
1608+
1609+
The "Get Certificate" action may fail if a self-signed certificate is used or if the system contains an untrusted or malicious CA.</value>
16081610
</data>
16091611
<data name="TbFetchCert" xml:space="preserve">
16101612
<value>Fetch Certificate</value>

v2rayN/ServiceLib/Services/CoreConfig/Singbox/SingboxOutboundService.cs

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -261,13 +261,7 @@ private async Task<int> GenOutboundTls(ProfileItem node, Outbound4Sbox outbound)
261261
}
262262
if (node.StreamSecurity == Global.StreamSecurity)
263263
{
264-
var certs = node.Cert
265-
?.Split("-----END CERTIFICATE-----", StringSplitOptions.RemoveEmptyEntries)
266-
.Select(s => s.TrimEx())
267-
.Where(s => !s.IsNullOrEmpty())
268-
.Select(s => s + "\n-----END CERTIFICATE-----")
269-
.Select(s => s.Replace("\r\n", "\n"))
270-
.ToList() ?? new();
264+
var certs = CertPemManager.ParsePemChain(node.Cert);
271265
if (certs.Count > 0)
272266
{
273267
tls.certificate = certs;

0 commit comments

Comments
 (0)