0

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?

RickyTad
  • 281
  • 1
  • 3
  • 15
  • Thanks for the link, it gives indeed an idea about could this be done gracefully. But as the original poster, I am not sure what would be the best option to "hide" the custom information about the server certificate validation error inside the HttpRequestMessage object (maybe add it to HttpRequestMessage.Options as suggested in the link?). Do you maybe have a link with some code sample? – RickyTad Dec 17 '22 at 13:37
  • Do not know any sample but you can start by extending HttpClientHandler. Take a look at [HttpClientHandler.cs](https://github.com/microsoft/referencesource/blob/master/System/net/System/Net/Http/HttpClientHandler.cs) you at least need to change HandleAsyncException and `_serverCertificateCustomValidationCallback` and `ServerCertificateCustomValidationCallback` – MD Zand Dec 17 '22 at 14:25

0 Answers0