using System.Collections.Concurrent; using System.Globalization; using System.Net; using System.Net.Security; using System.Net.WebSockets; using System.Security.Cryptography; using System.Security.Cryptography.X509Certificates; using System.Text; using Microsoft.AspNetCore.Server.Kestrel.Https; using PCC2.Networking; using PCC2.Networking.KestrelSsl; using PCC2.Security; namespace PCC2.Pocs; internal static class PocKestrelSsl { //服务器S // > 服务端证书S1 // > >信任对等体C1 // > >信任对等体C2 // > 服务端证书S2 // > >信任对等体C3 // > >信任对等体C4 //客户端C // >信任对等体S1 // >信任对等体S2 // C从 C1、C2、C3、C4 中选一个 // S从 S1、S2 中选一个 //具体流程 // 1、 C通过SNI发送(服务端公钥哈希+自己的证书指纹+时间戳) // 2、 S根据SNI的公钥哈希选择服务端证书(SSL握手流程) // 创建【SSL连接上下文】,用服务端证书、客户端证书指纹 // 3、 S验证C证书确保公钥在S信任列表(SSL握手流程) // 这个上下文取不到SNI,但是能根据客户端证书指纹获取【SSL连接上下文】。 // 根据 客户端公钥哈希 检索 S信任对等体 所属 服务器证书 确保和【SSL连接上下文】的 服务器公钥哈希 匹配 // 检查确保有对应证书指纹而且未标记【客户端证书已验证】 // 在【SSL连接上下文】标记这个连接为【客户端证书已验证】 // 4、 C验证S证书确保公钥在C信任列表(SSL握手流程) // 5、 S在应用层核对C证书所属服务端(SSL握手完成) // 使用客户端证书指纹查询上下文,验证客户端证书在【SSL连接上下文】并且【客户端证书已验证】 //这个PoC案例只为双方创建一个证书,调试以验证概念 private const string ContentClientHello = "Hi Server 汉字"; private const string ContentServerHello = "Hi Client 汉字"; private static X509Certificate2 cerSv, cerCl; private const int SkewSeconds = 15; //替换ConnectionId 上下文机制 时间戳、客户端证书指纹、服务端 private static readonly ConcurrentDictionary clientCertTpToSslCtx = new(); //实际使用需要考虑释放,根据已验证的时间戳? private class SslConnectionContext( string serverPubHash //,string clientCertTp, //DateTimeOffset timeStamp ) { public string ServerPubHash { get; set; } = serverPubHash; //public string ClientCertTp { get; set; } = clientCertTp; //public DateTimeOffset TimeStamp { get; set; } = timeStamp; public bool IsClientCertValidated { get; set; } } public static async Task RunPoc() { cerSv = RsaUtility.GenerateShortLivedCertificate(RsaUtility.FromKey(RsaUtility.GenerateKey(1024)), SkewSeconds); //节省调试时间,实际使用要增加强度 cerCl = RsaUtility.GenerateShortLivedCertificate(RsaUtility.FromKey(RsaUtility.GenerateKey(1024)), SkewSeconds); var kws = new KestrelSslListener(new IPEndPoint(IPAddress.Loopback, 12345), SelectServerCer, ValidationClientCer, HandleWsConnection, ClientCertificateMode.RequireCertificate, new CustomLogger()); await kws.StartAsync(); // 1、 C通过SNI发送(服务端公钥哈希+自己的证书指纹+时间戳) var svPubHash = Convert.ToHexString(SHA256.HashData(cerSv.PublicKey.GetRSAPublicKey().ExportRSAPublicKey())); var clCertTp = cerCl.Thumbprint; var sni = $"{svPubHash}.{clCertTp}.{DateTimeOffset.Now.ToUnixTimeMilliseconds():X}"; Console.WriteLine($"CL_CON: SNI: {sni}"); var ws = new SniWssClient(sni, IPAddress.Loopback.ToString(), 12345); ws.ClientCertificates.Add(cerCl); ws.RemoteCertificateValidationCallback = (_, serverCertificate, _, _) => { // 4、 C验证S证书确保公钥在C信任列表(SSL握手流程) if (serverCertificate is X509Certificate2 x2) { var provided = serverCertificate?.GetPublicKeyString(); var excepted = cerSv.GetPublicKeyString(); //核对公钥 if (provided == excepted) { //验证有效期 var currentTime = DateTime.Now; if (currentTime >= x2.NotBefore && currentTime <= x2.NotAfter && (x2.NotAfter - x2.NotBefore).TotalSeconds <= SkewSeconds * 2) return true; Console.WriteLine("Certificate is expired or not yet valid or too long time."); } } return false; }; await ws.ConnectAsync(CancellationToken.None); await ws.SendAsync(Encoding.UTF8.GetBytes(ContentClientHello), WebSocketMessageType.Text, true, CancellationToken.None); var buffer = new byte[1024]; var r = await ws.ReceiveAsync(buffer, CancellationToken.None); var rxStr = Encoding.UTF8.GetString(buffer, 0, r.Count); var OK = rxStr == ContentServerHello; if (OK) { int bpSuccess = 0; } else { int bpFail = 0; } while (true) //用于调试浏览器时等待请求 { await Task.Delay(100); } } private static X509Certificate2? SelectServerCer(string? sni) { // 2、 S根据SNI的公钥哈希选择服务端证书(SSL握手流程) // 创建【SSL连接上下文】,用服务端证书、客户端证书指纹、时间戳 Console.WriteLine($"SV_ACC: SNI: {sni}"); var sniPart = sni?.Split('.', 3); if (sniPart == null) return null; if (sniPart.Length < 3) return null; //通过sni传递的字符串值会变小写 var svPubHash = sniPart[0].ToUpper(); var clCertTp = sniPart[1].ToUpper(); //时间戳不使用,为了避免Kestrel对相同的SNI缓存服务器证书而加上去 //var timeStamp = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(sniPart[2], NumberStyles.HexNumber)); var expectedSvPubHash = Convert.ToHexString(SHA256.HashData(cerSv.PublicKey.GetRSAPublicKey().ExportRSAPublicKey())); if (expectedSvPubHash == svPubHash) { if (clientCertTpToSslCtx.ContainsKey(clCertTp)) { //客户端证书指纹重复,可能是内鬼,阻止连接 Console.WriteLine($"SV_ACC: SECURE ALERT: Client Cert Thumbprint DUP <{clCertTp}>"); return null; } clientCertTpToSslCtx[clCertTp] = new SslConnectionContext(svPubHash); //, clCertTp, timeStamp); Console.WriteLine($"SV_CTX: CTP: {clCertTp}"); return cerSv; } return null; } private static bool ValidationClientCer(X509Certificate2 providedClientCer, X509Chain? arg2, SslPolicyErrors arg3) { // 3、 S验证C证书确保公钥在S信任列表(SSL握手流程) // 这个上下文取不到SNI,但是能根据客户端证书指纹获取【SSL连接上下文】。 // 根据 客户端公钥哈希 检索 S信任对等体 所属 服务器证书 确保和【SSL连接上下文】的 服务器公钥哈希 匹配 // 检查确保有对应证书指纹而且未标记【客户端证书已验证】 // 在【SSL连接上下文】标记这个连接为【客户端证书已验证】 if (providedClientCer is not X509Certificate2 x2ClientCer) return false; //获取【SSL连接上下文】 if (clientCertTpToSslCtx.TryGetValue(x2ClientCer.Thumbprint, out var ctx) == false) { Console.WriteLine($"SV_VAL: Err: Ctx not found <{x2ClientCer.Thumbprint}>"); return false; } //实际情况: // 根据 ctx.ServerPubHash 找所属 服务端证书id // 按 服务端证书id 下挂信任对等体找对应 客户端公钥哈希 的 公钥 得到 excepted // 如果找不到就直接拒绝 var excepted = cerCl.GetPublicKeyString(); var provided = x2ClientCer.GetPublicKeyString(); //核对公钥 if (provided != excepted) { Console.WriteLine("SV_VAL: Err: UnTrusted public key."); return false; } //验证有效期 var currentTime = DateTime.Now; if (currentTime < x2ClientCer.NotBefore || currentTime > x2ClientCer.NotAfter || !((x2ClientCer.NotAfter - x2ClientCer.NotBefore).TotalSeconds <= SkewSeconds * 2)) { Console.WriteLine("SV_VAL: Err: Certificate is expired or not yet valid or too long duration time."); return false; } if (ctx.IsClientCertValidated) { //上下文早已通过验证,还来?是内鬼? Console.WriteLine($"SV_VAL: SECURE ALERT: Ctx already validated <{x2ClientCer.Thumbprint}>"); return false; } ctx.IsClientCertValidated = true; return true; } private static async Task HandleWsConnection(X509Certificate2? clientCert, WebSocket ws) { // 5、 S在应用层核对C证书所属服务端(SSL握手完成) // 使用客户端证书指纹查询上下文,验证客户端证书在【SSL连接上下文】并且【客户端证书已验证】 if (clientCert == null) return; // 没提供证书,出去! if (clientCertTpToSslCtx.TryGetValue(clientCert.Thumbprint, out var ctx) == false) { Console.WriteLine($"CL_WSH: Err: ctx not found <{clientCert.Thumbprint}>"); return; } if (ctx.IsClientCertValidated == false) // 这个检查估计多余,按照前面的流程 { Console.WriteLine($"CL_WSH: SECURE ALERT: ctx not validated <{clientCert.Thumbprint}>"); return; } clientCertTpToSslCtx.Remove(clientCert.Thumbprint, out _); //如果OK就提前释放 var buff = new byte[1024]; var r = await ws.ReceiveAsync(buff, CancellationToken.None); var rText = Encoding.UTF8.GetString(buff, 0, r.Count); if (rText == ContentClientHello) { await ws.SendAsync(new ReadOnlyMemory(Encoding.UTF8.GetBytes(ContentServerHello)), WebSocketMessageType.Text, true, CancellationToken.None); await Task.Delay(5000); } } private class CustomLogger : ILogger { public IDisposable? BeginScope(TState state) where TState : notnull => null; public bool IsEnabled(LogLevel logLevel) => true; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { Console.WriteLine(formatter != null! ? $"{logLevel}: {eventId}# {formatter(state, exception)}, {exception}" : $"{logLevel}: {eventId}# {state}, {exception}"); } } }