You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
186 lines
8.7 KiB
186 lines
8.7 KiB
using Microsoft.Win32;
|
|
using System;
|
|
using System.Net.Security;
|
|
using System.Net.Sockets;
|
|
using System.Security.Authentication;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace AIKKey.Infrastructure
|
|
{
|
|
/// <summary>
|
|
/// Win7 系统层 SChannel 默认不启用 TLS 1.2 协议,即使代码层(Flurl.Http /
|
|
/// websocket-sharp 连接级 SslProtocols)显式指定 TLS 1.2,底层握手仍会失败
|
|
/// (表现:websocket-sharp 关闭码 1015、HTTP 请求报"未能创建 SSL/TLS 安全通道")。
|
|
/// 需要同时写入两层注册表并重启程序才能生效:
|
|
/// 1. .NET 层:SchUseStrongCrypto=1
|
|
/// 2. 系统层:SCHANNEL\Protocols\TLS 1.2 的 Enabled=1 / DisabledByDefault=0
|
|
/// </summary>
|
|
public static class TlsRegistryHelper
|
|
{
|
|
private const string NetFrameworkKeyPath = @"SOFTWARE\Microsoft\.NETFramework\v4.0.30319";
|
|
private const string ValueName = "SchUseStrongCrypto";
|
|
|
|
// 系统级 SChannel 协议开关
|
|
private const string SchannelTls12ClientKeyPath =
|
|
@"SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client";
|
|
private const string SchannelTls12ServerKeyPath =
|
|
@"SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server";
|
|
private const string EnabledValueName = "Enabled";
|
|
private const string DisabledByDefaultValueName = "DisabledByDefault";
|
|
|
|
/// <summary>
|
|
/// 是否 Windows 7(含 SP1)。Win7 的 SChannel 默认不启用 TLS 1.2,
|
|
/// 需要系统级注册表 + 重启程序才生效。
|
|
/// </summary>
|
|
public static bool IsWindows7 =>
|
|
Environment.OSVersion.Platform == PlatformID.Win32NT &&
|
|
Environment.OSVersion.Version.Major == 6 &&
|
|
Environment.OSVersion.Version.Minor == 1;
|
|
|
|
/// <summary>
|
|
/// 检测并写入两层 TLS 1.2 注册表配置(.NET 层 SchUseStrongCrypto + 系统层 SChannel)。
|
|
/// 写入 HKLM 需要管理员权限,无权限时只累计到 <paramref name="error"/>,不抛异常。
|
|
/// </summary>
|
|
/// <param name="changed">本次是否实际写入了注册表(需要重启进程才能完全生效)。</param>
|
|
/// <param name="error">写入失败的原因(可能包含多个键的错误信息)。</param>
|
|
/// <returns>与 <paramref name="changed"/> 相同:true 表示至少一处已写入。</returns>
|
|
public static bool EnsureTls12Enabled(out bool changed, out string error)
|
|
{
|
|
changed = false;
|
|
error = string.Empty;
|
|
|
|
// ---- 1. .NET 层:SchUseStrongCrypto=1 ----
|
|
TryWriteDwordIfNeeded(RegistryView.Registry64, NetFrameworkKeyPath, ValueName, 1, ref changed, ref error);
|
|
|
|
// ---- 2. 系统层:SChannel TLS 1.2 Client / Server 启用 ----
|
|
TryWriteDwordIfNeeded(RegistryView.Registry64, SchannelTls12ClientKeyPath, EnabledValueName, 1, ref changed, ref error);
|
|
TryWriteDwordIfNeeded(RegistryView.Registry64, SchannelTls12ClientKeyPath, DisabledByDefaultValueName, 0, ref changed, ref error);
|
|
TryWriteDwordIfNeeded(RegistryView.Registry64, SchannelTls12ServerKeyPath, EnabledValueName, 1, ref changed, ref error);
|
|
TryWriteDwordIfNeeded(RegistryView.Registry64, SchannelTls12ServerKeyPath, DisabledByDefaultValueName, 0, ref changed, ref error);
|
|
|
|
return changed;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 检测两层 TLS 1.2 配置是否已生效(.NET 层或系统层任一已启用即可)。
|
|
/// 用于区分"尚未配置(需管理员运行一次)"与"已配置但可能需重启"。
|
|
/// </summary>
|
|
public static bool IsTls12Enabled()
|
|
{
|
|
return IsDwordValue(RegistryView.Registry64, NetFrameworkKeyPath, ValueName, 1)
|
|
|| IsDwordValue(RegistryView.Registry64, SchannelTls12ClientKeyPath, EnabledValueName, 1);
|
|
}
|
|
|
|
private static bool IsDwordValue(RegistryView view, string keyPath, string valueName, int expected)
|
|
{
|
|
try
|
|
{
|
|
using var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view);
|
|
using var key = baseKey.OpenSubKey(keyPath);
|
|
return key?.GetValue(valueName, 0) is int value && value == expected;
|
|
}
|
|
catch
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private static void TryWriteDwordIfNeeded(
|
|
RegistryView view,
|
|
string keyPath,
|
|
string valueName,
|
|
int value,
|
|
ref bool changed,
|
|
ref string error)
|
|
{
|
|
try
|
|
{
|
|
using var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, view);
|
|
using var key = baseKey.OpenSubKey(keyPath, writable: true)
|
|
?? baseKey.CreateSubKey(keyPath, writable: true);
|
|
if (key is null)
|
|
{
|
|
error += $"无法打开或创建注册表键: {keyPath} ({view})。";
|
|
return;
|
|
}
|
|
|
|
var current = key.GetValue(valueName, -1);
|
|
if (current is int cur && cur == value)
|
|
{
|
|
return; // 已生效,无需写入
|
|
}
|
|
|
|
key.SetValue(valueName, value, RegistryValueKind.DWord);
|
|
changed = true;
|
|
}
|
|
catch (UnauthorizedAccessException ex)
|
|
{
|
|
error += $"无权限写入 {keyPath}\\{valueName} ({view}): {ex.Message}。请以管理员身份运行一次,或手动执行 reg add 命令。";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
error += $"写入 {keyPath}\\{valueName} ({view}) 失败: {ex.Message}。";
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 读取当前两层 TLS 1.2 注册表配置的实际值,用于诊断为何 Win7 仍无法协商 TLS 1.2。
|
|
/// </summary>
|
|
public static string GetRegistryStatus()
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine($"SchUseStrongCrypto(.NET): {ReadDword(NetFrameworkKeyPath, ValueName)}");
|
|
sb.AppendLine($"SCHANNEL TLS1.2 Client Enabled: {ReadDword(SchannelTls12ClientKeyPath, EnabledValueName)}, DisabledByDefault: {ReadDword(SchannelTls12ClientKeyPath, DisabledByDefaultValueName)}");
|
|
sb.AppendLine($"SCHANNEL TLS1.2 Server Enabled: {ReadDword(SchannelTls12ServerKeyPath, EnabledValueName)}, DisabledByDefault: {ReadDword(SchannelTls12ServerKeyPath, DisabledByDefaultValueName)}");
|
|
return sb.ToString();
|
|
}
|
|
|
|
/// <summary>
|
|
/// 用 SslStream 对指定主机做一次 TLS 1.2 握手探测。
|
|
/// 代码层 SslProtocols 只是"请求"协议,最终能否协商取决于系统 SChannel 是否启用 TLS 1.2
|
|
/// (Win7 缺补丁/注册表未生效时,此处会抛"未能创建 SSL/TLS 安全通道")。
|
|
/// </summary>
|
|
public static async Task<string> ProbeTls12HandshakeAsync(string host, int port)
|
|
{
|
|
try
|
|
{
|
|
using var tcp = new TcpClient();
|
|
var connectTask = tcp.ConnectAsync(host, port);
|
|
if (await Task.WhenAny(connectTask, Task.Delay(TimeSpan.FromSeconds(8))) != connectTask)
|
|
{
|
|
return $"TCP 连接 {host}:{port} 超时(网络不通或防火墙拦截)";
|
|
}
|
|
await connectTask;
|
|
|
|
using var ssl = new SslStream(tcp.GetStream(), false, (s, cert, chain, errors) => true);
|
|
var handshakeTask = ssl.AuthenticateAsClientAsync(host, null, SslProtocols.Tls12, false);
|
|
if (await Task.WhenAny(handshakeTask, Task.Delay(TimeSpan.FromSeconds(8))) != handshakeTask)
|
|
{
|
|
return $"TLS 1.2 握手 {host}:{port} 超时";
|
|
}
|
|
await handshakeTask;
|
|
return $"TLS 1.2 握手成功: {host}:{port} (协议={ssl.SslProtocol})";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return $"TLS 1.2 握手失败: {host}:{port} — {ex.GetType().Name}: {ex.Message}";
|
|
}
|
|
}
|
|
|
|
private static string ReadDword(string keyPath, string valueName)
|
|
{
|
|
try
|
|
{
|
|
using var baseKey = RegistryKey.OpenBaseKey(RegistryHive.LocalMachine, RegistryView.Registry64);
|
|
using var key = baseKey.OpenSubKey(keyPath);
|
|
var value = key?.GetValue(valueName, -1);
|
|
return value is int i ? i.ToString() : $"未设置({value ?? "null"})";
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
return $"读取失败: {ex.Message}";
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|