2015-09-15 10 views
9

Chcę wysłać żądanie HTTPS do niestandardowego serwera z samopodpisanym certyfikatem. Używam NSURLConnection i uwierzytelniania klasy przetwarzanie wyzwania, ale zawsze komunikat o błędzie w konsoli:Tworzenie żądania HTTPS w systemie iOS 9 z samopodpisanym certyfikatem

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

następnie metodą „gra: didFailWithError:” jest wywoływana z powodu następującego błędu:

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} 

App otrzymuje dwa wyzwania uwierzytelnienia (NSURLAuthenticationMethodClientCertificate i NSURLAuthenticationMethodServerTrust) i przetwarza je w następujący sposób:

- (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]; 
} 

W dzienniku diagnostycznym CFNetwork widzę, że procedura Handshake ma się rozpocząć. Przynajmniej aplikacja wysyła wiadomość "ClientHello", a następnie serwer wysyła wiadomość "ServerHello" i wymaga uwierzytelnienia. I tu aplikacja próbuje wysłać odpowiedź uwierzytelnienia, ale natychmiast otrzymuje błąd. (W tym samym czasie w dziennikach serwera nie widzę żadnych komunikatów o uścisku dłoni). Oto część dziennika diagnostycznego:

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] 

Nasza instancja back-end mogą być instalowane po stronie klienta, więc nie można ustawić żadnego wyjątku domeny w Info.plist pliku. Również aplikacja może żądać serwera za pomocą adresu IP w postaci IPv4, ale nie według nazwy domeny (jak w moim przykładzie).

co ja próbowałem:

  • wykorzystywane NSURLSession zamiast NSURLConnection, ale bez powodzenia;
  • sprawdził wymagania ATS Apple dotyczące wdrożenia serwera here (programista back-end jest pewien, że jego implementacja spełnia wszystkie z nich);
  • odtwarzane z ustawianiem certyfikatów zakotwiczeń do sprawdzania zaufania zgodnie z różnymi rozwiązanymi problemami ze stackoverflow i forów deweloperskich Apple;
  • zwrócił uwagę szczególnie na numer similar i powiązane z nim solution na forach programistów;

Testuję żądanie https na iPadzie Air 2 z iOS 9 GM Seed (Build 13A340) i XCode 7 GM Seed (Build 7A218). Ważna uwaga: ta funkcjonalność działa dobrze z iOS 8. Biorąc to pod uwagę, mogę założyć, że problem jest na naszym serwerze, ale nasz programista zapewnił mnie, że wszystko jest w porządku.

Teraz nie mam pomysłów. Byłbym wdzięczny, gdyby ktoś mógł dać mi wskazówkę, lub przynajmniej zasugerować inną diagnostykę, która ujawniłaby konkretny błąd, bardziej konkretny niż "śmiertelny alarm".

Dzięki.

EDYCJA 1: SecTrustEvaluate zawsze zwraca kSecTrustResultRecoverableTrustFailure, dlatego musiałem znaleźć jakiś sposób obejścia tego problemu.

+0

Znalazłeś rozwiązanie? Mam również ten problem i chciałbym użyć mojego lokalnego serwera do testowania, ale z samopodpisanym certyfikatem niemożliwe jest działanie i otrzymuję te same błędy ... –

+0

Jeszcze nie, muszę pracować nad innymi priorytetowymi zadaniami z kilku powodów. Skontaktuję się z Tobą, jeśli będę miał rozwiązanie. – alfared

+0

Jest to błąd protokołu PITA iOS. (PITA oznacza ból w ...) – Josh

Odpowiedz

0

Ten problem został rozwiązany jakiś czas temu. Okazało się, że jest to nieważny samopodpisany certyfikat. Nie spełniało wszystkich wymagań Apple. Niestety nie wiem, co to dokładnie było.

3

Czy użyłeś nscurl do zdiagnozowania problemu z połączeniem? Jeśli masz Maca z systemem OS X 10.11, możesz uruchomić coś takiego:

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

Ewentualnie, jeśli nie masz 10.11, można pobrać przykładowy kod tutaj: https://developer.apple.com/library/mac/samplecode/SC1236/ i zbudować go z Xcode i uruchomić go tak (zmieniając ścieżkę odpowiednią dla danej maszyny):

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

(Aby znaleźć pełną ścieżkę do powyższego, po Zbudowałeś, otwórz grupę Produkty w Nawigatorze projektu, kliknij prawym przyciskiem myszy TLSTool i "Pokaż w Finderze".)

Masz już link do noty technicznej Apple na ten temat, https://developer.apple.com/library/prerelease/ios/technotes/App-Transport-Security-Technote/, ale nie powiedziałeś, że pobiegł nscurl lub nie.

+0

Punkt końcowy mojej maszyny wirtualnej przechodzi wszystkie testy w/usr/bin/nscurl --ats-diagnostics [https: //moja_domena.local: 8888] (https: // mydomain. local: 8888), ale nadal otrzymuję błąd w symulatorze. –

3

Zgodnie z tym: https://forums.developer.apple.com/message/36842#36842

Najlepszym podejściem do ustalenia obciążenia HTTP nie powiodło się (kCFStreamErrorDomainSSL, -9802) jest ustawiony wyjątek w pliku Info.plist następująco:

<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> 

Ważną chodzi o to, że nie jest to mniej bezpieczne niż iOS8, po prostu nie tak bezpieczne, jak pełne ATS obsługiwane przez iOS9.

+1

Dzięki - działało świetnie po zamknięciu tagów dyktatorskich. – dlw

+1

Wielkie dzięki, ale: 1. To pozwala na zwykłe pobieranie HTTP i potrzebuję HTTPS (ze względów bezpieczeństwa). 2. Jak napisałem, nie mogę ustawić żadnej domeny na liście wyjątków, ponieważ nasi klienci często instalują back-end na swoich serwerach. Na przykład może to być "server.company1.com" i "server.company2.com". Twoja odpowiedź zakłada, że ​​muszę ponownie zbudować aplikację i dodać nową domenę wyjątków "server.company3.com" po tym, jak sprzedamy aplikację do trzeciej firmy. – alfared

+0

Może być możliwe użycie kombinacji informacji z dokumentacji firmy Apple w witrynie ATS oraz w tym poście na blogu w przypadku certyfikatów z podpisem własnym # 5, w szczególności w celu rozwiązania problemu (Prawdopodobnie będziesz musiał powtórzyć kilka razy, dopóki nie uzyskasz poprawnych wyników). – spirographer

0

właśnie spotkał ten sam problem i rozwiązuje z ur's.Now it.It dlatego, że wersja TLS oraz świadectwo sign.As dokument jabłkiem mówią poniżej apple's document

więc robię to coś info.plist setting

i działa

Powiązane problemy