Beyond SHA-1: Finding Certificates by SHA-256 and SHA-3 Thumbprints in .NET 10

Beyond SHA-1: Finding Certificates by SHA-256 and SHA-3 Thumbprints in .NET 10

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.Thumbprint is 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.Thumbprint is always SHA‑1 (for legacy reasons).
  • To compute another thumbprint, use GetCertHashString(HashAlgorithmName) or GetCertHash(HashAlgorithmName).

Minimal example: find by SHA‑256 thumbprint

C#
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.
  • FindByThumbprint returns a collection. SingleOrDefault is convenient when you expect a unique match.

Windows vs. Linux/macOS stores

  • Windows: StoreLocation.CurrentUser and StoreLocation.LocalMachine with familiar stores like My (Personal), Root (Trusted Root), CA (Intermediate), etc.
  • Linux/macOS: .NET maps to platform stores (OpenSSL / Keychain). StoreName.My exists, but content and permissions differ by distro/host.
  • If you’re deploying cross‑platform, prefer CurrentUser unless you explicitly need machine‑wide trust and have the right permissions.

Example: search LocalMachine/My on Windows

C#
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:

C#
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):
C#
# 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):
C#
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.

C#
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:

C#
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 >1 results explicitly.
  • Permissions: opening LocalMachine stores may require elevated rights in some environments.
  • Containerized Linux: system stores might be minimal; consider bundling certs or using X509Store with 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

C#
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.

Discover more from Roxeem

Subscribe now to keep reading and get access to the full archive.

Continue reading