mirror of
https://github.com/status-im/status-keycard.git
synced 2025-03-03 06:10:29 +00:00
implement PIN-less path
This commit is contained in:
parent
5fc82298b8
commit
d778080899
@ -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
|
||||
@ -254,4 +256,17 @@ 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.
|
||||
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.
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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")
|
||||
|
Loading…
x
Reference in New Issue
Block a user