Add the MUTUALLY AUTHENTICATE command
This commit is contained in:
parent
09fe778d85
commit
60f18b7afd
|
@ -64,6 +64,10 @@ which must 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).
|
||||
|
||||
### MUTUALLY AUTHENTICATE
|
||||
|
||||
The MUTUALLY AUTHENTICATE command is as specified in the [SECURE_CHANNEL.MD](SECURE_CHANNEL.MD).
|
||||
|
||||
### PAIR
|
||||
|
||||
The PAIR command is as specified in the [SECURE_CHANNEL.MD](SECURE_CHANNEL.MD). The shared secret is the SHA-256 of the
|
||||
|
|
|
@ -15,6 +15,8 @@ A short description of establishing a session is as follows
|
|||
generate a shared 256-bit secret (more details below).
|
||||
3. The generated secret is used as an AES key to encrypt all further communication. CBC mode is used with a random IV
|
||||
generated for each APDU and prepended to the APDU payload. Both command and responses are encrypted.
|
||||
4. The client sends a MUTUALLY AUTHENTICATE command to verify that the keys are matching and thus the secure channel is
|
||||
successfully established.
|
||||
|
||||
The EC keyset used by the card for the EC-DH algorithm is generated on-card on applet installation and is not used
|
||||
for anything else. The EC keyset used by the client is generated every time a new secure channel session must be
|
||||
|
@ -32,7 +34,7 @@ opened.
|
|||
* Response Data = A 256-bit salt
|
||||
* Response SW = 0x9000 on success, 0x6A86 if P1 is invalid
|
||||
|
||||
This APDU is sent to establish a Secure Channel session. A session is aborted when the application is deselected,
|
||||
This APDU is the first step to establish a Secure Channel session. A session is aborted when the application is deselected,
|
||||
either directly or because of a card reset/tear. This APDU and its response are not encrypted.
|
||||
|
||||
The card generates a random 256-bit salt which is sent to the client. Both the client and the card do the following
|
||||
|
@ -43,12 +45,28 @@ for key derivation
|
|||
calculated.
|
||||
3. The output of the SHA-256 algorithm is used as the AES key for further communication.
|
||||
|
||||
TODO: define a second step where client and card mutually verify that they have the same keys and thus are authenticated
|
||||
### MUTUALLY AUTHENTICATE
|
||||
|
||||
* CLA = 0x80
|
||||
* INS = 0x11
|
||||
* P1 = 0x00
|
||||
* P2 = 0x00
|
||||
* Data = 256-bit random number and its SHA-256 hash
|
||||
* Response Data = 256-bit random number and its SHA-256 hash
|
||||
* Response SW = 0x9000 on success, 0x6985 if the previous successfully executed APDU was not OPEN SECURE CHANNEL, 0x6982
|
||||
if authentication failed, 0x6A80 if the data is not exactly 64 bytes long
|
||||
|
||||
This APDU allows both parties to verify that the keys generated in the OPEN SECURE CHANNEL step are matching and thus
|
||||
guarantee authentication of the counterpart. The data sent by both parties is a 32-bit random number followed by its own
|
||||
SHA-256 hash. The APDU data is sent encrypted with the keys generated in the OPEN SECURE CHANNEL step. Each party must
|
||||
verify that the hash indeed matches the value sent. If this is true, then the decryption was correct, meaning that the
|
||||
keys are matching. Only after this step has been executed the secure channel can be considered to be open and other
|
||||
commands can be sent.
|
||||
|
||||
### PAIR
|
||||
|
||||
* CLA = 0x80
|
||||
* INS = 0x11
|
||||
* INS = 0x12
|
||||
* P1 = pairing phase
|
||||
* P2 = 0x00
|
||||
* Data = see below
|
||||
|
@ -89,7 +107,7 @@ happens depend on the specific applet.
|
|||
### UNPAIR
|
||||
|
||||
* CLA = 0x80
|
||||
* INS = 0x12
|
||||
* INS = 0x13
|
||||
* P1 = the index to unpair
|
||||
* P2 = 0x00
|
||||
* Data = the same index as in P1
|
||||
|
|
|
@ -15,8 +15,9 @@ public class SecureChannel {
|
|||
public static final short SC_OUT_OFFSET = ISO7816.OFFSET_CDATA + (SC_BLOCK_SIZE * 2);
|
||||
|
||||
public static final byte INS_OPEN_SECURE_CHANNEL = 0x10;
|
||||
public static final byte INS_PAIR = 0x11;
|
||||
public static final byte INS_UNPAIR = 0x12;
|
||||
public static final byte INS_MUTUALLY_AUTHENTICATE = 0x11;
|
||||
public static final byte INS_PAIR = 0x12;
|
||||
public static final byte INS_UNPAIR = 0x13;
|
||||
|
||||
public static final byte PAIR_P1_FIRST_STEP = 0x00;
|
||||
public static final byte PAIR_P1_LAST_STEP = 0x01;
|
||||
|
@ -34,6 +35,7 @@ public class SecureChannel {
|
|||
private byte[] pairingKeys;
|
||||
|
||||
private short preassignedPairingOffset = -1;
|
||||
private boolean mutuallyAuthenticated = false;
|
||||
|
||||
/**
|
||||
* Instantiates a Secure Channel. All memory allocations needed for the secure channel are performed here. The keypair
|
||||
|
@ -64,6 +66,7 @@ public class SecureChannel {
|
|||
*/
|
||||
public void openSecureChannel(APDU apdu) {
|
||||
preassignedPairingOffset = -1;
|
||||
mutuallyAuthenticated = false;
|
||||
|
||||
apdu.setIncomingAndReceive();
|
||||
byte[] apduBuffer = apdu.getBuffer();
|
||||
|
@ -86,6 +89,39 @@ public class SecureChannel {
|
|||
apdu.setOutgoingAndSend((short) 0, SC_SECRET_LENGTH);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the MUTUALLY AUTHENTICATE command.
|
||||
*
|
||||
* @param apdu the JCRE-owned APDU object.
|
||||
*/
|
||||
public void mutuallyAuthenticate(APDU apdu) {
|
||||
if (!scKey.isInitialized() || mutuallyAuthenticated) {
|
||||
ISOException.throwIt(ISO7816.SW_CONDITIONS_NOT_SATISFIED);
|
||||
}
|
||||
|
||||
apdu.setIncomingAndReceive();
|
||||
byte[] apduBuffer = apdu.getBuffer();
|
||||
|
||||
short len = decryptAPDU(apduBuffer);
|
||||
|
||||
if (len != (short) (SC_SECRET_LENGTH * 2)) {
|
||||
ISOException.throwIt(ISO7816.SW_WRONG_DATA);
|
||||
}
|
||||
|
||||
Crypto.sha256.doFinal(apduBuffer, ISO7816.OFFSET_CDATA, SC_SECRET_LENGTH, apduBuffer, ISO7816.OFFSET_CDATA);
|
||||
|
||||
if (Util.arrayCompare(apduBuffer, ISO7816.OFFSET_CDATA, apduBuffer, (short) (ISO7816.OFFSET_CDATA + SC_SECRET_LENGTH), SC_SECRET_LENGTH) != 0) {
|
||||
ISOException.throwIt(ISO7816.SW_SECURITY_STATUS_NOT_SATISFIED);
|
||||
}
|
||||
|
||||
mutuallyAuthenticated = true;
|
||||
|
||||
Crypto.random.generateData(apduBuffer, SC_OUT_OFFSET, SC_SECRET_LENGTH);
|
||||
Crypto.sha256.doFinal(apduBuffer, SC_OUT_OFFSET, SC_SECRET_LENGTH, apduBuffer, (short) (SC_OUT_OFFSET + SC_SECRET_LENGTH));
|
||||
len = encryptAPDU(apduBuffer, len);
|
||||
apdu.setOutgoingAndSend(ISO7816.OFFSET_CDATA, len);
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes the PAIR command.
|
||||
*
|
||||
|
@ -243,7 +279,7 @@ public class SecureChannel {
|
|||
* @return whether a secure channel is currently established or not.
|
||||
*/
|
||||
public boolean isOpen() {
|
||||
return scKey.isInitialized();
|
||||
return scKey.isInitialized() && mutuallyAuthenticated;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -185,6 +185,9 @@ public class WalletApplet extends Applet {
|
|||
case SecureChannel.INS_OPEN_SECURE_CHANNEL:
|
||||
secureChannel.openSecureChannel(apdu);
|
||||
break;
|
||||
case SecureChannel.INS_MUTUALLY_AUTHENTICATE:
|
||||
secureChannel.mutuallyAuthenticate(apdu);
|
||||
break;
|
||||
case SecureChannel.INS_PAIR:
|
||||
secureChannel.pair(apdu);
|
||||
break;
|
||||
|
|
|
@ -71,13 +71,18 @@ public class SecureChannelSession {
|
|||
* @return the card response
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public ResponseAPDU openSecureChannel(CardChannel apduChannel) throws CardException {
|
||||
CommandAPDU openSecureChannel = new CommandAPDU(0x80, SecureChannel.INS_OPEN_SECURE_CHANNEL, pairingIndex, 0, publicKey);
|
||||
ResponseAPDU response = apduChannel.transmit(openSecureChannel);
|
||||
public void autoOpenSecureChannel(CardChannel apduChannel) throws CardException {
|
||||
ResponseAPDU response = openSecureChannel(apduChannel, pairingIndex, publicKey);
|
||||
byte[] salt = response.getData();
|
||||
|
||||
if (response.getSW() != 0x9000) {
|
||||
throw new CardException("OPEN SECURE CHANNEL failed");
|
||||
}
|
||||
|
||||
MessageDigest md;
|
||||
|
||||
try {
|
||||
MessageDigest md = MessageDigest.getInstance("SHA256", "BC");
|
||||
md = MessageDigest.getInstance("SHA256", "BC");
|
||||
md.update(secret);
|
||||
md.update(pairingKey);
|
||||
sessionKey = new SecretKeySpec(md.digest(salt), "AES");
|
||||
|
@ -86,7 +91,24 @@ public class SecureChannelSession {
|
|||
throw new RuntimeException("Is BouncyCastle in the classpath?", e);
|
||||
}
|
||||
|
||||
return response;
|
||||
random.nextBytes(salt);
|
||||
byte[] digest = md.digest(salt);
|
||||
salt = Arrays.copyOf(salt, SecureChannel.SC_SECRET_LENGTH * 2);
|
||||
System.arraycopy(digest, 0, salt, SecureChannel.SC_SECRET_LENGTH, SecureChannel.SC_SECRET_LENGTH);
|
||||
response = mutuallyAuthenticate(apduChannel, salt);
|
||||
salt = decryptAPDU(response.getData());
|
||||
|
||||
if (response.getSW() != 0x9000) {
|
||||
throw new CardException("MUTUALLY AUTHENTICATE failed");
|
||||
}
|
||||
|
||||
md.update(salt, 0, SecureChannel.SC_SECRET_LENGTH);
|
||||
digest = md.digest();
|
||||
salt = Arrays.copyOfRange(salt, SecureChannel.SC_SECRET_LENGTH, salt.length);
|
||||
|
||||
if (!Arrays.equals(salt, digest)) {
|
||||
throw new CardException("Invalid authentication data from the card");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -153,6 +175,33 @@ public class SecureChannelSession {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a OPEN SECURE CHANNEL APDU.
|
||||
*
|
||||
* @param apduChannel the apdu channel
|
||||
* @param index the P1 parameter
|
||||
* @param data the data
|
||||
* @return the raw card response
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public ResponseAPDU openSecureChannel(CardChannel apduChannel, byte index, byte[] data) throws CardException {
|
||||
CommandAPDU openSecureChannel = new CommandAPDU(0x80, SecureChannel.INS_OPEN_SECURE_CHANNEL, index, 0, data);
|
||||
return apduChannel.transmit(openSecureChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a MUTUALLY AUTHENTICATE APDU.
|
||||
*
|
||||
* @param apduChannel the apdu channel
|
||||
* @param data the data
|
||||
* @return the raw card response
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public ResponseAPDU mutuallyAuthenticate(CardChannel apduChannel, byte[] data) throws CardException {
|
||||
CommandAPDU mutuallyAuthenticate = new CommandAPDU(0x80, SecureChannel.INS_MUTUALLY_AUTHENTICATE, 0, 0, encryptAPDU(data));
|
||||
return apduChannel.transmit(mutuallyAuthenticate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a PAIR APDU.
|
||||
*
|
||||
|
|
|
@ -51,8 +51,8 @@ public class WalletAppletCommandSet {
|
|||
* @return the raw card response
|
||||
* @throws CardException communication error
|
||||
*/
|
||||
public ResponseAPDU openSecureChannel() throws CardException {
|
||||
return secureChannel.openSecureChannel(apduChannel);
|
||||
public void autoOpenSecureChannel() throws CardException {
|
||||
secureChannel.autoOpenSecureChannel(apduChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -73,6 +73,20 @@ public class WalletAppletCommandSet {
|
|||
secureChannel.autoUnpair(apduChannel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a OPEN SECURE CHANNEL APDU. Calls the corresponding method of the SecureChannel class.
|
||||
*/
|
||||
public ResponseAPDU openSecureChannel(byte index, byte[] data) throws CardException {
|
||||
return secureChannel.openSecureChannel(apduChannel, index, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a MUTUALLY AUTHENTICATE APDU. Calls the corresponding method of the SecureChannel class.
|
||||
*/
|
||||
public ResponseAPDU mutuallyAuthenticate(byte[] data) throws CardException {
|
||||
return secureChannel.mutuallyAuthenticate(apduChannel, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a PAIR APDU. Calls the corresponding method of the SecureChannel class.
|
||||
*/
|
||||
|
|
|
@ -115,9 +115,7 @@ public class WalletAppletTest {
|
|||
@Test
|
||||
@DisplayName("OPEN SECURE CHANNEL command")
|
||||
void openSecureChannelTest() throws CardException {
|
||||
ResponseAPDU response = cmdSet.openSecureChannel();
|
||||
assertEquals(0x9000, response.getSW());
|
||||
assertEquals(SecureChannel.SC_SECRET_LENGTH, response.getData().length);
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -126,7 +124,7 @@ public class WalletAppletTest {
|
|||
// Security condition violation: SecureChannel not open
|
||||
ResponseAPDU response = cmdSet.getStatus(WalletApplet.GET_STATUS_P1_APPLICATION);
|
||||
assertEquals(0x6985, response.getSW());
|
||||
cmdSet.openSecureChannel();
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
|
||||
// Good case. Since the order of test execution is undefined, the test cannot know if the keys are initialized or not.
|
||||
// Additionally, support for public key derivation is hw dependent.
|
||||
|
@ -163,7 +161,7 @@ public class WalletAppletTest {
|
|||
ResponseAPDU response = cmdSet.verifyPIN("000000");
|
||||
assertEquals(0x6985, response.getSW());
|
||||
|
||||
cmdSet.openSecureChannel();
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
|
||||
// Wrong PIN
|
||||
response = cmdSet.verifyPIN("123456");
|
||||
|
@ -198,7 +196,7 @@ public class WalletAppletTest {
|
|||
ResponseAPDU response = cmdSet.changePIN("123456");
|
||||
assertEquals(0x6985, response.getSW());
|
||||
|
||||
cmdSet.openSecureChannel();
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
|
||||
// Security condition violation: PIN n ot verified
|
||||
response = cmdSet.changePIN("123456");
|
||||
|
@ -242,7 +240,7 @@ public class WalletAppletTest {
|
|||
ResponseAPDU response = cmdSet.unblockPIN("123456789012", "000000");
|
||||
assertEquals(0x6985, response.getSW());
|
||||
|
||||
cmdSet.openSecureChannel();
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
|
||||
// Condition violation: PIN is not blocked
|
||||
response = cmdSet.unblockPIN("123456789012", "000000");
|
||||
|
@ -294,7 +292,7 @@ public class WalletAppletTest {
|
|||
ResponseAPDU response = cmdSet.loadKey(keyPair);
|
||||
assertEquals(0x6985, response.getSW());
|
||||
|
||||
cmdSet.openSecureChannel();
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
|
||||
int publicKeyDerivationSW = cmdSet.getPublicKeyDerivationSupport() ? 0x9000 : 0x6a81;
|
||||
|
||||
|
@ -351,7 +349,7 @@ public class WalletAppletTest {
|
|||
// Security condition violation: SecureChannel not open
|
||||
ResponseAPDU response = cmdSet.generateMnemonic(4);
|
||||
assertEquals(0x6985, response.getSW());
|
||||
cmdSet.openSecureChannel();
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
|
||||
// Wrong P1 (too short, too long)
|
||||
response = cmdSet.generateMnemonic(3);
|
||||
|
@ -389,7 +387,7 @@ public class WalletAppletTest {
|
|||
ResponseAPDU response = cmdSet.deriveKey(new byte[] {0x00, 0x00, 0x00, 0x00});
|
||||
assertEquals(0x6985, response.getSW());
|
||||
|
||||
cmdSet.openSecureChannel();
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
boolean autonomousDerivation = cmdSet.getPublicKeyDerivationSupport();
|
||||
|
||||
// Security condition violation: PIN is not verified
|
||||
|
@ -509,7 +507,7 @@ public class WalletAppletTest {
|
|||
ResponseAPDU response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true, true);
|
||||
assertEquals(0x6985, response.getSW());
|
||||
|
||||
cmdSet.openSecureChannel();
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
|
||||
// Security condition violation: PIN not verified
|
||||
response = cmdSet.sign(hash, WalletApplet.SIGN_P1_PRECOMPUTED_HASH,true,true);
|
||||
|
@ -558,7 +556,7 @@ public class WalletAppletTest {
|
|||
ResponseAPDU response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02});
|
||||
assertEquals(0x6985, response.getSW());
|
||||
|
||||
cmdSet.openSecureChannel();
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
|
||||
// Security condition violation: PIN not verified
|
||||
response = cmdSet.setPinlessPath(new byte[] {0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x02});
|
||||
|
@ -644,7 +642,7 @@ public class WalletAppletTest {
|
|||
ResponseAPDU response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER);
|
||||
assertEquals(0x6985, response.getSW());
|
||||
|
||||
cmdSet.openSecureChannel();
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
|
||||
// Security condition violation: PIN not verified
|
||||
response = cmdSet.exportKey(WalletApplet.EXPORT_KEY_P1_WHISPER);
|
||||
|
@ -698,7 +696,7 @@ public class WalletAppletTest {
|
|||
byte[] smallData = Arrays.copyOf(data, 20);
|
||||
r.nextBytes(data);
|
||||
|
||||
cmdSet.openSecureChannel();
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
|
||||
ResponseAPDU response = cmdSet.verifyPIN("000000");
|
||||
assertEquals(0x9000, response.getSW());
|
||||
|
@ -798,7 +796,7 @@ public class WalletAppletTest {
|
|||
Credentials wallet2 = WalletUtils.loadCredentials("testwallet", "testwallets/wallet2.json");
|
||||
|
||||
// Load keys on card
|
||||
cmdSet.openSecureChannel();
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
ResponseAPDU response = cmdSet.verifyPIN("000000");
|
||||
assertEquals(0x9000, response.getSW());
|
||||
response = cmdSet.loadKey(wallet1.getEcKeyPair());
|
||||
|
@ -874,7 +872,7 @@ public class WalletAppletTest {
|
|||
private void resetAndSelectAndOpenSC() throws CardException {
|
||||
reset();
|
||||
cmdSet.select();
|
||||
cmdSet.openSecureChannel();
|
||||
cmdSet.autoOpenSecureChannel();
|
||||
}
|
||||
|
||||
private void assertMnemonic(int expectedLength, byte[] data) {
|
||||
|
|
Loading…
Reference in New Issue