PocKestrelSsl.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265
  1. using System.Collections.Concurrent;
  2. using System.Globalization;
  3. using System.Net;
  4. using System.Net.Security;
  5. using System.Net.WebSockets;
  6. using System.Security.Cryptography;
  7. using System.Security.Cryptography.X509Certificates;
  8. using System.Text;
  9. using Microsoft.AspNetCore.Server.Kestrel.Https;
  10. using PCC2.Networking;
  11. using PCC2.Networking.KestrelSsl;
  12. using PCC2.Security;
  13. namespace PCC2.Pocs;
  14. internal static class PocKestrelSsl
  15. {
  16. //服务器S
  17. // > 服务端证书S1
  18. // > >信任对等体C1
  19. // > >信任对等体C2
  20. // > 服务端证书S2
  21. // > >信任对等体C3
  22. // > >信任对等体C4
  23. //客户端C
  24. // >信任对等体S1
  25. // >信任对等体S2
  26. // C从 C1、C2、C3、C4 中选一个
  27. // S从 S1、S2 中选一个
  28. //具体流程
  29. // 1、 C通过SNI发送(服务端公钥哈希+自己的证书指纹+时间戳)
  30. // 2、 S根据SNI的公钥哈希选择服务端证书(SSL握手流程)
  31. // 创建【SSL连接上下文】,用服务端证书、客户端证书指纹
  32. // 3、 S验证C证书确保公钥在S信任列表(SSL握手流程)
  33. // 这个上下文取不到SNI,但是能根据客户端证书指纹获取【SSL连接上下文】。
  34. // 根据 客户端公钥哈希 检索 S信任对等体 所属 服务器证书 确保和【SSL连接上下文】的 服务器公钥哈希 匹配
  35. // 检查确保有对应证书指纹而且未标记【客户端证书已验证】
  36. // 在【SSL连接上下文】标记这个连接为【客户端证书已验证】
  37. // 4、 C验证S证书确保公钥在C信任列表(SSL握手流程)
  38. // 5、 S在应用层核对C证书所属服务端(SSL握手完成)
  39. // 使用客户端证书指纹查询上下文,验证客户端证书在【SSL连接上下文】并且【客户端证书已验证】
  40. //这个PoC案例只为双方创建一个证书,调试以验证概念
  41. private const string ContentClientHello = "Hi Server 汉字";
  42. private const string ContentServerHello = "Hi Client 汉字";
  43. private static X509Certificate2 cerSv, cerCl;
  44. private const int SkewSeconds = 15;
  45. //替换ConnectionId 上下文机制 时间戳、客户端证书指纹、服务端
  46. private static readonly ConcurrentDictionary<string, SslConnectionContext> clientCertTpToSslCtx = new(); //实际使用需要考虑释放,根据已验证的时间戳?
  47. private class SslConnectionContext(
  48. string serverPubHash
  49. //,string clientCertTp,
  50. //DateTimeOffset timeStamp
  51. )
  52. {
  53. public string ServerPubHash { get; set; } = serverPubHash;
  54. //public string ClientCertTp { get; set; } = clientCertTp;
  55. //public DateTimeOffset TimeStamp { get; set; } = timeStamp;
  56. public bool IsClientCertValidated { get; set; }
  57. }
  58. public static async Task RunPoc()
  59. {
  60. cerSv = RsaUtility.GenerateShortLivedCertificate(RsaUtility.FromKey(RsaUtility.GenerateKey(1024)), SkewSeconds); //节省调试时间,实际使用要增加强度
  61. cerCl = RsaUtility.GenerateShortLivedCertificate(RsaUtility.FromKey(RsaUtility.GenerateKey(1024)), SkewSeconds);
  62. var kws = new KestrelSslListener(new IPEndPoint(IPAddress.Loopback, 12345), SelectServerCer, ValidationClientCer, HandleWsConnection, ClientCertificateMode.RequireCertificate, new CustomLogger<KestrelSslListener>());
  63. await kws.StartAsync();
  64. // 1、 C通过SNI发送(服务端公钥哈希+自己的证书指纹+时间戳)
  65. var svPubHash = Convert.ToHexString(SHA256.HashData(cerSv.PublicKey.GetRSAPublicKey().ExportRSAPublicKey()));
  66. var clCertTp = cerCl.Thumbprint;
  67. var sni = $"{svPubHash}.{clCertTp}.{DateTimeOffset.Now.ToUnixTimeMilliseconds():X}";
  68. Console.WriteLine($"CL_CON: SNI: {sni}");
  69. var ws = new SniWssClient(sni, IPAddress.Loopback.ToString(), 12345);
  70. ws.ClientCertificates.Add(cerCl);
  71. ws.RemoteCertificateValidationCallback = (_, serverCertificate, _, _) =>
  72. {
  73. // 4、 C验证S证书确保公钥在C信任列表(SSL握手流程)
  74. if (serverCertificate is X509Certificate2 x2)
  75. {
  76. var provided = serverCertificate?.GetPublicKeyString();
  77. var excepted = cerSv.GetPublicKeyString();
  78. //核对公钥
  79. if (provided == excepted)
  80. {
  81. //验证有效期
  82. var currentTime = DateTime.Now;
  83. if (currentTime >= x2.NotBefore && currentTime <= x2.NotAfter && (x2.NotAfter - x2.NotBefore).TotalSeconds <= SkewSeconds * 2) return true;
  84. Console.WriteLine("Certificate is expired or not yet valid or too long time.");
  85. }
  86. }
  87. return false;
  88. };
  89. await ws.ConnectAsync(CancellationToken.None);
  90. await ws.SendAsync(Encoding.UTF8.GetBytes(ContentClientHello), WebSocketMessageType.Text, true, CancellationToken.None);
  91. var buffer = new byte[1024];
  92. var r = await ws.ReceiveAsync(buffer, CancellationToken.None);
  93. var rxStr = Encoding.UTF8.GetString(buffer, 0, r.Count);
  94. var OK = rxStr == ContentServerHello;
  95. if (OK)
  96. {
  97. int bpSuccess = 0;
  98. }
  99. else
  100. {
  101. int bpFail = 0;
  102. }
  103. while (true) //用于调试浏览器时等待请求
  104. {
  105. await Task.Delay(100);
  106. }
  107. }
  108. private static X509Certificate2? SelectServerCer(string? sni)
  109. {
  110. // 2、 S根据SNI的公钥哈希选择服务端证书(SSL握手流程)
  111. // 创建【SSL连接上下文】,用服务端证书、客户端证书指纹、时间戳
  112. Console.WriteLine($"SV_ACC: SNI: {sni}");
  113. var sniPart = sni?.Split('.', 3);
  114. if (sniPart == null) return null;
  115. if (sniPart.Length < 3) return null;
  116. //通过sni传递的字符串值会变小写
  117. var svPubHash = sniPart[0].ToUpper();
  118. var clCertTp = sniPart[1].ToUpper();
  119. //时间戳不使用,为了避免Kestrel对相同的SNI缓存服务器证书而加上去
  120. //var timeStamp = DateTimeOffset.FromUnixTimeMilliseconds(long.Parse(sniPart[2], NumberStyles.HexNumber));
  121. var expectedSvPubHash = Convert.ToHexString(SHA256.HashData(cerSv.PublicKey.GetRSAPublicKey().ExportRSAPublicKey()));
  122. if (expectedSvPubHash == svPubHash)
  123. {
  124. if (clientCertTpToSslCtx.ContainsKey(clCertTp))
  125. {
  126. //客户端证书指纹重复,可能是内鬼,阻止连接
  127. Console.WriteLine($"SV_ACC: SECURE ALERT: Client Cert Thumbprint DUP <{clCertTp}>");
  128. return null;
  129. }
  130. clientCertTpToSslCtx[clCertTp] = new SslConnectionContext(svPubHash); //, clCertTp, timeStamp);
  131. Console.WriteLine($"SV_CTX: CTP: {clCertTp}");
  132. return cerSv;
  133. }
  134. return null;
  135. }
  136. private static bool ValidationClientCer(X509Certificate2 providedClientCer, X509Chain? arg2, SslPolicyErrors arg3)
  137. {
  138. // 3、 S验证C证书确保公钥在S信任列表(SSL握手流程)
  139. // 这个上下文取不到SNI,但是能根据客户端证书指纹获取【SSL连接上下文】。
  140. // 根据 客户端公钥哈希 检索 S信任对等体 所属 服务器证书 确保和【SSL连接上下文】的 服务器公钥哈希 匹配
  141. // 检查确保有对应证书指纹而且未标记【客户端证书已验证】
  142. // 在【SSL连接上下文】标记这个连接为【客户端证书已验证】
  143. if (providedClientCer is not X509Certificate2 x2ClientCer) return false;
  144. //获取【SSL连接上下文】
  145. if (clientCertTpToSslCtx.TryGetValue(x2ClientCer.Thumbprint, out var ctx) == false)
  146. {
  147. Console.WriteLine($"SV_VAL: Err: Ctx not found <{x2ClientCer.Thumbprint}>");
  148. return false;
  149. }
  150. //实际情况:
  151. // 根据 ctx.ServerPubHash 找所属 服务端证书id
  152. // 按 服务端证书id 下挂信任对等体找对应 客户端公钥哈希 的 公钥 得到 excepted
  153. // 如果找不到就直接拒绝
  154. var excepted = cerCl.GetPublicKeyString();
  155. var provided = x2ClientCer.GetPublicKeyString();
  156. //核对公钥
  157. if (provided != excepted)
  158. {
  159. Console.WriteLine("SV_VAL: Err: UnTrusted public key.");
  160. return false;
  161. }
  162. //验证有效期
  163. var currentTime = DateTime.Now;
  164. if (currentTime < x2ClientCer.NotBefore || currentTime > x2ClientCer.NotAfter || !((x2ClientCer.NotAfter - x2ClientCer.NotBefore).TotalSeconds <= SkewSeconds * 2))
  165. {
  166. Console.WriteLine("SV_VAL: Err: Certificate is expired or not yet valid or too long duration time.");
  167. return false;
  168. }
  169. if (ctx.IsClientCertValidated)
  170. {
  171. //上下文早已通过验证,还来?是内鬼?
  172. Console.WriteLine($"SV_VAL: SECURE ALERT: Ctx already validated <{x2ClientCer.Thumbprint}>");
  173. return false;
  174. }
  175. ctx.IsClientCertValidated = true;
  176. return true;
  177. }
  178. private static async Task HandleWsConnection(X509Certificate2? clientCert, WebSocket ws)
  179. {
  180. // 5、 S在应用层核对C证书所属服务端(SSL握手完成)
  181. // 使用客户端证书指纹查询上下文,验证客户端证书在【SSL连接上下文】并且【客户端证书已验证】
  182. if (clientCert == null) return; // 没提供证书,出去!
  183. if (clientCertTpToSslCtx.TryGetValue(clientCert.Thumbprint, out var ctx) == false)
  184. {
  185. Console.WriteLine($"CL_WSH: Err: ctx not found <{clientCert.Thumbprint}>");
  186. return;
  187. }
  188. if (ctx.IsClientCertValidated == false) // 这个检查估计多余,按照前面的流程
  189. {
  190. Console.WriteLine($"CL_WSH: SECURE ALERT: ctx not validated <{clientCert.Thumbprint}>");
  191. return;
  192. }
  193. clientCertTpToSslCtx.Remove(clientCert.Thumbprint, out _); //如果OK就提前释放
  194. var buff = new byte[1024];
  195. var r = await ws.ReceiveAsync(buff, CancellationToken.None);
  196. var rText = Encoding.UTF8.GetString(buff, 0, r.Count);
  197. if (rText == ContentClientHello)
  198. {
  199. await ws.SendAsync(new ReadOnlyMemory<byte>(Encoding.UTF8.GetBytes(ContentServerHello)), WebSocketMessageType.Text, true, CancellationToken.None);
  200. await Task.Delay(5000);
  201. }
  202. }
  203. private class CustomLogger<T> : ILogger<T>
  204. {
  205. public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
  206. public bool IsEnabled(LogLevel logLevel) => true;
  207. public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
  208. {
  209. Console.WriteLine(formatter != null!
  210. ? $"{logLevel}: {eventId}# {formatter(state, exception)}, {exception}"
  211. : $"{logLevel}: {eventId}# {state}, {exception}");
  212. }
  213. }
  214. }