implement PIN-less path

This commit is contained in:
Michele Balistreri 2017-10-26 12:11:49 +03:00
parent 5fc82298b8
commit d778080899
4 changed files with 151 additions and 10 deletions

View File

@ -20,7 +20,7 @@ and passed as an installation parameter to the applet according to the JavaCard
to unblock the applet using the PUK, the PUK is blocked, meaning the the wallet is lost.
After authentication, the user remains authenticated until the application is either deselected or the card is reset.
Authentication with PIN is a requirement for all further commands to succeed.
Authentication with PIN is a requirement for most commands to succeed.
The PIN can be changed by the user after authentication.
@ -32,7 +32,9 @@ specifications. This keyset is used to sign transactions. When the applet is fir
signing will fail. It is necessary to first load the keyset in order for the application to be fully operational.
Signing of transactions is done by uploading the data in blocks no larger than 255 bytes (including the overhead caused
by the Secure Channel). Segmentation must be handled at the application protocol.
by the Secure Channel). Segmentation must be handled at the application protocol. Another option is to sign the hash
of the transaction, with the hash being calculated off-card. Signing generally requires the PIN to be authenticated,
however the user can set a special key path which requires no authentication.
## APDUs
@ -57,6 +59,7 @@ be used by the client to establish the Secure Channel.
The OPEN SECURE CHANNEL command is as specified in the [SECURE_CHANNEL.MD](SECURE_CHANNEL.MD).
### GET STATUS
* CLA = 0x80
* INS = 0xF2
* P1 = 0x00 for application status, 0x01 for key path status
@ -165,7 +168,7 @@ signing sessions, if any. Unless a DERIVE KEY is sent, a subsequent SIGN command
* Response SW = 0x9000 on success, 0x6A80 if the format is invalid, 0x6A81 if public key derivation is not supported and
bit 0 of P1 is set, 0x6A86 if P2 = 0x01 and bit 0 of P1 is not set.
* Response Data = On assisted derivation and P2 = 0x01 the key derivation template. Empty otherwise.
* Preconditions: Secure Channel must be opened, user PIN must be verified, an extended keyset must be loaded
* Preconditions: Secure Channel must be opened, user PIN must be verified (if no PIN-less key is defined), an extended keyset must be loaded
This command is used before a signing session to generate a private key according to the [BIP32](https://github.com/bitcoin/bips/blob/master/bip-0032.mediawiki)
specifications. This command always aborts open signing sessions, if any. The generated key is used for all subsequent
@ -181,7 +184,7 @@ P1:
* bit 7 = if 0 derive from master keys, if 1 derive from current keys
P2:
* 0x00 = data is 32 a sequence of 32-bit integers
* 0x00 = data is a sequence of 32-bit integers
* 0x01 = data is a public key
Response Data format:
@ -216,14 +219,13 @@ human-readable mnemonic. Each integer can have a value from 0 to 2047.
* 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
* Preconditions: Secure Channel must be opened, user PIN must be verified, a valid keypair must be loaded
* 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
@ -255,3 +257,16 @@ This segmentation scheme allows resuming signature sessions if other commands mu
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.
### SET PINLESS PATH
* CLA = 0x80
* INS = 0xC1
* P1 = 0x00
* P2 = 0x00
* Data = a sequence of 32-bit integers
* Response SW = 0x9000 on success, 0x6A80 if data is invalid
* Preconditions: Secure Channel must be opened, user PIN must be verified
Sets the given sequence of 32-bit integers as a PIN-less path. When the current derived key matches this path, SIGN
will work even if no PIN authentication has been performed. An empty sequence means that no PIN-less path is defined.

View File

@ -12,11 +12,13 @@ public class WalletApplet extends Applet {
static final byte INS_DERIVE_KEY = (byte) 0xD1;
static final byte INS_GENERATE_MNEMONIC = (byte) 0xD2;
static final byte INS_SIGN = (byte) 0xC0;
static final byte INS_SET_PINLESS_PATH = (byte) 0xC1;
static final byte PUK_LENGTH = 12;
static final byte PUK_MAX_RETRIES = 5;
static final byte PIN_LENGTH = 6;
static final byte PIN_MAX_RETRIES = 3;
static final short KEY_PATH_MAX_DEPTH = 10;
static final short EC_KEY_SIZE = 256;
static final short CHAIN_CODE_SIZE = 32;
@ -62,7 +64,6 @@ public class WalletApplet extends Applet {
static final byte TLV_PUBLIC_KEY_DERIVATION = (byte) 0xC3;
private static final byte[] ASSISTED_DERIVATION_HASH = { (byte) 0xAA, (byte) 0x2D, (byte) 0xA9, (byte) 0x9D, (byte) 0x91, (byte) 0x8C, (byte) 0x7D, (byte) 0x95, (byte) 0xB8, (byte) 0x96, (byte) 0x89, (byte) 0x87, (byte) 0x3E, (byte) 0xAA, (byte) 0x37, (byte) 0x67, (byte) 0x25, (byte) 0x0C, (byte) 0xFF, (byte) 0x50, (byte) 0x13, (byte) 0x9A, (byte) 0x2F, (byte) 0x87, (byte) 0xBB, (byte) 0x4F, (byte) 0xCA, (byte) 0xB4, (byte) 0xAE, (byte) 0xC3, (byte) 0xE8, (byte) 0x90};
private static final short KEY_PATH_MAX_DEPTH = 10;
private OwnerPIN pin;
private OwnerPIN puk;
@ -80,6 +81,9 @@ public class WalletApplet extends Applet {
private byte[] keyPath;
private short keyPathLen;
private byte[] pinlessPath;
private short pinlessPathLen;
private Signature signature;
private boolean signInProgress;
private boolean expectPublicKey;
@ -109,6 +113,7 @@ public class WalletApplet extends Applet {
masterChainCode = new byte[32];
chainCode = new byte[32];
keyPath = new byte[KEY_PATH_MAX_DEPTH * 4];
pinlessPath = new byte[KEY_PATH_MAX_DEPTH * 4];
publicKey = (ECPublicKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PUBLIC, EC_KEY_SIZE, false);
privateKey = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, EC_KEY_SIZE, false);
@ -160,6 +165,9 @@ public class WalletApplet extends Applet {
case INS_SIGN:
sign(apdu);
break;
case INS_SET_PINLESS_PATH:
setPinlessPath(apdu);
break;
default:
ISOException.throwIt(ISO7816.SW_INS_NOT_SUPPORTED);
break;
@ -391,7 +399,7 @@ public class WalletApplet extends Applet {
}
private void deriveKey(APDU apdu) {
if (!(secureChannel.isOpen() && pin.isValidated() && isExtended)) {
if (!(secureChannel.isOpen() && (pin.isValidated() || (pinlessPathLen > 0)) && isExtended)) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
@ -539,7 +547,7 @@ public class WalletApplet extends Applet {
private void sign(APDU apdu) {
apdu.setIncomingAndReceive();
if (!(secureChannel.isOpen() && pin.isValidated() && privateKey.isInitialized() && !expectPublicKey)) {
if (!(secureChannel.isOpen() && (pin.isValidated() || isPinless()) && privateKey.isInitialized() && !expectPublicKey)) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
@ -581,6 +589,26 @@ public class WalletApplet extends Applet {
}
}
private void setPinlessPath(APDU apdu) {
apdu.setIncomingAndReceive();
if (!(secureChannel.isOpen() && pin.isValidated())) {
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
}
byte[] apduBuffer = apdu.getBuffer();
short len = secureChannel.decryptAPDU(apduBuffer);
if (((short) (len % 4) != 0) || (len > pinlessPath.length)) {
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
}
JCSystem.beginTransaction();
pinlessPathLen = len;
Util.arrayCopy(apduBuffer, ISO7816.OFFSET_CDATA, pinlessPath, (short) 0, len);
JCSystem.commitTransaction();
}
private boolean allDigits(byte[] buffer, short off, short len) {
while(len > 0) {
len--;
@ -594,4 +622,8 @@ public class WalletApplet extends Applet {
return true;
}
private boolean isPinless() {
return (pinlessPathLen > 0) && (pinlessPathLen == keyPathLen) && (Util.arrayCompare(keyPath, (short) 0, pinlessPath, (short) 0, keyPathLen) == 0);
}
}

View File

@ -195,4 +195,9 @@ public class WalletAppletCommandSet {
CommandAPDU deriveKey = new CommandAPDU(0x80, WalletApplet.INS_DERIVE_KEY, p1, p2, secureChannel.encryptAPDU(data));
return apduChannel.transmit(deriveKey);
}
public ResponseAPDU setPinlessPath(byte [] data) throws CardException {
CommandAPDU setPinlessPath = new CommandAPDU(0x80, WalletApplet.INS_SET_PINLESS_PATH, 0x00, 0x00, secureChannel.encryptAPDU(data));
return apduChannel.transmit(setPinlessPath);
}
}

View File

@ -538,6 +538,95 @@ public class WalletAppletTest {
assertTrue(signature.verify(sig));
}
@Test
@DisplayName("SET PINLESS PATH command")
void setPinlessPathTest() throws Exception {
byte[] data = "some data to be hashed".getBytes();
byte[] hash = sha256(data);
KeyPairGenerator g = keypairGenerator();
KeyPair keyPair = g.generateKeyPair();
byte[] chainCode = new byte[32];
new Random().nextBytes(chainCode);
// Security condition violation: SecureChannel not open
ResponseAPDU response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02});
assertEquals(0x6985, response.getSW());
cmdSet.openSecureChannel();
// Security condition violation: PIN not verified
response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02});
assertEquals(0x6985, response.getSW());
response = cmdSet.verifyPIN("000000");
assertEquals(0x9000, response.getSW());
response = cmdSet.loadKey(keyPair, false, chainCode);
assertEquals(0x9000, response.getSW());
// Wrong data
response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00});
assertEquals(0x6a80, response.getSW());
response = cmdSet.setPinlessPath(new byte[(WalletApplet.KEY_PATH_MAX_DEPTH + 1)* 4]);
assertEquals(0x6a80, response.getSW());
// Correct
response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02});
assertEquals(0x9000, response.getSW());
// Verify that only PINless path can be used without PIN
resetAndSelectAndOpenSC();
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
assertEquals(0x6985, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, true, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, false, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true);
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
assertEquals(0x6985, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, false, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true);
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
assertEquals(0x9000, response.getSW());
// Verify changing path
response = cmdSet.verifyPIN("000000");
assertEquals(0x9000, response.getSW());
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);
assertEquals(0x6985, response.getSW());
assertEquals(0x6985, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, true, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, false, true, false);
assertEquals(0x9000, response.getSW());
response = cmdSet.deriveKey(derivePublicKey(secureChannel.decryptAPDU(response.getData())), false, true, true);
assertEquals(0x9000, response.getSW());
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
assertEquals(0x9000, response.getSW());
// Reset
response = cmdSet.verifyPIN("000000");
assertEquals(0x9000, response.getSW());
response = cmdSet.setPinlessPath(new byte[] {});
assertEquals(0x9000, response.getSW());
resetAndSelectAndOpenSC();
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
assertEquals(0x6985, response.getSW());
response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, true, true, false);
assertEquals(0x6985, response.getSW());
}
@Test
@DisplayName("SIGN data (unused for the current scenario)")
@Tag("manual")