Skip to content

Commit

Permalink
Add support for multiple counters.
Browse files Browse the repository at this point in the history
This commit adds support for multiple (8) counters, which as assigned
in a round-robin style to each registered credential. Each counter is
independent and starts at zero. This makes it more difficult for
colluding services to determine which authenticators might be shared
between accounts.

The impetus for this change is well articulated in the [Webauthn
Specification][webauthn]:

> [Authenticators] SHOULD implement per credential signature counters.
> This prevents the signature counter value from being shared between
> Relying Parties and being possibly employed as a correlation handle
> for the user.

[webauthn]: (https://www.w3.org/TR/webauthn/#sign-counter

In order to do this, the `FIDOAPI` interface needed to be modified to
allow the counter index to be stored at registration and recovered at
authentication. Also, the implementation of `FIDOStandalone` was
significantly modified to allow the counter index to be securely
included in the key handle without increasing the key handle length.

Previously, `FIDOStandalone` was storing the entire application
parameter in the key handle. In this change only the CBC-MAC of the
application parameter is stored in the key handle. Of the 16 bytes
that were freed up by doing this, the first of them is used to store
the counter index and the rest are set to zero. No other changes
to the key handle format were made.

While it would have been possible to reduce the key handle length to
49 bytes, this would have a negative impact on privacy since 49 bytes
is an unusual key handle length. 64 byte key handles are very common,
so this change maintains that length.
  • Loading branch information
darconeous committed Mar 20, 2020
1 parent 24b6f13 commit 554b071
Show file tree
Hide file tree
Showing 4 changed files with 223 additions and 102 deletions.
40 changes: 37 additions & 3 deletions src/main/java/com/ledger/u2f/FIDOAPI.java
Original file line number Diff line number Diff line change
Expand Up @@ -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);

}
142 changes: 115 additions & 27 deletions src/main/java/com/ledger/u2f/FIDOStandalone.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,39 +19,47 @@

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;
import javacard.security.RandomData;
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);
Expand All @@ -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;
}
}
45 changes: 0 additions & 45 deletions src/main/java/com/ledger/u2f/FIDOUtils.java

This file was deleted.

Loading

0 comments on commit 554b071

Please sign in to comment.