Я пытаюсь использовать сертификат клиента для аутентификации и авторизации устройств с использованием веб-API и разработал простое доказательство концепции для решения проблем с возможным решением. Я столкнулся с проблемой, когда веб-приложение не получает сертификат клиента. Ряд людей сообщили об этой проблеме, в том числе в этом Q & A, но ни у одного из них нет ответа. Моя надежда состоит в том, чтобы предоставить более подробную информацию, чтобы оживить эту проблему и, надеюсь, получить ответ на мою проблему. Я открыт для других решений. Основное требование состоит в том, что автономный процесс, написанный на С#, может вызывать веб-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 для этого веб-приложения.
Вот код для клиента, который отправляет запрос с сертификатом клиента. Это консольное приложение.
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 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.
Обновление 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
Вот сертификат клиента, который я использую:
Я не понимаю, в чем проблема. Я добавляю щедрость для всех, кто может помочь мне понять это.