個人的なまとめ。

てきとーに何か書きます。

Alamofire5でクライアント証明書を読み込んで通信

まだまだSwiftでのNetwork系フレームワークはAlamofire一強みたいなので、特別な事情が無い限りAlamofireを使用するようにしています。
github.com


今回、サーバ証明書だけでなくクライアント証明書をAlamofireで読み込んで使用する状況が発生して結構時間を取られたのでコードを書いておきます。
※証明書はすべて自己証明書です。

今回の環境は以下です。

  • Swift 5.3.1 (swiftlang-1200.0.41 clang-1200.0.32.8)
  • Alamofire5 12/1時点の最新
  • nginx 1.14.1 (CentOS8)
  • ※サーバ類はAWS上で構築


1. まずは、nginxで設定する用の証明書を発行します。
qiita.com
上記サイトを参考に設定を行っていきますが、DESの使用は脆弱性があるため今回は使用しません。

openssl genrsa 4096 > ca.key

みたいな感じで作成しました。

クライアント証明書で使用する鍵は、PKCS12方式のものです。
上記サイトの最後に発行している鍵がそれに該当します。これをアプリ側に読み込ませる事で、クライアント証明書を使用して通信を行う事ができます。

2. nginx側の設定をする。
下記のように設定しました。(一部抜粋)

http {
    server_tokens off; // サーバ情報を隠す
    server {
        listen       443 ssl http2 default_server;
        server_name  _;
        root         /usr/share/nginx/html;
        ssl on;
	ssl_certificate "/etc/nginx/certificates/user.crt";
 	ssl_certificate_key "/etc/nginx/certificates/user.key";
 
 	ssl_client_certificate "/etc/nginx/certificates/ca.crt";
 	ssl_verify_client on;
    }
}

※ locationやerror_pageの設定はそのまま使用しています。

3. 作成後に一度ブラウザでアクセスする。
権限が足りません。などのメッセージが表示されれば、クライアント証明書の設定完了です。
Chromeで開いた場合に下記画面で先に進めない場合は、「thisisunsafe」と入力すれば自動的に遷移します。
f:id:kosuke128:20201212150352p:plain

下みたいに表示されればクライアント証明書の設定が完了しています。
f:id:kosuke128:20201212150354p:plain

※クライアント証明書を端末にインストール後は、Welcomeページが表示されます。

4. Swift

Alamofireをgitから落としておきます。cocoapodsを使用しても良いですが、今回はcocoapodsを使用せずに直接ファイルをgitから落としてきて、プロジェクト内で読み込んでいます。
基本的に使用するファイルは以下です。

  • ViewController.swift
  • extension.swift
  • Alamofire.swift (gitから落としてきたSource)
  • Resoucesフォルダを作成して、この中にクライアント証明書(今回はuser.pfx)を入れる

まず、AlamofireのAFの設定を変更する。
Alamofire.swift

private var AF = Session.default

これを下記のように変更する。

let manager = ServerTrustManager(allHostsMustBeEvaluated: false, evaluators:["IP Address or Domain Name": DisabledTrustEvaluator()])
let session = Session(serverTrustManager: manager)
public var AF = session

managerのallHostsMustBeEvaluated: false は、デフォルトの設定で、evaluators以下に書いてある部分は、IPまたはドメイン名で指定された先のサーバ証明書の信頼性を無視する設定になっています。
なのでここでは、指定された宛先の証明書の信頼性は無視するが、それ以外は通常通りの動きを行う、といった風になります。

Alamofireの中身は単純なSessionなので、自分で変更したSessionを既存のAFと置き換えるように設定します。


次に、ViewControllerの設定です。

ViewController.swift

import UIKit
import Alamofire

public class ViewController: UIViewController, URLSessionDelegate, NSURLConnectionDelegate, URLSessionDataDelegate{
        
    @IBOutlet weak var apiButton: UIButton!
    @IBOutlet weak var apiLabel: UILabel!
    
    // サーバの指定は、ドメインでもIPでも可能
    //let domain: String = "AWS-DOMAIN.ap-northeast-1.compute.amazonaws.com"
    let domain: String = "IP ADDRESS"

     
    public override func viewDidLoad() {
        super.viewDidLoad()
        /// リクエストを投げる時に、クライアント証明書をくっ付ける
        AF.request("https://\(self.domain)").authenticate(with: getClientUrlCredential())
            .responseString { response in
                print(response)
        }
        
        /// 制限を付けてないため、authenticateは不要
        AF.request("https://www.google.co.jp")
            .responseString { response in
                print(response)
        }
    }
    
    // クライアント証明書が正しいか判定する
    func getClientUrlCredential()->URLCredential {
        let userIdentityAndTrust = Bundle.main.identity(named: "user", password: "")
        //Create URLCredential
        let urlCredential = URLCredential(identity: userIdentityAndTrust,
                                          certificates: userIdentityAndTrust as? [Any],
                                          persistence: URLCredential.Persistence.permanent)
        
        return urlCredential
    }
}

簡単に説明をすると、クライアント証明書を使用したい宛先にリクエストを投げる場合は、.authenticateをくっつけて、その中でURLCredentialを返すようにします。
その際、クライアント証明書の設定が問題ないかを確認しています。(後述)
クライアント証明書を使用していないサーバへのリクエストは、.authenticateを付けずにそのままリクエストを投げます。

クライアント証明書の判定を行う。
extension.swift

import Foundation

extension Bundle {
    // クライアント証明書の確認
    func identity(named name: String, password: String) -> SecIdentity {
        let p12URL = self.url(forResource: name, withExtension: "pfx")!
        let p12Data = try! Data(contentsOf: p12URL)

        var importedCF: CFArray? = nil
        let options = [kSecImportExportPassphrase as String: password]
        let err = SecPKCS12Import(p12Data as CFData, options as CFDictionary, &importedCF)
        precondition(err == errSecSuccess)
        let imported = importedCF! as NSArray as! [[String:AnyObject]]
        precondition(imported.count == 1)

        return (imported[0][kSecImportItemIdentity as String]!) as! SecIdentity
    }
}

p12URLでクライアント証明書を読み込むようにしています。
この時、拡張子が.pfx以外の場合はwithExtensionを変更してください。
もし、複数のクライアント証明書が存在してそれぞれ読み込む必要がある場合は、withExtensionは必須じゃないので、forResourceのname引数に拡張子まで含めて渡して上げれば問題ないです。

ここで判定してクライアント証明書が問題なければ通信を行う事ができます。

基本的に、アプリ自体の更新やProvisioning Profileの更新があるので、そのタイミングでクライアント証明書も更新してあげれば良いかと思います。

ちなみに、iPhoneのシミュレータにクライアント証明書を入れる場合は、ドラッグアンドドロップでインストールできます。


今回AWSでCentOS8の環境を作成する時に、AWS公式が配布しているAMIではない元のAMIを使用していたせいで、
1時間ごとにOSに対して課金されていたので、発行元は気をつけたほうが良いです。