Use X509CertificateLoader on .NET 9+ (#1543)

* Use X509CertificateLoader on .NET 9+

Preserve older-target certificate loading and add tests for
supported formats, passwords, and embedded certificates.

* !

* Potential fix for pull request finding 'Missed 'using' opportunity'

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>

* enum

* namespace WireMock.Compatiblity;

---------

Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
This commit is contained in:
Stef HeyenrathandCopilot Autofix powered by AI authored and GitHub committed 2026-11-12 20:56:01 +01:00
1 parent 384c083a78
commit 22576eb539
5 files changed
+146 -22

No files matched your search

@@ -2,6 +2,9 @@
using System.Security.Cryptography.X509Certificates;
using WireMock.Owin;
#if NETSTANDARD2_0
using Enum = WireMock.Compatiblity.EnumCompat;
#endif
namespace WireMock.HttpsCertificate;
@@ -18,32 +21,24 @@ internal static class CertificateLoader
{
var thumbprintOrSubjectNameOrHost = options.X509ThumbprintOrSubjectName ?? host;
var certStore = new X509Store((StoreName)Enum.Parse(typeof(StoreName), options.X509StoreName!), (StoreLocation)Enum.Parse(typeof(StoreLocation), options.X509StoreLocation!));
try
{
certStore.Open(OpenFlags.ReadOnly);
using var certStore = new X509Store(Enum.Parse<StoreName>(options.X509StoreName!), Enum.Parse<StoreLocation>(options.X509StoreLocation!));
certStore.Open(OpenFlags.ReadOnly);
// Attempt to find by Thumbprint first
var matchingCertificates = certStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprintOrSubjectNameOrHost, false);
// Attempt to find by Thumbprint first
var matchingCertificates = certStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprintOrSubjectNameOrHost, false);
if (matchingCertificates.Count == 0)
{
// Fallback to SubjectName
matchingCertificates = certStore.Certificates.Find(X509FindType.FindBySubjectName, thumbprintOrSubjectNameOrHost, false);
if (matchingCertificates.Count == 0)
{
// Fallback to SubjectName
matchingCertificates = certStore.Certificates.Find(X509FindType.FindBySubjectName, thumbprintOrSubjectNameOrHost, false);
if (matchingCertificates.Count == 0)
{
// No certificates matched the search criteria.
throw new FileNotFoundException($"No Certificate found with in store '{options.X509StoreName}', location '{options.X509StoreLocation}' for Thumbprint or SubjectName '{thumbprintOrSubjectNameOrHost}'.");
}
// No certificates matched the search criteria.
throw new FileNotFoundException($"No Certificate found with in store '{options.X509StoreName}', location '{options.X509StoreLocation}' for Thumbprint or SubjectName '{thumbprintOrSubjectNameOrHost}'.");
}
}
// Use the first matching certificate.
return matchingCertificates[0];
}
finally
{
certStore.Dispose();
certStore.Close();
}
// Use the first matching certificate.
return matchingCertificates[0];
}
if (!string.IsNullOrEmpty(options.X509CertificateFilePath))
@@ -55,10 +50,14 @@ internal static class CertificateLoader
if (!string.IsNullOrEmpty(options.X509CertificatePassword))
{
var certPem = File.ReadAllText(options.X509CertificateFilePath);
var cert = X509Certificate2.CreateFromPem(certPem, options.X509CertificatePassword);
using var cert = X509Certificate2.CreateFromPem(certPem, options.X509CertificatePassword);
const string defaultPasswordPem = "WireMock.Net";
#if NET9_0_OR_GREATER
return X509CertificateLoader.LoadPkcs12(cert.Export(X509ContentType.Pfx, defaultPasswordPem), defaultPasswordPem);
#else
return new X509Certificate2(cert.Export(X509ContentType.Pfx, defaultPasswordPem), defaultPasswordPem);
#endif
}
return X509Certificate2.CreateFromPemFile(options.X509CertificateFilePath);
#else
@@ -66,9 +65,29 @@ internal static class CertificateLoader
#endif
}
#if NET9_0_OR_GREATER
var data = File.ReadAllBytes(options.X509CertificateFilePath);
var contentType = X509Certificate2.GetCertContentType(data);
if (contentType == X509ContentType.Pfx)
{
return X509CertificateLoader.LoadPkcs12(data, string.IsNullOrEmpty(options.X509CertificatePassword) ? null : options.X509CertificatePassword);
}
if (contentType == X509ContentType.Cert)
{
return X509CertificateLoader.LoadCertificate(data);
}
// The loader does not support legacy formats such as PKCS#7 and serialized certificates.
#pragma warning disable SYSLIB0057
return !string.IsNullOrEmpty(options.X509CertificatePassword) ?
new X509Certificate2(data, options.X509CertificatePassword) :
new X509Certificate2(data);
#pragma warning restore SYSLIB0057
#else
return !string.IsNullOrEmpty(options.X509CertificatePassword) ?
new X509Certificate2(options.X509CertificateFilePath, options.X509CertificatePassword) :
new X509Certificate2(options.X509CertificateFilePath);
#endif
}
if (options.X509Certificate != null)
@@ -89,6 +89,10 @@ wTM1Z+CJZG9xAcf1zAVGl4ggYyYEFGBFyJ8VBwijS2zy1qwN1WYGtcWoAgIH0A==
public static X509Certificate2 GetX509Certificate2()
{
byte[] data = Convert.FromBase64String(Data);
#if NET9_0_OR_GREATER
return X509CertificateLoader.LoadPkcs12(data, null);
#else
return new X509Certificate2(data);
#endif
}
}
@@ -0,0 +1,10 @@
// Copyright © WireMock.Net
#if NETSTANDARD2_0
namespace WireMock.Compatiblity;
internal static class EnumCompat
{
public static TEnum Parse<TEnum>(string value) where TEnum : struct, Enum => (TEnum)Enum.Parse(typeof(TEnum), value);
}
#endif
@@ -0,0 +1,87 @@
// Copyright © WireMock.Net
#if NET8_0_OR_GREATER
using System.Security.Cryptography;
using System.Security.Cryptography.X509Certificates;
using WireMock.HttpsCertificate;
using WireMock.Owin;
namespace WireMock.Net.Tests.HttpsCertificate;
public class CertificateLoaderTests
{
[Theory]
[InlineData("pfx", null, true)]
[InlineData("pfx", "password", true)]
[InlineData("cer", null, false)]
[InlineData("cer", "password", false)]
[InlineData("pem", null, true)]
[InlineData("pem", "private-key", true)]
public void LoadCertificate_LoadsSupportedFormats(string format, string? password, bool hasPrivateKey)
{
using var key = RSA.Create(2048);
var request = new CertificateRequest("CN=localhost", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
using var original = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1));
var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.{format}");
try
{
if (format == "pem")
{
var pem = original.ExportCertificatePem();
File.WriteAllText(path, password == null ? pem + "\n" + key.ExportPkcs8PrivateKeyPem() : pem);
}
else
{
var contentType = format == "pfx" ? X509ContentType.Pfx : X509ContentType.Cert;
File.WriteAllBytes(path, original.Export(contentType, format == "pfx" ? password : null));
}
var options = new WireMockMiddlewareOptions
{
X509CertificateFilePath = path,
X509CertificatePassword = format == "pem" && password != null ? key.ExportPkcs8PrivateKeyPem() : password
};
using var loaded = CertificateLoader.LoadCertificate(options, "localhost");
loaded.Thumbprint.Should().Be(original.Thumbprint);
loaded.HasPrivateKey.Should().Be(hasPrivateKey);
}
finally
{
File.Delete(path);
}
}
[Fact]
public void LoadCertificate_InvalidPfxPassword_Throws()
{
using var key = RSA.Create(2048);
var request = new CertificateRequest("CN=localhost", key, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
using var original = request.CreateSelfSigned(DateTimeOffset.UtcNow.AddDays(-1), DateTimeOffset.UtcNow.AddDays(1));
var path = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid()}.pfx");
try
{
File.WriteAllBytes(path, original.Export(X509ContentType.Pfx, "password"));
var options = new WireMockMiddlewareOptions { X509CertificateFilePath = path, X509CertificatePassword = "wrong" };
Action load = () => CertificateLoader.LoadCertificate(options, "localhost");
load.Should().Throw<CryptographicException>();
}
finally
{
File.Delete(path);
}
}
[Fact]
public void GetX509Certificate2_LoadsEmbeddedCertificate()
{
using var certificate = PublicCertificateHelper.GetX509Certificate2();
certificate.HasPrivateKey.Should().BeTrue();
certificate.Thumbprint.Should().NotBeNullOrEmpty();
}
}
#endif
@@ -35,8 +35,12 @@ public partial class WireMockServerTests
: 403
}));
#if NET9_0_OR_GREATER
var certificates = X509CertificateLoader.LoadPkcs12CollectionFromFile("client_cert.pfx", "1234", X509KeyStorageFlags.Exportable);
#else
var certificates = new X509Certificate2Collection();
certificates.Import("client_cert.pfx", "1234", X509KeyStorageFlags.Exportable);
#endif
var httpMessageHandler = new HttpClientHandler
{