| | | 1 | | // Licensed to the .NET Foundation under one or more agreements. |
| | | 2 | | // The .NET Foundation licenses this file to you under the MIT license. |
| | | 3 | | |
| | | 4 | | using System.Buffers; |
| | | 5 | | using System.Collections.Generic; |
| | | 6 | | using System.Diagnostics; |
| | | 7 | | using System.Runtime.CompilerServices; |
| | | 8 | | using System.Runtime.ExceptionServices; |
| | | 9 | | using System.Security; |
| | | 10 | | using System.Security.Authentication; |
| | | 11 | | using System.Security.Authentication.ExtendedProtection; |
| | | 12 | | using System.Security.Cryptography; |
| | | 13 | | using System.Security.Cryptography.X509Certificates; |
| | | 14 | | using System.Threading; |
| | | 15 | | using System.Threading.Tasks; |
| | | 16 | | |
| | | 17 | | namespace System.Net.Security |
| | | 18 | | { |
| | | 19 | | public partial class SslStream |
| | | 20 | | { |
| | | 21 | | |
| | | 22 | | |
| | | 23 | | private SafeFreeCredentials? _credentialsHandle; |
| | | 24 | | // Keeps a cache hit alive until SSPI has retained its own credential reference. |
| | | 25 | | private SafeFreeCredentials? _cachedCredentialsHandle; |
| | | 26 | | |
| | | 27 | | #if TARGET_APPLE |
| | | 28 | | // on OSX, we have two implementations of SafeDeleteContext, so store a reference to the base class |
| | | 29 | | private SafeDeleteContext? _securityContext; |
| | | 30 | | #else |
| | | 31 | | internal SafeDeleteSslContext? _securityContext; |
| | | 32 | | #endif |
| | | 33 | | |
| | | 34 | | private SslConnectionInfo _connectionInfo; |
| | | 35 | | private X509Certificate? _selectedClientCertificate; |
| | | 36 | | private X509Certificate2? _remoteCertificate; |
| | | 37 | | private bool _remoteCertificateExposed; |
| | | 38 | | |
| | | 39 | | // -1 for uninitialized, 0 for false, 1 for true, should be accessed via IsLocalClientCertificateUsed property |
| | 31 | 40 | | private int _localClientCertificateUsed = -1; |
| | | 41 | | |
| | | 42 | | // These are the MAX encrypt buffer output sizes, not the actual sizes. |
| | 31 | 43 | | private int _headerSize = 5; //ATTN must be set to at least 5 by default |
| | 31 | 44 | | private int _trailerSize = 16; |
| | 31 | 45 | | private int _maxDataSize = 16354; |
| | | 46 | | |
| | 1 | 47 | | private static readonly Oid s_serverAuthOid = new Oid("1.3.6.1.5.5.7.3.1", "1.3.6.1.5.5.7.3.1"); |
| | 1 | 48 | | private static readonly Oid s_clientAuthOid = new Oid("1.3.6.1.5.5.7.3.2", "1.3.6.1.5.5.7.3.2"); |
| | | 49 | | |
| | | 50 | | // |
| | | 51 | | // Protocol properties |
| | | 52 | | // |
| | | 53 | | // LocalServerCertificate - local certificate for server mode channel |
| | | 54 | | // LocalClientCertificate - selected certificated used in the client channel mode otherwise null |
| | | 55 | | // IsRemoteCertificateAvailable - true if the remote side has provided a certificate |
| | | 56 | | // HeaderSize - Header & trailer sizes used in the TLS stream |
| | | 57 | | // TrailerSize - |
| | | 58 | | // |
| | | 59 | | internal X509Certificate? LocalServerCertificate |
| | | 60 | | { |
| | | 61 | | get |
| | 0 | 62 | | { |
| | 0 | 63 | | return _sslAuthenticationOptions.CertificateContext?.TargetCertificate; |
| | 0 | 64 | | } |
| | | 65 | | } |
| | | 66 | | |
| | | 67 | | // IsLocalCertificateUsed is expensive, but it does not change during the lifetime of the SslStream except for r |
| | | 68 | | // can cache the value. |
| | | 69 | | private bool IsLocalClientCertificateUsed |
| | | 70 | | { |
| | | 71 | | get |
| | 0 | 72 | | { |
| | 0 | 73 | | if (_localClientCertificateUsed == -1) |
| | 0 | 74 | | { |
| | 0 | 75 | | _localClientCertificateUsed = CertificateValidationPal.IsLocalCertificateUsed(_credentialsHandle, _s |
| | 0 | 76 | | ? 1 |
| | 0 | 77 | | : 0; |
| | 0 | 78 | | } |
| | | 79 | | |
| | 0 | 80 | | return _localClientCertificateUsed == 1; |
| | 0 | 81 | | } |
| | | 82 | | } |
| | | 83 | | |
| | | 84 | | internal X509Certificate? LocalClientCertificate |
| | | 85 | | { |
| | | 86 | | get |
| | 0 | 87 | | { |
| | 0 | 88 | | if (_selectedClientCertificate != null && IsLocalClientCertificateUsed) |
| | 0 | 89 | | { |
| | 0 | 90 | | return _selectedClientCertificate; |
| | | 91 | | } |
| | | 92 | | |
| | 0 | 93 | | return null; |
| | 0 | 94 | | } |
| | | 95 | | } |
| | | 96 | | |
| | | 97 | | internal bool IsRemoteCertificateAvailable |
| | | 98 | | { |
| | | 99 | | get |
| | 0 | 100 | | { |
| | 0 | 101 | | return _remoteCertificate != null; |
| | 0 | 102 | | } |
| | | 103 | | } |
| | | 104 | | |
| | | 105 | | internal ChannelBinding? GetChannelBinding(ChannelBindingKind kind) |
| | 0 | 106 | | { |
| | 0 | 107 | | ChannelBinding? result = null; |
| | 0 | 108 | | if (_securityContext != null) |
| | 0 | 109 | | { |
| | 0 | 110 | | result = SslStreamPal.QueryContextChannelBinding(_securityContext, kind); |
| | 0 | 111 | | } |
| | | 112 | | |
| | 0 | 113 | | return result; |
| | 0 | 114 | | } |
| | | 115 | | |
| | | 116 | | internal int MaxDataSize |
| | | 117 | | { |
| | | 118 | | get |
| | 0 | 119 | | { |
| | 0 | 120 | | return _maxDataSize; |
| | 0 | 121 | | } |
| | | 122 | | } |
| | | 123 | | |
| | | 124 | | internal bool IsValidContext |
| | | 125 | | { |
| | | 126 | | [MethodImpl(MethodImplOptions.AggressiveInlining)] |
| | | 127 | | get |
| | 0 | 128 | | { |
| | 0 | 129 | | return !(_securityContext == null || _securityContext.IsInvalid); |
| | 0 | 130 | | } |
| | | 131 | | } |
| | | 132 | | |
| | | 133 | | internal bool RemoteCertRequired |
| | | 134 | | { |
| | | 135 | | get |
| | | 136 | | { |
| | | 137 | | return _sslAuthenticationOptions.RemoteCertRequired; |
| | | 138 | | } |
| | | 139 | | } |
| | | 140 | | |
| | | 141 | | internal void CloseContext() |
| | 31 | 142 | | { |
| | 31 | 143 | | if (!_remoteCertificateExposed) |
| | 31 | 144 | | { |
| | 31 | 145 | | _remoteCertificate?.Dispose(); |
| | 31 | 146 | | _remoteCertificate = null; |
| | 31 | 147 | | } |
| | | 148 | | |
| | 31 | 149 | | _securityContext?.Dispose(); |
| | 31 | 150 | | _credentialsHandle?.Dispose(); |
| | 31 | 151 | | ReleaseCachedCredentials(); |
| | | 152 | | |
| | 31 | 153 | | _sslAuthenticationOptions.Dispose(); |
| | 31 | 154 | | } |
| | | 155 | | |
| | | 156 | | private void ReleaseCachedCredentials() => |
| | 31 | 157 | | Interlocked.Exchange(ref _cachedCredentialsHandle, null)?.DangerousRelease(); |
| | | 158 | | |
| | | 159 | | // |
| | | 160 | | // SECURITY: we open a private key container on behalf of the caller |
| | | 161 | | // and we require the caller to have permission associated with that operation. |
| | | 162 | | // |
| | | 163 | | internal static unsafe X509Certificate2? FindCertificateWithPrivateKey(object instance, bool isServer, X509Certi |
| | 0 | 164 | | { |
| | 0 | 165 | | if (certificate == null) |
| | 0 | 166 | | { |
| | 0 | 167 | | return null; |
| | | 168 | | } |
| | | 169 | | |
| | 0 | 170 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 171 | | NetEventSource.Log.LocatingPrivateKey(certificate, instance); |
| | | 172 | | |
| | | 173 | | try |
| | 0 | 174 | | { |
| | | 175 | | // Protecting from X509Certificate2 derived classes. |
| | 0 | 176 | | X509Certificate2? certEx = MakeEx(certificate); |
| | | 177 | | |
| | 0 | 178 | | if (certEx is null) |
| | 0 | 179 | | { |
| | 0 | 180 | | return null; |
| | | 181 | | } |
| | | 182 | | |
| | 0 | 183 | | if (certEx.HasPrivateKey) |
| | 0 | 184 | | { |
| | 0 | 185 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 186 | | NetEventSource.Log.CertIsType2(instance); |
| | | 187 | | |
| | 0 | 188 | | return certEx; |
| | | 189 | | } |
| | | 190 | | |
| | 0 | 191 | | Span<byte> certHash = stackalloc byte[SHA512.HashSizeInBytes]; |
| | 0 | 192 | | bool ret = certEx.TryGetCertHash(HashAlgorithmName.SHA512, certHash, out int written); |
| | 0 | 193 | | Debug.Assert(ret && written == certHash.Length); |
| | | 194 | | |
| | 0 | 195 | | if (!object.ReferenceEquals(certificate, certEx)) |
| | 0 | 196 | | { |
| | 0 | 197 | | certEx.Dispose(); |
| | 0 | 198 | | } |
| | | 199 | | |
| | | 200 | | // ELSE Try the MY user and machine stores for private key check. |
| | | 201 | | // For server side mode MY machine store takes priority. |
| | 0 | 202 | | X509Certificate2? found = |
| | 0 | 203 | | FindCertWithPrivateKey(isServer, certHash) ?? |
| | 0 | 204 | | FindCertWithPrivateKey(!isServer, certHash); |
| | 0 | 205 | | if (found is not null) |
| | 0 | 206 | | { |
| | 0 | 207 | | return found; |
| | | 208 | | } |
| | | 209 | | |
| | | 210 | | X509Certificate2? FindCertWithPrivateKey(bool isServer, ReadOnlySpan<byte> certHash) |
| | 0 | 211 | | { |
| | 0 | 212 | | if (CertificateValidationPal.EnsureStoreOpened(isServer) is X509Store store) |
| | 0 | 213 | | { |
| | 0 | 214 | | X509Certificate2Collection certs = store.Certificates; |
| | 0 | 215 | | X509Certificate2Collection found = certs.FindByThumbprint(HashAlgorithmName.SHA512, certHash); |
| | 0 | 216 | | X509Certificate2? cert = null; |
| | | 217 | | try |
| | 0 | 218 | | { |
| | 0 | 219 | | if (found.Count > 0) |
| | 0 | 220 | | { |
| | 0 | 221 | | cert = found[0]; |
| | 0 | 222 | | if (cert.HasPrivateKey) |
| | 0 | 223 | | { |
| | 0 | 224 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 225 | | { |
| | 0 | 226 | | NetEventSource.Log.FoundCertInStore(isServer, instance); |
| | 0 | 227 | | } |
| | | 228 | | |
| | 0 | 229 | | return cert; |
| | | 230 | | } |
| | 0 | 231 | | } |
| | 0 | 232 | | } |
| | | 233 | | finally |
| | 0 | 234 | | { |
| | 0 | 235 | | for (int i = 0; i < certs.Count; i++) |
| | 0 | 236 | | { |
| | 0 | 237 | | X509Certificate2 toDispose = certs[i]; |
| | 0 | 238 | | if (!ReferenceEquals(toDispose, cert)) |
| | 0 | 239 | | { |
| | 0 | 240 | | toDispose.Dispose(); |
| | 0 | 241 | | } |
| | 0 | 242 | | } |
| | 0 | 243 | | } |
| | 0 | 244 | | } |
| | | 245 | | |
| | 0 | 246 | | return null; |
| | 0 | 247 | | } |
| | 0 | 248 | | } |
| | 0 | 249 | | catch (CryptographicException) |
| | 0 | 250 | | { |
| | 0 | 251 | | } |
| | | 252 | | |
| | 0 | 253 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 254 | | NetEventSource.Log.NotFoundCertInStore(instance); |
| | 0 | 255 | | return null; |
| | 0 | 256 | | } |
| | | 257 | | |
| | | 258 | | private static X509Certificate2? MakeEx(X509Certificate certificate) |
| | 0 | 259 | | { |
| | 0 | 260 | | Debug.Assert(certificate != null); |
| | | 261 | | |
| | 0 | 262 | | if (certificate.GetType() == typeof(X509Certificate2)) |
| | 0 | 263 | | { |
| | 0 | 264 | | return (X509Certificate2)certificate; |
| | | 265 | | } |
| | | 266 | | |
| | 0 | 267 | | X509Certificate2? certificateEx = null; |
| | | 268 | | try |
| | 0 | 269 | | { |
| | 0 | 270 | | if (certificate.Handle != IntPtr.Zero) |
| | 0 | 271 | | { |
| | 0 | 272 | | certificateEx = new X509Certificate2(certificate); |
| | 0 | 273 | | } |
| | 0 | 274 | | } |
| | 0 | 275 | | catch (SecurityException) { } |
| | 0 | 276 | | catch (CryptographicException) { } |
| | | 277 | | |
| | 0 | 278 | | return certificateEx; |
| | 0 | 279 | | } |
| | | 280 | | |
| | | 281 | | // |
| | | 282 | | // Get certificate_authorities list, according to RFC 5246, Section 7.4.4. |
| | | 283 | | // Used only by client SSL code, never returns null. |
| | | 284 | | // |
| | | 285 | | private string[] GetRequestCertificateAuthorities() |
| | 0 | 286 | | { |
| | 0 | 287 | | string[] issuers = Array.Empty<string>(); |
| | | 288 | | |
| | 0 | 289 | | if (IsValidContext) |
| | 0 | 290 | | { |
| | 0 | 291 | | issuers = CertificateValidationPal.GetRequestCertificateAuthorities(_securityContext!); |
| | 0 | 292 | | } |
| | 0 | 293 | | return issuers; |
| | 0 | 294 | | } |
| | | 295 | | |
| | | 296 | | internal X509Certificate2? SelectClientCertificate() |
| | 0 | 297 | | { |
| | 0 | 298 | | X509Certificate? clientCertificate = null; // candidate certificate that can come from the user callb |
| | 0 | 299 | | X509Certificate2? selectedCert = null; // final selected cert (ensured that it does have private |
| | 0 | 300 | | List<X509Certificate>? filteredCerts = null; // This is an intermediate client certs collection that tr |
| | | 301 | | string[] issuers; // This is a list of issuers sent by the server, only vali |
| | | 302 | | |
| | 0 | 303 | | if (_sslAuthenticationOptions.CertificateContext != null) |
| | 0 | 304 | | { |
| | 0 | 305 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 306 | | NetEventSource.Log.CertificateFromCertContext(this); |
| | | 307 | | |
| | | 308 | | // |
| | | 309 | | // SslStreamCertificateContext can only be constructed with a cert with a |
| | | 310 | | // private key, so we don't have to do any further processing. |
| | | 311 | | // |
| | | 312 | | |
| | 0 | 313 | | _selectedClientCertificate = _sslAuthenticationOptions.CertificateContext.TargetCertificate; |
| | 0 | 314 | | if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Selected cert = {_selectedClientCertific |
| | 0 | 315 | | return _sslAuthenticationOptions.CertificateContext.TargetCertificate; |
| | | 316 | | } |
| | 0 | 317 | | else if (_sslAuthenticationOptions.CertSelectionDelegate != null) |
| | 0 | 318 | | { |
| | 0 | 319 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 320 | | NetEventSource.Info(this, "Calling CertificateSelectionCallback"); |
| | | 321 | | |
| | 0 | 322 | | X509Certificate2? remoteCert = null; |
| | | 323 | | try |
| | 0 | 324 | | { |
| | 0 | 325 | | issuers = GetRequestCertificateAuthorities(); |
| | 0 | 326 | | remoteCert = CertificateValidationPal.GetRemoteCertificate(_securityContext); |
| | 0 | 327 | | _sslAuthenticationOptions.ClientCertificates ??= new X509CertificateCollection(); |
| | 0 | 328 | | clientCertificate = _sslAuthenticationOptions.CertSelectionDelegate(this, _sslAuthenticationOptions. |
| | 0 | 329 | | } |
| | | 330 | | finally |
| | 0 | 331 | | { |
| | 0 | 332 | | remoteCert?.Dispose(); |
| | 0 | 333 | | } |
| | | 334 | | |
| | 0 | 335 | | if (clientCertificate != null) |
| | 0 | 336 | | { |
| | 0 | 337 | | EnsureInitialized(ref filteredCerts).Add(clientCertificate); |
| | 0 | 338 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 339 | | NetEventSource.Log.CertificateFromDelegate(this); |
| | 0 | 340 | | } |
| | | 341 | | else |
| | 0 | 342 | | { |
| | 0 | 343 | | if (_sslAuthenticationOptions.ClientCertificates == null || _sslAuthenticationOptions.ClientCertific |
| | 0 | 344 | | { |
| | 0 | 345 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 346 | | NetEventSource.Log.NoDelegateNoClientCert(this); |
| | 0 | 347 | | } |
| | | 348 | | else |
| | 0 | 349 | | { |
| | 0 | 350 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 351 | | NetEventSource.Log.NoDelegateButClientCert(this); |
| | 0 | 352 | | } |
| | 0 | 353 | | } |
| | 0 | 354 | | } |
| | 0 | 355 | | else if (_credentialsHandle == null && _sslAuthenticationOptions.ClientCertificates != null && _sslAuthentic |
| | 0 | 356 | | { |
| | | 357 | | // This is where we attempt to restart a session by picking the FIRST cert from the collection. |
| | | 358 | | // Otherwise it is either server sending a client cert request or the session is renegotiated. |
| | 0 | 359 | | clientCertificate = _sslAuthenticationOptions.ClientCertificates[0]; |
| | 0 | 360 | | if (clientCertificate != null) |
| | 0 | 361 | | { |
| | 0 | 362 | | EnsureInitialized(ref filteredCerts).Add(clientCertificate); |
| | 0 | 363 | | } |
| | | 364 | | |
| | 0 | 365 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 366 | | NetEventSource.Log.AttemptingRestartUsingCert(clientCertificate, this); |
| | 0 | 367 | | } |
| | 0 | 368 | | else if (_sslAuthenticationOptions.ClientCertificates != null && _sslAuthenticationOptions.ClientCertificate |
| | 0 | 369 | | { |
| | | 370 | | // |
| | | 371 | | // This should be a server request for the client cert sent over currently anonymous sessions. |
| | | 372 | | // |
| | 0 | 373 | | issuers = GetRequestCertificateAuthorities(); |
| | | 374 | | |
| | 0 | 375 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 376 | | { |
| | 0 | 377 | | if (issuers == null || issuers.Length == 0) |
| | 0 | 378 | | { |
| | 0 | 379 | | NetEventSource.Log.NoIssuersTryAllCerts(this); |
| | 0 | 380 | | } |
| | | 381 | | else |
| | 0 | 382 | | { |
| | 0 | 383 | | NetEventSource.Log.LookForMatchingCerts(issuers.Length, this); |
| | 0 | 384 | | } |
| | 0 | 385 | | } |
| | | 386 | | |
| | 0 | 387 | | for (int i = 0; i < _sslAuthenticationOptions.ClientCertificates.Count; ++i) |
| | 0 | 388 | | { |
| | | 389 | | // |
| | | 390 | | // Make sure we add only if the cert matches one of the issuers. |
| | | 391 | | // If no issuers were sent and then try all client certs starting with the first one. |
| | | 392 | | // |
| | 0 | 393 | | if (issuers != null && issuers.Length != 0) |
| | 0 | 394 | | { |
| | 0 | 395 | | X509Certificate2? certificateEx = null; |
| | 0 | 396 | | X509Chain? chain = null; |
| | | 397 | | try |
| | 0 | 398 | | { |
| | 0 | 399 | | certificateEx = MakeEx(_sslAuthenticationOptions.ClientCertificates[i]); |
| | 0 | 400 | | if (certificateEx == null) |
| | 0 | 401 | | { |
| | 0 | 402 | | continue; |
| | | 403 | | } |
| | | 404 | | |
| | 0 | 405 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 406 | | NetEventSource.Info(this, $"Root cert: {certificateEx}"); |
| | | 407 | | |
| | 0 | 408 | | chain = new X509Chain(); |
| | | 409 | | |
| | 0 | 410 | | chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck; |
| | 0 | 411 | | chain.ChainPolicy.VerificationFlags = X509VerificationFlags.IgnoreInvalidName; |
| | 0 | 412 | | chain.Build(certificateEx); |
| | 0 | 413 | | bool found = false; |
| | | 414 | | |
| | | 415 | | // |
| | | 416 | | // We ignore any errors happened with chain. |
| | | 417 | | // |
| | 0 | 418 | | if (chain.ChainElements.Count > 0) |
| | 0 | 419 | | { |
| | 0 | 420 | | int elementsCount = chain.ChainElements.Count; |
| | 0 | 421 | | for (int ii = 0; ii < elementsCount; ++ii) |
| | 0 | 422 | | { |
| | 0 | 423 | | string issuer = chain.ChainElements[ii].Certificate!.Issuer; |
| | 0 | 424 | | found = Array.IndexOf(issuers, issuer) >= 0; |
| | 0 | 425 | | if (found) |
| | 0 | 426 | | { |
| | 0 | 427 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 428 | | NetEventSource.Info(this, $"Matched {issuer}"); |
| | 0 | 429 | | break; |
| | | 430 | | } |
| | 0 | 431 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 432 | | NetEventSource.Info(this, $"No match: {issuer}"); |
| | 0 | 433 | | } |
| | 0 | 434 | | } |
| | | 435 | | |
| | 0 | 436 | | if (!found) |
| | 0 | 437 | | { |
| | 0 | 438 | | continue; |
| | | 439 | | } |
| | 0 | 440 | | } |
| | | 441 | | finally |
| | 0 | 442 | | { |
| | 0 | 443 | | if (chain != null) |
| | 0 | 444 | | { |
| | 0 | 445 | | int elementsCount = chain.ChainElements.Count; |
| | 0 | 446 | | for (int element = 0; element < elementsCount; element++) |
| | 0 | 447 | | { |
| | 0 | 448 | | chain.ChainElements[element].Certificate.Dispose(); |
| | 0 | 449 | | } |
| | | 450 | | |
| | 0 | 451 | | chain.Dispose(); |
| | 0 | 452 | | } |
| | | 453 | | |
| | 0 | 454 | | if (certificateEx != null && (object)certificateEx != (object)_sslAuthenticationOptions.Clie |
| | 0 | 455 | | { |
| | 0 | 456 | | certificateEx.Dispose(); |
| | 0 | 457 | | } |
| | 0 | 458 | | } |
| | 0 | 459 | | } |
| | | 460 | | |
| | 0 | 461 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 462 | | NetEventSource.Log.SelectedCert(_sslAuthenticationOptions.ClientCertificates[i], this); |
| | | 463 | | |
| | 0 | 464 | | EnsureInitialized(ref filteredCerts).Add(_sslAuthenticationOptions.ClientCertificates[i]); |
| | 0 | 465 | | } |
| | 0 | 466 | | } |
| | | 467 | | |
| | 0 | 468 | | clientCertificate = null; |
| | | 469 | | |
| | 0 | 470 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 471 | | { |
| | 0 | 472 | | if (filteredCerts != null && filteredCerts.Count != 0) |
| | 0 | 473 | | { |
| | 0 | 474 | | NetEventSource.Log.CertsAfterFiltering(filteredCerts.Count, this); |
| | 0 | 475 | | NetEventSource.Log.FindingMatchingCerts(this); |
| | 0 | 476 | | } |
| | | 477 | | else |
| | 0 | 478 | | { |
| | 0 | 479 | | NetEventSource.Log.CertsAfterFiltering(0, this); |
| | 0 | 480 | | NetEventSource.Info(this, "No client certificate to choose from"); |
| | 0 | 481 | | } |
| | 0 | 482 | | } |
| | | 483 | | |
| | | 484 | | // |
| | | 485 | | // ATTN: When the client cert was returned by the user callback OR it was guessed AND it has no private key, |
| | | 486 | | // THEN anonymous (no client cert) credential will be used. |
| | | 487 | | // |
| | | 488 | | // SECURITY: Accessing X509 cert Credential is disabled for semitrust. |
| | | 489 | | // We no longer need to demand for unmanaged code permissions. |
| | | 490 | | // FindCertificateWithPrivateKey should do the right demand for us. |
| | 0 | 491 | | if (filteredCerts != null) |
| | 0 | 492 | | { |
| | 0 | 493 | | for (int i = 0; i < filteredCerts.Count; ++i) |
| | 0 | 494 | | { |
| | 0 | 495 | | clientCertificate = filteredCerts[i]; |
| | 0 | 496 | | if ((selectedCert = FindCertificateWithPrivateKey(this, _sslAuthenticationOptions.IsServer, clientCe |
| | 0 | 497 | | { |
| | 0 | 498 | | break; |
| | | 499 | | } |
| | | 500 | | |
| | 0 | 501 | | clientCertificate = null; |
| | 0 | 502 | | selectedCert = null; |
| | 0 | 503 | | } |
| | 0 | 504 | | } |
| | | 505 | | |
| | 0 | 506 | | Debug.Assert((object?)clientCertificate == (object?)selectedCert || clientCertificate!.Equals(selectedCert), |
| | | 507 | | |
| | 0 | 508 | | if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(this, $"Selected cert = {selectedCert}"); |
| | | 509 | | |
| | 0 | 510 | | _selectedClientCertificate = clientCertificate; |
| | | 511 | | |
| | 0 | 512 | | return selectedCert; |
| | 0 | 513 | | } |
| | | 514 | | |
| | | 515 | | /*++ |
| | | 516 | | AcquireCredentials - Attempts to find Client Credential |
| | | 517 | | Information, that can be sent to the server. In our case, |
| | | 518 | | this is only Client Certificates, that we have Credential Info. |
| | | 519 | | |
| | | 520 | | How it works: |
| | | 521 | | case 0: Cert Selection delegate is present |
| | | 522 | | Always use its result as the client cert answer. |
| | | 523 | | Try to use cached credential handle whenever feasible. |
| | | 524 | | Do not use cached anonymous creds if the delegate has returned null |
| | | 525 | | and the collection is not empty (allow responding with the cert later). |
| | | 526 | | |
| | | 527 | | case 1: Certs collection is empty |
| | | 528 | | Always use the same statically acquired anonymous SSL Credential |
| | | 529 | | |
| | | 530 | | case 2: Before our Connection with the Server |
| | | 531 | | If we have a cached credential handle keyed by first X509Certificate |
| | | 532 | | **content** in the passed collection, then we use that cached |
| | | 533 | | credential and hoping to restart a session. |
| | | 534 | | |
| | | 535 | | Otherwise create a new anonymous (allow responding with the cert later). |
| | | 536 | | |
| | | 537 | | case 3: After our Connection with the Server (i.e. during handshake or re-handshake) |
| | | 538 | | The server has requested that we send it a Certificate then |
| | | 539 | | we Enumerate a list of server sent Issuers trying to match against |
| | | 540 | | our list of Certificates, the first match is sent to the server. |
| | | 541 | | |
| | | 542 | | Once we got a cert we again try to match cached credential handle if possible. |
| | | 543 | | This will not restart a session but helps minimizing the number of handles we create. |
| | | 544 | | |
| | | 545 | | In the case of an error getting a Certificate or checking its private Key we fall back |
| | | 546 | | to the behavior of having no certs, case 1. |
| | | 547 | | |
| | | 548 | | Returns: True if cached creds were used, false otherwise. |
| | | 549 | | |
| | | 550 | | --*/ |
| | | 551 | | |
| | | 552 | | internal bool AcquireClientCredentials(ref byte[]? thumbPrint, bool newCredentialsRequested = false) |
| | | 553 | | { |
| | 0 | 554 | | ReleaseCachedCredentials(); |
| | | 555 | | |
| | | 556 | | // Acquire possible Client Certificate information and set it on the handle. |
| | 0 | 557 | | bool cachedCred = false; // this is a return result from this method. |
| | | 558 | | |
| | 0 | 559 | | X509Certificate2? selectedCert = SelectClientCertificate(); |
| | | 560 | | |
| | 0 | 561 | | if (newCredentialsRequested) |
| | 0 | 562 | | { |
| | 0 | 563 | | UpdateCertificateContext(selectedCert); |
| | | 564 | | |
| | 0 | 565 | | if (SslStreamPal.TryUpdateClintCertificate(_credentialsHandle, _securityContext, _sslAuthenticationOptio |
| | | 566 | | { |
| | | 567 | | // If the certificate was updated we do not need to deal with the credential handle. |
| | | 568 | | return false; |
| | | 569 | | } |
| | 0 | 570 | | } |
| | | 571 | | |
| | 0 | 572 | | SslStreamCertificateContext? certificateContextToRestore = null; |
| | | 573 | | try |
| | 0 | 574 | | { |
| | | 575 | | // Try to locate cached creds first. |
| | | 576 | | // |
| | | 577 | | // SECURITY: selectedCert ref if not null is a safe object that does not depend on possible **user** inh |
| | | 578 | | // |
| | 0 | 579 | | byte[]? guessedThumbPrint = selectedCert?.GetCertHash(HashAlgorithmName.SHA512); |
| | 0 | 580 | | SafeFreeCredentials? cachedCredentialHandle = SslSessionsCache.TryCachedCredential( |
| | 0 | 581 | | guessedThumbPrint, |
| | 0 | 582 | | _sslAuthenticationOptions.EnabledSslProtocols, |
| | 0 | 583 | | _sslAuthenticationOptions.IsServer, |
| | 0 | 584 | | _sslAuthenticationOptions.EncryptionPolicy, |
| | 0 | 585 | | _sslAuthenticationOptions.CertificateRevocationCheckMode != X509RevocationMode.NoCheck, |
| | 0 | 586 | | _sslAuthenticationOptions.AllowTlsResume, |
| | 0 | 587 | | sendTrustList: false, |
| | 0 | 588 | | _sslAuthenticationOptions.AllowRsaPssPadding, |
| | 0 | 589 | | _sslAuthenticationOptions.AllowRsaPkcs1Padding); |
| | 0 | 590 | | Volatile.Write(ref _cachedCredentialsHandle, cachedCredentialHandle); |
| | | 591 | | |
| | | 592 | | // We can probably do some optimization here. If the selectedCert is returned by the delegate |
| | | 593 | | // we can always go ahead and use the certificate to create our credential |
| | | 594 | | // (instead of going anonymous as we do here). |
| | 0 | 595 | | if (!newCredentialsRequested && |
| | 0 | 596 | | cachedCredentialHandle == null && |
| | 0 | 597 | | selectedCert != null && |
| | 0 | 598 | | SslStreamPal.StartMutualAuthAsAnonymous) |
| | 0 | 599 | | { |
| | 0 | 600 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 601 | | NetEventSource.Info(this, "Reset to anonymous session."); |
| | | 602 | | |
| | | 603 | | // IIS does not renegotiate a restarted session if client cert is needed. |
| | | 604 | | // So we don't want to reuse **anonymous** cached credential for a new SSL connection if the client |
| | | 605 | | // The following block happens if client did specify a certificate but no cached creds were found in |
| | | 606 | | // Since we don't restart a session the server side can still challenge for a client cert. |
| | 0 | 607 | | if ((object?)_selectedClientCertificate != (object?)selectedCert) |
| | 0 | 608 | | { |
| | 0 | 609 | | selectedCert.Dispose(); |
| | 0 | 610 | | } |
| | | 611 | | |
| | 0 | 612 | | guessedThumbPrint = null; |
| | 0 | 613 | | selectedCert = null; |
| | 0 | 614 | | _selectedClientCertificate = null; |
| | 0 | 615 | | certificateContextToRestore = _sslAuthenticationOptions.CertificateContext; |
| | 0 | 616 | | _sslAuthenticationOptions.CertificateContext = null; |
| | 0 | 617 | | } |
| | | 618 | | |
| | 0 | 619 | | if (cachedCredentialHandle != null) |
| | 0 | 620 | | { |
| | 0 | 621 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 622 | | NetEventSource.Log.UsingCachedCredential(this); |
| | 0 | 623 | | _credentialsHandle = cachedCredentialHandle; |
| | 0 | 624 | | cachedCred = true; |
| | 0 | 625 | | UpdateCertificateContext(selectedCert); |
| | 0 | 626 | | } |
| | | 627 | | else |
| | 0 | 628 | | { |
| | 0 | 629 | | UpdateCertificateContext(selectedCert); |
| | | 630 | | |
| | 0 | 631 | | _credentialsHandle = AcquireCredentialsHandle(_sslAuthenticationOptions, newCredentialsRequested); |
| | 0 | 632 | | thumbPrint = guessedThumbPrint; // Delay until here in case something above threw. |
| | 0 | 633 | | } |
| | 0 | 634 | | } |
| | | 635 | | finally |
| | 0 | 636 | | { |
| | 0 | 637 | | UpdateCertificateContext(selectedCert); |
| | 0 | 638 | | if (certificateContextToRestore is not null) |
| | 0 | 639 | | { |
| | 0 | 640 | | Debug.Assert(_sslAuthenticationOptions.CertificateContext is null); |
| | 0 | 641 | | _sslAuthenticationOptions.CertificateContext = certificateContextToRestore; |
| | 0 | 642 | | } |
| | 0 | 643 | | } |
| | | 644 | | |
| | 0 | 645 | | return cachedCred; |
| | | 646 | | |
| | | 647 | | void UpdateCertificateContext(X509Certificate2? cert) |
| | 0 | 648 | | { |
| | 0 | 649 | | if (cert != null && _sslAuthenticationOptions.CertificateContext == null) |
| | 0 | 650 | | { |
| | 0 | 651 | | _sslAuthenticationOptions.SetCertificateContextFromCert(cert); |
| | 0 | 652 | | } |
| | 0 | 653 | | } |
| | 0 | 654 | | } |
| | | 655 | | |
| | 0 | 656 | | private static List<T> EnsureInitialized<T>(ref List<T>? list) => list ??= new List<T>(); |
| | | 657 | | |
| | | 658 | | // |
| | | 659 | | // Acquire Server Side Certificate information and set it on the class. |
| | | 660 | | // |
| | | 661 | | private bool AcquireServerCredentials(ref byte[]? thumbPrint) |
| | 0 | 662 | | { |
| | 0 | 663 | | ReleaseCachedCredentials(); |
| | | 664 | | |
| | 0 | 665 | | X509Certificate? localCertificate = null; |
| | 0 | 666 | | X509Certificate2? selectedCert = null; |
| | 0 | 667 | | bool cachedCred = false; |
| | | 668 | | |
| | | 669 | | // There are three options for selecting the server certificate. When |
| | | 670 | | // selecting which to use, we prioritize the new ServerCertSelectionDelegate |
| | | 671 | | // API. If the new API isn't used we call LocalCertSelectionCallback (for compat |
| | | 672 | | // with .NET Framework), and if neither is set we fall back to using CertificateContext. |
| | 0 | 673 | | if (_sslAuthenticationOptions.ServerCertSelectionDelegate != null) |
| | 0 | 674 | | { |
| | 0 | 675 | | localCertificate = _sslAuthenticationOptions.ServerCertSelectionDelegate(this, _sslAuthenticationOptions |
| | 0 | 676 | | if (localCertificate == null) |
| | 0 | 677 | | { |
| | 0 | 678 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 679 | | NetEventSource.Error(this, $"ServerCertSelectionDelegate returned no certificate for '{_sslAuthe |
| | 0 | 680 | | throw new AuthenticationException(SR.net_ssl_io_no_server_cert); |
| | | 681 | | } |
| | | 682 | | |
| | 0 | 683 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 684 | | NetEventSource.Info(this, "ServerCertSelectionDelegate selected Cert"); |
| | 0 | 685 | | } |
| | 0 | 686 | | else if (_sslAuthenticationOptions.CertSelectionDelegate != null) |
| | 0 | 687 | | { |
| | 0 | 688 | | X509CertificateCollection tempCollection = new X509CertificateCollection(); |
| | 0 | 689 | | tempCollection.Add(_sslAuthenticationOptions.CertificateContext!.TargetCertificate!); |
| | | 690 | | // We pass string.Empty here to maintain strict compatibility with .NET Framework. |
| | 0 | 691 | | localCertificate = _sslAuthenticationOptions.CertSelectionDelegate(this, string.Empty, tempCollection, n |
| | 0 | 692 | | if (localCertificate == null) |
| | 0 | 693 | | { |
| | 0 | 694 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 695 | | NetEventSource.Error(this, $"CertSelectionDelegate returned no certificaete for '{_sslAuthentica |
| | 0 | 696 | | throw new NotSupportedException(SR.net_ssl_io_no_server_cert); |
| | | 697 | | } |
| | | 698 | | |
| | 0 | 699 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 700 | | NetEventSource.Info(this, "CertSelectionDelegate selected Cert"); |
| | 0 | 701 | | } |
| | 0 | 702 | | else if (_sslAuthenticationOptions.CertificateContext != null) |
| | 0 | 703 | | { |
| | 0 | 704 | | selectedCert = _sslAuthenticationOptions.CertificateContext.TargetCertificate; |
| | 0 | 705 | | } |
| | | 706 | | |
| | 0 | 707 | | if (selectedCert == null) |
| | 0 | 708 | | { |
| | | 709 | | // We will get here if certificate was selected via legacy callback using X509Certificate |
| | | 710 | | // Fail immediately if no certificate was given. |
| | 0 | 711 | | if (localCertificate == null) |
| | 0 | 712 | | { |
| | 0 | 713 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 714 | | NetEventSource.Error(this, "Certiticate callback returned no certificaete."); |
| | 0 | 715 | | throw new NotSupportedException(SR.net_ssl_io_no_server_cert); |
| | | 716 | | } |
| | | 717 | | |
| | | 718 | | // SECURITY: Accessing X509 cert Credential is disabled for semitrust. |
| | | 719 | | // We no longer need to demand for unmanaged code permissions. |
| | | 720 | | // EnsurePrivateKey should do the right demand for us. |
| | 0 | 721 | | selectedCert = FindCertificateWithPrivateKey(this, _sslAuthenticationOptions.IsServer, localCertificate) |
| | | 722 | | |
| | 0 | 723 | | if (selectedCert == null) |
| | 0 | 724 | | { |
| | 0 | 725 | | throw new NotSupportedException(SR.net_ssl_io_no_server_cert); |
| | | 726 | | } |
| | | 727 | | |
| | 0 | 728 | | Debug.Assert(localCertificate.Equals(selectedCert), "'selectedCert' does not match 'localCertificate'.") |
| | 0 | 729 | | _sslAuthenticationOptions.SetCertificateContextFromCert(selectedCert); |
| | 0 | 730 | | } |
| | | 731 | | |
| | 0 | 732 | | Debug.Assert(_sslAuthenticationOptions.CertificateContext != null); |
| | | 733 | | // |
| | | 734 | | // Note selectedCert is a safe ref possibly cloned from the user passed Cert object |
| | | 735 | | // |
| | 0 | 736 | | byte[] guessedThumbPrint = selectedCert.GetCertHash(HashAlgorithmName.SHA512); bool sendTrustedList = _sslAu |
| | 0 | 737 | | SafeFreeCredentials? cachedCredentialHandle = SslSessionsCache.TryCachedCredential(guessedThumbPrint, |
| | 0 | 738 | | _sslAuthenticationOptions.EnabledSslProtocols, |
| | 0 | 739 | | _sslAuthenticationOptions.IsServer, |
| | 0 | 740 | | _sslAuthenticationOptions.EncryptionPolicy, |
| | 0 | 741 | | _sslAuthenticationOptions.CertificateRevocationCheckMode |
| | 0 | 742 | | _sslAuthenticationOptions.AllowTlsResume, |
| | 0 | 743 | | sendTrustedList, |
| | 0 | 744 | | _sslAuthenticationOptions.AllowRsaPssPadding, |
| | 0 | 745 | | _sslAuthenticationOptions.AllowRsaPkcs1Padding); |
| | 0 | 746 | | Volatile.Write(ref _cachedCredentialsHandle, cachedCredentialHandle); |
| | 0 | 747 | | if (cachedCredentialHandle != null) |
| | 0 | 748 | | { |
| | 0 | 749 | | _credentialsHandle = cachedCredentialHandle; |
| | 0 | 750 | | cachedCred = true; |
| | 0 | 751 | | } |
| | | 752 | | else |
| | 0 | 753 | | { |
| | 0 | 754 | | _credentialsHandle = AcquireCredentialsHandle(_sslAuthenticationOptions); |
| | 0 | 755 | | thumbPrint = guessedThumbPrint; |
| | 0 | 756 | | } |
| | | 757 | | |
| | 0 | 758 | | return cachedCred; |
| | 0 | 759 | | } |
| | | 760 | | |
| | | 761 | | private static SafeFreeCredentials? AcquireCredentialsHandle(SslAuthenticationOptions sslAuthenticationOptions, |
| | 0 | 762 | | { |
| | 0 | 763 | | SafeFreeCredentials? cred = SslStreamPal.AcquireCredentialsHandle(sslAuthenticationOptions, newCredentialsRe |
| | | 764 | | |
| | 0 | 765 | | if (sslAuthenticationOptions.CertificateContext != null && cred != null) |
| | 0 | 766 | | { |
| | | 767 | | // |
| | | 768 | | // Since the SafeFreeCredentials can be cached and reused, it may happen on long running processes that |
| | | 769 | | // the chain expires and all subsequent connections would send expired intermediate certificates. Find t |
| | | 770 | | // NotAfter timestamp on the chain and use it as expiration timestamp for the credentials. |
| | | 771 | | // This provides an opportunity to recreate the credentials with an alternative (and still valid) |
| | | 772 | | // certificate chain. |
| | | 773 | | // |
| | 0 | 774 | | SslStreamCertificateContext certificateContext = sslAuthenticationOptions.CertificateContext; |
| | 0 | 775 | | cred._expiry = GetExpiryTimestamp(certificateContext); |
| | | 776 | | |
| | 0 | 777 | | if (cred._expiry < DateTime.UtcNow) |
| | 0 | 778 | | { |
| | | 779 | | // |
| | | 780 | | // The CertificateContext from auth options is recreated just before creating the SafeFreeCredential |
| | | 781 | | // it was provided by the user code, it may still contain the (now expired) certificate chain. Such |
| | | 782 | | // effectively disable caching as it would lead to creating new credentials for each connection. We |
| | | 783 | | // a temporary certificate context (which builds a new chain with hopefully more recent chain). |
| | | 784 | | // |
| | 0 | 785 | | certificateContext = certificateContext.Duplicate(); |
| | 0 | 786 | | cred._expiry = GetExpiryTimestamp(certificateContext); |
| | 0 | 787 | | } |
| | | 788 | | |
| | | 789 | | static DateTime GetExpiryTimestamp(SslStreamCertificateContext certificateContext) |
| | 0 | 790 | | { |
| | 0 | 791 | | DateTime expiry = certificateContext.TargetCertificate.NotAfter; |
| | | 792 | | |
| | 0 | 793 | | foreach (X509Certificate2 cert in certificateContext.IntermediateCertificates) |
| | 0 | 794 | | { |
| | 0 | 795 | | if (cert.NotAfter < expiry) |
| | 0 | 796 | | { |
| | 0 | 797 | | expiry = cert.NotAfter; |
| | 0 | 798 | | } |
| | 0 | 799 | | } |
| | | 800 | | |
| | 0 | 801 | | return expiry.ToUniversalTime(); |
| | 0 | 802 | | } |
| | 0 | 803 | | } |
| | | 804 | | |
| | 0 | 805 | | return cred; |
| | 0 | 806 | | } |
| | | 807 | | |
| | | 808 | | // |
| | | 809 | | internal ProtocolToken NextMessage(ReadOnlySpan<byte> incomingBuffer, out int consumed) |
| | 0 | 810 | | { |
| | 0 | 811 | | if (!LocalAppContextSwitches.UseLegacySslStreamHandshake && |
| | 0 | 812 | | TryNextMessageViaTlsSession(incomingBuffer, out ProtocolToken wedged, out consumed)) |
| | 0 | 813 | | { |
| | 0 | 814 | | if (NetEventSource.Log.IsEnabled() && wedged.Failed) |
| | 0 | 815 | | { |
| | 0 | 816 | | NetEventSource.Error(this, $"Authentication failed. Status: {wedged.Status}, Exception message: {wed |
| | 0 | 817 | | } |
| | 0 | 818 | | return wedged; |
| | | 819 | | } |
| | | 820 | | |
| | 0 | 821 | | ProtocolToken token = GenerateToken(incomingBuffer, out consumed); |
| | 0 | 822 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 823 | | { |
| | 0 | 824 | | if (token.Failed) |
| | 0 | 825 | | { |
| | 0 | 826 | | NetEventSource.Error(this, $"Authentication failed. Status: {token.Status}, Exception message: {toke |
| | 0 | 827 | | } |
| | 0 | 828 | | } |
| | | 829 | | |
| | 0 | 830 | | return token; |
| | 0 | 831 | | } |
| | | 832 | | |
| | | 833 | | private partial bool TryNextMessageViaTlsSession(ReadOnlySpan<byte> incomingBuffer, out ProtocolToken token, out |
| | | 834 | | |
| | | 835 | | /*++ |
| | | 836 | | GenerateToken - Called after each successive state |
| | | 837 | | in the Client - Server handshake. This function |
| | | 838 | | generates a set of bytes that will be sent next to |
| | | 839 | | the server. The server responds, each response, |
| | | 840 | | is pass then into this function, again, and the cycle |
| | | 841 | | repeats until successful connection, or failure. |
| | | 842 | | |
| | | 843 | | Input: |
| | | 844 | | input - bytes from the wire |
| | | 845 | | Return: |
| | | 846 | | token - ProtocolToken with status and optionally buffer. |
| | | 847 | | --*/ |
| | | 848 | | private ProtocolToken GenerateToken(ReadOnlySpan<byte> inputBuffer, out int consumed) |
| | 0 | 849 | | { |
| | 0 | 850 | | bool cachedCreds = false; |
| | 0 | 851 | | bool sendTrustList = false; |
| | 0 | 852 | | byte[]? thumbPrint = null; |
| | | 853 | | |
| | 0 | 854 | | ProtocolToken token = default; |
| | 0 | 855 | | token.RentBuffer = true; |
| | | 856 | | |
| | | 857 | | // We need to try get credentials at the beginning. |
| | | 858 | | // _credentialsHandle may be always null on some platforms but |
| | | 859 | | // _securityContext will be allocated on first call. |
| | 0 | 860 | | bool refreshCredentialNeeded = _securityContext == null; |
| | | 861 | | try |
| | 0 | 862 | | { |
| | | 863 | | do |
| | 0 | 864 | | { |
| | 0 | 865 | | thumbPrint = null; |
| | 0 | 866 | | if (refreshCredentialNeeded) |
| | 0 | 867 | | { |
| | 0 | 868 | | cachedCreds = _sslAuthenticationOptions.IsServer |
| | 0 | 869 | | ? AcquireServerCredentials(ref thumbPrint) |
| | 0 | 870 | | : AcquireClientCredentials(ref thumbPrint); |
| | 0 | 871 | | } |
| | | 872 | | |
| | 0 | 873 | | if (_sslAuthenticationOptions.IsServer) |
| | 0 | 874 | | { |
| | 0 | 875 | | sendTrustList = _sslAuthenticationOptions.CertificateContext?.Trust?._sendTrustInHandshake ?? fa |
| | | 876 | | |
| | 0 | 877 | | token = SslStreamPal.AcceptSecurityContext( |
| | 0 | 878 | | ref _credentialsHandle!, |
| | 0 | 879 | | ref _securityContext, |
| | 0 | 880 | | inputBuffer, |
| | 0 | 881 | | out consumed, |
| | 0 | 882 | | _sslAuthenticationOptions); |
| | 0 | 883 | | if (token.Status.ErrorCode == SecurityStatusPalErrorCode.HandshakeStarted) |
| | 0 | 884 | | { |
| | 0 | 885 | | token.Status = SslStreamPal.SelectApplicationProtocol( |
| | 0 | 886 | | _credentialsHandle!, |
| | 0 | 887 | | _securityContext!, |
| | 0 | 888 | | _sslAuthenticationOptions, |
| | 0 | 889 | | _lastFrame.RawApplicationProtocols); |
| | | 890 | | |
| | 0 | 891 | | if (token.Status.ErrorCode == SecurityStatusPalErrorCode.OK) |
| | 0 | 892 | | { |
| | 0 | 893 | | token = SslStreamPal.AcceptSecurityContext( |
| | 0 | 894 | | ref _credentialsHandle!, |
| | 0 | 895 | | ref _securityContext, |
| | 0 | 896 | | ReadOnlySpan<byte>.Empty, |
| | 0 | 897 | | out _, |
| | 0 | 898 | | _sslAuthenticationOptions); |
| | 0 | 899 | | } |
| | 0 | 900 | | } |
| | 0 | 901 | | } |
| | | 902 | | else |
| | 0 | 903 | | { |
| | 0 | 904 | | string hostName = TargetHostNameHelper.NormalizeHostName(_sslAuthenticationOptions.TargetHost); |
| | 0 | 905 | | token = SslStreamPal.InitializeSecurityContext( |
| | 0 | 906 | | ref _credentialsHandle!, |
| | 0 | 907 | | ref _securityContext, |
| | 0 | 908 | | hostName, |
| | 0 | 909 | | inputBuffer, |
| | 0 | 910 | | out consumed, |
| | 0 | 911 | | _sslAuthenticationOptions); |
| | | 912 | | |
| | 0 | 913 | | if (token.Status.ErrorCode == SecurityStatusPalErrorCode.CredentialsNeeded) |
| | 0 | 914 | | { |
| | 0 | 915 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 916 | | NetEventSource.Info(this, "InitializeSecurityContext() returned 'CredentialsNeeded'."); |
| | | 917 | | |
| | 0 | 918 | | refreshCredentialNeeded = true; |
| | 0 | 919 | | cachedCreds = AcquireClientCredentials(ref thumbPrint, newCredentialsRequested: true); |
| | | 920 | | |
| | 0 | 921 | | token = SslStreamPal.InitializeSecurityContext( |
| | 0 | 922 | | ref _credentialsHandle!, |
| | 0 | 923 | | ref _securityContext, |
| | 0 | 924 | | hostName, |
| | 0 | 925 | | ReadOnlySpan<byte>.Empty, |
| | 0 | 926 | | out _, |
| | 0 | 927 | | _sslAuthenticationOptions); |
| | 0 | 928 | | } |
| | 0 | 929 | | } |
| | | 930 | | |
| | | 931 | | #if TARGET_APPLE |
| | | 932 | | if (token.Status.ErrorCode == SecurityStatusPalErrorCode.CertValidationNeeded) |
| | | 933 | | { |
| | | 934 | | token = VerifyRemoteCertificateAndGenerateNextToken(token); |
| | | 935 | | } |
| | | 936 | | #endif |
| | 0 | 937 | | } while (cachedCreds && _credentialsHandle == null); |
| | 0 | 938 | | } |
| | | 939 | | finally |
| | 0 | 940 | | { |
| | 0 | 941 | | ReleaseCachedCredentials(); |
| | 0 | 942 | | if (refreshCredentialNeeded) |
| | 0 | 943 | | { |
| | | 944 | | // |
| | | 945 | | // Assuming the ISC or ASC has referenced the credential, |
| | | 946 | | // we want to call dispose so to decrement the effective ref count. |
| | | 947 | | // |
| | 0 | 948 | | _credentialsHandle?.Dispose(); |
| | | 949 | | |
| | | 950 | | // |
| | | 951 | | // This call may bump up the credential reference count further. |
| | | 952 | | // Note that thumbPrint is retrieved from a safe cert object that was possible cloned from the user |
| | | 953 | | // |
| | 0 | 954 | | if (!cachedCreds && _securityContext != null && !_securityContext.IsInvalid && _credentialsHandle != |
| | 0 | 955 | | { |
| | 0 | 956 | | SslSessionsCache.CacheCredential( |
| | 0 | 957 | | _credentialsHandle, |
| | 0 | 958 | | thumbPrint, |
| | 0 | 959 | | _sslAuthenticationOptions.EnabledSslProtocols, |
| | 0 | 960 | | _sslAuthenticationOptions.IsServer, |
| | 0 | 961 | | _sslAuthenticationOptions.EncryptionPolicy, |
| | 0 | 962 | | _sslAuthenticationOptions.CertificateRevocationCheckMode != X509RevocationMode.NoCheck, |
| | 0 | 963 | | _sslAuthenticationOptions.AllowTlsResume, |
| | 0 | 964 | | sendTrustList, |
| | 0 | 965 | | _sslAuthenticationOptions.AllowRsaPssPadding, |
| | 0 | 966 | | _sslAuthenticationOptions.AllowRsaPkcs1Padding); |
| | 0 | 967 | | } |
| | 0 | 968 | | } |
| | 0 | 969 | | } |
| | | 970 | | |
| | 0 | 971 | | return token; |
| | 0 | 972 | | } |
| | | 973 | | |
| | | 974 | | #if TARGET_APPLE |
| | | 975 | | private ProtocolToken VerifyRemoteCertificateAndGenerateNextToken(ProtocolToken token) |
| | | 976 | | { |
| | | 977 | | // SecureTransport pauses the handshake (errSSL{Server,Client}AuthCompleted) before |
| | | 978 | | // any bytes are produced for the next handshake flight, so the pending-writes buffer |
| | | 979 | | // drained into token should be empty here. Assert to catch any future regression |
| | | 980 | | // that would silently drop handshake bytes. |
| | | 981 | | Debug.Assert(token.Size == 0, "Expected empty payload at CertValidationNeeded pause; dropping non-empty payl |
| | | 982 | | token.ReleasePayload(); |
| | | 983 | | |
| | | 984 | | ProtocolToken alertToken = default; |
| | | 985 | | SslPolicyErrors sslPolicyErrors = SslPolicyErrors.None; |
| | | 986 | | |
| | | 987 | | if (!VerifyRemoteCertificate(_sslAuthenticationOptions.CertificateContext?.Trust, ref alertToken, ref sslPol |
| | | 988 | | { |
| | | 989 | | alertToken.Status = new SecurityStatusPal(SecurityStatusPalErrorCode.CertValidationFailed, CreateCertifi |
| | | 990 | | return alertToken; |
| | | 991 | | } |
| | | 992 | | |
| | | 993 | | return GenerateToken(ReadOnlySpan<byte>.Empty, out _); |
| | | 994 | | } |
| | | 995 | | #endif |
| | | 996 | | |
| | | 997 | | internal ProtocolToken Renegotiate() |
| | 0 | 998 | | { |
| | 0 | 999 | | Debug.Assert(_securityContext != null); |
| | | 1000 | | |
| | 0 | 1001 | | return SslStreamPal.Renegotiate( |
| | 0 | 1002 | | ref _credentialsHandle!, |
| | 0 | 1003 | | ref _securityContext, |
| | 0 | 1004 | | _sslAuthenticationOptions); |
| | 0 | 1005 | | } |
| | | 1006 | | |
| | | 1007 | | /*++ |
| | | 1008 | | ProcessHandshakeSuccess - |
| | | 1009 | | Called on successful completion of Handshake - |
| | | 1010 | | used to set header/trailer sizes for encryption use |
| | | 1011 | | |
| | | 1012 | | Fills in the information about established protocol |
| | | 1013 | | --*/ |
| | | 1014 | | internal void ProcessHandshakeSuccess() |
| | 0 | 1015 | | { |
| | 0 | 1016 | | SslStreamPal.QueryContextStreamSizes(_securityContext!, out StreamSizes streamSizes); |
| | | 1017 | | |
| | 0 | 1018 | | _headerSize = streamSizes.Header; |
| | 0 | 1019 | | _trailerSize = streamSizes.Trailer; |
| | 0 | 1020 | | _maxDataSize = streamSizes.MaximumMessage; |
| | 0 | 1021 | | Debug.Assert(_maxDataSize > 0); |
| | | 1022 | | |
| | 0 | 1023 | | SslStreamPal.QueryContextConnectionInfo(_securityContext!, ref _connectionInfo); |
| | | 1024 | | #if DEBUG |
| | 0 | 1025 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 1026 | | { |
| | | 1027 | | // This keeps the property alive only for tests via reflection |
| | | 1028 | | // Otherwise it could be optimized out as it is not used by production code. |
| | 0 | 1029 | | NetEventSource.Info(this, $"TLS resumed {_connectionInfo.TlsResumed}"); |
| | 0 | 1030 | | } |
| | | 1031 | | #endif |
| | 0 | 1032 | | } |
| | | 1033 | | |
| | | 1034 | | private ProtocolToken EncryptData(ReadOnlyMemory<byte> buffer) |
| | 0 | 1035 | | { |
| | 0 | 1036 | | ThrowIfExceptionalOrNotAuthenticated(); |
| | | 1037 | | |
| | 0 | 1038 | | lock (_handshakeLock) |
| | 0 | 1039 | | { |
| | 0 | 1040 | | if (_handshakeWaiter != null) |
| | 0 | 1041 | | { |
| | 0 | 1042 | | ProtocolToken waitToken = default; |
| | | 1043 | | // avoid waiting under lock. |
| | 0 | 1044 | | waitToken.Status = new SecurityStatusPal(SecurityStatusPalErrorCode.TryAgain); |
| | 0 | 1045 | | return waitToken; |
| | | 1046 | | } |
| | | 1047 | | |
| | 0 | 1048 | | if (NetEventSource.Log.IsEnabled()) NetEventSource.DumpBuffer(this, buffer.Span); |
| | | 1049 | | |
| | 0 | 1050 | | ProtocolToken token = SslStreamPal.EncryptMessage( |
| | 0 | 1051 | | _securityContext!, |
| | 0 | 1052 | | buffer, |
| | 0 | 1053 | | _headerSize, |
| | 0 | 1054 | | _trailerSize); |
| | | 1055 | | |
| | 0 | 1056 | | if (token.Status.ErrorCode != SecurityStatusPalErrorCode.OK) |
| | 0 | 1057 | | { |
| | 0 | 1058 | | if (NetEventSource.Log.IsEnabled()) NetEventSource.Error(this, $"ERROR {token.Status}"); |
| | 0 | 1059 | | } |
| | | 1060 | | |
| | 0 | 1061 | | return token; |
| | | 1062 | | } |
| | 0 | 1063 | | } |
| | | 1064 | | |
| | | 1065 | | // On some platforms, the platform APIs decrypt in-place via single |
| | | 1066 | | // call (Schannel), while others have separate write-ciphertext + |
| | | 1067 | | // read-plaintext primitives. To allow the most efficient thing (copying |
| | | 1068 | | // plaintext straight to the `destination` buffer provided by the |
| | | 1069 | | // SslStream caller) on platforms that support it, the contract of this |
| | | 1070 | | // method is as follows: |
| | | 1071 | | // - After the call, first `bytesWritten` bytes of `destination` contain decrypted plaintext |
| | | 1072 | | // - Rest of the decrypted plaintext, if any, is stored in `_buffer.DecryptedSpan`. |
| | | 1073 | | private SecurityStatusPal DecryptData(int frameSize, Span<byte> destination, out int bytesWritten) |
| | 0 | 1074 | | { |
| | | 1075 | | SecurityStatusPal status; |
| | | 1076 | | |
| | 0 | 1077 | | lock (_handshakeLock) |
| | 0 | 1078 | | { |
| | 0 | 1079 | | ThrowIfExceptionalOrNotAuthenticated(); |
| | | 1080 | | |
| | 0 | 1081 | | status = SslStreamPal.DecryptMessage( |
| | 0 | 1082 | | _securityContext!, |
| | 0 | 1083 | | _buffer.EncryptedSpanSliced(frameSize), |
| | 0 | 1084 | | destination, |
| | 0 | 1085 | | out bytesWritten, |
| | 0 | 1086 | | out int leftoverOffset, |
| | 0 | 1087 | | out int leftoverLength); |
| | | 1088 | | |
| | 0 | 1089 | | _buffer.OnDecrypted(leftoverOffset, leftoverLength, frameSize); |
| | | 1090 | | |
| | 0 | 1091 | | if (NetEventSource.Log.IsEnabled() && status.ErrorCode == SecurityStatusPalErrorCode.OK) |
| | 0 | 1092 | | { |
| | 0 | 1093 | | if (bytesWritten > 0) |
| | 0 | 1094 | | { |
| | 0 | 1095 | | NetEventSource.DumpBuffer(this, destination.Slice(0, bytesWritten)); |
| | 0 | 1096 | | } |
| | | 1097 | | |
| | 0 | 1098 | | if (_buffer.DecryptedSpan.Length > 0) |
| | 0 | 1099 | | { |
| | 0 | 1100 | | NetEventSource.DumpBuffer(this, _buffer.DecryptedSpan); |
| | 0 | 1101 | | } |
| | 0 | 1102 | | } |
| | | 1103 | | |
| | 0 | 1104 | | if (status.ErrorCode == SecurityStatusPalErrorCode.Renegotiate) |
| | 0 | 1105 | | { |
| | | 1106 | | // The status indicates that the peer or TLS implementation requires additional |
| | | 1107 | | // handshake/session processing. In practice, there can be other reasons too, |
| | | 1108 | | // like TLS1.3 session creation or alert handling. We need to pass the data to |
| | | 1109 | | // the underlying security provider and it is not safe to do parallel write any |
| | | 1110 | | // more as that can change TLS state and the EncryptData() can fail in strange ways. |
| | | 1111 | | |
| | | 1112 | | // To handle this we call DecryptData() under lock and we create TCS waiter. |
| | | 1113 | | // EncryptData() checks that under same lock and if it exist it will not call low-level crypto. |
| | | 1114 | | // Instead it will wait synchronously or asynchronously and it will try again after the wait. |
| | | 1115 | | // The result will be set when ReplyOnReAuthenticationAsync() is finished e.g. lsass business is ove |
| | | 1116 | | // If that happen before EncryptData() runs, _handshakeWaiter will be set to null |
| | | 1117 | | // and EncryptData() will work normally e.g. no waiting, just exclusion with DecryptData() |
| | | 1118 | | |
| | 0 | 1119 | | if (_sslAuthenticationOptions.AllowRenegotiation || SslProtocol == SslProtocols.Tls13 || _nestedAuth |
| | 0 | 1120 | | { |
| | | 1121 | | // create TCS only if we plan to proceed. If not, we will throw later outside of the lock. |
| | | 1122 | | // Tls1.3 does not have renegotiation. However on Windows this error code is used |
| | | 1123 | | // for session management e.g. anything lsass needs to see. |
| | | 1124 | | // We also allow it when explicitly requested using RenegotiateAsync(). |
| | 0 | 1125 | | _handshakeWaiter = new TaskCompletionSource<bool>(TaskCreationOptions.RunContinuationsAsynchrono |
| | 0 | 1126 | | } |
| | 0 | 1127 | | } |
| | 0 | 1128 | | } |
| | | 1129 | | |
| | 0 | 1130 | | return status; |
| | 0 | 1131 | | } |
| | | 1132 | | |
| | | 1133 | | /*++ |
| | | 1134 | | VerifyRemoteCertificate - Validates the content of a Remote Certificate |
| | | 1135 | | |
| | | 1136 | | checkCRL if true, checks the certificate revocation list for validity. |
| | | 1137 | | checkCertName, if true checks the CN field of the certificate |
| | | 1138 | | --*/ |
| | | 1139 | | |
| | | 1140 | | //This method validates a remote certificate. |
| | | 1141 | | internal bool VerifyRemoteCertificate(SslCertificateTrust? trust, ref ProtocolToken alertToken, ref SslPolicyErr |
| | 0 | 1142 | | { |
| | | 1143 | | // We need to note the number of certs in ExtraStore that were |
| | | 1144 | | // provided (by the user), we will add more from the received peer |
| | | 1145 | | // chain and we want to dispose only these after we perform the |
| | | 1146 | | // validation. |
| | | 1147 | | // TODO: this forces allocation of X509Certificate2Collection |
| | 0 | 1148 | | int preexistingExtraCertsCount = _sslAuthenticationOptions.CertificateChainPolicy?.ExtraStore?.Count ?? 0; |
| | | 1149 | | |
| | 0 | 1150 | | X509Chain? chain = null; |
| | 0 | 1151 | | bool certificateValidationSkippedOnResume = false; |
| | | 1152 | | |
| | | 1153 | | try |
| | 0 | 1154 | | { |
| | 0 | 1155 | | X509Certificate2? certificate = CertificateValidationPal.GetRemoteCertificate(_securityContext, ref chai |
| | | 1156 | | |
| | 0 | 1157 | | return VerifyRemoteCertificateCore( |
| | 0 | 1158 | | this, |
| | 0 | 1159 | | !_isRenego && !_isReAuthentication, |
| | 0 | 1160 | | _sslAuthenticationOptions, |
| | 0 | 1161 | | _securityContext, |
| | 0 | 1162 | | ref _remoteCertificate, |
| | 0 | 1163 | | ref _connectionInfo, |
| | 0 | 1164 | | certificate, |
| | 0 | 1165 | | chain, |
| | 0 | 1166 | | trust, |
| | 0 | 1167 | | ref alertToken, |
| | 0 | 1168 | | ref sslPolicyErrors, |
| | 0 | 1169 | | out chainStatus, |
| | 0 | 1170 | | out certificateValidationSkippedOnResume, |
| | 0 | 1171 | | peerCertificateChain: null, |
| | 0 | 1172 | | cloneCertificateChainPolicy: false); |
| | | 1173 | | } |
| | | 1174 | | finally |
| | 0 | 1175 | | { |
| | | 1176 | | // At least on Win2k server the chain is found to have dependencies on the original cert context. |
| | | 1177 | | // So it should be closed first. |
| | | 1178 | | |
| | 0 | 1179 | | if (chain != null) |
| | 0 | 1180 | | { |
| | | 1181 | | // Only cleanup certificates if no user callback was provided. |
| | | 1182 | | // When a callback is provided, users might add their own certificates to ExtraStore |
| | | 1183 | | // or keep references to certificates from ChainElements. |
| | | 1184 | | // On a resumed handshake we skip the callback entirely (see the resumption shortcut |
| | | 1185 | | // in VerifyRemoteCertificateCore), so nothing else adopts the peer-sent intermediates |
| | | 1186 | | // GetRemoteCertificate appended; dispose them here even when a callback is configured |
| | | 1187 | | // to avoid leaking X509Certificate2 handles across repeated resumptions. |
| | 0 | 1188 | | if (_sslAuthenticationOptions.CertValidationDelegate == null || certificateValidationSkippedOnResume |
| | 0 | 1189 | | { |
| | | 1190 | | // Dispose only the certificates that were added by GetRemoteCertificate |
| | 0 | 1191 | | for (int i = preexistingExtraCertsCount; i < chain.ChainPolicy.ExtraStore.Count; i++) |
| | 0 | 1192 | | { |
| | 0 | 1193 | | chain.ChainPolicy.ExtraStore[i].Dispose(); |
| | 0 | 1194 | | } |
| | | 1195 | | |
| | 0 | 1196 | | int elementsCount = chain.ChainElements.Count; |
| | 0 | 1197 | | for (int i = 0; i < elementsCount; i++) |
| | 0 | 1198 | | { |
| | 0 | 1199 | | chain.ChainElements[i].Certificate.Dispose(); |
| | 0 | 1200 | | } |
| | 0 | 1201 | | } |
| | | 1202 | | |
| | 0 | 1203 | | chain.Dispose(); |
| | 0 | 1204 | | } |
| | 0 | 1205 | | } |
| | 0 | 1206 | | } |
| | | 1207 | | |
| | | 1208 | | internal bool VerifyRemoteCertificate( |
| | | 1209 | | X509Certificate2? certificate, |
| | | 1210 | | X509Chain? chain, |
| | | 1211 | | SslCertificateTrust? trust, |
| | | 1212 | | ref ProtocolToken alertToken, |
| | | 1213 | | ref SslPolicyErrors sslPolicyErrors, |
| | | 1214 | | out X509ChainStatusFlags chainStatus) |
| | | 1215 | | { |
| | | 1216 | | return VerifyRemoteCertificateCore( |
| | | 1217 | | this, |
| | | 1218 | | !_isRenego && !_isReAuthentication, |
| | | 1219 | | _sslAuthenticationOptions, |
| | | 1220 | | _securityContext, |
| | | 1221 | | ref _remoteCertificate, |
| | | 1222 | | ref _connectionInfo, |
| | | 1223 | | certificate, |
| | | 1224 | | chain, |
| | | 1225 | | trust, |
| | | 1226 | | ref alertToken, |
| | | 1227 | | ref sslPolicyErrors, |
| | | 1228 | | out chainStatus, |
| | | 1229 | | out _, |
| | | 1230 | | peerCertificateChain: null, |
| | | 1231 | | cloneCertificateChainPolicy: false); |
| | | 1232 | | } |
| | | 1233 | | |
| | | 1234 | | internal static bool VerifyRemoteCertificateCore( |
| | | 1235 | | object sender, |
| | | 1236 | | bool isInitialHandshake, |
| | | 1237 | | SslAuthenticationOptions sslAuthenticationOptions, |
| | | 1238 | | #if TARGET_APPLE |
| | | 1239 | | SafeDeleteContext? securityContext, |
| | | 1240 | | #else |
| | | 1241 | | SafeDeleteSslContext? securityContext, |
| | | 1242 | | #endif |
| | | 1243 | | ref X509Certificate2? remoteCertificateSlot, |
| | | 1244 | | ref SslConnectionInfo connectionInfo, |
| | | 1245 | | X509Certificate2? certificate, |
| | | 1246 | | X509Chain? chain, |
| | | 1247 | | SslCertificateTrust? trust, |
| | | 1248 | | ref ProtocolToken alertToken, |
| | | 1249 | | ref SslPolicyErrors sslPolicyErrors, |
| | | 1250 | | out X509ChainStatusFlags chainStatus, |
| | | 1251 | | out bool certificateValidationSkippedOnResume, |
| | | 1252 | | X509Certificate2Collection? peerCertificateChain, |
| | | 1253 | | bool cloneCertificateChainPolicy) |
| | | 1254 | | { |
| | 0 | 1255 | | chainStatus = X509ChainStatusFlags.NoError; |
| | 0 | 1256 | | certificateValidationSkippedOnResume = false; |
| | | 1257 | | |
| | 0 | 1258 | | bool success = false; |
| | | 1259 | | |
| | 0 | 1260 | | RemoteCertificateValidationCallback? remoteCertValidationCallback = sslAuthenticationOptions.CertValidationD |
| | | 1261 | | |
| | 0 | 1262 | | if (remoteCertificateSlot != null && |
| | 0 | 1263 | | certificate != null && |
| | 0 | 1264 | | certificate.RawDataMemory.Span.SequenceEqual(remoteCertificateSlot.RawDataMemory.Span)) |
| | 0 | 1265 | | { |
| | | 1266 | | // This is renegotiation or TLS 1.3 post-handshake auth and the (remote) certificate did not change. |
| | | 1267 | | // Revalidating the same certificate MAY fail for a couple of reasons (expiration, revocation, |
| | | 1268 | | // change in system trust, ...), but we have already established trust on this particular |
| | | 1269 | | // connection to even get this far. |
| | 0 | 1270 | | certificate.Dispose(); |
| | 0 | 1271 | | return true; |
| | | 1272 | | } |
| | | 1273 | | |
| | 0 | 1274 | | if (certificate != null && |
| | 0 | 1275 | | isInitialHandshake && |
| | 0 | 1276 | | connectionInfo.TlsResumed && |
| | 0 | 1277 | | !LocalAppContextSwitches.RevalidateCertificateOnTlsResume) |
| | 0 | 1278 | | { |
| | | 1279 | | // The initial TLS handshake was a resumption via an abbreviated handshake. The |
| | | 1280 | | // peer did not send its certificate again; its identity was established and |
| | | 1281 | | // validated during the original full handshake that produced the session ticket |
| | | 1282 | | // / session id. Common TLS stacks (e.g. OpenSSL, SChannel) do not re-run |
| | | 1283 | | // certificate verification on resumption, so by default neither do we: adopt the |
| | | 1284 | | // cached peer certificate for the RemoteCertificate property but skip rebuilding |
| | | 1285 | | // the chain and invoking the user validation callback. Set the |
| | | 1286 | | // System.Net.Security.RevalidateCertificateOnTlsResume switch to opt back into |
| | | 1287 | | // re-validating the peer certificate on every resumption. |
| | | 1288 | | // |
| | | 1289 | | // This shortcut is gated on the initial handshake: during renegotiation or |
| | | 1290 | | // TLS 1.3 post-handshake authentication the peer can present a new certificate, |
| | | 1291 | | // which must always be validated (the identical-certificate case above is handled |
| | | 1292 | | // separately). |
| | 0 | 1293 | | remoteCertificateSlot = certificate; |
| | 0 | 1294 | | certificateValidationSkippedOnResume = true; |
| | 0 | 1295 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 1296 | | { |
| | 0 | 1297 | | NetEventSource.Info(sender, "Skipping remote certificate validation on resumed TLS session."); |
| | 0 | 1298 | | } |
| | 0 | 1299 | | return true; |
| | | 1300 | | } |
| | | 1301 | | |
| | | 1302 | | // don't assign to remoteCertificateSlot yet, this prevents weird exceptions if SslStream is disposed in par |
| | | 1303 | | |
| | 0 | 1304 | | if (certificate == null) |
| | 0 | 1305 | | { |
| | 0 | 1306 | | if (NetEventSource.Log.IsEnabled() && sslAuthenticationOptions.RemoteCertRequired) |
| | 0 | 1307 | | { |
| | 0 | 1308 | | NetEventSource.Error(sender, $"Remote certificate required, but no remote certificate received"); |
| | 0 | 1309 | | } |
| | 0 | 1310 | | sslPolicyErrors |= SslPolicyErrors.RemoteCertificateNotAvailable; |
| | 0 | 1311 | | } |
| | | 1312 | | else |
| | 0 | 1313 | | { |
| | 0 | 1314 | | chain ??= new X509Chain(); |
| | | 1315 | | |
| | 0 | 1316 | | if (sslAuthenticationOptions.CertificateChainPolicy != null) |
| | 0 | 1317 | | { |
| | 0 | 1318 | | chain.ChainPolicy = cloneCertificateChainPolicy |
| | 0 | 1319 | | ? sslAuthenticationOptions.CertificateChainPolicy.Clone() |
| | 0 | 1320 | | : sslAuthenticationOptions.CertificateChainPolicy; |
| | 0 | 1321 | | } |
| | | 1322 | | else |
| | 0 | 1323 | | { |
| | 0 | 1324 | | chain.ChainPolicy.RevocationMode = sslAuthenticationOptions.CertificateRevocationCheckMode; |
| | 0 | 1325 | | chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot; |
| | | 1326 | | |
| | 0 | 1327 | | if (sslAuthenticationOptions.IsServer && !LocalAppContextSwitches.EnableServerAiaDownloads) |
| | 0 | 1328 | | { |
| | 0 | 1329 | | chain.ChainPolicy.DisableCertificateDownloads = true; |
| | 0 | 1330 | | } |
| | | 1331 | | |
| | 0 | 1332 | | if (trust != null) |
| | 0 | 1333 | | { |
| | 0 | 1334 | | chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust; |
| | 0 | 1335 | | if (trust._store != null) |
| | 0 | 1336 | | { |
| | 0 | 1337 | | chain.ChainPolicy.CustomTrustStore.AddRange(trust._store.Certificates); |
| | 0 | 1338 | | } |
| | 0 | 1339 | | if (trust._trustList != null) |
| | 0 | 1340 | | { |
| | 0 | 1341 | | chain.ChainPolicy.CustomTrustStore.AddRange(trust._trustList); |
| | 0 | 1342 | | } |
| | 0 | 1343 | | } |
| | 0 | 1344 | | } |
| | | 1345 | | |
| | 0 | 1346 | | if (peerCertificateChain is { Count: > 0 }) |
| | 0 | 1347 | | { |
| | 0 | 1348 | | chain.ChainPolicy.ExtraStore.AddRange(peerCertificateChain); |
| | 0 | 1349 | | } |
| | | 1350 | | |
| | | 1351 | | // set ApplicationPolicy unless already provided. |
| | 0 | 1352 | | if (chain.ChainPolicy.ApplicationPolicy.Count == 0) |
| | 0 | 1353 | | { |
| | | 1354 | | // Authenticate the remote party: (e.g. when operating in server mode, authenticate the client). |
| | 0 | 1355 | | chain.ChainPolicy.ApplicationPolicy.Add(sslAuthenticationOptions.IsServer ? s_clientAuthOid : s_serv |
| | 0 | 1356 | | } |
| | | 1357 | | |
| | 0 | 1358 | | sslPolicyErrors |= CertificateValidationPal.VerifyCertificateProperties( |
| | 0 | 1359 | | securityContext!, |
| | 0 | 1360 | | chain, |
| | 0 | 1361 | | certificate, |
| | 0 | 1362 | | sslAuthenticationOptions.CheckCertName, |
| | 0 | 1363 | | sslAuthenticationOptions.IsServer, |
| | 0 | 1364 | | TargetHostNameHelper.NormalizeHostName(sslAuthenticationOptions.TargetHost)); |
| | 0 | 1365 | | } |
| | | 1366 | | |
| | 0 | 1367 | | remoteCertificateSlot = certificate; |
| | | 1368 | | |
| | 0 | 1369 | | if (remoteCertValidationCallback != null) |
| | 0 | 1370 | | { |
| | | 1371 | | // Ensure connection info is populated before calling the user callback, |
| | | 1372 | | // which may access properties like SslProtocol or CipherAlgorithm. |
| | | 1373 | | // During inline cert validation the handshake hasn't completed yet, so |
| | | 1374 | | // connectionInfo may not have been set by ProcessHandshakeSuccess. |
| | 0 | 1375 | | if (connectionInfo.Protocol == 0 && securityContext is not null) |
| | 0 | 1376 | | { |
| | 0 | 1377 | | SslStreamPal.QueryContextConnectionInfo(securityContext, ref connectionInfo); |
| | 0 | 1378 | | } |
| | | 1379 | | |
| | 0 | 1380 | | success = remoteCertValidationCallback(sender, certificate, chain, sslPolicyErrors); |
| | 0 | 1381 | | } |
| | | 1382 | | else |
| | 0 | 1383 | | { |
| | 0 | 1384 | | if (!sslAuthenticationOptions.RemoteCertRequired) |
| | 0 | 1385 | | { |
| | 0 | 1386 | | sslPolicyErrors &= ~SslPolicyErrors.RemoteCertificateNotAvailable; |
| | 0 | 1387 | | } |
| | | 1388 | | |
| | 0 | 1389 | | success = sslPolicyErrors == SslPolicyErrors.None; |
| | 0 | 1390 | | } |
| | | 1391 | | |
| | 0 | 1392 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 1393 | | { |
| | 0 | 1394 | | LogCertificateValidation(sender, remoteCertValidationCallback, sslPolicyErrors, success, chain); |
| | 0 | 1395 | | NetEventSource.Info(sender, $"Cert validation, remote cert = {remoteCertificateSlot}"); |
| | 0 | 1396 | | } |
| | | 1397 | | |
| | 0 | 1398 | | if (!success) |
| | 0 | 1399 | | { |
| | | 1400 | | #pragma warning disable CS0162 // unreachable code detected (compile time const) |
| | 0 | 1401 | | if (SslStreamPal.CanGenerateCustomAlertsForContext(securityContext) && !SslStreamPal.CertValidationInCal |
| | 0 | 1402 | | { |
| | 0 | 1403 | | sslStream.CreateFatalHandshakeAlertToken(sslPolicyErrors, chain!, ref alertToken); |
| | 0 | 1404 | | } |
| | | 1405 | | #pragma warning restore CS0162 // unreachable code detected (compile time const) |
| | | 1406 | | |
| | 0 | 1407 | | if (chain != null) |
| | 0 | 1408 | | { |
| | 0 | 1409 | | foreach (X509ChainStatus status in chain.ChainStatus) |
| | 0 | 1410 | | { |
| | 0 | 1411 | | chainStatus |= status.Status; |
| | 0 | 1412 | | } |
| | 0 | 1413 | | } |
| | 0 | 1414 | | } |
| | | 1415 | | |
| | 0 | 1416 | | return success; |
| | 0 | 1417 | | } |
| | | 1418 | | |
| | | 1419 | | private void CreateFatalHandshakeAlertToken(SslPolicyErrors sslPolicyErrors, X509Chain? chain, ref ProtocolToken |
| | 0 | 1420 | | { |
| | | 1421 | | TlsAlertMessage alertMessage; |
| | | 1422 | | |
| | 0 | 1423 | | switch (sslPolicyErrors) |
| | | 1424 | | { |
| | | 1425 | | case SslPolicyErrors.RemoteCertificateChainErrors: |
| | 0 | 1426 | | Debug.Assert(chain != null); |
| | 0 | 1427 | | alertMessage = GetAlertMessageFromChain(chain!); |
| | 0 | 1428 | | break; |
| | | 1429 | | case SslPolicyErrors.RemoteCertificateNameMismatch: |
| | 0 | 1430 | | alertMessage = TlsAlertMessage.BadCertificate; |
| | 0 | 1431 | | break; |
| | | 1432 | | case SslPolicyErrors.RemoteCertificateNotAvailable: |
| | | 1433 | | default: |
| | 0 | 1434 | | alertMessage = TlsAlertMessage.CertificateUnknown; |
| | 0 | 1435 | | break; |
| | | 1436 | | } |
| | | 1437 | | |
| | 0 | 1438 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 1439 | | NetEventSource.Info(this, $"alertMessage:{alertMessage}"); |
| | | 1440 | | |
| | | 1441 | | SecurityStatusPal status; |
| | 0 | 1442 | | status = SslStreamPal.ApplyAlertToken(_securityContext, TlsAlertType.Fatal, alertMessage); |
| | | 1443 | | |
| | 0 | 1444 | | if (status.ErrorCode != SecurityStatusPalErrorCode.OK) |
| | 0 | 1445 | | { |
| | 0 | 1446 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 1447 | | NetEventSource.Info(this, $"ApplyAlertToken() returned {status.ErrorCode}"); |
| | | 1448 | | |
| | 0 | 1449 | | if (status.Exception != null) |
| | 0 | 1450 | | { |
| | 0 | 1451 | | ExceptionDispatchInfo.Throw(status.Exception); |
| | | 1452 | | } |
| | 0 | 1453 | | } |
| | | 1454 | | |
| | | 1455 | | #if TARGET_APPLE |
| | | 1456 | | if (_securityContext is not null && !SslStreamPal.IsAsyncSecurityContext(_securityContext)) |
| | | 1457 | | { |
| | | 1458 | | byte[] alertFrame = TlsFrameHelper.CreateAlertFrame(_lastFrame.Header.Version, (TlsAlertDescription)aler |
| | | 1459 | | if (alertFrame.Length != 0) |
| | | 1460 | | { |
| | | 1461 | | alertToken.SetPayload(alertFrame); |
| | | 1462 | | return; |
| | | 1463 | | } |
| | | 1464 | | } |
| | | 1465 | | #endif |
| | 0 | 1466 | | alertToken = GenerateAlertToken(); |
| | 0 | 1467 | | } |
| | | 1468 | | |
| | | 1469 | | private ProtocolToken CreateShutdownToken() |
| | 0 | 1470 | | { |
| | | 1471 | | SecurityStatusPal status; |
| | 0 | 1472 | | status = SslStreamPal.ApplyShutdownToken(_securityContext!); |
| | | 1473 | | |
| | 0 | 1474 | | if (status.ErrorCode != SecurityStatusPalErrorCode.OK) |
| | 0 | 1475 | | { |
| | 0 | 1476 | | if (NetEventSource.Log.IsEnabled()) |
| | 0 | 1477 | | NetEventSource.Info(this, $"ApplyAlertToken() returned {status.ErrorCode}"); |
| | | 1478 | | |
| | 0 | 1479 | | if (status.Exception != null) |
| | 0 | 1480 | | { |
| | 0 | 1481 | | ExceptionDispatchInfo.Throw(status.Exception); |
| | | 1482 | | } |
| | | 1483 | | |
| | 0 | 1484 | | return default; |
| | | 1485 | | } |
| | | 1486 | | |
| | 0 | 1487 | | return GenerateToken(default, out _); |
| | 0 | 1488 | | } |
| | | 1489 | | |
| | | 1490 | | private ProtocolToken GenerateAlertToken() |
| | 0 | 1491 | | { |
| | 0 | 1492 | | return GenerateToken(default, out _); |
| | 0 | 1493 | | } |
| | | 1494 | | |
| | | 1495 | | internal static TlsAlertMessage GetAlertMessageFromChain(X509Chain chain) |
| | 0 | 1496 | | { |
| | 0 | 1497 | | foreach (X509ChainStatus chainStatus in chain.ChainStatus) |
| | 0 | 1498 | | { |
| | 0 | 1499 | | if (chainStatus.Status == X509ChainStatusFlags.NoError) |
| | 0 | 1500 | | { |
| | 0 | 1501 | | continue; |
| | | 1502 | | } |
| | | 1503 | | |
| | 0 | 1504 | | if ((chainStatus.Status & |
| | 0 | 1505 | | (X509ChainStatusFlags.UntrustedRoot | X509ChainStatusFlags.PartialChain | |
| | 0 | 1506 | | X509ChainStatusFlags.Cyclic)) != 0) |
| | 0 | 1507 | | { |
| | 0 | 1508 | | return TlsAlertMessage.UnknownCA; |
| | | 1509 | | } |
| | | 1510 | | |
| | 0 | 1511 | | if ((chainStatus.Status & |
| | 0 | 1512 | | (X509ChainStatusFlags.Revoked | X509ChainStatusFlags.OfflineRevocation)) != 0) |
| | 0 | 1513 | | { |
| | 0 | 1514 | | return TlsAlertMessage.CertificateRevoked; |
| | | 1515 | | } |
| | | 1516 | | |
| | 0 | 1517 | | if ((chainStatus.Status & |
| | 0 | 1518 | | (X509ChainStatusFlags.CtlNotTimeValid | X509ChainStatusFlags.NotTimeNested | |
| | 0 | 1519 | | X509ChainStatusFlags.NotTimeValid)) != 0) |
| | 0 | 1520 | | { |
| | 0 | 1521 | | return TlsAlertMessage.CertificateExpired; |
| | | 1522 | | } |
| | | 1523 | | |
| | 0 | 1524 | | if ((chainStatus.Status & X509ChainStatusFlags.CtlNotValidForUsage) != 0) |
| | 0 | 1525 | | { |
| | 0 | 1526 | | return TlsAlertMessage.UnsupportedCert; |
| | | 1527 | | } |
| | | 1528 | | |
| | 0 | 1529 | | if ((chainStatus.Status & |
| | 0 | 1530 | | (X509ChainStatusFlags.CtlNotSignatureValid | X509ChainStatusFlags.InvalidExtension | |
| | 0 | 1531 | | X509ChainStatusFlags.NotSignatureValid | X509ChainStatusFlags.InvalidPolicyConstraints | |
| | 0 | 1532 | | X509ChainStatusFlags.NoIssuanceChainPolicy | X509ChainStatusFlags.NotValidForUsage)) != 0) |
| | 0 | 1533 | | { |
| | 0 | 1534 | | return TlsAlertMessage.BadCertificate; |
| | | 1535 | | } |
| | | 1536 | | |
| | | 1537 | | // All other errors: |
| | 0 | 1538 | | return TlsAlertMessage.CertificateUnknown; |
| | | 1539 | | } |
| | | 1540 | | |
| | 0 | 1541 | | return TlsAlertMessage.BadCertificate; |
| | 0 | 1542 | | } |
| | | 1543 | | |
| | | 1544 | | private static void LogCertificateValidation(object sender, RemoteCertificateValidationCallback? remoteCertValid |
| | 0 | 1545 | | { |
| | 0 | 1546 | | if (!NetEventSource.Log.IsEnabled()) |
| | 0 | 1547 | | return; |
| | | 1548 | | |
| | 0 | 1549 | | if (sslPolicyErrors != SslPolicyErrors.None) |
| | 0 | 1550 | | { |
| | 0 | 1551 | | NetEventSource.Log.RemoteCertificateError(sender, SR.net_log_remote_cert_has_errors); |
| | 0 | 1552 | | if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNotAvailable) != 0) |
| | 0 | 1553 | | { |
| | 0 | 1554 | | NetEventSource.Log.RemoteCertificateError(sender, SR.net_log_remote_cert_not_available); |
| | 0 | 1555 | | } |
| | | 1556 | | |
| | 0 | 1557 | | if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateNameMismatch) != 0) |
| | 0 | 1558 | | { |
| | 0 | 1559 | | NetEventSource.Log.RemoteCertificateError(sender, SR.net_log_remote_cert_name_mismatch); |
| | 0 | 1560 | | } |
| | | 1561 | | |
| | 0 | 1562 | | if ((sslPolicyErrors & SslPolicyErrors.RemoteCertificateChainErrors) != 0) |
| | 0 | 1563 | | { |
| | 0 | 1564 | | Debug.Assert(chain != null); |
| | 0 | 1565 | | string chainStatusString = "ChainStatus: "; |
| | 0 | 1566 | | foreach (X509ChainStatus chainStatus in chain!.ChainStatus) |
| | 0 | 1567 | | { |
| | 0 | 1568 | | chainStatusString += "\t" + chainStatus.StatusInformation; |
| | 0 | 1569 | | } |
| | 0 | 1570 | | NetEventSource.Log.RemoteCertificateError(sender, chainStatusString); |
| | 0 | 1571 | | } |
| | 0 | 1572 | | } |
| | | 1573 | | |
| | 0 | 1574 | | if (success) |
| | 0 | 1575 | | { |
| | 0 | 1576 | | if (remoteCertValidationCallback != null) |
| | 0 | 1577 | | { |
| | 0 | 1578 | | NetEventSource.Log.RemoteCertDeclaredValid(sender); |
| | 0 | 1579 | | } |
| | | 1580 | | else |
| | 0 | 1581 | | { |
| | 0 | 1582 | | NetEventSource.Log.RemoteCertHasNoErrors(sender); |
| | 0 | 1583 | | } |
| | 0 | 1584 | | } |
| | | 1585 | | else |
| | 0 | 1586 | | { |
| | 0 | 1587 | | if (remoteCertValidationCallback != null) |
| | 0 | 1588 | | { |
| | 0 | 1589 | | NetEventSource.Log.RemoteCertUserDeclaredInvalid(sender); |
| | 0 | 1590 | | } |
| | 0 | 1591 | | } |
| | 0 | 1592 | | } |
| | | 1593 | | } |
| | | 1594 | | |
| | | 1595 | | // ProtocolToken - used to process and handle the return codes from the SSPI wrapper |
| | | 1596 | | internal struct ProtocolToken |
| | | 1597 | | { |
| | | 1598 | | internal SecurityStatusPal Status; |
| | | 1599 | | internal byte[]? Payload; |
| | | 1600 | | internal int Size; |
| | | 1601 | | internal bool RentBuffer; |
| | | 1602 | | |
| | | 1603 | | internal bool Failed |
| | | 1604 | | { |
| | | 1605 | | get |
| | | 1606 | | { |
| | | 1607 | | return ((Status.ErrorCode != SecurityStatusPalErrorCode.OK) && (Status.ErrorCode != SecurityStatusPalErr |
| | | 1608 | | } |
| | | 1609 | | } |
| | | 1610 | | |
| | | 1611 | | internal bool Done |
| | | 1612 | | { |
| | | 1613 | | get |
| | | 1614 | | { |
| | | 1615 | | return (Status.ErrorCode == SecurityStatusPalErrorCode.OK); |
| | | 1616 | | } |
| | | 1617 | | } |
| | | 1618 | | |
| | | 1619 | | internal bool Renegotiate |
| | | 1620 | | { |
| | | 1621 | | get |
| | | 1622 | | { |
| | | 1623 | | return (Status.ErrorCode == SecurityStatusPalErrorCode.Renegotiate); |
| | | 1624 | | } |
| | | 1625 | | } |
| | | 1626 | | |
| | | 1627 | | internal bool CloseConnection |
| | | 1628 | | { |
| | | 1629 | | get |
| | | 1630 | | { |
| | | 1631 | | return (Status.ErrorCode == SecurityStatusPalErrorCode.ContextExpired); |
| | | 1632 | | } |
| | | 1633 | | } |
| | | 1634 | | internal void SetPayload(ReadOnlySpan<byte> payload) |
| | | 1635 | | { |
| | | 1636 | | Debug.Assert(Payload == null); |
| | | 1637 | | Size = payload.Length; |
| | | 1638 | | |
| | | 1639 | | if (Size > 0) |
| | | 1640 | | { |
| | | 1641 | | Payload = RentBuffer ? ArrayPool<byte>.Shared.Rent(Size) : new byte[Size]; |
| | | 1642 | | payload.CopyTo(new Span<byte>(Payload, 0, Size)); |
| | | 1643 | | } |
| | | 1644 | | } |
| | | 1645 | | |
| | | 1646 | | internal void EnsureAvailableSpace(int size) |
| | | 1647 | | { |
| | | 1648 | | if (Available >= size) |
| | | 1649 | | { |
| | | 1650 | | return; |
| | | 1651 | | } |
| | | 1652 | | |
| | | 1653 | | var oldPayload = Payload; |
| | | 1654 | | |
| | | 1655 | | Payload = RentBuffer ? ArrayPool<byte>.Shared.Rent(Size + size) : new byte[Size + size]; |
| | | 1656 | | if (oldPayload != null) |
| | | 1657 | | { |
| | | 1658 | | oldPayload.AsSpan<byte>().CopyTo(Payload); |
| | | 1659 | | if (RentBuffer) |
| | | 1660 | | { |
| | | 1661 | | ArrayPool<byte>.Shared.Return(oldPayload); |
| | | 1662 | | } |
| | | 1663 | | } |
| | | 1664 | | } |
| | | 1665 | | |
| | | 1666 | | internal int Available => Payload == null ? 0 : Payload.Length - Size; |
| | | 1667 | | internal Span<byte> AvailableSpan => Payload == null ? Span<byte>.Empty : new Span<byte>(Payload, Size, Availabl |
| | | 1668 | | |
| | | 1669 | | internal ReadOnlyMemory<byte> AsMemory() => new ReadOnlyMemory<byte>(Payload, 0, Size); |
| | | 1670 | | |
| | | 1671 | | internal void ReleasePayload() |
| | | 1672 | | { |
| | | 1673 | | Debug.Assert(Payload != null || Size == 0); |
| | | 1674 | | |
| | | 1675 | | byte[]? toReturn = Payload; |
| | | 1676 | | Payload = null; |
| | | 1677 | | Size = 0; |
| | | 1678 | | if (RentBuffer && toReturn != null) |
| | | 1679 | | { |
| | | 1680 | | ArrayPool<byte>.Shared.Return(toReturn); |
| | | 1681 | | } |
| | | 1682 | | } |
| | | 1683 | | |
| | | 1684 | | internal Exception? GetException() |
| | | 1685 | | { |
| | | 1686 | | // If it's not done, then there's got to be an error, even if it's |
| | | 1687 | | // a Handshake message up, and we only have a Warning message. |
| | | 1688 | | return Done ? null : SslStreamPal.GetException(Status); |
| | | 1689 | | } |
| | | 1690 | | } |
| | | 1691 | | } |
| | | 1692 | | |