【问题标题】:How to use a client certificate to authenticate and authorize in a Web API如何使用客户端证书在 Web API 中进行身份验证和授权
【发布时间】:2016-02-23 16:06:57
【问题描述】:

我正在尝试使用客户端证书通过 Web API 对设备进行身份验证和授权,并开发了一个简单的概念证明来解决潜在解决方案的问题。我遇到了 Web 应用程序没有收到客户端证书的问题。许多人报告了这个问题,including in this Q&A,但他们都没有答案。我希望提供更多细节来重振这个问题,并希望得到我的问题的答案。我对其他解决方案持开放态度。主要要求是用 C# 编写的独立进程可以调用 Web API 并使用客户端证书进行身份验证。

这个 POC 中的 Web API 非常简单,只返回一个值。它使用一个属性来验证是否使用了 HTTPS 以及是否存在客户端证书。

public class SecureController : ApiController
{
    [RequireHttps]
    public string Get(int id)
    {
        return "value";
    }

}

这里是 RequireHttpsAttribute 的代码:

public class RequireHttpsAttribute : AuthorizationFilterAttribute 
{ 
    public override void OnAuthorization(HttpActionContext actionContext) 
    { 
        if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps) 
        { 
            actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden) 
            { 
                ReasonPhrase = "HTTPS Required" 
            }; 
        } 
        else 
        {
            var cert = actionContext.Request.GetClientCertificate();
            if (cert == null)
            {
                actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                {
                    ReasonPhrase = "Client Certificate Required"
                }; 

            }
            base.OnAuthorization(actionContext); 
        } 
    } 
}

在这个 POC 中,我只是检查客户端证书的可用性。一旦这工作正常,我可以在证书中添加信息检查,以根据证书列表进行验证。

以下是 IIS 中针对此 Web 应用程序的 SSL 设置。

这是发送带有客户端证书的请求的客户端的代码。这是一个控制台应用程序。

    private static async Task SendRequestUsingHttpClient()
    {
        WebRequestHandler handler = new WebRequestHandler();
        X509Certificate certificate = GetCert("ClientCertificate.cer");
        handler.ClientCertificates.Add(certificate);
        handler.ServerCertificateValidationCallback = new RemoteCertificateValidationCallback(ValidateServerCertificate);
        handler.ClientCertificateOptions = ClientCertificateOption.Manual;
        using (var client = new HttpClient(handler))
        {
            client.BaseAddress = new Uri("https://localhost:44398/");
            client.DefaultRequestHeaders.Accept.Clear();
            client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json"));

            HttpResponseMessage response = await client.GetAsync("api/Secure/1");
            if (response.IsSuccessStatusCode)
            {
                string content = await response.Content.ReadAsStringAsync();
                Console.WriteLine("Received response: {0}",content);
            }
            else
            {
                Console.WriteLine("Error, received status code {0}: {1}", response.StatusCode, response.ReasonPhrase);
            }
        }
    }

    public static bool ValidateServerCertificate(
      object sender,
      X509Certificate certificate,
      X509Chain chain,
      SslPolicyErrors sslPolicyErrors)
    {
        Console.WriteLine("Validating certificate {0}", certificate.Issuer);
        if (sslPolicyErrors == SslPolicyErrors.None)
            return true;

        Console.WriteLine("Certificate error: {0}", sslPolicyErrors);

        // Do not allow this client to communicate with unauthenticated servers.
        return false;
    }

当我运行这个测试应用程序时,我得到一个 403 Forbidden 状态代码,其中包含“需要客户端证书”的原因短语,表明它正在进入我的 RequireHttpsAttribute 并且没有找到任何客户端证书。通过调试器运行它,我已经验证证书正在加载并添加到 WebRequestHandler。证书将导出到正在加载的 CER 文件中。带有私钥的完整证书位于 Web 应用程序服务器的本地计算机的个人和受信任的根存储中。对于此测试,客户端和 Web 应用程序在同一台机器上运行。

我可以使用 Fiddler 调用这个 Web API 方法,附加相同的客户端证书,它工作正常。使用 Fiddler 时,它通过了 RequireHttpsAttribute 中的测试并返回成功状态码 200 并返回预期值。

有没有人遇到过同样的问题,HttpClient 没有在请求中发送客户端证书并找到了解决方案?

更新 1:

我还尝试从包含私钥的证书存储中获取证书。这是我检索它的方法:

    private static X509Certificate2 GetCert2(string hostname)
    {
        X509Store myX509Store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
        myX509Store.Open(OpenFlags.ReadWrite);
        X509Certificate2 myCertificate = myX509Store.Certificates.OfType<X509Certificate2>().FirstOrDefault(cert => cert.GetNameInfo(X509NameType.SimpleName, false) == hostname);
        return myCertificate;
    }

我确认此证书已正确检索,并且已添加到客户端证书集合中。但是在服务器代码没有检索任何客户端证书的情况下,我得到了相同的结果。

为了完整起见,这里是用于从文件中检索证书的代码:

    private static X509Certificate GetCert(string filename)
    {
        X509Certificate Cert = X509Certificate.CreateFromCertFile(filename);
        return Cert;

    }

您会注意到,当您从文件中获取证书时,它会返回 X509Certificate 类型的对象,而当您从证书存储中检索它时,它会返回 X509Certificate2 类型的对象。 X509CertificateCollection.Add 方法需要 X509Certificate 类型。

更新 2: 我仍在努力解决这个问题,并尝试了许多不同的选择,但无济于事。

  • 我将 Web 应用程序更改为在主机名而不是本地主机上运行。
  • 我将 Web 应用程序设置为需要 SSL
  • 我确认证书已设置为客户端身份验证,并且它位于受信任的根目录中
  • 除了在 Fiddler 中测试客户端证书外,我还在 Chrome 中对其进行了验证。

在尝试这些选项的某个时刻,它开始起作用。然后我开始撤销更改,看看是什么导致它工作。它继续工作。然后我尝试从受信任的根中删除证书以验证这是必需的并且它停止工作,现在即使我将证书放回受信任的根中,我也无法使其恢复工作。现在 Chrome 甚至不会提示我使用它使用的证书,它在 Chrome 中失败,但在 Fiddler 中仍然有效。一定是我缺少一些神奇的配置。

我还尝试在绑定中启用“协商客户端证书”,但 Chrome 仍然不会提示我输入客户端证书。这是使用“netsh http show sslcert”的设置

 IP:port                 : 0.0.0.0:44398
 Certificate Hash        : 429e090db21e14344aa5d75d25074712f120f65f
 Application ID          : {4dc3e181-e14b-4a21-b022-59fc669b0914}
 Certificate Store Name  : MY
 Verify Client Certificate Revocation    : Disabled
 Verify Revocation Using Cached Client Certificate Only    : Disabled
 Usage Check    : Enabled
 Revocation Freshness Time : 0
 URL Retrieval Timeout   : 0
 Ctl Identifier          : (null)
 Ctl Store Name          : (null)
 DS Mapper Usage    : Disabled
 Negotiate Client Certificate    : Enabled

这是我正在使用的客户端证书:

我对问题所在感到困惑。我正在为任何可以帮助我解决这个问题的人添加赏金。

【问题讨论】:

    标签: c# security asp.net-web-api ssl-certificate client-certificates


    【解决方案1】:

    跟踪帮助我找到了问题所在(感谢 Fabian 的建议)。我通过进一步测试发现我可以让客户端证书在另一台服务器(Windows Server 2012)上工作。我在我的开发机器(Window 7)上测试这个,所以我可以调试这个过程。因此,通过将跟踪与一个正常工作的 IIS 服务器和一个不工作的 IIS 服务器进行比较,我能够查明跟踪日志中的相关行。这是客户端证书起作用的日志的一部分。这是发送前的设置

    System.Net Information: 0 : [17444] InitializeSecurityContext(In-Buffers count=2, Out-Buffer length=0, returned code=CredentialsNeeded).
    System.Net Information: 0 : [17444] SecureChannel#54718731 - We have user-provided certificates. The server has not specified any issuers, so try all the certificates.
    System.Net Information: 0 : [17444] SecureChannel#54718731 - Selected certificate:
    

    这是客户端证书失败的机器上的跟踪日志的样子。

    System.Net Information: 0 : [19616] InitializeSecurityContext(In-Buffers count=2, Out-Buffer length=0, returned code=CredentialsNeeded).
    System.Net Information: 0 : [19616] SecureChannel#54718731 - We have user-provided certificates. The server has specified 137 issuer(s). Looking for certificates that match any of the issuers.
    System.Net Information: 0 : [19616] SecureChannel#54718731 - Left with 0 client certificates to choose from.
    System.Net Information: 0 : [19616] Using the cached credential handle.
    

    专注于指示服务器指定 137 个发行者的行,我发现了这个 Q&A that seemed similar to my issue。我的解决方案不是标记为答案的解决方案,因为我的证书位于受信任的根目录中。答案是the one under it,您可以在其中更新注册表。我只是将值添加到注册表项。

    HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL

    值名称:SendTrustedIssuerList 值类型:REG_DWORD 值数据:0 (False)

    将此值添加到注册表后,它开始在我的 Windows 7 机器上运行。这似乎是 Windows 7 的问题。

    【讨论】:

      【解决方案2】:

      更新:

      来自微软的示例:

      https://docs.microsoft.com/en-us/azure/app-service/app-service-web-configure-tls-mutual-auth#special-considerations-for-certificate-validation

      原创

      这就是我如何让客户端认证工作并检查特定根 CA 是否已颁发它以及它是否是特定证书。

      首先我编辑了&lt;src&gt;\.vs\config\applicationhost.config 并进行了更改:&lt;section name="access" overrideModeDefault="Allow" /&gt;

      这允许我在web.config 中编辑&lt;system.webServer&gt; 并添加以下行,这将需要在IIS Express 中进行客户端认证。 注意:我出于开发目的对其进行了编辑,不允许在生产中覆盖。

      对于生产,请按照以下指南设置 IIS:

      https://medium.com/@hafizmohammedg/configuring-client-certificates-on-iis-95aef4174ddb

      web.config:

      <security>
        <access sslFlags="Ssl,SslNegotiateCert,SslRequireCert" />
      </security>
      

      API 控制器:

      [RequireSpecificCert]
      public class ValuesController : ApiController
      {
          // GET api/values
          public IHttpActionResult Get()
          {
              return Ok("It works!");
          }
      }
      

      属性:

      public class RequireSpecificCertAttribute : AuthorizationFilterAttribute
      {
          public override void OnAuthorization(HttpActionContext actionContext)
          {
              if (actionContext.Request.RequestUri.Scheme != Uri.UriSchemeHttps)
              {
                  actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                  {
                      ReasonPhrase = "HTTPS Required"
                  };
              }
              else
              {
                  X509Certificate2 cert = actionContext.Request.GetClientCertificate();
                  if (cert == null)
                  {
                      actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                      {
                          ReasonPhrase = "Client Certificate Required"
                      };
      
                  }
                  else
                  {
                      X509Chain chain = new X509Chain();
      
                      //Needed because the error "The revocation function was unable to check revocation for the certificate" happened to me otherwise
                      chain.ChainPolicy = new X509ChainPolicy()
                      {
                          RevocationMode = X509RevocationMode.NoCheck,
                      };
                      try
                      {
                          var chainBuilt = chain.Build(cert);
                          Debug.WriteLine(string.Format("Chain building status: {0}", chainBuilt));
      
                          var validCert = CheckCertificate(chain, cert);
      
                          if (chainBuilt == false || validCert == false)
                          {
                              actionContext.Response = new HttpResponseMessage(System.Net.HttpStatusCode.Forbidden)
                              {
                                  ReasonPhrase = "Client Certificate not valid"
                              };
                              foreach (X509ChainStatus chainStatus in chain.ChainStatus)
                              {
                                  Debug.WriteLine(string.Format("Chain error: {0} {1}", chainStatus.Status, chainStatus.StatusInformation));
                              }
                          }
                      }
                      catch (Exception ex)
                      {
                          Debug.WriteLine(ex.ToString());
                      }
                  }
      
                  base.OnAuthorization(actionContext);
              }
          }
      
          private bool CheckCertificate(X509Chain chain, X509Certificate2 cert)
          {
              var rootThumbprint = WebConfigurationManager.AppSettings["rootThumbprint"].ToUpper().Replace(" ", string.Empty);
      
              var clientThumbprint = WebConfigurationManager.AppSettings["clientThumbprint"].ToUpper().Replace(" ", string.Empty);
      
              //Check that the certificate have been issued by a specific Root Certificate
              var validRoot = chain.ChainElements.Cast<X509ChainElement>().Any(x => x.Certificate.Thumbprint.Equals(rootThumbprint, StringComparison.InvariantCultureIgnoreCase));
      
              //Check that the certificate thumbprint matches our expected thumbprint
              var validCert = cert.Thumbprint.Equals(clientThumbprint, StringComparison.InvariantCultureIgnoreCase);
      
              return validRoot && validCert;
          }
      }
      

      然后可以像这样调用具有客户端认证的 API,从另一个 Web 项目测试。

      [RoutePrefix("api/certificatetest")]
      public class CertificateTestController : ApiController
      {
      
          public IHttpActionResult Get()
          {
              var handler = new WebRequestHandler();
              handler.ClientCertificateOptions = ClientCertificateOption.Manual;
              handler.ClientCertificates.Add(GetClientCert());
              handler.UseProxy = false;
              var client = new HttpClient(handler);
              var result = client.GetAsync("https://localhost:44331/api/values").GetAwaiter().GetResult();
              var resultString = result.Content.ReadAsStringAsync().GetAwaiter().GetResult();
              return Ok(resultString);
          }
      
          private static X509Certificate GetClientCert()
          {
              X509Store store = null;
              try
              {
                  store = new X509Store(StoreName.My, StoreLocation.CurrentUser);
                  store.Open(OpenFlags.OpenExistingOnly | OpenFlags.ReadOnly);
      
                  var certificateSerialNumber= "‎81 c6 62 0a 73 c7 b1 aa 41 06 a3 ce 62 83 ae 25".ToUpper().Replace(" ", string.Empty);
      
                  //Does not work for some reason, could be culture related
                  //var certs = store.Certificates.Find(X509FindType.FindBySerialNumber, certificateSerialNumber, true);
      
                  //if (certs.Count == 1)
                  //{
                  //    var cert = certs[0];
                  //    return cert;
                  //}
      
                  var cert = store.Certificates.Cast<X509Certificate>().FirstOrDefault(x => x.GetSerialNumberString().Equals(certificateSerialNumber, StringComparison.InvariantCultureIgnoreCase));
      
                  return cert;
              }
              finally
              {
                  store?.Close();
              }
          }
      }
      

      【讨论】:

        【解决方案3】:

        确保 HttpClient 可以访问完整的客户端证书(包括私钥)。

        您正在使用文件“ClientCertificate.cer”调用 GetCert,这导致假设不包含私钥 - 应该是 Windows 中的 pfx 文件。 从 windows 证书存储区访问证书并使用指纹搜索它可能会更好。

        复制指纹时要小心:在证书管理中查看时有一些不可打印的字符(将字符串复制到notepad++并检查显示字符串的长度)。

        【讨论】:

        • 感谢您的建议。我曾尝试从证书存储中检索并得到相同的结果。请参阅我原来的问题的更新。
        • 您能否检查代码是否可以访问证书的私钥(可在 prop HasPrivateKey 上获得) - 我们已经在运行时无法访问私钥的其他软件上看到了一些问题密钥,因此无法使用证书。仅出于兴趣:使用 iexplore 访问 URI 时是否收到证书选择提示?
        • 是的,HasPrivateKey 的属性设置为 true。我不确定使用 iexplore 访问 URI 是什么意思。托管 Web API 的 Web 应用程序与客户端应用程序使用的证书不同。
        • 好的,私钥很好。除非请求,否则 httpclient 不会发送证书。通过 iexplore 访问网站时,您将看到一个弹出窗口,您可以在其中选择客户端证书 - 如果服务器设置正确。这只是一个快速测试。如果 iexplore 不要求您提供证书,则 iis 设置存在问题,httpclient 不会发送它的客户端证书。
        • 用 Fiddler 进行的测试不会完成同样的验证吗?
        【解决方案4】:

        我实际上遇到了类似的问题,我们需要许多受信任的根证书。我们新安装的网络服务器有一百多个。我们的根以字母 Z 开头,所以它在列表的末尾结束。

        问题在于 IIS 仅将前 20 个受信任的根发送到客户端,并截断了其余的,包括我们的。那是几年前的事了,不记得工具的名称了……它是 IIS 管理套件的一部分,但 Fiddler 也应该这样做。在意识到错误之后,我们删除了很多我们不需要的可信根。这是经过反复试验完成的,所以要小心你删除的内容。

        清理之后,一切都像魅力一样。

        【讨论】:

          【解决方案5】:

          看源码我也觉得私钥肯定有问题。

          它实际上是在检查通过的证书是否为 X509Certificate2 类型以及是否具有私钥。

          如果它没有找到私钥,它会尝试在 CurrentUser 存储中查找证书,然后在 LocalMachine 存储中查找。如果找到证书,它会检查私钥是否存在。

          (参见 SecureChannnel 类中的 source code,方法 EnsurePrivateKey)

          因此,根据您导入的文件(.cer - 不带私钥或 .pfx - 带私钥)以及在哪个存储上可能找不到正确的文件,不会填充 Request.ClientCertificate。

          您可以激活Network Tracing 来尝试调试它。它会给你这样的输出:

          • 尝试在证书存储中查找匹配的证书
          • 在 LocalMachine 存储或 CurrentUser 存储中找不到证书。

          【讨论】:

          • 正如我在问题描述中提到的,我从 LocalMachine 和 .cer 文件中检索了证书,结果相同。从描述中可以看出,LocalMachine中的那个有私钥,证书也复制到了Trusted Root中。
          • 好的。对不起。那么它可能不是问题。你激活追踪了吗?
          【解决方案6】:

          我最近遇到了一个类似的问题,并且遵循 Fabian 的建议实际上使我找到了解决方案。 事实证明,使用客户端证书,您必须确保两件事:

          1. 私钥实际上是作为证书的一部分导出的。

          2. 运行应用程序的应用程序池身份可以访问所述私钥。

          在我们的例子中,我必须:

          1. 将 pfx 文件导入本地服务器存储,同时选中导出复选框以确保已发送私钥。
          2. 使用 MMC 控制台,授予使用的服务帐户对证书私钥的访问权限。

          其他答案中解释的可信根问题是有效的,但在我们的案例中不是问题。

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2012-01-10
            • 2019-06-21
            • 2014-08-23
            • 2015-10-29
            • 2018-07-05
            • 2019-01-14
            • 2012-11-02
            • 1970-01-01
            相关资源
            最近更新 更多