Skip to content

Commit

Permalink
Merge PR #105.
Browse files Browse the repository at this point in the history
  • Loading branch information
AdamVe committed Oct 25, 2023
2 parents 9cf2704 + 52ae251 commit 6d2c4f9
Show file tree
Hide file tree
Showing 30 changed files with 2,224 additions and 356 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (C) 2020-2022 Yubico.
* Copyright (C) 2020-2023 Yubico.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -23,7 +23,7 @@
* An error on the CTAP-level, returned from the Authenticator.
* <p>
* These error codes are defined by the
* <a href="https://fidoalliance.org/specs/fido-v2.0-id-20180227/fido-client-to-authenticator-protocol-v2.0-id-20180227.html#error-responses">CTAP2 specification</a>
* <a href="https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#error-responses">CTAP2 specification</a>
*/
public class CtapException extends CommandException {
public static final byte ERR_SUCCESS = 0x00;
Expand Down Expand Up @@ -68,6 +68,11 @@ public class CtapException extends CommandException {
public static final byte ERR_REQUEST_TOO_LARGE = 0x39;
public static final byte ERR_ACTION_TIMEOUT = 0x3A;
public static final byte ERR_UP_REQUIRED = 0x3B;
public static final byte ERR_UV_BLOCKED = 0x3C;
public static final byte ERR_INTEGRITY_FAILURE = 0x3D;
public static final byte ERR_INVALID_SUBCOMMAND = 0x3E;
public static final byte ERR_UV_INVALID = 0x3F;
public static final byte ERR_UNAUTHORIZED_PERMISSION = 0x40;
public static final byte ERR_OTHER = 0x7F;
public static final byte ERR_SPEC_LAST = (byte) 0xDF;
public static final byte ERR_EXTENSION_FIRST = (byte) 0xE0;
Expand Down
24 changes: 13 additions & 11 deletions core/src/main/java/com/yubico/yubikit/core/fido/FidoProtocol.java
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,16 @@ public class FidoProtocol implements Closeable {

public static final byte TYPE_INIT = (byte) 0x80;

private static final byte CMD_PING = TYPE_INIT | 0x01;
private static final byte CMD_APDU = TYPE_INIT | 0x03;
private static final byte CMD_INIT = TYPE_INIT | 0x06;
private static final byte CMD_WINK = TYPE_INIT | 0x08;
private static final byte CMD_CANCEL = TYPE_INIT | 0x11;
public static final byte CTAPHID_PING = TYPE_INIT | 0x01;
public static final byte CTAPHID_MSG = TYPE_INIT | 0x03;
public static final byte CTAPHID_LOCK = TYPE_INIT | 0x04;
public static final byte CTAPHID_INIT = TYPE_INIT | 0x06;
public static final byte CTAPHID_WINK = TYPE_INIT | 0x08;
public static final byte CTAPHID_CBOR = TYPE_INIT | 0x10;
public static final byte CTAPHID_CANCEL = TYPE_INIT | 0x11;

private static final byte STATUS_ERROR = TYPE_INIT | 0x3f;
private static final byte STATUS_KEEPALIVE = TYPE_INIT | 0x3b;
public static final byte CTAPHID_ERROR = TYPE_INIT | 0x3f;
public static final byte CTAPHID_KEEPALIVE = TYPE_INIT | 0x3b;

private final CommandState defaultState = new CommandState();

Expand All @@ -61,7 +63,7 @@ public FidoProtocol(FidoConnection connection) throws IOException {
new SecureRandom().nextBytes(nonce);

channelId = 0xffffffff;
ByteBuffer buffer = ByteBuffer.wrap(sendAndReceive(CMD_INIT, nonce, null));
ByteBuffer buffer = ByteBuffer.wrap(sendAndReceive(CTAPHID_INIT, nonce, null));
byte[] responseNonce = new byte[nonce.length];
buffer.get(responseNonce);
if (!MessageDigest.isEqual(nonce, responseNonce)) {
Expand Down Expand Up @@ -104,7 +106,7 @@ public byte[] sendAndReceive(byte cmd, byte[] payload, @Nullable CommandState st
if (state.waitForCancel(0)) {
Logger.debug(logger, "sending CTAP cancel...");
Arrays.fill(buffer, (byte) 0);
packet.putInt(channelId).put(CMD_CANCEL);
packet.putInt(channelId).put(CTAPHID_CANCEL);
connection.send(buffer);
Logger.trace(logger, "Sent over fido: {}", StringUtils.bytesToHex(buffer));
packet.clear();
Expand All @@ -120,10 +122,10 @@ public byte[] sendAndReceive(byte cmd, byte[] payload, @Nullable CommandState st
byte responseCmd = packet.get();
if (responseCmd == cmd) {
response = ByteBuffer.allocate(packet.getShort());
} else if (responseCmd == STATUS_KEEPALIVE) {
} else if (responseCmd == CTAPHID_KEEPALIVE) {
state.onKeepAliveStatus(packet.get());
continue;
} else if (responseCmd == STATUS_ERROR) {
} else if (responseCmd == CTAPHID_ERROR) {
throw new IOException(String.format("CTAPHID error: %02x", packet.get()));
} else {
throw new IOException(String.format("Wrong response command. Expecting: %x, Got: %x", cmd, responseCmd));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import com.yubico.yubikit.fido.ctap.Ctap2Session;
import com.yubico.yubikit.fido.ctap.PinUvAuthDummyProtocol;
import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV1;
import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV2;
import com.yubico.yubikit.fido.webauthn.AttestationObject;
import com.yubico.yubikit.fido.webauthn.AuthenticatorAttestationResponse;
import com.yubico.yubikit.fido.webauthn.AuthenticatorSelectionCriteria;
Expand All @@ -37,6 +38,8 @@
import com.yubico.yubikit.fido.webauthn.SerializationType;
import com.yubico.yubikit.fido.webauthn.UserVerificationRequirement;

import org.slf4j.LoggerFactory;

import java.io.Closeable;
import java.io.IOException;
import java.security.MessageDigest;
Expand Down Expand Up @@ -68,9 +71,9 @@
@SuppressWarnings("unused")
public class BasicWebAuthnClient implements Closeable {
private static final String OPTION_CLIENT_PIN = "clientPin";
private static final String OPTION_CREDENTIAL_MANAGEMENT = "credentialMgmtPreview";
private static final String OPTION_USER_VERIFICATION = "uv";
private static final String OPTION_RESIDENT_KEY = "rk";
private static final String OPTION_EP = "ep";

private final Ctap2Session ctap;

Expand All @@ -84,7 +87,9 @@ public class BasicWebAuthnClient implements Closeable {
private boolean pinConfigured;
private final boolean uvConfigured;

final private boolean credentialManagementSupported;
final private boolean epSupported;

private static final org.slf4j.Logger logger = LoggerFactory.getLogger(BasicWebAuthnClient.class);

public BasicWebAuthnClient(Ctap2Session session) throws IOException, CommandException {
this.ctap = session;
Expand All @@ -96,8 +101,20 @@ public BasicWebAuthnClient(Ctap2Session session) throws IOException, CommandExce

Boolean clientPin = (Boolean) options.get(OPTION_CLIENT_PIN);
pinSupported = clientPin != null;
if (pinSupported && info.getPinUvAuthProtocols().contains(PinUvAuthProtocolV1.VERSION)) {
this.clientPin = new ClientPin(ctap, new PinUvAuthProtocolV1());

final List<Integer> pinUvAuthProtocols = info.getPinUvAuthProtocols();
if (pinUvAuthProtocols.size() > 0) {
// List of supported PIN/UV auth protocols in order of decreasing authenticator
// preference. MUST NOT contain duplicate values nor be empty if present.
int preferredPinUvAuthProtocol = pinUvAuthProtocols.get(0);

if (pinSupported && preferredPinUvAuthProtocol == PinUvAuthProtocolV2.VERSION) {
this.clientPin = new ClientPin(ctap, new PinUvAuthProtocolV2());
} else if (pinSupported && preferredPinUvAuthProtocol == PinUvAuthProtocolV1.VERSION) {
this.clientPin = new ClientPin(ctap, new PinUvAuthProtocolV1());
} else {
this.clientPin = new ClientPin(ctap, new PinUvAuthDummyProtocol());
}
} else {
this.clientPin = new ClientPin(ctap, new PinUvAuthDummyProtocol());
}
Expand All @@ -107,7 +124,7 @@ public BasicWebAuthnClient(Ctap2Session session) throws IOException, CommandExce
uvSupported = uv != null;
uvConfigured = uvSupported && uv;

credentialManagementSupported = Boolean.TRUE.equals(options.get(OPTION_CREDENTIAL_MANAGEMENT));
epSupported = Boolean.TRUE.equals(options.get(OPTION_EP));
}

@Override
Expand Down Expand Up @@ -135,6 +152,7 @@ public PublicKeyCredential makeCredential(
PublicKeyCredentialCreationOptions options,
String effectiveDomain,
@Nullable char[] pin,
@Nullable Integer enterpriseAttestation,
@Nullable CommandState state
) throws IOException, CommandException, ClientError {
byte[] clientDataHash = hash(clientDataJson);
Expand All @@ -145,6 +163,7 @@ public PublicKeyCredential makeCredential(
options,
effectiveDomain,
pin,
epSupported ? enterpriseAttestation : null,
state
);

Expand All @@ -163,7 +182,7 @@ public PublicKeyCredential makeCredential(
);
} catch (CtapException e) {
if (e.getCtapError() == CtapException.ERR_PIN_INVALID) {
throw new PinInvalidClientError(e, clientPin.getPinRetries());
throw new PinInvalidClientError(e, clientPin.getPinRetries().first);
}
throw ClientError.wrapCtapException(e);
}
Expand Down Expand Up @@ -219,7 +238,7 @@ public PublicKeyCredential getAssertion(

} catch (CtapException e) {
if (e.getCtapError() == CtapException.ERR_PIN_INVALID) {
throw new PinInvalidClientError(e, clientPin.getPinRetries());
throw new PinInvalidClientError(e, clientPin.getPinRetries().first);
}
throw ClientError.wrapCtapException(e);
}
Expand All @@ -243,6 +262,17 @@ public boolean isPinConfigured() {
return pinConfigured;
}

/**
* Check if the Authenticator supports Enterprise Attestation feature.
*
* @return true if the authenticator is enterprise attestation capable and enterprise
* attestation is enabled.
* @see <a href="https://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-errata-20220621.html#sctn-feature-descriptions-enterp-attstn">Enterprise Attestation</a>
*/
public boolean isEpSupported() {
return epSupported;
}

/**
* Set the PIN for an Authenticator which supports PIN, but doesn't have one configured.
*
Expand Down Expand Up @@ -298,15 +328,20 @@ public void changePin(char[] currentPin, char[] newPin) throws IOException, Comm
* @throws CommandException A communication in the protocol layer.
* @throws ClientError A higher level error.
*/
public CredentialManager getCredentialManager(char[] pin) throws IOException, CommandException, ClientError {
if (!credentialManagementSupported) {
throw new ClientError(ClientError.Code.CONFIGURATION_UNSUPPORTED, "Credential management is not supported on this device");
}
public CredentialManager getCredentialManager(char[] pin)
throws IOException, CommandException, ClientError {
if (!pinConfigured) {
throw new ClientError(ClientError.Code.BAD_REQUEST, "No PIN currently configured on this device");
throw new ClientError(ClientError.Code.BAD_REQUEST,
"No PIN currently configured on this device");
}
try {
return new CredentialManager(new CredentialManagement(ctap, clientPin.getPinUvAuth(), clientPin.getPinToken(pin)));
return new CredentialManager(
new CredentialManagement(
ctap,
clientPin.getPinUvAuth(),
clientPin.getPinToken(pin, ClientPin.PIN_PERMISSION_CM, null)
)
);
} catch (CtapException e) {
throw ClientError.wrapCtapException(e);
}
Expand Down Expand Up @@ -336,6 +371,7 @@ protected Ctap2Session.CredentialData ctapMakeCredential(
PublicKeyCredentialCreationOptions options,
String effectiveDomain,
@Nullable char[] pin,
@Nullable Integer enterpriseAttestation,
@Nullable CommandState state
) throws IOException, CommandException, ClientError {

Expand Down Expand Up @@ -384,7 +420,11 @@ protected Ctap2Session.CredentialData ctapMakeCredential(
}

if (pin != null) {
pinToken = clientPin.getPinToken(pin);
pinToken = clientPin.getPinToken(pin, ClientPin.PIN_PERMISSION_MC, rpId);
pinUvAuthParam = clientPin.getPinUvAuth().authenticate(pinToken, clientDataHash);
pinUvAuthProtocol = clientPin.getPinUvAuth().getVersion();
} else if (pinConfigured && ctapOptions.containsKey(OPTION_USER_VERIFICATION)) {
pinToken = clientPin.getUvToken(ClientPin.PIN_PERMISSION_MC, rpId, null);
pinUvAuthParam = clientPin.getPinUvAuth().authenticate(pinToken, clientDataHash);
pinUvAuthProtocol = clientPin.getPinUvAuth().getVersion();
} else if (pinConfigured && !ctapOptions.containsKey(OPTION_USER_VERIFICATION)) {
Expand Down Expand Up @@ -415,6 +455,7 @@ protected Ctap2Session.CredentialData ctapMakeCredential(
ctapOptions.isEmpty() ? null : ctapOptions,
pinUvAuthParam,
pinUvAuthProtocol,
enterpriseAttestation,
state
);
} finally {
Expand Down Expand Up @@ -471,7 +512,11 @@ protected List<Ctap2Session.AssertionData> ctapGetAssertions(
byte[] pinToken = null;
try {
if (pin != null) {
pinToken = clientPin.getPinToken(pin);
pinToken = clientPin.getPinToken(pin, ClientPin.PIN_PERMISSION_GA, rpId);
pinUvAuthParam = clientPin.getPinUvAuth().authenticate(pinToken, clientDataHash);
pinUvAuthProtocol = clientPin.getPinUvAuth().getVersion();
} else if (pinConfigured && ctapOptions.containsKey(OPTION_USER_VERIFICATION)) {
pinToken = clientPin.getUvToken(ClientPin.PIN_PERMISSION_GA, rpId, null);
pinUvAuthParam = clientPin.getPinUvAuth().authenticate(pinToken, clientDataHash);
pinUvAuthProtocol = clientPin.getPinUvAuth().getVersion();
}
Expand All @@ -492,7 +537,7 @@ protected List<Ctap2Session.AssertionData> ctapGetAssertions(
);
} catch (CtapException e) {
if (e.getCtapError() == CtapException.ERR_PIN_INVALID) {
throw new PinInvalidClientError(e, clientPin.getPinRetries());
throw new PinInvalidClientError(e, clientPin.getPinRetries().first);
}
throw ClientError.wrapCtapException(e);
} finally {
Expand All @@ -505,6 +550,7 @@ protected List<Ctap2Session.AssertionData> ctapGetAssertions(
/**
* Returns list of transports the authenticator is believed to support. This can be empty if
* the information is not available.
*
* @return list of transports
*/
protected List<String> getTransports() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ static ClientError wrapCtapException(CtapException error) {
case CtapException.ERR_PIN_TOKEN_EXPIRED:
case CtapException.ERR_PIN_AUTH_INVALID:
case CtapException.ERR_PIN_AUTH_BLOCKED:
case CtapException.ERR_UV_BLOCKED:
case CtapException.ERR_UV_INVALID:
case CtapException.ERR_REQUEST_TOO_LARGE:
case CtapException.ERR_OPERATION_DENIED:
return new ClientError(Code.BAD_REQUEST, error);
Expand Down
Loading

0 comments on commit 6d2c4f9

Please sign in to comment.