Skip to content

Commit a891213

Browse files
committed
Merge PR #116.
2 parents 4cd4ef6 + a597952 commit a891213

File tree

4 files changed

+340
-111
lines changed

4 files changed

+340
-111
lines changed

fido/src/main/java/com/yubico/yubikit/fido/client/BasicWebAuthnClient.java

+134-104
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2020-2023 Yubico.
2+
* Copyright (C) 2020-2024 Yubico.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -105,6 +105,20 @@ boolean supportsEpForRpId(@Nullable String rpId) {
105105
}
106106
}
107107

108+
private static class AuthParams {
109+
@Nullable
110+
private final byte[] pinUvAuthParam;
111+
@Nullable
112+
private final Integer pinUvAuthProtocol;
113+
114+
AuthParams(
115+
@Nullable byte[] pinUvAuthParam,
116+
@Nullable Integer pinUvAuthProtocol) {
117+
this.pinUvAuthParam = pinUvAuthParam;
118+
this.pinUvAuthProtocol = pinUvAuthProtocol;
119+
}
120+
}
121+
108122
public BasicWebAuthnClient(Ctap2Session session) throws IOException, CommandException {
109123
this.ctap = session;
110124
Ctap2Session.InfoData info = ctap.getInfo();
@@ -380,101 +394,86 @@ protected Ctap2Session.CredentialData ctapMakeCredential(
380394

381395
final SerializationType serializationType = SerializationType.CBOR;
382396

383-
byte[] pinToken = null;
384-
try {
385-
if (options.getExtensions() != null) {
386-
throw new ClientError(
387-
ClientError.Code.CONFIGURATION_UNSUPPORTED,
388-
"Extensions not supported");
389-
}
397+
if (options.getExtensions() != null) {
398+
throw new ClientError(
399+
ClientError.Code.CONFIGURATION_UNSUPPORTED,
400+
"Extensions not supported");
401+
}
390402

391-
Map<String, ?> rp = options.getRp().toMap(serializationType);
392-
String rpId = options.getRp().getId();
393-
if (rpId == null) {
394-
((Map<String, Object>) rp).put("id", effectiveDomain);
395-
} else if (!(effectiveDomain.equals(rpId) || effectiveDomain.endsWith("." + rpId))) {
396-
throw new ClientError(
397-
ClientError.Code.BAD_REQUEST,
398-
"RP ID is not valid for effective domain");
399-
}
403+
Map<String, ?> rp = options.getRp().toMap(serializationType);
404+
String rpId = options.getRp().getId();
405+
if (rpId == null) {
406+
((Map<String, Object>) rp).put("id", effectiveDomain);
407+
} else if (!(effectiveDomain.equals(rpId) || effectiveDomain.endsWith("." + rpId))) {
408+
throw new ClientError(
409+
ClientError.Code.BAD_REQUEST,
410+
"RP ID is not valid for effective domain");
411+
}
400412

401-
byte[] pinUvAuthParam = null;
402-
int pinUvAuthProtocol = 0;
403-
404-
Map<String, Boolean> ctapOptions = new HashMap<>();
405-
AuthenticatorSelectionCriteria authenticatorSelection =
406-
options.getAuthenticatorSelection();
407-
if (authenticatorSelection != null) {
408-
String residentKeyRequirement = authenticatorSelection.getResidentKey();
409-
if (ResidentKeyRequirement.REQUIRED.equals(residentKeyRequirement) ||
410-
(ResidentKeyRequirement.PREFERRED.equals(residentKeyRequirement) &&
411-
(pinSupported || uvSupported)
412-
)
413-
) {
414-
ctapOptions.put(OPTION_RESIDENT_KEY, true);
415-
}
416-
if (getCtapUv(authenticatorSelection.getUserVerification(), pin != null)) {
417-
ctapOptions.put(OPTION_USER_VERIFICATION, true);
418-
}
419-
} else {
420-
if (getCtapUv(UserVerificationRequirement.PREFERRED, pin != null)) {
421-
ctapOptions.put(OPTION_USER_VERIFICATION, true);
422-
}
413+
Map<String, Boolean> ctapOptions = new HashMap<>();
414+
AuthenticatorSelectionCriteria authenticatorSelection =
415+
options.getAuthenticatorSelection();
416+
if (authenticatorSelection != null) {
417+
String residentKeyRequirement = authenticatorSelection.getResidentKey();
418+
if (ResidentKeyRequirement.REQUIRED.equals(residentKeyRequirement) ||
419+
(ResidentKeyRequirement.PREFERRED.equals(residentKeyRequirement) &&
420+
(pinSupported || uvSupported)
421+
)
422+
) {
423+
ctapOptions.put(OPTION_RESIDENT_KEY, true);
423424
}
424-
425-
if (pin != null) {
426-
pinToken = clientPin.getPinToken(pin, ClientPin.PIN_PERMISSION_MC, rpId);
427-
pinUvAuthParam = clientPin.getPinUvAuth().authenticate(pinToken, clientDataHash);
428-
pinUvAuthProtocol = clientPin.getPinUvAuth().getVersion();
429-
} else if (pinConfigured && ctapOptions.containsKey(OPTION_USER_VERIFICATION)) {
430-
pinToken = clientPin.getUvToken(ClientPin.PIN_PERMISSION_MC, rpId, null);
431-
pinUvAuthParam = clientPin.getPinUvAuth().authenticate(pinToken, clientDataHash);
432-
pinUvAuthProtocol = clientPin.getPinUvAuth().getVersion();
433-
} else if (pinConfigured && !ctapOptions.containsKey(OPTION_USER_VERIFICATION)) {
434-
throw new PinRequiredClientError();
425+
if (getCtapUv(authenticatorSelection.getUserVerification(), pin != null)) {
426+
ctapOptions.put(OPTION_USER_VERIFICATION, true);
427+
}
428+
} else {
429+
if (getCtapUv(UserVerificationRequirement.PREFERRED, pin != null)) {
430+
ctapOptions.put(OPTION_USER_VERIFICATION, true);
435431
}
432+
}
436433

437-
final List<PublicKeyCredentialDescriptor> excludeCredentials =
438-
removeUnsupportedCredentials(
439-
options.getExcludeCredentials()
440-
);
434+
final AuthParams authParams = getAuthParams(
435+
clientDataHash,
436+
ctapOptions.containsKey(OPTION_USER_VERIFICATION),
437+
pin,
438+
ClientPin.PIN_PERMISSION_MC,
439+
rpId);
441440

442-
final Map<String, ?> user = options.getUser().toMap(serializationType);
441+
final List<PublicKeyCredentialDescriptor> excludeCredentials =
442+
removeUnsupportedCredentials(
443+
options.getExcludeCredentials()
444+
);
443445

444-
List<Map<String, ?>> pubKeyCredParams = new ArrayList<>();
445-
for (PublicKeyCredentialParameters param : options.getPubKeyCredParams()) {
446-
if (isPublicKeyCredentialTypeSupported(param.getType())) {
447-
pubKeyCredParams.add(param.toMap(serializationType));
448-
}
449-
}
446+
final Map<String, ?> user = options.getUser().toMap(serializationType);
450447

451-
@Nullable Integer validatedEnterpriseAttestation = null;
452-
if (isEnterpriseAttestationSupported() &&
453-
AttestationConveyancePreference.ENTERPRISE.equals(options.getAttestation()) &&
454-
userAgentConfiguration.supportsEpForRpId(rpId) &&
455-
enterpriseAttestation != null &&
456-
(enterpriseAttestation == 1 || enterpriseAttestation == 2)) {
457-
validatedEnterpriseAttestation = enterpriseAttestation;
448+
List<Map<String, ?>> pubKeyCredParams = new ArrayList<>();
449+
for (PublicKeyCredentialParameters param : options.getPubKeyCredParams()) {
450+
if (isPublicKeyCredentialTypeSupported(param.getType())) {
451+
pubKeyCredParams.add(param.toMap(serializationType));
458452
}
453+
}
459454

460-
return ctap.makeCredential(
461-
clientDataHash,
462-
rp,
463-
user,
464-
pubKeyCredParams,
465-
getCredentialList(excludeCredentials),
466-
null,
467-
ctapOptions.isEmpty() ? null : ctapOptions,
468-
pinUvAuthParam,
469-
pinUvAuthProtocol,
470-
validatedEnterpriseAttestation,
471-
state
472-
);
473-
} finally {
474-
if (pinToken != null) {
475-
Arrays.fill(pinToken, (byte) 0);
476-
}
455+
@Nullable Integer validatedEnterpriseAttestation = null;
456+
if (isEnterpriseAttestationSupported() &&
457+
AttestationConveyancePreference.ENTERPRISE.equals(options.getAttestation()) &&
458+
userAgentConfiguration.supportsEpForRpId(rpId) &&
459+
enterpriseAttestation != null &&
460+
(enterpriseAttestation == 1 || enterpriseAttestation == 2)) {
461+
validatedEnterpriseAttestation = enterpriseAttestation;
477462
}
463+
464+
return ctap.makeCredential(
465+
clientDataHash,
466+
rp,
467+
user,
468+
pubKeyCredParams,
469+
getCredentialList(excludeCredentials),
470+
null,
471+
ctapOptions.isEmpty() ? null : ctapOptions,
472+
authParams.pinUvAuthParam,
473+
authParams.pinUvAuthProtocol,
474+
validatedEnterpriseAttestation,
475+
state
476+
);
478477
}
479478

480479
/**
@@ -519,20 +518,14 @@ protected List<Ctap2Session.AssertionData> ctapGetAssertions(
519518
throw new ClientError(ClientError.Code.CONFIGURATION_UNSUPPORTED, "Extensions not supported");
520519
}
521520

522-
byte[] pinUvAuthParam = null;
523-
int pinUvAuthProtocol = 0;
524-
byte[] pinToken = null;
525-
try {
526-
if (pin != null) {
527-
pinToken = clientPin.getPinToken(pin, ClientPin.PIN_PERMISSION_GA, rpId);
528-
pinUvAuthParam = clientPin.getPinUvAuth().authenticate(pinToken, clientDataHash);
529-
pinUvAuthProtocol = clientPin.getPinUvAuth().getVersion();
530-
} else if (pinConfigured && ctapOptions.containsKey(OPTION_USER_VERIFICATION)) {
531-
pinToken = clientPin.getUvToken(ClientPin.PIN_PERMISSION_GA, rpId, null);
532-
pinUvAuthParam = clientPin.getPinUvAuth().authenticate(pinToken, clientDataHash);
533-
pinUvAuthProtocol = clientPin.getPinUvAuth().getVersion();
534-
}
521+
final AuthParams authParams = getAuthParams(
522+
clientDataHash,
523+
ctapOptions.containsKey(OPTION_USER_VERIFICATION),
524+
pin,
525+
ClientPin.PIN_PERMISSION_GA,
526+
rpId);
535527

528+
try {
536529
final List<PublicKeyCredentialDescriptor> allowCredentials = removeUnsupportedCredentials(
537530
options.getAllowCredentials()
538531
);
@@ -543,19 +536,15 @@ protected List<Ctap2Session.AssertionData> ctapGetAssertions(
543536
getCredentialList(allowCredentials),
544537
null,
545538
ctapOptions.isEmpty() ? null : ctapOptions,
546-
pinUvAuthParam,
547-
pinUvAuthProtocol,
539+
authParams.pinUvAuthParam,
540+
authParams.pinUvAuthProtocol,
548541
state
549542
);
550543
} catch (CtapException e) {
551544
if (e.getCtapError() == CtapException.ERR_PIN_INVALID) {
552545
throw new PinInvalidClientError(e, clientPin.getPinRetries().getCount());
553546
}
554547
throw ClientError.wrapCtapException(e);
555-
} finally {
556-
if (pinToken != null) {
557-
Arrays.fill(pinToken, (byte) 0);
558-
}
559548
}
560549
}
561550

@@ -603,6 +592,47 @@ private boolean getCtapUv(String userVerification, boolean pinProvided) throws C
603592
}
604593
}
605594

595+
private AuthParams getAuthParams(
596+
byte[] clientDataHash,
597+
boolean shouldUv,
598+
@Nullable char[] pin,
599+
@Nullable Integer permissions,
600+
@Nullable String rpId
601+
) throws ClientError, IOException, CommandException {
602+
@Nullable byte[] authToken = null;
603+
@Nullable byte[] authParam = null;
604+
@Nullable Integer authProtocolVersion = null;
605+
606+
try {
607+
if (pin != null) {
608+
authToken = clientPin.getPinToken(pin, permissions, rpId);
609+
authParam = clientPin.getPinUvAuth().authenticate(authToken, clientDataHash);
610+
authProtocolVersion = clientPin.getPinUvAuth().getVersion();
611+
} else if (pinConfigured) {
612+
if (shouldUv && uvConfigured) {
613+
if (ClientPin.isTokenSupported(ctap.getCachedInfo())) {
614+
authToken = clientPin.getUvToken(permissions, rpId, null);
615+
authParam = clientPin.getPinUvAuth().authenticate(authToken, clientDataHash);
616+
authProtocolVersion = clientPin.getPinUvAuth().getVersion();
617+
}
618+
// no authToken is created means that internal UV is used
619+
} else {
620+
// the authenticator supports pin but no PIN was provided
621+
throw new PinRequiredClientError();
622+
}
623+
}
624+
return new AuthParams(
625+
authParam,
626+
authProtocolVersion
627+
);
628+
629+
} finally {
630+
if (authToken != null) {
631+
Arrays.fill(authToken, (byte) 0);
632+
}
633+
}
634+
}
635+
606636
/**
607637
* Calculates the preferred pinUvAuth protocol for authenticator provided list.
608638
* Returns PinUvAuthDummyProtocol if the authenticator does not support any of the SDK

testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/BasicWebAuthnClientInstrumentedTests.java

+14-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (C) 2022-2023 Yubico.
2+
* Copyright (C) 2022-2024 Yubico.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,6 +20,7 @@
2020

2121
import androidx.test.filters.LargeTest;
2222

23+
import com.yubico.yubikit.fido.ctap.ClientPin;
2324
import com.yubico.yubikit.fido.ctap.PinUvAuthProtocol;
2425
import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV1;
2526
import com.yubico.yubikit.fido.ctap.PinUvAuthProtocolV2;
@@ -54,7 +55,18 @@ public void testMakeCredentialGetAssertion() throws Throwable {
5455
withCtap2Session(
5556
(device, session) -> supportsPinUvAuthProtocol(session, pinUvAuthProtocol),
5657
BasicWebAuthnClientTests::testMakeCredentialGetAssertion,
57-
pinUvAuthProtocol);
58+
pinUvAuthProtocol,
59+
TestData.PIN);
60+
}
61+
62+
@Test
63+
public void testMakeCredentialGetAssertionTokenUvOnly() throws Throwable {
64+
withCtap2Session(
65+
(device, session) -> supportsPinUvAuthProtocol(session, pinUvAuthProtocol)
66+
&& ClientPin.isTokenSupported(session.getCachedInfo()),
67+
BasicWebAuthnClientTests::testMakeCredentialGetAssertion,
68+
pinUvAuthProtocol,
69+
null);
5870
}
5971

6072
@Test
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
/*
2+
* Copyright (C) 2024 Yubico.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package com.yubico.yubikit.testing.fido;
18+
19+
import androidx.test.filters.LargeTest;
20+
21+
import com.yubico.yubikit.fido.client.PinRequiredClientError;
22+
import com.yubico.yubikit.fido.ctap.Ctap2Session;
23+
import com.yubico.yubikit.testing.framework.FidoInstrumentedTests;
24+
25+
import org.junit.Test;
26+
27+
@LargeTest
28+
public class UvDiscouragedInstrumentedTests extends FidoInstrumentedTests {
29+
30+
static boolean hasPin(Ctap2Session session) {
31+
final Ctap2Session.InfoData info = session.getCachedInfo();
32+
return Boolean.TRUE.equals(info.getOptions().get("clientPin"));
33+
}
34+
35+
@Test
36+
public void testMakeCredentialGetAssertion() throws Throwable {
37+
withCtap2Session(
38+
"This device has a PIN set",
39+
(device, session) -> !hasPin(session),
40+
BasicWebAuthnClientTests::testUvDiscouragedMakeCredentialGetAssertion);
41+
}
42+
43+
44+
/**
45+
* Run this test only on devices with PIN set
46+
* Expected to fail with PinRequiredClientError
47+
*/
48+
@Test(expected = PinRequiredClientError.class)
49+
public void testMakeCredentialGetAssertionOnProtected() throws Throwable {
50+
withCtap2Session(
51+
"This device has no PIN set",
52+
(device, session) -> hasPin(session),
53+
BasicWebAuthnClientTests::testUvDiscouragedMakeCredentialGetAssertion);
54+
}
55+
}

0 commit comments

Comments
 (0)