private bool AcquireClientCredentials(ref byte[] thumbPrint)
{
if (NetEventSource.IsEnabled) NetEventSource.Enter(this);
// Acquire possible Client Certificate information and set it on the handle.
X509Certificate clientCertificate = null; // This is a candidate that can come from the user callback or be guessed when targeting a session restart.
var filteredCerts = new List<X509Certificate>(); // This is an intermediate client certs collection that try to use if no selectedCert is available yet.
string[] issuers = null; // This is a list of issuers sent by the server, only valid is we do know what the server cert is.
bool sessionRestartAttempt = false; // If true and no cached creds we will use anonymous creds.
if (_certSelectionDelegate != null)
{
issuers = GetRequestCertificateAuthorities();
if (NetEventSource.IsEnabled) NetEventSource.Info(this, "Calling CertificateSelectionCallback");
X509Certificate2 remoteCert = null;
try
{
X509Certificate2Collection dummyCollection;
remoteCert = CertificateValidationPal.GetRemoteCertificate(_securityContext, out dummyCollection);
clientCertificate = _certSelectionDelegate(_hostName, ClientCertificates, remoteCert, issuers);
}
finally
{
if (remoteCert != null)
{
remoteCert.Dispose();
}
}
if (clientCertificate != null)
{
if (_credentialsHandle == null)
{
sessionRestartAttempt = true;
}
filteredCerts.Add(clientCertificate);
if (NetEventSource.IsEnabled) NetEventSource.Log.CertificateFromDelegate(this);
}
else
{
if (ClientCertificates.Count == 0)
{
if (NetEventSource.IsEnabled) NetEventSource.Log.NoDelegateNoClientCert(this);
sessionRestartAttempt = true;
}
else
{
if (NetEventSource.IsEnabled) NetEventSource.Log.NoDelegateButClientCert(this);
}
}
}
else if (_credentialsHandle == null && _clientCertificates != null && _clientCertificates.Count > 0)
{
// This is where we attempt to restart a session by picking the FIRST cert from the collection.
// Otherwise it is either server sending a client cert request or the session is renegotiated.
clientCertificate = ClientCertificates[0];
sessionRestartAttempt = true;
if (clientCertificate != null)
{
filteredCerts.Add(clientCertificate);
}
if (NetEventSource.IsEnabled) NetEventSource.Log.AttemptingRestartUsingCert(clientCertificate, this);
}
else if (_clientCertificates != null && _clientCertificates.Count > 0)
{
//
// This should be a server request for the client cert sent over currently anonymous sessions.
//
issuers = GetRequestCertificateAuthorities();
if (NetEventSource.IsEnabled)
{
if (issuers == null || issuers.Length == 0)
{
NetEventSource.Log.NoIssuersTryAllCerts(this);
}
else
{
NetEventSource.Log.LookForMatchingCerts(issuers.Length, this);
}
}
for (int i = 0; i < _clientCertificates.Count; ++i)
{
//
// Make sure we add only if the cert matches one of the issuers.
// If no issuers were sent and then try all client certs starting with the first one.
//
if (issuers != null && issuers.Length != 0)
{
X509Certificate2 certificateEx = null;
X509Chain chain = null;
try
{
certificateEx = MakeEx(_clientCertificates[i]);
if (certificateEx == null)
{
continue;
}
if (NetEventSource.IsEnabled) NetEventSource.Info(this, $"Root cert: {certificateEx}");
chain = new X509Chain();
chain.ChainPolicy.RevocationMode = X509RevocationMode.NoCheck;
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.IgnoreInvalidName;
chain.Build(certificateEx);
bool found = false;
//
// We ignore any errors happened with chain.
//
if (chain.ChainElements.Count > 0)
{
for (int ii = 0; ii < chain.ChainElements.Count; ++ii)
{
string issuer = chain.ChainElements[ii].Certificate.Issuer;
found = Array.IndexOf(issuers, issuer) != -1;
if (found)
{
if (NetEventSource.IsEnabled) NetEventSource.Info(this, $"Matched {issuer}");
break;
}
if (NetEventSource.IsEnabled) NetEventSource.Info(this, $"No match: {issuer}");
}
}
if (!found)
{
continue;
}
}
finally
{
if (chain != null)
{
chain.Dispose();
}
if (certificateEx != null && (object)certificateEx != (object)_clientCertificates[i])
{
certificateEx.Dispose();
}
}
}
if (NetEventSource.IsEnabled) NetEventSource.Log.SelectedCert(_clientCertificates[i], this);
filteredCerts.Add(_clientCertificates[i]);
}
}
bool cachedCred = false; // This is a return result from this method.
X509Certificate2 selectedCert = null; // This is a final selected cert (ensured that it does have private key with it).
clientCertificate = null;
if (NetEventSource.IsEnabled)
{
NetEventSource.Log.CertsAfterFiltering(filteredCerts.Count, this);
if (filteredCerts.Count != 0)
{
NetEventSource.Log.FindingMatchingCerts(this);
}
}
//
// ATTN: When the client cert was returned by the user callback OR it was guessed AND it has no private key,
// THEN anonymous (no client cert) credential will be used.
//
// SECURITY: Accessing X509 cert Credential is disabled for semitrust.
// We no longer need to demand for unmanaged code permissions.
// EnsurePrivateKey should do the right demand for us.
for (int i = 0; i < filteredCerts.Count; ++i)
{
clientCertificate = filteredCerts[i];
if ((selectedCert = EnsurePrivateKey(clientCertificate)) != null)
{
break;
}
clientCertificate = null;
selectedCert = null;
}
if ((object)clientCertificate != (object)selectedCert && !clientCertificate.Equals(selectedCert))
{
NetEventSource.Fail(this, "'selectedCert' does not match 'clientCertificate'.");
}
if (NetEventSource.IsEnabled) NetEventSource.Info(this, $"Selected cert = {selectedCert}");
try
{
// Try to locate cached creds first.
//
// SECURITY: selectedCert ref if not null is a safe object that does not depend on possible **user** inherited X509Certificate type.
//
byte[] guessedThumbPrint = selectedCert == null ? null : selectedCert.GetCertHash();
SafeFreeCredentials cachedCredentialHandle = SslSessionsCache.TryCachedCredential(guessedThumbPrint, _sslProtocols, _serverMode, _encryptionPolicy);
// We can probably do some optimization here. If the selectedCert is returned by the delegate
// we can always go ahead and use the certificate to create our credential
// (instead of going anonymous as we do here).
if (sessionRestartAttempt &&
cachedCredentialHandle == null &&
selectedCert != null &&
SslStreamPal.StartMutualAuthAsAnonymous)
{
if (NetEventSource.IsEnabled) NetEventSource.Info(this, "Reset to anonymous session.");
// IIS does not renegotiate a restarted session if client cert is needed.
// So we don't want to reuse **anonymous** cached credential for a new SSL connection if the client has passed some certificate.
// The following block happens if client did specify a certificate but no cached creds were found in the cache.
// Since we don't restart a session the server side can still challenge for a client cert.
if ((object)clientCertificate != (object)selectedCert)
{
selectedCert.Dispose();
}
guessedThumbPrint = null;
selectedCert = null;
clientCertificate = null;
}
if (cachedCredentialHandle != null)
{
if (NetEventSource.IsEnabled) NetEventSource.Log.UsingCachedCredential(this);
_credentialsHandle = cachedCredentialHandle;
_selectedClientCertificate = clientCertificate;
cachedCred = true;
}
else
{
_credentialsHandle = SslStreamPal.AcquireCredentialsHandle(selectedCert, _sslProtocols, _encryptionPolicy, _serverMode);
thumbPrint = guessedThumbPrint; // Delay until here in case something above threw.
_selectedClientCertificate = clientCertificate;
}
}
finally
{
// An extra cert could have been created, dispose it now.
if (selectedCert != null && (object)clientCertificate != (object)selectedCert)
{
selectedCert.Dispose();
}
}
if (NetEventSource.IsEnabled) NetEventSource.Exit(this, cachedCred, _credentialsHandle);
return cachedCred;
}