Problemas AFNetworking con TLS Verificación de una CA raíz autógrafa del server

Esta es una pregunta que trata de encontrar soluciones para mi caso de uso particular, y documentar lo que he intentado hacer para cualquier otra persona que esté siguiendo este process.

Tenemos un server RESTful y una aplicación de iOS. Tenemos nuestra propia autoridad de certificación y el server tiene una autoridad de certificación raíz y un certificate autofirmado. Seguimos este process para generar los siguientes files:

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

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

Solo los certificates del server se almacenan en nuestro server y, como parte del process SSL, se envían las keys públicas con las llamadas a la API para su verificación.

He seguido este process para usar AFNetworking para usar la fijación de certificates y la fijación de keys públicas para verificar nuestros certificates autofirmados:

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

Convertimos el file .crt a un file .cer (en formatting DER) de acuerdo con esta guía:

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

e incluya el file .cer (server.cer) en el package de aplicaciones iOS. Esto permite que nuestra aplicación realice requestes GET / POST a nuestro server. Sin embargo, debido a que nuestro certificate de server puede expirar o volverse a emitir, queremos usar la CA raíz, como lo hacen las personas en este hilo en AFNetworking:

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

Actualmente, hemos actualizado a AFNetworking 2.6.0, por lo que nuestras bibliotecas de networkinges deberían include definitivamente todas las actualizaciones, incluidas las de esta discusión:

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

El código utilizado para crear nuestra política de security:

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 

Con server.cer incluido, podemos confiar en nuestro server fijando la key pública (también probamos AFSecurityPolicyPinningMode.Certificate); Esto funcionó porque se incluye el certificate exacto. Sin embargo, porque podríamos cambiar el file server.crt que tiene el server, por lo que queremos poder hacerlo con rootCA.cer.

Sin embargo, con solo el rootCA incluido en el package de aplicaciones, esto no parece funcionar. ¿Es que el rootCA no tiene suficiente información sobre la key pública para verificar el certificate del server, que se firmó con la CA raíz? El file server.crt también puede tener un CommonName cambiante.

Además, como mi fluidez en la terminología de SSL es bastante bruta, si alguien puede aclarar si estoy haciendo las preguntas correctas, sería genial. Las preguntas específicas son:

  1. ¿Estoy generando los certificates correctamente para que el server pueda probar su identidad usando el file autocompletado server.crt?
  2. ¿Es posible include solo el file rootCA.cer en el package y poder verificar el certificate de hoja server.crt? ¿Será capaz de verificar otro file server2.crt firmado por el mismo rootCA? ¿O deberíamos include un certificate intermedio entre rootCA y la hoja?
  3. ¿La fijación de la key pública o el certificate están fijando la solución adecuada para esto? Cada publicación de foro y blog que he leído dice que sí, pero incluso con la biblioteca de AFNetworking más actualizada, no hemos tenido suerte.
  4. ¿El server necesita de algún modo enviar las firmas server.crt y roomCA.pem?

Con la ayuda de un montón de diferentes resources SSL, encontré la solución para permitir el uso de certificates autofirmados para validar un server privado habilitado para SSL. También obtuve una comprensión mucho mejor de SSL, las soluciones iOS existentes y los problemas menores con cada uno que no funcionó en mi sistema. Intentaré esbozar todos los resources que entraron en mi solución y qué pequeñas cosas marcaron la diferencia.

Todavía usamos AFNetworking y actualmente es 2.6.0, que supuestamente incluye la fijación de certificates. Esta fue la raíz de nuestro problema; no pudimos verificar la identidad de nuestro server privado, que estaba enviando un certificate de hoja firmado por una raíz de CA autofirmada. En nuestra aplicación para iOS, agrupamos el certificate raíz autofirmado, que AFNetworking establece a continuación como un ancla de confianza. Sin embargo, debido a que el server es un server local (hardware incluido con nuestro producto), la dirección IP es dinámica, por lo que falla la validation del certificate de AFNetworking porque no pudimos deshabilitar la verificación de IP.

Para llegar a la raíz de la respuesta, estamos utilizando un AFHTTPSessionManager para implementar una sessionDidReceiveAuthenticationChallengeCallback personalizada. (Ver: https://gist.github.com/r00m/e450b8b391a4bf312966 ) En esa callback, validamos el certificate del server utilizando una SecPolicy que no verifica el nombre del host; ver http://blog.roderickmann.org/2013/05/validating-a-self-signed-ssl-certificate-in-ios-and-os-x-against-a-changing-host-name/ , que es una implementación anterior para NSURLConnection en lugar de NSURLSession.

El código:

Crear un AFHTTPSessionManager

  var manager: AFHTTPSessionManager = AFHTTPSessionManager() manager.requestSerializer = AFJSONRequestSerializer() // force serializer to use JSON encoding manager.setSessionDidReceiveAuthenticationChallengeBlock { (session, challenge, cnetworkingential) -> NSURLSessionAuthChallengeDisposition in if self.shouldTrustProtectionSpace(challenge, cnetworkingential: cnetworkingential) { // shouldTrustProtectionSpace will evaluate the challenge using bundled certificates, and set a value into cnetworkingential if it succeeds return NSURLSessionAuthChallengeDisposition.UseCnetworkingential } return NSURLSessionAuthChallengeDisposition.PerformDefaultHandling } 

Implementación de validation personalizada.

 class func shouldTrustProtectionSpace(challenge: NSURLAuthenticationChallenge, var cnetworkingential: AutoreleasingUnsafeMutablePointer<NSURLCnetworkingential?>) -> Bool { // note: cnetworkingential is a reference; any created cnetworkingential should be sent back using cnetworkingential.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 cnetworkingential to be used cnetworkingential.memory = NSURLCnetworkingential(trust: trust) return true } return false } 

Unas pocas cosas que aprendí sobre la rapidez al pasar por este código.

  1. La implementación de AFNetworking de setSessionDidReceiveAuthenticationChallengeBlock tiene esta firma:

    • (void) setSessionDidReceiveAuthenticationChallengeBlock: (nullable NSURLSessionAuthChallengeDisposition (^) (session NSURLSession *, NSURLAuthenticationChallenge * challenge, NSURLCnetworkingential * __nullable __autoreleasing * __nullible cnetworkingential)) block;

El parámetro de cnetworkingencial es una variable de reference / input que debe asignarse. En forma rápida, se ve así: AutoreleasingUnsafeMutablePointer. Para asignarle algo en C, haría algo como esto:

 *cnetworkingential = [[NSURLCnetworkingential alloc] initWithTrust...]; 

En forma rápida, se ve así: (de convertir NSArray a RLMArray con RKValueTransFormer no se puede convertir outputValue a AutoreleasingUnsafeMutablePointer <AnyObject?> )

 cnetworkingential.memory = NSURLCnetworkingential(trust: trust) 
  1. SecPolicyCreateSSL, SecCertificateCreateWithData y SecTrustGetCertificateAtIndex vuelven sin gestionar. objects, debe convertirlos / puentearlos utilizando takeRetainedValue () o takeUnainedValue (). (Ver http://nshipster.com/unmanaged/ ). Tuvimos problemas de memory / lockings cuando usamos takeRetainedValue () y llamamos al método más de una vez (hubo un locking en SecDestroy). En este momento, la compilation parece estable después de que cambiamos a usar takeUnretainedValue (), ya que no necesita los certificates ni las directivas ssl después de la validation.

  2. Caché de sesiones TLS. https://developer.apple.com/library/ios/qa/qa1727/_index.html Eso significa que cuando obtienes una verificación exitosa de un desafío, nunca más vuelves a tener el desafío. Esto puede complicar su cabeza cuando testing un certificate válido, luego testing un certificate no válido, que luego omite toda la validation y obtiene una respuesta exitosa del server. La solución es Product-> Clean en su simulador iOS después de cada vez que usa un certificate válido y atesting el desafío de validation. De lo contrario, podría pasar algún time pensando incorrectamente que finalmente obtuvo la CA raíz para validar.

Entonces, esta es simplemente una solución de trabajo para los problemas que estaba teniendo con mis serveres. Quería publicar todo aquí para poder ayudar a alguien más que esté ejecutando un server local o dev con una CA autofirmada y un producto iOS que necesita estar habilitado para SSL. Por supuesto, con ATS en iOS 9 espero estar cavando en SSL muy pronto otra vez.

Este código actualmente tiene algunos problemas de administración de memory y se actualizará en un futuro cercano. Además, si alguien ve esta implementación y dice "¡Ah, esto es tan malo como devolver TRUE por certificates no válidos", por favor avíseme! Por lo que puedo decir a través de nuestras propias testings, la aplicación rechaza los certificates de server inválidos que no están firmados por nuestra CA raíz y acepta el certificate de hoja generado y firmado por la CA raíz. El package de aplicaciones solo tiene la CA raíz incluida, por lo que el certificate del server puede completarse después de caducar y las aplicaciones existentes no fallarán.

Si busco un poco más en AFNetworking y descubro una solución de una a tres líneas para todo esto (alternando todas esas pequeñas banderas que proporcionan) también publicaré una actualización.

Si AlamoFire comienza a admitir SSL, también puede enviar una solución aquí.

Si está utilizando coco pods, subclass la class AFSecurityPolicy e implemente la comprobación de security según la respuesta de mitrenegade https://stackoverflow.com/a/32469609/4000434

Escucha es mi código.

Inicialice el AFHttpRequestOperationManager mientras publica la request como a continuación.

 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 shanetworkingApplication] setNetworkActivityIndicatorVisible:NO]; } failure:^(AFHTTPRequestOperation *operation, NSError *error) { [[UIApplication shanetworkingApplication] setNetworkActivityIndicatorVisible:NO]; NSLog(@"Error %@",error); failure(operation,error); }]; 

RootCAAFSecurityPolicy es la subclass de AFSecurityPolicy Class. Vea a continuación para rootCAAFSecurityPolicy .h y .m class anulan el método

– (BOOL) evaluateServerTrust: (SecTrustRef) serverTrust forDomain: dominio (NSString *)

Clase RootCAAFSecurityPolicy.h

 #import <AFNetworking/AFNetworking.h> @interface RootCAAFSecurityPolicy : AFSecurityPolicy @end 

Clase RootCAAFSecurityPolicy.m

Reemplace RootCA con su nombre de file de certificate

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