implement SIGN

This commit is contained in:
Michele Balistreri 2017-09-30 17:26:06 +03:00
parent dd11e0cfa4
commit ac5929a9fe
5 changed files with 187 additions and 10 deletions

View File

@ -142,7 +142,8 @@ P2:
Used to sign transactions. Since the maximum short APDU size is 255 bytes the transaction must be segmented before
being sent if it is larger than that. The overhead from the Secure Channel must be also accounted for. When the last
segment is sent, the card returns the calculated signature.
segment is sent, the card returns the calculated signature. The signature is an ECDSA signature calculated over the
SHA-1 hash of the sent data.
The P2 parameter is used to manage the signing session and is treated as a bitmask. The rightmost bit indicates whether
this block is the first one (1) or not (0). On the first block the card resets the signature state. The leftmost bit
@ -155,5 +156,7 @@ the signing session is composed of a single session P2 will have the value of 0x
After a signature is generated, the next SIGN command must have the rightmost bit of P2 set, otherwise 0x6A86 will
be returned.
This segmentation scheme allows resuming signature sessions on power loss and at the same time avoid generating
signatures over partial data, since both the first and the last block are marked.
This segmentation scheme allows resuming signature sessions if other commands must be sent in between and at
the same time avoid generating signatures over partial data, since both the first and the last block are marked.
On applet selection any pending signing session is aborted.

View File

@ -19,6 +19,9 @@ public class WalletApplet extends Applet {
static final byte LOAD_KEY_EC = 0x01;
static final byte SIGN_FIRST_BLOCK_MASK = 0x01;
static final byte SIGN_LAST_BLOCK_MASK = (byte) 0x80;
static final byte TLV_KEY_TEMPLATE = (byte) 0xA1;
static final byte TLV_PUB_KEY = (byte) 0x80;
static final byte TLV_PRIV_KEY = (byte) 0x81;
@ -54,7 +57,6 @@ public class WalletApplet extends Applet {
ECCurves.setSECP256K1CurveParameters(privateKey);
signature = Signature.getInstance(Signature.ALG_ECDSA_SHA, false);
signInProgress = false;
register(bArray, (short) (bOffset + 1), bArray[bOffset]);
}
@ -93,6 +95,7 @@ public class WalletApplet extends Applet {
}
private void selectApplet(APDU apdu) {
signInProgress = false;
pin.reset();
puk.reset();
@ -199,8 +202,25 @@ public class WalletApplet extends Applet {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
//signature.init(privateKey, Signature.MODE_SIGN);
byte[] apduBuffer = apdu.getBuffer();
if ((apduBuffer[ISO7816.OFFSET_P2] & SIGN_FIRST_BLOCK_MASK) == SIGN_FIRST_BLOCK_MASK) {
signInProgress = true;
signature.init(privateKey, Signature.MODE_SIGN);
} else if (!signInProgress) {
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
}
short len = secureChannel.decryptAPDU(apduBuffer);
if ((apduBuffer[ISO7816.OFFSET_P2] & SIGN_LAST_BLOCK_MASK) == SIGN_LAST_BLOCK_MASK) {
signInProgress = false;
len = signature.sign(apduBuffer, ISO7816.OFFSET_CDATA, len, apduBuffer, SecureChannel.SC_OUT_OFFSET);
len = secureChannel.encryptAPDU(apduBuffer, len);
apdu.setOutgoingAndSend(ISO7816.OFFSET_CDATA, len);
} else {
signature.update(apduBuffer, ISO7816.OFFSET_CDATA, len);
}
}
private boolean allDigits(byte[] buffer, short off, short len) {

View File

@ -16,6 +16,8 @@ import javax.smartcardio.ResponseAPDU;
import java.security.*;
public class SecureChannelSession {
public static final int PAYLOAD_MAX_SIZE = 223;
private byte[] secret;
private byte[] publicKey;
private Cipher sessionCipher;
@ -65,6 +67,8 @@ public class SecureChannelSession {
}
public byte[] encryptAPDU(byte[] data) {
assert data.length <= PAYLOAD_MAX_SIZE;
if (sessionKey == null) {
return data;
}

View File

@ -14,6 +14,7 @@ import java.security.KeyPair;
public class WalletAppletCommandSet {
public static final String APPLET_AID = "53746174757357616C6C6574417070";
public static final byte[] APPLET_AID_BYTES = Hex.decode(APPLET_AID);
private final CardChannel apduChannel;
private SecureChannelSession secureChannel;
@ -78,4 +79,10 @@ public class WalletAppletCommandSet {
CommandAPDU loadKey = new CommandAPDU(0x80, WalletApplet.INS_LOAD_KEY, keyType, 0, secureChannel.encryptAPDU(data));
return apduChannel.transmit(loadKey);
}
public ResponseAPDU sign(byte[] data, boolean isFirst, boolean isLast) throws CardException {
byte p2 = (byte) ((isFirst ? 0x01 : 0x00) | (isLast ? 0x80 : 0x00));
CommandAPDU sign = new CommandAPDU(0x80, WalletApplet.INS_SIGN, 0, p2, secureChannel.encryptAPDU(data));
return apduChannel.transmit(sign);
}
}

View File

@ -8,8 +8,12 @@ import javax.smartcardio.*;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Security;
import java.security.Signature;
import java.util.Arrays;
import java.util.Random;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
@DisplayName("Test the Wallet Applet")
public class WalletAppletTest {
@ -74,17 +78,21 @@ public class WalletAppletTest {
@Test
@DisplayName("VERIFY PIN command")
void verifyPinTest() throws CardException {
// Security condition violation: SecureChannel not open
ResponseAPDU response = cmdSet.verifyPIN("000000");
assertEquals(0x6985, response.getSW());
cmdSet.openSecureChannel();
// Wrong PIN
response = cmdSet.verifyPIN("123456");
assertEquals(0x63C2, response.getSW());
// Correct PIN
response = cmdSet.verifyPIN("000000");
assertEquals(0x9000, response.getSW());
// Check max retry counter
response = cmdSet.verifyPIN("123456");
assertEquals(0x63C2, response.getSW());
@ -97,6 +105,7 @@ public class WalletAppletTest {
response = cmdSet.verifyPIN("000000");
assertEquals(0x63C0, response.getSW());
// Unblock PIN to make further tests possible
response = cmdSet.unblockPIN("123456789012", "000000");
assertEquals(0x9000, response.getSW());
}
@ -104,14 +113,17 @@ public class WalletAppletTest {
@Test
@DisplayName("CHANGE PIN command")
void changePinTest() throws CardException {
// Security condition violation: SecureChannel not open
ResponseAPDU response = cmdSet.changePIN("123456");
assertEquals(0x6985, response.getSW());
cmdSet.openSecureChannel();
// Security condition violation: PIN n ot verified
response = cmdSet.changePIN("123456");
assertEquals(0x6985, response.getSW());
// Change PIN correctly, check that after PIN change the PIN remains validated
response = cmdSet.verifyPIN("000000");
assertEquals(0x9000, response.getSW());
@ -121,6 +133,7 @@ public class WalletAppletTest {
response = cmdSet.changePIN("654321");
assertEquals(0x9000, response.getSW());
// Reset card and verify that the new PIN has really been set
apduChannel.getCard().getATR();
cmdSet.select();
cmdSet.openSecureChannel();
@ -128,6 +141,7 @@ public class WalletAppletTest {
response = cmdSet.verifyPIN("654321");
assertEquals(0x9000, response.getSW());
// Test wrong PIN formats (non-digits, too short, too long)
response = cmdSet.changePIN("654a21");
assertEquals(0x6A80, response.getSW());
@ -137,6 +151,7 @@ public class WalletAppletTest {
response = cmdSet.changePIN("7654321");
assertEquals(0x6A80, response.getSW());
// Reset the PIN to make further tests possible
response = cmdSet.changePIN("000000");
assertEquals(0x9000, response.getSW());
}
@ -144,14 +159,17 @@ public class WalletAppletTest {
@Test
@DisplayName("UNBLOCK PIN command")
void unblockPinTest() throws CardException {
ResponseAPDU response = cmdSet.changePIN("123456");
// Security condition violation: SecureChannel not open
ResponseAPDU response = cmdSet.unblockPIN("123456789012", "000000");
assertEquals(0x6985, response.getSW());
cmdSet.openSecureChannel();
// Condition violation: PIN is not blocked
response = cmdSet.unblockPIN("123456789012", "000000");
assertEquals(0x6985, response.getSW());
// Block the PIN
response = cmdSet.verifyPIN("123456");
assertEquals(0x63C2, response.getSW());
@ -161,18 +179,22 @@ public class WalletAppletTest {
response = cmdSet.verifyPIN("123456");
assertEquals(0x63C0, response.getSW());
// Wrong PUK formats (too short, too long)
response = cmdSet.unblockPIN("12345678901", "000000");
assertEquals(0x6A80, response.getSW());
response = cmdSet.unblockPIN("1234567890123", "000000");
assertEquals(0x6A80, response.getSW());
// Wrong PUK
response = cmdSet.unblockPIN("123456789010", "000000");
assertEquals(0x63C4, response.getSW());
// Correct PUK
response = cmdSet.unblockPIN("123456789012", "654321");
assertEquals(0x9000, response.getSW());
// Check that PIN has been changed and unblocked
apduChannel.getCard().getATR();
cmdSet.select();
cmdSet.openSecureChannel();
@ -180,6 +202,7 @@ public class WalletAppletTest {
response = cmdSet.verifyPIN("654321");
assertEquals(0x9000, response.getSW());
// Reset the PIN to make further tests possible
response = cmdSet.changePIN("000000");
assertEquals(0x9000, response.getSW());
}
@ -187,26 +210,27 @@ public class WalletAppletTest {
@Test
@DisplayName("LOAD KEY command")
void loadKeyTest() throws Exception {
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
KeyPairGenerator g = KeyPairGenerator.getInstance("ECDH", "BC");
g.initialize(ecSpec);
KeyPairGenerator g = keypairGenerator();
KeyPair keyPair = g.generateKeyPair();
// Security condition violation: SecureChannel not open
ResponseAPDU response = cmdSet.loadKey(keyPair);
assertEquals(0x6985, response.getSW());
cmdSet.openSecureChannel();
// Security condition violation: PIN not verified
response = cmdSet.loadKey(keyPair);
assertEquals(0x6985, response.getSW());
response = cmdSet.verifyPIN("000000");
assertEquals(0x9000, response.getSW());
// Wrong key type
response = cmdSet.loadKey(new byte[] { (byte) 0xAA, 0x02, (byte) 0x80, 0x00}, (byte) 0x00);
assertEquals(0x6A86, response.getSW());
// Wrong data (wrong template, missing private key, invalid keys)
response = cmdSet.loadKey(new byte[] { (byte) 0xAA, 0x02, (byte) 0x80, 0x00}, WalletApplet.LOAD_KEY_EC);
assertEquals(0x6A80, response.getSW());
@ -216,12 +240,131 @@ public class WalletAppletTest {
response = cmdSet.loadKey(new byte[] { (byte) 0xA1, 0x06, (byte) 0x80, 0x01, 0x01, (byte) 0x81, 0x01, 0x02}, WalletApplet.LOAD_KEY_EC);
assertEquals(0x6A80, response.getSW());
// Correct LOAD KEY
response = cmdSet.loadKey(keyPair);
assertEquals(0x9000, response.getSW());
keyPair = g.generateKeyPair();
// Check replacing keys
response = cmdSet.loadKey(keyPair);
assertEquals(0x9000, response.getSW());
}
@Test
@DisplayName("SIGN command")
void signTest() throws Exception {
Random r = new Random();
byte[] data = new byte[SecureChannelSession.PAYLOAD_MAX_SIZE];
byte[] smallData = Arrays.copyOf(data, 20);
r.nextBytes(data);
// Security condition violation: SecureChannel not open
ResponseAPDU response = cmdSet.sign(smallData, true, true);
assertEquals(0x6985, response.getSW());
cmdSet.openSecureChannel();
// Security condition violation: PIN not verified
response = cmdSet.sign(smallData, true, true);
assertEquals(0x6985, response.getSW());
response = cmdSet.verifyPIN("000000");
assertEquals(0x9000, response.getSW());
KeyPairGenerator g = keypairGenerator();
KeyPair keyPair = keypairGenerator().generateKeyPair();
Signature signature = Signature.getInstance("ECDSAwithSHA1", "BC");
signature.initVerify(keyPair.getPublic());
response = cmdSet.loadKey(keyPair);
assertEquals(0x9000, response.getSW());
// Wrong P2: no active signing session but first block bit not set
response = cmdSet.sign(data, false, false);
assertEquals(0x6A86, response.getSW());
response = cmdSet.sign(data, false, true);
assertEquals(0x6A86, response.getSW());
// Correctly sign 1 block (P2: 0x81)
response = cmdSet.sign(smallData, true, true);
assertEquals(0x9000, response.getSW());
byte[] sig = secureChannel.decryptAPDU(response.getData());
signature.update(smallData);
assertTrue(signature.verify(sig));
// Correctly sign 2 blocks (P2: 0x01, 0x81)
response = cmdSet.sign(data, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(smallData, false, true);
assertEquals(0x9000, response.getSW());
sig = secureChannel.decryptAPDU(response.getData());
signature.update(data);
signature.update(smallData);
assertTrue(signature.verify(sig));
// Correctly sign 3 blocks (P2: 0x01, 0x00, 0x80)
response = cmdSet.sign(data, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(data, false, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(smallData, false, true);
assertEquals(0x9000, response.getSW());
sig = secureChannel.decryptAPDU(response.getData());
signature.update(data);
signature.update(data);
signature.update(smallData);
assertTrue(signature.verify(sig));
// Re-start signing session by sending new first block
response = cmdSet.sign(data, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(smallData, true, true);
assertEquals(0x9000, response.getSW());
sig = secureChannel.decryptAPDU(response.getData());
signature.update(smallData);
assertTrue(signature.verify(sig));
// Abort signing session by loading new keys
response = cmdSet.sign(data, true, false);
assertEquals(0x9000, response.getSW());
keyPair = keypairGenerator().generateKeyPair();
signature.initVerify(keyPair.getPublic());
response = cmdSet.loadKey(keyPair);
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(smallData, false, true);
assertEquals(0x6A86, response.getSW());
// Signing session is aborted on reselection
response = cmdSet.sign(data, true, false);
assertEquals(0x9000, response.getSW());
apduChannel.getCard().getATR();
cmdSet.select();
cmdSet.openSecureChannel();
response = cmdSet.verifyPIN("000000");
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(smallData, false, true);
assertEquals(0x6A86, response.getSW());
// Signing session can be resumed if other commands are sent
response = cmdSet.sign(data, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.changePIN("000000");
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(smallData, false, true);
assertEquals(0x9000, response.getSW());
sig = secureChannel.decryptAPDU(response.getData());
signature.update(data);
signature.update(smallData);
assertTrue(signature.verify(sig));
}
private KeyPairGenerator keypairGenerator() throws Exception {
ECParameterSpec ecSpec = ECNamedCurveTable.getParameterSpec("secp256k1");
KeyPairGenerator g = KeyPairGenerator.getInstance("ECDH", "BC");
g.initialize(ecSpec);
return g;
}
}