remove plain data signing, closes #15

This commit is contained in:
Michele Balistreri 2018-11-07 16:02:21 +03:00
parent e78397c198
commit bc0cb02eca
4 changed files with 44 additions and 205 deletions

View File

@ -7,6 +7,7 @@ compared to the previous released version. This is version 2.0 of the specs (unr
### Changes since 1.2
* **BREAKING** Removed assisted key derivation
* **BREAKING** Removed plain data signing, now only 32-byte long hashes can be signed
* Added internal key generation
## Overview
@ -88,7 +89,7 @@ When the applet is in pre-initializated state, it only returns the ECC public ke
* P1 = 0x00
* P2 = 0x00
* Data = EC public key (LV encoded) | IV | encrypted payload
* Response SW = 0x9000 on success, 0x6D00 if the applet is already initialized
* Response SW = 0x9000 on success, 0x6D00 if the applet is already initialized, 0x6A80 if the data is invalid
This command is only available when the applet is in pre-initialized state and successful execution brings the applet in
the initialized state. This command is needed to allow securely storing secrets on the applet at a different moment and
@ -309,23 +310,14 @@ had been performed.
* CLA = 0x80
* INS = 0xC0
* P1 = data type
* P2 = segment flag
* Data = the data to sign
* Response = if P2 indicates last segment, the public key and the signature are returned
* Response SW = 0x9000 on success, 0x6A86 if P2 is invalid
* P1 = 0x00
* P2 = 0x00
* Data = the hash to sign
* Response = public key and the signature
* Response SW = 0x9000 on success, 0x6A80 if the data is not 32-byte long
* Preconditions: Secure Channel must be opened, user PIN must be verified (or a PIN-less key must be active), a valid
keypair must be loaded
P1:
* 0x00 = transaction data
* 0x01 = precomputed hash
P2:
* bit 0 = if 1 first block, if 0 other block
* bit 1-6 = reserved
* bit 7 = if 0 more blocks, if 1 last block
Response Data format:
- Tag 0xA0 = signature template
- Tag 0x80 = ECC public key component
@ -333,26 +325,12 @@ Response Data format:
- Tag 0x02 = R value
- Tag 0x02 = S value
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. The signature is an ECDSA signature calculated over the
SHA-256 hash of the sent data or directly over the provided hash if P1 = 0x01.
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
indicates whether this is the last block (1) or not (0). On the last block, the card generates and sends the signatures
to the client.
For example, if a signing session spans over 3 segments, the value of P2 will be respectively 0x01, 0x00, 0x80. If
the signing session is composed of a single session P2 will have the value of 0x81.
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 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.
Returns the ECDSA signature of the hash. The hash can be calculated using any algorithm, but must be 32-bytes long. The
signature is returned in a signature template, containing the public key associated to the signature and the signature
itself. For usage on the blockchain, you will need to calculate the recovery ID in addition to extracting R and S.
To calculate the recovery ID you need to apply the same algorithm used for public key recovery from a transaction starting
with a recovery ID of 0. If the public key matches the one returned in the template, then you have found the recovery ID,
otherwise you try again by incrementing the recovery ID.
### SET PINLESS PATH

View File

@ -47,12 +47,6 @@ public class WalletApplet extends Applet {
static final byte LOAD_KEY_P1_EXT_EC = 0x02;
static final byte LOAD_KEY_P1_SEED = 0x03;
static final byte SIGN_P1_DATA = 0x00;
static final byte SIGN_P1_PRECOMPUTED_HASH = 0x01;
static final byte SIGN_P2_FIRST_BLOCK_MASK = 0x01;
static final byte SIGN_P2_LAST_BLOCK_MASK = (byte) 0x80;
static final byte DERIVE_P1_SOURCE_MASTER = (byte) 0x00;
static final byte DERIVE_P1_SOURCE_PARENT = (byte) 0x40;
static final byte DERIVE_P1_SOURCE_CURRENT = (byte) 0x80;
@ -110,7 +104,6 @@ public class WalletApplet extends Applet {
private short pinlessPathLen;
private Signature signature;
private boolean signInProgress;
private byte[] keyUID;
@ -278,11 +271,7 @@ public class WalletApplet extends Applet {
} else if (apduBuffer[ISO7816.OFFSET_INS] == INS_INIT) {
secureChannel.oneShotDecrypt(apduBuffer);
if (apduBuffer[ISO7816.OFFSET_LC] != (byte)(PIN_LENGTH + PUK_LENGTH + SecureChannel.SC_SECRET_LENGTH)) {
ISOException.throwIt(ISO7816.SW_WRONG_LENGTH);
}
if (!allDigits(apduBuffer, ISO7816.OFFSET_CDATA, (short)(PIN_LENGTH + PUK_LENGTH))) {
if ((apduBuffer[ISO7816.OFFSET_LC] != (byte)(PIN_LENGTH + PUK_LENGTH + SecureChannel.SC_SECRET_LENGTH)) || !allDigits(apduBuffer, ISO7816.OFFSET_CDATA, (short)(PIN_LENGTH + PUK_LENGTH))) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
@ -329,7 +318,6 @@ public class WalletApplet extends Applet {
* @param apdu the JCRE-owned APDU object.
*/
private void selectApplet(APDU apdu) {
signInProgress = false;
pin.reset();
puk.reset();
secureChannel.reset();
@ -598,7 +586,6 @@ public class WalletApplet extends Applet {
* manipulation has happened to be sure that the state is always consistent.
*/
private void resetKeyStatus(boolean toParent) {
signInProgress = false;
parentValid = false;
keyPathLen = toParent ? (short) (keyPathLen - 4) : 0;
}
@ -743,8 +730,6 @@ public class WalletApplet extends Applet {
resetKeys(fromParent, apduBuffer, chainEnd);
}
signInProgress = false;
for (short i = ISO7816.OFFSET_CDATA; i < chainEnd; i += 4) {
JCSystem.beginTransaction();
@ -902,7 +887,6 @@ public class WalletApplet extends Applet {
pinlessPathLen = 0;
parentValid = false;
isExtended = false;
signInProgress = false;
privateKey.clearKey();
publicKey.clearKey();
masterPrivate.clearKey();
@ -960,40 +944,26 @@ public class WalletApplet extends Applet {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
if ((apduBuffer[ISO7816.OFFSET_P2] & SIGN_P2_FIRST_BLOCK_MASK) == SIGN_P2_FIRST_BLOCK_MASK) {
signInProgress = true;
signature.init(privateKey, Signature.MODE_SIGN);
} else if (!signInProgress) {
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
if (len != MessageDigest.LENGTH_SHA_256) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
if ((apduBuffer[ISO7816.OFFSET_P2] & SIGN_P2_LAST_BLOCK_MASK) == SIGN_P2_LAST_BLOCK_MASK) {
signInProgress = false;
signature.init(privateKey, Signature.MODE_SIGN);
apduBuffer[SecureChannel.SC_OUT_OFFSET] = TLV_SIGNATURE_TEMPLATE;
apduBuffer[(short)(SecureChannel.SC_OUT_OFFSET + 3)] = TLV_PUB_KEY;
short outLen = apduBuffer[(short)(SecureChannel.SC_OUT_OFFSET + 4)] = (byte) publicKey.getW(apduBuffer, (short) (SecureChannel.SC_OUT_OFFSET + 5));
apduBuffer[SecureChannel.SC_OUT_OFFSET] = TLV_SIGNATURE_TEMPLATE;
apduBuffer[(short)(SecureChannel.SC_OUT_OFFSET + 3)] = TLV_PUB_KEY;
short outLen = apduBuffer[(short)(SecureChannel.SC_OUT_OFFSET + 4)] = (byte) publicKey.getW(apduBuffer, (short) (SecureChannel.SC_OUT_OFFSET + 5));
outLen += 5;
short sigOff = (short) (SecureChannel.SC_OUT_OFFSET + outLen);
outLen += 5;
short sigOff = (short) (SecureChannel.SC_OUT_OFFSET + outLen);
if ((apduBuffer[ISO7816.OFFSET_P1]) == SIGN_P1_DATA) {
outLen += signature.sign(apduBuffer, ISO7816.OFFSET_CDATA, len, apduBuffer, sigOff);
} else if ((apduBuffer[ISO7816.OFFSET_P1]) == SIGN_P1_PRECOMPUTED_HASH) {
outLen += signature.signPreComputedHash(apduBuffer, ISO7816.OFFSET_CDATA, len, apduBuffer, sigOff);
} else {
ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2);
}
outLen += signature.signPreComputedHash(apduBuffer, ISO7816.OFFSET_CDATA, len, apduBuffer, sigOff);
outLen += crypto.fixS(apduBuffer, sigOff);
outLen += crypto.fixS(apduBuffer, sigOff);
apduBuffer[(short)(SecureChannel.SC_OUT_OFFSET + 1)] = (byte) 0x81;
apduBuffer[(short)(SecureChannel.SC_OUT_OFFSET + 2)] = (byte) (outLen - 3);
apduBuffer[(short)(SecureChannel.SC_OUT_OFFSET + 1)] = (byte) 0x81;
apduBuffer[(short)(SecureChannel.SC_OUT_OFFSET + 2)] = (byte) (outLen - 3);
secureChannel.respond(apdu, outLen, ISO7816.SW_NO_ERROR);
} else {
signature.update(apduBuffer, ISO7816.OFFSET_CDATA, len);
}
secureChannel.respond(apdu, outLen, ISO7816.SW_NO_ERROR);
}
/**

View File

@ -394,21 +394,14 @@ public class WalletAppletCommandSet {
}
/**
* Sends a SIGN APDU. The dataType is P1 as defined in the applet. The isFirst and isLast arguments are used to form
* the P2 parameter. The data is the data to sign, or part of it. Only when sending the last block a signature is
* generated and thus returned. When signing a precomputed hash it must be done in a single block, so isFirst and
* isLast will always be true at the same time.
* Sends a SIGN APDU. This signs a precomputed hash so the input must be exactly 32-bytes long.
*
* @param data the data to sign
* @param dataType the P1 parameter
* @param isFirst whether this is the first block of the command or not
* @param isLast whether this is the last block of the command or not
* @return the raw card response
* @throws CardException communication error
*/
public ResponseAPDU sign(byte[] data, byte dataType, boolean isFirst, boolean isLast) throws CardException {
byte p2 = (byte) ((isFirst ? 0x01 : 0x00) | (isLast ? 0x80 : 0x00));
CommandAPDU sign = secureChannel.protectedCommand(0x80, WalletApplet.INS_SIGN, dataType, p2, data);
public ResponseAPDU sign(byte[] data) throws CardException {
CommandAPDU sign = secureChannel.protectedCommand(0x80, WalletApplet.INS_SIGN, 0x00, 0x00, data);
return secureChannel.transmit(apduChannel, sign);
}

View File

@ -793,13 +793,13 @@ public class WalletAppletTest {
byte[] hash = sha256(data);
// Security condition violation: SecureChannel not open
ResponseAPDU response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
ResponseAPDU response = cmdSet.sign(hash);
assertEquals(0x6985, response.getSW());
cmdSet.autoOpenSecureChannel();
// Security condition violation: PIN not verified
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true,true);
response = cmdSet.sign(hash);
assertEquals(0x6985, response.getSW());
response = cmdSet.verifyPIN("000000");
@ -812,15 +812,12 @@ public class WalletAppletTest {
response = cmdSet.loadKey(keyPair);
assertEquals(0x9000, response.getSW());
// Wrong P2: no active signing session but first block bit not set
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,false, false);
assertEquals(0x6A86, response.getSW());
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,false, true);
assertEquals(0x6A86, response.getSW());
// Wrong Data length
response = cmdSet.sign(data);
assertEquals(0x6A80, response.getSW());
// Correctly sign a precomputed hash
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
response = cmdSet.sign(hash);
assertEquals(0x9000, response.getSW());
byte[] sig = response.getData();
byte[] keyData = extractPublicKeyFromSignature(sig);
@ -869,15 +866,15 @@ public class WalletAppletTest {
// Verify that only PINless path can be used without PIN
resetAndSelectAndOpenSC();
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
response = cmdSet.sign(hash);
assertEquals(0x6985, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01}, WalletApplet.DERIVE_P1_SOURCE_MASTER);
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
response = cmdSet.sign(hash);
assertEquals(0x6985, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, WalletApplet.DERIVE_P1_SOURCE_CURRENT);
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
response = cmdSet.sign(hash);
assertEquals(0x9000, response.getSW());
// Verify changing path
@ -886,12 +883,12 @@ public class WalletAppletTest {
response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01});
assertEquals(0x9000, response.getSW());
resetAndSelectAndOpenSC();
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
response = cmdSet.sign(hash);
assertEquals(0x6985, response.getSW());
assertEquals(0x6985, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01}, WalletApplet.DERIVE_P1_SOURCE_MASTER);
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
response = cmdSet.sign(hash);
assertEquals(0x9000, response.getSW());
// Reset
@ -900,7 +897,7 @@ public class WalletAppletTest {
response = cmdSet.setPinlessPath(new byte[] {});
assertEquals(0x9000, response.getSW());
resetAndSelectAndOpenSC();
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
response = cmdSet.sign(hash);
assertEquals(0x6985, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, WalletApplet.DERIVE_P1_SOURCE_MASTER);
assertEquals(0x6985, response.getSW());
@ -987,105 +984,6 @@ public class WalletAppletTest {
assertEquals(0x6985, response.getSW());
}
@Test
@DisplayName("SIGN data (unused for the current scenario)")
@Tag("manual")
void signDataTest() throws Exception {
Random r = new Random();
byte[] data = new byte[SecureChannelSession.PAYLOAD_MAX_SIZE];
byte[] smallData = Arrays.copyOf(data, 20);
r.nextBytes(data);
cmdSet.autoOpenSecureChannel();
ResponseAPDU response = cmdSet.verifyPIN("000000");
assertEquals(0x9000, response.getSW());
KeyPair keyPair = keypairGenerator().generateKeyPair();
Signature signature = Signature.getInstance("SHA256withECDSA", "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, WalletApplet.SIGN_P1_DATA,false, false);
assertEquals(0x6A86, response.getSW());
response = cmdSet.sign(data, WalletApplet.SIGN_P1_DATA,false, true);
assertEquals(0x6A86, response.getSW());
// Correctly sign 1 block (P2: 0x81)
response = cmdSet.sign(smallData, WalletApplet.SIGN_P1_DATA,true, true);
assertEquals(0x9000, response.getSW());
byte[] sig = extractSignature(response.getData());
signature.update(smallData);
assertTrue(signature.verify(sig));
// Correctly sign 2 blocks (P2: 0x01, 0x81)
response = cmdSet.sign(data, WalletApplet.SIGN_P1_DATA,true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(smallData, WalletApplet.SIGN_P1_DATA,false, true);
assertEquals(0x9000, response.getSW());
sig = extractSignature(response.getData());
signature.update(data);
signature.update(smallData);
assertTrue(signature.verify(sig));
// Correctly sign 3 blocks (P2: 0x01, 0x00, 0x80)
response = cmdSet.sign(data, WalletApplet.SIGN_P1_DATA,true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(data, WalletApplet.SIGN_P1_DATA,false, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(smallData, WalletApplet.SIGN_P1_DATA,false, true);
assertEquals(0x9000, response.getSW());
sig = extractSignature(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, WalletApplet.SIGN_P1_DATA,true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(smallData, WalletApplet.SIGN_P1_DATA,true, true);
assertEquals(0x9000, response.getSW());
sig = extractSignature(response.getData());
signature.update(smallData);
assertTrue(signature.verify(sig));
// Abort signing session by loading new keys
response = cmdSet.sign(data, WalletApplet.SIGN_P1_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, WalletApplet.SIGN_P1_DATA,false, true);
assertEquals(0x6A86, response.getSW());
// Signing session is aborted on reselection
response = cmdSet.sign(data, WalletApplet.SIGN_P1_DATA,true, false);
assertEquals(0x9000, response.getSW());
resetAndSelectAndOpenSC();
response = cmdSet.verifyPIN("000000");
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(smallData, WalletApplet.SIGN_P1_DATA,false, true);
assertEquals(0x6A86, response.getSW());
// Signing session can be resumed if other commands are sent
response = cmdSet.sign(data, WalletApplet.SIGN_P1_DATA,true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.changePIN(WalletApplet.CHANGE_PIN_P1_USER_PIN, "000000");
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(smallData, WalletApplet.SIGN_P1_DATA,false, true);
assertEquals(0x9000, response.getSW());
sig = extractSignature(response.getData());
signature.update(data);
signature.update(smallData);
assertTrue(signature.verify(sig));
}
@Test
@DisplayName("Sign actual Ethereum transaction")
@Tag("manual")
@ -1263,7 +1161,7 @@ public class WalletAppletTest {
DeterministicKey key = deriveKey(keyPair, chainCode, path);
byte[] hash = Hash.sha3(new byte[8]);
ResponseAPDU resp = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH, true, true);
ResponseAPDU resp = cmdSet.sign(hash);
assertEquals(0x9000, resp.getSW());
byte[] sig = resp.getData();
byte[] publicKey = extractPublicKeyFromSignature(sig);
@ -1347,7 +1245,7 @@ public class WalletAppletTest {
private Sign.SignatureData signMessage(byte[] message) throws Exception {
byte[] messageHash = Hash.sha3(message);
ResponseAPDU response = cmdSet.sign(messageHash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
ResponseAPDU response = cmdSet.sign(messageHash);
assertEquals(0x9000, response.getSW());
byte[] respData = response.getData();
byte[] rawSig = extractSignature(respData);