• .NET
  • Exact text checked with the real tool

The remote certificate is invalid according to the validation procedure

Also seen in: PowerShell, Invoke-WebRequest, Invoke-RestMethod, HttpClient, SslStream, SQL Server clients

In one sentence

.NET rejected the server's certificate; the reason after the colon (UntrustedRoot, PartialChain, NotTimeValid or RemoteCertificateNameMismatch) tells you whether trust, the chain, the dates or the name is wrong.

Where you see it

In .NET applications that use HttpClient or SslStream, and in PowerShell’s Invoke-WebRequest and Invoke-RestMethod. The exception is an AuthenticationException, often wrapped in another one.

Windows PowerShell 5.1 (.NET Framework) shows the outer message; the title text is in the inner exception:

Invoke-WebRequest : The underlying connection was closed: Could not establish trust relationship for the SSL/TLS
secure channel.
At line:1 char:1
+ Invoke-WebRequest -UseBasicParsing https://api.example.com/
PS> $Error[0].Exception.InnerException.Message
The remote certificate is invalid according to the validation procedure.

Since .NET 5, the message adds the reason after a colon (System.Net.Security strings). These are real messages from .NET 10:

System.Net.Http.HttpRequestException: The SSL connection could not be established, see inner exception.
 ---> System.Security.Authentication.AuthenticationException: The remote certificate is invalid because of errors in the certificate chain: UntrustedRoot
The remote certificate is invalid because of errors in the certificate chain: PartialChain
The remote certificate is invalid because of errors in the certificate chain: NotTimeValid, PartialChain
The remote certificate is invalid according to the validation procedure: RemoteCertificateNameMismatch, RemoteCertificateChainErrors

What it means

.NET checks three things: that the certificate chain leads to a trusted root, that no certificate is expired, and that the server’s name is in the certificate. The words after the colon are values of SslPolicyErrors and X509ChainStatusFlags and name the failed check.

Likely causes

UntrustedRoot: the root is not trusted

The chain is complete, but its root is not in the machine’s trust store: a company CA, a TLS-inspection proxy, or a self-signed development certificate. On Windows, inspect the chain of a saved server certificate:

$cert  = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new("C:\temp\server.cer")
$chain = [System.Security.Cryptography.X509Certificates.X509Chain]::new()
$chain.ChainPolicy.RevocationMode = "NoCheck"
$chain.Build($cert)
$chain.ChainStatus | Format-Table Status, StatusInformation
$chain.ChainElements | ForEach-Object { $_.Certificate.Subject }

Fix: get the root from your PKI team or IT and install it for the machine:

Import-Certificate -FilePath C:\certs\corp-root.cer -CertStoreLocation Cert:\LocalMachine\Root

On Linux, .NET uses the system OpenSSL store, which SSL_CERT_FILE and SSL_CERT_DIR can override (cross-platform cryptography in .NET): copy the root to /usr/local/share/ca-certificates/corp-root.crt and run update-ca-certificates (Debian, Ubuntu and the official .NET container images), or use /etc/pki/ca-trust/source/anchors/ and update-ca-trust extract on RHEL.

If only one application should trust a private root, use a custom trust store in code instead of switching checks off (this example needs .NET 9 or later):

var policy = new X509ChainPolicy { TrustMode = X509ChainTrustMode.CustomRootTrust };
policy.CustomTrustStore.Add(X509CertificateLoader.LoadCertificateFromFile("corp-root.pem"));
var handler = new SocketsHttpHandler();
handler.SslOptions.CertificateChainPolicy = policy;
var client = new HttpClient(handler);

A new X509ChainPolicy checks revocation online by default. If the private CA publishes no reachable CRL or OCSP service, you will see RevocationStatusUnknown; fix the CA’s revocation service rather than disabling the check.

PartialChain: the chain could not be built

.NET could not build a path to any root. Either the server does not send its intermediate certificate and .NET could not download it (.NET normally fetches it from the certificate’s AIA address, see DisableCertificateDownloads, which fails on servers without outbound internet access), or the root is unknown. Check what the server sends:

openssl s_client -connect api.example.com:443 -servername api.example.com -showcerts </dev/null 2>/dev/null | grep -E '^ [0-9]+ s:|^   i:'

Fix: configure the server with its full chain (certificate first, then intermediates); on IIS, install the intermediate in the computer’s “Intermediate Certification Authorities” store. If the chain is complete, install the root as above.

NotTimeValid: a certificate expired or is not valid yet

Check the dates: $cert.NotBefore and $cert.NotAfter in PowerShell. Renew and redeploy an expired certificate; if the dates look right, check the client’s clock with w32tm /query /status.

RemoteCertificateNameMismatch: wrong name

The URL’s hostname is not in the certificate’s Subject Alternative Name list. Connect with a listed name or reissue the certificate with every name clients use. Connecting by IP address needs an IP entry in the list.

Check yours

Try it: Certificate decoderOpen the full tool

Runs in your browser. Nothing you paste is sent anywhere.

PEM text, several PEM blocks, or a .pem, .crt, .cer or .der file (drop it on the box). Certificates are public, so they are safe to paste.

Paste the server’s certificate (export it from the browser or from openssl s_client). The decoder shows the issuer, the validity dates and the SAN list, which covers each reason above.

Learn why

In the gameLevel 12: Who do we trust?Registry of Roots, Keystone. In development

Early edition. Checked against the standards; an independent expert review is in progress. Found a mistake? Tell us.

Last reviewed Edition 0.9