In this post, we’ll dig into a small but mighty change in .NET 10’s cryptography APIs: finding certificates by thumbprints using algorithms other than SHA‑1. If you’ve ever needed to key off a certificate’s SHA‑256 thumbprint, you know the old API only searched SHA‑1. That mismatch led to brittle workarounds and confusion.
We’ll cover why this matters, what’s new in .NET 10, how to use it across platforms, and how to fall back cleanly on older target frameworks.
What problem does this solve?
- Historically, X509 “thumbprint” meant the SHA‑1 hash of the certificate bytes. The property
X509Certificate2.Thumbprintis always SHA‑1. - The widely used
X509Certificate2Collection.Find(X509FindType.FindByThumbprint, ...)also only matched SHA‑1 values. - Many orgs now index or share certificate identifiers as SHA‑256 (or even SHA‑3) thumbprints. Prior to .NET 10, you had to enumerate stores and compare
GetCertHash(HashAlgorithmName.SHA256)yourself.
With .NET 10, you can ask the framework to find certificates by the hash algorithm you specify. That keeps your code correct, fast, and much easier to read.
What’s new in .NET 10
.NET 10 adds overloads to X509Certificate2Collection:
FindByThumbprint(HashAlgorithmName hashAlgorithm, string thumbprintHex)FindByThumbprint(HashAlgorithmName hashAlgorithm, ReadOnlySpan<char> thumbprintHex)FindByThumbprint(HashAlgorithmName hashAlgorithm, ReadOnlySpan<byte> thumbprintBytes)
These search the collection for certificates whose hash (computed with the requested algorithm) matches the input.
Why it’s safer: SHA‑256 and SHA‑3‑256 are the same length; trying to overload the old Find with ambiguous lengths is risky. The new API removes ambiguity by requiring the algorithm.
Quick refresher: “thumbprint” vs. algorithm
- Thumbprint = hash of the certificate’s DER bytes.
X509Certificate2.Thumbprintis always SHA‑1 (for legacy reasons).- To compute another thumbprint, use
GetCertHashString(HashAlgorithmName)orGetCertHash(HashAlgorithmName).
Minimal example: find by SHA‑256 thumbprint
using System;
using System.Diagnostics;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
public static class CertLookup
{
public static X509Certificate2? FindBySha256Thumbprint(
string thumbprintHex,
StoreName storeName = StoreName.My,
StoreLocation storeLocation = StoreLocation.CurrentUser)
{
// Normalize: remove whitespace/colons, upper/lower doesn’t matter.
string normalized = NormalizeHex(thumbprintHex);
using var store = new X509Store(storeName, storeLocation);
store.Open(OpenFlags.ReadOnly);
X509Certificate2Collection coll = store.Certificates.FindByThumbprint(
HashAlgorithmName.SHA256,
normalized);
Debug.Assert(coll.Count < 2, "Multiple matches for a SHA-256 thumbprint would be surprising.");
return coll.Cast<X509Certificate2>().SingleOrDefault();
}
private static string NormalizeHex(string hex)
{
return new string(hex.Where(c => !char.IsWhiteSpace(c) && c != ':' && c != '-').ToArray());
}
}
Notes
- The API accepts hex input. Normalize by stripping spaces/colons/dashes to avoid format surprises.
FindByThumbprintreturns a collection.SingleOrDefaultis convenient when you expect a unique match.
Windows vs. Linux/macOS stores
- Windows:
StoreLocation.CurrentUserandStoreLocation.LocalMachinewith familiar stores likeMy(Personal),Root(Trusted Root),CA(Intermediate), etc. - Linux/macOS: .NET maps to platform stores (OpenSSL / Keychain).
StoreName.Myexists, but content and permissions differ by distro/host. - If you’re deploying cross‑platform, prefer
CurrentUserunless you explicitly need machine‑wide trust and have the right permissions.
Example: search LocalMachine/My on Windows
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
X509Certificate2? cert = CertLookup.FindBySha256Thumbprint(
thumbprintHex: "80:7A:CF:45:...:9E", // from MMC/PowerShell/openssl
storeName: StoreName.My,
storeLocation: StoreLocation.LocalMachine);
Verifying a thumbprint value
If you only have the certificate file, compute the thumbprint you intend to use:
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
var cert = new X509Certificate2("mycert.cer");
string sha256Hex = cert.GetCertHashString(HashAlgorithmName.SHA256);
// Optionally: compare sha256Hex to a known value or use it to find the installed instance.
Common sources of truth
- Windows MMC: Details -> Thumbprint shows SHA‑1 by default. For SHA‑256, compute in code or via PowerShell.
- PowerShell (Windows):
# SHA-1 (Thumbprint property):
Get-ChildItem Cert:\CurrentUser\My | Select-Object Subject, Thumbprint
# SHA-256 via .NET:
Get-ChildItem Cert:\CurrentUser\My |
ForEach-Object {
[PSCustomObject]@{
Subject = $_.Subject
SHA256 = ([System.Convert]::ToHexString($_.GetCertHash([System.Security.Cryptography.HashAlgorithmName]::SHA256)))
}
}
- OpenSSL (Linux/macOS/Windows):
openssl x509 -in mycert.pem -noout -fingerprint -sha256
# Output like: SHA256 Fingerprint=AA:BB:CC:...
# Strip colons when using with FindByThumbprint.
Fallback for .NET ≤ 9: manual enumeration
If you can’t target .NET 10 yet, you can emulate the behavior by hashing each candidate certificate yourself.
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
public static class LegacyCertLookup
{
public static X509Certificate2? FindByThumbprint(
HashAlgorithmName alg,
string thumbprintHex,
StoreName storeName = StoreName.My,
StoreLocation storeLocation = StoreLocation.CurrentUser)
{
var normalized = new string(thumbprintHex.Where(c => !char.IsWhiteSpace(c) && c != ':' && c != '-').ToArray());
using var store = new X509Store(storeName, storeLocation);
store.Open(OpenFlags.ReadOnly);
foreach (var cert in store.Certificates.Cast<X509Certificate2>())
{
string candidate = cert.GetCertHashString(alg);
if (candidate.Equals(normalized, StringComparison.OrdinalIgnoreCase))
{
return cert;
}
}
return null;
}
}
For multi‑targeted libraries, you can prefer the new API when available:
public static X509Certificate2? FindPortable(HashAlgorithmName alg, string thumbprintHex, StoreName sn, StoreLocation sl)
{
#if NET10_0_OR_GREATER
using var store = new X509Store(sn, sl);
store.Open(OpenFlags.ReadOnly);
var coll = store.Certificates.FindByThumbprint(alg, thumbprintHex);
return coll.Cast<X509Certificate2>().SingleOrDefault();
#else
return LegacyCertLookup.FindByThumbprint(alg, thumbprintHex, sn, sl);
#endif
}
Practical tips and edge cases
- Input normalization: remove whitespace, colons, and dashes; compare case‑insensitively.
- Multiple matches: extremely unlikely with SHA‑256/3, but handle
0,1, or>1results explicitly. - Permissions: opening
LocalMachinestores may require elevated rights in some environments. - Containerized Linux: system stores might be minimal; consider bundling certs or using
X509Storewith custom stores. - Performance: open the store once per operation; avoid repeated opens in tight loops. The new API performs the hash comparison internally and efficiently.
- Security: thumbprints identify certs; they don’t validate trust or revocation. Still validate chain and EKU as appropriate for your scenario.
End-to-end sample: pick best available cert by SHA‑256, fallback to subject
using System;
using System.Linq;
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
public static class ClientCertificateSelector
{
public static X509Certificate2? Select(string preferredSha256Thumbprint, string subjectCn)
{
// Try exact SHA-256 match first
#if NET10_0_OR_GREATER
var store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
store.Open(OpenFlags.ReadOnly);
var coll = store.Certificates.FindByThumbprint(HashAlgorithmName.SHA256, preferredSha256Thumbprint);
var match = coll.Cast<X509Certificate2>().SingleOrDefault();
store.Close();
#else
var match = LegacyCertLookup.FindByThumbprint(HashAlgorithmName.SHA256, preferredSha256Thumbprint);
#endif
if (match != null)
{
return match;
}
// Fallback: choose newest cert that matches subject
using var fallbackStore = new X509Store(StoreName.My, StoreLocation.CurrentUser);
fallbackStore.Open(OpenFlags.ReadOnly);
return fallbackStore.Certificates
.Cast<X509Certificate2>()
.Where(c => c.SubjectName.Name?.Contains($"CN={subjectCn}", StringComparison.OrdinalIgnoreCase) == true)
.OrderByDescending(c => c.NotBefore)
.FirstOrDefault();
}
}
Why you should adopt this now
- Aligns with modern security baselines that avoid SHA‑1.
- Eliminates custom enumeration code and reduces bugs.
- Clear intent: the algorithm is explicit at the call site.
References
- What’s new in .NET 10 – Cryptography: Find certificates by thumbprints other than SHA‑1
- X509Certificate2Collection.FindByThumbprint: API reference
- X509Certificate.GetCertHashString(HashAlgorithmName):
- Legacy X509FindType.FindByThumbprint: API reference
Conclusion
The new FindByThumbprint overloads in .NET 10 make it straightforward to locate certificates by SHA‑256 or SHA‑3 thumbprints—no more DIY enumeration. Use them directly where you can, and keep a small polyfill for older target frameworks. Your future self (and your security reviewers) will thank you.

