In Windows Public Key Infrastructure (PKI), certificate validation follows a strict chain of trust. By default, Windows requires the entire chain - including the root CA - to be trusted for any end-entity certificate to be considered valid. This creates security concerns when you need to trust a specific server certificate without implicitly trusting all certificates signed by its root CA.
Consider this common scenario:
Dept-Root-CA
└── Dept-Intermediate-1
└── Server-Certificate
If you add Dept-Root-CA to the Trusted Root Certification Authorities store, Windows will automatically trust any certificate signed by this CA hierarchy. This violates the principle of least privilege when you only want to trust a specific server certificate.
Windows provides granular control through certificate stores. Here's how to trust only the server certificate:
# PowerShell: Import only the server certificate to Trusted People store
Import-Certificate -FilePath "C:\certs\server.cer" -CertStoreLocation "Cert:\LocalMachine\TrustedPeople"
For applications that need to validate this certificate:
// C# example of custom certificate validation
ServicePointManager.ServerCertificateValidationCallback +=
(sender, cert, chain, errors) =>
{
// Compare thumbprints instead of relying on chain trust
return cert.Thumbprint == "a909502dd82ae41433e6f83886b00d4277a32a7a";
};
For more security-critical applications, certificate pinning provides even stricter control:
// PowerShell: Convert certificate to public key hash
$cert = Get-PfxCertificate -FilePath "server.pfx"
$pubKeyBytes = $cert.PublicKey.EncodedKeyValue.RawData
$sha256 = [System.Security.Cryptography.SHA256]::Create()
$pubKeyHash = [System.BitConverter]::ToString($sha256.ComputeHash($pubKeyBytes))
Remember that:
- This approach requires application-level changes
- Certificate expiration and revocation still need to be handled
- Storing certificates in the Local Machine store affects all users
In Windows certificate validation, we often face a dilemma: needing to trust a specific end-entity certificate while maintaining distrust toward its root CA. The default chain validation in Windows (via CryptoAPI/CNG) inherently trusts all certificates if their root is in the Trusted Root store.
Consider this typical chain:
// Example certificate chain
Dept-Root-CA (untrusted root)
├── Dept-Intermediate-1
└── Server-Certificate (specific cert we want to trust)
Windows' default behavior will reject this chain unless Dept-Root-CA is in the Trusted Root store. Here's how to bypass this while maintaining security.
We'll use .NET's X509Chain
class with custom policy:
using System.Security.Cryptography.X509Certificates;
bool ValidateSpecificCertificate(X509Certificate2 certToVerify, byte[] trustedCertThumbprint)
{
var chain = new X509Chain
{
ChainPolicy =
{
RevocationMode = X509RevocationMode.NoCheck,
VerificationFlags = X509VerificationFlags.AllowUnknownCertificateAuthority,
ExtraStore = new X509Certificate2Collection(new X509Certificate2(trustedCertThumbprint))
}
};
return chain.Build(certToVerify)
&& chain.ChainElements
.Cast<X509ChainElement>()
.Any(x => x.Certificate.Thumbprint == trustedCertThumbprint);
}
For Win32 applications, use CertVerifyCertificateChainPolicy
with custom policy:
#include <wincrypt.h>
BOOL VerifySingleCert(PCCERT_CONTEXT pCert, PCCERT_CONTEXT pTrustedCert)
{
CERT_CHAIN_PARA ChainPara = { sizeof(ChainPara) };
PCCERT_CHAIN_CONTEXT pChainContext = NULL;
CertGetCertificateChain(NULL, pCert, NULL, NULL, &ChainPara,
CERT_CHAIN_REVOCATION_CHECK_CHAIN,
NULL, &pChainContext);
// Custom validation logic here
// Compare against pTrustedCert
// ...
CertFreeCertificateChain(pChainContext);
return TRUE;
}
Key points when implementing this approach:
- Always verify certificate expiration dates
- Consider implementing CRL/OCSP checks separately
- Store the trusted certificate thumbprint securely
- Log all validation failures for auditing
Here's how to implement this in an ASP.NET Core application:
public void ConfigureServices(IServiceCollection services)
{
services.AddCertificateForwarding(options => { });
services.AddAuthentication(CertificateAuthenticationDefaults.AuthenticationScheme)
.AddCertificate(options =>
{
options.AllowedCertificateTypes = CertificateTypes.All;
options.RevocationMode = X509RevocationMode.NoCheck;
options.Events = new CertificateAuthenticationEvents
{
OnCertificateValidated = context =>
{
var cert = context.ClientCertificate;
if (!ValidateSpecificCertificate(cert, trustedThumbprint))
{
context.Fail("Untrusted certificate");
}
return Task.CompletedTask;
}
};
});
}