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 System.Security.Cryptography.X509Certificates;
|
||||||
using WireMock.Owin;
|
using WireMock.Owin;
|
||||||
|
#if NETSTANDARD2_0
|
||||||
|
using Enum = WireMock.Compatiblity.EnumCompat;
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace WireMock.HttpsCertificate;
|
namespace WireMock.HttpsCertificate;
|
||||||
|
|
||||||
@@ -18,32 +21,24 @@ internal static class CertificateLoader
|
|||||||
{
|
{
|
||||||
var thumbprintOrSubjectNameOrHost = options.X509ThumbprintOrSubjectName ?? host;
|
var thumbprintOrSubjectNameOrHost = options.X509ThumbprintOrSubjectName ?? host;
|
||||||
|
|
||||||
var certStore = new X509Store((StoreName)Enum.Parse(typeof(StoreName), options.X509StoreName!), (StoreLocation)Enum.Parse(typeof(StoreLocation), options.X509StoreLocation!));
|
using var certStore = new X509Store(Enum.Parse<StoreName>(options.X509StoreName!), Enum.Parse<StoreLocation>(options.X509StoreLocation!));
|
||||||
try
|
certStore.Open(OpenFlags.ReadOnly);
|
||||||
{
|
|
||||||
certStore.Open(OpenFlags.ReadOnly);
|
|
||||||
|
|
||||||
// Attempt to find by Thumbprint first
|
// Attempt to find by Thumbprint first
|
||||||
var matchingCertificates = certStore.Certificates.Find(X509FindType.FindByThumbprint, thumbprintOrSubjectNameOrHost, false);
|
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)
|
if (matchingCertificates.Count == 0)
|
||||||
{
|
{
|
||||||
// Fallback to SubjectName
|
// No certificates matched the search criteria.
|
||||||
matchingCertificates = certStore.Certificates.Find(X509FindType.FindBySubjectName, thumbprintOrSubjectNameOrHost, false);
|
throw new FileNotFoundException($"No Certificate found with in store '{options.X509StoreName}', location '{options.X509StoreLocation}' for Thumbprint or SubjectName '{thumbprintOrSubjectNameOrHost}'.");
|
||||||
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}'.");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Use the first matching certificate.
|
// Use the first matching certificate.
|
||||||
return matchingCertificates[0];
|
return matchingCertificates[0];
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
certStore.Dispose();
|
|
||||||
certStore.Close();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!string.IsNullOrEmpty(options.X509CertificateFilePath))
|
if (!string.IsNullOrEmpty(options.X509CertificateFilePath))
|
||||||
@@ -55,10 +50,14 @@ internal static class CertificateLoader
|
|||||||
if (!string.IsNullOrEmpty(options.X509CertificatePassword))
|
if (!string.IsNullOrEmpty(options.X509CertificatePassword))
|
||||||
{
|
{
|
||||||
var certPem = File.ReadAllText(options.X509CertificateFilePath);
|
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";
|
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);
|
return new X509Certificate2(cert.Export(X509ContentType.Pfx, defaultPasswordPem), defaultPasswordPem);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
return X509Certificate2.CreateFromPemFile(options.X509CertificateFilePath);
|
return X509Certificate2.CreateFromPemFile(options.X509CertificateFilePath);
|
||||||
#else
|
#else
|
||||||
@@ -66,9 +65,29 @@ internal static class CertificateLoader
|
|||||||
#endif
|
#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) ?
|
return !string.IsNullOrEmpty(options.X509CertificatePassword) ?
|
||||||
new X509Certificate2(options.X509CertificateFilePath, options.X509CertificatePassword) :
|
new X509Certificate2(options.X509CertificateFilePath, options.X509CertificatePassword) :
|
||||||
new X509Certificate2(options.X509CertificateFilePath);
|
new X509Certificate2(options.X509CertificateFilePath);
|
||||||
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
if (options.X509Certificate != null)
|
if (options.X509Certificate != null)
|
||||||
|
|||||||
@@ -89,6 +89,10 @@ wTM1Z+CJZG9xAcf1zAVGl4ggYyYEFGBFyJ8VBwijS2zy1qwN1WYGtcWoAgIH0A==
|
|||||||
public static X509Certificate2 GetX509Certificate2()
|
public static X509Certificate2 GetX509Certificate2()
|
||||||
{
|
{
|
||||||
byte[] data = Convert.FromBase64String(Data);
|
byte[] data = Convert.FromBase64String(Data);
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
return X509CertificateLoader.LoadPkcs12(data, null);
|
||||||
|
#else
|
||||||
return new X509Certificate2(data);
|
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
|
: 403
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
#if NET9_0_OR_GREATER
|
||||||
|
var certificates = X509CertificateLoader.LoadPkcs12CollectionFromFile("client_cert.pfx", "1234", X509KeyStorageFlags.Exportable);
|
||||||
|
#else
|
||||||
var certificates = new X509Certificate2Collection();
|
var certificates = new X509Certificate2Collection();
|
||||||
certificates.Import("client_cert.pfx", "1234", X509KeyStorageFlags.Exportable);
|
certificates.Import("client_cert.pfx", "1234", X509KeyStorageFlags.Exportable);
|
||||||
|
#endif
|
||||||
|
|
||||||
var httpMessageHandler = new HttpClientHandler
|
var httpMessageHandler = new HttpClientHandler
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in new issue
Block a user