swift初心者がSmartNews風ニュースアプリを作ってみる過程を晒す(5) - ニュースアプリにおけるApp Transport Securityについて考える


## <i class="fa fa-cube" style="font-size:1em;"></i> はじめに

swiftはほとんど未経験ですが、SmartNews風ニュースアプリを作ってみて、その過程をさらしています。

前回は、こんな記事を書きました。

[swift初心者がSmartNews風ニュースアプリを作ってみる過程を晒す(4) - Carthageを用いてAlamofireとSwiftyJSONを導入する - Qiita](http://qiita.com/tjnet/items/aa268821346583c8795d)


今回は、前回の記事の中で少し触れたApp Transport Securityについて、もう少しみていくことにします。


## <i class="fa fa-cube" style="font-size:1em;"></i>現在の進捗

前回は、Alamofireを用いて、qiitaのAPIと通信してみました。

![スクリーンショット 2015-11-27 0.07.15.png](https://qiita-image-store.s3.amazonaws.com/0/34321/f2a570ba-14c2-d9b5-900f-3f6f4f7de286.png "スクリーンショット 2015-11-27 0.07.15.png")

ここまでのソースコードは下記コマンドで取得できます。

``git clone --branch v1.3 https://github.com/tjnet/NewsAppWithSwift.git``


## <i class="fa fa-cube" style="font-size:1em;"></i>今回、作るもの

iOS9に導入されたATSへの対応について調査し、より良い実装に変更します。

## <i class="fa fa-cube" style="font-size:1em;"></i>開発環境

XCode7.2を使用しています。

## <i class="fa fa-cube" style="font-size:1em;"></i>App Transport Security(ATS)とは

アプリケーションとサーバの通信をよりセキュアにするためにAppleが導入した仕組みです。

ATSはアプリケーションと、そのバックエンドの通信をセキュアに保つためのベストプラクティス(HTTPSの使用など)に従うことを要求します。ATSはiOS 9.0以降、OS X v10.11以降でデフォルトで有効化されます([Base SDK]をiOS9.0に設定してビルドするとATSがデフォルトでONになる、という意味です)。

新規のアプリ開発/既存アプリのアップデート対応に関わらず、ATSをできる限り早く採用することが望ましいです。

実際には、現時点でサーバからの全てのアセットをセキュアな通信で提供することは難しいので、AppleはATSを部分的、もしくは全体的に無効にする方法も提供しています。

## <i class="fa fa-cube" style="font-size:1em;"></i>ATS有効化の影響範囲

### 概要


ATSを有効化した場合には、アプリ内からの通信先に以下の条件が要求されます。

- HTTPS(TLS version 1.2)
- forward secrecyに対応していること

この要件を満たせない通信を行う場合、例外が発生し、以下のようなエラーメッセージが表示されてしまいます。

```
2015-11-26 23:14:36.134 NewsApp[4444:245357] App Transport Security has blocked a cleartext HTTP (http://) resource load since it is insecure. Temporary exceptions can be configured via your app's Info.plist file.
```

[iOS 9.0](https://developer.apple.com/library/ios/releasenotes/General/WhatsNewIniOS/Articles/iOS9.html#//apple_ref/doc/uid/TP40016198-SW14)

## <i class="fa fa-cube" style="font-size:1em;"></i>対応方法を考えてみる

アプリからアクセスするサーバのすべてが、ATSの要求する条件に適合していれば問題ないのですが、そうではない場合には、下記の対応を検討することになります。

- 案A. サーバ側で頑張って対応する
- 案B. ATS自体を無効にする
- 案C. 特定のドメインにおいて、ATSを無効にする
- 案D. SFSafariViewControllerを使用する

それぞれの対応方法について考えてみます。

### 案A. サーバ側で頑張って、アプリとの通信がATSの要求に適合するように対応する



[Requirements for Connecting Using ATS](https://developer.apple.com/library/mac/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW35)によれば、ATSをすべて有効化した場合、アプリのHTTP通信はHTTPSを使用しなければなりません。また、以下の条件を満たす必要があります。

- サーバ証明書が、以下のうち少なくとも1つの要件を満たしていること
 - ルート証明書がOSに組み込まれたCA(Certificate Authority)に発行されたものであること
 - 信頼できる[ルート認証局](http://e-words.jp/w/%E3%83%AB%E3%83%BC%E3%83%88%E8%AA%8D%E8%A8%BC%E5%B1%80.html)によって発行されており、userかシステム管理者によってインストールされたものであること

- Transport Layer Security versionがTLS 1.2であること
- 暗号化スイートはforward secrecy (FS)をサポートしていて、次のうちの1つであること
 - TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
 - TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
 - TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384
 - TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA
 - TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256
 - TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA
 - TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
 - TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
 - TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384
 - TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256
 - TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA
- leaf certificateが以下のタイプの鍵のうちの１つで署名されていること
 - 最低2048ビットのRSA key
 - 最低256ビットのECC key
 - 加えて、leaf server certificateのハッシュアルゴリズムは、SHA-2であり、ダイジェストの長さは最低256ビット

(leaf certificateがわからなかったので、[この辺り](http://security.stackexchange.com/questions/56678/unrecognized-leaf-certificate)を確認しました。)


上記がサーバサイドで実現できれば、アプリ側の対応は不要になります。ただ、現在(http://)で通信しているサーバが、自分の管理下にないサーバである場合は、今すぐにこの手法で対応するのは無理ですね。。。



### 案B. ATS自体を無効にする

[前回の記事](http://qiita.com/tjnet/items/aa268821346583c8795d)では、この方法でATSを回避しました。具体的には、Info.plistに以下の記述を追加します。

```
<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <true/>
</dict>
```

しかし、[このコメント](https://github.com/AFNetworking/AFNetworking/issues/2779#issuecomment-112030880)を確認する限り、明確な理由なしに ``NSAllowsArbitraryLoads ``を使用するアプリはリジェクトされる可能性が高そうです。


>Setting NSAllowsArbitraryLoads to true will allow it to work, but Apple was very clear in that they intend to reject apps who use this flag without a specific reason.


以下のコメントも気になるところです。

>If you do need access to specific URLs that are not served over TLS 1.2, you need to write specific exceptions for those domains, not use NSAllowsArbitraryLoads set to yes. You can find more info in the NSURLSesssion WWDC session.

>Please be careful in sharing the NSAllowsArbitraryLoads solution. It is not the recommended fix from Apple.



公式な情報としては、[Networking with NSURLSession - WWDC 2015 - Videos - Apple Developer](https://developer.apple.com/videos/play/wwdc2015-711/)を参照ください。


### 案C. 特定のドメインにおいて、ATSを無効にする

ATSを基本的には有効にして、対象ドメインのみ無効にするにはInfo.plistに以下のように指定すると良いようです。

```
<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <false/>
  <key>NSExceptionDomains</key>
  <dict>
    <key>[ATSを無効にするドメイン]</key>
    <dict>
      <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
      <true/>
    </dict>
  </dict>
</dict>
```



### 案D. SFSafariViewControllerを使用する

案Cを採用することで、NSExceptionDomainsに指定した特定のドメイン(例: qiita-stock.info)から記事一覧を取得することが可能となります。ところが、問題はまだ残っています。

Web APIから取得した記事一覧が、このようなURLの配列で表せるとしましょう。

```
[
  'https://hoge.com/articles/1', 
  'http://foo.com/articles/35', 
  'https://bar.com/articles/43',       
]

```

1番目のhoge.com, 3番目のbar.comは良いとして、
2番目の記事を表示するために、UIWebViewで ``http://foo.com/articles/35`` にアクセスしようとした際、エラーが発生してしまうはずです(試してないけど)。

ただ、Web APIにアクセスして記事一覧を取得する前に、それぞれの記事がどんなURLにリンクしているかを知る術はなく、表示する可能性はあるけれどhttpしか用意されていないドメインを、あらかじめ全てATSのホワイトリストに追記することはできません。。。

そこで、UIWebViewで表示しようとした記事のURLが、https://以外のURLである場合は、UIWebViewでコンテンツを表示するのではなく、SFSafariViewControllerを開いてしまおうと思います。

詳細な実装は以下を参考にします。

[iOS9 - UIWebViewでhttpなURLにアクセスしようとしたら代わりにSFSafariViewControllerで開くようにする - Qiita](http://qiita.com/bricklife/items/ac92be3e33c7c9eaca9b)


### 結論

ここまでの調査を踏まえると、

- 案Aは、管理下にない外部の記事を取得して表示するニュースアプリでは難しい。
- 案Bは、積極的に採用する理由がない。

ということで、AppStoreにリリースする場合には、ATS対応として案C, 案Dを採用することになりそうです。

つまり、

- 案Cを採用して、記事の一覧を取得する際にアクセスするドメインをNSExceptionDomainsとしてATSのホワイトリストに追加する。
- 案Dを採用して、UIWebViewで表示しようとした記事のURLが、https://以外のURLである場合は、UIWebViewでコンテンツを表示するのではなく、SFSafariViewControllerを開く。

という流れで実装を進めることになりそうです。



## <i class="fa fa-cube" style="font-size:1em;"></i>実装



ひとまず、ここでは ``http://qiita-stock.info/api.json``から記事の一覧を取得できれば良いので、Info.plistを以下のように変更します。

**変更前**

```Info.plist
<key>NSAppTransportSecurity</key>
<dict>
    <key>NSAllowsArbitraryLoads</key>
    <true/>
</dict>
```

**変更後**

```Info.plist
<key>NSAppTransportSecurity</key>
<dict>
  <key>NSAllowsArbitraryLoads</key>
  <false/>
  <key>NSExceptionDomains</key>
  <dict>
    <key>qiita-stock.info</key>
    <dict>
      <key>NSTemporaryExceptionAllowsInsecureHTTPLoads</key>
      <true/>
    </dict>
  </dict>
</dict>
```

## <i class="fa fa-cube" style="font-size:1em;"></i>動作確認

Info.plistの設定変更後、再度、以下のコードを実行してみます。

```ViewController.swift
        Alamofire.request(.GET, "http://qiita-stock.info/api.json").responseJSON {
            response in
            if response.result.isSuccess {
                print(response)
            }
        }
```

**ログ**

...動きました!responseを受け取ってprintできています。

```
SUCCESS: (
        {
        "begin_date" = "2015-12-21";
        "end_date" = "2015-12-27";
        links =         (
                        {
                author = greymd;
                icon = "https://s3-ap-northeast-1.amazonaws.com/qiita-image-store/0/68963/e30764b7a9e2fe658c9371090e92bff50bcba41d/medium.png?1448973839";
                "is_new" = 1;
                rank = 1;
                stock = 617;
                title = "\U3010\U305f\U306e\U3057\U3044\U306a\U3011\U69d8\U3005\U306a\U30b3\U30de\U30f3\U30c9\U9054\U3092\U4f55\U3082\U8003\U3048\U305a\U306b\U3064\U306a\U3044\U3067\U904a\U3076";
                url = "http://qiita.com/greymd/items/a4ecf8e70f11eb1e5f72";
            },
```            



ここまでのソースコードは下記コマンドで取得できます。

``git clone --branch v1.4 https://github.com/tjnet/NewsAppWithSwift.git``




## <i class="fa fa-cube" style="font-size:1em;"></i>感想

ATSへの対応も、調査し始めると非常に奥が深いものでした。

間違い等、ございましたらご指摘ください:bow:

## <i class="fa fa-cube" style="font-size:1em;"></i>参考

- [How to use Alamofire and SwiftyJSON with Swift? – Swift 2 – iOS 9 – Xcode 7 | Ashish Kakkad](http://ashishkakkad.com/2015/10/how-to-use-alamofire-and-swiftyjson-with-swift-swift-2-ios-9-xcode-7/)
- [How do I load an HTTP URL with App Transport Security enabled in iOS 9? - Stack Overflow](http://stackoverflow.com/questions/30731785/how-do-i-load-an-http-url-with-app-transport-security-enabled-in-ios-9)
- [ios - Transport Security has Blocked a cleartext HTTP - Stack Overflow](http://stackoverflow.com/questions/31254725/transport-security-has-blocked-a-cleartext-http)
- [xcodeにてApp Transport Security（ATS）除外のドメインを複数設定する - Qiita](http://qiita.com/_daniel_/items/87b8c33e2c7ad767c9dc)
- [iOS9のWebviewでNSURLErrorDomainとなる場合の対処法(ATS) - Qiita](http://qiita.com/peromasamune/items/f5b72c4dbd33b5019611)
- [iOS9でThe resource could not be loaded because the App Transport Security policy requires the use of a secure connection.のエラーが出る場合のメモ - Qiita](http://qiita.com/nogson/items/38026353e00ac30c8d32)
- [Cocoa Keys](https://developer.apple.com/library/ios/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW33)
- [iOS9 : App Transport Securityの確認方法 - VOYAGE GROUP techlog](http://techlog.voyagegroup.com/entry/2015/09/04/182843)
- [Apple iOS9 (ATS)への対応](http://www.slideshare.net/tech_jstream/iosats)
- [How to disable iOS 9 App Transport Security in Xcode 7](https://www.linkedin.com/pulse/how-disable-ios-9-app-transport-security-xcode-7-jatin-panchal-1)
- [iOS9から導入されたATSとは？そして回避する方法は？ - Be a Engineer.](http://shirakiya.hatenablog.com/entry/2015/10/03/234816)