implement SIGN
This commit is contained in:
parent
dd11e0cfa4
commit
ac5929a9fe
|
@ -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.
|
|
@ -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) {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue