将客户端证书添加到 .NET Core HttpClient

Add client certificate to .NET Core HttpClient

我在玩弄 .NET Core 并构建了一个使用支付 API 的 API。需要将客户端证书添加到双向 SSL 身份验证请求中。 如何使用 HttpClient 在 .NET Core 中实现此目的?

我查看了各种文章,发现 HttpClientHandler 没有提供任何添加客户端证书的选项。

我的客户端没有使用 .NET,但服务器端可以通过 IIS 简单地配置它,方法是在 IIS 后面部署我的 ASP.NET 核心网站,为 HTTPS + 客户端证书配置 IIS:

IIS 客户端证书设置:

那么在代码中就可以简单的获取到:

        var clientCertificate = await HttpContext.Connection.GetClientCertificateAsync();

        if(clientCertificate!=null)
            return new ContentResult() { Content = clientCertificate.Subject };

它对我来说工作正常,但我使用 curl 或 chrome 作为客户端,而不是 .NET 客户端。在 HTTPS 握手期间,客户端从服务器获取请求以提供证书并将其发送到服务器。

如果您使用的是 .NET Core 客户端,它不能有特定于平台的代码,如果它不能将自己连接到任何 OS 特定的证书存储,提取它是有意义的并将其发送到服务器。如果您针对 .NET 4.5.x 进行编译,那么这似乎很简单:

Using HttpClient with SSL/TLS-based client side authentication

就像编译curl一样。如果您希望能够将它连接到 Windows 证书存储,您必须针对某些特定的 Windows 库进行编译。

我 运行 为我的平台全新安装(Linux Mint 17.3) following these steps: .NET Tutorial - Hello World in 5 minutes。我创建了一个针对 netcoreapp1.0 框架的新控制台应用程序,能够提交客户端证书;但是,我在测试时确实收到“SSL 连接错误”(CURLE_SSL_CONNECT_ERROR 35),即使我使用的是有效证书。我的错误可能特定于我的 libcurl。

我 运行 在 Windows 7 上使用完全相同的东西,它完全符合需要。

// using System.Net.Http;
// using System.Security.Authentication;
// using System.Security.Cryptography.X509Certificates;

var handler = new HttpClientHandler();
handler.ClientCertificateOptions = ClientCertificateOption.Manual;
handler.SslProtocols = SslProtocols.Tls12;
handler.ClientCertificates.Add(new X509Certificate2("cert.crt"));
var client = new HttpClient(handler);
var result = client.GetAsync("https://apitest.startssl.com").GetAwaiter().GetResult();

我有一个类似的项目,我在服务之间以及移动和桌面之间通过服务进行通信。

我们使用 EXE 文件中的 Authenticode 证书来确保是我们的二进制文件在执行请求。

请求方(post 过于简化)。

Module m = Assembly.GetEntryAssembly().GetModules()[0];
using (var cert = m.GetSignerCertificate())
using (var cert2 = new X509Certificate2(cert))
{
   var _clientHandler = new HttpClientHandler();
   _clientHandler.ClientCertificates.Add(cert2);
   _clientHandler.ClientCertificateOptions = ClientCertificateOption.Manual;
   var myModel = new Dictionary<string, string>
   {
       { "property1","value" },
       { "property2","value" },
   };
   using (var content = new FormUrlEncodedContent(myModel))
   using (var _client = new HttpClient(_clientHandler))
   using (HttpResponseMessage response = _client.PostAsync($"{url}/{controler}/{action}", content).Result)
   {
       response.EnsureSuccessStatusCode();
       string jsonString = response.Content.ReadAsStringAsync().Result;
       var myClass = JsonConvert.DeserializeObject<MyClass>(jsonString);
    }
}

然后我在获取请求的操作上使用以下代码:

X509Certificate2 clientCertInRequest = Request.HttpContext.Connection.ClientCertificate;
if (!clientCertInRequest.Verify() || !AllowedCerialNumbers(clientCertInRequest.SerialNumber))
{
    Response.StatusCode = 404;
    return null;
}

我们宁愿提供 404 而不是 500,因为我们喜欢那些尝试 URL 以获得错误请求而不是让他们知道他们“在正确的轨道上”的人

在.NET Core中,获取证书的方式不再是通过Module。可能适合您的现代方法是:

private static X509Certificate2? Signer()
{
    using var cert = X509Certificate2.CreateFromSignedFile(Assembly.GetExecutingAssembly().Location);
    if (cert is null)
        return null;

    return new X509Certificate2(cert);
}

如果你看.NET Standard reference for the HttpClientHandler class, you can see that the ClientCertificates property exists, but is hidden due to the use of EditorBrowsableState.Never。这会阻止 IntelliSense 显示它,但在使用它的代码中仍然有效。

[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
public System.Security.Cryptography.X509Certificates.X509CertificateCollection ClientCertificates { get; }

可同时用于 .NET Core 2.0< 和 .NET Framework 4.7.1<:

var handler = new HttpClientHandler();
handler.ClientCertificates.Add(new X509Certificate2("cert.crt"));
var client = new HttpClient(handler);

https://docs.microsoft.com/en-us/dotnet/api/system.net.http.httpclienthandler?view=netframework-4.7.1

我认为已提供最佳答案 here

利用X-ARR-ClientCert header您可以提供证书信息。

这里有一个适应的解决方案:

X509Certificate2 certificate;
var handler = new HttpClientHandler {
    ClientCertificateOptions = ClientCertificateOption.Manual,
    SslProtocols = SslProtocols.Tls12
};
handler.ClientCertificates.Add(certificate);
handler.CheckCertificateRevocationList = false;
// this is required to get around self-signed certs
handler.ServerCertificateCustomValidationCallback =
    (httpRequestMessage, cert, cetChain, policyErrors) => {
        return true;
    };
var client = new HttpClient(handler);
requestMessage.Headers.Add("X-ARR-ClientCert", certificate.GetRawCertDataString());
requestMessage.Content = new StringContent(JsonConvert.SerializeObject(requestData), Encoding.UTF8, "application/json");
var response = await client.SendAsync(requestMessage);

if (response.IsSuccessStatusCode)
{
    var responseContent = await response.Content.ReadAsStringAsync();
    var keyResponse = JsonConvert.DeserializeObject<KeyResponse>(responseContent);

    return keyResponse;
}

并且在您的 .net 核心服务器的启动例程中:

public IServiceProvider ConfigureServices(IServiceCollection services)
{
    services.AddCertificateForwarding(options => {
        options.CertificateHeader = "X-ARR-ClientCert";
        options.HeaderConverter = (headerValue) => {
            X509Certificate2 clientCertificate = null;
            try
            {
                if (!string.IsNullOrWhiteSpace(headerValue))
                {
                    var bytes = ConvertHexToBytes(headerValue);
                    clientCertificate = new X509Certificate2(bytes);
                }
            }
            catch (Exception)
            {
                // invalid certificate
            }

            return clientCertificate;
        };
    });
}

Main() 中进行所有配置,如下所示:

public static void Main(string[] args)
{
    var configuration = new ConfigurationBuilder().AddJsonFile("appsettings.json").Build();
    var logger = new LoggerConfiguration().ReadFrom.Configuration(configuration).CreateLogger();
    string env="", sbj="", crtf = "";

    try
    {
        var whb = WebHost.CreateDefaultBuilder(args).UseContentRoot(Directory.GetCurrentDirectory());

        var environment = env = whb.GetSetting("environment");
        var subjectName = sbj = CertificateHelper.GetCertificateSubjectNameBasedOnEnvironment(environment);
        var certificate = CertificateHelper.GetServiceCertificate(subjectName);

        crtf = certificate != null ? certificate.Subject : "It will after the certification";

        if (certificate == null) // present apies even without server certificate but dont give permission on authorization
        {
            var host = whb
                .ConfigureKestrel(_ => { })
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseStartup<Startup>()
                .UseConfiguration(configuration)
                .UseSerilog((context, config) =>
                {
                    config.ReadFrom.Configuration(context.Configuration);
                })
                .Build();
            host.Run();
        }
        else
        {
            var host = whb
                .ConfigureKestrel(options =>
                {
                    options.Listen(new IPEndPoint(IPAddress.Loopback, 443), listenOptions =>
                    {
                        var httpsConnectionAdapterOptions = new HttpsConnectionAdapterOptions()
                        {
                            ClientCertificateMode = ClientCertificateMode.AllowCertificate,
                            SslProtocols = System.Security.Authentication.SslProtocols.Tls12,
                            ServerCertificate = certificate
                        };
                        listenOptions.UseHttps(httpsConnectionAdapterOptions);
                    });
                })
                .UseContentRoot(Directory.GetCurrentDirectory())
                .UseIISIntegration()
                .UseUrls("https://*:443")
                .UseStartup<Startup>()
                .UseConfiguration(configuration)
                .UseSerilog((context, config) =>
                {
                    config.ReadFrom.Configuration(context.Configuration);
                })
                .Build();
            host.Run();
        }

        Log.Logger.Information("Information: Environment = " + env +
            " Subject = " + sbj +
            " Certificate Subject = " + crtf);
    }
    catch(Exception ex)
    {
        Log.Logger.Error("Main handled an exception: Environment = " + env +
            " Subject = " + sbj +
            " Certificate Subject = " + crtf +
            " Exception Detail = " + ex.Message);
    }
}

像这样配置文件startup.cs

#region 2way SSL settings
services.AddMvc();
services.AddAuthentication(options =>
{
    options.DefaultAuthenticateScheme = CertificateAuthenticationDefaults.AuthenticationScheme;
    options.DefaultChallengeScheme = CertificateAuthenticationDefaults.AuthenticationScheme;
})
.AddCertificateAuthentication(certOptions =>
{
    var certificateAndRoles = new List<CertficateAuthenticationOptions.CertificateAndRoles>();
    Configuration.GetSection("AuthorizedCertficatesAndRoles:CertificateAndRoles").Bind(certificateAndRoles);
    certOptions.CertificatesAndRoles = certificateAndRoles.ToArray();
});

services.AddAuthorization(options =>
{
    options.AddPolicy("CanAccessAdminMethods", policy => policy.RequireRole("Admin"));
    options.AddPolicy("CanAccessUserMethods", policy => policy.RequireRole("User"));
});
#endregion

证书助手

public class CertificateHelper
{
    protected internal static X509Certificate2 GetServiceCertificate(string subjectName)
    {
        using (var certStore = new X509Store(StoreName.Root, StoreLocation.LocalMachine))
        {
            certStore.Open(OpenFlags.ReadOnly);
            var certCollection = certStore.Certificates.Find(
                                       X509FindType.FindBySubjectDistinguishedName, subjectName, true);
            X509Certificate2 certificate = null;
            if (certCollection.Count > 0)
            {
                certificate = certCollection[0];
            }
            return certificate;
        }
    }

    protected internal static string GetCertificateSubjectNameBasedOnEnvironment(string environment)
    {
        var builder = new ConfigurationBuilder()
         .SetBasePath(Directory.GetCurrentDirectory())
        .AddJsonFile($"appsettings.{environment}.json", optional: false);

        var configuration = builder.Build();
        return configuration["ServerCertificateSubject"];
    }
}

在对这个问题进行了大量测试之后,我最终得到了这个。

  1. 使用 SSL,我从证书和密钥创建了一个 pfx 文件。
  2. 创建一个HttpClient如下:
_httpClient = new(new HttpClientHandler
{
    ClientCertificateOptions = ClientCertificateOption.Manual,
    SslProtocols = SslProtocols.Tls12,
    ClientCertificates = { new X509Certificate2(@"C:\kambiDev.pfx") }
});