Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FIDO] Support extensions #161

Open
wants to merge 44 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
9901dfa
wip
AdamVe Sep 25, 2024
8524e9b
poc
AdamVe Sep 26, 2024
f03d145
credBlob
AdamVe Sep 27, 2024
f626d4d
largeBlob
AdamVe Oct 11, 2024
39eaa58
handle unit tests
AdamVe Oct 11, 2024
4188ac7
update log message
AdamVe Oct 11, 2024
b72f4b1
PRF extension implementation
AdamVe Oct 15, 2024
33ac890
credProps instrumented test
AdamVe Oct 16, 2024
d64aa66
PRF instrumented tests
AdamVe Oct 16, 2024
240ab63
LargeBlob instrumented tests
AdamVe Oct 17, 2024
dfcceff
refactor instrumented tests of existing extensions
AdamVe Oct 17, 2024
12ea9eb
credBlob and its instrumented test
AdamVe Oct 18, 2024
37a1d53
use ExtensionDataProvider
AdamVe Oct 21, 2024
6cfb025
fix hmacGetSecret/hmacCreateSecret, add tests
AdamVe Oct 22, 2024
08bcd31
credential list filtering
AdamVe Oct 22, 2024
a981b49
refactor exts permission and data provider APIs
AdamVe Oct 28, 2024
f56968a
remove dataProvider, use only b64 encoding
AdamVe Oct 30, 2024
b6d1ef8
prf evalByCredential parameter impl
AdamVe Oct 30, 2024
90d5457
simplify internal client api
AdamVe Nov 1, 2024
cca52eb
move extension processing to client
AdamVe Nov 1, 2024
252f62d
move LargeBlobs to correct package
AdamVe Nov 4, 2024
eb86be2
simplify
AdamVe Nov 4, 2024
e4a2a53
refactor internal processing APIs
AdamVe Nov 4, 2024
e96f097
unify processing result for input and output
AdamVe Nov 4, 2024
90656f1
use ServiceLoader for extensions
AdamVe Nov 4, 2024
6f3787c
Merge branch 'adamve/fido_extensions_refactor' into adamve/fido_exten…
AdamVe Nov 5, 2024
a7c9d5a
update arguments API
AdamVe Nov 5, 2024
0102ab2
fix pinuvauth for makeCred/getAssertions
AdamVe Nov 7, 2024
6275a31
simplify public API
AdamVe Nov 18, 2024
e2d59cc
update extension API
AdamVe Nov 19, 2024
3a8d9ec
Merge branch 'adamve/fido_extensions_refactor_2' into adamve/fido_ext…
AdamVe Nov 19, 2024
3c818b7
remove extensions services
AdamVe Nov 19, 2024
f008bf7
rename test classes
AdamVe Nov 20, 2024
c9f02df
Add credProtect instrumented test
AdamVe Nov 20, 2024
6c71e24
add minPinLength extensions test
AdamVe Nov 20, 2024
cf198de
fix Preferred ResidentKeyRequirement
AdamVe Nov 20, 2024
59719bc
update CTAP errors
AdamVe Nov 20, 2024
abac677
self review
AdamVe Nov 20, 2024
5ad1555
fix Extensions.fromMap
AdamVe Nov 20, 2024
f518c77
update javadoc
AdamVe Nov 20, 2024
585daf2
unify javadoc format in file
AdamVe Nov 20, 2024
5945e5d
fix spotbugs warnings
AdamVe Nov 20, 2024
06ca113
remove usage of Stream API
AdamVe Nov 21, 2024
1850f47
review public extensions API
AdamVe Nov 22, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion android/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ dependencies {
testImplementation project(':testing')
testImplementation 'androidx.test.ext:junit:1.2.1'
testImplementation 'org.robolectric:robolectric:4.11.1'
testImplementation 'org.mockito:mockito-core:5.12.0'
testImplementation 'org.mockito:mockito-core:5.13.0'

androidTestImplementation 'androidx.test.ext:junit:1.2.1'
androidTestImplementation 'androidx.test:runner:1.6.1'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ public class CtapException extends CommandException {
public static final byte ERR_LIMIT_EXCEEDED = 0x15;
public static final byte ERR_UNSUPPORTED_EXTENSION = 0x16;
public static final byte ERR_FP_DATABASE_FULL = 0x17;
public static final byte ERR_LARGE_BLOB_STORAGE_FULL = 0x18;
public static final byte ERR_CREDENTIAL_EXCLUDED = 0x19;
public static final byte ERR_PROCESSING = 0x21;
public static final byte ERR_INVALID_CREDENTIAL = 0x22;
Expand All @@ -63,7 +64,9 @@ public class CtapException extends CommandException {
public static final byte ERR_PIN_AUTH_INVALID = 0x33;
public static final byte ERR_PIN_AUTH_BLOCKED = 0x34;
public static final byte ERR_PIN_NOT_SET = 0x35;
@Deprecated // use ERR_PUAT_REQUIRED
public static final byte ERR_PIN_REQUIRED = 0x36;
public static final byte ERR_PUAT_REQUIRED = 0x36; // CTAP2.1 naming
public static final byte ERR_PIN_POLICY_VIOLATION = 0x37;
public static final byte ERR_PIN_TOKEN_EXPIRED = 0x38;
public static final byte ERR_REQUEST_TOO_LARGE = 0x39;
Expand Down
2 changes: 2 additions & 0 deletions fido/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ apply plugin: 'yubikit-java-library'

dependencies {
api project(':core')

testImplementation 'org.mockito:mockito-core:5.13.0'
}

ext.pomName = "Yubico YubiKit ${project.name.capitalize()}"
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ static ClientError wrapCtapException(CtapException error) {
case CtapException.ERR_INVALID_CBOR:
case CtapException.ERR_MISSING_PARAMETER:
case CtapException.ERR_INVALID_OPTION:
case CtapException.ERR_PIN_REQUIRED:
case CtapException.ERR_PUAT_REQUIRED:
case CtapException.ERR_PIN_INVALID:
case CtapException.ERR_PIN_BLOCKED:
case CtapException.ERR_PIN_NOT_SET:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ public Map<PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity> getCred
Map<PublicKeyCredentialDescriptor, PublicKeyCredentialUserEntity> credentials = new HashMap<>();
byte[] rpIdHash = rpIdHashes.get(rpId);
if (rpIdHash == null) {
rpIdHash = BasicWebAuthnClient.hash(rpId.getBytes(StandardCharsets.UTF_8));
rpIdHash = BasicWebAuthnClient.Utils.hash(rpId.getBytes(StandardCharsets.UTF_8));
}

for (CredentialManagement.CredentialData credData : credentialManagement.enumerateCredentials(rpIdHash)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@
*/
public class MultipleAssertionsAvailable extends Throwable {
private final byte[] clientDataJson;
private final List<Ctap2Session.AssertionData> assertions;
private final List<BasicWebAuthnClient.WithExtensionResults<Ctap2Session.AssertionData>> assertions;

MultipleAssertionsAvailable(byte[] clientDataJson, List<Ctap2Session.AssertionData> assertions) {
MultipleAssertionsAvailable(byte[] clientDataJson, List<BasicWebAuthnClient.WithExtensionResults<Ctap2Session.AssertionData>> assertions) {
super("Request returned multiple assertions");

this.clientDataJson = clientDataJson;
Expand Down Expand Up @@ -65,10 +65,10 @@ public int getAssertionCount() {
*/
public List<PublicKeyCredentialUserEntity> getUsers() throws UserInformationNotAvailableError {
List<PublicKeyCredentialUserEntity> users = new ArrayList<>();
for (Ctap2Session.AssertionData assertion : assertions) {
for (BasicWebAuthnClient.WithExtensionResults<Ctap2Session.AssertionData> assertion : assertions) {
try {
users.add(PublicKeyCredentialUserEntity.fromMap(
Objects.requireNonNull(assertion.getUser()),
Objects.requireNonNull(assertion.data.getUser()),
SerializationType.CBOR
));
} catch (NullPointerException e) {
Expand All @@ -89,20 +89,20 @@ public PublicKeyCredential select(int index) {
if (assertions.isEmpty()) {
throw new IllegalStateException("Assertion has already been selected");
}
Ctap2Session.AssertionData assertion = assertions.get(index);
BasicWebAuthnClient.WithExtensionResults<Ctap2Session.AssertionData> assertion = assertions.get(index);
assertions.clear();

final Map<String, ?> user = Objects.requireNonNull(assertion.getUser());
final Map<String, ?> credential = Objects.requireNonNull(assertion.getCredential());
final Map<String, ?> user = Objects.requireNonNull(assertion.data.getUser());
final Map<String, ?> credential = Objects.requireNonNull(assertion.data.getCredential());
final byte[] credentialId = Objects.requireNonNull((byte[]) credential.get(PublicKeyCredentialDescriptor.ID));
return new PublicKeyCredential(
credentialId,
new AuthenticatorAssertionResponse(
clientDataJson,
assertion.getAuthenticatorData(),
assertion.getSignature(),
assertion.data.getAuthenticatorData(),
assertion.data.getSignature(),
Objects.requireNonNull((byte[]) user.get(PublicKeyCredentialUserEntity.ID))
)
);
),
assertion.clientExtensionResults);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* 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.fido.client.extensions;

import static com.yubico.yubikit.core.internal.codec.Base64.fromUrlSafeString;

import com.yubico.yubikit.fido.ctap.Ctap2Session;
import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol;
import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialCreationOptions;
import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialRequestOptions;

import java.util.Collections;

import javax.annotation.Nullable;

public class CredBlobExtension extends Extension {

public CredBlobExtension() {
super("credBlob");
}

@Nullable
@Override
public RegistrationProcessor makeCredential(
Ctap2Session ctap,
PublicKeyCredentialCreationOptions options,
PinUvAuthProtocol pinUvAuthProtocol) {

if (isSupported(ctap)) {
String b64Blob = (String) options.getExtensions().get("credBlob");
if (b64Blob != null) {
byte[] blob = fromUrlSafeString(b64Blob);
if (blob.length <= ctap.getCachedInfo().getMaxCredBlobLength()) {
return new RegistrationProcessor(
pinToken -> Collections.singletonMap(name, blob)
);
}
}
}
return null;
}

@Nullable
@Override
public AuthenticationProcessor getAssertion(
Ctap2Session ctap,
PublicKeyCredentialRequestOptions options,
PinUvAuthProtocol pinUvAuthProtocol) {
if (isSupported(ctap) &&
Boolean.TRUE.equals(options.getExtensions().get("getCredBlob"))) {
return new AuthenticationProcessor(
(AuthenticationInput) (selected, pinToken) -> Collections.singletonMap(name, true)
);
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* 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.fido.client.extensions;

import com.yubico.yubikit.fido.ctap.Ctap2Session;
import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol;
import com.yubico.yubikit.fido.webauthn.AuthenticatorSelectionCriteria;
import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialCreationOptions;
import com.yubico.yubikit.fido.webauthn.ResidentKeyRequirement;

import java.util.Collections;

import javax.annotation.Nullable;

public class CredPropsExtension extends Extension {

public CredPropsExtension() {
super("credProps");
}

@Nullable
@Override
public RegistrationProcessor makeCredential(
Ctap2Session ctap,
PublicKeyCredentialCreationOptions options,
PinUvAuthProtocol pinUvAuthProtocol) {

if (options.getExtensions().has(name)) {
AuthenticatorSelectionCriteria authenticatorSelection = options.getAuthenticatorSelection();
String optionsRk = authenticatorSelection != null
? authenticatorSelection.getResidentKey()
: null;
Boolean authenticatorRk = (Boolean) ctap.getCachedInfo().getOptions().get("rk");
boolean rk = (ResidentKeyRequirement.REQUIRED.equals(optionsRk) ||
(ResidentKeyRequirement.PREFERRED.equals(optionsRk) &&
Boolean.TRUE.equals(authenticatorRk)));

return new RegistrationProcessor(
(attestationObject, pinToken) ->
Collections.singletonMap(name,
Collections.singletonMap("rk", rk)));
}
return null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* 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.fido.client.extensions;

import com.yubico.yubikit.fido.ctap.Ctap2Session;
import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol;
import com.yubico.yubikit.fido.webauthn.Extensions;
import com.yubico.yubikit.fido.webauthn.PublicKeyCredentialCreationOptions;

import java.util.Collections;

import javax.annotation.Nullable;

public class CredProtectExtension extends Extension {

static final String OPTIONAL = "userVerificationOptional";
static final String OPTIONAL_WITH_LIST = "userVerificationOptionalWithCredentialIDList";
static final String REQUIRED = "userVerificationRequired";

public CredProtectExtension() {
super("credProtect");
}

@Nullable
@Override
public RegistrationProcessor makeCredential(
Ctap2Session ctap,
PublicKeyCredentialCreationOptions options,
PinUvAuthProtocol pinUvAuthProtocol) {

Extensions extensions = options.getExtensions();
String credentialProtectionPolicy = (String) extensions.get("credentialProtectionPolicy");
if (credentialProtectionPolicy == null) {
return null;
}

Integer credProtect = credProtectValue(credentialProtectionPolicy);
Boolean enforce = (Boolean) extensions.get("enforceCredentialProtectionPolicy");
if (Boolean.TRUE.equals(enforce) &&
!isSupported(ctap) &&
credProtect != null &&
credProtect > 0x01) {
throw new IllegalArgumentException("Authenticator does not support Credential Protection");
}
return credProtect != null
? new RegistrationProcessor(
pinToken -> Collections.singletonMap(name, credProtect))
: null;
}

@Nullable
private Integer credProtectValue(String optionsValue) {
switch(optionsValue) {
case OPTIONAL:
return 0x01;
case OPTIONAL_WITH_LIST:
return 0x02;
case REQUIRED:
return 0x03;
default:
return null;
}
}
}
Loading