iOS受託開発における署名付け替えの技術


受託開発におけるiOSアプリの納品方法は様々です。

iTunes Connectにアップロードしてくれと頼まれる場合があったり、証明書とProvisioning File渡すからipaをビルドして納品してくれと言われる場合があったり。


お客様のDeveloperIDとパスワードを受領できるケースは、なんでもできると思いますが、そうでない場合は色々と方法を考えなければなりません。
普段関わっているアプリは証明書すらもらえないため、App.xcodeprojを丸ごと納品するスタイルをとっているのですが、下記のような問題があります。

- そのままだとソースコードが丸見えで、悪意がある人間が簡単に流用できてしまう。 
- ので、ソースコードだけは一旦static libraryに固めています。これのせいで納品手順が煩雑になっている上に、[Swiftも使えません](https://github.com/ksm/SwiftInFlux#static-libraries)。
- 納品用にプロジェクトを構成し直すので、みんなで一生懸命テストするものと、Apple審査に出すものが別物になってしまう

もっといい方法があるはず、と思い調べてみました。お楽しみください。

# アウトライン（App.xcarchive案）

まずはxcarchiveを納品するスタイルを考えてみました。結論から言うと、最終的にiTunesConnectで利用するのと同じTeamを使えないとダメでした。つまりお客さんのDeveloperIDとパスワードが分からない場合は採用できないと思います。一応情報としてやろうとしたことを残しておきます。

- App.xcarchive を作る
- App.xcarchiveから開発用の署名をして、ipaを作って配布、テストする
- App.xcarchive を納品する
- iTunesConnectへアップする

## App.xcarchive を作る

Archiveして、codesignを確認します。

```
 $ cd /path/to/archive/
 $ codesign -d CodeSignSample\ 11-28-15\,\ 10.44.xcarchive/
CodeSignSample 11-28-15, 10.44.xcarchive/: code object is not signed at all
```

Archiveした時点ではcodesignはされていないことがわかります。

## App.xcarchiveから開発用の署名をして、ipaを作って配布、テストする

```bash
 $ xcodebuild -exportArchive -archivePath /path/to/App.xcarchive/ -exportPath /path/to/hello.ipa
 ~
 ~(省略)
 ~
 Results at '/var/folders/tr/3lftss053tqdh731cgt0w1p00000gn/T/713B1E1E-9F4D-4F8D-98DD-826D6428F82A-23330-000024AF65316D33/App' 
Moving exported product to '/Users/toshi0383/Library/Developer/Xcode/Archives/2015-11-28/hello.ipa'
** EXPORT SUCCEEDED **
```

ipaができました。

## App.xcarchive を納品する

ソースコード丸見えになっていたりしないか、念のため確認します。

```
 $ cd /path/to/App.xcarchive/
 $ tree
 ~
 ~
 ~
```

省略しますが、ソースコードは (多分 `Products/Applications/App.app/App` に)コンパイルされて実行形態になっているようです。
これで安心して納品できそうです。

## iTunesConnectへアップする

App.xcarchiveをダブルクリックするとXcodeのOrganizerが開きます。ここからいつも通り(本番用のProvisioningで署名をして)ipaを作ってiTunesConnectへアップします。

<img src="https://qiita-image-store.s3.amazonaws.com/0/35008/93ab0e2f-d231-9d97-bdd4-7206c186cc7c.png" height="210"/> 


![Screen Shot 2015-11-28 at 19.07.20.png](https://qiita-image-store.s3.amazonaws.com/0/35008/ce5816cb-31b4-0568-4ffc-9d342f0ea208.png "Screen Shot 2015-11-28 at 19.07.20.png")

ここでコケます。ビルドに使ったチームと違うアカウントでipa作ろうとしてるでしょ？って言われています。ズバリその通り！！むしろそれがやりたいことなんだけど。。

というわけで、Teamが異なってしまう場合はxcarchiveを納品する案は使えないようです。

# アウトライン（ipa案）

そのような場合でも、fastlane sign を使えばイケるという情報を入手しました。

  <a href="https://github.com/fastlane/sigh" target="_blank"><img src="https://github.com/fastlane/sigh/raw/master/assets/sigh.png" height="110"></a>
 

> If you generated your ipa file but want to apply a different code signing onto the ipa file, you can use sigh resign

説明を読む限りはズバリこれっぽいです。
まとめるとこのような流れになります。

- App.ipaを開発用の署名で作る、配布する、テストする（ご自由に）
- App.ipaを納品する
- お客さん側でAppStore用の署名+BundleIDに付け替えてそのままiTunesConnectへ

## まずは納品まで

開発で使用するTeamとProvisioningでArchiveします。

![Screen Shot 2015-11-28 at 20.12.17.png](https://qiita-image-store.s3.amazonaws.com/0/35008/1d1a98e7-6c9c-affb-2ae3-604c81ebc94d.png "Screen Shot 2015-11-28 at 20.12.17.png")

Archive成功しました。sigh resignはipaを求めているので、開発で使用するTeamを指定して、ipaを作成します。
![Screen Shot 2015-11-28 at 20.14.13.png](https://qiita-image-store.s3.amazonaws.com/0/35008/fdb876a4-27b6-07c8-7acd-0b0bf870dfc1.png "Screen Shot 2015-11-28 at 20.14.13.png")

ビルドとipa exportのTeamが同じなのでエラーになりませんでした。
念のため、codesignを確認します。

```
 $ unzip CodeSignSample_InHouseSigned.ipa 
 $ codesign -d --entitlements - Payload/CodeSignSample.app/
Executable=/Users/toshi0383/Desktop/inhouse/Payload/CodeSignSample.app/CodeSignSample
??qq?<?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>application-identifier</key>
	<string>P945A6YU32.net.nextscape.hello.world</string>
	<key>aps-environment</key>
	<string>production</string>
	<key>com.apple.developer.default-data-protection</key>
	<string>NSFileProtectionComplete</string>
	<key>com.apple.developer.team-identifier</key>
	<string>P945A6YU32</string>
	<key>com.apple.external-accessory.wireless-configuration</key>
	<true/>
	<key>get-task-allow</key>
	<false/>
	<key>keychain-access-groups</key>
	<array>
		<string>P945A6YU32.net.nextscape.hello.world</string>
	</array>
</dict>
</plist>
$ grep -A1 CFBundleIdentifier ./Payload/CodeSignSample.app/Info.plist
    <key>CFBundleIdentifier</key>
    <string>net.nextscape.hello.world</string>

```

P945A6YU32が開発で使用する証明書のIDです。BundleIDも開発用のものになっています。問題なさそうです。これが、我々が動作確認に使用するipaになります。

次はいよいよこのipaを納品して、お客様側で署名とバンドルIDを付け替えることになりますね。

sighの使い方を調べたところ、

1. Provisioning Profileをダウンロードする
2. Provisioning Profileを指定してresignする

という2段階になりそうです。

```bash
$ sigh -a jp.toshi0383.JamNavi -u ${AppleDeveloperID} -q jamnavi-prod.mobileprovision
# 初回はパスワード聞かれます
# カレントにjamnavi-prod.mobileprovisionが展開されます
[20:50:54]: Successfully logged in
[20:50:54]: Fetching profiles...
[20:50:58]: Found 1 matching profile(s)
[20:50:58]: Downloading provisioning profile...
[20:50:58]: Successfully downloaded provisioning profile...
[20:50:58]: Installing provisioning profile...
/Users/toshi0383/Desktop/codesign/jamnavi-prod.mobileprovision

$ sigh resign ./CodeSignSample_InHouseSigned.ipa --signing_identity 'iPhone Distribution: Toshihiro Suzuki (T47KPZ35V7)' -n "jamnavi-prod.mobileprovision"
# パラメータが間違っていた場合はプロンプトで聞いてきますので答えてあげてください。
~
~(省略)
~
Resigning application using certificate: 'iPhone Distribution: Toshihiro Suzuki (T47KPZ35V7)'
and entitlements from provisioning profile: /Users/toshi0383/Desktop/codesign/jamnavi-prod.mobileprovision
_floatsignTemp/Payload/CodeSignSample.app: replacing existing signature
Repackaging as ../CodeSignSample_InHouseSigned.ipa
Process complete

[20:51:14]: Successfully signed ./CodeSignSample_InHouseSigned.ipa!
```

成功したと言っています。確認しましょう。

```
$ unzip CodeSignSample_InHouseSigned.ipa 
$ codesign -d --entitlements - Payload/CodeSignSample.app/
Executable=/Users/toshi0383/Desktop/codesign/Payload/CodeSignSample.app/CodeSignSample
??qq?<?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>application-identifier</key>
	<string>T47KPZ35V7.jp.toshi0383.JamNavi</string>
	<key>aps-environment</key>
	<string>production</string>
	<key>beta-reports-active</key>
	<true/>
	<key>com.apple.developer.team-identifier</key>
	<string>T47KPZ35V7</string>
	<key>com.apple.security.application-groups</key>
	<array>
		<string>group.jp.toshi0383.jamnavi</string>
	</array>
	<key>get-task-allow</key>
	<false/>
	<key>keychain-access-groups</key>
	<array>
		<string>T47KPZ35V7.*</string>
	</array>
</dict>
</plist>
$ grep -A1 CFBundleIdentifier ./Payload/CodeSignSample.app/Info.plist
	<key>CFBundleIdentifier</key>
	<string>jp.toshi0383.JamNavi</string>
```

署名がつけ変わり、BundleIDも書き換えられました。

## iTunesConnectへアップする

準備が整いましたので、iTunesConnectにアップしましょう。Application Loaderを使いました。

![Screen Shot 2015-11-28 at 23.43.21.png](https://qiita-image-store.s3.amazonaws.com/0/35008/ca3bb19c-71fd-baff-85de-ece9528d4f80.png "Screen Shot 2015-11-28 at 23.43.21.png")

成功した画面をキャプチャし忘れましたが、無事iTunesConnectの画面上に表示されました。

せっかくなのでfastlane deliverも使ってみました。が、アプリのページでエラーが出ているとコケるみたいです。コマンドはこう。

```bash
$ deliver --ipa App.ipa -u ${itc_user}
```

アップロードだけをする方法を教えて欲しい旨[issueにコメント](https://github.com/fastlane/fastlane/issues/391#issuecomment-160310079)しました。回答あったら更新します。

`deliver`はメタデータのチェックとか余計なことまでしてしまうので、お客さんにやってもらうには`altool`の方が安全かもしれません。ただこちらも初回にちゃんと設定しないとまともに動かないという。。私だけ？

```bash
$ PATH=$PATH:$(dirname "$(find $(dirname $(xcode-select -p)) -name altool | head -1)")
$ ln -s "/Applications/Xcode.app/Contents/Applications/Application Loader.app/Contents/itms" /usr/local/

$ altool --upload-app --file '${ipa_filename}' --username ${ITC_USER} --password ${ITC_PASSWORD}
```

最初2行は初回エラーになるときのワークアラウンドです。

# おまけ sigh resignは何をやっているのか

sighのresignは、[resign.sh](https://github.com/fastlane/sigh/blob/0e4e010dbffd3b4cab17ce90f15601fc305a0914/lib/assets/resign.sh) というのが実態のようです。

中を覗くと、`/usr/libexec/PlistBuddy` を使ってアプリのInfo.plistを編集していることがわかります。
ipaをunzipして直接いじっちゃってます。なんて荒技！

PlistBuddyで値を更新しているのは下記の4つでした。

```
`PlistBuddy -c "Set :CFBundleDisplayName $DISPLAY_NAME" "$APP_PATH/Info.plist"`
`PlistBuddy -c "Set :CFBundleIdentifier $BUNDLE_IDENTIFIER" "$APP_PATH/Info.plist"`
`PlistBuddy -c "Set :CFBundleVersion $VERSION_NUMBER" "$APP_PATH/Info.plist"`
`PlistBuddy -c "Set :CFBundleShortVersionString $VERSION_NUMBER" "$APP_PATH/Info.plist"`
```

署名情報も基本的にはplistの形で抜き出せるので、これを編集した上でcodesignし直しているようです。これはXcodeもやっていることだとかソースコードのコメントに書いてありました。

```bash
/usr/bin/codesign -f -s "$CERTIFICATE" --entitlements="$TEMP_DIR/newEntitlements" "$APP_PATH"
```

スクリプト冒頭のライセンス表記を見ると、このスクリプト自体は2011年頃からメンテナンスが継続されているようで、向こうでは結構実績ありそうな感じします。

さらに

> 1. now re-signs embedded iOS frameworks, if present, prior to re-signing the application itself

とある通り、embedded frameworkの署名もつけかえてくれるようです。

# まとめ

以上、iOS受託開発における署名付け替えの技術について解説してきました。

半年以上メンバー全員の悩みの種だった問題が、数日で一気に解決に近づいた気がします。
結論としては、

- sigh resignを使うと、ipaの署名付け替えが簡単にできる。BundleIDも、Provisioning Profileにひもづくものに書き換えてくれる。ということは、ipa納品にできる。
- ipa納品にすることで、ついに納品が自動化できる。
- ipa納品にすることで、static library作成の必要がなくなり、Swiftが使える！！
- sigh resignは、内部的にはPlistBuddyを使用してかなり泥臭いことをしている。

ということになります。
なお今回の検証はiTunesConnectへアップロードするところまでにとどまっており、実際に審査が通るかどうかまでは検証できていません。参考にする際は自己責任でお願いします。一応今、半年以上放置していたアプリのアップデートを兼ねて、署名付け替えた版で審査まで確認しようと思っています。

最後に、再署名からアップロードを自動化する処理をまとめておきます。

```bash
#!/bin/bash
#
# description:
#    ipaに再署名してiTunesConnectにアップロードする
#    冒頭のパラメータを適切な値に設定してください
#
# dependency:
#    sigh: `sudo gem install sigh` to install
#


###
### パラメータ
###

# BundleID
BUNDLE_ID=its.your.app

# Apple Developer ID
APPLE_DEVELOPER_ID=

# 証明書の名称です
# Keychain Accessの一覧に表示されるものを設定してください
CERT_NAME='iPhone Distribution: Hoge Huga (T47KPZ35V7)'

###
### 
###

IPA_NAME=App.ipa
PROVISIONING_FILE_NAME=App.mobileprovision

function checkStatus {
if [ $? -ne 0 ];then
    echo "Encountered an error, aborting!" >&2
    exit 1
fi
}


which sigh > /dev/null
if [ $? -ne 0 ];then
	echo "`sigh`がないようです。こちらのコマンドでインストールしてください。" >&2
	echo "sudo gem install sigh" >&2
	exit 1
fi

#sudo gem update sigh
#sudo gem update altool

sigh -a $BUNDLE_ID -u $APPLE_DEVELOPER_ID -q $PROVISIONING_FILE_NAME
checkStatus
sigh resign $IPA_NAME --signing_identity $CERT_NAME -n $PROVISIONING_FILE_NAME
checkStatus

which altool > /dev/null
if [ $? -ne 0 ];then
    PATH=$PATH:$(dirname "$(find $(dirname $(xcode-select -p)) -name altool | head -1)")
    ln -s "`xcode-select -p`/../Applications/Application Loader.app/Contents/itms" /usr/local/
    checkStatus
fi

altool --upload-app --file $IPA_NAME --username $APPLE_DEVELOPER_ID #--password $PASSWORD
checkStatus
```

(altoolの解決がモヤッとします。)

以上になります。
全体的に不明点やここおかしいなどあればコメント、編集リクエスト、もしくは[Twitter](https://twitter.com/toshi0383)でご連絡いただければと思います。お気軽にどうぞ。

# 参考

http://blog.kishikawakatsumi.com/entry/20141022/1413963656
こちらは岸川先生が一つのxcarchiveから複数の異なるコード署名(AdHoc用とAppStore用)をして配布と申請をしている記事です。xcodebuildだけでうまくいっているのは、Teamが同じだったからだと推測しています。

http://qiita.com/naotokui/items/9cd44475c37d0c3e722f
岸川先生と同じことをやろうとしてTeamが違ったためハマった事例かと思われます。結局AppleIDとかもらえたみたいなので、それだったらiTunesConnectに直接アップする、でも良さそうです。

http://stackoverflow.com/questions/18447064/xcodebuild-code-sign-error-provisioning-profile-even-though-all-profiles-are-in
こちらは xxx.mobileprovision からIDを取得する方法が書いてあったので助かりました。

http://blogs.zealot.co.jp/archives/1003
altoolが$PATHにないときの対処法。助かりました。

http://blog.ch3cooh.jp/entry/20150210/1423573065
altoolがiTMSTransporterを見つけられないでエラーになる時の対処法。助かりました。