Как подключить открытый ключ сертификата к iOS

При повышении безопасности разрабатываемого нами приложения iOS мы обнаружили необходимость PIN-кода (всего или части) SSL-сертификата сервера для предотвращения атак типа "человек-в-середине".

Несмотря на то, что для этого существуют различные подходы, когда вы ищете это, я нашел примеры для привязки всего сертификата. Такая практика создает проблему: как только сертификат будет обновлен, ваше приложение больше не сможет подключаться. Если вы решите привязать открытый ключ, а не весь сертификат, вы окажетесь (как я полагаю) в одинаково безопасной ситуации, будучи более устойчивым к обновлениям сертификатов на сервере.

Но как вы это делаете?

Ответ 1

Если вам нужно знать, как извлечь эту информацию из сертификата в вашем коде 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;
}

}

Ответ 2

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

Ответ 3

Вы можете использовать шифрование SSL с открытым ключом, используя функцию SecTrustCopyPublicKey в Security.framework. См. Пример в connection: willSendRequestForAuthenticationChallenge: проекта AFNetworking.

Если вам нужна openSSL для iOS, используйте https://gist.github.com/foozmeat/5154962 Он основан на st3fan/ios-openssl, который в настоящее время не работает.

Ответ 4

Вы можете использовать плагин PhoneGap (Build), упомянутый здесь: http://www.x-services.nl/certificate-pinning-plugin-for-phonegap-to-prevent-man-in-the-middle-attacks/734

Плагин поддерживает несколько сертификатов, поэтому сервер и клиент не должны обновляться одновременно. Если ваш отпечаток изменится каждый раз (скажем) на 2 года, а затем реализуйте механизм для принудительного обновления клиентов (добавьте версию в приложение и создайте на сервере API-метод minimumRequiredVersion). Сообщите клиенту обновить, если версия приложения слишком низкий (fi при активации нового сертификата).

Ответ 5

Если вы используете AFNetworking (точнее, AFSecurityPolicy), и вы выбираете режим AFSSLPinningModePublicKey, не имеет значения, изменились ли ваши сертификаты или нет, если открытые ключи остаются неизменными. Да, это правда, что AFSecurityPolicy не предоставляет метод, позволяющий вам напрямую устанавливать ваши открытые ключи; вы можете устанавливать только свои сертификаты, вызывая setPinnedCertificates. Однако, если вы посмотрите на реализацию setPinnedCertificates, вы увидите, что структура распаковывает открытые ключи из сертификатов и затем сравнивает ключи.

Короче говоря, пройдите сертификаты и не беспокойтесь о том, что они меняются в будущем. Рамки заботятся только об открытых ключах в этих сертификатах.

Следующий код работает для меня.

AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
[manager.securityPolicy setPinnedCertificates:myCertificate];

Ответ 6

... для закрепления всего сертификата. Такая практика создает проблему...

Кроме того, Google ежемесячно меняет сертификат (или так), но сохраняет или повторно подтверждает публикацию. Таким образом, выдача сертификата приведет к множеству ложных предупреждений, в то время как пиннинг с открытым ключом будет проходить тесты на непрерывность ключа.

Я считаю, что Google делает это, чтобы сохранить списки CRL, OCSP и списки отзыва управляемыми, и я ожидаю, что другие тоже это сделают. Для моих сайтов я обычно повторно сертифицирую ключи так, чтобы люди обеспечивали непрерывность ключа.

Но как вы это делаете?

Сертификат и открытый ключ. В статье обсуждается практика и предлагается образец кода для OpenSSL, Android, iOS и .Net. Существует, по крайней мере, одна проблема с iOS, существующей на платформе, обсуждаемой в iOS: Предоставить значащую ошибку из NSUrlConnection didReceiveAuthenticationChallenge (отказ сертификата).

Кроме того, Питер Гутманн отлично справляется с ключевой непрерывностью и закреплением в своей книге Инженерная безопасность.

Ответ 7

Если вы используете AFNetworking, используйте AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];

Ответ 8

Вот ответ Swifty. Сохраните сертификат (как .cer файл) вашего веб-сайта в главном комплекте. Затем используйте this Метод 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, например this.