Android と SIM カードとの間で APDU コマンドの送受信を実行しようと思うと、通常は下記のどちらかの公開 API を利用することになると思います。しかし、一般のアプリ開発者にとってはそのどちらも使いにくい、と言うか著しく自由度の低いものになっています。
1. android.se.omapi で APDU
APDU を送受信するためには、まず Session.openLogicalChannel() を呼んで論理チャネルを得る必要があります。しかし、多くのケースではplatform/packages/apps/SecureElement の Terminal によって SecurityException がスローされることになるでしょう。SIM が持つアクセスルールの他に、OMAPI には GP SEAC のアクセスルールによる制限も存在するためです。リクエストを受けた SIM カード側でその許可・不許可が判断される前の段階で、ターミナル側のチェックによってエラーを折り返されてしまいます。
public Channel openLogicalChannel(SecureElementSession session, byte[] aid, byte p2, ISecureElementListener listener, String packageName, int pid) throws IOException, NoSuchElementException { ... try { channelAccess = setUpChannelAccess(aid, packageName, pid); // ここで SecurityException
どのようにすれば、この制限を突破して APDU 送受信用論理チャネルを得ることができるでしょうか。
- ルート権限を得て platform/packages/apps/SecureElement の挙動を変える
- SIM を改変して GP SEAC のアクセスルールに自分のアプリを許可させる
どちらも、一般のアプリ開発者にはハードルが高い選択肢だと思います。しかし、GP SEAC のアクセスルールによる OMAPI の利用制限は仕様で規定されていることなので、やむを得ません。アクセスルールを自由に設計できる開発者用 SIM カードを持っているマニアの方々なら、後者の選択肢を採って Java Card で ARA を書けば大丈夫です。
cheerio-the-bear.hatenablog.com
cheerio-the-bear.hatenablog.com
2. android.telephony (TelephonyManager) で APDU
TelephonyManager に公開 API として用意されている下記メソッドを用いることで、論理チャネルのオープン・クローズや、APDU コマンドの送受信を行えるようになっています。
iccOpenLogicalChannel(AID: String!, p2: Int): IccOpenLogicalChannelResponse! iccTransmitApduLogicalChannel(channel: Int, cla: Int, instruction: Int, p1: Int, p2: Int, p3: Int, data: String!): String! iccCloseLogicalChannel(channel: Int): Boolean
そう、不満があるのはこちらです。ISO や ETSI や 3GPP でとびきり厳重な制限をかけるようにと指示されているわけでもないのに、TelephonyManager API を使って論理チャネルを獲得するには、特別な権限が必要です。platform/packages/services/Telephony で実装されている PhoneInterfaceManager のココで引っかかります。
@Override public IccOpenLogicalChannelResponse iccOpenLogicalChannel( int subId, String callingPackage, String aid, int p2) { TelephonyPermissions.enforceCallingOrSelfModifyPermissionOrCarrierPrivilege( // ココで引っかかる mApp, subId, "iccOpenLogicalChannel"); ...
では、enforceCallingOrSelfModifyPermissionOrCarrierPrivilege() の実装を見てみましょう。MODIFY_PHONE_STATE のパーミッションを持っているアプリケーションか、もしくは UICC Carrier Privileges でアクセスを許可されているアプリケーションであれば、ここで SecurityException をスローされることはありません。
public static void enforceCallingOrSelfModifyPermissionOrCarrierPrivilege( Context context, int subId, String message) { if (context.checkCallingOrSelfPermission(android.Manifest.permission.MODIFY_PHONE_STATE) == PERMISSION_GRANTED) { return; } if (DBG) Rlog.d(LOG_TAG, "No modify permission, check carrier privilege next."); enforceCallingOrSelfCarrierPrivilege(subId, message); }
MODIFY_PHONE_STATE のパーミッションを得るには、アプリケーションがプラットフォーム署名されている必要があります。
<!-- Allows modification of the telephony state - power on, mmi, etc. Does not include placing calls. <p>Not for use by third-party applications. --> <permission android:name="android.permission.MODIFY_PHONE_STATE" android:protectionLevel="signature|privileged" />
ということで、こちらの方法で APDU 送受信用の論理チャネルを得るには、次のような選択肢が用意されていることになります。
- MODIFY_PHONE_STATE のためにプラットフォーム署名を得る
- Android の platform/packages/services/Telephony を改変する
- SIM を改変して UICC Carrier Privileges のアクセスルールに自分のアプリを許可させる
そもそも SIM には ARR ファイルや ADM キー等のセキュリティーの機構が用意されていますし、PC/SC リーダー・ライターで Windows や Linux と繋いでしまえば、そのレベルのセキュリティしかかからないわけです。Android でかけられている上記の制限は、不必要に高く設定されたものであると言えると思います。
3. Android Developer ページの曖昧な説明
余談みたいなものです。上述のような機能制限があるため、仕方ないからまた久しぶりに Java Card を引っ張り出してきて適当な ARA-M アプレットでも書くことにしようかと思ったわけです。開発用 SIM カードも余っていますし、UICC Carrier Privileges の説明によると、DeviceAppID を 0 (empty) にしておけばどんなアプリケーションに対してもオールマイティーに機能しそうではないですか。いや違う、そんなテスト目的の値は推奨されないんだから 0 (empty) は駄目だということですか ..
If DeviceAppID is 0 (empty), would you really apply the rule to all device applications not covered by a specific rule?
A: Carrier apis require deviceappid-ref-do be non-empty. Being empty is intended for test purpose and is not recommended for operational deployments.
ということでコードを読んでみたのですが、0 (empty) が許容されそうな気が全くしません。長さからして異なるのに Arrays.equals() が「一緒だよ」と言うはずがありません。全くもって面倒くさいです。
public int getCarrierPrivilegeStatus(Signature signature, String packageName) { // SHA-1 is for backward compatible support only, strongly discouraged for new use. byte[] certHash = getCertHash(signature, "SHA-1"); byte[] certHash256 = getCertHash(signature, "SHA-256"); if (matches(certHash, packageName) || matches(certHash256, packageName)) { return TelephonyManager.CARRIER_PRIVILEGE_STATUS_HAS_ACCESS; } return TelephonyManager.CARRIER_PRIVILEGE_STATUS_NO_ACCESS; } private boolean matches(byte[] certHash, String packageName) { return certHash != null && Arrays.equals(this.mCertificateHash, certHash) && (TextUtils.isEmpty(this.mPackageName) || this.mPackageName.equals(packageName)); }