System.Security.Cryptography.X509Certificates is the .NET namespace for everything certificate-shaped: loading and inspecting X.509 certificates, querying platform certificate stores, building and validating trust chains, and generating certificates and CSRs in code. It is also a namespace where the convenient path and the secure path have historically diverged — enough that .NET 9 obsoleted the most-used constructors in it. This guide covers the types you will actually touch, the .NET 9 loading changes, where System.Security.Cryptography.Pkcs fits in, and the handful of patterns that separate a hardened TLS setup from one that only looks like it.
The cast of types
X509Certificate2— the certificate object you work with everywhere: subject, issuer, validity dates, extensions, and (optionally) an associated private key. Its ancestorX509Certificatesurvives mainly for legacy signatures; new code should not use it directly.X509Store— access to the platform certificate stores (CurrentUser/LocalMachine, store names likeMy,Root,CA). On Linux the store model is emulated and rooted in the system CA bundle, so behavior differs across OSes more than most code assumes.X509Chain— chain building and validation: given a leaf, walk to a trusted root, checking signatures, validity periods, revocation, and policy.CertificateRequest— programmatic CSR and certificate generation (self-signed or CA-signed), available since .NET Core 2.0. This is how you mint test certificates in code instead of shelling out toopenssl.X509Certificate2Collection— bags of certificates, withFindmethods for thumbprint, subject, and so on.
Loading certificates: the .NET 9 change you must know
For years, the idiom was new X509Certificate2(bytes) or new X509Certificate2(path, password). Those constructors are content-sniffing: they accept DER, PEM-ish content, PKCS#7, or PKCS#12/PFX and decide what you gave them. That flexibility is a security problem — attacker-supplied "certificate" bytes could be a full PKCS#12 payload (with key material and import side effects) where the code only intended to parse a public certificate.
Starting in .NET 9, the byte-array, span, and file-path constructors on X509Certificate and X509Certificate2 are obsolete (compiler warning SYSLIB0057). The replacement is the intent-explicit X509CertificateLoader:
using System.Security.Cryptography.X509Certificates;
// Exactly one certificate, X.509 DER only — will not silently accept PFX
X509Certificate2 cert = X509CertificateLoader.LoadCertificate(derBytes);
X509Certificate2 fromFile = X509CertificateLoader.LoadCertificateFromFile("server.cer");
// PKCS#12/PFX, stated explicitly
X509Certificate2 pfx = X509CertificateLoader.LoadPkcs12FromFile(
"server.pfx", password, X509KeyStorageFlags.EphemeralKeySet);
You choose LoadCertificate or LoadPkcs12 (and the collection variants) up front; mismatched content fails instead of being creatively interpreted. On .NET Framework and .NET Standard, the same API ships in the Microsoft.Bcl.Cryptography package. PEM content has its own dedicated APIs — X509Certificate2.CreateFromPemFile and CreateFromPem — which pair a cert with its PEM-encoded key.
Migration advice: treat SYSLIB0057 as a real finding, not noise to suppress. Every suppressed call site is a place where content-type confusion remains possible.
Validating chains properly
TLS clients get chain validation from SslStream/HttpClient for free. Where teams write their own validation — mTLS services validating client certificates, webhook signature verification, artifact signing — X509Chain is the tool:
using var chain = new X509Chain();
chain.ChainPolicy.RevocationMode = X509RevocationMode.Online;
chain.ChainPolicy.RevocationFlag = X509RevocationFlag.ExcludeRoot;
chain.ChainPolicy.VerificationFlags = X509VerificationFlags.NoFlag;
bool valid = chain.Build(clientCert);
if (!valid)
{
foreach (var status in chain.ChainStatus)
logger.LogWarning("Chain error: {Status} {Info}", status.Status, status.StatusInformation);
}
Points that matter in production:
- Pinning to a private CA: set
chain.ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrustand add your root toCustomTrustStore. This validates against only your CA — the correct way to do internal mTLS, and far safer than comparing thumbprints by hand. - Revocation is a decision, not a default you can ignore.
Onlineadds latency and an availability dependency on CRL/OCSP endpoints;NoCheckmeans a compromised-and-revoked certificate still validates. Choose per threat model and document it. Buildreturning true is the only success signal. Checkingchain.ChainStatus.Length == 0after ignoring the return value is a recurring bug.
The anti-pattern that undoes everything
The most damaging certificate code in the .NET ecosystem is one line:
// NEVER ship this
handler.ServerCertificateCustomValidationCallback = (msg, cert, chain, errors) => true;
Accepting all server certificates turns TLS into unauthenticated encryption — any on-path attacker can present any certificate and read the traffic. It gets committed as a "temporary" fix for internal endpoints with private CAs, then ships. The correct fixes are: install the private root into the trust store, or use CustomRootTrust validation as above, or (narrowest case) compare against a known certificate explicitly. Static analyzers flag this pattern, and it is worth a zero-tolerance policy in code review; runtime testing with a DAST tool against a proxy can also catch services that accept forged certificates in practice.
The sibling anti-patterns: X509VerificationFlags.AllowUnknownCertificateAuthority in production code, and mTLS "validation" that only checks the subject string without building the chain.
Where System.Security.Cryptography.Pkcs fits
System.Security.Cryptography.Pkcs (shipped as a NuGet package of the same name) covers the PKCS standards that surround certificates rather than the certificates themselves:
SignedCms— PKCS#7/CMS detached and attached signatures: verifying signed payloads, code-signing-style flows, S/MIME building blocks.EnvelopedCms— CMS encryption to certificate recipients.Pkcs12Builder/Pkcs12Info— constructing and inspecting PFX files with control over encryption and MAC parameters, instead of acceptingX509Certificate2export defaults.Rfc3161TimestampRequest— trusted timestamping, the piece that keeps signatures verifiable after the signing certificate expires.
A useful security note: when verifying CMS signatures with SignedCms.CheckSignature, understand that it verifies the cryptographic signature — deciding whether the signer's certificate chains to a CA you trust is your job, via X509Chain. Verification without a trust decision is half a control.
Operational hygiene: keys, stores, expiry
- Prefer
EphemeralKeySetwhen loading PFX files you do not need persisted; the default behavior can write key files to disk (user profile or machine key store) that outlive the process and become audit findings. - Never load certificates with private keys from world-readable paths or bake PFX files into container images. Mount them at runtime from a secrets manager; an image containing
server.pfxplus its password in an env var is a common finding in image scans — the kind of secrets-in-artifact issue SCA and container scanning surfaces automatically. Safeguard, for instance, flags private key material discovered in image layers for exactly this reason. - Watch expiry programmatically.
cert.NotAfteris trivially queryable; a scheduled check that alerts 30 days out is an hour of work and prevents the most common self-inflicted outage in TLS operations. - Cross-platform stores differ.
StoreName.Rooton Linux maps onto the system CA bundle semantics, and writing to stores may behave differently than on Windows. If your product ships cross-platform, test store interactions on each OS rather than extrapolating from Windows.
FAQ
What replaced the X509Certificate2 constructors in .NET 9?
X509CertificateLoader, with explicit methods per content type: LoadCertificate/LoadCertificateFromFile for X.509 content and LoadPkcs12/LoadPkcs12FromFile (plus collection variants) for PFX. The old byte/path constructors trigger warning SYSLIB0057 because they guessed the content format, which enabled content-type confusion.
What is the difference between System.Security.Cryptography.X509Certificates and System.Security.Cryptography.Pkcs?
The X509Certificates namespace handles certificates, stores, and chain validation. The Pkcs package handles the surrounding message formats — CMS/PKCS#7 signing (SignedCms), encryption (EnvelopedCms), and PKCS#12 construction (Pkcs12Builder).
How do I validate a certificate against my own private CA in .NET?
Use X509Chain with ChainPolicy.TrustMode = X509ChainTrustMode.CustomRootTrust and add your root certificate to ChainPolicy.CustomTrustStore. This validates the full chain against only your CA, unlike thumbprint comparisons or subject-string checks.
Is returning true from ServerCertificateCustomValidationCallback ever acceptable?
Only in throwaway local development, and even then a custom trust store is better. In any shipped code it disables server authentication entirely, allowing man-in-the-middle interception of the connection.