Как закрепить открытый ключ сертификата на iOS
Повышая безопасность разрабатываемого нами приложения для iOS, мы обнаружили необходимость в ПИН-коде (полностью или частично) SSL-сертификата сервера для предотвращения атак "человек посередине".
Несмотря на то, что для этого есть разные подходы, при поиске я нашел только примеры закрепления всего сертификата. Такая практика создает проблему: как только сертификат будет обновлен, ваше приложение больше не сможет подключиться. Если вы решите закрепить открытый ключ вместо всего сертификата, вы окажетесь (я считаю) в такой же безопасной ситуации, при этом будучи более устойчивым к обновлениям сертификатов на сервере.
Но как ты это делаешь?
8 ответов
Если вам нужно знать, как извлечь эту информацию из сертификата в коде iOS, у вас есть один способ сделать это.
Прежде всего, добавьте структуру безопасности.
#import <Security/Security.h>
Добавить библиотеки openssl. Вы можете скачать их с https://github.com/st3fan/ios-openssl
#import <openssl/x509.h>
Протокол NSURLConnectionDelegate позволяет вам решить, должно ли соединение быть в состоянии ответить на пространство защиты. В двух словах, это когда вы можете взглянуть на сертификат, поступающий с сервера, и принять решение разрешить или отключить соединение. Здесь вы хотите сравнить открытый ключ сертификата с тем, который вы закрепили. Теперь вопрос в том, как получить такой открытый ключ? Посмотрите на следующий код:
Сначала получите сертификат в формате X509 (для этого вам понадобятся библиотеки ssl)
const unsigned char *certificateDataBytes = (const unsigned char *)[serverCertificateData bytes];
X509 *certificateX509 = d2i_X509(NULL, &certificateDataBytes, [serverCertificateData length]);
Теперь мы подготовимся к чтению данных открытого ключа.
ASN1_BIT_STRING *pubKey2 = X509_get0_pubkey_bitstr(certificateX509);
NSString *publicKeyString = [[NSString alloc] init];
На этом этапе вы можете перебрать строку pubKey2 и извлечь байты в формате HEX в строку с помощью следующего цикла
for (int i = 0; i < pubKey2->length; i++)
{
NSString *aString = [NSString stringWithFormat:@"%02x", pubKey2->data[i]];
publicKeyString = [publicKeyString stringByAppendingString:aString];
}
Распечатайте открытый ключ, чтобы увидеть его
NSLog(@"%@", publicKeyString);
Полный код
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace
{
const unsigned char *certificateDataBytes = (const unsigned char *)[serverCertificateData bytes];
X509 *certificateX509 = d2i_X509(NULL, &certificateDataBytes, [serverCertificateData length]);
ASN1_BIT_STRING *pubKey2 = X509_get0_pubkey_bitstr(certificateX509);
NSString *publicKeyString = [[NSString alloc] init];
for (int i = 0; i < pubKey2->length; i++)
{
NSString *aString = [NSString stringWithFormat:@"%02x", pubKey2->data[i]];
publicKeyString = [publicKeyString stringByAppendingString:aString];
}
if ([publicKeyString isEqual:myPinnedPublicKeyString]){
NSLog(@"YES THEY ARE EQUAL, PROCEED");
return YES;
}else{
NSLog(@"Security Breach");
[connection cancel];
return NO;
}
}
Насколько я могу сказать, вы не можете легко создать ожидаемый открытый ключ непосредственно в iOS, вам нужно сделать это с помощью сертификата. Таким образом, необходимые шаги аналогичны закреплению сертификата, но дополнительно необходимо извлечь открытый ключ из фактического сертификата и из справочного сертификата (ожидаемый открытый ключ).
Что вам нужно сделать, это:
- Используйте NSURLConnectionDelegate для извлечения данных и реализации
willSendRequestForAuthenticationChallenge
, - Включить справочный сертификат в формате DER. В примере я использовал простой файл ресурсов.
- Извлечь открытый ключ, представленный сервером
- Извлеките открытый ключ из вашего справочного сертификата
- Сравните два
- Если они совпадают, продолжайте регулярные проверки (имя хоста, подписание сертификата и т. Д.)
- Если они не совпадают, потерпите неудачу.
Пример кода:
(void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
// get the public key offered by the server
SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
SecKeyRef actualKey = SecTrustCopyPublicKey(serverTrust);
// load the reference certificate
NSString *certFile = [[NSBundle mainBundle] pathForResource:@"ref-cert" ofType:@"der"];
NSData* certData = [NSData dataWithContentsOfFile:certFile];
SecCertificateRef expectedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData);
// extract the expected public key
SecKeyRef expectedKey = NULL;
SecCertificateRef certRefs[1] = { expectedCertificate };
CFArrayRef certArray = CFArrayCreate(kCFAllocatorDefault, (void *) certRefs, 1, NULL);
SecPolicyRef policy = SecPolicyCreateBasicX509();
SecTrustRef expTrust = NULL;
OSStatus status = SecTrustCreateWithCertificates(certArray, policy, &expTrust);
if (status == errSecSuccess) {
expectedKey = SecTrustCopyPublicKey(expTrust);
}
CFRelease(expTrust);
CFRelease(policy);
CFRelease(certArray);
// check a match
if (actualKey != NULL && expectedKey != NULL && [(__bridge id) actualKey isEqual:(__bridge id)expectedKey]) {
// public keys match, continue with other checks
[challenge.sender performDefaultHandlingForAuthenticationChallenge:challenge];
} else {
// public keys do not match
[challenge.sender cancelAuthenticationChallenge:challenge];
}
if(actualKey) {
CFRelease(actualKey);
}
if(expectedKey) {
CFRelease(expectedKey);
}
}
Отказ от ответственности: это только пример кода, а не тщательно проверен. Для полной реализации начните с примера закрепления сертификата OWASP.
И помните, что закрепления сертификата всегда можно избежать с помощью SSL Kill Switch и аналогичных инструментов.
Вы можете сделать закрепление SSL с открытым ключом, используя SecTrustCopyPublicKey
функция Security.framework. См. Пример при подключении: willSendRequestForAuthenticationChallenge: проекта AFNetworking.
Если вам нужен openSSL для iOS, используйте https://gist.github.com/foozmeat/5154962 Он основан на st3fan/ios-openssl, который в настоящее время не работает.
Вы можете использовать плагин PhoneGap (Build), упомянутый здесь: http://www.x-services.nl/certificate-pinning-plugin-for-phonegap-to-prevent-man-in-the-middle-attacks/734
Плагин поддерживает несколько сертификатов, поэтому сервер и клиент не нуждаются в обновлении одновременно. Если ваш отпечаток пальца меняется, скажем, каждые 2 года, то внедрите механизм принуждения клиентов к обновлению (добавьте версию в ваше приложение и создайте метод API "minimalRequiredVersion" на сервере. Скажите клиенту обновить, если версия приложения слишком низкий (например, когда новый сертификат активирован).
Если вы используете AFNetworking (точнее, AFSecurityPolicy) и выбираете режим AFSSLPinningModePublicKey, не имеет значения, изменяются ваши сертификаты или нет, если открытые ключи остаются прежними. Да, это правда, что AFSecurityPolicy не предоставляет метод для вас непосредственно установить ваши открытые ключи; Вы можете установить свои сертификаты только по телефону setPinnedCertificates
, Однако, если вы посмотрите на реализацию setPinnedCertificates, вы увидите, что инфраструктура извлекает открытые ключи из сертификатов, а затем сравнивает ключи.
Короче говоря, передайте сертификаты и не беспокойтесь об их изменении в будущем. Фреймворк заботится только об открытых ключах в этих сертификатах.
Следующий код работает для меня.
AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
[manager.securityPolicy setPinnedCertificates:myCertificate];
Здесь ответ Swifty. Сохраните сертификат (как файл.cer) вашего сайта в основном комплекте. Затем используйте этот метод URLSessionDelegate:
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {
guard
challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
let serverTrust = challenge.protectionSpace.serverTrust,
SecTrustEvaluate(serverTrust, nil) == errSecSuccess,
let serverCert = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
reject(with: completionHandler)
return
}
let serverCertData = SecCertificateCopyData(serverCert) as Data
guard
let localCertPath = Bundle.main.path(forResource: "shop.rewe.de", ofType: "cer"),
let localCertData = NSData(contentsOfFile: localCertPath) as Data?,
localCertData == serverCertData else {
reject(with: completionHandler)
return
}
accept(with: serverTrust, completionHandler)
}
...
func reject(with completionHandler: ((URLSession.AuthChallengeDisposition, URLCredential?) -> Void)) {
completionHandler(.cancelAuthenticationChallenge, nil)
}
func accept(with serverTrust: SecTrust, _ completionHandler: ((URLSession.AuthChallengeDisposition, URLCredential?) -> Void)) {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
}
Вы можете получить файл.cer с помощью Chrome следующим образом.
... для закрепления всего сертификата. Такая практика создает проблему...
Кроме того, Google меняет сертификат ежемесячно (или около того), но сохраняет или повторно сертифицирует публику. Таким образом, закрепление сертификата приведет к множеству ложных предупреждений, а закрепление открытого ключа пройдет проверку целостности ключа.
Я полагаю, что Google делает это для того, чтобы CRL, OCSP и списки отзыва были управляемыми, и я ожидаю, что это сделают и другие. Для моих сайтов я обычно заново сертифицирую ключи, чтобы обеспечить непрерывность ключей.
Но как ты это делаешь?
Закрепление сертификата и открытого ключа. В статье обсуждается практика и предлагается пример кода для OpenSSL, Android, iOS и.Net. Существует, по крайней мере, одна проблема с iOS, связанная с платформой, обсуждаемой в iOS: обеспечить значимую ошибку от NSUrlConnection didReceiveAuthenticationChallenge (Ошибка сертификата).
Кроме того, Питер Гутманн прекрасно рассматривает последовательность и закрепление в своей книге " Инженерная безопасность".
Если вы используете AFNetworking, используйте AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];