mirror of
https://github.com/wiremock/WireMock.Net.git
synced 2026-11-13 12:23:42 +01:00
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:
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
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user