mirror of
https://github.com/wiremock/WireMock.Net.git
synced 2026-01-11 21:10:32 +01:00
Add option to provide X509Certificate (#1303)
* Add option to provide X509CertificateRawData * X509Certificate * remove X509CertificateRawData
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user