Skip to content

Commit

Permalink
add support for Bio metadata and verify extensions
Browse files Browse the repository at this point in the history
  • Loading branch information
AdamVe committed May 31, 2024
1 parent f666ed5 commit 0ae247c
Show file tree
Hide file tree
Showing 4 changed files with 300 additions and 0 deletions.
63 changes: 63 additions & 0 deletions piv/src/main/java/com/yubico/yubikit/piv/BioMetadata.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/*
* Copyright (C) 2024 Yubico.
*
* 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.yubico.yubikit.piv;

public class BioMetadata {
final private boolean configured;
final private int attemptsRemaining;
final private boolean temporaryPin;

public BioMetadata(boolean configured, int attemptsRemaining, boolean temporaryPin) {
this.configured = configured;
this.attemptsRemaining = attemptsRemaining;
this.temporaryPin = temporaryPin;
}

/**
* Indicates whether biometrics are configured or not (fingerprints enrolled or not).
* <p>
* A false return value indicates a YubiKey Bio without biometrics configured and hence the
* client should fallback to a PIN based authentication.
*
* @return true if biometrics are configured or not.
*/
public boolean isConfigured() {
return configured;
}

/**
* Returns value of biometric match retry counter which states how many biometric match retries
* are left until a YubiKey Bio is blocked.
* <p>
* If this method returns 0 and {@link #isConfigured()} returns true, the device is blocked for
* biometric match and the client should invoke PIN based authentication to reset the biometric
* match retry counter.
*/
public int getAttemptsRemaining() {
return attemptsRemaining;
}

/**
* Indicates whether a temporary PIN has been generated in the YubiKey in relation to a
* successful biometric match.
*
* @return true if a temporary PIN has been generated.
*/
public boolean hasTemporaryPin() {
return temporaryPin;
}
}
141 changes: 141 additions & 0 deletions piv/src/main/java/com/yubico/yubikit/piv/PivSession.java
Original file line number Diff line number Diff line change
Expand Up @@ -140,10 +140,14 @@ public boolean isSupportedBy(Version version) {
public static final Feature<PivSession> FEATURE_RSA3072_RSA4096 = new Feature.Versioned<>("RSA3072 and RSA4096 keys", 5, 7, 0);

private static final int PIN_LEN = 8;
private static final int TEMPORARY_PIN_LEN = 16;

// Special slot for the Management Key
private static final int SLOT_CARD_MANAGEMENT = 0x9b;

// Special slot for bio metadata
private static final int SLOT_OCC_AUTH = 0x96;

// Instruction set
private static final byte INS_VERIFY = 0x20;
private static final byte INS_CHANGE_REFERENCE = 0x24;
Expand Down Expand Up @@ -184,6 +188,8 @@ public boolean isSupportedBy(Version version) {
private static final int TAG_METADATA_PUBLIC_KEY = 0x04;
private static final int TAG_METADATA_IS_DEFAULT = 0x05;
private static final int TAG_METADATA_RETRIES = 0x06;
private static final int TAG_METADATA_BIO_CONFIGURED = 0x07;
private static final int TAG_METADATA_TEMPORARY_PIN = 0x08;

private static final byte ORIGIN_GENERATED = 1;
private static final byte ORIGIN_IMPORTED = 2;
Expand Down Expand Up @@ -271,6 +277,16 @@ public int getSerialNumber() throws IOException, ApduException {
*/
public void reset() throws IOException, ApduException {
Logger.debug(logger, "Preparing PIV reset");

try {
BioMetadata bioMetadata = getBioMetadata();
if (bioMetadata.isConfigured()) {
throw new IllegalArgumentException("Cannot perform PIV reset when biometrics are configured");
}
} catch (UnsupportedOperationException e) {
// ignored
}

blockPin();
blockPuk();
Logger.debug(logger, "Sending reset");
Expand Down Expand Up @@ -600,6 +616,131 @@ public void verifyPin(char[] pin) throws IOException, ApduException, InvalidPinE
}
}

/**
* Reads metadata specific to YubiKey Bio multi-protocol.
*
* @return metadata about a slot
* @throws IOException in case of connection error
* @throws ApduException in case of an error response from the YubiKey
* @throws UnsupportedOperationException in case the metadata cannot be retrieved
*/
public BioMetadata getBioMetadata() throws IOException, ApduException {
Logger.debug(logger, "Getting bio metadata");
try {
Map<Integer, byte[]> data = Tlvs.decodeMap(
protocol.sendAndReceive(new Apdu(0, INS_GET_METADATA, 0, SLOT_OCC_AUTH, null)));
return new BioMetadata(
data.get(TAG_METADATA_BIO_CONFIGURED)[0] == 1,
data.get(TAG_METADATA_RETRIES)[0],
data.get(TAG_METADATA_TEMPORARY_PIN)[0] == 1
);
} catch (ApduException apduException) {
if (apduException.getSw() == SW.REFERENCED_DATA_NOT_FOUND) {
throw new UnsupportedOperationException("Biometric verification not supported by this YubiKey");
}
throw apduException;
}
}

/**
* Authenticate with YubiKey Bio multi-protocol capabilities.
*
* @param requestTemporaryPin - after successful match generate a temporary PIN
* @param checkOnly - check verification state of biometrics, don't perform UV
* @return temporary pin if requestTemporaryPin is true, otherwise null.
* @throws IOException in case of connection error
* @throws ApduException in case of an error response from the YubiKey
* @throws InvalidPinException in case of unsuccessful match
* @throws IllegalArgumentException in case of invalid key configuration
* @throws UnsupportedOperationException in case bio specific verification is not supported
*/
@Nullable
public byte[] verifyUv(boolean requestTemporaryPin, boolean checkOnly)
throws IOException, ApduException,
com.yubico.yubikit.core.application.InvalidPinException {
if (requestTemporaryPin && checkOnly) {
throw new IllegalArgumentException("Cannot request temporary pin when doing check-only verification");
}

// verify that the authenticator is bio-capable and not blocked for bio matching
BioMetadata bioMetadata = getBioMetadata();
if (bioMetadata.getAttemptsRemaining() == 0) {
throw new IllegalArgumentException("Biometric UV is blocked");
}

try {
final int TAG_GET_TEMPORARY_PIN = 0x02;
final int TAG_VERIFY_UV = 0x03;
byte[] data = null;
if (!checkOnly) {
if (requestTemporaryPin) {
data = new Tlv(TAG_GET_TEMPORARY_PIN, null).getBytes();
} else {
data = new Tlv(TAG_VERIFY_UV, null).getBytes();
}
}

byte[] response = protocol.sendAndReceive(new Apdu(0, INS_VERIFY, 0, SLOT_OCC_AUTH, data));
return requestTemporaryPin ? response : null;
} catch (ApduException e) {
if (e.getSw() == SW.REFERENCED_DATA_NOT_FOUND) {
throw new UnsupportedOperationException("Biometric verification not supported by this YubiKey");
}
int retries = getRetriesFromCode(e.getSw());
if (retries >= 0) {
throw new com.yubico.yubikit.core.application.InvalidPinException(
retries, "Fingerprint mismatch, " + retries + " attempts remaining");
} else {
// status code returned error, not number of retries
throw e;
}
}
}

/**
* Authenticate YubiKey Bio multi-protocol with temporary PIN.
* <p>
* The PIN has to be generated by calling {@link #verifyUv(boolean, boolean)} and is valid only
* for operations during this session and depending on slot {@link PinPolicy}.
*
* @param pin - temporary pin
* @throws IOException in case of connection error
* @throws ApduException in case of an error response from the YubiKey
* @throws InvalidPinException in case of unsuccessful match
* @throws IllegalArgumentException in case of invalid key configuration
* @throws UnsupportedOperationException in case bio specific verification is not supported
*/
public void verifyTemporaryPin(byte[] pin)
throws IOException, ApduException,
com.yubico.yubikit.core.application.InvalidPinException {
if (pin.length != TEMPORARY_PIN_LEN) {
throw new IllegalArgumentException("Temporary PIN must be exactly " + TEMPORARY_PIN_LEN + " bytes");
}

// verify that the authenticator is bio-capable and not blocked for bio matching
BioMetadata bioMetadata = getBioMetadata();
if (bioMetadata.getAttemptsRemaining() == 0) {
throw new IllegalArgumentException("Biometric UV is blocked");
}

try {
final int TAG_VERIFY_TEMPORARY_PIN = 0x01;
protocol.sendAndReceive(new Apdu(0, INS_VERIFY, 0, SLOT_OCC_AUTH, new Tlv(TAG_VERIFY_TEMPORARY_PIN, pin).getBytes()));
} catch (ApduException e) {
if (e.getSw() == SW.REFERENCED_DATA_NOT_FOUND) {
throw new UnsupportedOperationException("Biometric verification not supported by this YubiKey");
}
int retries = getRetriesFromCode(e.getSw());
if (retries >= 0) {
throw new com.yubico.yubikit.core.application.InvalidPinException(
retries, "Invalid temporary PIN, " + retries + " attempts remaining");
} else {
// status code returned error, not number of retries
throw e;
}
}
}

/**
* Receive number of attempts left for PIN from YubiKey
* <p>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright (C) 2024 Yubico.
*
* 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.yubico.yubikit.testing.piv;

import com.yubico.yubikit.testing.framework.PivInstrumentedTests;

import org.junit.Test;

public class PivBioMultiProtocolTests extends PivInstrumentedTests {

@Test
public void testAuthenticate() throws Throwable {
withPivSession(PivBioMultiProtocolDeviceTests::testAuthenticate);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Copyright (C) 2024 Yubico.
*
* 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.yubico.yubikit.testing.piv;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import static org.junit.Assume.assumeNoException;
import static org.junit.Assume.assumeTrue;

import com.yubico.yubikit.core.application.InvalidPinException;
import com.yubico.yubikit.core.smartcard.ApduException;
import com.yubico.yubikit.piv.BioMetadata;
import com.yubico.yubikit.piv.PivSession;

import java.io.IOException;

public class PivBioMultiProtocolDeviceTests {

/**
* Verify authentication with YubiKey Bio Multi-protocol.
* <p>
* To run the test, create a PIN and enroll at least one fingerprint. The test will ask twice
* for fingerprint authentication.
*/
public static void testAuthenticate(PivSession piv) throws IOException, ApduException, InvalidPinException {
try {
BioMetadata bioMetadata = piv.getBioMetadata();

// we have correct key, is it configured?
assumeTrue("Key has no bio multi-protocol functionality", bioMetadata.isConfigured());
assumeTrue("Key has no matches left", bioMetadata.getAttemptsRemaining() > 0);

assertNull(piv.verifyUv(false, false));
assertFalse(piv.getBioMetadata().hasTemporaryPin());

// check verified state
assertNull(piv.verifyUv(false, true));

byte[] pin = piv.verifyUv(true, false);
assertNotNull(pin);
assertTrue(piv.getBioMetadata().hasTemporaryPin());

// check verified state
assertNull(piv.verifyUv(false, true));

piv.verifyTemporaryPin(pin);

} catch (UnsupportedOperationException e) {
assumeNoException("Key has no bio multi-protocol functionality", e);
}
}
}

0 comments on commit 0ae247c

Please sign in to comment.