123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265 |
- 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<string, SslConnectionContext> 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<KestrelSslListener>());
- 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<byte>(Encoding.UTF8.GetBytes(ContentServerHello)), WebSocketMessageType.Text, true, CancellationToken.None);
- await Task.Delay(5000);
- }
- }
- private class CustomLogger<T> : ILogger<T>
- {
- public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
- public bool IsEnabled(LogLevel logLevel) => true;
- public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
- {
- Console.WriteLine(formatter != null!
- ? $"{logLevel}: {eventId}# {formatter(state, exception)}, {exception}"
- : $"{logLevel}: {eventId}# {state}, {exception}");
- }
- }
- }
|