diff --git a/src/libraries/Common/src/Interop/Windows/SspiCli/SecuritySafeHandles.cs b/src/libraries/Common/src/Interop/Windows/SspiCli/SecuritySafeHandles.cs index e0128ab0b994a..3c5bbe532161e 100644 --- a/src/libraries/Common/src/Interop/Windows/SspiCli/SecuritySafeHandles.cs +++ b/src/libraries/Common/src/Interop/Windows/SspiCli/SecuritySafeHandles.cs @@ -171,11 +171,13 @@ internal abstract class SafeFreeCredentials : SafeHandle { #endif + internal DateTime _expiry; internal Interop.SspiCli.CredHandle _handle; //should be always used as by ref in PInvokes parameters protected SafeFreeCredentials() : base(IntPtr.Zero, true) { _handle = default; + _expiry = DateTime.MaxValue; } public override bool IsInvalid @@ -183,6 +185,8 @@ public override bool IsInvalid get { return IsClosed || _handle.IsZero; } } + public DateTime Expiry => _expiry; + #if DEBUG public new IntPtr DangerousGetHandle() { diff --git a/src/libraries/Common/src/System/Net/Security/Unix/SafeFreeCredentials.cs b/src/libraries/Common/src/System/Net/Security/Unix/SafeFreeCredentials.cs index 7d26be95ab96b..c2e0e0226eace 100644 --- a/src/libraries/Common/src/System/Net/Security/Unix/SafeFreeCredentials.cs +++ b/src/libraries/Common/src/System/Net/Security/Unix/SafeFreeCredentials.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Diagnostics; using System.Runtime.ConstrainedExecution; using System.Runtime.InteropServices; @@ -18,8 +19,13 @@ internal abstract class SafeFreeCredentials : DebugSafeHandle internal abstract class SafeFreeCredentials : SafeHandle { #endif + internal DateTime _expiry; + + public DateTime Expiry => _expiry; + protected SafeFreeCredentials(IntPtr handle, bool ownsHandle) : base(handle, ownsHandle) { + _expiry = DateTime.MaxValue; } } diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs index c9b67dda132ca..fbf29e0364cb0 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SecureChannel.cs @@ -171,8 +171,8 @@ internal void Close() { if (!_remoteCertificateExposed) { - _remoteCertificate?.Dispose(); - _remoteCertificate = null; + _remoteCertificate?.Dispose(); + _remoteCertificate = null; } _securityContext?.Dispose(); @@ -607,9 +607,7 @@ private bool AcquireClientCredentials(ref byte[]? thumbPrint) _sslAuthenticationOptions.CertificateContext = SslStreamCertificateContext.Create(selectedCert!); } - _credentialsHandle = SslStreamPal.AcquireCredentialsHandle(_sslAuthenticationOptions.CertificateContext, - _sslAuthenticationOptions.EnabledSslProtocols, _sslAuthenticationOptions.EncryptionPolicy, _sslAuthenticationOptions.IsServer); - + _credentialsHandle = AcquireCredentialsHandle(_sslAuthenticationOptions); thumbPrint = guessedThumbPrint; // Delay until here in case something above threw. } } @@ -713,14 +711,65 @@ private bool AcquireServerCredentials(ref byte[]? thumbPrint) } else { - _credentialsHandle = SslStreamPal.AcquireCredentialsHandle(_sslAuthenticationOptions.CertificateContext, _sslAuthenticationOptions.EnabledSslProtocols, - _sslAuthenticationOptions.EncryptionPolicy, _sslAuthenticationOptions.IsServer); + _credentialsHandle = AcquireCredentialsHandle(_sslAuthenticationOptions); thumbPrint = guessedThumbPrint; } return cachedCred; } + private static SafeFreeCredentials AcquireCredentialsHandle(SslAuthenticationOptions sslAuthenticationOptions) + { + SafeFreeCredentials cred = SslStreamPal.AcquireCredentialsHandle(sslAuthenticationOptions.CertificateContext, sslAuthenticationOptions.EnabledSslProtocols, + sslAuthenticationOptions.EncryptionPolicy, sslAuthenticationOptions.IsServer); + + if (sslAuthenticationOptions.CertificateContext != null) + { + // + // Since the SafeFreeCredentials can be cached and reused, it may happen on long running processes that some cert on + // the chain expires and all subsequent connections would send expired intermediate certificates. Find the earliest + // NotAfter timestamp on the chain and use it as expiration timestamp for the credentials. + // This provides an opportunity to recreate the credentials with an alternative (and still valid) + // certificate chain. + // + SslStreamCertificateContext certificateContext = sslAuthenticationOptions.CertificateContext; + cred._expiry = GetExpiryTimestamp(certificateContext); + + if (cred._expiry < DateTime.UtcNow) + { + // + // The CertificateContext from auth options is recreated just before creating the SafeFreeCredentials. However, in case when + // it was provided by the user code, it may still contain the (now expired) certificate chain. Such expiration timestamp would + // effectively disable caching as it would lead to creating new credentials for each connection. We attempt to recover by creating + // a temporary certificate context (which builds a new chain with hopefully more recent chain). + // + certificateContext = SslStreamCertificateContext.Create( + certificateContext.Certificate, + new X509Certificate2Collection(certificateContext.IntermediateCertificates), + trust: certificateContext.Trust); + + cred._expiry = GetExpiryTimestamp(certificateContext); + } + + static DateTime GetExpiryTimestamp(SslStreamCertificateContext certificateContext) + { + DateTime expiry = certificateContext.Certificate.NotAfter; + + foreach (X509Certificate2 cert in certificateContext.IntermediateCertificates) + { + if (cert.NotAfter < expiry) + { + expiry = cert.NotAfter; + } + } + + return expiry.ToUniversalTime(); + } + } + + return cred; + } + // internal ProtocolToken NextMessage(ReadOnlySpan incomingBuffer) { diff --git a/src/libraries/System.Net.Security/src/System/Net/Security/SslSessionsCache.cs b/src/libraries/System.Net.Security/src/System/Net/Security/SslSessionsCache.cs index f7b730c102c93..59009e72b4cfa 100644 --- a/src/libraries/System.Net.Security/src/System/Net/Security/SslSessionsCache.cs +++ b/src/libraries/System.Net.Security/src/System/Net/Security/SslSessionsCache.cs @@ -134,9 +134,9 @@ public bool Equals(SslCredKey other) //SafeCredentialReference? cached; SafeFreeCredentials? credentials = GetCachedCredential(key); - if (credentials == null || credentials.IsClosed || credentials.IsInvalid) + if (credentials == null || credentials.IsClosed || credentials.IsInvalid || credentials.Expiry < DateTime.UtcNow) { - if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(null, $"Not found or invalid, Current Cache Coun = {s_cachedCreds.Count}"); + if (NetEventSource.Log.IsEnabled()) NetEventSource.Info(null, $"Not found or invalid, Current Cache Count = {s_cachedCreds.Count}"); return null; } @@ -169,12 +169,13 @@ internal static void CacheCredential(SafeFreeCredentials creds, byte[]? thumbPri SafeFreeCredentials? credentials = GetCachedCredential(key); - if (credentials == null || credentials.IsClosed || credentials.IsInvalid) + DateTime utcNow = DateTime.UtcNow; + if (credentials == null || credentials.IsClosed || credentials.IsInvalid || credentials.Expiry < utcNow) { lock (s_cachedCreds) { credentials = GetCachedCredential(key); - if (credentials == null || credentials.IsClosed || credentials.IsInvalid) + if (credentials == null || credentials.IsClosed || credentials.IsInvalid || credentials.Expiry < utcNow) { SafeCredentialReference? cached = SafeCredentialReference.CreateReference(creds);