it-roy-ru.com

Выполнить HTTPS-запрос в iOS 9 с самозаверяющим сертификатом

Я хочу сделать HTTPS-запрос на пользовательский сервер с самозаверяющим сертификатом. Я использую класс NSURLConnection и обрабатываю задачи аутентификации, но всегда получаю сообщение об ошибке в консоли:

NSURLSession/NSURLConnection HTTP load failed (kCFStreamErrorDomainSSL, -9802)

тогда метод "connection: didFailWithError:" вызывается со следующей ошибкой:

Error Domain=NSURLErrorDomain Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made." UserInfo={NSURLErrorFailingURLPeerTrustErrorKey=<SecTrustRef: 0x150094100>, NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9802, NSErrorPeerCertificateChainKey=<CFArray 0x1500ddd90 [0x19f6dab68]>{type = immutable, count = 1, values = (
    0 : <cert(0x14e6fb370) s: (server certificate name) i: (custom CA name)>
)}, NSUnderlyingError=0x1504ae170 {Error Domain=kCFErrorDomainCFNetwork Code=-1200 "An SSL error has occurred and a secure connection to the server cannot be made." UserInfo={NSErrorFailingURLStringKey=https://217.92.80.156:9090/(method name and parameters), NSLocalizedRecoverySuggestion=Would you like to connect to the server anyway?, _kCFNetworkCFStreamSSLErrorOriginalValue=-9802, kCFStreamPropertySSLPeerCertificates=<CFArray 0x1500ddd90 [0x19f6dab68]>{type = immutable, count = 1, values = (
    0 : <cert(0x14e6fb370) s: (server certificate name) i: (custom CA name)>
)}, _kCFStreamPropertySSLClientCertificateState=2, kCFStreamPropertySSLPeerTrust=<SecTrustRef: 0x150094100>, NSLocalizedDescription=An SSL error has occurred and a secure connection to the server cannot be made., _kCFStreamPropertySSLClientCertificates=<CFArray 0x14e5ee8e0 [0x19f6dab68]>{type = mutable-small, count = 2, values = (
    0 : <SecIdentityRef: 0x15012cd40>
    1 : <cert(0x15014aa70) s: (client certificate name) i: (custom CA name)>
)}, _kCFStreamErrorDomainKey=3, NSErrorFailingURLKey=https://217.92.80.156:9090/(method name and parameters), _kCFStreamErrorCodeKey=-9802}}, NSErrorClientCertificateChainKey=<CFArray 0x14e5ee8e0 [0x19f6dab68]>{type = mutable-small, count = 2, values = (
    0 : <SecIdentityRef: 0x15012cd40>
    1 : <cert(0x15014aa70) s: (client certificate name) i: (custom CA name)>
)}, NSLocalizedDescription=An SSL error has occurred and a secure connection to the server cannot be made., NSErrorFailingURLKey=https://217.92.80.156:9090/(method name and parameters), NSErrorFailingURLStringKey=https://217.92.80.156:9090/(method name and parameters), NSErrorClientCertificateStateKey=2}

Приложение получает две проверки подлинности (NSURLAuthenticationMethodClientCertificate и NSURLAuthenticationMethodServerTrust) и обрабатывает их следующим образом:

- (void) connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
    if(challenge.proposedCredential && !challenge.error)
    {
        [challenge.sender useCredential:challenge.proposedCredential forAuthenticationChallenge:challenge];

        return;
    }

    NSString *strAuthenticationMethod = challenge.protectionSpace.authenticationMethod;
    NSLog(@"authentication method: %@", strAuthenticationMethod);

    NSURLCredential *credential = nil;
    if([strAuthenticationMethod isEqualToString:NSURLAuthenticationMethodClientCertificate])
    {
        // get identity and certificate from p.12
        NSData *PKCS12Data = [NSData dataWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"client" ofType:@"p12"]];

        NSDictionary *optionsDictionary = [NSDictionary dictionaryWithObject:@"password" forKey:(__bridge id)kSecImportExportPassphrase];
        CFArrayRef items = CFArrayCreate(NULL, 0, 0, NULL);
        OSStatus securityError = SecPKCS12Import((__bridge CFDataRef)PKCS12Data,(__bridge CFDictionaryRef)optionsDictionary, &items);

        SecIdentityRef identity = NULL;
        SecCertificateRef certificate = NULL;
        if(securityError == errSecSuccess)
        { 
            CFDictionaryRef myIdentityAndTrust = CFArrayGetValueAtIndex(items, 0);
            identity = (SecIdentityRef)CFDictionaryGetValue (myIdentityAndTrust, kSecImportItemIdentity);

            CFArrayRef array = (CFArrayRef)CFDictionaryGetValue(myIdentityAndTrust, kSecImportItemCertChain);
            certificate = (SecCertificateRef)CFArrayGetValueAtIndex(array, 0);
        }

        credential = [NSURLCredential credentialWithIdentity:identity certificates:[NSArray arrayWithObject:(__bridge id)(certificate)] persistence:NSURLCredentialPersistenceNone];

        CFRelease(items);
    }
    else if([strAuthenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust])
    {       
        int trustCertificateCount = (int)SecTrustGetCertificateCount(challenge.protectionSpace.serverTrust);
        NSMutableArray *trustCertificates = [[NSMutableArray alloc] initWithCapacity:trustCertificateCount];
        for(int i = 0; i < trustCertificateCount; i ++)
        {
            SecCertificateRef trustCertificate =  SecTrustGetCertificateAtIndex(challenge.protectionSpace.serverTrust, i);
            [trustCertificates addObject:(__bridge id) trustCertificate];
        }            

        SecPolicyRef policyRef = NULL;
        policyRef = SecPolicyCreateSSL(YES, (__bridge CFStringRef) challenge.protectionSpace.Host);

        SecTrustRef trustRef = NULL;
        if(policyRef)
        {
            SecTrustCreateWithCertificates((__bridge CFArrayRef) trustCertificates, policyRef, &trustRef);
            CFRelease(policyRef);
        }

        if(trustRef)
        {
//          SecTrustSetAnchorCertificates(trustRef, (__bridge CFArrayRef) [NSArray array]);
//          SecTrustSetAnchorCertificatesOnly(trustRef, NO);

            SecTrustResultType result;
            OSStatus trustEvalStatus = SecTrustEvaluate(trustRef, &result);
            if(trustEvalStatus == errSecSuccess)
            {
                // just temporary attempt to make it working.
                // i hope, there is no such problem, when we have final working version of certificates.
                if(result == kSecTrustResultRecoverableTrustFailure)
                {
                    CFDataRef errDataRef = SecTrustCopyExceptions(trustRef);
                    SecTrustSetExceptions(trustRef, errDataRef);

                    SecTrustEvaluate(trustRef, &result);
                }

                if(result == kSecTrustResultProceed || result == kSecTrustResultUnspecified)
                    credential = [NSURLCredential credentialForTrust:trustRef];
            }

            CFRelease(trustRef);
        }
    }
    else
    {
        DDLogWarn(@"Unexpected authentication method. Cancelling authentication ...");
        [challenge.sender cancelAuthenticationChallenge:challenge];
    }

    if(credential)
        [challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
    else
        [challenge.sender cancelAuthenticationChallenge:challenge];
}

В журнале диагностики CFNetwork я вижу, что процедура рукопожатия вот-вот начнется. По крайней мере, приложение отправляет сообщение «ClientHello», затем сервер отправляет свое сообщение «ServerHello» и требует проверки подлинности. И здесь приложение пытается отправить ответ аутентификации, но сразу же получает ошибку. (В то же время в журналах сервера я вообще не вижу сообщений о рукопожатии). Вот часть диагностического журнала:

Sep 15 10:51:49  AppName[331] <Notice>: CFNetwork Diagnostics [3:49] 10:51:49.185 {
    Authentication Challenge
       Loader: <CFURLRequest 0x1501931c0 [0x19f6dab68]> {url = https://217.92.80.156:9090/(method name and parameters), cs = 0x0}
    Challenge: challenge space https://217.92.80.156:9090/, ServerTrustEvaluationRequested (Hash f9810ad8165b3620)
    } [3:49]
Sep 15 10:51:49  AppName[331] <Notice>: CFNetwork Diagnostics [3:50] 10:51:49.189 {
    Use Credential
        Loader: <CFURLRequest 0x1501931c0 [0x19f6dab68]> {url = https://217.92.80.156:9090/(method name and parameters), cs = 0x0}
    Credential: Name: server, Persistence: session
    } [3:50]
Sep 15 10:51:49  AppName[331] <Notice>: CFNetwork Diagnostics [3:51] 10:51:49.190 {
     touchConnection
              Loader: <CFURLRequest 0x1501931c0 [0x19f6dab68]> {url = https://217.92.80.156:9090/(method name and parameters), cs = 0x0}
    Timeout Interval: 60.000 seconds
    } [3:51]
Sep 15 10:51:49  AppName[331] <Notice>: CFNetwork Diagnostics [3:52] 10:51:49.192 {
    Response Error
    Request: <CFURLRequest 0x14e5d02a0 [0x19f6dab68]> {url = https://217.92.80.156:9090/(method name and parameters), cs = 0x0}
      Error: Error Domain=kCFErrorDomainCFNetwork Code=-1200 "(null)" UserInfo={_kCFNetworkCFStreamSSLErrorOriginalValue=-9802, kCFStreamPropertySSLPeerCertificates=<CFArray 0x1500ddd90 [0x19f6dab68]>{type = immutable, count = 1, values = (
                0 : <cert(0x14e6fb370) s: (server certificate name) i: (custom CA name)>
             )}, _kCFStreamPropertySSLClientCertificateState=2, kCFStreamPropertySSLPeerTrust=<SecTrustRef: 0x150094100>, _kCFStreamPropertySSLClientCertificates=<CFArray 0x14e5ee8e0 [0x19f6dab68]>{type = mutable-small, count = 2, values = (
                0 : <SecIdentityRef: 0x15012cd40>
                1 : <cert(0x15014aa70) s: (client certificate name) i: (custom CA name)>
             )}, _kCFStreamErrorDomainKey=3, _kCFStreamErrorCodeKey=-9802}
    } [3:52]

Наш серверный экземпляр может быть установлен на стороне клиента, поэтому я не могу установить исключение для домена в файле Info.plist. Также приложение может запрашивать сервер по IP-адресу в форме IPv4, но не по имени домена (как в моем примере).

Что я попробовал:

  • использовал NSURLSession вместо NSURLConnection, но безуспешно;
  • проверил требования Apple к ATS для реализации сервера here (внутренний разработчик уверен, что его реализация соответствует всем им);
  • играли с установкой якорных сертификатов для проверки доверия в соответствии с различными решенными проблемами из stackoverflow и форумов разработчиков Apple;
  • уделил особое внимание похожему сообщению и связанному с ним solution на форумах разработчиков;

Я испытываю запрос https на iPad Air 2 с iOS 9 GM Seed (сборка 13A340) и xCode 7 GM Seed (сборка 7A218). Важное примечание: эта функциональность прекрасно работает с iOS 8. Принимая это во внимание, я могу предположить, что проблема в нашем сервере, но наш внутренний разработчик заверил меня, что там все в порядке.

Теперь у меня нет идей. Я был бы признателен, если бы кто-нибудь дал мне подсказку или хотя бы предложил какую-то другую диагностику, которая выявила бы конкретную ошибку, более конкретную, чем «фатальная тревога».

Благодарю.

Правка 1: SecTrustEvaluate всегда возвращает kSecTrustResultRecoverableTrustFailure, поэтому я должен был найти какой-то обходной путь.

10
alfared

Эта проблема была решена некоторое время назад. Оказалось недействительным самозаверяющий сертификат. Он не отвечал всем требованиям Apple. К сожалению, я не знаю, что именно это было.

0
alfared

В соответствии с этим: https://forums.developer.Apple.com/message/36842#36842

Наилучший подход для исправления сбоя загрузки HTTP (kCFStreamErrorDomainSSL, -9802) - это установить исключение в файле info.plist следующим образом:

<key>NSAppTransportSecurity</key>
<dict>
  <key>NSExceptionDomains</key>
  <dict>
    <key>test.testdomain.com</key>
    <dict>
      <key>NSIncludesSubdomains</key>
      <true/>
      <key>NSExceptionAllowsInsecureHTTPLoads</key>
      <true/>
    </dict>
  </dict>
</dict>

Важным моментом является то, что это не менее безопасно, чем iOS8, просто не так безопасно, как полная ATS, поддерживаемая iOS9.

3
spirographer

Вы использовали nscurl для диагностики проблемы с соединением? Если у вас Mac под управлением OS X v10.11, вы можете запустить что-то вроде этого:

/usr/bin/nscurl --ats-diagnostics https://www.yourdomain.com

В качестве альтернативы, если у вас нет 10.11, вы можете скачать пример кода здесь: https://developer.Apple.com/library/mac/samplecode/SC1236/ и собрать его с помощью XCode и запустить его следующим образом (изменение пути в соответствии с вашей машиной):

/Users/somebody/Library/Developer/Xcode/DerivedData/TLSTool-hjuytnjaqebcfradighsrffxxyzq/Build/Products/Debug/TLSTool s_client -connect www.yourdomain.com:443

(Чтобы найти полный путь к вышеперечисленному, после того, как вы построите, откройте группу «Продукты» в навигаторе проекта, щелкните правой кнопкой мыши TLSTool и «Показать в Finder».)

Вы уже связались с техническими комментариями Apple по этому вопросу, https://developer.Apple.com/library/prerelease/ios/technotes/App-Transport-Security-Technote/ , но вы не сказали, запускали ли вы nscurl или нет.

3
DFedor

Я только что столкнулся с той же проблемой, что и у вас. Теперь я исправляю это. Это потому, что версия tls и знак сертификата. Как говорится в документе Apple ниже Документ Apple

Так что я делаю эту вещь настройка info.plist

и это работает

0
zms