diff --git a/src/main/java/im/status/wallet/WalletApplet.java b/src/main/java/im/status/wallet/WalletApplet.java index 4863599..9487ef0 100644 --- a/src/main/java/im/status/wallet/WalletApplet.java +++ b/src/main/java/im/status/wallet/WalletApplet.java @@ -279,6 +279,7 @@ public class WalletApplet extends Applet { } signInProgress = false; + expectPublicKey = false; } private void loadKeyPair(byte[] apduBuffer, boolean newExtended) { @@ -500,7 +501,7 @@ public class WalletApplet extends Applet { private void sign(APDU apdu) { apdu.setIncomingAndReceive(); - if (!(secureChannel.isOpen() && pin.isValidated() && privateKey.isInitialized())) { + if (!(secureChannel.isOpen() && pin.isValidated() && privateKey.isInitialized() && !expectPublicKey)) { ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED); } diff --git a/src/test/java/im/status/wallet/WalletAppletCommandSet.java b/src/test/java/im/status/wallet/WalletAppletCommandSet.java index 0402ef1..9b07c14 100644 --- a/src/test/java/im/status/wallet/WalletAppletCommandSet.java +++ b/src/test/java/im/status/wallet/WalletAppletCommandSet.java @@ -42,6 +42,12 @@ public class WalletAppletCommandSet { return apduChannel.transmit(getStatus); } + public boolean getPublicKeyDerivationSupport() throws CardException { + ResponseAPDU resp = getStatus(); + byte[] data = secureChannel.decryptAPDU(resp.getData()); + return data[data.length - 1] == 1; + } + public ResponseAPDU verifyPIN(String pin) throws CardException { CommandAPDU verifyPIN = new CommandAPDU(0x80, WalletApplet.INS_VERIFY_PIN, 0, 0, secureChannel.encryptAPDU(pin.getBytes())); return apduChannel.transmit(verifyPIN); diff --git a/src/test/java/im/status/wallet/WalletAppletTest.java b/src/test/java/im/status/wallet/WalletAppletTest.java index dfe80f0..81b0ac1 100644 --- a/src/test/java/im/status/wallet/WalletAppletTest.java +++ b/src/test/java/im/status/wallet/WalletAppletTest.java @@ -282,6 +282,8 @@ public class WalletAppletTest { cmdSet.openSecureChannel(); + int publicKeyDerivationSW = cmdSet.getPublicKeyDerivationSupport() ? 0x9000 : 0x6a81; + // Security condition violation: PIN not verified response = cmdSet.loadKey(keyPair); assertEquals(0x6985, response.getSW()); @@ -320,13 +322,13 @@ public class WalletAppletTest { // Check omitted public key response = cmdSet.loadKey(keyPair, true, null); - assertEquals(0x9000, response.getSW()); + assertEquals(publicKeyDerivationSW, response.getSW()); response = cmdSet.loadKey(keyPair, true, chainCode); - assertEquals(0x9000, response.getSW()); + assertEquals(publicKeyDerivationSW, response.getSW()); // Check seed load response = cmdSet.loadKey(keyPair.getPrivate(), chainCode); - assertEquals(0x9000, response.getSW()); + assertEquals(publicKeyDerivationSW, response.getSW()); } @Test @@ -374,6 +376,7 @@ public class WalletAppletTest { assertEquals(0x6985, response.getSW()); cmdSet.openSecureChannel(); + boolean autonomousDerivation = cmdSet.getPublicKeyDerivationSupport(); // Security condition violation: PIN is not verified response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x00}); @@ -410,29 +413,35 @@ public class WalletAppletTest { response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, true, true, false); assertEquals(0x6A80, response.getSW()); - // Correct - response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}); - assertEquals(0x9000, response.getSW()); - verifyKeyDerivation(keyPair, chainCode, new int[] { 1 }); - // 3 levels with hardened key - response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01, (byte) 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}); - assertEquals(0x9000, response.getSW()); - verifyKeyDerivation(keyPair, chainCode, new int[] { 1, 0x80000000, 2}); + if (autonomousDerivation) { + // Correct + response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x01}); + assertEquals(0x9000, response.getSW()); + verifyKeyDerivation(keyPair, chainCode, new int[]{1}); - // Reset master key - response = cmdSet.deriveKey(new byte[0]); - assertEquals(0x9000, response.getSW()); - verifyKeyDerivation(keyPair, chainCode, new int[0]); + // 3 levels with hardened key + response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x01, (byte) 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02}); + assertEquals(0x9000, response.getSW()); + verifyKeyDerivation(keyPair, chainCode, new int[]{1, 0x80000000, 2}); - // 3 levels with hardened key using separate commands - response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, true, false, false); - assertEquals(0x9000, response.getSW()); - response = cmdSet.deriveKey(new byte[] {(byte) 0x80, 0x00, 0x00, 0x00}, false, false, false); - assertEquals(0x9000, response.getSW()); - response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, false, false, false); - assertEquals(0x9000, response.getSW()); - verifyKeyDerivation(keyPair, chainCode, new int[] { 1, 0x80000000, 2}); + // Reset master key + response = cmdSet.deriveKey(new byte[0]); + assertEquals(0x9000, response.getSW()); + verifyKeyDerivation(keyPair, chainCode, new int[0]); + + // 3 levels with hardened key using separate commands + response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x01}, true, false, false); + assertEquals(0x9000, response.getSW()); + response = cmdSet.deriveKey(new byte[]{(byte) 0x80, 0x00, 0x00, 0x00}, false, false, false); + assertEquals(0x9000, response.getSW()); + response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x02}, false, false, false); + assertEquals(0x9000, response.getSW()); + verifyKeyDerivation(keyPair, chainCode, new int[]{1, 0x80000000, 2}); + } else { + response = cmdSet.deriveKey(new byte[]{0x00, 0x00, 0x00, 0x01}); + assertEquals(0x6a81, response.getSW()); + } // Assisted derivation response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x01}, true, true, false); @@ -452,9 +461,21 @@ public class WalletAppletTest { assertEquals(0x9000, response.getSW()); response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, false, true, false); assertEquals(0x6a86, response.getSW()); + + // Reset master key response = cmdSet.deriveKey(new byte[0]); assertEquals(0x9000, response.getSW()); verifyKeyDerivation(keyPair, chainCode, new int[0]); + + // Try to sign before load public key, then resume loading public key + response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x02}, false, true, false); + assertEquals(0x9000, response.getSW()); + byte[] key = derivePublicKey(secureChannel.decryptAPDU(response.getData())); + response = cmdSet.sign(sha256("test".getBytes()), WalletApplet.SIGN_P1_PRECOMPUTED_HASH, true, true); + assertEquals(0x6985, response.getSW()); + response = cmdSet.deriveKey(key, false, true, true); + assertEquals(0x9000, response.getSW()); + verifyKeyDerivation(keyPair, chainCode, new int[] { 2 }); } @Test @@ -732,13 +753,14 @@ public class WalletAppletTest { private byte[] derivePublicKey(byte[] data) { byte[] pubKey = Arrays.copyOfRange(data, 3, 4 + data[3]); byte[] signature = Arrays.copyOfRange(data, 4 + data[3], data.length); + byte[] hash = sha256("STATUS KEY DERIVATION".getBytes()); pubKey[0] = 0x02; ECKey candidate = ECKey.fromPublicOnly(pubKey); - if (!candidate.verify(WalletApplet.ASSISTED_DERIVATION_HASH, signature)) { + if (!candidate.verify(hash, signature)) { pubKey[0] = 0x03; candidate = ECKey.fromPublicOnly(pubKey); - assertTrue(candidate.verify(WalletApplet.ASSISTED_DERIVATION_HASH, signature)); + assertTrue(candidate.verify(hash, signature)); } return candidate.decompress().getPubKey();