Add option to provide X509Certificate (#1303)

* Add option to provide X509CertificateRawData

* X509Certificate

* remove X509CertificateRawData
This commit is contained in:
Stef Heyenrath
2025-05-28 07:15:46 +02:00
committed by GitHub
parent 3438539138
commit 86d4717216
8 changed files with 138 additions and 123 deletions

View File

@@ -3,6 +3,7 @@
using System;
using System.IO;
using System.Security.Cryptography.X509Certificates;
using WireMock.Owin;
namespace WireMock.HttpsCertificate;
@@ -13,19 +14,13 @@ internal static class CertificateLoader
/// <summary>
/// Used by the WireMock.Net server
/// </summary>
public static X509Certificate2 LoadCertificate(
string? storeName,
string? storeLocation,
string? thumbprintOrSubjectName,
string? filePath,
string? passwordOrKey,
string host)
public static X509Certificate2 LoadCertificate(IWireMockMiddlewareOptions options, string host)
{
if (!string.IsNullOrEmpty(storeName) && !string.IsNullOrEmpty(storeLocation))
if (!string.IsNullOrEmpty(options.X509StoreName) && !string.IsNullOrEmpty(options.X509StoreLocation))
{
var thumbprintOrSubjectNameOrHost = thumbprintOrSubjectName ?? host;
var thumbprintOrSubjectNameOrHost = options.X509ThumbprintOrSubjectName ?? host;
var certStore = new X509Store((StoreName)Enum.Parse(typeof(StoreName), storeName), (StoreLocation)Enum.Parse(typeof(StoreLocation), storeLocation));
var certStore = new X509Store((StoreName)Enum.Parse(typeof(StoreName), options.X509StoreName!), (StoreLocation)Enum.Parse(typeof(StoreLocation), options.X509StoreLocation!));
try
{
certStore.Open(OpenFlags.ReadOnly);
@@ -39,7 +34,7 @@ internal static class CertificateLoader
if (matchingCertificates.Count == 0)
{
// No certificates matched the search criteria.
throw new FileNotFoundException($"No Certificate found with in store '{storeName}', location '{storeLocation}' for Thumbprint or SubjectName '{thumbprintOrSubjectNameOrHost}'.");
throw new FileNotFoundException($"No Certificate found with in store '{options.X509StoreName}', location '{options.X509StoreLocation}' for Thumbprint or SubjectName '{thumbprintOrSubjectNameOrHost}'.");
}
}
@@ -56,27 +51,28 @@ internal static class CertificateLoader
}
}
if (!string.IsNullOrEmpty(filePath))
if (!string.IsNullOrEmpty(options.X509CertificateFilePath))
{
if (filePath!.EndsWith(ExtensionPem, StringComparison.OrdinalIgnoreCase))
if (options.X509CertificateFilePath.EndsWith(ExtensionPem, StringComparison.OrdinalIgnoreCase))
{
// PEM logic based on: https://www.scottbrady91.com/c-sharp/pem-loading-in-dotnet-core-and-dotnet
#if NET5_0_OR_GREATER
if (!string.IsNullOrEmpty(passwordOrKey))
if (!string.IsNullOrEmpty(options.X509CertificatePassword))
{
var certPem = File.ReadAllText(filePath);
var cert = X509Certificate2.CreateFromPem(certPem, passwordOrKey);
var certPem = File.ReadAllText(options.X509CertificateFilePath);
var cert = X509Certificate2.CreateFromPem(certPem, options.X509CertificatePassword);
const string defaultPasswordPem = "WireMock.Net";
return new X509Certificate2(cert.Export(X509ContentType.Pfx, defaultPasswordPem), defaultPasswordPem);
}
return X509Certificate2.CreateFromPemFile(filePath);
return X509Certificate2.CreateFromPemFile(options.X509CertificateFilePath);
#elif NETCOREAPP3_1
var cert = new X509Certificate2(filePath);
if (!string.IsNullOrEmpty(passwordOrKey))
var cert = new X509Certificate2(options.X509CertificateFilePath);
if (!string.IsNullOrEmpty(options.X509CertificatePassword))
{
var key = System.Security.Cryptography.ECDsa.Create()!;
key.ImportECPrivateKey(System.Text.Encoding.UTF8.GetBytes(passwordOrKey), out _);
key.ImportECPrivateKey(System.Text.Encoding.UTF8.GetBytes(options.X509CertificatePassword), out _);
return cert.CopyWithPrivateKey(key);
}
return cert;
@@ -85,10 +81,17 @@ internal static class CertificateLoader
#endif
}
return !string.IsNullOrEmpty(passwordOrKey) ? new X509Certificate2(filePath, passwordOrKey) : new X509Certificate2(filePath);
return !string.IsNullOrEmpty(options.X509CertificatePassword) ?
new X509Certificate2(options.X509CertificateFilePath, options.X509CertificatePassword) :
new X509Certificate2(options.X509CertificateFilePath);
}
throw new InvalidOperationException("X509StoreName and X509StoreLocation OR X509CertificateFilePath are mandatory. Note that X509CertificatePassword is optional.");
if (options.X509Certificate != null)
{
return options.X509Certificate;
}
throw new InvalidOperationException("X509StoreName and X509StoreLocation OR X509CertificateFilePath OR X509Certificate are mandatory. Note that X509CertificatePassword is optional.");
}
/// <summary>

View File

@@ -11,113 +11,105 @@ using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using CertificateLoader = WireMock.HttpsCertificate.CertificateLoader;
namespace WireMock.Owin
namespace WireMock.Owin;
internal partial class AspNetCoreSelfHost
{
internal partial class AspNetCoreSelfHost
private static void SetKestrelOptionsLimits(KestrelServerOptions options)
{
private static void SetKestrelOptionsLimits(KestrelServerOptions options)
{
options.Limits.MaxRequestBodySize = null; // https://stackoverflow.com/questions/46738364/increase-upload-request-length-limit-in-kestrel
options.Limits.MaxRequestBufferSize = null;
options.Limits.MaxRequestHeaderCount = 100;
options.Limits.MaxResponseBufferSize = null;
}
options.Limits.MaxRequestBodySize = null; // https://stackoverflow.com/questions/46738364/increase-upload-request-length-limit-in-kestrel
options.Limits.MaxRequestBufferSize = null;
options.Limits.MaxRequestHeaderCount = 100;
options.Limits.MaxResponseBufferSize = null;
}
private static void SetHttpsAndUrls(KestrelServerOptions kestrelOptions, IWireMockMiddlewareOptions wireMockMiddlewareOptions, IEnumerable<HostUrlDetails> urlDetails)
private static void SetHttpsAndUrls(KestrelServerOptions kestrelOptions, IWireMockMiddlewareOptions wireMockMiddlewareOptions, IEnumerable<HostUrlDetails> urlDetails)
{
foreach (var urlDetail in urlDetails)
{
foreach (var urlDetail in urlDetails)
if (urlDetail.IsHttps)
{
if (urlDetail.IsHttps)
Listen(kestrelOptions, urlDetail, listenOptions =>
{
Listen(kestrelOptions, urlDetail, listenOptions =>
listenOptions.UseHttps(options =>
{
listenOptions.UseHttps(options =>
if (wireMockMiddlewareOptions.CustomCertificateDefined)
{
if (wireMockMiddlewareOptions.CustomCertificateDefined)
{
options.ServerCertificate = CertificateLoader.LoadCertificate(
wireMockMiddlewareOptions.X509StoreName,
wireMockMiddlewareOptions.X509StoreLocation,
wireMockMiddlewareOptions.X509ThumbprintOrSubjectName,
wireMockMiddlewareOptions.X509CertificateFilePath,
wireMockMiddlewareOptions.X509CertificatePassword,
urlDetail.Host
);
}
options.ServerCertificate = CertificateLoader.LoadCertificate(wireMockMiddlewareOptions, urlDetail.Host);
}
options.ClientCertificateMode = (ClientCertificateMode)wireMockMiddlewareOptions.ClientCertificateMode;
if (wireMockMiddlewareOptions.AcceptAnyClientCertificate)
{
options.ClientCertificateValidation = (_, _, _) => true;
}
});
if (urlDetail.IsHttp2)
options.ClientCertificateMode = (ClientCertificateMode)wireMockMiddlewareOptions.ClientCertificateMode;
if (wireMockMiddlewareOptions.AcceptAnyClientCertificate)
{
listenOptions.Protocols = HttpProtocols.Http2;
options.ClientCertificateValidation = (_, _, _) => true;
}
});
continue;
}
if (urlDetail.IsHttp2)
{
Listen(kestrelOptions, urlDetail, listenOptions =>
if (urlDetail.IsHttp2)
{
listenOptions.Protocols = HttpProtocols.Http2;
});
continue;
}
Listen(kestrelOptions, urlDetail, _ => { });
}
});
continue;
}
}
private static void Listen(KestrelServerOptions kestrelOptions, HostUrlDetails urlDetail, Action<ListenOptions> configure)
{
// Listens on any IP with the given port.
if (urlDetail is { Port: > 0, Host: "0.0.0.0" })
if (urlDetail.IsHttp2)
{
kestrelOptions.ListenAnyIP(urlDetail.Port, configure);
return;
}
// Listens on ::1 and 127.0.0.1 with the given port.
if (urlDetail is { Port: > 0, Host: "localhost" or "127.0.0.1" or "::1" })
{
kestrelOptions.ListenLocalhost(urlDetail.Port, configure);
return;
Listen(kestrelOptions, urlDetail, listenOptions =>
{
listenOptions.Protocols = HttpProtocols.Http2;
});
continue;
}
// Try to parse the host as a valid IP address and bind to the given IP address and port.
if (IPAddress.TryParse(urlDetail.Host, out var ipAddress))
{
kestrelOptions.Listen(ipAddress, urlDetail.Port, configure);
return;
}
// Otherwise, listen on all IPs.
kestrelOptions.ListenAnyIP(urlDetail.Port, configure);
Listen(kestrelOptions, urlDetail, _ => { });
}
}
internal static class IWebHostBuilderExtensions
private static void Listen(KestrelServerOptions kestrelOptions, HostUrlDetails urlDetail, Action<ListenOptions> configure)
{
internal static IWebHostBuilder ConfigureAppConfigurationUsingEnvironmentVariables(this IWebHostBuilder builder)
// Listens on any IP with the given port.
if (urlDetail is { Port: > 0, Host: "0.0.0.0" })
{
return builder.ConfigureAppConfiguration(config =>
{
config.AddEnvironmentVariables();
});
kestrelOptions.ListenAnyIP(urlDetail.Port, configure);
return;
}
internal static IWebHostBuilder ConfigureKestrelServerOptions(this IWebHostBuilder builder)
// Listens on ::1 and 127.0.0.1 with the given port.
if (urlDetail is { Port: > 0, Host: "localhost" or "127.0.0.1" or "::1" })
{
return builder.ConfigureServices((context, services) =>
{
services.Configure<KestrelServerOptions>(context.Configuration.GetSection("Kestrel"));
});
kestrelOptions.ListenLocalhost(urlDetail.Port, configure);
return;
}
// Try to parse the host as a valid IP address and bind to the given IP address and port.
if (IPAddress.TryParse(urlDetail.Host, out var ipAddress))
{
kestrelOptions.Listen(ipAddress, urlDetail.Port, configure);
return;
}
// Otherwise, listen on all IPs.
kestrelOptions.ListenAnyIP(urlDetail.Port, configure);
}
}
#endif
internal static class IWebHostBuilderExtensions
{
internal static IWebHostBuilder ConfigureAppConfigurationUsingEnvironmentVariables(this IWebHostBuilder builder)
{
return builder.ConfigureAppConfiguration(config =>
{
config.AddEnvironmentVariables();
});
}
internal static IWebHostBuilder ConfigureKestrelServerOptions(this IWebHostBuilder builder)
{
return builder.ConfigureServices((context, services) =>
{
services.Configure<KestrelServerOptions>(context.Configuration.GetSection("Kestrel"));
});
}
}
#endif

View File

@@ -28,19 +28,9 @@ internal partial class AspNetCoreSelfHost
{
options.UseHttps(new HttpsConnectionFilterOptions
{
ServerCertificate = wireMockMiddlewareOptions.CustomCertificateDefined
? CertificateLoader.LoadCertificate(
wireMockMiddlewareOptions.X509StoreName,
wireMockMiddlewareOptions.X509StoreLocation,
wireMockMiddlewareOptions.X509ThumbprintOrSubjectName,
wireMockMiddlewareOptions.X509CertificateFilePath,
wireMockMiddlewareOptions.X509CertificatePassword,
urlDetail.Host)
: PublicCertificateHelper.GetX509Certificate2(),
ServerCertificate = wireMockMiddlewareOptions.CustomCertificateDefined ? CertificateLoader.LoadCertificate(wireMockMiddlewareOptions, urlDetail.Host) : PublicCertificateHelper.GetX509Certificate2(),
ClientCertificateMode = (ClientCertificateMode) wireMockMiddlewareOptions.ClientCertificateMode,
ClientCertificateValidation = wireMockMiddlewareOptions.AcceptAnyClientCertificate
? (_, _, _) => true
: null,
ClientCertificateValidation = wireMockMiddlewareOptions.AcceptAnyClientCertificate ? (_, _, _) => true : null
});
}
}

View File

@@ -7,6 +7,10 @@ using WireMock.Logging;
using WireMock.Matchers;
using WireMock.Types;
using WireMock.Util;
using System.Security.Cryptography.X509Certificates;
using JetBrains.Annotations;
#if !USE_ASPNETCORE
using Owin;
#else
@@ -70,6 +74,11 @@ internal interface IWireMockMiddlewareOptions
string? X509CertificateFilePath { get; set; }
/// <summary>
/// A X.509 certificate instance.
/// </summary>
public X509Certificate2? X509Certificate { get; set; }
string? X509CertificatePassword { get; set; }
bool CustomCertificateDefined { get; }

View File

@@ -7,6 +7,8 @@ using WireMock.Logging;
using WireMock.Matchers;
using WireMock.Types;
using WireMock.Util;
using System.Security.Cryptography.X509Certificates;
#if !USE_ASPNETCORE
using Owin;
#else
@@ -81,13 +83,17 @@ internal class WireMockMiddlewareOptions : IWireMockMiddlewareOptions
/// <inheritdoc cref="IWireMockMiddlewareOptions.X509CertificateFilePath"/>
public string? X509CertificateFilePath { get; set; }
/// <inheritdoc />
public X509Certificate2? X509Certificate { get; set; }
/// <inheritdoc cref="IWireMockMiddlewareOptions.X509CertificatePassword"/>
public string? X509CertificatePassword { get; set; }
/// <inheritdoc cref="IWireMockMiddlewareOptions.CustomCertificateDefined"/>
public bool CustomCertificateDefined =>
!string.IsNullOrEmpty(X509StoreName) && !string.IsNullOrEmpty(X509StoreLocation) ||
!string.IsNullOrEmpty(X509CertificateFilePath);
!string.IsNullOrEmpty(X509CertificateFilePath) ||
X509Certificate != null;
/// <inheritdoc cref="IWireMockMiddlewareOptions.SaveUnmatchedRequests"/>
public bool? SaveUnmatchedRequests { get; set; }

View File

@@ -39,6 +39,7 @@ internal static class WireMockMiddlewareOptionsHelper
options.X509StoreName = settings.CertificateSettings!.X509StoreName;
options.X509StoreLocation = settings.CertificateSettings.X509StoreLocation;
options.X509ThumbprintOrSubjectName = settings.CertificateSettings.X509StoreThumbprintOrSubjectName;
options.X509Certificate = settings.CertificateSettings.X509Certificate;
options.X509CertificateFilePath = settings.CertificateSettings.X509CertificateFilePath;
options.X509CertificatePassword = settings.CertificateSettings.X509CertificatePassword;
}

View File

@@ -1,5 +1,6 @@
// Copyright © WireMock.Net
using System.Security.Cryptography.X509Certificates;
using JetBrains.Annotations;
namespace WireMock.Settings;
@@ -9,36 +10,44 @@ namespace WireMock.Settings;
///
/// X509StoreName and X509StoreLocation should be defined
/// OR
/// X509CertificateFilePath and X509CertificatePassword should be defined
/// X509CertificateFilePath should be defined
/// OR
/// X509Certificate should be defined
/// </summary>
public class WireMockCertificateSettings
{
/// <summary>
/// X509 StoreName (AddressBook, AuthRoot, CertificateAuthority, My, Root, TrustedPeople or TrustedPublisher)
/// X.509 certificate StoreName (AddressBook, AuthRoot, CertificateAuthority, My, Root, TrustedPeople or TrustedPublisher)
/// </summary>
[PublicAPI]
public string? X509StoreName { get; set; }
/// <summary>
/// X509 StoreLocation (CurrentUser or LocalMachine)
/// X.509 certificate StoreLocation (CurrentUser or LocalMachine)
/// </summary>
[PublicAPI]
public string? X509StoreLocation { get; set; }
/// <summary>
/// X509 Thumbprint or SubjectName (if not defined, the 'host' is used)
/// X.509 certificate Thumbprint or SubjectName (if not defined, the 'host' is used)
/// </summary>
[PublicAPI]
public string? X509StoreThumbprintOrSubjectName { get; set; }
/// <summary>
/// X509Certificate FilePath
/// X.509 certificate FilePath
/// </summary>
[PublicAPI]
public string? X509CertificateFilePath { get; set; }
/// <summary>
/// X509Certificate Password
/// A X.509 certificate instance.
/// </summary>
[PublicAPI]
public X509Certificate2? X509Certificate { get; set; }
/// <summary>
/// X.509 certificate Password
/// </summary>
[PublicAPI]
public string? X509CertificatePassword { get; set; }
@@ -46,10 +55,13 @@ public class WireMockCertificateSettings
/// <summary>
/// X509StoreName and X509StoreLocation should be defined
/// OR
/// X509CertificateFilePath and X509CertificatePassword should be defined
/// X509CertificateFilePath should be defined
/// OR
/// X509Certificate should be defined
/// </summary>
[PublicAPI]
public bool IsDefined =>
!string.IsNullOrEmpty(X509StoreName) && !string.IsNullOrEmpty(X509StoreLocation) ||
!string.IsNullOrEmpty(X509CertificateFilePath);
!string.IsNullOrEmpty(X509CertificateFilePath) ||
X509Certificate != null;
}

View File

@@ -235,7 +235,9 @@ public class WireMockServerSettings
///
/// X509StoreName and X509StoreLocation should be defined
/// OR
/// X509CertificateFilePath and X509CertificatePassword should be defined
/// X509CertificateFilePath should be defined
/// OR
/// X509Certificate should be defined
/// </summary>
[PublicAPI]
public WireMockCertificateSettings? CertificateSettings { get; set; }