From 0021c1f15f9f40361d65f9d6c6b5cc33dbe99457 Mon Sep 17 00:00:00 2001 From: Vincent Breitmoser Date: Sun, 29 Oct 2017 02:40:24 +0200 Subject: [PATCH] add tests for CcidTransceiver --- .../securitytoken/usb/CcidDescription.java | 6 + .../securitytoken/usb/CcidTransceiver.java | 27 +- .../securitytoken/usb/UsbTransport.java | 10 +- .../usb/UsbTransportException.java | 16 + .../usb/CcidTransceiverTest.java | 313 ++++++++++++++++++ 5 files changed, 358 insertions(+), 14 deletions(-) create mode 100644 OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiverTest.java diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidDescription.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidDescription.java index 5a30c830f..bdf3cdc39 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidDescription.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidDescription.java @@ -23,6 +23,7 @@ import java.nio.ByteOrder; import java.util.ArrayList; import android.support.annotation.NonNull; +import android.support.annotation.VisibleForTesting; import com.google.auto.value.AutoValue; import org.sufficientlysecure.keychain.securitytoken.usb.tpdu.T1ShortApduProtocol; @@ -54,6 +55,11 @@ abstract class CcidDescription { public abstract int getProtocols(); public abstract int getFeatures(); + @VisibleForTesting + static CcidDescription fromValues(byte maxSlotIndex, byte voltageSupport, int protocols, int features) { + return new AutoValue_CcidDescription(maxSlotIndex, voltageSupport, protocols, features); + } + @NonNull static CcidDescription fromRawDescriptors(byte[] desc) throws UsbTransportException { int dwProtocols = 0, dwFeatures = 0; diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiver.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiver.java index 18912da4a..72b2a1fb2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiver.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiver.java @@ -33,6 +33,7 @@ import com.google.auto.value.AutoValue; import org.bouncycastle.util.Arrays; import org.bouncycastle.util.encoders.Hex; import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException.UsbCcidErrorException; public class CcidTransceiver { @@ -40,6 +41,7 @@ public class CcidTransceiver { private static final int MESSAGE_TYPE_RDR_TO_PC_DATA_BLOCK = 0x80; private static final int MESSAGE_TYPE_PC_TO_RDR_ICC_POWER_ON = 0x62; + private static final int MESSAGE_TYPE_PC_TO_RDR_ICC_POWER_OFF = 0x63; private static final int MESSAGE_TYPE_PC_TO_RDR_XFR_BLOCK = 0x6f; private static final int COMMAND_STATUS_SUCCESS = 0; @@ -86,15 +88,20 @@ public class CcidTransceiver { CcidDataBlock response = null; for (CcidDescription.Voltage v : usbCcidDescription.getVoltages()) { Log.v(Constants.TAG, "CCID: attempting to power on with voltage " + v.toString()); - response = iccPowerOnVoltage(v.powerOnValue); + try { + response = iccPowerOnVoltage(v.powerOnValue); + } catch (UsbCcidErrorException e) { + if (e.getErrorResponse().getError() == 7) { // Power select error + Log.v(Constants.TAG, "CCID: failed to power on with voltage " + v.toString()); + iccPowerOff(); + Log.v(Constants.TAG, "CCID: powered off"); + continue; + } - if (response.getStatus() == 1 && response.getError() == 7) { // Power select error - Log.v(Constants.TAG, "CCID: failed to power on with voltage " + v.toString()); - iccPowerOff(); - Log.v(Constants.TAG, "CCID: powered off"); - } else { - break; + throw e; } + + break; } if (response == null) { throw new UsbTransportException("Couldn't power up ICC2"); @@ -127,7 +134,7 @@ public class CcidTransceiver { private void iccPowerOff() throws UsbTransportException { byte sequenceNumber = currentSequenceNumber++; final byte[] iccPowerCommand = { - 0x63, + MESSAGE_TYPE_PC_TO_RDR_ICC_POWER_OFF, 0x00, 0x00, 0x00, 0x00, 0x00, sequenceNumber, @@ -193,7 +200,7 @@ public class CcidTransceiver { } while (response.isStatusTimeoutExtensionRequest()); if (!response.isStatusSuccess()) { - throw new UsbTransportException("USB-CCID error: " + response); + throw new UsbCcidErrorException("USB-CCID error!", response); } return response; @@ -213,7 +220,6 @@ public class CcidTransceiver { throw new UsbTransportException("USB-CCID error - bad CCID header type " + inputBuffer[0]); } - CcidDataBlock result = CcidDataBlock.parseHeaderFromBytes(inputBuffer); if (expectedSequenceNumber != result.getSeq()) { @@ -235,6 +241,7 @@ public class CcidTransceiver { } result = result.withData(dataBuffer); + return result; } diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransport.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransport.java index 0ff4e9f02..7c52994e2 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransport.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransport.java @@ -17,6 +17,9 @@ package org.sufficientlysecure.keychain.securitytoken.usb; + +import java.io.IOException; + import android.hardware.usb.UsbConstants; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbDeviceConnection; @@ -28,15 +31,13 @@ import android.support.annotation.Nullable; import android.util.Pair; import org.sufficientlysecure.keychain.Constants; +import org.sufficientlysecure.keychain.securitytoken.CommandApdu; +import org.sufficientlysecure.keychain.securitytoken.ResponseApdu; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TokenType; import org.sufficientlysecure.keychain.securitytoken.SecurityTokenInfo.TransportType; import org.sufficientlysecure.keychain.securitytoken.Transport; -import org.sufficientlysecure.keychain.securitytoken.CommandApdu; -import org.sufficientlysecure.keychain.securitytoken.ResponseApdu; import org.sufficientlysecure.keychain.util.Log; -import java.io.IOException; - /** * Based on USB CCID Specification rev. 1.1 * http://www.usb.org/developers/docs/devclass_docs/DWG_Smart-Card_CCID_Rev110.pdf @@ -136,6 +137,7 @@ public class UsbTransport implements Transport { } CcidDescription ccidDescription = CcidDescription.fromRawDescriptors(usbConnection.getRawDescriptors()); + Log.d(Constants.TAG, "CCID Description: " + ccidDescription); CcidTransceiver transceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, ccidDescription); ccidTransportProtocol = ccidDescription.getSuitableTransportProtocol(); diff --git a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransportException.java b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransportException.java index 8c9d618b9..43c22afb6 100644 --- a/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransportException.java +++ b/OpenKeychain/src/main/java/org/sufficientlysecure/keychain/securitytoken/usb/UsbTransportException.java @@ -19,6 +19,9 @@ package org.sufficientlysecure.keychain.securitytoken.usb; import java.io.IOException; +import org.sufficientlysecure.keychain.securitytoken.usb.CcidTransceiver.CcidDataBlock; + + public class UsbTransportException extends IOException { public UsbTransportException(String detailMessage) { super(detailMessage); @@ -31,4 +34,17 @@ public class UsbTransportException extends IOException { public UsbTransportException(Throwable cause) { super(cause); } + + static class UsbCcidErrorException extends UsbTransportException { + private CcidDataBlock errorResponse; + + UsbCcidErrorException(String detailMessage, CcidDataBlock errorResponse) { + super(detailMessage + " " + errorResponse); + this.errorResponse = errorResponse; + } + + CcidDataBlock getErrorResponse() { + return errorResponse; + } + } } diff --git a/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiverTest.java b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiverTest.java new file mode 100644 index 000000000..626c680ae --- /dev/null +++ b/OpenKeychain/src/test/java/org/sufficientlysecure/keychain/securitytoken/usb/CcidTransceiverTest.java @@ -0,0 +1,313 @@ +package org.sufficientlysecure.keychain.securitytoken.usb; + + +import java.util.LinkedList; + +import android.annotation.TargetApi; +import android.hardware.usb.UsbDeviceConnection; +import android.hardware.usb.UsbEndpoint; +import android.os.Build.VERSION_CODES; + +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.encoders.Hex; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; +import org.sufficientlysecure.keychain.KeychainTestRunner; +import org.sufficientlysecure.keychain.securitytoken.usb.CcidTransceiver.CcidDataBlock; +import org.sufficientlysecure.keychain.securitytoken.usb.UsbTransportException.UsbCcidErrorException; + +import static org.junit.Assert.assertArrayEquals; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.AdditionalMatchers.aryEq; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.eq; +import static org.mockito.Matchers.same; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + + +@SuppressWarnings("WeakerAccess") +@RunWith(KeychainTestRunner.class) +@TargetApi(VERSION_CODES.JELLY_BEAN_MR2) +public class CcidTransceiverTest { + static final String ATR = "3bda11ff81b1fe551f0300318473800180009000e4"; + static final int MAX_PACKET_LENGTH_IN = 61; + static final int MAX_PACKET_LENGTH_OUT = 63; + + UsbDeviceConnection usbConnection; + UsbEndpoint usbBulkIn; + UsbEndpoint usbBulkOut; + + LinkedList expectReplies; + LinkedList expectRepliesVerify; + + @Before + public void setUp() throws Exception { + usbConnection = mock(UsbDeviceConnection.class); + usbBulkIn = mock(UsbEndpoint.class); + when(usbBulkIn.getMaxPacketSize()).thenReturn(MAX_PACKET_LENGTH_IN); + usbBulkOut = mock(UsbEndpoint.class); + when(usbBulkOut.getMaxPacketSize()).thenReturn(MAX_PACKET_LENGTH_OUT); + + expectReplies = new LinkedList<>(); + expectRepliesVerify = new LinkedList<>(); + when(usbConnection.bulkTransfer(same(usbBulkIn), any(byte[].class), any(Integer.class), any(Integer.class))) + .thenAnswer( + new Answer() { + @Override + public Integer answer(InvocationOnMock invocation) throws Throwable { + byte[] reply = expectReplies.poll(); + if (reply == null) { + return -1; + } + + byte[] buf = invocation.getArgumentAt(1, byte[].class); + assertEquals(buf.length, MAX_PACKET_LENGTH_IN); + + int len = Math.min(buf.length, reply.length); + System.arraycopy(reply, 0, buf, 0, len); + + if (len < reply.length) { + byte[] rest = Arrays.copyOfRange(reply, len, reply.length); + expectReplies.addFirst(rest); + } + + return len; + } + }); + + } + + @Test + public void testAutoVoltageSelection() throws Exception { + CcidDescription description = CcidDescription.fromValues((byte) 0, (byte) 1, 2, 132218); + CcidTransceiver ccidTransceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, description); + + byte[] iccPowerOnVoltageAutoCommand = Hex.decode("62000000000000000000"); + byte[] iccPowerOnReply = Hex.decode("80150000000000000000" + ATR); + expectReadPreamble(); + expect(iccPowerOnVoltageAutoCommand, iccPowerOnReply); + + + CcidDataBlock ccidDataBlock = ccidTransceiver.iccPowerOn(); + + + verifyDialog(); + assertArrayEquals(Hex.decode(ATR), ccidDataBlock.getData()); + } + + @Test + public void testManualVoltageSelection() throws Exception { + CcidDescription description = CcidDescription.fromValues((byte) 0, (byte) 1, 2, 132210); + CcidTransceiver ccidTransceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, description); + + byte[] iccPowerOnVoltage5VCommand = Hex.decode("62000000000000010000"); + byte[] iccPowerOnReply = Hex.decode("80150000000000000000" + ATR); + expectReadPreamble(); + expect(iccPowerOnVoltage5VCommand, iccPowerOnReply); + + + CcidDataBlock ccidDataBlock = ccidTransceiver.iccPowerOn(); + + + verifyDialog(); + assertArrayEquals(Hex.decode(ATR), ccidDataBlock.getData()); + } + + @Test + public void testManualVoltageSelection_failFirst() throws Exception { + CcidDescription description = CcidDescription.fromValues((byte) 0, (byte) 3, 2, 132210); + CcidTransceiver ccidTransceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, description); + + byte[] iccPowerOnVoltage5VCommand = Hex.decode("62000000000000010000"); + byte[] iccPowerOnFailureReply = Hex.decode("80000000000000010700"); + byte[] iccPowerOffCommand = Hex.decode("6300000000000100"); + byte[] iccPowerOnVoltage3VCommand = Hex.decode("62000000000002020000"); + byte[] iccPowerOnReply = Hex.decode("80150000000002000000" + ATR); + expectReadPreamble(); + expect(iccPowerOnVoltage5VCommand, iccPowerOnFailureReply); + expect(iccPowerOffCommand, null); + expect(iccPowerOnVoltage3VCommand, iccPowerOnReply); + + + CcidDataBlock ccidDataBlock = ccidTransceiver.iccPowerOn(); + + + verifyDialog(); + assertArrayEquals(Hex.decode(ATR), ccidDataBlock.getData()); + } + + @Test + public void testXfer() throws Exception { + CcidTransceiver ccidTransceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, null); + + String commandData = "010203"; + byte[] command = Hex.decode("6F030000000000000000" + commandData); + String responseData = "0304"; + byte[] response = Hex.decode("80020000000000000000" + responseData); + expect(command, response); + + CcidDataBlock ccidDataBlock = ccidTransceiver.sendXfrBlock(Hex.decode(commandData)); + + verifyDialog(); + assertArrayEquals(Hex.decode(responseData), ccidDataBlock.getData()); + } + + @Test + public void testXfer_IncrementalSeqNums() throws Exception { + CcidTransceiver ccidTransceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, null); + + String commandData = "010203"; + byte[] commandSeq1 = Hex.decode("6F030000000000000000" + commandData); + byte[] commandSeq2 = Hex.decode("6F030000000001000000" + commandData); + String responseData = "0304"; + byte[] responseSeq1 = Hex.decode("80020000000000000000" + responseData); + byte[] responseSeq2 = Hex.decode("80020000000001000000" + responseData); + expect(commandSeq1, responseSeq1); + expect(commandSeq2, responseSeq2); + + ccidTransceiver.sendXfrBlock(Hex.decode(commandData)); + ccidTransceiver.sendXfrBlock(Hex.decode(commandData)); + + verifyDialog(); + } + + @Test(expected = UsbTransportException.class) + public void testXfer_badSeqNumberReply() throws Exception { + CcidTransceiver ccidTransceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, null); + + String commandData = "010203"; + byte[] command = Hex.decode("6F030000000000000000" + commandData); + String responseData = "0304"; + byte[] response = Hex.decode("800200000000AA000000" + responseData); + expect(command, response); + + + ccidTransceiver.sendXfrBlock(Hex.decode(commandData)); + } + + @Test + public void testXfer_errorReply() throws Exception { + CcidTransceiver ccidTransceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, null); + + String commandData = "010203"; + byte[] command = Hex.decode("6F030000000000000000" + commandData); + byte[] response = Hex.decode("80000000000000012A00"); + expect(command, response); + + try { + ccidTransceiver.sendXfrBlock(Hex.decode(commandData)); + } catch (UsbCcidErrorException e) { + assertEquals(0x01, e.getErrorResponse().getIccStatus()); + assertEquals(0x2A, e.getErrorResponse().getError()); + return; + } + + fail(); + } + + @Test + public void testXfer_chainedCommand() throws Exception { + CcidTransceiver ccidTransceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, null); + + String commandData = + "0000000000000123456789000000000000000000000000000000000000000000" + + "0000000000000000000000012345678900000000000000000000000000000000" + + "00000000000001234567890000000000"; + byte[] command = Hex.decode("6F500000000000000000" + commandData); + String responseData = "0304"; + byte[] response = Hex.decode("80020000000000000000" + responseData); + expectChained(command, response); + + CcidDataBlock ccidDataBlock = ccidTransceiver.sendXfrBlock(Hex.decode(commandData)); + + verifyDialog(); + assertArrayEquals(Hex.decode(responseData), ccidDataBlock.getData()); + } + + @Test + public void testXfer_chainedReply() throws Exception { + CcidTransceiver ccidTransceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, null); + + String commandData = "010203"; + byte[] command = Hex.decode("6F030000000000000000" + commandData); + String responseData = + "0000000000000000000000000000000000012345678900000000000000000000" + + "0000000000000000000000000001234567890000000000000000000000000000" + + "00000012345678900000000000000000"; + byte[] response = Hex.decode("80500000000000000000" + responseData); + expect(command, response); + + CcidDataBlock ccidDataBlock = ccidTransceiver.sendXfrBlock(Hex.decode(commandData)); + + verifyDialog(); + assertArrayEquals(Hex.decode(responseData), ccidDataBlock.getData()); + } + + @Test + public void testXfer_timeoutExtensionReply() throws Exception { + CcidTransceiver ccidTransceiver = new CcidTransceiver(usbConnection, usbBulkIn, usbBulkOut, null); + + String commandData = "010203"; + byte[] command = Hex.decode("6F030000000000000000" + commandData); + byte[] timeExtensionResponse = Hex.decode("80000000000000800000"); + String responseData = "0304"; + byte[] response = Hex.decode("80020000000000000000" + responseData); + expect(command, timeExtensionResponse); + expect(null, response); + + CcidDataBlock ccidDataBlock = ccidTransceiver.sendXfrBlock(Hex.decode(commandData)); + + verifyDialog(); + assertArrayEquals(Hex.decode(responseData), ccidDataBlock.getData()); + } + + private void verifyDialog() { + assertTrue(expectReplies.isEmpty()); + assertFalse(expectRepliesVerify.isEmpty()); + + for (byte[] command : expectRepliesVerify) { + if (command == null) { + continue; + } + verify(usbConnection).bulkTransfer(same(usbBulkIn), aryEq(command), any(Integer.class), any(Integer.class)); + } + + expectRepliesVerify.clear(); + } + + private void expectReadPreamble() { + expectReplies.add(null); + expectRepliesVerify.add(null); + } + + private void expectChained(byte[] command, byte[] reply) { + for (int i = 0; i < command.length; i+= MAX_PACKET_LENGTH_OUT) { + int len = Math.min(MAX_PACKET_LENGTH_OUT, command.length - i); + when(usbConnection.bulkTransfer(same(usbBulkOut), aryEq(command), eq(i), eq(len), + any(Integer.class))).thenReturn(len); + } + if (reply != null) { + expectReplies.add(reply); + expectRepliesVerify.add(null); + } + } + + private void expect(byte[] command, byte[] reply) { + if (command != null) { + when(usbConnection.bulkTransfer(same(usbBulkOut), aryEq(command), eq(0), eq(command.length), + any(Integer.class))).thenReturn(command.length); + } + if (reply != null) { + expectReplies.add(reply); + expectRepliesVerify.add(null); + } + } +}