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

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

各種警告系ステータスワードの返却

ターミナルとカードの間で行われるAPDUコマンドのやり取りには、当然ながらカードOSが介在します。ふと見つけてドイツから個人輸入したsysmoUSIM-SJS1 4FFですが、カードOSの制限事項のようなもの、例えばカードOSが通過を許してくれないクラスバイトやインストラクションコードには、これまでのところ出会っていません。OMAPI系テストに関しては、このまますんなり完成させてもらえるような気がしてきました(楽観的)。では、引き続きAndroid Secure Element CTSが期待するJava Cardアプレットを書いてゆきます。

shop.sysmocom.de

Omapi.testStatusWordTransmit()が期待すること

テスト側の実装は、下記のようになっています。様々なバリエーションの警告系ステータスワードが期待通りにカードから返ってくることがテストのポイントで、Case 1からCase 4まで全てのパターンのAPDUコマンドでそれを確認したいようです。下記コードはざっくり三つの内部ループで構成されていますが、最初のループでCase 1とCase 3を、次のループでCase 2を、そして最後のループでCase 4をテストするように書かれています。

    public void testStatusWordTransmit() {
        try {
            waitForConnection();
            Reader[] readers = seService.getReaders();

            for (Reader reader : readers) {
                for (byte[] apdu : SW_62xx_NO_DATA_APDU) {
                    for (byte i = 0x00; i < SW_62xx.length; i++) {
                        apdu[2] = (byte)(i+1);
                        byte[] response = internalTransmitApdu(reader, apdu);
                        byte[] SW = SW_62xx[i];
                        assertThat(response[response.length - 1], is(SW[1]));
                        assertThat(response[response.length - 2], is(SW[0]));
                    }
                }

                for (byte i = 0x00; i < SW_62xx.length; i++) {
                    byte[] apdu = SW_62xx_DATA_APDU;
                    apdu[2] = (byte)(i+1);
                    byte[] response = internalTransmitApdu(reader, apdu);
                    byte[] SW = SW_62xx[i];
                    assertGreaterOrEqual(response.length, 3);
                    assertThat(response[response.length - 1], is(SW[1]));
                    assertThat(response[response.length - 2], is(SW[0]));
                }

                for (byte i = 0x00; i < SW_62xx.length; i++) {
                    byte[] apdu = SW_62xx_VALIDATE_DATA_APDU;
                    apdu[2] = (byte)(i+1);
                    byte[] response = internalTransmitApdu(reader, apdu);
                    assertGreaterOrEqual(response.length, apdu.length + 2);
                    byte[] responseSubstring = Arrays.copyOfRange(response, 0, apdu.length);
                    // We should not care about which channel number is actually assigned.
                    responseSubstring[0] = apdu[0];
                    assertTrue(Arrays.equals(responseSubstring, apdu));
                    byte[] SW = SW_62xx[i];
                    assertThat(response[response.length - 1], is(SW[1]));
                    assertThat(response[response.length - 2], is(SW[0]));
                }
            }
        } catch (Exception e) {
            fail("unexpected exception " + e);
        }
    }

Android Secure Element CTSのウェブページでは、下記のように説明(テストに用いるAPDUコマンドや警告系ステータスワードの表はここでは割愛)されているところです。Case 4でカードから受け取るデータの内容については言及がありますが、Case 2については全く説明されていません。Case 4とそっくり同じデータをCase 2のAPDUコマンドに対して返しても問題ないはずですが、Case 2のデータの内容はカード側の実装に依存することになるので、(他の方が作成されたカードもテストできるようにするなら)テスト側では真ん中のループと最後のループを分けざるを得ません。

a. 0xA000000476416E64726F696443545331
iv. The applet should return the following status word responses for the respective Transmit APDU:
...
*The response should contain data that is the same as input APDU, except the first byte is 0x01 instead of 0x00.

本テストで利用されるAPDUコマンドのインストラクションコードは、'F3'。カード側では、P2値を見て期待されている振る舞いを選択する必要があります。

CLA INS P1 P2 Lc Data(Lc) Le Data(Le)
XX F3 YY 06 なし
XX F3 YY 0A 01 AA なし
XX F3 YY 08 00 あり
XX F3 YY 0C 01 AA 00 あり

APDUコマンドのP1の値に応じて、それぞれ特定の警告系ステータスワードがカードから応答されることが期待されています。

P1 specified in APDU Expected SW Description
01 6200 Warning. State of non-volatile memory is unchanged.
02 6281 SW2: Part of returned data may be corrupted.
03 6282 SW2: End of file or record reached before reading Ne bytes.
04 6283 SW2: Selected file deactivated.
05 6285 SW2: Selected file in termination state.
06 62F1 SW2: More data available.
07 62F2 SW2: More data available and proactive command pending.
08 63F1 SW2: More data expected.
09 63F2 SW2: More data expected and proactive command pending.
0A 63C2 SW2: Verification failed, 2 retries remaining.
0B 6202 SW2: Triggering by the card (02).
0C 6280 SW2: Triggering by the card (80).
0D 6284 SW2: File control information not formatted.
0E 6286 SW2: No input data available from a sensor on the card.
0F 6300 Warning. State of non-volatile memory has changed.
10 6381 SW2: File filled up by the last write.

これを書いている時点では、上表の下から三つ目のステータスワードは'6286'ではなく、何故か'6282'が再登場していました。Android Secure Element CTSのテスト側の実装を見ると'6286'を期待していましたので、Android Secure Element CTSページの誤記だとわかります。

    private final static byte[][] SW_62xx =
            new byte[][]{{0x62, 0x00}, {0x62, (byte) 0x81}, {0x62, (byte) 0x82},
                    {0x62, (byte) 0x83},
                    {0x62, (byte) 0x85}, {0x62, (byte) 0xF1}, {0x62, (byte) 0xF2},
                    {0x63, (byte) 0xF1},
                    {0x63, (byte) 0xF2}, {0x63, (byte) 0xC2}, {0x62, 0x02}, {0x62, (byte) 0x80},
                    {0x62, (byte) 0x84}, {0x62, (byte) 0x86}, {0x63, 0x00}, {0x63, (byte) 0x81}
            };

2018/12/13 追記 : 上記の誤記は既に訂正され、'6286'が期待されていることをAndroid Secure Element CTSページ上でも確認できます。

このテストもPythonで書きました

こんなに処理を束ねてしまわないで、APDU毎に個別にコードブロックを設けたほうが読みやすかったかもしれません。微妙なラインですが、分解せずに束ねたままにしておきます。

    def testStatusWordTransmit(self):
        selectable_aid = 'A000000476416E64726F696443545331'

        apdu_list = [
            '00F30006',
            '00F3000A01AA',
            '00F3000800',
            '00F3000C01AA00',
        ]

        warning_sw_list = [
                '6200', '6281', '6282', '6283', '6285', '62F1', '62F2', '63F1',
                '63F2', '63C2', '6202', '6280', '6284', '6286', '6300', '6381'
        ]

        for apdu in apdu_list:
            for p1 in range(0x10):
                apdu = apdu[:4] + format(p1 + 1, '02X') + apdu[6:]
                (response, sw) = self.commandif.send_apdu(selectable_aid, apdu)

                if sw.upper() != warning_sw_list[p1]:
                    raise RuntimeError('Unexpected warning SW : ' + sw)

                p2 = apdu[6:8]
                if p2 in {'06', '0A'}:
                    if len(response) > 0:
                        raise RuntimeError('Unexpected outgoing data : ' + response)
                elif p2 == '08':
                    if len(response) == 0:
                        raise RuntimeError('Outgoing data is expected')
                elif p2 == '0C':
                    if apdu[2:] != response[2:len(apdu)].upper():
                        raise RuntimeError('Outgoing data is different from APDU')
                else:
                    raise RuntimeError('Program error - P2 : ' + p2)

これをマージしたコミットはこちら。

github.com

最初に書いたカード側のコード

まずは、このコミットを作成しました。256バイトを超えるような大きなデータのやり取りはありませんし、簡単に実装してしまおうと考えた次第です。

github.com

Case 2コマンドの処理

本テストでターミナル側から受信するAPDUコマンドのLeには、'00'が指定されています。256バイトのデータを返すことにすれば、ステータスワード'6CXX'を使ってデータサイズのネゴシエーションを行なう必要もなく、単純なシーケンスで処理を終えることができます。

        case P2_WARNING_SW_CASE2:
            sendShortData(apdu, buffer[ISO7816.OFFSET_LC]);
            ISOException.throwIt(WARNING_SWS[p1 - 1]);
            break;
...
    private void sendShortData(APDU apdu, short expected) {
        if (expected == 0x00) {
            expected = DATA_BUFFER_SIZE;
        }
        apdu.setOutgoing();
        apdu.setOutgoingLength(expected);
        apdu.sendBytesLong(sOutgoingData, (short) (DATA_BUFFER_SIZE - expected), expected);
    }

APDUコマンドのやり取りは下記のようになり、こちらは想定通りです。

C-APDU : 01F3010800
R-APDU : 00000000000000000000000000000000
         00000000000000000000000000000000
...
         00000000000000000000000000000000
         000000000000000000000000000000FF
SW     : 6200

Case 4コマンドの処理

こちらのコマンドでは、受け取ったコマンドとほぼ同じものを(クラスバイトだけ'01'に差し替えて)ターミナルに返すことが求められています。APDUクラスのバッファにはコマンドが入っていますので、単純にそれを返せば良いものとして設計されたテストであることが予想されます。APDU.setOutgoingAndSend()を使う簡単な方法を選択し、下記のように書いてみました。

        case P2_WARNING_SW_CASE4:
            if (apdu.setIncomingAndReceive() != 0x01) {
                ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
            }
            buffer[ISO7816.OFFSET_CLA] = 0x01;
            apdu.setOutgoingAndSend((short) 0, (short) 7);
            ISOException.throwIt(WARNING_SWS[p1 - 1]);
            break;

これを動作させてみたときのコマンドのやり取りが、こちら。シーケンス的には合っていて、テストも通るのですが、ステータスワード'9F07'が気に入らない。APDU.setOutgoingAndSend()を呼んだ後にカードOSが自動的に返してくれているので、この実装のままでは制御しようがありません。

C-APDU : 01F3010C01AA00
R-APDU + SW : 9F07
C-APDU : 01C0000007
R-APDU + SW : 01F3010C01AA006200

他のカードベンダ様のカードならこんな実装は不要なのではないかと思われますが、アプレット側の制御でステータスワード'61XX'を返す処理へと書き換えました。

github.com