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

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

Case 2コマンドやCase 4コマンドへの長いデータの応答

Android Secure Element CTSに使えるSIMカードsysmoUSIM-SJS1 4FFで作ってみる件、次はOmapiTest.testSegmentedResponseTransmit()です。

testSegmentedResponseTransmit()が期待する動作

テストコードによると、セキュアエレメントから返ってくるデータのサイズがAPDUコマンドのP1/P2部分と一致すること、また返ってきたデータの末尾が0xFFであることが期待されています。

    /** Test if the responses are segmented by the underlying implementation */
    @Test
    public void testSegmentedResponseTransmit() {
        try {
            waitForConnection();
            Reader[] readers = seService.getReaders();

            for (Reader reader : readers) {
                for (byte[] apdu : SEGMENTED_RESP_APDU) {
                    byte[] response = internalTransmitApdu(reader, apdu);
                    byte[] b = { 0x00, 0x00, apdu[2], apdu[3] };
                    ByteBuffer wrapped = ByteBuffer.wrap(b);
                    int expectedLength = wrapped.getInt();
                    assertThat(response.length, is(expectedLength + 2));
                    assertThat(response[response.length - 1] & 0xFF, is(0x00));
                    assertThat(response[response.length - 2] & 0xFF, is(0x90));
                    assertThat(response[response.length - 3] & 0xFF, is(0xFF));
                }
            }
        } catch (Exception e) {
            fail("unexpected exception " + e);
        }
    }

Android Secure Element CTSサイトでは、下記のように説明されているところです。データのやり取りが送受信ともにあるCase 4のコマンドがふたつ、その他はCase 2コマンドということになります。

v. The applet should return segmented responses with 0xFF as the last data byte and have the respective status words and response lengths for the following APDUs.
- 0x00C2080000
- 0x00C4080002123400
- 0x00C6080000
- 0x00C8080002123400
- 0x00C27FFF00
- 0x00CF080000
- 0x94C2080000

ただ、テストコードのコメントを確認すると、もうひとつ「セグメントのサイズ」についても期待があることがわかります。インストラクションコード0xC2および0xC4、0xCFのコマンドでは、ひとつのAPDUコマンドに対してカードから応答されるデータの最大サイズが255バイト(+ステータスワード)であること、またその他のインストラクションコードにおいては256バイトであることが期待されています。テストでは実際にはチェックされないところですが、期待に沿えるようにします。

    private final static byte[][] SEGMENTED_RESP_APDU = new byte[][]{
            //Get response Case2 61FF+61XX with answer length (P1P2) of 0x0800, 2048 bytes
            {0x00, (byte) 0xC2, 0x08, 0x00, 0x00},
            //Get response Case4 61FF+61XX with answer length (P1P2) of 0x0800, 2048 bytes
            {0x00, (byte) 0xC4, 0x08, 0x00, 0x02, 0x12, 0x34, 0x00},
            //Get response Case2 6100+61XX with answer length (P1P2) of 0x0800, 2048 bytes
            {0x00, (byte) 0xC6, 0x08, 0x00, 0x00},
            //Get response Case4 6100+61XX with answer length (P1P2) of 0x0800, 2048 bytes
            {0x00, (byte) 0xC8, 0x08, 0x00, 0x02, 0x12, 0x34, 0x00},
            //Test device buffer capacity 7FFF data
            {0x00, (byte) 0xC2, (byte) 0x7F, (byte) 0xFF, 0x00},
            //Get response 6CFF+61XX with answer length (P1P2) of 0x0800, 2048 bytes
            {0x00, (byte) 0xCF, 0x08, 0x00, 0x00},
            //Get response with another CLA  with answer length (P1P2) of 0x0800, 2048 bytes
            {(byte) 0x94, (byte) 0xC2, 0x08, 0x00, 0x00}
    };

Case 4コマンド(T=0)

順番は前後しますが、データの送受信がともに発生するCase 4コマンドから順におさらいします。今回のテストケースに登場するふたつ目のコマンド、0x00C4080002123400がアプリケーションレイヤのCase 4コマンド(C-APDU)で、下記のように構成されています。

CLA INS P1 P2 Lc Data(Lc) Le
XX C4 08 00 02 1234 00

トランスポートレイヤでは、見え方がちょっと変わります。トランスポートレイヤでカードに最初に届けられるのは、最初の5バイトです。それを受け取ったカードがインストラクションコードをターミナル側に返すと、ターミナル側からカード側に送りたかったデータ(ここでは0x1234)を受け取ることができます。その逆、カード側からターミナル側に送信したいデータは、このコマンドのやり取りの中には登場しません。最後にステータスワードとして61 YY(このSW2は次のコマンドでカード側が送信可能なデータのサイズ)を返すことで、次にターミナルがGET RESPONSE (0xC0)コマンドを使ってそのデータを取りにくることを期待します。

Terminal Card
XX C4 08 00 02
INS (C4)
Data (1234)
SW (61 YY)

Java Cardアプレットを実装する上では、細かいところを気にしなくてもそれなりに動いてくれるようになっています。ADPU.setIncomingAndReceive()をコールするだけで、インストラクションコードの送信とデータの受信をフレームワークが実行してくれるようです。前回までのCase 4コマンド処理は下記のようになっていましたが、ステータスワード61 YYを送信する処理を書いていませんでした。送信できるデータをフレームワークに預けておくだけで、自動的にステータスワード61 YYを送信し、次に受信しているはずのGET RESPONSE (0xC0)コマンドも知らないところでハンドリングしてくれるようです。

        case INS_BASIC_CASE_4:
    ...
            if (apdu.setIncomingAndReceive() != 0x01) {
                ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
            }
            apdu.setOutgoing();
            apdu.setOutgoingLength((short) sResponseBuffer.length);
            apdu.sendBytesLong(sResponseBuffer, (short) 0, (short) sResponseBuffer.length);
            break;

フレームワークに自動的に処理させず、作成中のアプレットに可能な限りコマンドを処理させたいので、GET RESPONSE (0xC0)コマンドがアプレットに届くよう、ステータスワード61 YYを自前で返すように書き換えます。

Case 2コマンド(T=0)

今回のテストケースの最初のコマンド0x00C2080000は、Case 2のAPDUコマンド(C-APDU)です。Case 2の場合にはデータのやり取りは一方向、カード側からターミナル側への送信のみになります。

CLA INS P1 P2 Le
0xXX 0xC2 0x08 0x00 0x00

ターミナル側が期待している受信データのサイズ(Le)よりも、実際にカードが提供できるデータのサイズのほうが少ない場合には、ステータスワード6C YYを応答し、Leだけ書き直した同じコマンドがターミナル側から再送されるのを待ちます。

Terminal Card
XX C2 08 00 00
SW (6C YY)
XX C2 08 00 YY
INS (C2) Data (..) SW (90 00)

両者のデータサイズの認識が一致している場合、もしくは実際にカードが提供できるデータサイズのほうが多い場合には、このコマンドのやり取りの中でデータの送受信まで完結します。

Terminal Card
XX C2 08 00 00
INS (C2) Data (..) SW (90 00)

前回までのCase 2コマンド処理が、こちら。上記の判断をフレームワークにお任せするコードにしていましたが、こちらもアプレットの判断で制御するように書き換えます。

        case INS_BASIC_CASE_2:
    ...
            apdu.setOutgoing();
            apdu.setOutgoingLength((short) sResponseBuffer.length);
            apdu.sendBytesLong(sResponseBuffer, (short) 0, (short) sResponseBuffer.length);
            break;

テストコードの追加

OmapiTest.testSegmentedResponseTransmit()をPythonのコードで再現したものが、こちらです。

github.com


ターミナル側で受信したデータの長さが期待通りであること(APDUコマンドのP1/P2と一致すること)を確認し、その最終バイトが0xFFで、ステータスワードが正常終了を示していることを確認します。Pythonのスライスにも慣れてきたような気がします。

    def testSegmentedResponseTransmit(self):
    ...
        for apdu in segmented_response_apdu_list:
            (response, sw) = self.commandif.send_apdu(selectable_aid, apdu)
            # P1 + P2 indicates the expected length of the output data.
            if len(response) != (int(apdu[4:8], 16) * 2):
                raise RuntimeError('Unexpected length of data is received : ' + str(len(response)))
            # The last data byte shall be 0xFF though the other bytes are not cared at all.
            if int(response[-2:], 16) != 0xFF:
                raise RuntimeError('Unexpected byte is received : ' + response[-2:])
            if sw != '9000':
                raise RuntimeError('SW is not 9000 : ' + sw)

Java Cardアプレット実装の追加

Java Cardアプレット側の実装追加については、こちらを。

github.com

ステータスワード61 YYもしくは6C YYを発生させたコマンドの情報を覚えておく必要があり、下記のインスタンス変数を追加しました。値の変更が発生する度にEEPROMの更新が発生するので、可能な限り避けたほうが良いとのことですが、やむを得ないところです。

    private short mSegmentSize = 0;
    private short mRemainingSize = 0;
    private byte mCurrentClass = 0x00;