web-dev-qa-db-fra.com

Comment épingler la clé publique d'un certificat sur iOS

Tout en améliorant la sécurité d'une application iOS que nous développons, nous avons constaté la nécessité de PIN (tout ou partie du) certificat SSL du serveur pour empêcher les attaques de type "man-in-the-middle". 

Même s'il existe différentes approches pour le faire, lorsque vous recherchez ceci, je ne trouve que des exemples pour épingler le certificat entier. Une telle pratique pose un problème: dès que le certificat est mis à jour, votre application ne pourra plus se connecter. Si vous choisissez d'épingler la clé publique au lieu de l'intégralité du certificat, vous vous retrouverez (je crois) dans une situation tout aussi sécurisée, tout en étant plus résistant aux mises à jour des certificats sur le serveur. 

Mais comment faites-vous cela?

47
Javier Quevedo

Au cas où vous auriez besoin de savoir comment extraire ces informations du certificat dans votre code iOS, vous avez un moyen de le faire.

Tout d'abord, ajoutez le cadre de sécurité. 

#import <Security/Security.h>

L'ajout des bibliothèques openssl. Vous pouvez les télécharger à partir de https://github.com/st3fan/ios-openssl

#import <openssl/x509.h>

Le protocole NSURLConnectionDelegate vous permet de décider si la connexion doit pouvoir répondre à un espace de protection. En un mot, vous pouvez consulter le certificat provenant du serveur et décider d’autoriser la connexion ou son annulation. Ce que vous voulez faire ici est de comparer la clé publique des certificats avec celle que vous avez épinglée. Maintenant, la question est, comment obtenez-vous une telle clé publique? Regardez le code suivant:

Commencez par obtenir le certificat au format X509 (vous aurez besoin des bibliothèques ssl pour cela)

const unsigned char *certificateDataBytes = (const unsigned char *)[serverCertificateData bytes];
X509 *certificateX509 = d2i_X509(NULL, &certificateDataBytes, [serverCertificateData length]);

Maintenant, nous allons nous préparer à lire les données de clé publique

ASN1_BIT_STRING *pubKey2 = X509_get0_pubkey_bitstr(certificateX509);

NSString *publicKeyString = [[NSString alloc] init];    

À ce stade, vous pouvez parcourir la chaîne pubKey2 et extraire les octets au format HEX dans une chaîne avec la boucle suivante.

 for (int i = 0; i < pubKey2->length; i++)
{
    NSString *aString = [NSString stringWithFormat:@"%02x", pubKey2->data[i]];
    publicKeyString = [publicKeyString stringByAppendingString:aString];
}

Imprimer la clé publique pour la voir

 NSLog(@"%@", publicKeyString);

Le code complet

- (BOOL)connection:(NSURLConnection *)connection canAuthenticateAgainstProtectionSpace:(NSURLProtectionSpace *)protectionSpace
{
const unsigned char *certificateDataBytes = (const unsigned char *)[serverCertificateData bytes];
X509 *certificateX509 = d2i_X509(NULL, &certificateDataBytes, [serverCertificateData length]);
ASN1_BIT_STRING *pubKey2 = X509_get0_pubkey_bitstr(certificateX509);

NSString *publicKeyString = [[NSString alloc] init];    

for (int i = 0; i < pubKey2->length; i++)
 {
     NSString *aString = [NSString stringWithFormat:@"%02x", pubKey2->data[i]];
     publicKeyString = [publicKeyString stringByAppendingString:aString];
 }

if ([publicKeyString isEqual:myPinnedPublicKeyString]){
    NSLog(@"YES THEY ARE EQUAL, PROCEED");
    return YES;
}else{
   NSLog(@"Security Breach");
   [connection cancel];
   return NO;
}

}
34
Javier Quevedo

Autant que je sache, vous ne pouvez pas facilement créer la clé publique attendue directement dans iOS, vous devez le faire via un certificat . Les étapes à suivre ressemblent donc à l'identification du certificat, mais vous devez en outre extraire la clé publique. du certificat actuel et d'un certificat de référence (la clé publique attendue).

Ce que vous devez faire c'est:

  1. Utilisez un NSURLConnectionDelegate pour récupérer les données et implémentez willSendRequestForAuthenticationChallenge.
  2. Incluez un certificat de référence au format DER . Dans l'exemple, j'ai utilisé un simple fichier de ressources.
  3. Extraire la clé publique présentée par le serveur
  4. Extrayez la clé publique de votre certificat de référence
  5. Comparer les deux
  6. Si elles correspondent, continuez avec les vérifications habituelles (nom d’hôte, signature du certificat, etc.)
  7. Si elles ne correspondent pas, échouez.

Quelques exemples de code:

 (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge {
    // get the public key offered by the server
    SecTrustRef serverTrust = challenge.protectionSpace.serverTrust;
    SecKeyRef actualKey = SecTrustCopyPublicKey(serverTrust);

    // load the reference certificate
    NSString *certFile = [[NSBundle mainBundle] pathForResource:@"ref-cert" ofType:@"der"];
    NSData* certData = [NSData dataWithContentsOfFile:certFile];
    SecCertificateRef expectedCertificate = SecCertificateCreateWithData(NULL, (__bridge CFDataRef)certData);

    // extract the expected public key
    SecKeyRef expectedKey = NULL;
    SecCertificateRef certRefs[1] = { expectedCertificate };
    CFArrayRef certArray = CFArrayCreate(kCFAllocatorDefault, (void *) certRefs, 1, NULL);
    SecPolicyRef policy = SecPolicyCreateBasicX509();
    SecTrustRef expTrust = NULL;
    OSStatus status = SecTrustCreateWithCertificates(certArray, policy, &expTrust);
    if (status == errSecSuccess) {
      expectedKey = SecTrustCopyPublicKey(expTrust);
    }
    CFRelease(expTrust);
    CFRelease(policy);
    CFRelease(certArray);

    // check a match
    if (actualKey != NULL && expectedKey != NULL && [(__bridge id) actualKey isEqual:(__bridge id)expectedKey]) {
      // public keys match, continue with other checks
      [challenge.sender performDefaultHandlingForAuthenticationChallenge:challenge];
    } else {
      // public keys do not match
      [challenge.sender cancelAuthenticationChallenge:challenge];
    }
    if(actualKey) {
      CFRelease(actualKey);
    }
    if(expectedKey) {
      CFRelease(expectedKey);
    }
 }

Clause de non-responsabilité: ceci est un exemple de code et n’a pas fait l’objet de tests approfondis. Pour une implémentation complète, commencez par l’exemple d’identification de certificat par OWASP .

Et rappelez-vous que l’épinglage de certificats peut toujours être évité avec SSL Kill Switch et des outils similaires.

19
beetstra

Vous pouvez effectuer le repérage de la clé publique SSL à l’aide de la fonction SecTrustCopyPublicKey de Security.framework. Voir un exemple sur connection: willSendRequestForAuthenticationChallenge: du projet AFNetworking.

Si vous avez besoin d’openSSL pour iOS, utilisez https://Gist.github.com/foozmeat/5154962 Il est basé sur st3fan/ios-openssl, qui ne fonctionne pas actuellement.

9
Jano

Vous pouvez utiliser le plugin PhoneGap (Build) mentionné ici: http://www.x-services.nl/certificate-pinning-plugin-for-phonegap-to-prevent-man-in-the-middle-attacks/ 734

Le plug-in prend en charge plusieurs certificats, il n'est donc pas nécessaire de mettre à jour le serveur et le client en même temps. Si votre empreinte digitale change tous les (disons) tous les 2 ans, mettez en place un mécanisme pour forcer les clients à se mettre à jour (ajoutez une version à votre application et créez une méthode API 'minimalRequiredVersion' sur le serveur. Demandez au client de mettre à jour si trop bas (par exemple lorsque le nouveau certificat est activé).

5
Eddy Verbruggen

Si vous utilisez AFNetworking (plus précisément AFSecurityPolicy) et que vous choisissez le mode AFSSLPinningModePublicKey, peu importe si vos certificats changent ou non, tant que les clés publiques restent les mêmes. Oui, il est vrai qu'AFSecurityPolicy ne vous fournit pas de méthode pour définir directement vos clés publiques. vous ne pouvez définir vos certificats qu'en appelant setPinnedCertificates. Toutefois, si vous examinez l'implémentation de setPinnedCertificates, vous constaterez que le cadre extrait les clés publiques des certificats, puis les compare. 

En bref, transmettez les certificats et ne vous inquiétez pas de leur évolution future. Le cadre ne s'intéresse qu'aux clés publiques de ces certificats.

Le code suivant fonctionne pour moi. 

AFHTTPRequestOperationManager *manager = [AFHTTPRequestOperationManager manager];
manager.securityPolicy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];
[manager.securityPolicy setPinnedCertificates:myCertificate];
3
SeaJelly

Voici la réponse Swifty. Enregistrez le certificat (en tant que fichier .cer) de votre site Web dans le bundle principal. Ensuite, utilisez la méthode this URLSessionDelegate:

func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void) {

    guard
        challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust,
        let serverTrust = challenge.protectionSpace.serverTrust,
        SecTrustEvaluate(serverTrust, nil) == errSecSuccess,
        let serverCert = SecTrustGetCertificateAtIndex(serverTrust, 0) else {

            reject(with: completionHandler)
            return
    }

    let serverCertData = SecCertificateCopyData(serverCert) as Data

    guard
        let localCertPath = Bundle.main.path(forResource: "shop.rewe.de", ofType: "cer"),
        let localCertData = NSData(contentsOfFile: localCertPath) as Data?,

        localCertData == serverCertData else {

            reject(with: completionHandler)
            return
    }

    accept(with: serverTrust, completionHandler)

}

... 

func reject(with completionHandler: ((URLSession.AuthChallengeDisposition, URLCredential?) -> Void)) {
    completionHandler(.cancelAuthenticationChallenge, nil)
}

func accept(with serverTrust: SecTrust, _ completionHandler: ((URLSession.AuthChallengeDisposition, URLCredential?) -> Void)) {
    completionHandler(.useCredential, URLCredential(trust: serverTrust))
}

Vous pouvez obtenir le fichier .cer avec Chrome comme this .

2
schirrmacher

... pour épingler le certificat entier. Une telle pratique pose problème ...

En outre, Google modifie le certificat tous les mois (ou presque) mais conserve ou certifie à nouveau le public. Ainsi, l’identification de certificat entraînera de nombreux avertissements parasites, tandis que l’identification de clé publique passera les tests de continuité de clé.

Je pense que Google le fait pour que les listes de révocation de certificats, les listes OCSP et les listes de révocation restent gérables, et j'espère que d'autres le feront également. Pour mes sites, je re-certifie généralement les clés afin que les personnes puissent en assurer la continuité.

Mais comment faites-vous cela?

Épinglage de certificat et de clé publique . L'article décrit la pratique et offre un exemple de code pour OpenSSL, Android, iOS et .Net. Il existe au moins un problème avec iOS qui incombe au framework décrit à l'adresse iOS: erreur significative de NSUrlConnection didReceiveAuthenticationChallenge (Certificate Failure) .

En outre, Peter Gutmann traite avec un soin particulier la continuité des clés et son épingle du jeu dans son livre Engineering Security .

1
jww

Si vous utilisez AFNetworking, utilisez AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];

0
Oshitha Wimalasuriya