iOS9で問題になりそうなATSをまとめました。
ご指摘事項あれば是非コメントをいただきたいです。特にAFNetworkingまわり・・。
AFNetwotking部分は下に記載していますが、iOS8向けのビルドでiOS9端末でも発生したので要注意です。
2015/09/21追記
iOS9GM以降は(もうreleaseされちゃいましたが・・)AFNetworkingでの証明書判定がiOS8とおなじになりました。。。
2016/07/27追記
toshi0383さん
修正依頼ありがとうございます。
1年間間違っていることに気が付きませんでした。。修正ありがとうございます。
App Transport Security
App Transport Security (ATS) enforces best practices in the secure connections between an app and its back end. ATS prevents accidental disclosure, provides secure default behavior, and is easy to adopt; it is also on by default in iOS 9 and OS X v10.11. You should adopt ATS as soon as possible, regardless of whether you’re creating a new app or updating an existing one.
If you’re developing a new app, you should use HTTPS exclusively. If you have an existing app, you should use HTTPS as much as you can right now, and create a plan for migrating the rest of your app as soon as possible. In addition, your communication through higher-level APIs needs to be encrypted using TLS version 1.2 with forward secrecy. If you try to make a connection that doesn't follow this requirement, an error is thrown. If your app needs to make a request to an insecure domain, you have to specify this domain in your app's Info.plist file.
要約すると
- iOS9の新機能
- Appleは今後HTTPS通信を推奨していく
- 機能動作はTLSv1.2以上
- HTTPなど安全でないドメインへのアクセスをする場合はinfo.plistにドメイン指定すること
ATS設定の方法
- ATSを無効にする(Appleは非推奨)
- ドメインを指定してATSを無効にする(推奨ではないが、暫定対応としてはこちらを推奨していると思われる)
ATS設定の適用範囲
- NSURLRequest
- NSURLConnection
- NSURLSession
- UIWebView
- WKWebView
- CFNetwork
ATS設定の記述(info.plist)
- ATSを無効にする(Appleは非推奨)
<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>
- ドメインを指定してATSを無効にする(推奨ではないが、暫定対応としてはこちらを推奨していると思われる)
<key>NSAppTransportSecurity</key>
<dict>
<key>NSExceptionDomains</key>
<dict>
<key>xxx.co.jp</key>
<dict>
<key>NSIncludesSubdomains</key>
<true/>
<key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
<true/>
<key>NSTemporaryExceptionRequiresForwardSecrecy</key>
<false/>
</dict>
</dict>
</dict>
NSTemporaryExceptionAllowsInsecureHTTPLoads
NSTemporaryExceptionAllowsInsecureHTTPLoads自体がどのようなセキュリティ設定なのかドキュメントが見当たらないです。
Mac Developer Library — PrereleaseのApp Transport Security Technoteにも記載がなく、情報がないです。
https://developer.apple.com/library/prerelease/mac/technotes/App-Transport-Security-Technote/index.html
ただし似たようなNSExceptionAllowsInsecureHTTPLoadsの場合は以下の記述があります
A Bool for overriding the requirement for all connections to use HTTPS. This allows accessing domains with no certificate, or with an error for a self-signed, expired, or hostname mismatch certificate.
NO is the default value.
NSTemporaryExceptionRequiresForwardSecrecy
NSTemporaryExceptionRequiresForwardSecrecy自体がどのようなセキュリティ設定なのかドキュメントが見当たらないです。
Mac Developer Library — PrereleaseのApp Transport Security Technoteにも記載がなく、情報がないです。
https://developer.apple.com/library/prerelease/mac/technotes/App-Transport-Security-Technote/index.html
ただし似たようなNSExceptionRequiresForwardSecrecyの場合は以下の記述があります
A Bool for overriding the requirement for the domain to use ciphers supporting forward secrecy.
YES is the default value and limits the ciphers to those show in Default Behavior above.
Setting the value to NO adds the following the list of accepted ciphers:
TLS_DHE_RSA_WITH_AES_256_GCM_SHA384
TLS_DHE_RSA_WITH_AES_128_GCM_SHA256
TLS_DHE_RSA_WITH_AES_256_CBC_SHA256
TLS_DHE_RSA_WITH_AES_256_CBC_SHA
TLS_DHE_RSA_WITH_AES_128_CBC_SHA256
TLS_DHE_RSA_WITH_AES_128_CBC_SHA
TLS_RSA_WITH_AES_256_GCM_SHA384
TLS_RSA_WITH_AES_128_GCM_SHA256
TLS_RSA_WITH_AES_256_CBC_SHA256
TLS_RSA_WITH_AES_256_CBC_SHA
TLS_RSA_WITH_AES_128_CBC_SHA256
TLS_RSA_WITH_AES_128_CBC_SHA
ATS設定の記述(info.plist以外)
NSTemporaryExceptionRequiresForwardSecrecyをfalseにすることでNSURLConnectionの以下のdelegateメソッドが走ります
- (void)connection:(NSURLConnection *)connection
willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
willSendRequestForAuthenticationChallengeはBasic/SSL認証の際に走るメソッドです。
NSTemporaryExceptionRequiresForwardSecrecyのfaulseに設定していない場合は走りません。
このメソッドの中で認証処理をおこなえば処理は通ります。
※ただしNSTemporaryExceptionAllowsInsecureHTTPLoadsをtrueにしていないと認証処理はOKでもconnectionのerrorへハンドリングされる
ATSの対応方法
根本的なところは、サーバー側の修正です。
未検証ではありますが、次のように対応すればATSの問題は発生しないと思われます。
- サーバー環境を最新にする(TLS1.2対応やApple指定の暗号化方式など)
- httpsしか使わない
AFNetworking
以下はiOS9GM以降問題なくなりました・・beta5までの問題だったようです。
ATSとは別ですが、iOS9ではSSL証明書チェックの返却値がiOS8と違うという問題が発生しました。
この問題はiOS8ビルド(SDK8)のアプリでiOS9端末で起動した場合にも発生しますので要注意です。
問題となったアプリはAFNetworking(以下AF)を利用しており、willSendRequestForAuthenticationChallengeもAF内で実装されています。
その中でwillSendRequestForAuthenticationChallengeの返却値であるNSURLAuthenticationChallenge(challenge)の値をチェックしています。
challenge.protectionSpace.serverTrust
上記の値を解析し、この値が今までと異なる結果が返ってきていることを確認しました。
- SDK8ビルドのiOS8まで(今まで) kSecTrustResultUnspecified
- SDK8ビルドのiOS9 or SDK9ビルドのiOS9 kSecTrustResultRecoverableTrustFailure
kSecTrustResultUnspecified
kSecTrustResultUnspecified Indicates the evaluation succeeded and the certificate is implicitly trusted, but user intent was not explicitly specified. This value may be returned by the SecTrustEvaluate function or stored as part of the user trust settings.
kSecTrustResultRecoverableTrustFailure
kSecTrustResultRecoverableTrustFailure Indicates a trust policy failure which can be overridden by the user. This value may be returned by the SecTrustEvaluate function but not stored as part of the user trust settings.
iOS9となりセキュリティが厳しくなったことで返却値が変化したものと思われます。
そしてAFが上記返却値を判定しています。
kSecTrustResultUnspecifiedの場合
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
kSecTrustResultRecoverableTrustFailureの場合
[[challenge sender] cancelAuthenticationChallenge:challenge];
上記のように処理の切り分けを行ってYESは利用、NOはキャンセルとしていることにより通信処理が失敗しているという状況が発生していました。
アプリ側での暫定対応方法
info.plistに本文記載の設定を追加すること、
またAFを利用している場合はAF内のAFURLConnectionOperationクラスを次のように変更することで通信できますが、そもそもこの修正ではiOSが求めるユーザー問い合わせによる通信の許可を無視するような形なので十分に検証する必要があります。
この修正はとりあえず通信するためのものです。OSレベルからの返却値としてはユーザー問い合わせを求めるものなので、実際にどうするかは検討する必要があります。決してコピペで実装して審査通したりはしないでください!
修正前
- (void)connection:(NSURLConnection *)connection
willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
if (self.authenticationChallenge) {
self.authenticationChallenge(connection, challenge);
return;
}
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
} else {
[[challenge sender] cancelAuthenticationChallenge:challenge];
}
} else {
if ([challenge previousFailureCount] == 0) {
if (self.credential) {
[[challenge sender] useCredential:self.credential forAuthenticationChallenge:challenge];
} else {
[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
}
} else {
[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
}
}
}
修正後
- (void)connection:(NSURLConnection *)connection
willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge
{
if (self.authenticationChallenge) {
self.authenticationChallenge(connection, challenge);
return;
}
if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
[[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
// if ([self.securityPolicy evaluateServerTrust:challenge.protectionSpace.serverTrust forDomain:challenge.protectionSpace.host]) {
// NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
// [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
// } else {
// [[challenge sender] cancelAuthenticationChallenge:challenge];
// }
} else {
if ([challenge previousFailureCount] == 0) {
if (self.credential) {
[[challenge sender] useCredential:self.credential forAuthenticationChallenge:challenge];
} else {
[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
}
} else {
[[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
}
}
}
参考情報URL
App Transport Security Technote
https://developer.apple.com/library/prerelease/mac/technotes/App-Transport-Security-Technote/index.html
willSendRequestForAuthenticationChallenge
https://developer.apple.com/library/mac/documentation/Foundation/Reference/NSURLConnectionDelegate_Protocol/
NSURLAuthenticationChallengeSender
https://developer.apple.com/library/mac/documentation/Cocoa/Reference/Foundation/Protocols/NSURLAuthenticationChallengeSender_Protocol/
謝辞
この記事を提供いただいた方に感謝いたします。
修正
info.plist(xml)部分が正しくありませんでした。
uasiさん修正ありがとうございます。