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

92
我正在尝试使用.NET Core构建一个利用支付API的API,并需要添加客户端证书以进行双向SSL身份验证。如何在.NET Core中使用HttpClient实现这一点?我查看了各种文章,发现HttpClientHandler没有提供添加客户端证书的选项。
9个回答

76

我为我的平台(Linux Mint 17.3)进行了全新安装,按照以下步骤操作:.NET教程 - 5分钟内完成Hello World。我创建了一个针对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();

1
哇,伙计,它成功了。非常感谢你抽出时间来帮助我。这些天我一直在度假。 - Krishna.N
我已经在Windows(10)和Linux(Mint 17.3、18和18.1)上使其工作。当我尝试使用没有私钥的证书时,我确实遇到了“SSL连接错误”(CURLE_SSL_CONNECT_ERROR 35)。 - user1289580
@yfisaqt,你能详细说明一下你是如何看到CURL错误的吗?我发现在Windows上可以使用证书进行签名,但是在Ubuntu 17.04上相同的代码会导致错误。 - JosephGarrone
@JosephGarrone 如果我的证书文件只包含公钥,那么我会在Linux上遇到错误35,但如果证书包含私钥,则该错误将消失。在Windows世界中,这可能是一个PFX文件。 - user1289580
2
@Tom 从技术角度来看,这取决于文件格式。请注意,在我升级到.NET Core SDK 2之前就已经回答了这个问题,因此在2.0中可能会有所不同。当时我回答这个问题时,Windows需要文件是PFX格式,但在Linux上我可以使用普通证书(没有私钥)。 - user1289580
显示剩余6条评论

35

我有一个类似的项目,其中我通过服务与移动设备和桌面设备之间进行通信。

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

在请求端(为了本文过于简化)。

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);
}

1
你如何确认客户端请求是使用HTTP/1.1发出的?在HTTP/2中没有客户端证书认证。 - Jari Turkia
我知道这个很旧了,但是为了回答你的评论:handler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) => true; - Michael

6

在处理这个问题时,经过大量测试后,我最终得到了以下解决方案。

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

它在Linux机器上无法工作。 - sina_Islam

4
我不使用.NET作为我的客户端,但是通过在IIS上部署我的ASP.NET Core网站,并配置IIS支持HTTPS和客户端证书,可以简单地对其进行配置。
IIS客户端证书设置: 然后你可以在代码中简单获取它:
        var clientCertificate = await HttpContext.Connection.GetClientCertificateAsync();

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

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

如果您正在使用.NET Core客户端,则它不能具有特定于平台的代码,并且如果它无法连接到任何特定于操作系统的证书存储库以提取证书并将其发送到服务器,则是有道理的。如果您针对.NET 4.5.x进行编译,则似乎很容易:

使用带有基于SSL / TLS的客户端身份验证的HttpClient

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


谢谢您抽出时间回答我的问题,但是我是在使用 ASP.NET Core。上面的代码对我有用。 - Krishna.N

2

1
handler.ClientCertificates.Add(new X509Certificate2("cert.crt")) 只适用于 Framework 4.8(https://learn.microsoft.com/ru-ru/dotnet/api/system.net.http.httpclienthandler.clientcertificates?view=netframework-4.8#System_Net_Http_HttpClientHandler_ClientCertificates) - Kate
@Kate,我向你报告一下,代码在.Net Framework 4.5上无法运行,但是在.Net Framework 4.7.1上可以正常工作。我已经在本地测试过,并且文档也有说明。我已经更新了答案。 - Ogglas
版本规范似乎被反转了,我认为它应该写成:可用于 .NET Core >2.0 和 .NET Framework >4.7.1 两者。请注意符号的反向。 - Jonathan

2
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

我认为这里提供的最佳答案

通过利用 X-ARR-ClientCert 头,您可以提供证书信息。

这里提供了一个适应性解决方案:

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 core 服务器的启动例程中:

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;
        };
    });
}

1
X-ARR-ClientCert是Azure特定的东西,这段代码实际上并没有验证客户端是否拥有证书的私钥。它只是将公钥附加到请求中。请注意,链接的文章已经进行了更正,以正确地使用客户端证书。 - Austin
当代理/负载均衡器在X-Client-Cert头中重新发送客户端证书,并且ASP.NET Core应用程序需要使用该证书来创建用户ClaimsPrincipal时,这一点非常重要。实际上,在这种情况下,默认选项已经足够了,只需要以下这行代码进行设置:app.UseCertificateForwarding(); - drpdrp

0

如果您查看.NET Standard HttpClientHandler类的参考文档, 您会发现ClientCertificates属性是存在的,但由于使用了EditorBrowsableState.Never而被隐藏。这可以防止IntelliSense显示它,但仍然可以在使用它的代码中正常工作。

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

0
我从这里的AppData/Local/BROWSERNAME/UserData中删除了Local State文件。
顺便说一下,这个删除可能会破坏你的书签或密码等。
谢谢。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接