Как использовать сертификат клиента для аутентификации и авторизации в веб-API
Я пытаюсь использовать сертификат клиента для аутентификации и авторизации устройств с использованием веб-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
Вот сертификат клиента, который я использую:
![введите описание изображения здесь]()
![введите описание изображения здесь]()
![введите описание изображения здесь]()
Я не понимаю, в чем проблема. Я добавляю щедрость для всех, кто может помочь мне понять это.
Ответы
Ответ 1
Трассировка помогла мне найти проблему (спасибо Fabian за это предложение). Я нашел с дальнейшим тестированием, что я могу получить сертификат клиента для работы на другом сервере (Windows Server 2012). Я тестировал это на своей машине разработки (Window 7), чтобы я мог отлаживать этот процесс. Таким образом, сравнивая трассировку с IIS Server, который работал, и тот, который мне не удалось определить соответствующие строки в журнале трассировки. Вот часть журнала, в котором работал клиентский сертификат. Это настройка прямо перед отправкой
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, который похож на мою проблему. Решение для меня было не тем, что было отмечено как ответ, так как мой сертификат был в доверенном корне. Ответ тот, что под ним, где вы обновляете реестр. Я просто добавил значение в раздел реестра.
HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL
Имя значения: SendTrustedIssuerList Тип значения: REG_DWORD Значение: 0 (False)
После добавления этого значения в реестр он начал работать на моей машине с Windows 7. Это, по-видимому, проблема Windows 7.
Ответ 2
Обновить:
Пример от 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();
}
}
}
Ответ 3
Убедитесь, что HttpClient имеет доступ к полному клиентскому сертификату (включая закрытый ключ).
Вы вызываете GetCert с файлом "ClientCertificate.cer", который приводит к предположению, что нет закрытого ключа, - скорее, это файл pfx в окнах.
Еще лучше получить доступ к сертификату из хранилища сертификатов Windows и выполнить поиск с помощью отпечатка пальца.
Соблюдайте осторожность при копировании отпечатка пальца. При просмотре в режиме управления сертификатом есть некоторые непечатаемые символы (скопируйте строку в блокнот ++ и проверьте длину отображаемой строки).
Ответ 4
У меня действительно была аналогичная проблема, когда у нас было много доверенных корневых сертификатов. У нашего нового установленного веб-сервера было много ресурсов. Наш корень начался с буквы Z, поэтому он оказался в конце списка.
Проблема заключалась в том, что IIS отправил клиенту только первые двадцать доверенных корней и урезал остальных, включая наш. Это было несколько лет назад, я не могу вспомнить название инструмента... он был частью пакета администрирования IIS, но Fiddler тоже должен был это сделать. После реализации ошибки мы удалили много доверенных корней, которые нам не нужны. Это было сделано проб и ошибок, поэтому будьте осторожны, что вы удаляете.
После очистки все работало как шарм.
Ответ 5
Посмотрев исходный код, я также думаю, что с закрытым ключом должна быть проблема.
То, что он делает, это проверить, прошел ли сертификат, который был передан, типа X509Certificate2 и если он имеет закрытый ключ.
Если он не находит закрытый ключ, он пытается найти сертификат в хранилище CurrentUser, а затем в магазине LocalMachine. Если он находит сертификат, он проверяет наличие закрытого ключа.
(см. исходный код из класса SecureChannnel, метод EnsurePrivateKey)
Таким образом, в зависимости от того, какой файл вы импортировали (.cer - без закрытого ключа или .pfx - с закрытым ключом) и в каком хранилище он может не найти правильный, а Request.ClientCertificate не будет заполнен.
Вы можете активировать Network Tracing, чтобы попытаться отладить это. Он даст вам выход следующим образом:
- Попытка найти соответствующий сертификат в хранилище сертификатов
- Не удается найти сертификат в хранилище LocalMachine или в хранилище CurrentUser.
Ответ 6
Недавно я столкнулся с подобной проблемой, и, следуя советам Фабиана, я фактически нашел решение. Оказывается, с клиентскими сертификатами вы должны обеспечить две вещи:
-
Закрытый ключ фактически экспортируется как часть сертификата.
-
Удостоверение пула приложений, в котором выполняется приложение, имеет доступ к указанному закрытому ключу.
В нашем случае мне пришлось:
- Импортируйте файл pfx в хранилище локального сервера, установив флажок экспорта, чтобы убедиться, что закрытый ключ был разослан.
- Используя консоль MMC, предоставьте служебной учетной записи доступ к закрытому ключу для сертификата.
Проблема доверенного корня, описанная в других ответах, является действительной, но в нашем случае это была не проблема.