Como usar um certificado de cliente para autenticar e autorizar em uma API da Web

Estou tentando usar um certificado de cliente para autenticar e autorizar dispositivos usando uma API da Web e desenvolvi uma prova de conceito simples para solucionar problemas com a solução em potencial. Estou com um problema em que o certificado do cliente não está sendo recebido pelo aplicativo Web. Várias pessoas relataram esse problema,incluindo nesta sessão de perguntas e respostas, mas nenhum deles tem uma resposta. Minha esperança é fornecer mais detalhes para reavivar esse problema e, com sorte, obter uma resposta para ele. Estou aberto a outras soluções. O principal requisito é que um processo independente escrito em C # possa chamar uma API da Web e ser autenticado usando um certificado de cliente.

A API da Web neste POC é muito simples e retorna apenas um valor. Ele usa um atributo para validar que HTTPS é usado e que um certificado de cliente está presente.

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

}

Aqui está o código para o 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); 
        } 
    } 
}

Neste POC, estou apenas verificando a disponibilidade do certificado do cliente. Depois que isso estiver funcionando, posso adicionar verificações de informações no certificado para validar uma lista de certificados.

Aqui está a configuração no IIS para SSL para este aplicativo Web.

Aqui está o código para o cliente que envia a solicitação com um certificado de cliente. Este é um aplicativo de console.

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

Quando executo este aplicativo de teste, recebo de volta um código de status 403 Proibido com uma frase de razão "Certificado de Cliente Necessário", indicando que ele está entrando no meu RequireHttpsAttribute e não está encontrando nenhum certificado de cliente. Executando isso por meio de um depurador, verifiquei que o certificado está sendo carregado e adicionado ao WebRequestHandler. O certificado é exportado para um arquivo CER que está sendo carregado. O certificado completo com a chave privada está localizado nos armazenamentos Raiz Confiável e Pessoal da Máquina Local para o servidor de aplicativos da web. Para este teste, o cliente e o aplicativo da web estão sendo executados na mesma máquina.

Posso chamar esse método de API da Web usando o Fiddler, anexando o mesmo certificado de cliente e funciona bem. Ao usar o Fiddler, ele passa nos testes em RequireHttpsAttribute e retorna um código de status bem-sucedido de 200 e retorna o valor esperado.

Alguém encontrou o mesmo problema em que o HttpClient não envia um certificado de cliente na solicitação e encontrou uma solução?

Atualização 1:

Também tentei obter o certificado do repositório de certificados que inclui a chave privada. Aqui está como eu o recuperei:

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

Eu verifiquei que este certificado estava sendo recuperado corretamente e estava sendo adicionado à coleção de certificados do cliente. Mas obtive os mesmos resultados em que o código do servidor não recupera nenhum certificado de cliente.

Para completar, aqui está o código usado para recuperar o certificado de um arquivo:

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

    }

Você notará que, quando você obtém o certificado de um arquivo, ele retorna um objeto do tipo X509Certificate e, quando você o recupera do armazenamento de certificados, é do tipo X509Certificate2. O método X509CertificateCollection.Add está esperando um tipo de X509Certificate.

Atualização 2: Ainda estou tentando descobrir isso e tentei muitas opções diferentes, mas sem sucesso.

Mudei o aplicativo Web para executar em um nome de host em vez de host local.Defino o aplicativo Web para exigir SSLEu verifiquei que o certificado foi definido para autenticação do cliente e se está na raiz confiávelAlém de testar o certificado do cliente no Fiddler, também o validei no Chrome.

Em um ponto durante a tentativa dessas opções, ele começou a funcionar. Então comecei a fazer o backup das alterações para ver o que causou o trabalho. Continuou a funcionar. Tentei remover o certificado da raiz confiável para confirmar se isso era necessário e ele parou de funcionar e agora não consigo recuperá-lo, mesmo que eu coloque o certificado novamente na raiz confiável. Agora, o Chrome nem mesmo solicitará um certificado como o usado e falha no Chrome, mas ainda funciona no Fiddler. Falta alguma configuração mágica.

Também tentei ativar "Negociar certificado de cliente" na ligação, mas o Chrome ainda não solicitará um certificado de cliente. Aqui estão as configurações usando "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

Aqui está o certificado do cliente que estou usando:

Estou confuso quanto ao problema. Estou adicionando uma recompensa para qualquer um que possa me ajudar a descobrir isso.

questionAnswers(5)

yourAnswerToTheQuestion