iOS Development 証明書と Provisioning profile を自作するとどうなるのか

## TL;DR

- :white_check_mark: 証明書と profile を Mac にインストールできる
- :white_check_mark: Xcode 上で自作 profile を指定できる
- :x: iOS app ビルド時に署名できる
- :x: iOS framework ビルド時に署名できる

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73424/0c3c0cd8-0d68-0795-edb5-c25eb8b8b485.png)

## 概要

ひょんなことから iOS Development 証明書と provisioning profile を、オレオレ SSL 証明書を作る要領で自作した場合、どんなことが起こるのか検証してみました。

## 参考文献

実は試す前からこれが可能であることは、以下の記事を読んで知っていました。
ただ記事が 2018 年に書かれたものであるので、今でもできるのか知りたかったのと、Keychain Access を使わずに作成することでより理解が深まると思って始めました。

https://dkimitsa.github.io/2018/01/04/ios-homemade-provision-profile/

ちなみに記事によると 2018 年当時は ipa ファイルのコード署名をすることもできたらしいです。当然 AppStore にアップロードはできないので意味はないようですが。

## 手順

さて、さっそく証明書と provisioning profile を自作してみましょう。
証明書の作成や署名に OpenSSL を利用するので、あらかじめインストールが必要です。
今回は以下のような環境で行いました。

- macOS 12.5
- Xcode 14.1
- OpenSSL 3.0.7

### 概略

作業の流れとしては以下のような感じに単純化できます。

```mermaid
flowchart LR
  signCert[自己署名証明書] --> |sign| profile[provisioning profile]
  caCert[ルート CA] --> |create| signCert
```

初めにカスタムルート CA を作成し、そこに証明書署名要求を送ることで自己署名証明書を作ります。
自己署名証明書を利用して provisioning profile に署名をすれば完成です。

ただし、それぞれ秘密鍵の作成が必要だったり、provisioning profile の作成に証明書の情報が必要になったりして実際にはもうちょっと複雑になります。

### ルート CA を作成する

まずは秘密鍵が必要なので、作成します。

```sh
openssl genrsa -out ca_key.pem 2048
```

続いて証明書 (ca.cer) とシリアル番号 (ca.srl) を発行します。
subject や拡張、ハッシュ関数は [本家 Apple の CA 証明書](https://www.apple.com/certificateauthority/) になるべく合わせています。

```sh
openssl req -x509 -new -noenc -key ca_key.pem -sha1 -days 365 \
  -subj '/C=US/O=SELFSIGNED/OU=SELFSIGNED Certification Authority/CN=SELFSIGNED Root CA' \
  -addext 'keyUsage=critical,keyCertSign,cRLSign' \
  -outform DER \
  -out ca.cer

```

### 自己署名証明書を作成する

先ほど作成した CA に証明書署名要求を送るので、まずは秘密鍵を作成します。

```sh
openssl genrsa -out sign_key.pem 2048
```

続いて CSR ファイルを作成します。
subject やハッシュ関数は Keychain Access.app が作成するものに一応合わせていますが、ここは適当で大丈夫だと思います。

```sh
openssl req -new -noenc -key sign_key.pem -sha256 \
  -subj '/emailAddress=user@example.com/CN=user/C=JP' \
  -out sign.csr
```

拡張領域の定義をするためのファイルを作成します。
いずれの項目も重要ですが、特徴的なのは最後の行の `1.2.840.113635.100.6.1.2` でしょうか。
これは Apple の Developer 証明書であることを示す拡張の OID です。データに `05:00` (時刻ではなく hex bytes です) が入っていますが、なんのことかわからないまま、他の profile を見てコピーしてきました。

```conf
authorityKeyIdentifier=keyid,issuer
basicConstraints=critical,CA:FALSE
certificatePolicies=1.2.840.113635.100.5.1
extendedKeyUsage=critical,codeSigning
keyUsage=critical,digitalSignature
1.2.840.113635.100.6.1.2=critical,DER:05:00
```

最後に証明書を要求します。
例によって subject は本家に合わせています。

```sh
openssl x509 -req -CA ca.cer -CAkey ca_key.pem -CAcreateserial -days 365 \
  -extfile codesign_cert.conf \
  -subj '/UID=0000000000/CN=iPhone Developer: $(USER_EMAIL) (0000000000)/OU=0000000000/C=US' \
  -in sign.csr -outform DER -out sign.cer \
```

### Provisioning profile のテンプレートを作る

Provisioning profile の実態はよくある XML Property List に PKCS#7 署名を施したものです。
したがってまずは元となる XML Property List を書きます。
イチから作るのは大変なので、お手元の開発用途の provisioning profile をコピーしてくるのをおすすめします。

`__APP_ID_NAME__` などところどころに `__` で囲まれた文字列を置いてありますが、これらはあとで置換するためのものです。
いずれもハードコードしておけば置換の手間が省けますが、自由にいろんな profile をつくるためにこうしています。

```xml:example.plist.in
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
	<key>AppIDName</key>
	<string>__APP_ID_NAME__</string>
	<key>ApplicationIdentifierPrefix</key>
	<array>
	<string>__APP_ID_PREFIX__</string>
	</array>
	<key>CreationDate</key>
	<date>__CREATION_DATE__</date>
	<key>Platform</key>
	<array>
		<string>iOS</string>
	</array>
    <key>IsXcodeManaged</key>
    <false/>
	<key>DeveloperCertificates</key>
	<array>
		<data>__DEVELOPER_CERTIFICATES__</data>
	</array>
	<key>Entitlements</key>
	<dict>
		<key>keychain-access-groups</key>
		<array>
			<string>__APP_ID_PREFIX__.*</string>
		</array>
		<key>get-task-allow</key>
		<true/>
		<key>application-identifier</key>
		<string>__APP_ID__</string>
		<key>com.apple.developer.associated-domains</key>
		<string>*</string>
		<key>com.apple.developer.team-identifier</key>
		<string>__APP_ID_PREFIX__</string>
		<key>aps-environment</key>
		<string>development</string>
	</dict>
	<key>ExpirationDate</key>
	<date>__EXPIRATION_DATE__</date>
	<key>Name</key>
	<string>__NAME__</string>
	<key>ProvisionedDevices</key>
	<array>
	</array>
	<key>TeamIdentifier</key>
	<array>
		<string>__APP_ID_PREFIX__</string>
	</array>
	<key>TeamName</key>
	<string>__TEAM_NAME__</string>
	<key>TimeToLive</key>
	<integer>__TIME_TO_LIVE__</integer>
	<key>UUID</key>
	<string>__UUID__</string>
	<key>Version</key>
	<integer>1</integer>
</dict>
</plist>
```

なお、JSON や NeXTSTEP (ASCII) 形式の Property List では表現できない `date` 型が含まれているので、初めに JSON で書いて `plutil` で変換するというような小技は使えません。

### Provisioning profile の値を埋める

先ほど作ったテンプレートの値を埋めます。
何のツールを使ってもいいのですが、今回は懐かしの `m4` を使います。
`m4` は多くの OS で追加のインストールなしに利用可能なので選択しましたが、Xcode が動く macOS なら高度な機能を持つ ERuby テンプレートが使えるので、そちらの方が楽かもしれないですね。

```sh
m4 \
  -D '__APP_ID_NAME__=Example App' \
  -D '__APP_ID_PREFIX__=0000000000' \
  -D '__APP_ID__=$(APP_ID)' \
  -D "__CREATION_DATE__=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
  -D "__DEVELOPER_CERTIFICATES__=$(openssl x509 -inform DER -in sign.cer -outform PEM | sed -e '/-----/d' | tr -d '\n')" \
  -D "__EXPIRATION_DATE__=$(openssl x509 -enddate -dateopt iso_8601 -noout -inform DER -in sign.cer | cut -d= -f2 | tr ' ' T)" \
  -D '__NAME__=Example mobileprovision' \
  -D '__TEAM_NAME__=Example Inc.' \
  -D '__TIME_TO_LIVE__=365' \
  -D "__UUID__=$(uuidgen)" \
  example.plist.in > example.plist
```

`__DEVELOPER_CERTIFICATES__` にはこれから署名に利用する証明書を PEM 形式に変換したものからヘッダとフッタを取り除いた BASE64 文字列を入れます。
また、`__EXPIRATION_DATE__` は証明書の有効期限を越えないようにします。今回は証明書の有効期限をそのまま ISO8601 形式で入れます (ISO8601 には幅があるので、既存の profile にフォーマットを合わせています)。

### Provisioning profile に署名する

さて、満を持して署名をします。
ひとくちに PKCS#7 といっても PEM や SMIME などの形式があり、コンテンツの表現もさまざまですが、developer.apple.com から出力される形式に揃えると、SHA1 ハッシュを利用し (`-md sha1`) 、コンテンツは変換なしで付加し (`-binary -nodetach`) DER 形式で出力する (`-outform DER`) のが良いようです。

```sh
openssl cms -sign -md sha1 -binary -nodetach \
  -signer sign.cer \
  -keyform PEM -inkey sign_key.pem -in $< \
  -outform DER -out example.mobileprovision
```

### 証明書と profile を Mac にインストールする

証明書を Xcode から参照するためには keychain に登録する必要があります。
せっかくなのでコマンドから実行してみましょう。

ちなみに、なぜか私の環境では sign_key.pem のみ GUI では登録できず、CLI でインポートする必要がありました。

```sh
security import ca.cer -k ~/Library/Keychains/login.keychain-db
security import sign.cer -k ~/Library/Keychains/login.keychain-db
security import sign_key.pem -k ~/Library/Keychains/login.keychain-db
```

Provisioning profile はダブルクリックするだけでインポートできますが、あえてコマンドで実行するなら以下のようになります。
`xed` コマンドで Xcode に渡していますが、`open` でも良いし、たぶん `~/Library/MobileDevice/Provisioning Profiles` にコピーするだけでもいいような気がします。

```sh
xed example.mobileprovision
```

あとはルート CA を信頼します。
コマンドでもできる気がしますが、ここは Keychain Access.app を起動しました。
`Command + i` で開く画面から信頼設定を `常に信頼` に変更します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73424/a668fcc9-5a51-0835-7f6a-ea4b1a796e14.png)

### 証明書と profile を Xcode プロジェクトに設定する

あとは適当な Xcode プロジェクトに設定してみましょう。
ここまでの手順にミスがある場合は選択肢に出てこなかったりして設定できないので、よく見直しましょう。
証明書は `openssl x509 -text -noout -in sign.cer` のようにして内容を調べたり、profile は `security cms -Di example.mobileprovision | plutil -p -` のようにして調べることができます。

Xcode プロジェクトファイルの Build settings タブから、以下の項目を編集します。

項目|値
---|---
CODE_SIGN_IDENTITY | sign.cer の Common Name
CODE_SIGN_STYLE | Manual
DEVELOPMENT_TEAM | profile に設定した開発チームの ID (英数10桁)
PROVISIONING_PROFILE_SPECIFIER | profile の名前 (選択肢に出てくるはず)

この状態でビルドすると、冒頭のようにエラーが出てきます。
これにて実験終了です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73424/0c3c0cd8-0d68-0795-edb5-c25eb8b8b485.png)

## 考察

### ビルド時の証明書の検証プロセス

さて、残念ながら自己署名証明書とそれによって署名された profile ではコード署名ができないという結果がわかってしまいました。

しかし、なぜ Xcode はこの証明書が不正なものだと判断したのでしょうか。
自己署名とはいえ、ルート CA は OS によって信頼されていますし、Apple 独自の拡張領域の設定や、profile の項目、署名の仕方に至るまで正当な provisioning profile と同等なのだから、署名くらい通してくれてもいいはずです。

その答えを知るために、Xcode のログを調べてみることにします。
おもむろに Console.app を立ち上げ、`Xcode` プロセスのログを取りながら再びビルドを失敗させると、以下の情報が取れます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/73424/5be95330-6175-2fb5-a5e2-8a0dcf36b77b.png)

> Trust evaluate failure: [leaf AnchorApple ChainLength IntermediateMarkerOid]

どうも証明書の信頼性の検証に失敗しているような気がします。
ここに登場するキーワード `AnchorApple` `ChainLength` `IntermediateMarkerOid` あたりで opensource.apple.com を検索してみると、以下のファイルがヒットしました。

https://opensource.apple.com/source/security_certificates/security_certificates-55161.40.5/Pinning/CertificatePinning.plist.auto.html

名前や内容からすると、macOS の証明書検証の過程で証明書ピンニングを行なっているようです。
ルート CA が Apple Inc. が発行したものかを検証しているようなので、自己署名証明書を使うことはできません。

もしかしたら設定を間違えていたり、なにかしら回避方法があるのかもしれないですが、調査としてはここまでにしようと思います。

## おまけ

ここまでの内容を Makefile にしてみました。

https://github.com/manicmaniac/homemade-profile