Подтвердить что ты не робот

Проблемы с сетью AFS с TLS Проверка корневого сервера с самозаверяющим сервером

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

У нас есть сервер RESTful и приложение iOS. У нас есть собственный центр сертификации, и у сервера есть корневой центр сертификации и самоподписанный сертификат. Мы выполнили этот процесс для создания следующих файлов:

http://datacenteroverlords.com/2012/03/01/creating-your-own-ssl-certificate-authority/

rootCA.pem rootCA.key server.crt server.key

Только серверные сертификаты хранятся на нашем сервере, и как часть процесса SSL открытые ключи отправляются с вызовами API для проверки.

Я следил за этим процессом, чтобы использовать AFNetworking для использования фиксации сертификата, а также для фиксации открытого ключа для проверки наших самоподписанных сертификатов:

http://initwithfunk.com/blog/2014/03/12/afnetworking-ssl-pinning-with-self-signed-certificates/

Мы преобразуем файл .crt в файл .cer(в формате DER) в соответствии с этим руководством:

https://support.ssl.com/Knowledgebase/Article/View/19/0/der-vs-crt-vs-cer-vs-pem-certificates-and-how-to-convert-them

и включите файл .cer(server.cer) в комплекте приложений iOS. Это позволяет нашему приложению делать запросы GET/POST на наш сервер. Однако, поскольку наш серверный сертификат может истекать или переиздаваться, мы хотим вместо этого использовать корневой ЦС, как это сделали люди из этого потока в AFNetworking:

https://github.com/AFNetworking/AFNetworking/issues/1944

В настоящее время мы обновили AFNetworking 2.6.0, поэтому наши сетевые библиотеки должны обязательно включать все обновления, включая те, которые были в этом обсуждении:

https://github.com/AFNetworking/AFNetworking/issues/2744

Код, используемый для создания нашей политики безопасности:

    var manager: AFHTTPRequestOperationManager = AFHTTPRequestOperationManager()
    manager.requestSerializer = AFJSONRequestSerializer() // force serializer to use JSON encoding

    let policy: AFSecurityPolicy = AFSecurityPolicy(pinningMode: AFSSLPinningMode.PublicKey)
    var data: [NSData] = [NSData]()
    for name: String in ["rootCA", "server"] {
        let path: String? = NSBundle.mainBundle().pathForResource(name, ofType: "cer")
        let keyData: NSData = NSData(contentsOfFile: path!)!
        data.append(keyData)
    }
    policy.pinnedCertificates = data
    policy.allowInvalidCertificates = true 
    policy.validatesDomainName = false 
    manager.securityPolicy = policy

С включенным server.cer мы можем доверять нашему серверу, привязывая открытый ключ (также пытаемся использовать AFSecurityPolicyPinningMode.Certificate); это сработало, потому что точный сертификат включен. Однако, поскольку мы можем изменить файл server.crt, который имеет сервер, поэтому мы хотим иметь возможность сделать это с помощью только rootCA.cer.

Однако, только с rootCA, включенным в комплект приложения, это, похоже, не работает. Разве что у rootCA недостаточно информации о публичном ключе для проверки сертификата сервера, который был подписан с корневым центром сертификации? Файл server.crt также может иметь изменение CommonName.

Кроме того, поскольку моя беглость в терминологии SSL довольно грубая, если кто-нибудь может уточнить, задаю ли я правильные вопросы, это было бы здорово. Конкретные вопросы:

  • Правильно ли я создаю сертификаты, чтобы сервер мог подтвердить свою личность с помощью самоподписанного файла server.crt?
  • Можно ли включить в комплект только файл rootCA.cer и проверить ли серверный сертификат server.crt? Будет ли он проверять другой файл server2.crt, подписанный тем же корневым центром? Или мы должны включать промежуточный сертификат между rootCA и листом?
  • Является ли публичный ключ закреплением или сертификатом для правильного решения? Каждый форум и запись в блоге, которые я читал, говорят "да", но даже с самой обновленной библиотекой AFNetworking нам не повезло.
  • Нужно ли серверу каким-либо образом отправлять как серверные, так и сигнатуры roomCA.pem?
4b9b3361

Ответ 1

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

Мы по-прежнему используем AFNetworking, и в настоящее время это 2.6.0, который предположительно включает в себя сертификацию. Это и есть корень нашей проблемы; мы не смогли проверить личность нашего частного сервера, который отправлял листовой сертификат, подписанный самозаписываемым корнем CA. В нашем приложении iOS мы объединяем самоподписанный корневой сертификат, который затем устанавливается как доверенный якорь AFNetworking. Однако, поскольку сервер является локальным сервером (аппаратное обеспечение, входящее в состав нашего продукта), IP-адрес является динамическим, поэтому проверка сертификата AFNetworking терпит неудачу, потому что мы не смогли отключить проверку IP.

Чтобы добраться до корня ответа, мы используем AFHTTPSessionManager, чтобы реализовать пользовательский sessionDidReceiveAuthenticationChallengeCallback. (Смотрите: https://gist.github.com/r00m/e450b8b391a4bf312966). В этом обратном вызове мы проверяем сертификат сервера с помощью SecPolicy, который не проверяет имя хоста; см. http://blog.roderickmann.org/2013/05/validating-a-self-signed-ssl-certificate-in-ios-and-os-x-against-a-changing-host-name/, которая является более старой версией для NSURLConnection, а не NSURLSession.

Код:

Создание AFHTTPSessionManager

    var manager: AFHTTPSessionManager = AFHTTPSessionManager()
    manager.requestSerializer = AFJSONRequestSerializer() // force serializer to use JSON encoding
    manager.setSessionDidReceiveAuthenticationChallengeBlock { (session, challenge, credential) -> NSURLSessionAuthChallengeDisposition in

        if self.shouldTrustProtectionSpace(challenge, credential: credential) {
            // shouldTrustProtectionSpace will evaluate the challenge using bundled certificates, and set a value into credential if it succeeds
            return NSURLSessionAuthChallengeDisposition.UseCredential
        }
        return NSURLSessionAuthChallengeDisposition.PerformDefaultHandling
    }

Выполнение пользовательской проверки

class func shouldTrustProtectionSpace(challenge: NSURLAuthenticationChallenge, var credential: AutoreleasingUnsafeMutablePointer<NSURLCredential?>) -> Bool {
    // note: credential is a reference; any created credential should be sent back using credential.memory

    let protectionSpace: NSURLProtectionSpace = challenge.protectionSpace
    var trust: SecTrustRef = protectionSpace.serverTrust!

    // load the root CA bundled with the app
    let certPath: String? = NSBundle.mainBundle().pathForResource("rootCA", ofType: "cer")
    if certPath == nil {
        println("Certificate does not exist!")
        return false
    }

    let certData: NSData = NSData(contentsOfFile: certPath!)!
    let cert: SecCertificateRef? = SecCertificateCreateWithData(kCFAllocatorDefault, certData).takeUnretainedValue()

    if cert == nil {
        println("Certificate data could not be loaded. DER format?")
        return false
    }

    // create a policy that ignores hostname
    let domain: CFString? = nil
    let policy:SecPolicy = SecPolicyCreateSSL(1, domain).takeRetainedValue() 

    // takes all certificates from existing trust
    let numCerts = SecTrustGetCertificateCount(trust)
    var certs: [SecCertificateRef] = [SecCertificateRef]()
    for var i = 0; i < numCerts; i++ {
        let c: SecCertificateRef? = SecTrustGetCertificateAtIndex(trust, i).takeUnretainedValue()
        certs.append(c!)
    }

    // and adds them to the new policy
    var newTrust: Unmanaged<SecTrust>? = nil
    var err: OSStatus = SecTrustCreateWithCertificates(certs, policy, &newTrust)
    if err != noErr {
        println("Could not create trust")
    }
    trust = newTrust!.takeUnretainedValue() // replace old trust

    // set root cert
    let rootCerts: [AnyObject] = [cert!]
    err = SecTrustSetAnchorCertificates(trust, rootCerts)

    // evaluate the certificate and product a trustResult
    var trustResult: SecTrustResultType = SecTrustResultType()
    SecTrustEvaluate(trust, &trustResult)

    if Int(trustResult) == Int(kSecTrustResultProceed) || Int(trustResult) == Int(kSecTrustResultUnspecified) {
        // create the credential to be used
        credential.memory = NSURLCredential(trust: trust)
        return true
    }
    return false
}

Несколько вещей, которые я узнал о быстром просмотре этого кода.

  • Реализация AFNetworking для setSessionDidReceiveAuthenticationChallengeBlock имеет следующую подпись:

    • (void) setSessionDidReceiveAuthenticationChallengeBlock: (nullable NSURLSessionAuthChallengeDisposition (^) (NSURLSession * session, NSURLAuthenticationChallenge * challenge, NSURLCredential * __nullable __autoreleasing * __nullable credential)) block;

Параметр учетных данных является переменной reference/inout, которая должна быть назначена. Быстро это выглядит так: AutoreleasingUnsafeMutablePointer. Чтобы присвоить ему что-то в C, вы сделали бы что-то вроде этого:

*credential = [[NSURLCredential alloc] initWithTrust...];

В быстром режиме он выглядит так: (от преобразование NSArray в RLMArray с RKValueTransFormer не удается преобразовать outputValue в AutoreleasingUnsafeMutablePointer < AnyObject? > )

credential.memory = NSURLCredential(trust: trust)
  1. SecPolicyCreateSSL, SecCertificateCreateWithData и SecTrustGetCertificateAtIndex возвращают неуправляемые! объектов, вы должны по существу преобразовать их/объединить их с помощью takeRetainedValue() или takeUnretainedValue(). (См. http://nshipster.com/unmanaged/). У нас были проблемы с памятью/сбои, когда мы использовали takeRetainedValue() и вызывали метод более одного раза (произошел сбой в SecDestroy). Прямо сейчас сборка кажется стабильной после того, как мы переключились на использование takeUnretainedValue(), так как после проверки вам не нужны политики сертификатов или ssl.

  2. Кэш сеансов TLS. https://developer.apple.com/library/ios/qa/qa1727/_index.html Это означает, что когда вы получаете успешную проверку на вызов, вы никогда не получите вызов снова. Это может повредить вам голову, когда вы тестируете действительный сертификат, а затем проверяете недействительный сертификат, который затем пропускает все проверки, и вы получаете успешный ответ с сервера. Решение заключается в Product- > Clean в вашем iOS-симуляторе после каждого использования действительного сертификата и прохождения проверки. В противном случае вы можете потратить некоторое время на неправильное мышление, чтобы вы, наконец, получили корневой ЦС для проверки.

Итак, вот просто рабочее решение для проблем, которые у меня были с моими серверами. Я хотел бы опубликовать все здесь, чтобы, надеюсь, помочь кому-то другому, работающему на локальном или dev-сервере, с самоподписанным ЦС и iOS-продуктом, который должен быть включен SSL. Конечно, с ATS в iOS 9 я ожидаю, что вы скоро перейдете в SSL.

В настоящее время этот код имеет некоторые проблемы с управлением памятью и будет обновлен в ближайшем будущем. Кроме того, если кто-либо видит эту реализацию и говорит: "Ах, это так же плохо, как возвращение ИСТИННОГО для недействительных сертификатов", пожалуйста, дайте мне знать! Насколько я могу судить по собственному тестированию, приложение отклоняет неверные сертификаты сервера, не подписанные нашим корневым центром сертификации, и принимает сертификат листа, сгенерированный и подписанный корневым центром сертификации. В комплект приложения входит только корневой ЦС, поэтому сертификат сервера может быть циклически после истечения срока действия, а существующие приложения не будут работать.

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

Если AlamoFire начинает поддерживать SSL, не стесняйтесь публиковать решение здесь.

Ответ 2

Если вы используете coco pods, тогда подкласс класса AFSecurityPolicy и выполните проверку безопасности в соответствии с ответом на митенерометр fooobar.com/questions/294926/...

Слушайте мой код.

Инициализируйте AFHttpRequestOperationManager во время отправки запроса, как показано ниже.

AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
    manager.responseSerializer = [AFJSONResponseSerializer serializer];
    manager.requestSerializer = [AFJSONRequestSerializer serializer];
    manager.securityPolicy = [RootCAAFSecurityPolicy policyWithPinningMode:AFSSLPinningModeCertificate];
    [manager.requestSerializer setValue:@"application/json" forHTTPHeaderField:@"Content-Type"];
    [manager POST:Domain_Name parameters:parameters success:^(AFHTTPRequestOperation *operation, id responseObject) {
        success(operation,responseObject);
        [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
    } failure:^(AFHTTPRequestOperation *operation, NSError *error) {
        [[UIApplication sharedApplication] setNetworkActivityIndicatorVisible:NO];
        NSLog(@"Error  %@",error);
        failure(operation,error);
    }];

RootCAAFSecurityPolicy - это подкласс класса AFSecurityPolicy. См. Ниже для RootCAAFSecurityPolicy.h и .m class переопределить метод

- (BOOL) evaluateServerTrust: (SecTrustRef) serverTrust forDomain: (NSString *) домен

Класс RootCAAFSecurityPolicy.h

#import <AFNetworking/AFNetworking.h>

@interface RootCAAFSecurityPolicy : AFSecurityPolicy

@end

RootCAAFSecurityPolicy.m класс

Замените RootCA именем вашего файла сертификата

#import "RootCAAFSecurityPolicy.h"

@implementation RootCAAFSecurityPolicy
-(BOOL)evaluateServerTrust:(SecTrustRef)serverTrust forDomain:(NSString *)domain
{
    if(self.SSLPinningMode == AFSSLPinningModeCertificate)
    {
        return [self shouldTrustServerTrust:serverTrust];
    }
    else
    {
        return [super evaluateServerTrust:serverTrust forDomain:domain];
    }
}
- (BOOL)shouldTrustServerTrust:(SecTrustRef)serverTrust
{
    // load up the bundled root CA
    NSString *certPath = [[NSBundle mainBundle] pathForResource:@"RootCA" ofType:@"der"];

    NSAssert(certPath != nil, @"Specified certificate does not exist!");

    NSData *certData = [[NSData alloc] initWithContentsOfFile:certPath];
    CFDataRef certDataRef = (__bridge_retained CFDataRef)certData;
    SecCertificateRef cert = SecCertificateCreateWithData(NULL, certDataRef);

    NSAssert(cert != NULL, @"Failed to create certificate object. Is the certificate in DER format?");


    // establish a chain of trust anchored on our bundled certificate
    CFArrayRef certArrayRef = CFArrayCreate(NULL, (void *)&cert, 1, NULL);
    OSStatus anchorCertificateStatus = SecTrustSetAnchorCertificates(serverTrust, certArrayRef);

    NSAssert(anchorCertificateStatus == errSecSuccess, @"Failed to specify custom anchor certificate");


    // trust also built-in certificates besides the specified CA
    OSStatus trustBuiltinCertificatesStatus = SecTrustSetAnchorCertificatesOnly(serverTrust, false);

    NSAssert(trustBuiltinCertificatesStatus == errSecSuccess, @"Failed to reenable trusting built-in anchor certificates");


    // verify that trust
    SecTrustResultType trustResult;
    OSStatus evalStatus =  SecTrustEvaluate(serverTrust, &trustResult);

    NSAssert(evalStatus == errSecSuccess, @"Failed to evaluate certificate trust");


    // clean up
    CFRelease(certArrayRef);
    CFRelease(cert);
    CFRelease(certDataRef);


    // did our custom trust chain evaluate successfully
    return (trustResult == kSecTrustResultProceed || trustResult == kSecTrustResultUnspecified);
}
@end

Ответ 3

У меня была та же проблема, и я исправил ее, сравнив открытые ключи цепочки в методе didReceiveChallenge объекта AFURLSessionManager.

-(void)URLSession:(NSURLSession *)session didReceiveChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential * _Nullable))completionHandler {
        // Get remote certificate
        SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;

        NSMutableArray *policies = [NSMutableArray array];
        [policies addObject:(__bridge_transfer id)SecPolicyCreateSSL(true, (__bridge CFStringRef) challenge.protectionSpace.host)];

        SecTrustSetPolicies(serverTrust, (__bridge CFArrayRef)policies);
        NSUInteger trustedPublicKeyCount = 0;
        NSArray *publicKeys = AFPublicKeyTrustChainForServerTrust(serverTrust);

        for (id trustChainPublicKey in publicKeys) {
            for (id pinnedPublicKey in self.pinnedPublicKeys) {
                if (AFSecKeyIsEqualToKey((__bridge SecKeyRef)trustChainPublicKey, (__bridge SecKeyRef)pinnedPublicKey)) {
                    trustedPublicKeyCount += 1;
                }
            }
        }

        // The pinnning check
        if (trustedPublicKeyCount > 0) {
            NSURLCredential *credential = [NSURLCredential credentialForTrust:serverTrust];
            completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
        } else {
            completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, NULL);
        }
    }

Вот инициализация pinnedPublicKeys:

    // Get local certificates
    NSArray *certNames = @[@"root_cert"];
    self.pinnedPublicKeys = [NSMutableSet new];

    for (NSString *certName in certNames) {
        NSString *path = [bundle pathForResource:certName ofType:@"der"];
        NSData *certificate = [NSData dataWithContentsOfFile:path];

        id publicKey = AFPublicKeyForCertificate(certificate);
        if (publicKey) {
            [self.pinnedPublicKeys addObject:publicKey];
        }
    }

Вот вспомогательные методы для получения цепочки доверия ключей (AFPublicKeyTrustChainForServerTrust), сравнение открытых ключей (AFSecKeyIsEqualToKey) и Метод получения открытого ключа из сертификата (AFPublicKeyTrustChainForServerTrust):

static NSArray * AFPublicKeyTrustChainForServerTrust(SecTrustRef serverTrust) {
    SecPolicyRef policy = SecPolicyCreateBasicX509();
    CFIndex certificateCount = SecTrustGetCertificateCount(serverTrust);
    NSMutableArray *trustChain = [NSMutableArray arrayWithCapacity:(NSUInteger)certificateCount];
    for (CFIndex i = 0; i < certificateCount; i++) {
        SecCertificateRef certificate = SecTrustGetCertificateAtIndex(serverTrust, i);

        SecCertificateRef someCertificates[] = {certificate};
        CFArrayRef certificates = CFArrayCreate(NULL, (const void **)someCertificates, 1, NULL);

        SecTrustRef trust;
        SecTrustCreateWithCertificates(certificates, policy, &trust);

        SecTrustResultType result;
        SecTrustEvaluate(trust, &result);

        [trustChain addObject:(__bridge_transfer id)SecTrustCopyPublicKey(trust)];

        if (trust) {
            CFRelease(trust);
        }

        if (certificates) {
            CFRelease(certificates);
        }

        continue;
    }
    CFRelease(policy);

    return [NSArray arrayWithArray:trustChain];
}

static BOOL AFSecKeyIsEqualToKey(SecKeyRef key1, SecKeyRef key2) {
    return [(__bridge id)key1 isEqual:(__bridge id)key2];
}

static id AFPublicKeyForCertificate(NSData *certificate) {
    id allowedPublicKey = nil;
    SecCertificateRef allowedCertificate;
    SecCertificateRef allowedCertificates[1];
    CFArrayRef tempCertificates = nil;
    SecPolicyRef policy = nil;
    SecTrustRef allowedTrust = nil;
    SecTrustResultType result;

    allowedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certificate);

    allowedCertificates[0] = allowedCertificate;
    tempCertificates = CFArrayCreate(NULL, (const void **)allowedCertificates, 1, NULL);

    policy = SecPolicyCreateBasicX509();
    SecTrustCreateWithCertificates(tempCertificates, policy, &allowedTrust);
    SecTrustEvaluate(allowedTrust, &result);

    allowedPublicKey = (__bridge_transfer id)SecTrustCopyPublicKey(allowedTrust);

    if (allowedTrust) {
        CFRelease(allowedTrust);
    }

    if (policy) {
        CFRelease(policy);
    }

    if (tempCertificates) {
        CFRelease(tempCertificates);
    }

    if (allowedCertificate) {
        CFRelease(allowedCertificate);
    }

    return allowedPublicKey;
}