it-roy-ru.com

Как использовать сертификат клиента для аутентификации и авторизации в веб-API

Я пытаюсь использовать клиентский сертификат для проверки подлинности и авторизации устройств с помощью веб-API и разработал простое подтверждение концепции для решения проблем с возможным решением. Я столкнулся с проблемой, когда сертификат клиента не получен веб-приложением. Некоторые люди сообщили об этой проблеме, в том числе в этом Q & A , но ни один из них не имеет ответа. Я надеюсь предоставить более подробную информацию для восстановления этой проблемы и, надеюсь, получить ответ на мою проблему. Я открыт для других решений. Основным требованием является то, что автономный процесс, написанный на C #, может вызывать веб-API и проходить проверку подлинности с использованием сертификата клиента. 

Веб-API в этом POC очень прост и просто возвращает одно значение. Он использует атрибут для проверки того, что используется 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 для SSL для этого веб-приложения.

 enter image description here

Вот код для клиента, который отправляет запрос с сертификатом клиента. Это консольное приложение.

    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. Полный сертификат с закрытым ключом находится в хранилищах Personal и Trusted Root локального компьютера для сервера веб-приложений. Для этого теста клиент и веб-приложение запускаются на одном компьютере.

Я могу вызвать этот метод Web API, используя Fiddler, прикрепив тот же сертификат клиента, и он работает нормально. При использовании 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 ожидает тип сертификата X509.

Update 2: Я все еще пытаюсь выяснить это и перепробовал много разных вариантов, но безрезультатно. 

  • Я изменил веб-приложение для запуска на имени хоста вместо локального хоста.
  • Я установил для веб-приложения требование 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

Вот сертификат клиента, который я использую:

 enter image description here

 enter image description here

 enter image description here

Я сбит с толку относительно того, в чем проблема. Я добавляю награду для любого, кто может помочь мне понять это.

30
Kevin Junghans

Трассировка помогла мне выяснить, в чем проблема (спасибо Фабиан за это предложение). В ходе дальнейшего тестирования я обнаружил, что могу получить сертификат клиента для работы на другом сервере (Windows Server 2012). Я тестировал это на своей машине разработки (Windows 7), чтобы я мог отладить этот процесс. Таким образом, сравнивая трассировку с работающим 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 эмитентов, я нашел это вопросы и ответы, которые казались похожими на мою проблему . Решение для меня не было помечено как ответ, так как мой сертификат был в доверенном корне. Ответ тот, что под ним , где вы обновляете реестр. Я просто добавил значение в раздел реестра.

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL

Имя значения: SendTrustedIssuerList Тип значения: REG_DWORD Данные значения: 0 (False)

После добавления этого значения в реестр он начал работать на моем компьютере с Windows 7. Похоже, это проблема Windows 7.

7
Kevin Junghans

Обновление:

Пример от Microsoft:

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

Оригинал

Так я получил сертификацию клиента и проверил, выдал ли ее конкретный корневой центр сертификации, а также какой-то конкретный сертификат.

Сначала я отредактировал <src>\.vs\config\applicationhost.config и сделал это изменение: <section name="access" overrideModeDefault="Allow" />

Это позволяет мне редактировать <system.webServer> в web.config и добавлять следующие строки, которые потребуют сертификации клиента в 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 с такой же сертификацией клиента, протестированной из другого веб-проекта.

[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();
        }
    }
}
5
Ogglas

У меня фактически была похожая проблема, когда у нас было много доверенных корневых сертификатов. Наш новый установленный веб-сервер был перегружен. Наш корень начался с буквы Z, поэтому он оказался в конце списка. 

Проблема заключалась в том, что IIS отправлял клиенту только первые двадцать доверенных корней, а остальные , включая наши, обрезал. Это было несколько лет назад, я не могу вспомнить название инструмента ... это было частью IIS пакета администрирования, но Fiddler должен сделать то же самое. Осознав ошибку, мы удалили много доверенных корней, которые нам не нужны. Это было сделано методом проб и ошибок, поэтому будьте осторожны, что вы удаляете.

После уборки все заработало как шарм.

1
Brandtware

Убедитесь, что HttpClient имеет доступ к полному клиентскому сертификату (включая закрытый ключ). 

Вы вызываете GetCert с файлом «ClientCertificate.cer», что приводит к предположению, что закрытый ключ не содержится - это скорее файл pfx в Windows. Может быть даже лучше получить доступ к сертификату из хранилища сертификатов Windows и выполнить поиск по отпечатку пальца.

Будьте осторожны при копировании отпечатка пальца: при просмотре в управлении сертификатами есть некоторые непечатаемые символы (скопируйте строку в notepad ++ и проверьте длину отображаемой строки).

1
Daniel Nachtrub

Глядя на исходный код, я также думаю, что должна быть некоторая проблема с закрытым ключом.

На самом деле он проверяет, является ли переданный сертификат типа X509Certificate2 и имеет ли он закрытый ключ. 

Если он не находит закрытый ключ, он пытается найти сертификат в хранилище CurrentUser, а затем в хранилище LocalMachine. Если он находит сертификат, он проверяет наличие закрытого ключа.

(см. исходный код из класса SecureChannnel, метод EnsurePrivateKey)

Таким образом, в зависимости от того, какой файл вы импортировали (.cer - без закрытого ключа или .pfx - с закрытым ключом) и в каком хранилище он может не найти нужный, и Request.ClientCertificate не будет заполнен.

Вы можете активировать Network Tracing , чтобы попытаться отладить это. Это даст вам вывод, как это:

  • Попытка найти соответствующий сертификат в хранилище сертификатов
  • Не удается найти сертификат ни в хранилище LocalMachine, ни в хранилище CurrentUser.
0
Fabian