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

Posted

技术标签:

【中文标题】将客户端证书添加到 .NET Core HttpClient【英文标题】:Add client certificate to .NET Core HttpClient 【发布时间】:2016-10-13 06:44:36 【问题描述】:

我正在使用 .NET Core 并构建一个利用支付 API 的 API。有一个客户端证书需要添加到双向 SSL 身份验证的请求中。 如何在 .NET Core 中使用 HttpClient 实现这一点?

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

【问题讨论】:

【参考方案1】:

我按照以下步骤为我的平台 (Linux Mint17.3) 运行了全新安装:.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();

【讨论】:

哇哈,伙计,非常感谢您花时间帮助我。这些天我都在休假。 我很好奇。你是让它在 linux 机器或 windows 上工作吗? 我让它在 Windows (10) 和 Linux(Mint 17.3、18 和 18.1)上运行。当我尝试使用没有私钥的证书时,我确实收到了“SSL 连接错误”(CURLE_SSL_CONNECT_ERROR 35)。 @yfisaqt 你能详细说明你是如何看到 CURL 错误的吗?我发现我可以在 Windows 上使用证书签名,但 Ubuntu 17.04 上的相同代码会导致错误。 @Tom 从技术角度来说,这取决于文件格式。请记住,在我升级到 .NET Core SDK 2 之前已经回答了这个问题,因此这在 2.0 中的行为可能会有所不同。在我回答这个问题时,Windows 要求该文件是 PFX,但在 Linux 上我能够使用常规证书(无私钥)。【参考方案2】:

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

我们使用 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);

【讨论】:

您如何确认客户端请求是使用 HTTP/1.1 发出的? HTTP/2 中没有客户端证书认证。 我知道这是旧的,但要回答您的评论:handler.ServerCertificateCustomValidationCallback += (sender, cert, chain, sslPolicyErrors) =&gt; true;【参考方案3】:

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

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 进行编译,那么这似乎很容易:

Using HttpClient with SSL/TLS-based client side authentication

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

【讨论】:

感谢您花时间回答,但我将它用于 asp.net 核心。上面的代码对我有用。【参考方案4】:

可用于 .NET Core 2.0

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

【讨论】:

handler.ClientCertificates.Add(new X509Certificate2("cert.crt")) 仅适用于 Framework 4.8 (docs.microsoft.com/ru-ru/dotnet/api/…) @Kate Miss 我说它不适用于.Net Framework 4.5.Net Framework 4.7.1。在本地进行了测试,文档也这么说。更新了答案。【参考方案5】:

在对这个问题进行了大量测试后,我最终解决了这个问题。

    使用SSL,我从证书和密钥创建了一个pfx 文件。 如下创建HttpClient
_httpClient = new(new HttpClientHandler

    ClientCertificateOptions = ClientCertificateOption.Manual,
    SslProtocols = SslProtocols.Tls12,
    ClientCertificates =  new X509Certificate2(@"C:\kambiDev.pfx") 
);

【讨论】:

【参考方案6】:

像这样在 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"];
    

【讨论】:

【参考方案7】:

如果您查看.NET Standard reference for the HttpClientHandler class,您可以看到 ClientCertificates 属性存在,但由于使用了EditorBrowsableState.Never 而被隐藏。这会阻止 IntelliSense 显示它,但仍可在使用它的代码中工作。

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

【讨论】:

【参考方案8】:

我认为最好的答案是here。

通过使用 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 核心服务器的启动例程中:

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

【讨论】:

X-ARR-ClientCert 是 Azure 特定的东西,此代码实际上并未验证客户端是否拥有证书的私钥。它只是将公钥附加到请求中。请注意,链接的文章已更正为实际正确使用客户端证书。

以上是关于将客户端证书添加到 .NET Core HttpClient的主要内容,如果未能解决你的问题,请参考以下文章

将处理程序添加到 ASP.NET Core 中的默认 http 客户端 [重复]

Golang的一个简单实用的http客户端库httpc

在 .Net Core 控制台应用程序中生成受信任的自签名证书

在.Net Core中使用HttpClient添加证书

ASP.NET Core 3.1 中的证书身份验证实现

Kestel (.NET Core 3.1) 是不是支持 md5RSA 证书?