JSSEC セキュリティフォーラム 2017行ってまいりましたと 「Android7.0対応セキュアコーディングガイド更新内容まとめ①」を即日書いてから後半を書くまで2週間も間が空いてしまってすいません。
さて、今回は前回予告していた「Pinning実装」についてです。
ガイドライン2大改定ポイントに上がったPinning実装とは
通常Androidのデフォルトは”すべての CA を信頼する”となっています。
つまり、CAになりすまして偽の証明書を発行してしまえば、容易に中間者攻撃(man-in-the-middle )が成立してしまいます。
そこでアプリケーション内部にSSL証明書にピン留め(SSL Pinning)という手法を用います。
Pinningとは通信に成功した際の証明書情報のハッシュ値を保存しておいて
設定する箇所は前回のNetwork Security Configurationでも紹介させていただいたres/xml/network_security_config.xmlです。
res/xml/network_security_config.xml
タグに記述するのは、ピンニング検証の対象となる公開鍵のハッシュ値を base64 でエンコードしたも のです。また、ハッシュ関数は SHA-256 のみサポートされています。
<?xml version="1.0" encoding="utf-8"?> <network-security-config> <domain-config> <domain includeSubdomains="true">example.com</domain> <pin-set expiration="2018-01-01"> <pin digest="SHA-256">7HIpactkIAq2Y49orFOOQKurWxmmSFZhBCoQYcRhJ3Y=</pin> <!-- backup pin --> <pin digest="SHA-256">fwza0LRMXouZHRC8Ei+4PyuldPDcf3UKgO/04cDM1oE=</pin> </domain-config> </network-security-config> Android4.2での実装を開く// ハンドシェイク時の検証によりシステムに信頼された証明書チェーンを取得する X509Certificate[] chain = (X509Certificate[]) connection.getServerCertificates(); X509TrustManagerExtensions trustManagerExt = new X509TrustManagerExtensions((X509TrustManager) (trustManagerFa ctory.getTrustManagers()[0])); List trustedChain = trustManagerExt.checkServerTrusted(chain, "RSA", url.getHost()); // 公開鍵ピンニングを用いて検証する boolean isValidChain = false; for (X509Certificate cert : trustedChain) { PublicKey key = cert.getPublicKey(); MessageDigest md = MessageDigest.getInstance("SHA-256"); String keyHash = bytesToHex(md.digest(key.getEncoded())); // ピンニングしておいた公開鍵のハッシュ値と比較する if(PINS.contains(keyHash)) isValidChain = true; } if (isValidChain) { // 処理を継続する } else { // 処理を継続しない }ただし、pinningも万全ではありません。
最初の証明書取得がそもそも不正だった場合は効果あがありません。認証局が攻撃を受け「2011年イギリスのComodoが不正侵入を受け、不正なSSL証明書が発行される。」「フランスの政府系認証局ANSSI傘下の中間認証局から、不正なSSL証明書が発行される。」と言ったように度々認証局から不正な証明書が発行された場合もpinng実装では対応しようがないので通信内容の検証は必ず行い多重防御に徹しましょう。
pinning実装のその前に、そのSSL通信実装脆弱ではありませんか
pinning実装以前にSSL通信実装が脆弱、というアプリが少なくありません。
所感では今でも10個診断すれば1個はSSL検証関連の脆弱な実装が確認されます。
つい最近でもSSL検証不備に関する脆弱性は出ています。
Android アプリ「TVer」における SSL サーバ証明書の検証不備の脆弱性
SSL検証を脆弱にしない最低限守ってほしい3つの項目を記載したので確認しましょう。
検証環境だからという言い訳は前回書いたようにNetwork Security Configurationができたため、今後言い訳として苦しくなってくるので今一度確認してましょう。
① TrustManagerを変更しない、独自のTrustManagerを作らない
脆弱な例:下記コードでは証明書チェーンの検証をしない実装となっています。
KeyManager[] keyManagers = null; TrustManager[] transManagers = { new X509TrustManager() { public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {} public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {} public X509Certificate[] getAcceptedIssuers() { return new X509Certificate[0]; } } };② HostnameVerifierを変更しない、独自のHostnameVerifierを作らない
脆弱な例:URLのホスト名とCommon Nameが一致していることの検証をしていない connection.setHostnameVerifier(new HostnameVerifier() { public boolean verify(String hostname, SSLSession sslSession) { return true; } });③ SSLExceptionに対し(throwするだけでなく)適切な例外処理をする
証明書の例外(SSLHandshakeException)については「HTTPS および SSL によるセキュリティ」にあるように以下のいずれかの理由で発生します。
- サーバー証明書を発行した CA が不明である
- CA によってサーバー証明書が署名されていないが、自己署名されている
- サーバー設定に中間 CA がない
他にもHttpsURLConnectionは証明書の正当性を無視します。
HttpsURLConnection connection = makeOreOreHttpsURLConnection(url);証明書の正当性を検証するためにはopenConnectionを利用しましよう。
HttpsURLConnection connection = (HttpsURLConnection) url.openConnection();通信の実装例のコードでも意外に上記のような検証が漏れる関数が書いてあったりするのを見かけるのでWeb上で見つけたソースを採用する際には必ず重要情報が扱うに適切な関数で記載されているか確認することが推奨されます。
フォーラムでは他にもOkHttp2.5.0の脆弱性(ヘッダーインジェクション)が一部引数チェックがないので、改行文字(CR/LF)を検証しましょう。や関係ない検証コードが第3版からずっと載っていたのに気がついたので削除したなどなどTipsを挟みつつ紹介されていました。
以上です。参考になれば幸いです。