はじめに
間違いがある場合はご指摘いただけると嬉しい
あと、素直にSwiftかObj-Cで実装したほうが絶対にいい
どのような方向けのTipsか?
- Verisign、Let's Encryptなどから発行されたサーバ証明書および中間証明書を持つSSLサーバに、 iOS端末からNSURLConnectionなどではなく、OpenSSLのAPIを用いてSSL接続を行いたい
- ただし、接続後にサーバからクライアント側に渡されたサーバ証明書および中間証明書が正しいかどうかを、iOSのシステムに組み込まれたルートCA証明書まで辿って検証したい
- ここで、iOSに於いてはルートCA証明書リストへのアクセスは不可能(と思われる)
- つまり、OpenSSLで提供されている、SSL_CTX_load_verify_locationsやSSL_CTX_set_default_verify_pathsといったAPIが利用できず、BundleにルートCA証明書リストなどを追加しておかなくてはならない(オレオレ証明書を通すのと同様に)
- これは嫌だ!
という方向け
解決方法
OpenSSL(C/C++)での実装
- SSL_set_verifyを用いて、SSL_VERIFY_NONEにしておく
(なお、verifycallbackを利用する、という手段は未検証) - SSL_Connectする
- SSL_get_peer_certificate_chainする
これにより、サーバ証明書(と中間証明書)が得られる
得られた証明書は、X509形式のポインタで表されているため、OpenSSLで提供されるAPIを利用して、iOSで取り扱うことのできる、DER形式のバイナリ列に直す
unsigned char* buf = NULL; // NULL
int len = i2d_X509(cert, &buf); // NULLを与えると、新たにmallocしてDERを返してくれる。勿論、後ほどfree必須
Objective-C側の実装
CFMutableArrayRef certs = CFArrayCreateMutable(kCFAllocatorDefault, 0, NULL); // 最大配列個数 => 指定なし
for (サーバから返答された証明書チェーンに含まれる証明書の個数) {
CFDataRef data = CFDataCreate(kCFAllocatorDefault, buf, len); // 前節3.で得られたDERへのポインタと長さ
SecCertificateRef cert = SecCertificateCreateWithData(NULL, data);
CFArrayAppendValue(certs, cert);
}
SecTrustRef trust;
OSStatus status = SecTrustCreateWithCertificates(certs, SecPolicyreateBasicX509(), &trust);
SecTrustResultType r = kSecTrustResultInvalid;
status = SecTrustEvaluate(trust, &r);
これで、rの返り値がkSecTrustResultProceed==1もしくはkSecTrustResultUnspecified==4であれば、サーバ証明書検証成功1
なお、サーバから渡された証明書に中間CAが含まれていないなどのミスでルートCAまで辿れなかったり、オレオレ証明書+オレオレCA証明書(システムにインストールされていない証明書)がサーバから渡された場合は、kSecTrustResultRecoverableTrustFailure == 5となる
最後に
この辺りを実装したコードは、
https://github.com/linear-rpc/linear-objc/blob/master/src/LinearSSLClient.mm#L89
にあるので、興味があればご参照を
-
https://developer.apple.com/library/mac/qa/qa1360/_index.html
The semantics behind receiving a kSecTrustResultUnspecified (4) error from Security APIs is that the certificate is indeed valid. However, the user has not explicitly set the trust settings for the certificate via Keychain Access.