From 91f0c211c631ece35a9cb322fee07a24f5f8e1e1 Mon Sep 17 00:00:00 2001 From: Bryan Jacobs Date: Sat, 3 Sep 2022 19:57:53 +1000 Subject: [PATCH] Implement HMAC-Secret extension This adds the ability for Android apps (not web pages) to use the hwsecurity library to: - register credentials with the FIDO2 hmac-secret extension enabled - pass salts in to authenticators with getAssertion calls - retrieve decrypted results back from the authenticator It also lays the basic groundwork for supporting PIN Protocol 2, defined as a mandatory part of FIDO2 CTAP2.1. --- build.gradle | 2 +- .../BooleanExtensionParameterValue.java | 36 ++++++++++ .../ByteArrayExtensionParameterValue.java | 37 ++++++++++ .../hw/fido2/domain/ExtensionParameter.java | 42 +++++++++++ .../HmacSecretExtensionParameterValue.java | 49 +++++++++++++ .../domain/create/AuthenticatorData.java | 5 +- .../PublicKeyCredentialCreationOptions.java | 20 +++++- .../get/AuthenticatorAssertionResponse.java | 7 +- .../PublicKeyCredentialRequestOptions.java | 19 ++++- .../internal/ctap2/Ctap2CborSerializer.java | 69 +++++++++++++++++++ .../AuthenticatorGetAssertion.java | 24 +++++-- .../AuthenticatorGetAssertionResponse.java | 9 ++- ...henticatorGetAssertionResponseFactory.java | 20 +++++- .../AuthenticatorMakeCredential.java | 16 ++++- ...nticatorMakeCredentialResponseFactory.java | 1 + .../json/JsonWebauthnOptionsParser.java | 2 +- ...thenticatorGetAssertionCtap1Operation.java | 1 + .../AuthenticatorGetAssertionOperation.java | 18 +++-- .../AuthenticatorMakeCredentialOperation.java | 6 +- .../internal/pinauth/PinAuthCryptoUtil.java | 38 +++++++--- .../fido2/internal/pinauth/PinProtocol.java | 20 ++++++ .../fido2/internal/pinauth/PinProtocolV1.java | 31 +++++++-- .../hw/fido2/internal/pinauth/PinToken.java | 31 ++++++++- .../webauthn/AuthenticatorDataParser.java | 43 ++++++++++-- .../cotech/hw/fido2/Fido2SecurityKeyTest.java | 2 +- 25 files changed, 494 insertions(+), 54 deletions(-) create mode 100644 hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/BooleanExtensionParameterValue.java create mode 100644 hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/ByteArrayExtensionParameterValue.java create mode 100644 hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/ExtensionParameter.java create mode 100644 hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/HmacSecretExtensionParameterValue.java create mode 100644 hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinProtocol.java diff --git a/build.gradle b/build.gradle index bcaa38b..573d1e3 100644 --- a/build.gradle +++ b/build.gradle @@ -27,5 +27,5 @@ allprojects { ext { compileSdkVersion = 29 - hwSdkVersionName = '4.4.0' + hwSdkVersionName = '4.5.0' } diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/BooleanExtensionParameterValue.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/BooleanExtensionParameterValue.java new file mode 100644 index 0000000..13fc2dc --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/BooleanExtensionParameterValue.java @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2018-2021 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain; + +import com.google.auto.value.AutoValue; + +@AutoValue +public abstract class BooleanExtensionParameterValue implements ExtensionParameter.ExtensionParameterValue { + public abstract boolean value(); + + public static BooleanExtensionParameterValue create(boolean value) { + return new AutoValue_BooleanExtensionParameterValue(value); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/ByteArrayExtensionParameterValue.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/ByteArrayExtensionParameterValue.java new file mode 100644 index 0000000..976b6b7 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/ByteArrayExtensionParameterValue.java @@ -0,0 +1,37 @@ +/* + * Copyright (C) 2018-2021 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain; + +import com.google.auto.value.AutoValue; + +@AutoValue +public abstract class ByteArrayExtensionParameterValue implements ExtensionParameter.ExtensionParameterValue { + @SuppressWarnings("mutable") + public abstract byte[] value(); + + public static ByteArrayExtensionParameterValue create(byte[] value) { + return new AutoValue_ByteArrayExtensionParameterValue(value); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/ExtensionParameter.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/ExtensionParameter.java new file mode 100644 index 0000000..a23562f --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/ExtensionParameter.java @@ -0,0 +1,42 @@ +/* + * Copyright (C) 2018-2021 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain; +import android.os.Parcelable; +import com.google.auto.value.AutoValue; + +@AutoValue +public abstract class ExtensionParameter implements Parcelable { + + public interface ExtensionParameterValue extends Parcelable { + } + + public abstract String key(); + @SuppressWarnings("mutable") + public abstract ExtensionParameterValue value(); + + public static ExtensionParameter create(String key, ExtensionParameterValue value) { + return new AutoValue_ExtensionParameter(key, value); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/HmacSecretExtensionParameterValue.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/HmacSecretExtensionParameterValue.java new file mode 100644 index 0000000..7505f12 --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/HmacSecretExtensionParameterValue.java @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2018-2021 Confidential Technologies GmbH + * + * You can purchase a commercial license at https://hwsecurity.dev. + * Buying such a license is mandatory as soon as you develop commercial + * activities involving this program without disclosing the source code + * of your own applications. + * + * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +package de.cotech.hw.fido2.domain; + +import androidx.annotation.Nullable; + +import com.google.auto.value.AutoValue; + +@AutoValue +public abstract class HmacSecretExtensionParameterValue implements ExtensionParameter.ExtensionParameterValue { + @SuppressWarnings("mutable") + public abstract byte[] salt1(); + + @Nullable + @SuppressWarnings("mutable") + public abstract byte[] salt2(); + + public static HmacSecretExtensionParameterValue create(byte[] salt1, @Nullable byte[] salt2) { + if (salt1.length != 32) { + throw new IllegalArgumentException("HMAC salt1 must be 32 bytes in length exactly"); + } + if (salt2 != null && salt2.length != 32) { + throw new IllegalArgumentException("HMAC salt2 must be 32 bytes in length exactly"); + } + return new AutoValue_HmacSecretExtensionParameterValue(salt1, salt2); + } +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AuthenticatorData.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AuthenticatorData.java index 263e81a..01a59a6 100644 --- a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AuthenticatorData.java +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/AuthenticatorData.java @@ -27,6 +27,7 @@ import androidx.annotation.Nullable; import com.google.auto.value.AutoValue; +import de.cotech.hw.fido2.internal.cbor_java.model.Map; @AutoValue @@ -69,7 +70,7 @@ public abstract class AuthenticatorData { // extensions variable (if present) Extension-defined authenticator data. This is a CBOR [RFC7049] map with extension identifiers as keys, and authenticator extension outputs as values. See §9 WebAuthn Extensions for details. @Nullable @SuppressWarnings("mutable") - public abstract byte[] extensions(); + public abstract Map extensions(); public boolean hasAttestedCredentialData() { return (flags() & FLAG_ATTESTED_CREDENTIAL_DATA) != 0; @@ -80,7 +81,7 @@ public boolean hasExtensionData() { } public static AuthenticatorData create(byte[] rpIdHash, byte flags, int sigCounter, - AttestedCredentialData credentialData, byte[] extensions) { + AttestedCredentialData credentialData, Map extensions) { return new AutoValue_AuthenticatorData(rpIdHash, flags, sigCounter, credentialData, extensions); } diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/PublicKeyCredentialCreationOptions.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/PublicKeyCredentialCreationOptions.java index daea5d6..f06d7db 100644 --- a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/PublicKeyCredentialCreationOptions.java +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/create/PublicKeyCredentialCreationOptions.java @@ -31,6 +31,8 @@ import androidx.annotation.Nullable; import com.google.auto.value.AutoValue; + +import de.cotech.hw.fido2.domain.ExtensionParameter; import de.cotech.hw.fido2.domain.PublicKeyCredentialDescriptor; import de.cotech.hw.fido2.domain.PublicKeyCredentialParameters; import de.cotech.hw.fido2.domain.PublicKeyCredentialRpEntity; @@ -50,6 +52,8 @@ public abstract class PublicKeyCredentialCreationOptions implements Parcelable { @Nullable public abstract List excludeCredentials(); public abstract AttestationConveyancePreference attestation(); + @Nullable + public abstract List extensionParameters(); public static PublicKeyCredentialCreationOptions create( PublicKeyCredentialRpEntity rp, @@ -60,9 +64,23 @@ public static PublicKeyCredentialCreationOptions create( AuthenticatorSelectionCriteria authenticatorSelection, @Nullable List excludeCredentials, AttestationConveyancePreference attestation + ) { + return create(rp, user, challenge, pubKeyCredParams, timeout, authenticatorSelection, excludeCredentials, attestation); + } + + public static PublicKeyCredentialCreationOptions create( + PublicKeyCredentialRpEntity rp, + PublicKeyCredentialUserEntity user, + byte[] challenge, + List pubKeyCredParams, + @Nullable Long timeout, + AuthenticatorSelectionCriteria authenticatorSelection, + @Nullable List excludeCredentials, + AttestationConveyancePreference attestation, + @Nullable List extensionParameters ) { return new AutoValue_PublicKeyCredentialCreationOptions( rp, user, challenge, pubKeyCredParams, timeout, - authenticatorSelection, excludeCredentials, attestation); + authenticatorSelection, excludeCredentials, attestation, extensionParameters); } } diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/get/AuthenticatorAssertionResponse.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/get/AuthenticatorAssertionResponse.java index 200ebb2..c6be6ce 100644 --- a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/get/AuthenticatorAssertionResponse.java +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/get/AuthenticatorAssertionResponse.java @@ -39,8 +39,11 @@ public abstract class AuthenticatorAssertionResponse extends AuthenticatorRespon @Nullable @SuppressWarnings("mutable") public abstract byte[] userHandle(); + @Nullable + @SuppressWarnings("mutable") + public abstract byte[] hmacSecretData(); - public static AuthenticatorAssertionResponse create(byte[] clientDataJson, byte[] authenticatorData, byte[] signature, @Nullable byte[] userHandle) { - return new AutoValue_AuthenticatorAssertionResponse(clientDataJson, authenticatorData, signature, userHandle); + public static AuthenticatorAssertionResponse create(byte[] clientDataJson, byte[] authenticatorData, byte[] signature, @Nullable byte[] userHandle, @Nullable byte[] hmacSecretData) { + return new AutoValue_AuthenticatorAssertionResponse(clientDataJson, authenticatorData, signature, userHandle, hmacSecretData); } } diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/get/PublicKeyCredentialRequestOptions.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/get/PublicKeyCredentialRequestOptions.java index 1d3cca5..6c7d046 100644 --- a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/get/PublicKeyCredentialRequestOptions.java +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/domain/get/PublicKeyCredentialRequestOptions.java @@ -31,6 +31,8 @@ import androidx.annotation.Nullable; import com.google.auto.value.AutoValue; + +import de.cotech.hw.fido2.domain.ExtensionParameter; import de.cotech.hw.fido2.domain.PublicKeyCredentialDescriptor; import de.cotech.hw.fido2.domain.UserVerificationRequirement; @@ -47,6 +49,8 @@ public abstract class PublicKeyCredentialRequestOptions implements Parcelable { public abstract List allowCredentials(); @Nullable public abstract UserVerificationRequirement userVerification(); + @Nullable + public abstract List extensionParameters(); public static PublicKeyCredentialRequestOptions create( byte[] challenge, @@ -54,8 +58,21 @@ public static PublicKeyCredentialRequestOptions create( String rpId, List allowCredentials, UserVerificationRequirement userVerification + ) { + return create(challenge, timeout, rpId, allowCredentials, userVerification, null); + } + + public static PublicKeyCredentialRequestOptions create( + byte[] challenge, + Long timeout, + String rpId, + List allowCredentials, + UserVerificationRequirement userVerification, + @Nullable + List extensionParameters ) { return new AutoValue_PublicKeyCredentialRequestOptions( - challenge, timeout, rpId, allowCredentials, userVerification); + challenge, timeout, rpId, allowCredentials, userVerification, + extensionParameters); } } diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2CborSerializer.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2CborSerializer.java index c152be9..4560caa 100644 --- a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2CborSerializer.java +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/Ctap2CborSerializer.java @@ -30,6 +30,10 @@ import java.util.List; import java.util.Map.Entry; +import de.cotech.hw.fido2.domain.BooleanExtensionParameterValue; +import de.cotech.hw.fido2.domain.ByteArrayExtensionParameterValue; +import de.cotech.hw.fido2.domain.ExtensionParameter; +import de.cotech.hw.fido2.domain.HmacSecretExtensionParameterValue; import de.cotech.hw.fido2.internal.cbor_java.CborBuilder; import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; import de.cotech.hw.fido2.internal.cbor_java.CborEncoder; @@ -51,6 +55,8 @@ import de.cotech.hw.fido2.domain.PublicKeyCredentialUserEntity; import de.cotech.hw.fido2.internal.ctap2.commands.makeCredential.AuthenticatorMakeCredential.AuthenticatorMakeCredentialOptions; import de.cotech.hw.fido2.internal.ctap2.commands.rawCommand.RawCtap2Command; +import de.cotech.hw.fido2.internal.pinauth.PinToken; +import de.cotech.hw.util.Arrays; class Ctap2CborSerializer { @@ -126,6 +132,13 @@ private void writeToBuilder(CborBuilder cborBuilder2, AuthenticatorMakeCredentia arrayBuilder.end(); } // extensions 0x06 CBOR definite length map (CBOR major type 5). + List extensionParameters = amc.extensions(); + if (extensionParameters != null) { + MapBuilder mapBuilder = cborBuilder.putMap(0x06); + for (ExtensionParameter param : extensionParameters) { + writeToMap(mapBuilder, param); + } + } // options 0x07 CBOR definite length map (CBOR major type 5). AuthenticatorMakeCredentialOptions options = amc.options(); @@ -176,6 +189,13 @@ private void writeToBuilder(CborBuilder cborBuilder2, AuthenticatorGetAssertion } } // extensions 0x04 CBOR definite length map (CBOR major type 5). + List extensionParameters = aga.extensions(); + if (extensionParameters != null) { + MapBuilder mapBuilder = cborBuilder.putMap(0x04); + for (ExtensionParameter param : extensionParameters) { + writeToMap(mapBuilder, param, aga); + } + } // options 0x05 CBOR definite length map (CBOR major type 5). // pinAuth 0x06 byte string (CBOR major type 2). @@ -265,4 +285,53 @@ private void writeToMap(MapBuilder mapBuilder, PublicKeyCredentialDescriptor } } } + + private void writeToMap(MapBuilder mapBuilder, ExtensionParameter extensionParameters) { + Object value = extensionParameters.value(); + if (value instanceof BooleanExtensionParameterValue) { + mapBuilder.put(extensionParameters.key(), ((BooleanExtensionParameterValue) value).value()); + } else if (value instanceof ByteArrayExtensionParameterValue) { + mapBuilder.put(extensionParameters.key(), ((ByteArrayExtensionParameterValue) value).value()); + } else { + throw new UnsupportedOperationException(); + } + } + + private void writeToMap(MapBuilder mapBuilder, ExtensionParameter extensionParameters, AuthenticatorGetAssertion aga) { + // Handle parameters which require assertion-specific data here + Object value = extensionParameters.value(); + if (value instanceof HmacSecretExtensionParameterValue) { + HmacSecretExtensionParameterValue param = (HmacSecretExtensionParameterValue) value; + MapBuilder hmacParamsMap = mapBuilder.putMap(extensionParameters.key()); + + PinToken pinToken = aga.pinToken(); + if (pinToken == null) { + throw new UnsupportedOperationException("An HMAC secret cannot be requested without PIN auth"); + } + + byte[] salts; + if (param.salt2() == null) { + salts = param.salt1(); + } else { + salts = Arrays.concatenate(param.salt1(), param.salt2()); + } + + byte[] saltEnc = pinToken.encrypt(salts); + + try { + hmacParamsMap.put(Ctap2CborConstants.CBOR_ONE, new CborDecoder(new ByteArrayInputStream(pinToken.platformKeyAgreementKey())).decodeNext()); + } catch (CborException e) { + throw new IllegalArgumentException(e); + } // keyAgreement + hmacParamsMap.put(0x02, saltEnc); // saltEnc + hmacParamsMap.put(0x03, pinToken.authenticate(saltEnc)); // saltAuth + if (pinToken.pinProtocol().version() != 1) { // pinUvAuthProtocol + hmacParamsMap.put(0x04, pinToken.pinProtocol().version()); + } + + return; + } + + writeToMap(mapBuilder, extensionParameters); + } } diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertion.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertion.java index 5880fe0..0d47fb8 100644 --- a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertion.java +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertion.java @@ -29,10 +29,13 @@ import androidx.annotation.Nullable; import com.google.auto.value.AutoValue; + +import de.cotech.hw.fido2.domain.ExtensionParameter; import de.cotech.hw.fido2.domain.PublicKeyCredentialDescriptor; import de.cotech.hw.fido2.internal.ctap2.Ctap2Command; import de.cotech.hw.fido2.internal.ctap2.Ctap2ResponseFactory; import de.cotech.hw.fido2.internal.ctap2.commands.getInfo.AuthenticatorOptions; +import de.cotech.hw.fido2.internal.pinauth.PinToken; @AutoValue @@ -50,7 +53,7 @@ public abstract class AuthenticatorGetAssertion extends Ctap2Command extensions(); // options 0x05 CBOR definite length map (CBOR major type 5). @Nullable abstract AuthenticatorOptions options(); @@ -61,13 +64,26 @@ public abstract class AuthenticatorGetAssertion extends Ctap2Command allowCredentials, AuthenticatorOptions options) { - return new AutoValue_AuthenticatorGetAssertion(COMMAND_GET_ASSERTION, rpId, clientDataHash, clientDataJson, allowCredentials, null, options, null, null); + return new AutoValue_AuthenticatorGetAssertion(COMMAND_GET_ASSERTION, rpId, clientDataHash, clientDataJson, allowCredentials, null, options, null, null, null); + } + + public static AuthenticatorGetAssertion create(String rpId, byte[] clientDataHash, String clientDataJson, List allowCredentials, List extensionParameters, AuthenticatorOptions options) { + return new AutoValue_AuthenticatorGetAssertion(COMMAND_GET_ASSERTION, rpId, clientDataHash, clientDataJson, allowCredentials, extensionParameters, options, null, null, null); + } + + public static AuthenticatorGetAssertion create(String rpId, byte[] clientDataHash, String clientDataJson, List allowCredentials, AuthenticatorOptions options, PinToken pinToken) { + byte[] pinAuth = pinToken.calculatePinAuth(clientDataHash); + return new AutoValue_AuthenticatorGetAssertion(COMMAND_GET_ASSERTION, rpId, clientDataHash, clientDataJson, allowCredentials, null, options, pinAuth, pinToken.pinProtocol().version(), pinToken); } - public static AuthenticatorGetAssertion create(String rpId, byte[] clientDataHash, String clientDataJson, List allowCredentials, AuthenticatorOptions options, byte[] pinAuth, Integer pinProtocol) { - return new AutoValue_AuthenticatorGetAssertion(COMMAND_GET_ASSERTION, rpId, clientDataHash, clientDataJson, allowCredentials, null, options, pinAuth, pinProtocol); + public static AuthenticatorGetAssertion create(String rpId, byte[] clientDataHash, String clientDataJson, List allowCredentials, List extensionParameters, AuthenticatorOptions options, PinToken pinToken) { + byte[] pinAuth = pinToken.calculatePinAuth(clientDataHash); + return new AutoValue_AuthenticatorGetAssertion(COMMAND_GET_ASSERTION, rpId, clientDataHash, clientDataJson, allowCredentials, extensionParameters, options, pinAuth, pinToken.pinProtocol().version(), pinToken); } @Override diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertionResponse.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertionResponse.java index 6ed0a05..72484a7 100644 --- a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertionResponse.java +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertionResponse.java @@ -53,6 +53,10 @@ public abstract class AuthenticatorGetAssertionResponse extends Ctap2Response { @SuppressWarnings("mutable") public abstract byte[] clientDataJSON(); + @Nullable + @SuppressWarnings("mutable") + public abstract byte[] hmacSecretData(); + public static AuthenticatorGetAssertionResponse create( @Nullable byte[] credential, @@ -60,8 +64,9 @@ public static AuthenticatorGetAssertionResponse create( byte[] signature, @Nullable PublicKeyCredentialUserEntity user, @Nullable Integer numberOfCredentials, - byte[] clientDataJSON + byte[] clientDataJSON, + @Nullable byte[] hmacSecretData ) { - return new AutoValue_AuthenticatorGetAssertionResponse(credential, authData, signature, user, numberOfCredentials, clientDataJSON); + return new AutoValue_AuthenticatorGetAssertionResponse(credential, authData, signature, user, numberOfCredentials, clientDataJSON, hmacSecretData); } } diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertionResponseFactory.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertionResponseFactory.java index 1563e48..99c9f30 100644 --- a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertionResponseFactory.java +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/getAssertion/AuthenticatorGetAssertionResponseFactory.java @@ -28,6 +28,7 @@ import java.io.IOException; import java.util.List; +import de.cotech.hw.fido2.domain.create.AuthenticatorData; import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; import de.cotech.hw.fido2.internal.cbor_java.CborException; import de.cotech.hw.fido2.internal.cbor_java.model.ByteString; @@ -40,11 +41,14 @@ import de.cotech.hw.fido2.internal.cbor.CborUtils; import de.cotech.hw.fido2.internal.ctap2.Ctap2CborConstants; import de.cotech.hw.fido2.internal.ctap2.Ctap2ResponseFactory; +import de.cotech.hw.fido2.internal.webauthn.AuthenticatorDataParser; public class AuthenticatorGetAssertionResponseFactory implements Ctap2ResponseFactory { private final AuthenticatorGetAssertion authenticatorGetAssertion; + private static final AuthenticatorDataParser authenticatorDataParser = new AuthenticatorDataParser(); + private static final UnicodeString HMAC_SECRET_EXTENSION_ID = new UnicodeString("hmac-secret"); AuthenticatorGetAssertionResponseFactory(AuthenticatorGetAssertion authenticatorGetAssertion) { this.authenticatorGetAssertion = authenticatorGetAssertion; @@ -76,13 +80,27 @@ private AuthenticatorGetAssertionResponse readAuthenticatorGetAssertionResponse( PublicKeyCredentialUserEntity publicKeyCredentialUserEntity = readPublicKeyCredentialUserEntity(user); + byte[] decryptedHmacSecretValue = null; + AuthenticatorData decodedAuthData = authenticatorDataParser.fromBytes(authData.getBytes()); + if (decodedAuthData.hasExtensionData()) { + DataItem hmacSecretValue = decodedAuthData.extensions().get(HMAC_SECRET_EXTENSION_ID); + if (hmacSecretValue != null) { + if (hmacSecretValue.getMajorType() != MajorType.BYTE_STRING) { + throw new IOException("hmac-secret response is not a byte string"); + } + byte[] hmacSecretBytes = ((ByteString) hmacSecretValue).getBytes(); + decryptedHmacSecretValue = authenticatorGetAssertion.pinToken().decrypt(hmacSecretBytes); + } + } + return AuthenticatorGetAssertionResponse.create( credential != null ? CborUtils.writeCborDataToBytes(credential) : null, authData.getBytes(), signature.getBytes(), publicKeyCredentialUserEntity, numberOfCredentials != null ? numberOfCredentials.getValue().intValue() : null, - authenticatorGetAssertion.clientDataJson().getBytes() + authenticatorGetAssertion.clientDataJson().getBytes(), + decryptedHmacSecretValue ); } diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/makeCredential/AuthenticatorMakeCredential.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/makeCredential/AuthenticatorMakeCredential.java index 6ef783c..d87be87 100644 --- a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/makeCredential/AuthenticatorMakeCredential.java +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/makeCredential/AuthenticatorMakeCredential.java @@ -29,6 +29,8 @@ import androidx.annotation.Nullable; import com.google.auto.value.AutoValue; + +import de.cotech.hw.fido2.domain.ExtensionParameter; import de.cotech.hw.fido2.internal.ctap2.Ctap2Command; import de.cotech.hw.fido2.domain.PublicKeyCredentialDescriptor; import de.cotech.hw.fido2.domain.PublicKeyCredentialParameters; @@ -60,7 +62,8 @@ public abstract class AuthenticatorMakeCredential extends Ctap2Command excludeList(); // CBOR map of extension identifier → authenticator extension input values Optional Parameters to influence authenticator operation, as specified in [WebAuthN]. These parameters might be authenticator specific. - // public abstract int extensions(); + @Nullable + public abstract List extensions(); // Map of authenticator options Optional Parameters to influence authenticator operation, as specified in in the table below. @Nullable @@ -114,6 +117,15 @@ public static AuthenticatorMakeCredential create(byte[] clientDataHash, String c List excludeList, AuthenticatorMakeCredentialOptions options, byte[] pinAuth, Integer pinProtocol) { return new AutoValue_AuthenticatorMakeCredential(Ctap2Command.COMMAND_MAKE_CREDENTIAL, clientDataHash, clientDataJson, rp, user, - pubKeyCredParams, excludeList, options, pinAuth, pinProtocol); + pubKeyCredParams, excludeList, null, options, pinAuth, pinProtocol); + } + + public static AuthenticatorMakeCredential create(byte[] clientDataHash, String clientDataJson, + PublicKeyCredentialRpEntity rp, PublicKeyCredentialUserEntity user, List pubKeyCredParams, + List excludeList, + List extensionParameters, + AuthenticatorMakeCredentialOptions options, byte[] pinAuth, Integer pinProtocol) { + return new AutoValue_AuthenticatorMakeCredential(Ctap2Command.COMMAND_MAKE_CREDENTIAL, clientDataHash, clientDataJson, rp, user, + pubKeyCredParams, excludeList, extensionParameters, options, pinAuth, pinProtocol); } } diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/makeCredential/AuthenticatorMakeCredentialResponseFactory.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/makeCredential/AuthenticatorMakeCredentialResponseFactory.java index 6ff9fd0..7f003d0 100644 --- a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/makeCredential/AuthenticatorMakeCredentialResponseFactory.java +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/ctap2/commands/makeCredential/AuthenticatorMakeCredentialResponseFactory.java @@ -68,4 +68,5 @@ public AuthenticatorMakeCredentialResponse createResponse(byte[] rawResponseData throw new IOException(e); } } + } diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/json/JsonWebauthnOptionsParser.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/json/JsonWebauthnOptionsParser.java index f84a158..fab0648 100644 --- a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/json/JsonWebauthnOptionsParser.java +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/json/JsonWebauthnOptionsParser.java @@ -69,7 +69,7 @@ private PublicKeyCredentialCreationOptions fromJsonMakeCredential(JSONObject jso publicKeyObject.optJSONObject("authenticatorSelection")); return PublicKeyCredentialCreationOptions.create(rp, user, challenge, pubKeyCredParams, timeout, - authenticatorSelection, null, attestationConveyancePreference); + authenticatorSelection, null, attestationConveyancePreference, null); } private List jsonToPubKeyCredParamsList( diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap1/AuthenticatorGetAssertionCtap1Operation.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap1/AuthenticatorGetAssertionCtap1Operation.java index 0a40b76..187eb16 100644 --- a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap1/AuthenticatorGetAssertionCtap1Operation.java +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap1/AuthenticatorGetAssertionCtap1Operation.java @@ -115,6 +115,7 @@ private PublicKeyCredential ctap1ResponseApduToWebauthnResponse( authenticatorGetAssertion.clientDataJson().getBytes(), authenticatorDataBytes, u2fResponse.signature(), + null, null ); diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap2/AuthenticatorGetAssertionOperation.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap2/AuthenticatorGetAssertionOperation.java index 619ed1d..0174c75 100644 --- a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap2/AuthenticatorGetAssertionOperation.java +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap2/AuthenticatorGetAssertionOperation.java @@ -46,6 +46,7 @@ import de.cotech.hw.fido2.exceptions.FidoSecurityError; import de.cotech.hw.fido2.internal.Fido2AppletConnection; import de.cotech.hw.fido2.internal.cbor.CborPublicKeyCredentialDescriptorParser; +import de.cotech.hw.fido2.internal.cbor_java.model.UnicodeString; import de.cotech.hw.fido2.internal.ctap2.Ctap2Exception; import de.cotech.hw.fido2.internal.ctap2.CtapErrorResponse; import de.cotech.hw.fido2.internal.ctap2.commands.getAssertion.AuthenticatorGetAssertion; @@ -55,6 +56,7 @@ import de.cotech.hw.fido2.internal.pinauth.PinProtocolV1; import de.cotech.hw.fido2.internal.pinauth.PinToken; import de.cotech.hw.fido2.internal.utils.RelyingPartyIdUtils; +import de.cotech.hw.fido2.internal.webauthn.AuthenticatorDataParser; import de.cotech.hw.util.HashUtil; import de.cotech.hw.util.HwTimber; @@ -67,6 +69,7 @@ public class AuthenticatorGetAssertionOperation extends private final PinProtocolV1 pinProtocolV1; private final JsonCollectedClientDataSerializer jsonCollectedClientDataSerializer; private final RelyingPartyIdUtils relyingPartyIdUtils; + private final AuthenticatorDataParser authenticatorDataParser = new AuthenticatorDataParser(); public AuthenticatorGetAssertionOperation( CborPublicKeyCredentialDescriptorParser cborPublicKeyCredentialDescriptorParser, @@ -96,6 +99,7 @@ public PublicKeyCredential performWebauthnSecurityKeyOperation( try { AuthenticatorGetAssertionResponse response = fido2AppletConnection.ctap2CommunicateOrThrow(authenticatorGetAssertion); + return ctap2ResponseToWebauthnResponse(request, response); } catch (Ctap2Exception e) { switch (e.ctapErrorResponse.errorCode()) { @@ -154,20 +158,19 @@ public AuthenticatorGetAssertion webauthnCommandToCtap2Command( byte[] clientDataHash = HashUtil.sha256(clientDataJson); if (pinToken != null) { - byte[] pinAuth = pinProtocolV1.calculatePinAuth(pinToken, clientDataHash); return AuthenticatorGetAssertion - .create(rpId, clientDataHash, clientDataJson, options.allowCredentials(), null, - pinAuth, PinProtocolV1.PIN_PROTOCOL); + .create(rpId, clientDataHash, clientDataJson, options.allowCredentials(), options.extensionParameters(), null, + pinToken); } else { - return AuthenticatorGetAssertion.create(rpId, clientDataHash, clientDataJson, options.allowCredentials(), null); + return AuthenticatorGetAssertion.create(rpId, clientDataHash, clientDataJson, options.allowCredentials(), options.extensionParameters(), null); } } private PublicKeyCredential ctap2ResponseToWebauthnResponse( - PublicKeyCredentialGet credentialCreate, + PublicKeyCredentialGet credentialGet, AuthenticatorGetAssertionResponse response ) throws IOException { - byte[] credential = determinePublicKeyCredentialId(credentialCreate, response); + byte[] credential = determinePublicKeyCredentialId(credentialGet, response); PublicKeyCredentialUserEntity user = response.user(); AssertionCreationData assertionCreationData = AssertionCreationData.create( @@ -185,7 +188,8 @@ private PublicKeyCredential ctap2ResponseToWebauthnResponse( assertionCreationData.clientDataJSONResult(), assertionCreationData.authenticatorDataResult(), assertionCreationData.signatureResult(), - assertionCreationData.userHandleResult() + assertionCreationData.userHandleResult(), + response.hmacSecretData() ); return PublicKeyCredential .create(assertionCreationData.credentialIdResult(), authenticatorResponse); diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap2/AuthenticatorMakeCredentialOperation.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap2/AuthenticatorMakeCredentialOperation.java index a5caa27..79ba2f3 100644 --- a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap2/AuthenticatorMakeCredentialOperation.java +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/operations/ctap2/AuthenticatorMakeCredentialOperation.java @@ -143,12 +143,12 @@ public AuthenticatorMakeCredential webauthnToCtap2Command( } if (pinToken != null) { - byte[] pinAuth = pinProtocolV1.calculatePinAuth(pinToken, clientDataHash); + byte[] pinAuth = pinToken.calculatePinAuth(clientDataHash); return AuthenticatorMakeCredential.create(clientDataHash, clientDataJson, rp, options.user(), options.pubKeyCredParams(), - options.excludeCredentials(), authenticatorOptions, pinAuth, PinProtocolV1.PIN_PROTOCOL); + options.excludeCredentials(), options.extensionParameters(), authenticatorOptions, pinAuth, pinToken.pinProtocol().version()); } else { return AuthenticatorMakeCredential.create(clientDataHash, clientDataJson, rp, options.user(), options.pubKeyCredParams(), - options.excludeCredentials(), authenticatorOptions, null, null); + options.excludeCredentials(), options.extensionParameters(), authenticatorOptions, null, null); } } diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinAuthCryptoUtil.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinAuthCryptoUtil.java index 70e8843..e19dfeb 100644 --- a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinAuthCryptoUtil.java +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinAuthCryptoUtil.java @@ -67,6 +67,10 @@ public byte[] calculatePinAuth(byte[] pinToken, byte[] clientDataHash) { return hmacSha256Left16Bytes(pinToken, clientDataHash); } + public byte[] authenticate(byte[] secret, byte[] data) { + return hmacSha256Left16Bytes(secret, data); + } + /** * Leftmost 16 bytes of an HMAC-SHA-256 operation. */ @@ -87,19 +91,37 @@ private byte[] hmacSha256(byte[] secret, byte[] data) { } } - public byte[] calculatePinHashEnc(byte[] sharedSecret, String pin) throws IOException { - byte[] pinHash = calculatePinHash(pin); + public byte[] encrypt(byte[] sharedSecret, byte[] data) { try { SecretKeySpec secretKey = new SecretKeySpec(sharedSecret, "AES"); IvParameterSpec ivParameterSpec = new IvParameterSpec(getIv()); Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); cipher.init(Cipher.ENCRYPT_MODE, secretKey, ivParameterSpec); - return cipher.doFinal(pinHash); + return cipher.doFinal(data); } catch (IllegalBlockSizeException | InvalidAlgorithmParameterException | NoSuchAlgorithmException | NoSuchPaddingException | BadPaddingException | InvalidKeyException e) { throw new IllegalStateException(e); } } + public byte[] decrypt(byte[] sharedSecret, byte[] data) throws IOException { + try { + SecretKeySpec secretKey = new SecretKeySpec(sharedSecret, "AES"); + IvParameterSpec ivParameterSpec = new IvParameterSpec(new byte[16]); + Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec); + return cipher.doFinal(data); + } catch (NoSuchPaddingException | NoSuchAlgorithmException e) { + throw new IllegalStateException(e); + } catch (IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException | InvalidKeyException e) { + throw new IOException("Error decrypting data from authenticator", e); + } + } + + public byte[] calculatePinHashEnc(byte[] sharedSecret, String pin) throws IOException { + byte[] pinHash = calculatePinHash(pin); + return encrypt(sharedSecret, pinHash); + } + @Keep private byte[] getIv() { // IV=0 as per CTAP2 specification @@ -134,14 +156,8 @@ byte[] padPin(String pin) throws IOException { public byte[] decryptPinToken(byte[] sharedSecret, byte[] pinTokenEnc) throws IOException { try { - SecretKeySpec secretKey = new SecretKeySpec(sharedSecret, "AES"); - IvParameterSpec ivParameterSpec = new IvParameterSpec(new byte[16]); - Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding"); - cipher.init(Cipher.DECRYPT_MODE, secretKey, ivParameterSpec); - return cipher.doFinal(pinTokenEnc); - } catch (NoSuchPaddingException | NoSuchAlgorithmException e) { - throw new IllegalStateException(e); - } catch (IllegalBlockSizeException | BadPaddingException | InvalidAlgorithmParameterException | InvalidKeyException e) { + return decrypt(sharedSecret, pinTokenEnc); + } catch (IOException e) { throw new IOException("Error decrypting pinToken from authenticator", e); } } diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinProtocol.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinProtocol.java new file mode 100644 index 0000000..e0cc8ae --- /dev/null +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinProtocol.java @@ -0,0 +1,20 @@ +package de.cotech.hw.fido2.internal.pinauth; + +import java.io.IOException; + +import de.cotech.hw.fido2.internal.Fido2AppletConnection; + +public interface PinProtocol { + PinToken clientPinAuthenticate( + Fido2AppletConnection fido2AppletConnection, String pin, boolean lastAttemptOk) throws IOException; + + int version(); + + byte[] authenticate(PinToken pinToken, byte[] data); + + byte[] encrypt(PinToken pinToken, byte[] data); + + byte[] decrypt(PinToken pinToken, byte[] data) throws IOException; + + byte[] calculatePinAuth(PinToken pinToken, byte[] data); +} diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinProtocolV1.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinProtocolV1.java index bde6400..b1c5742 100644 --- a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinProtocolV1.java +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinProtocolV1.java @@ -44,7 +44,7 @@ import de.cotech.hw.util.HwTimber; -public class PinProtocolV1 { +public class PinProtocolV1 implements PinProtocol { public static final int PIN_PROTOCOL = 1; private final PinAuthCryptoUtil pinAuthCryptoUtil; @@ -53,6 +53,7 @@ public PinProtocolV1(PinAuthCryptoUtil pinAuthCryptoUtil) { this.pinAuthCryptoUtil = pinAuthCryptoUtil; } + @Override public PinToken clientPinAuthenticate( Fido2AppletConnection fido2AppletConnection, String pin, boolean lastAttemptOk) throws IOException { AuthenticatorClientPin ctap2Command; @@ -98,7 +99,7 @@ public PinToken clientPinAuthenticate( .decryptPinToken(sharedSecret, authenticatorClientPinResponse.pinToken()); HwTimber.d("Authentication successful. pinToken is " + Hex.encodeHexString(pinToken)); - return PinToken.create(pinToken); + return PinToken.create(this, pinToken, platformKeyAgreementKey, sharedSecret); } catch (Ctap2Exception e) { switch (e.ctapErrorResponse.errorCode()) { case CtapErrorResponse.CTAP2_ERR_PIN_BLOCKED: @@ -108,11 +109,14 @@ public PinToken clientPinAuthenticate( throw new FidoClientPinInvalidException(retriesLeft); } throw e; - } finally { - Arrays.fill(sharedSecret, (byte) 0); } } + @Override + public int version() { + return PIN_PROTOCOL; + } + private int checkRetries(Fido2AppletConnection fido2AppletConnection) throws IOException { Ctap2Command ctap2Command = AuthenticatorClientPin.createGetRetries(); @@ -124,7 +128,22 @@ private int checkRetries(Fido2AppletConnection fido2AppletConnection) return retries; } - public byte[] calculatePinAuth(PinToken pinToken, byte[] clientDataHash) { - return pinAuthCryptoUtil.calculatePinAuth(pinToken.pinToken(), clientDataHash); + public byte[] calculatePinAuth(PinToken pinToken, byte[] data) { + return pinAuthCryptoUtil.calculatePinAuth(pinToken.pinToken(), data); + } + + @Override + public byte[] authenticate(PinToken pinToken, byte[] data) { + return pinAuthCryptoUtil.authenticate(pinToken.sharedSecret(), data); + } + + @Override + public byte[] encrypt(PinToken pinToken, byte[] data) { + return pinAuthCryptoUtil.encrypt(pinToken.sharedSecret(), data); + } + + @Override + public byte[] decrypt(PinToken pinToken, byte[] data) throws IOException { + return pinAuthCryptoUtil.decrypt(pinToken.sharedSecret(), data); } } diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinToken.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinToken.java index 3e50684..6a087d8 100644 --- a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinToken.java +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/pinauth/PinToken.java @@ -27,13 +27,40 @@ import com.google.auto.value.AutoValue; +import java.io.IOException; + @AutoValue public abstract class PinToken { + + public abstract PinProtocol pinProtocol(); + @SuppressWarnings("mutable") public abstract byte[] pinToken(); - public static PinToken create(byte[] pinToken) { - return new AutoValue_PinToken(pinToken); + @SuppressWarnings("mutable") + public abstract byte[] platformKeyAgreementKey(); + + @SuppressWarnings("mutable") + abstract byte[] sharedSecret(); + + public byte[] authenticate(byte[] data) { + return pinProtocol().authenticate(this, data); + } + + public byte[] calculatePinAuth(byte[] data) { + return pinProtocol().calculatePinAuth(this, data); + } + + public byte[] encrypt(byte[] data) { + return pinProtocol().encrypt(this, data); + } + + public byte[] decrypt(byte[] data) throws IOException { + return pinProtocol().decrypt(this, data); + } + + public static PinToken create(PinProtocol pinProtocol, byte[] pinToken, byte[] platformKeyAgreementKey, byte[] sharedSecret) { + return new AutoValue_PinToken(pinProtocol, pinToken, platformKeyAgreementKey, sharedSecret); } } diff --git a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/webauthn/AuthenticatorDataParser.java b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/webauthn/AuthenticatorDataParser.java index 7477603..bcb88f1 100644 --- a/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/webauthn/AuthenticatorDataParser.java +++ b/hwsecurity/fido2/src/main/java/de/cotech/hw/fido2/internal/webauthn/AuthenticatorDataParser.java @@ -26,16 +26,20 @@ import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.ByteOrder; +import de.cotech.hw.fido2.domain.create.AuthenticatorData; import de.cotech.hw.fido2.internal.cbor_java.CborDecoder; +import de.cotech.hw.fido2.internal.cbor_java.CborEncoder; import de.cotech.hw.fido2.internal.cbor_java.CborException; import de.cotech.hw.fido2.internal.cbor_java.model.DataItem; import de.cotech.hw.fido2.domain.create.AttestedCredentialData; -import de.cotech.hw.fido2.domain.create.AuthenticatorData; import de.cotech.hw.fido2.internal.cbor.CborUtils; +import de.cotech.hw.fido2.internal.cbor_java.model.MajorType; +import de.cotech.hw.fido2.internal.cbor_java.model.Map; public class AuthenticatorDataParser { @@ -44,7 +48,7 @@ public class AuthenticatorDataParser { private static final int LENGTH_AAGUID = 16; - AuthenticatorData fromBytes(byte[] bytes) throws IOException { + public AuthenticatorData fromBytes(byte[] bytes) throws IOException { ByteBuffer buf = ByteBuffer.wrap(bytes).duplicate(); buf.order(ByteOrder.BIG_ENDIAN); @@ -54,7 +58,7 @@ AuthenticatorData fromBytes(byte[] bytes) throws IOException { int sigCounter = buf.getInt(); AttestedCredentialData attestedCredentialData = null; - byte[] extensionData = null; + Map extensionData = null; boolean hasAttestedCredentialData = (flags & AuthenticatorData.FLAG_ATTESTED_CREDENTIAL_DATA) != 0; if (hasAttestedCredentialData) { @@ -63,8 +67,7 @@ AuthenticatorData fromBytes(byte[] bytes) throws IOException { boolean hasExtensionData = (flags & AuthenticatorData.FLAG_EXTENSION_DATA) != 0; if (hasExtensionData) { - extensionData = new byte[buf.remaining()]; - buf.get(extensionData); + extensionData = parseExtensionData(buf); } return AuthenticatorData.create( @@ -74,8 +77,20 @@ AuthenticatorData fromBytes(byte[] bytes) throws IOException { public byte[] toBytes(AuthenticatorData authenticatorData) { byte[] attestedCredentialData = serializeAttestedCredentialData(authenticatorData.attestedCredentialData()); - byte[] extensionData = authenticatorData.extensions(); - int extensionDataLength = authenticatorData.hasExtensionData() && extensionData != null ? extensionData.length : 0; + byte[] extensionData = null; + + if (authenticatorData.hasExtensionData()) { + try { + ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); + new CborEncoder(outputStream).encode(authenticatorData.extensions()); + extensionData = outputStream.toByteArray(); + } catch (CborException e) { + // Failing to re-CBOR-encode a CBOR DataItem? + throw new IllegalStateException(e); + } + } + + int extensionDataLength = extensionData != null ? extensionData.length : 0; ByteBuffer result = ByteBuffer.allocate( TOTAL_LENGTH_HEADER + attestedCredentialData.length + extensionDataLength); @@ -108,6 +123,20 @@ private byte[] serializeAttestedCredentialData(AttestedCredentialData attestedCr return result.array(); } + private static Map parseExtensionData(ByteBuffer buf) throws IOException { + try { + ByteArrayInputStream inputStream = new ByteArrayInputStream( + buf.array(), buf.arrayOffset() + buf.position(), buf.remaining()); + DataItem dataItem = new CborDecoder(inputStream).decodeNext(); + if (dataItem.getMajorType() != MajorType.MAP) { + throw new IOException("Extension data not a CBOR map!"); + } + return (Map) dataItem; + } catch (CborException e) { + throw new IOException("Error reading CBOR-encoded credential data!", e); + } + } + private static AttestedCredentialData parseAttestedCredentialData(ByteBuffer buf) throws IOException { byte[] aaguid = new byte[LENGTH_AAGUID]; buf.get(aaguid); diff --git a/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/Fido2SecurityKeyTest.java b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/Fido2SecurityKeyTest.java index cff4680..b741953 100644 --- a/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/Fido2SecurityKeyTest.java +++ b/hwsecurity/fido2/src/test/java/de/cotech/hw/fido2/Fido2SecurityKeyTest.java @@ -267,4 +267,4 @@ public void makeCredential_withPin() throws Exception { assertEquals("{\"type\":\"webauthn.create\",\"origin\":\"https:\\/\\/webauthn.hwsecurity.dev\",\"challenge\":\"GNxfVQfEVOoi9uU1W_jM-w\",\"hashAlgorithm\":\"SHA-256\"}", new String(publicKeyCredential.response().clientDataJson())); } -} \ No newline at end of file +}