クマは森で用を足しますか?

アウトプットは重要です。

論理チャネルを開くときにP2値を割愛するケース

Android Secure Element CTSページのOpen Mobile API関連の記述について言えば、これまでの対応でカバーできていないのは、おそらく下記の件のみではないでしょうか。

a. 0xA000000476416E64726F696443545331
...
vi. The applet should return success status word 0x9000 for the given APDU: 0x00F40000

これだけを見れば、APDUコマンド'00F40000'に対してステータスワード'9000'を返せば良いだけのようですが、そんなことはありません。この誤記は、いずれ更新されるのではないでしょうか(と信じたい)。

2018/12/13 追記 : 下記のように訂正されていました。この下の説明文が要らなくなりましたね ..

a. 0xA000000476416E64726F696443545331
...
vi.The applet should return the value of P2 received in the SELECT command + the success status word (i.e 0x009000) for the given APDU: 0x00F4000000

OmapiTest.testP2Value()が期待すること

テスト側の実装は下記の通りで、ステータスワード'9000'の前にカードから'00'を受け取りたいことがわかります。

    private final static byte[] CHECK_SELECT_P2_APDU = new byte[]{0x00, (byte) 0xF4, 0x00, 0x00};
...
    /** Test the P2 value of the select command sent by the underlying implementation */
    @Test
    public void testP2Value() {
        try {
            waitForConnection();
            Reader[] readers = seService.getReaders();

            for (Reader reader : readers) {
                byte[] response = internalTransmitApdu(reader, CHECK_SELECT_P2_APDU);
                assertGreaterOrEqual(response.length, 3);
                assertThat(response[response.length - 1] & 0xFF, is(0x00));
                assertThat(response[response.length - 2] & 0xFF, is(0x90));
                assertThat(response[response.length - 3] & 0xFF, is(0x00));
            }
        } catch (Exception e) {
            fail("unexpected exception " + e);
        }
    }

このAPDUコマンドを送信する際に論理チャネルをひとつオープンしますが、そのときのSELECTコマンドで指定されるP2値が'00'であることを確認するテストケースであるものと解釈できます。Open Mobile APIのSession.openLogicalChannel()ではP2値の指定を割愛することができますが、その際には自動的にP2値として'00'が補われることを確認するものです。

    /**
     * This method is provided to ease the development of mobile application and for compliancy
     * with existing applications.
     * This method is equivalent to openLogicalChannel(aid, P2=0x00)
...
     */
    public @Nullable Channel openLogicalChannel(@Nullable byte[] aid) throws IOException {
        return openLogicalChannel(aid, (byte) 0x00);
    }

OmapiTest.testP2Value()をPythonで書き直す

単純なテストだなと思ってこれだけ書いたら、うまく動いてくれませんでした。

    def testP2Value(self):
        print('started: ' + sys._getframe().f_code.co_name)
        selectable_aid = 'A000000476416E64726F696443545331'
        apdu = '00F40000'

        (response, sw) = self.commandif.send_apdu(selectable_aid, apdu)
        if response != '00':
            raise RuntimeError('Unexpected outgoing data : ' + response)
        if sw != '9000':
            raise RuntimeError('Unexpected status word : ' + sw)

        print('finished: ' + sys._getframe().f_code.co_name)

確かによく見ると、このテストで送信するAPDUコマンドがちょっと変です。カードからのデータ受信を期待するCase 2のAPDUコマンドなのに、最後のLeを省略してCase 1のようなAPDUコマンド'00F40000'を送信することになっています。ターミナル側で最後の'00'が補われるので、カード側から見れば何も違いはないということなのでしょうが。

Terminal Card
CLA INS P1 P2 P3=00
SW (XX YY)

従って、下記のようにTPDUレベルでは末尾の'00'が補われたコマンド'XXF4000000'が送信され、ステータスワード'6C01'がカードから返ってくるのが期待されているコマンドシーケンスになります。

Terminal Card
XX F4 00 00 00
SW (6C 01)

ステータスワード'6CXX'が返ってくるときには、送信したAPDUコマンドの末尾にはLeが付いていることを想定していましたので、単純にそれを適切なデータサイズへと差し替える処理にしていました。

        if sw1 == 0x6C:
            (response, sw) = self.transport.send_apdu(apdu[:-2] + sw[2:4])

本テストで送信するAPDUコマンドにはLeがありませんので、その場合には適切なデータサイズを末尾にLeとして付加するように変更する必要があったというわけです。(正直どうでもいいこと)

        if sw1 == 0x6C:
            if len(apdu) > 8:
                apdu = apdu[:-2] + sw[2:4]
            else:
                apdu = apdu[:8] + sw[2:4]
            (response, sw) = self.transport.send_apdu(apdu)

今回のコミットがこちら。

github.com

2018/12/13 追記 : 最新のテストコードでは、'00F40000' ではなく '00F4000000' を送信するように修正されています。上述の話も、今となっては要らなくなっています。

アプレット側もサクサク実装

ほんの数秒で書いたものが動いてくれているので、本日のところはこれで良いことにしましょう。

        case INS_CHECK_P2:
            if (p1 != 0x00 || p2 != 0x00) {
                ISOException.throwIt(ISO7816.SW_WRONG_P1P2);
            }
            if (buffer[ISO7816.OFFSET_LC] == 0x01) {
                buffer[0] = mSelectP2;
                apdu.setOutgoingAndSend((short) 0, (short) 1);
            } else {
                // Return SW 6C01 if Le is bigger than the the actual outgoing data.
                ISOException.throwIt((short) (ISO7816.SW_CORRECT_LENGTH_00 + 0x01));
            }
            break;

期待通りに1バイトだけ要求されていれば、SELECTコマンドを受け取ったときに退避してあったP2値を返します。要求バイト数が1以外であれば、ステータスワード'6C01'を返します。アップロードしたコミットに仕込んだtypoは、また後日にでも修正することにします。

github.com


テストを走らせてみると、このようなコマンドシーケンスになります。

started: testP2Value
C-APDU : 0070000001
R-APDU + SW : 019000
C-APDU : 01A4040010A000000476416E64726F69644354533100
R-APDU + SW : 9F0C
C-APDU : 01C000000C
R-APDU + SW : 6F0a640353010162038501019000
C-APDU : 01F40000
R-APDU + SW : 6C01
C-APDU : 01F4000001
R-APDU + SW : 009000
C-APDU : 00708001
R-APDU + SW : 9000
finished: testP2Value