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

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

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

【问题讨论】:

    标签: c# .net asp.net-core .net-core dotnet-httpclient


    【解决方案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】:

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

    1. 使用SSL,我从证书和密钥创建了一个pfx 文件。
    2. 如下创建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 特定的东西,此代码实际上并未验证客户端是否拥有证书的私钥。它只是将公钥附加到请求中。请注意,链接的文章已更正为实际正确使用客户端证书。
          猜你喜欢
          • 2018-05-27
          • 1970-01-01
          • 1970-01-01
          • 2017-12-04
          • 2017-10-18
          • 2015-10-26
          • 2019-10-14
          相关资源
          最近更新 更多