strongSwanにiPhoneからオレオレ証明書認証でIPsec IKEv2を繋げる

# はじめに

iOSでIKEv2が使えるようになっているのですが、パスワード認証の話が多くて証明書による認証について書いてあるところがあまりないので、試行錯誤の結果をまとめておきます。

# 目的

以下の環境での接続を目指します。

* サーバ
    * Ubuntu 18.04
    * strongSwan (swanctl)
    * オレオレサーバ証明書
* クランアント
    * iPhone (iOS 14)
    * オレオレクライアント証明書
* 使用プロトコルと認証方法
    * IKEv2
    * クライアント証明書認証（パスワードなし）

# 凡例

この文章では以下のようになっているものとします。

* ドメイン名: `exmaple.com`
* サーバホスト名: `servername.example.com`
* サーバアドレス: `198.51.100.1` (DNS登録済み)
* クライアント名: `iphone`
* クライアントに割り当てる仮想IP: `203.0.113.0/24`
* クライアント用DNSサーバ: `198.51.100.2`
* オレオレ証明書の名前: `strongswan-ca.example.com`

説明上ここではIPv4しか使っていませんが、IPv6も使えます。poolとlocal_tsに`,`で区切ってIPv4アドレスとともに列挙すればOKです。
ただし、ping6は通るのですがtest-ipv6.comやaaaa onlyなwebサイトとの通信は失敗しますので事実上使い物になりません。

# ハマりどころ

試行錯誤の結果、以下に気をつけると繋がりました。

## 証明書はRSAではなくECDSAにする

以下のバグのためstrongSwanはRSA証明書での検証ではSHA1しか使えないのですが、iOSはRSA証明書についてはSHA2を使うようです（というかIKEv2のRFCにはSHA2を使えと書いてある）。
そこで、クライアント側もサーバ側もECDSAを使うことでこの問題を回避します。

[Issue #3343: signature validation failed, looking for another key - strongSwan](https://wiki.strongswan.org/issues/3343 "Issue #3343: signature validation failed, looking for another key - strongSwan")

## 証明書のCN, SAN, EKU

iOSにいれるクライアント証明書は、

* CN, SANともに`iphone@example.com` や `iphone.example.com` の形式ではなく `iphone` にする
* EKUは`clientAuth`

strongSwanにいれるサーバ側の証明書は、

* CN, SANともに`servername.example.com` にする
* EKUは`serverAuth`

となります。

[IKEv2 で接続できた各OSの認証方式とクライアント証明書 | KUSONEKOの見る世界](http://kusoneko.blogspot.com/2018/03/ikev2-usable-client-certificate.html "IKEv2 で接続できた各OSの認証方式とクライアント証明書 | KUSONEKOの見る世界")

## オレオレCAのルート証明書をいれて信頼する必要がある

後述のmobileconfigに同梱すればIKEv2接続時だけ勝手に信頼してくれるのかと思ったのですが、そうではありませんでした。
mobileconfigとは別にオレオレCAのルート証明書もいれて`設定 - 一般 - 情報 - 証明書信頼設定` から全面的信頼をつける必要があります。

[スマートフォン（iOS、Android）にオレオレ証明書がインストールできない？ | 世界一わかりやすいセキュリティ](https://security.data-site.info/291.html#iOS "スマートフォン（iOS、Android）にオレオレ証明書がインストールできない？ | 世界一わかりやすいセキュリティ")

サーバ証明書をletsencryptにした場合、この作業は不要なようです (2022/05追記)。

## mobileconfig

* 「証明書のタイプ」はECDSA521など正しい値を選択する（ECDSAなのにRSAのままにしない）
* 「PFSを使用する」はチェックしない

PFSを頑張ったのですがどうもうまくいきませんでした...

[IKEv2 Configuration Profile for Apple iOS 8 and newer - IKEv2 Configuration Profile for Apple iOS 8 and newer - strongSwan](https://wiki.strongswan.org/projects/strongswan/wiki/AppleIKEv2Profile#EAP-TLS-authentication "IKEv2 Configuration Profile for Apple iOS 8 and newer - IKEv2 Configuration Profile for Apple iOS 8 and newer - strongSwan")
[iOS (Apple iPhone/iPad...) and macOS Interoperability - iOS (Apple iPhone/iPad...) and macOS Interoperability - strongSwan](https://wiki.strongswan.org/projects/strongswan/wiki/AppleClients "iOS (Apple iPhone/iPad...) and macOS Interoperability - iOS (Apple iPhone/iPad...) and macOS Interoperability - strongSwan")

## OpenSSL 3.0系のp12はiOS/macOSでインポートできない

OpenSSL 3.0の生成したp12の暗号化の形式にiOS/macOSが対応していないようです。
Ubuntu 22のOpenSSLは3.0ですので、頑張って1.1.1系を入れるか`--certpbe NONE --keypbe NONE`と諦めてしまう必要があります。

[SecPKCS12Import is failing to impo… | Apple Developer Forums](https://developer.apple.com/forums/thread/697030)
[rdar://FB8988319: MacOS security framework fails to import RFC 7292 compliant PKCS #12 v1.1 file into keychain using modern cyphers](https://openradar.appspot.com/FB8988319)

## proposalsを指定する

strongswanのdefaultではだめでした。

# 証明書作成

こちらに掲載のスクリプトを修正して使いました。感謝。

[strongSwanでVPN (IKEv2)を構築する - Qiita](https://qiita.com/namoshika/items/472560b18ea417add2d0 "strongSwanでVPN (IKEv2)を構築する - Qiita")

## CA生成

一回だけ実行します。

```sh:initca.sh
#!/bin/bash

set -x
set -e

#各種 key,crt,サーバードメインを埋めて実行します。
#ファイル名
readonly CA_NAME=strongswan-ca
readonly DOMAIN=example.com
readonly OUTPATH=$HOME/strongswan/pki/certs/

readonly CA_KEY=$CA_NAME.key
readonly CA_CRT=$CA_NAME.crt

#初期化
rm -fr $OUTPATH
mkdir $OUTPATH

LIFETIME="--lifetime 3650"
#ルート証明書作成
cakey=${OUTPATH}/${CA_KEY}
cacrt=${OUTPATH}/${CA_CRT}
ipsec pki --gen $OPTS > $cakey
ipsec pki --self --in $cakey $LIFETIME --ca --dn "C=JP, O=strongSwan, CN=${CA_NAME}.${DOMAIN}" --san "${CA_NAME}.${DOMAIN}" > $cacrt

#確認
openssl x509 -text -fingerprint -noout -inform der -in $cacrt

#pem
openssl x509 -inform der -outform pem -in $cacrt > ${OUTPATH}/${CA_NAME}.pem

etckey=/etc/swanctl/ecdsa/$CA_KEY
etccrt=/etc/swanctl/x509ca/$CA_CRT
sudo cp $cakey $etckey
sudo cp $cacrt $etccrt
sudo chmod 600 $etckey $etccrt
sudo chown root:root $etckey $etccrt
```

## サーバ証明書生成

```sh:genserver.sh
#!/bin/bash

set -x
set -e

readonly HOST_NAME=$1

[ -z "$HOST_NAME" ] && no_host_name

readonly CA_NAME=strongswan-ca
readonly DOMAIN=example.com
readonly P12PASS=opensesame
readonly OUTPATH=$HOME/strongswan/pki/certs/
readonly CA_KEY=$CA_NAME.key
readonly CA_CRT=$CA_NAME.crt

readonly SRV_DOMAIN=example.com
readonly VPN_KEY=$HOST_NAME.key
readonly VPN_CRT=$HOST_NAME.crt

cakey=${OUTPATH}/${CA_KEY}
cacrt=${OUTPATH}/${CA_CRT}
srvkey=${OUTPATH}/${VPN_KEY}
srvcrt=${OUTPATH}/${VPN_CRT}

OPTS="--type ecdsa --size 521"
TYPE=ecdsa
TYPE_O=ec

#OPTS="--type rsa --size 4096"
#TYPE=rsa
#TYPE_O=rsa
LIFETIME="--lifetime 3650"

[ -e $srvkey ] && already_exists

ipsec pki --gen $OPTS > $srvkey

if [[ $0 =~ server ]] ;then
    echo server
    FLAGS="--flag serverAuth"
    SAN="${HOST_NAME}.${SRV_DOMAIN}"
elif [[ $0 =~ name ]] ;then
    # ios用
    FLAGS="--flag clientAuth"
    SAN="${HOST_NAME}"
elif [[ $0 =~ client ]] ;then
    # macos他普通のクライアント用
    FLAGS="--flag clientAuth"
    SAN="${HOST_NAME}@${SRV_DOMAIN}"
fi

ipsec pki --issue $LIFETIME $FLAGS \
      --in $srvkey --type priv --cacert $cacrt --cakey $cakey \
      --flag serverAuth \
      --dn "C=JP, O=strongSwan, CN=${SAN}" \
      --san ${SAN} > $srvcrt
#確認
openssl x509 -text -fingerprint -noout -inform der -in $srvcrt

etckey=/etc/swanctl/$TYPE/$VPN_KEY
etccrt=/etc/swanctl/x509/$VPN_CRT

sudo cp $srvkey $etckey
sudo cp $srvcrt $etccrt
sudo chmod 600 $etckey $etccrt
sudo chown root:root $etckey $etccrt

#p12
p12=${OUTPATH}/${HOST_NAME}.p12
ca_cert_pem=$(tempfile).pem
srv_cert_pem=$(tempfile).pem
srv_key_pem=$(tempfile).pem

openssl x509    -inform der -outform pem -in $cacrt  -out $ca_cert_pem
openssl x509    -inform der -outform pem -in $srvcrt -out $srv_cert_pem
openssl $TYPE_O -inform der -outform pem -in $srvkey -out $srv_key_pem
openssl pkcs12 -export -passout pass:$P12PASS -inkey $srv_key_pem -in $srv_cert_pem -certfile $ca_cert_pem -out $p12

rm $ca_cert_pem $srv_cert_pem $srv_key_pem
```

これを実行します。

```shell-session:
$ ./genserver.sh servername
```

なお、クライアント証明書はオレオレのままでサーバ証明書だけをletsencryptにすることもできます。
この場合、iOS/mac側でオレオレCAを強制的に信用しなくてもクライアント証明書が使えます。
[Ubuntuで作るiOS/AndroidでIKEv2接続できるVPN環境のつくりかた - Qiita](https://qiita.com/garupon/items/89297b022d4720bc6341)

## クライアント証明書生成

上記スクリプトを`gennamesh`に`ln -s`して実行します。

```shell-session:
$ ln -s genserver.sh genname.sh
$ ./genname.sh iphone
```

# StrongSwan (swanctl) の設定


```/etc/swanctl/conf.d/iphone.conf
connections {
iphone {
  local_addrs = 198.51.100.1
  proposals = aes256-sha256-modp2048

  local {
   auth = pubkey
   certs = servername.crt
   id = servername.exmaple.com
  }
  remote {
   auth = pubkey
   id = iphone
  }
  pools = iphone-pool
  children {
   iphone {
    local_ts = 0.0.0.0/0,::/0
    remote_ts = 203.0.113.0/24
    esp_proposals = aes256-sha256
   }
  }
  encap = yes
  send_certreq = no
  send_cert = always
  keyingtries = 10
  version = 2
  mobike = yes
  reauth_time = 0
 }
}

pools {
  iphone-pool {
    addrs = 203.0.113.0/24
    dns = 198.51.100.2
  }
}
```

# iPhoneの設定

Apple Configurator 2を使ってmobileconfigを生成します。

* 証明書
    * `genname.sh`で生成したPKCS12をいれる
* VPN
    * 接続のタイプ: IKEv2
    * サーバ: `servername.exmaple.com`
    * リモート識別子: `servername.example.com`
    * ローカル識別子: `iphone`
    * コンピュータ認証: 証明書
    * 固有名証明書: 上記の追加したPKCS12を選択
    * 証明書のタイプ: ECDSA521
    * PFSを有効にする: チェックしない
    * 暗号化アルゴリズム: AES-256
    * 整合性アルゴリズム: SHA2-256

作ったら安全にiPhoneに転送してプロファイルとして取り込みます。

それとは別に、`initca.sh`が生成してくれた`strongswan-ca.pem` もいれて、`設定 - 一般 - 情報 - 証明書信頼設定` から全面的信頼をつけます。

# 参考: macOSの場合

```shell-session:
$ ln -s genserver.sh genclient.sh
$ ./genclient.sh mac
```

として`mac.p12`を生成してキーチェーンに取り込み、やはりルート証明書として強制的に信頼させたあと、`環境設定 - ネットワーク`から以下の条件でVPNを設定します。

* VPNタイプ: IKEv2
* サーバアドレス: `servername.example.com`
* リモートID: `servername.exmample.com`
* ローカルID: `mac@example.com`
* 認証設定: 証明書 (`mac@example.com`の証明書を選択)

# ChaCha20-Poly1305

高速でNSAのバックドアが無いであろうchacha20-poly1305も使えます。
iPhone側はAESをハードウェアで処理できるそうですので、もしかしたらAESの方がバッテリーには優しいかもしれませんが...

[strongSwan と iOS (13+) 端末間で IKEv2 に ChaCha20-Poly1305 を使って遅いラズパイ VPN サーバーを高速化 - Qiita](https://qiita.com/aryta/items/e2d6d6123ed0934858c5 "strongSwan と iOS (13+) 端末間で IKEv2 に ChaCha20-Poly1305 を使って遅いラズパイ VPN サーバーを高速化 - Qiita")

一つだけ問題がありまして、Ubuntu 18.04のstrongSwan 5.6.2にはchacah20poly1305回りにバグがあり、そのままではつながりません。
以下のチケットに記載のパッチをあてるか、5.6.3以上を入れる必要があります。

[Issue #3195: chacha20poly1305 cipher name mismatch on proposals - strongSwan](https://wiki.strongswan.org/issues/3195 "Issue #3195: chacha20poly1305 cipher name mismatch on proposals - strongSwan")

# 謝辞

strongSwanの製作者、リンク先の記事の筆者の皆様、ありがとうございます。