Переопределяющая проверка цепочки TLS правильно

Эта статья описывает, как переопределить цепочечное поведение проверки сетевых соединений, защищенных с Transport Layer Security (TLS).

Когда сертификат TLS проверяется, операционная система проверяет свою цепочку доверия. Если та цепочка доверия содержит только допустимые сертификаты, и концы в известном (доверяли) сертификату привязки, то сертификат считают допустимым. Если это не делает, это считают недопустимым. При использовании коммерчески подписанного сертификата от крупного поставщика сертификат должен “просто работать”.

Однако, если Вы делаете что-то, что выходит за пределы нормы — создание клиентских сертификатов для Ваших пользователей, предоставление услуги для многократных доменов с единственным сертификатом, которому не доверяет для тех доменов, с помощью самоподписанного сертификата, соединяясь с узлом IP-адрес (где сетевой стек не может определить имя хоста сервера), и т.д. — необходимо предпринять дополнительные шаги, чтобы убедить операционную систему принимать сертификат.

На высоком уровне проверка цепочки TLS выполняется доверительным объектом (SecTrustRef). Этот объект содержит много флагов, управляющих тем, какие типы проверки выполняются. Как правило Вы не должны касаться этих флагов, но необходимо знать об их существовании. Кроме того, доверительный объект содержит политику (SecPolicyRef) это позволяет Вам обеспечивать имя хоста, которое должно использоваться при оценке сертификата TLS. Наконец, доверительный объект содержит список доверяемых сертификатов привязки, которые может изменить Ваше приложение.

Эта статья разделяется на многократные части. Первая часть, Управляя Доверительными Объектами, описывает распространенные способы управлять доверительным объектом изменить поведение проверки. Остающиеся разделы, Доверительные Объекты и NSURLConnection и Доверительные Объекты и NSStream, показывают, как интегрировать те изменения с различными сетевыми технологиями.

Управление доверительными объектами

Подробные данные управления доверительным объектом зависят в значительной степени от того, что Вы пытаетесь переопределить. Двумя наиболее распространенными вещами переопределить является имя хоста (который должен соответствовать или листовое общее название сертификата или одно из имен в его Подчиненном расширении Альтернативного названия), и набор привязок (которые определяют ряд доверенных центров сертификации).

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

Перечисление 1  , Добавляющее привязку к a SecTrustRef объект

SecTrustRef addAnchorToTrust(SecTrustRef trust, SecCertificateRef trustedCert)
{
#ifdef PRE_10_6_COMPAT
        CFArrayRef oldAnchorArray = NULL;
 
        /* In OS X prior to 10.6, copy the built-in
           anchors into a new array. */
        if (SecTrustCopyAnchorCertificates(&oldAnchorArray) != errSecSuccess) {
                /* Something went wrong. */
                return NULL;
        }
 
        CFMutableArrayRef newAnchorArray = CFArrayCreateMutableCopy(
                kCFAllocatorDefault, 0, oldAnchorArray);
        CFRelease(oldAnchorArray);
#else
        /* In iOS and OS X v10.6 and later, just create an empty
           array. */
        CFMutableArrayRef newAnchorArray = CFArrayCreateMutable (
                kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
#endif
 
        CFArrayAppendValue(newAnchorArray, trustedCert);
 
        SecTrustSetAnchorCertificates(trust, newAnchorArray);
 
#ifndef PRE_10_6_COMPAT
        /* In iOS or OS X v10.6 and later, reenable the
           built-in anchors after adding your own.
         */
        SecTrustSetAnchorCertificatesOnly(trust, false);
#endif
 
        return trust;

Для переопределения имени хоста (чтобы позволить сертификату для одного определенного сайта работать на другой определенный сайт или позволять сертификату работать, когда Вы соединились с узлом его IP-адресом) необходимо заменить объект политики, что доверительное использование политики, чтобы определить, как интерпретировать сертификат. Чтобы сделать это, сначала создайте новый объект политики TLS для желаемого имени хоста. Тогда создайте массив, содержащий ту политику. Наконец, скажите доверительному объекту использовать тот массив для будущей оценки доверия. Перечисление 2 показывает функцию, делающую это.

Перечисление 2  , Изменяющее удаленное имя хоста для a SecTrustRef объект

SecTrustRef changeHostForTrust(SecTrustRef trust)
{
        CFMutableArrayRef newTrustPolicies = CFArrayCreateMutable(
                kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
 
        SecPolicyRef sslPolicy = SecPolicyCreateSSL(true, CFSTR("www.example.com"));
 
        CFArrayAppendValue(newTrustPolicies, sslPolicy);
 
#ifdef MAC_BACKWARDS_COMPATIBILITY
        /* This technique works in OS X (v10.5 and later) */
 
        SecTrustSetPolicies(trust, newTrustPolicies);
        CFRelease(oldTrustPolicies);
 
        return trust;
#else
        /* This technique works in iOS 2 and later, or
           OS X v10.7 and later */
 
        CFMutableArrayRef certificates = CFArrayCreateMutable(
                kCFAllocatorDefault, 0, &kCFTypeArrayCallBacks);
 
        /* Copy the certificates from the original trust object */
        CFIndex count = SecTrustGetCertificateCount(trust);
        CFIndex i=0;
        for (i = 0; i < count; i++) {
                SecCertificateRef item = SecTrustGetCertificateAtIndex(trust, i);
                CFArrayAppendValue(certificates, item);
        }
 
        /* Create a new trust object */
        SecTrustRef newtrust = NULL;
        if (SecTrustCreateWithCertificates(certificates, newTrustPolicies, &newtrust) != errSecSuccess) {
                /* Probably a good spot to log something. */
 
                return NULL;
        }
 
        return newtrust;
#endif
}
 

Доверительные объекты и NSURLConnection

Переопределять цепочечное поведение проверки NSURLConnection, необходимо переопределить два метода:

Перечисление 3 показывает пример этих двух методов.

Перечисление 3  , Переопределяющее доверительный объект, используемый NSURLConnection объект

// If you are building for OS X 10.7 and later or iOS 5 and later,
// leave out the first method and use the second method as the
// connection:willSendRequestForAuthenticationChallenge: method.
// For earlier operating systems, include the first method, and
// use the second method as the connection:didReceiveAuthenticationChallenge:
// method.
 
#ifndef NEW_STYLE
- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace {
    #pragma unused(connection)
 
    NSString *method = [protectionSpace authenticationMethod];
    if (method == NSURLAuthenticationMethodServerTrust) {
           return YES;
    }
    return NO;
}
 
-(void)connection:(NSURLConnection *)connection
        didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
#else
-(void)connection:(NSURLConnection *)connection
        willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
#endif
{
    NSURLProtectionSpace *protectionSpace = [challenge protectionSpace];
    if ([protectionSpace authenticationMethod] == NSURLAuthenticationMethodServerTrust) {
    SecTrustRef trust = [protectionSpace serverTrust];
 
    /***** Make specific changes to the trust policy here. *****/
 
    /* Re-evaluate the trust policy. */
    SecTrustResultType secresult = kSecTrustResultInvalid;
    if (SecTrustEvaluate(trust, &secresult) != errSecSuccess) {
        /* Trust evaluation failed. */
 
        [connection cancel];
 
        // Perform other cleanup here, as needed.
        return;
    }
 
    switch (secresult) {
        case kSecTrustResultUnspecified: // The OS trusts this certificate implicitly.
        case kSecTrustResultProceed: // The user explicitly told the OS to trust it.
            {
            NSURLCredential *credential =
                [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            [challenge.sender useCredential:credential forAuthenticationChallenge:challenge];
            return;
            }
        default:
            /* It's somebody else's key. Fall through. */
    }
    /* The server sent a key other than the trusted key. */
    [connection cancel];
 
    // Perform other cleanup here, as needed.
    }
}

Доверительные объекты и NSStream

Путем Вы переопределяете доверие для NSStream зависит от того, что Вы пытаетесь сделать.

Если все, что необходимо сделать, указывают различное имя хоста TLS, можно сделать это тривиально путем выполнения трех строк кода перед открытием потоков:

Перечисление 4  , Переопределяющее имя хоста TLS с NSStream

        NSDictionary *sslSettings =
                [NSDictionary dictionaryWithObjectsAndKeys:
                        @"www.gatwood.net",
                        (__bridge id)kCFStreamSSLPeerName, nil];
        if (![myInputStream setProperty: sslSettings
            forKey: (__bridge NSString *)kCFStreamPropertySSLSettings]) {
                // Handle the error here.
        }

Это изменяет понятие потока его имени хоста так, чтобы, когда потоковый объект позже создает доверительный объект, это обеспечило новое имя.

Если необходимо фактически изменить список доверяемых привязок, процесс несколько более сложен. Как только потоковый объект создает доверительный объект, он оценивает его. Если та доверительная оценка перестала работать, поток закрывается, прежде чем Ваш код имеет возможность изменить доверительный объект. Таким образом, для переопределения доверительной оценки Вы должны:

К тому времени, когда Ваш потоковый обработчик событий делегата вызывают, чтобы указать, что существует пространство, доступное на сокете, операционная система уже создала канал TLS, получила цепочку сертификата из другого конца соединения и создала доверительный объект оценить его. В этой точке у Вас есть открыть поток TLS, но Вы понятия не имеете, можно ли доверять узлу в другом конце. Путем отключения цепочечной проверки это становится ответственностью проверить, что можно доверять узлу в другом конце. Среди прочего это означает:

С теми правилами в памяти, Перечисление 5 показывает, как использовать пользовательские привязки TLS с NSStream. Это перечисление также использует функцию addAnchorToTrust от Перечисления 1.

Перечисление 5  Используя пользовательские привязки TLS с NSStream

/* Code executed after creating the socket: */
 
        [inStream setProperty:NSStreamSocketSecurityLevelNegotiatedSSL
            forKey:NSStreamSocketSecurityLevelKey];
 
    NSDictionary *sslSettings =
        [NSDictionary dictionaryWithObjectsAndKeys:
        (id)kCFBooleanFalse, (id)kCFStreamSSLValidatesCertificateChain,
        nil];
 
    [inStream setProperty: sslSettings forKey: (__bridge NSString *)kCFStreamPropertySSLSettings];
 
 
...
 
 
/* Methods in your stream delegate class */
 
NSString *kAnchorAlreadyAdded = @"AnchorAlreadyAdded";
 
- (void)stream:(NSStream *)theStream handleEvent:(NSStreamEvent)streamEvent
{
    if (streamEvent == NSStreamEventHasBytesAvailable || streamEvent == NSStreamEventHasSpaceAvailable) {
        /* Check it. */
 
        NSArray *certs = [theStream propertyForKey: (__bridge NSString *)kCFStreamPropertySSLPeerCertificates];
        SecTrustRef trust = (SecTrustRef)[theStream propertyForKey: (__bridge NSString *)kCFStreamPropertySSLPeerTrust];
 
        /* Because you don't want the array of certificates to keep
           growing, you should add the anchor to the trust list only
           upon the initial receipt of data (rather than every time).
         */
        NSNumber *alreadyAdded = [theStream propertyForKey: kAnchorAlreadyAdded];
        if (!alreadyAdded || ![alreadyAdded boolValue]) {
            trust = addAnchorToTrust(trust, self.trustedCert); // defined earlier.
            [theStream setProperty: [NSNumber numberWithBool: YES] forKey: kAnchorAlreadyAdded];
        }
        SecTrustResultType res = kSecTrustResultInvalid;
 
        if (SecTrustEvaluate(trust, &res)) {
            /* The trust evaluation failed for some reason.
               This probably means your certificate was broken
               in some way or your code is otherwise wrong. */
 
            /* Tear down the input stream. */
            [theStream removeFromRunLoop: ... forMode: ...];
            [theStream setDelegate: nil];
            [theStream close];
 
            /* Tear down the output stream. */
            ...
 
            return;
 
        }
 
        if (res != kSecTrustResultProceed && res != kSecTrustResultUnspecified) {
            /* The host is not trusted. */
            /* Tear down the input stream. */
            [theStream removeFromRunLoop: ... forMode: ...];
            [theStream setDelegate: nil];
            [theStream close];
 
            /* Tear down the output stream. */
            ...
 
        } else {
            // Host is trusted.  Handle the data callback normally.
 
        }
    }
}