I am using a custom server validation callback based on this example
private void InitServices(IHostBuilder builder, ...)
{
...
builder
.ConfigureServices((context, services) =>
{
services.AddHttpClient("myHttpClient", c =>
{
c.Timeout = TimeSpan.FromSeconds(httpClientTimeoutSeconds);
})
.ConfigurePrimaryHttpMessageHandler(() =>
{
var handler = new HttpClientHandler();
var myPemCaCertificateString = File.ReadAllText(myPemCaFilePath);
var caRootBytes = Encoding.ASCII.GetBytes(myPemCaCertificateString);
myCaRootX509Certificate = new X509Certificate2(caRootBytes);
var myRootCertificates = new X509Certificate2Collection(myCaRootX509Certificate);
handler.ServerCertificateCustomValidationCallback = CreateCustomRootValidator(myRootCertificates);
...
}
The validation method checks the chain.Build result and detects the SslPolicyErrors value and also the ChainElementStatus in case of failure. But the method can return only a bool value if the validation fails.
private RemoteCertificateValidationCallback CreateCustomRootRemoteValidator(X509Certificate2Collection trustedRoots, X509Certificate2Collection intermediates = null)
{
if (trustedRoots == null)
throw new ArgumentNullException(nameof(trustedRoots));
if (trustedRoots.Count == 0)
throw new ArgumentException("No trusted roots were provided", nameof(trustedRoots));
X509Certificate2Collection roots = new X509Certificate2Collection(trustedRoots);
X509Certificate2Collection intermeds = null;
if (intermediates != null)
{
intermeds = new X509Certificate2Collection(intermediates);
}
intermediates = null;
trustedRoots = null;
return (sender, serverCert, chain, errors) =>
{
// Missing cert or the destination hostname wasn't valid for the cert.
if ((errors & ~SslPolicyErrors.RemoteCertificateChainErrors) != 0)
{
return false;
}
for (int i = 1; i < chain.ChainElements.Count; i++)
{
chain.ChainPolicy.ExtraStore.Add(chain.ChainElements[i].Certificate);
}
if (intermeds != null)
{
chain.ChainPolicy.ExtraStore.AddRange(intermeds);
}
chain.ChainPolicy.CustomTrustStore.Clear();
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust;
chain.ChainPolicy.CustomTrustStore.AddRange(roots);
var retChainBuild = chain.Build((X509Certificate2)serverCert);
// Check if chain.Build returned false, that means ServerCertificateCustomValidation failed
if (!retChainBuild)
{
var nrChainElements = chain.ChainElements.Count;
if (nrChainElements > 0)
{
// the last element in the chain should be the root ca
X509ChainElement chainElement = chain.ChainElements[nrChainElements - 1];
foreach (X509ChainStatus status in chainElement.ChainElementStatus)
{
if (status.Status == X509ChainStatusFlags.PartialChain)
{
}
else if (status.Status == X509ChainStatusFlags.UntrustedRoot)
{
}
}
}
}
return retChainBuild;
};
}
If the custom implementation of the ServerCertificateCustomValidationCallback returns false, then the following message is available in the AuthenticationException that is thrown when using the HttpClient:
The remote certificate was rejected by the provided RemoteCertificateValidationCallback.
If the ServerCertificateCustomValidationCallback is not defined for the HttpClientHandler, the original Microsoft implementation returns the following message in the AuthenticationException:
The remote certificate is invalid because of errors in the certificate chain: UntrustedRoot
How can the information about the SslPolicyErrors type and also about the ChainElementStatus be passed to the AuthenticationException, in case of defining a ServerCertificateCustomValidationCallback for the HttpClientHandler?