web-dev-qa-db-ja.com

iOSで証明書の公開キーを固定する方法

開発中のiOSアプリケーションのセキュリティを改善する一方で、中間者攻撃を防ぐためにサーバーのSSL証明書をPIN(全体または一部)にする必要があることがわかりました。

これを行うためのさまざまなアプローチがありますが、これを検索すると、証明書全体を固定する例しか見つかりませんでした。このような方法には問題があります。証明書が更新されるとすぐに、アプリケーションは接続できなくなります。証明書全体ではなく公開鍵を固定することを選択した場合、サーバーでの証明書の更新に対してより回復力がある一方で、同じように安全な状況にいると思います(私は信じています)。

しかし、これをどのように行うのですか?

48
Javier Quevedo

IOSコードの証明書からこの情報を抽出する方法を知る必要がある場合は、ここで1つの方法があります。

まず、セキュリティフレームワークを追加します。

#import <Security/Security.h>

Opensslライブラリを追加します。これらは https://github.com/st3fan/ios-openssl からダウンロードできます

#import <openssl/x509.h>

NSURLConnectionDelegate Protocolを使用すると、接続が保護スペースに応答できるかどうかを決定できます。一言で言えば、これは、サーバーから送られてきた証明書を見て、接続の続行またはキャンセルを許可することを決定できる場合です。ここでやりたいことは、証明書の公開キーと固定したものを比較することです。質問は、どのようにしてそのような公開鍵を取得するのですか?次のコードをご覧ください。

最初にX509形式の証明書を取得します(これにはSSLライブラリが必要です)

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

この時点で、次のループを使用して、pubKey2文字列を反復処理し、HEX形式のバイトを文字列に抽出できます。

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

公開鍵を印刷して確認してください

 NSLog(@"%@", publicKeyString);

完全なコード

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

}
36
Javier Quevedo

予想される公開キーをiOSで直接簡単に作成できないと私が言える限り、証明書を介してそれを行う必要があります。そのため、必要な手順は証明書を固定するのと似ていますが、さらに、実際の証明書と参照証明書(予想される公開鍵)から公開鍵を抽出する必要があります。

あなたがする必要があるのは:

  1. NSURLConnectionDelegateを使用してデータを取得し、willSendRequestForAuthenticationChallengeを実装します。
  2. [〜#〜] der [〜#〜] 形式の参照証明書を含めます。この例では、単純なリソースファイルを使用しました。
  3. サーバーによって提示された公開鍵を抽出します
  4. 参照証明書から公開キーを抽出します
  5. 2つを比較する
  6. 一致する場合は、通常のチェック(ホスト名、証明書署名など)を続行します
  7. それらが一致しない場合、失敗します。

いくつかのサンプルコード:

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

免責事項:これはサンプルコードであり、完全にはテストされていません。完全な実装では、 OWASPによる証明書の固定の例 で開始します。

SSL Kill Switch および同様のツールを使用すると、証明書のピン留めを常に回避できることに注意してください。

21
beetstra

Security.frameworkのSecTrustCopyPublicKey関数を使用して、公開キーSSLピンニングを行うことができます。 AFNetworkingプロジェクトの connection:willSendRequestForAuthenticationChallenge: の例を参照してください。

IOS用のopenSSLが必要な場合は、 https://Gist.github.com/foozmeat/5154962 を使用します。これは、現在動作しないst3fan/ios-opensslに基づいています。

9
Jano

ここに記載されているPhoneGap(ビルド)プラグインを使用できます: http://www.x-services.nl/certificate-pinning-plugin-for-phonegap-to-prevent-man-in-the-middle-攻撃/ 734

プラグインは複数の証明書をサポートしているため、サーバーとクライアントを同時に更新する必要はありません。指紋が(たとえば)2年ごとに変化する場合は、クライアントに強制的に更新するメカニズムを実装します(アプリにバージョンを追加し、サーバーに 'minimalRequiredVersion' APIメソッドを作成します。アプリのバージョンが低すぎる(新しい証明書がアクティブになったときのfi)。

5
Eddy Verbruggen

AFNetworking(より具体的にはAFSecurityPolicy)を使用し、AFSSLPinningModePublicKeyモードを選択した場合、公開鍵が同じである限り、証明書が変更されたかどうかは関係ありません。はい、AFSecurityPolicyが公開鍵を直接設定する方法を提供していないのは事実です。 setPinnedCertificatesを呼び出すことによってのみ証明書を設定できます。ただし、setPinnedCertificatesの実装を見ると、フレームワークが証明書から公開キーを抽出し、キーを比較していることがわかります。

要するに、証明書を渡して、それらが将来変更されることを心配しないでください。フレームワークは、それらの証明書の公開鍵のみを考慮します。

次のコードは私のために動作します。

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

ここでSwiftyの答え。 Webサイトの証明書を(.cerファイルとして)メインバンドルに保存します。次に、 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))
}

.cerファイルは、Chrome like this で取得できます。

1
schirrmacher

...証明書全体を固定するため。そのような練習は問題を提起します...

また、Googleは毎月(または)証明書を変更しますが、公衆を保持または再認証します。そのため、証明書のピン留めは多くの偽の警告になりますが、公開キーのピン留めはキーの継続性テストに合格します。

Googleは、CRL、OCSP、および失効リストを管理しやすくするためにこれを行っていると信じています。私のサイトでは、通常、キーの継続性を確保するためにキーを再認証します。

しかし、これをどのように行うのですか?

証明書と公開キーの固定 。この記事では、プラクティスについて説明し、OpenSSL、Android、iOS、および.Netのサンプルコードを提供します。 iOS:NSUrlConnection didReceiveAuthenticationChallenge(Certificate Failure)から意味のあるエラーを提供する で説明したフレームワークに属するiOSには、少なくとも1つの問題があります。

また、Peter Gutmannは、著書 Engineering Security で、キーの継続性とピン留めについて優れた扱いをしています。

1
jww

AFNetworkingを使用する場合は、AFSecurityPolicy *policy = [AFSecurityPolicy policyWithPinningMode:AFSSLPinningModePublicKey];