diff --git a/src/main/java/com/ledger/u2f/FIDOAPI.java b/src/main/java/com/ledger/u2f/FIDOAPI.java index 6df46c8..d85514d 100644 --- a/src/main/java/com/ledger/u2f/FIDOAPI.java +++ b/src/main/java/com/ledger/u2f/FIDOAPI.java @@ -20,11 +20,45 @@ package com.ledger.u2f; import javacard.security.ECPrivateKey; -import javacard.security.ECPublicKey; public interface FIDOAPI { - public short generateKeyAndWrap(byte[] applicationParameter, short applicationParameterOffset, ECPrivateKey generatedPrivateKey, byte[] publicKey, short publicKeyOffset, byte[] keyHandle, short keyHandleOffset); - public boolean unwrap(byte[] keyHandle, short keyHandleOffset, short keyHandleLength, byte[] applicationParameter, short applicationParameterOffset, ECPrivateKey unwrappedPrivateKey); + /** + * Generates a credential tied to this authenticator. + * + * @param applicationParameter Input buffer containing the 32-byte applicaiton parameter. + * @param applicationParameterOffset The offset into applicationParameter at which + * the application parameter starts. + * @param publicKey Output buffer that will hold the generated 65 byte public key. + * @param publicKeyOffset Where in publicKey to start writing. + * @param keyHandle Output buffer that will hold the generated key handle. + * @param keyHandleOffset Where in keyHandle to start writing. + * @param info A byte of information that can be recovered when the key handle is + * unwrapped. This is typically used for authenticators with multiple + * counters. + * @return The length of the generated key handle. + */ + short generateKeyAndWrap(byte[] applicationParameter, short applicationParameterOffset, byte[] publicKey, short publicKeyOffset, byte[] keyHandle, short keyHandleOffset, byte info); + + /** + * Unwraps a previously generated key handle into a private key + * and info byte. + * + * @param keyHandle Input buffer containing the key handle. + * @param keyHandleOffset Offset into buffer where the key + * handle starts. + * @param keyHandleLength The length of the key handle. + * @param applicationParameter Input buffer containing the + * 32-byte applicaiton parameter. + * @param applicationParameterOffset Offset into buffer where the + * applicaiton parameter starts. + * @param unwrappedPrivateKey ECPrivateKey instance to insert unwrapped + * private key into. + * @return The value of the "info" byte from generateKeyAndWrap() + * @throws javacard.framework.ISOException ISO7816.SW_WRONG_DATA if the + * key handle doesn't match this applicaiton parameter or doesn't + * belong to this authenticator. + */ + byte unwrap(byte[] keyHandle, short keyHandleOffset, short keyHandleLength, byte[] applicationParameter, short applicationParameterOffset, ECPrivateKey unwrappedPrivateKey); } diff --git a/src/main/java/com/ledger/u2f/FIDOStandalone.java b/src/main/java/com/ledger/u2f/FIDOStandalone.java index 1579c97..fcace52 100644 --- a/src/main/java/com/ledger/u2f/FIDOStandalone.java +++ b/src/main/java/com/ledger/u2f/FIDOStandalone.java @@ -19,12 +19,13 @@ package com.ledger.u2f; +import javacard.framework.ISO7816; +import javacard.framework.ISOException; import javacard.security.KeyBuilder; import javacard.security.KeyPair; import javacard.security.ECKey; import javacard.security.ECPrivateKey; import javacard.security.ECPublicKey; -import javacard.security.KeyPair; import javacard.security.AESKey; import javacardx.crypto.Cipher; import javacard.framework.JCSystem; @@ -32,26 +33,33 @@ import javacard.framework.Util; public class FIDOStandalone implements FIDOAPI { + // Most U2F authenticators use a key handle length of + // 64 bytes. While we could get away with using a 49 byte + // handle, this would be a tell about what kind of + // authenticator we were using. So to maintain privacy, + // we use a 64-byte key handle just like almost everyone + // else. + private static final short KEY_HANDLE_LENGTH = 64; + // Only used by generateKeyAndWrap(). private KeyPair keyPair; - private AESKey chipKey; + private Cipher cipherEncrypt; private Cipher cipherDecrypt; - private RandomData random; private byte[] scratch; private static final byte[] IV_ZERO_AES = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; public FIDOStandalone() { - scratch = JCSystem.makeTransientByteArray((short)64, JCSystem.CLEAR_ON_DESELECT); + scratch = JCSystem.makeTransientByteArray(KEY_HANDLE_LENGTH, JCSystem.CLEAR_ON_DESELECT); keyPair = new KeyPair( (ECPublicKey)KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PUBLIC, KeyBuilder.LENGTH_EC_FP_256, false), (ECPrivateKey)KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, KeyBuilder.LENGTH_EC_FP_256, false)); Secp256r1.setCommonCurveParameters((ECKey)keyPair.getPrivate()); Secp256r1.setCommonCurveParameters((ECKey)keyPair.getPublic()); - random = RandomData.getInstance(RandomData.ALG_SECURE_RANDOM); + RandomData random = RandomData.getInstance(RandomData.ALG_SECURE_RANDOM); // Initialize the unique wrapping key - chipKey = (AESKey)KeyBuilder.buildKey(KeyBuilder.TYPE_AES, KeyBuilder.LENGTH_AES_256, false); + AESKey chipKey = (AESKey) KeyBuilder.buildKey(KeyBuilder.TYPE_AES, KeyBuilder.LENGTH_AES_256, false); random.generateData(scratch, (short)0, (short)32); chipKey.setKey(scratch, (short)0); cipherEncrypt = Cipher.getInstance(Cipher.ALG_AES_BLOCK_128_CBC_NOPAD, false); @@ -78,34 +86,114 @@ private static void deinterleave(byte[] src, short srcOffset, byte[] array1, sho } } - public short generateKeyAndWrap(byte[] applicationParameter, short applicationParameterOffset, ECPrivateKey generatedPrivateKey, byte[] publicKey, short publicKeyOffset, byte[] keyHandle, short keyHandleOffset) { - // Generate a new pair + public short generateKeyAndWrap(byte[] applicationParameter, short applicationParameterOffset, byte[] publicKey, short publicKeyOffset, byte[] keyHandle, short keyHandleOffset, byte info) { + // Here we are using the cipherEncrypt object as + // a way to calculate a CBC-MAC. In this case we + // will be writing out the encrypted application + // parameter to bytes 16 thru 47 of `scratch`. + // However, we will only be using the last 16 + // bytes---the first 16 bytes will be overwritten + // by the private key a few steps down. + cipherEncrypt.doFinal(applicationParameter, applicationParameterOffset, (short)32, scratch, (short)16); + + // Put our "info" byte as the first byte after + // our CBC-MAC of the application parameter. + scratch[48] = info; + + // Fill bytes 49 through 63 with zeros. + // + // TODO: Would there be any advantage to + // doing a random fill here instead + // of zero fill? + Util.arrayFillNonAtomic(scratch, (short)49, (short)15, (byte)0x00); + + // Generate a new key pair. keyPair.genKeyPair(); - // Copy public key + + // Copy public key out. ((ECPublicKey)keyPair.getPublic()).getW(publicKey, publicKeyOffset); - // Wrap keypair and application parameters + + // Write the private key to bytes 0-31 of + // the scratch memory, overwriting the first + // block of the application parameter we + // encrypted above. This is OK because we + // only care about the later 16 bytes, which + // we will be using as a MAC. ((ECPrivateKey)keyPair.getPrivate()).getS(scratch, (short)0); - interleave(applicationParameter, applicationParameterOffset, scratch, (short)0, keyHandle, keyHandleOffset, (short)32); - cipherEncrypt.doFinal(keyHandle, keyHandleOffset, (short)64, keyHandle, keyHandleOffset); - Util.arrayFillNonAtomic(scratch, (short)0, (short)32, (byte)0x00); - return (short)64; + + // At this point the scratch looks like this: + // + // * bytes 0-31: Private key + // * bytes 32-47: CBC-MAC(chipKey, applicationParameter) + // * byte 48: "Info" byte + // * Bytes 49-63: Zero padding + + // Take the upper and lower parts of scratch + // memory and reversibly mix them together. + interleave(scratch, (short)32, scratch, (short)0, keyHandle, keyHandleOffset, (short)32); + + // Encrypt the mixed buffer using the chipKey. + cipherEncrypt.doFinal(keyHandle, keyHandleOffset, KEY_HANDLE_LENGTH, keyHandle, keyHandleOffset); + + // Zero out the bytes we used in scratch memory. + Util.arrayFillNonAtomic(scratch, (short)0, (short)49, (byte)0x00); + + return KEY_HANDLE_LENGTH; } - public boolean unwrap(byte[] keyHandle, short keyHandleOffset, short keyHandleLength, byte[] applicationParameter, short applicationParameterOffset, ECPrivateKey unwrappedPrivateKey) { - // Verify - cipherDecrypt.doFinal(keyHandle, keyHandleOffset, (short)64, keyHandle, keyHandleOffset); - deinterleave(keyHandle, keyHandleOffset, scratch, (short)0, scratch, (short)32, (short)32); - if (!FIDOUtils.compareConstantTime(applicationParameter, applicationParameterOffset, scratch, (short)0, (short)32)) { - Util.arrayFillNonAtomic(scratch, (short)32, (short)32, (byte)0x00); - Util.arrayFillNonAtomic(keyHandle, keyHandleOffset, (short)64, (byte)0x00); - return false; + public byte unwrap(byte[] keyHandle, short keyHandleOffset, short keyHandleLength, byte[] applicationParameter, short applicationParameterOffset, ECPrivateKey unwrappedPrivateKey) { + // Fail early if the key handle length is obviously wrong. + if (keyHandleLength != KEY_HANDLE_LENGTH) { + ISOException.throwIt(ISO7816.SW_WRONG_DATA); } - Util.arrayFillNonAtomic(keyHandle, keyHandleOffset, (short)64, (byte)0x00); + + // Decrypt the key handle in-place. + cipherDecrypt.doFinal(keyHandle, keyHandleOffset, KEY_HANDLE_LENGTH, keyHandle, keyHandleOffset); + + // Reverse the mixing step that we performed in + // generateKeyAndWrap. + deinterleave(keyHandle, keyHandleOffset, scratch, (short)32, scratch, (short)0, (short)32); + + // At this point the scratch *should* look like this: + // + // * bytes 0-31: Private key + // * bytes 32-47: CBC-MAC(chipKey, applicationParameter) + // * byte 48: "Info" byte + // * Bytes 49-63: Zero padding + + // Save our "info" byte for later. + byte info = scratch[48]; + + // In order to verify that this key handle is for this + // application parameter, we need to calculate the CBC-MAC + // of the application parameter so that we can compare it + // to the CBC-MAC in the decrypted and unmixed key handle. + // Here we encrypt the application parameter, but we will + // be using only the last 16-bytes. We encrypt it into the + // keyHandle buffer since we don't need it anymore. + cipherEncrypt.doFinal(applicationParameter, applicationParameterOffset, (short)32, keyHandle, keyHandleOffset); + + // This is where we actually verify if this key handle + // is for this application parameter on this device. + // We don't need to do a constant-time comparison here + // because we are comparing MAC values---so an attacker + // cannot glean any actionable information from a timing + // attack. + if (0 != Util.arrayCompare(keyHandle, (short)(keyHandleOffset+16), scratch, (short)32, (short)16)) { + // Clean up the buffers we used. + Util.arrayFillNonAtomic(scratch, (short)0, (short)64, (byte)0x00); + Util.arrayFillNonAtomic(keyHandle, keyHandleOffset, KEY_HANDLE_LENGTH, (byte)0x00); + + ISOException.throwIt(ISO7816.SW_WRONG_DATA); + } + if (unwrappedPrivateKey != null) { - unwrappedPrivateKey.setS(scratch, (short)32, (short)32); + unwrappedPrivateKey.setS(scratch, (short)0, (short)32); } - Util.arrayFillNonAtomic(scratch, (short)32, (short)32, (byte)0x00); - return true; - } + // Clean up the buffers we used. + Util.arrayFillNonAtomic(scratch, (short)0, (short)64, (byte)0x00); + Util.arrayFillNonAtomic(keyHandle, keyHandleOffset, KEY_HANDLE_LENGTH, (byte)0x00); + return info; + } } diff --git a/src/main/java/com/ledger/u2f/FIDOUtils.java b/src/main/java/com/ledger/u2f/FIDOUtils.java deleted file mode 100644 index 99e50e6..0000000 --- a/src/main/java/com/ledger/u2f/FIDOUtils.java +++ /dev/null @@ -1,45 +0,0 @@ -/* -******************************************************************************* -* FIDO U2F Authenticator -* (c) 2015 Ledger -* -* Licensed under the Apache License, Version 2.0 (the "License"); -* you may not use this file except in compliance with the License. -* You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -******************************************************************************* -*/ - -package com.ledger.u2f; - -import javacard.security.ECPrivateKey; -import javacard.security.ECPublicKey; - -public class FIDOUtils { - - public static boolean compareConstantTime(byte[] array1, short array1Offset, byte[] array2, short array2Offset, short length) { - short givenLength = length; - byte status = (byte)0; - short counter = (short)0; - - if (length == 0) { - return false; - } - while ((length--) != 0) { - status |= (byte)((array1[(short)(array1Offset + length)]) ^ (array2[(short)(array2Offset + length)])); - counter++; - } - if (counter != givenLength) { - return false; - } - return (status == 0); - } - -} diff --git a/src/main/java/com/ledger/u2f/U2FApplet.java b/src/main/java/com/ledger/u2f/U2FApplet.java index 98e411a..cadc93c 100644 --- a/src/main/java/com/ledger/u2f/U2FApplet.java +++ b/src/main/java/com/ledger/u2f/U2FApplet.java @@ -24,8 +24,11 @@ import javacardx.apdu.ExtendedLength; public class U2FApplet extends Applet implements ExtendedLength { + private static final byte COUNTER_COUNT = 8; + private static final byte COUNTER_MASK = 7; - private Counter counter; + private Counter[] counters; + private byte next_counter; private Presence presence; private byte[] scratch; private byte[] attestationCertificate; @@ -38,14 +41,12 @@ public class U2FApplet extends Applet implements ExtendedLength { private static final byte VERSION[] = { 'U', '2', 'F', '_', 'V', '2' }; - private static final byte FIDO_CLA = (byte)0x00; private static final byte FIDO_INS_ENROLL = (byte)0x01; private static final byte FIDO_INS_SIGN = (byte)0x02; private static final byte FIDO_INS_VERSION = (byte)0x03; private static final byte ISO_INS_GET_DATA = (byte)0xC0; private static final byte FIDO2_INS_NFCCTAP_MSG = (byte)0x10; - private static final byte PROPRIETARY_CLA = (byte)0xF0; private static final byte FIDO_ADM_SET_ATTESTATION_CERT = (byte)0x01; private static final byte SCRATCH_TRANSPORT_STATE = (byte)0; @@ -99,8 +100,20 @@ public U2FApplet(byte[] parameters, short parametersOffset, byte parametersLengt if (parametersLength != 35) { ISOException.throwIt(ISO7816.SW_WRONG_DATA); } - counter = new Counter(); + + // Initialize with 8 counters. + counters = new Counter[] { + new Counter(), new Counter(), + new Counter(), new Counter(), + new Counter(), new Counter(), + new Counter(), new Counter() + }; + + // First counter is counter zero. + next_counter = 0; + scratch = JCSystem.makeTransientByteArray((short)(SCRATCH_PAD + SCRATCH_PAD_SIZE), JCSystem.CLEAR_ON_DESELECT); + try { // ok, let's save RAM localPrivateKey = (ECPrivateKey)KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE_TRANSIENT_DESELECT, KeyBuilder.LENGTH_EC_FP_256, false); @@ -118,8 +131,10 @@ public U2FApplet(byte[] parameters, short parametersOffset, byte parametersLengt Secp256r1.setCommonCurveParameters(localPrivateKey); } } + attestationSignature = Signature.getInstance(Signature.ALG_ECDSA_SHA_256, false); localSignature = Signature.getInstance(Signature.ALG_ECDSA_SHA_256, false); + byte flags = parameters[parametersOffset]; if ((flags & INSTALL_FLAG_DISABLE_USER_PRESENCE) == 0) { @@ -129,10 +144,13 @@ public U2FApplet(byte[] parameters, short parametersOffset, byte parametersLengt } attestationCertificate = new byte[Util.getShort(parameters, (short)(parametersOffset + 1))]; + + // Set up our attestation signature object. ECPrivateKey attestationPrivateKey = (ECPrivateKey) KeyBuilder.buildKey(KeyBuilder.TYPE_EC_FP_PRIVATE, KeyBuilder.LENGTH_EC_FP_256, false); Secp256r1.setCommonCurveParameters(attestationPrivateKey); attestationPrivateKey.setS(parameters, (short)(parametersOffset + 3), (short)32); attestationSignature.init(attestationPrivateKey, Signature.MODE_SIGN); + fidoImpl = new FIDOStandalone(); } @@ -155,8 +173,8 @@ private void handleEnroll(APDU apdu) throws ISOException { short len = apdu.setIncomingAndReceive(); short dataOffset = apdu.getOffsetCdata(); boolean extendedLength = (dataOffset != ISO7816.OFFSET_CDATA); - short outOffset; + // The length of this command must always be 64 bytes. if (len != 64) { ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); } @@ -165,20 +183,28 @@ private void handleEnroll(APDU apdu) throws ISOException { presence.enforce_user_presence(); // Generate the key pair - if (localPrivateTransient) { - Secp256r1.setCommonCurveParameters(localPrivateKey); - } - short keyHandleLength = fidoImpl.generateKeyAndWrap(buffer, (short)(dataOffset + APDU_APPLICATION_PARAMETER_OFFSET), localPrivateKey, scratch, SCRATCH_PUBLIC_KEY_OFFSET, scratch, SCRATCH_KEY_HANDLE_OFFSET); + short keyHandleLength = fidoImpl.generateKeyAndWrap( + buffer, + (short)(dataOffset + APDU_APPLICATION_PARAMETER_OFFSET), + scratch, + SCRATCH_PUBLIC_KEY_OFFSET, + scratch, + SCRATCH_KEY_HANDLE_OFFSET, + (byte)(next_counter++ & COUNTER_MASK) + ); + scratch[SCRATCH_PAD] = ENROLL_LEGACY_VERSION; scratch[SCRATCH_KEY_HANDLE_LENGTH_OFFSET] = (byte)keyHandleLength; + // Prepare the attestation attestationSignature.update(RFU_ENROLL_SIGNED_VERSION, (short)0, (short)1); attestationSignature.update(buffer, (short)(dataOffset + APDU_APPLICATION_PARAMETER_OFFSET), (short)32); attestationSignature.update(buffer, (short)(dataOffset + APDU_CHALLENGE_OFFSET), (short)32); attestationSignature.update(scratch, SCRATCH_KEY_HANDLE_OFFSET, keyHandleLength); attestationSignature.update(scratch, SCRATCH_PUBLIC_KEY_OFFSET, (short)65); - outOffset = (short)(ENROLL_PUBLIC_KEY_OFFSET + 65 + 1 + keyHandleLength); + short signatureSize = attestationSignature.sign(buffer, (short)0, (short)0, scratch, SCRATCH_SIGNATURE_OFFSET); + short outOffset = (short)(ENROLL_PUBLIC_KEY_OFFSET + 65 + 1 + keyHandleLength); if (extendedLength) { // If using extended length, the message can be completed and sent immediately @@ -211,30 +237,40 @@ private void handleSign(APDU apdu) throws ISOException { short keyHandleLength; boolean extendedLength = (dataOffset != ISO7816.OFFSET_CDATA); short outOffset = SCRATCH_PAD; + if (len < 65) { ISOException.throwIt(ISO7816.SW_WRONG_LENGTH); } + switch(p1) { - case P1_ENFORCE_PRESENCE_AND_SIGN: - sign = true; - enforce_presence = true; - break; - case P1_IGNORE_PRESENCE_AND_SIGN: - sign = true; - break; - case P1_SIGN_CHECK_ONLY: - break; - default: - ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); + case P1_ENFORCE_PRESENCE_AND_SIGN: + sign = true; + enforce_presence = true; + break; + case P1_IGNORE_PRESENCE_AND_SIGN: + sign = true; + break; + case P1_SIGN_CHECK_ONLY: + break; + default: + ISOException.throwIt(ISO7816.SW_INCORRECT_P1P2); } - // Verify key handle + if (localPrivateTransient) { Secp256r1.setCommonCurveParameters(localPrivateKey); } + keyHandleLength = (short)(buffer[(short)(dataOffset + 64)] & 0xff); - if (!fidoImpl.unwrap(buffer, (short)(dataOffset + 65), keyHandleLength, buffer, (short)(dataOffset + APDU_APPLICATION_PARAMETER_OFFSET), (sign ? localPrivateKey : null))) { - ISOException.throwIt(FIDO_SW_INVALID_KEY_HANDLE); - } + + // Verify key handle, unwrap our private + // key, and get our counter index. This + // will throw a ISO7816.SW_WRONG_DATA if + // the key handle is bad. + byte counter_index = fidoImpl.unwrap( + buffer, (short)(dataOffset + 65), keyHandleLength, + buffer, (short)(dataOffset + APDU_APPLICATION_PARAMETER_OFFSET), + (sign ? localPrivateKey : null) + ); // If not signing, return with the "correct" exception if (!sign) { @@ -242,20 +278,28 @@ private void handleSign(APDU apdu) throws ISOException { } if (enforce_presence) { + // This will either wait for user presence to be + // asserted or throw ISO7816.SW_CONDITIONS_NOT_SATISFIED. scratch[outOffset++] = presence.enforce_user_presence(); } else { + // This version returns immediately and never + // throws an exception. The returned value will + // indicate user presence if it makes sense for + // this authenticator. scratch[outOffset++] = presence.check_user_presence(); } - // Increase the signature counter + // Increase the signature counter and write it to scratch + Counter counter = counters[counter_index & COUNTER_MASK]; counter.inc(); - outOffset = counter.writeValue(scratch, outOffset); + // Create our signature. localSignature.init(localPrivateKey, Signature.MODE_SIGN); localSignature.update(buffer, (short)(dataOffset + APDU_APPLICATION_PARAMETER_OFFSET), (short)32); localSignature.update(scratch, SCRATCH_PAD, (short)5); outOffset += localSignature.sign(buffer, (short)(dataOffset + APDU_CHALLENGE_OFFSET), (short)32, scratch, outOffset); + if (extendedLength) { // If using extended length, the message can be completed and sent immediately scratch[SCRATCH_TRANSPORT_STATE] = TRANSPORT_EXTENDED;