From d64aa667f250c8d0f82c0805fb7203b8cb67796f Mon Sep 17 00:00:00 2001 From: Adam Velebil Date: Wed, 16 Oct 2024 16:00:41 +0200 Subject: [PATCH] PRF instrumented tests --- .../yubikit/fido/webauthn/Extension.java | 30 +-- .../fido/ExtensionsInstrumentedTests.java | 10 + .../yubikit/testing/fido/ExtensionsTests.java | 174 +++++++++++++++++- 3 files changed, 199 insertions(+), 15 deletions(-) diff --git a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/Extension.java b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/Extension.java index 5007165a..b24ff925 100644 --- a/fido/src/main/java/com/yubico/yubikit/fido/webauthn/Extension.java +++ b/fido/src/main/java/com/yubico/yubikit/fido/webauthn/Extension.java @@ -228,14 +228,20 @@ public Object processGetInput(Map inputs) { return null; } - Salts salts; Map data = (Map) inputs.get("prf"); if (data != null) { Map secrets = (Map) data.get("eval"); + if (secrets == null) { + return null; + } + String firstInput = (String) secrets.get("first"); + if (firstInput == null) { + return null; + } byte[] first = SerializationUtils.deserializeBytes( - Objects.requireNonNull((String) secrets.get("first")), + Objects.requireNonNull(firstInput), SerializationType.JSON); byte[] second = secrets.containsKey("second") @@ -271,9 +277,9 @@ public Object processGetInput(Map inputs) { final ClientPin clientPin = new ClientPin(ctap, pinUvAuthProtocol); try { - Pair, byte[]> keyAgreemenbt = clientPin.getSharedSecret(); + Pair, byte[]> keyAgreement = clientPin.getSharedSecret(); - this.sharedSecret = keyAgreemenbt.second; + this.sharedSecret = keyAgreement.second; byte[] saltEnc = pinUvAuthProtocol.encrypt( sharedSecret, @@ -284,11 +290,11 @@ public Object processGetInput(Map inputs) { .array()); byte[] saltAuth = pinUvAuthProtocol.authenticate( - keyAgreemenbt.second, + keyAgreement.second, saltEnc); final Map hmacGetSecretInput = new HashMap<>(); - hmacGetSecretInput.put(1, keyAgreemenbt.first); + hmacGetSecretInput.put(1, keyAgreement.first); hmacGetSecretInput.put(2, saltEnc); hmacGetSecretInput.put(3, saltAuth); hmacGetSecretInput.put(4, clientPin.getPinUvAuth().getVersion()); @@ -322,7 +328,9 @@ public ExtensionResult processGetOutput( byte[] decrypted = this.pinUvAuthProtocol.decrypt(sharedSecret, value); byte[] output1 = Arrays.copyOf(decrypted, SALT_LEN); - byte[] output2 = Arrays.copyOfRange(decrypted, SALT_LEN, 2 * SALT_LEN); + byte[] output2 = decrypted.length > SALT_LEN + ? Arrays.copyOfRange(decrypted, SALT_LEN, 2 * SALT_LEN) + : new byte[0]; Logger.debug(logger, "Decrypted: {}, o1: {}, o2: {}", StringUtils.bytesToHex(decrypted), @@ -451,7 +459,7 @@ public InputWithPermission processGetInputWithPermissions(Map inputs) @SuppressWarnings("unchecked") Map data = inputs.containsKey("largeBlob") ? (Map) processGetInput((Map) inputs.get("largeBlob")) - : Collections.emptyMap(); + : null; int permissions = ClientPin.PIN_PERMISSION_NONE; @@ -519,12 +527,6 @@ public Object processCreateInput(Map inputs) { return null; } - - @Nullable - @Override - public Object processGetInput(Map inputs) { - return super.processGetInput(inputs); - } } static class CredProtectExtension extends Extension { diff --git a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/ExtensionsInstrumentedTests.java b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/ExtensionsInstrumentedTests.java index e64e6bfc..57ced47a 100644 --- a/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/ExtensionsInstrumentedTests.java +++ b/testing-android/src/androidTest/java/com/yubico/yubikit/testing/fido/ExtensionsInstrumentedTests.java @@ -37,6 +37,16 @@ public static class PinUvAuthV2Test extends FidoInstrumentedTests { public void testCredPropsExtension() throws Throwable { withDevice(ExtensionsTests::testCredPropsExtension); } + + @Test + public void testPrfExtension() throws Throwable { + withDevice(ExtensionsTests::testPrfExtension); + } + + @Test + public void testPrfExtensionNoSupport() throws Throwable { + withDevice(ExtensionsTests::testPrfExtensionNoSupport); + } } @Category(PinUvAuthProtocolV1Test.class) diff --git a/testing/src/main/java/com/yubico/yubikit/testing/fido/ExtensionsTests.java b/testing/src/main/java/com/yubico/yubikit/testing/fido/ExtensionsTests.java index 94137ee3..054d0549 100644 --- a/testing/src/main/java/com/yubico/yubikit/testing/fido/ExtensionsTests.java +++ b/testing/src/main/java/com/yubico/yubikit/testing/fido/ExtensionsTests.java @@ -36,10 +36,12 @@ import com.yubico.yubikit.fido.webauthn.SerializationType; import org.junit.Assert; +import org.junit.Assume; import java.io.IOException; import java.nio.charset.StandardCharsets; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; @@ -89,9 +91,174 @@ public static void testCredPropsExtension(FidoTestState state) throws Throwable }); } + @SuppressWarnings("unchecked") + public static void testPrfExtension(FidoTestState state) throws Throwable { + final String PRF_EXT = "prf"; + + // non-discoverable credential + { + // no output when no input + state.withCtap2(session -> { + Assume.assumeTrue(session.getCachedInfo().getExtensions().contains("hmac-secret")); + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredential cred = new Builder(webauthn).create(); + Map result = getResult(cred.getClientExtensionResults(), PRF_EXT); + Assert.assertNull(result); + }); + + // { prf: { enabled: true } } + PublicKeyCredentialDescriptor credDesc = state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredential cred = new Builder(webauthn) + .extensions(Collections.singletonMap(PRF_EXT, Collections.emptyMap())) + .create(); + Map result = getResult(cred.getClientExtensionResults(), PRF_EXT); + Assert.assertNotNull(result); + Assert.assertEquals(Boolean.TRUE, result.get("enabled")); + return new PublicKeyCredentialDescriptor("public-key", cred.getRawId()); + }); + + // assertion with { eval: { first: "value" } } + // { prf: { results: { first: String } } } + state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredential cred = new Builder(webauthn) + // this is no discoverable key, we have to pass the id + .allowedCredentials(Collections.singletonList(credDesc)) + .extensions(Collections.singletonMap(PRF_EXT, + Collections.singletonMap("eval", + Collections.singletonMap("first", "abba")))) + .getAssertions(); + Map result = getResult(cred.getClientExtensionResults(), PRF_EXT); + Assert.assertNotNull(result); + Assert.assertFalse(result.containsKey("enabled")); + Assert.assertTrue(result.containsKey("results")); + Map results = (Map) result.get("results"); + Assert.assertTrue(results.containsKey("first")); + Assert.assertTrue(results.get("first") instanceof String); + Assert.assertFalse(results.containsKey("second")); + }); + + // assertion with { eval: { first: "value", second: "value" } } + // { prf: { results: { first: String, second: String } } } + state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + + Map eval = new HashMap<>(); + eval.put("first", "abba"); + eval.put("second", "bebe"); + + PublicKeyCredential cred = new Builder(webauthn) + // this is no discoverable key, we have to pass the id + .allowedCredentials(Collections.singletonList(credDesc)) + .extensions(Collections.singletonMap(PRF_EXT, + Collections.singletonMap("eval", eval))) + .getAssertions(); + Map result = getResult(cred.getClientExtensionResults(), PRF_EXT); + Assert.assertNotNull(result); + Assert.assertFalse(result.containsKey("enabled")); + Assert.assertTrue(result.containsKey("results")); + Map results = (Map) result.get("results"); + Assert.assertTrue(results.containsKey("first")); + Assert.assertTrue(results.get("first") instanceof String); + Assert.assertTrue(results.containsKey("second")); + Assert.assertTrue(results.get("second") instanceof String); + }); + } + + // discoverable credential + { + // no output when no input + state.withCtap2(session -> { + Assume.assumeTrue(session.getCachedInfo().getExtensions().contains("hmac-secret")); + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredential cred = new Builder(webauthn).residentKey(true).create(); + Map result = getResult(cred.getClientExtensionResults(), PRF_EXT); + Assert.assertNull(result); + deleteCredentials(webauthn, Collections.singletonList(cred.getRawId())); + }); + + // { prf: { enabled: true } } + PublicKeyCredentialDescriptor credDesc = state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredential cred = new Builder(webauthn) + .residentKey(true) + .extensions(Collections.singletonMap(PRF_EXT, Collections.emptyMap())) + .create(); + Map result = getResult(cred.getClientExtensionResults(), PRF_EXT); + Assert.assertNotNull(result); + Assert.assertEquals(Boolean.TRUE, result.get("enabled")); + return new PublicKeyCredentialDescriptor("public-key", cred.getRawId()); + }); + + // assertion with { eval: { first: "value" } } + // { prf: { results: { first: String } } } + state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredential cred = new Builder(webauthn) + .extensions(Collections.singletonMap(PRF_EXT, + Collections.singletonMap("eval", + Collections.singletonMap("first", "abba")))) + .getAssertions(); + Map result = getResult(cred.getClientExtensionResults(), PRF_EXT); + Assert.assertNotNull(result); + Assert.assertFalse(result.containsKey("enabled")); + Assert.assertTrue(result.containsKey("results")); + Map results = (Map) result.get("results"); + Assert.assertTrue(results.containsKey("first")); + Assert.assertTrue(results.get("first") instanceof String); + Assert.assertFalse(results.containsKey("second")); + }); + + // assertion with { eval: { first: "value", second: "value" } } + // { prf: { results: { first: String, second: String } } } + state.withCtap2(session -> { + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + + Map eval = new HashMap<>(); + eval.put("first", "abba"); + eval.put("second", "bebe"); + + PublicKeyCredential cred = new Builder(webauthn) + .extensions(Collections.singletonMap(PRF_EXT, + Collections.singletonMap("eval", eval))) + .getAssertions(); + Map result = getResult(cred.getClientExtensionResults(), PRF_EXT); + Assert.assertNotNull(result); + Assert.assertFalse(result.containsKey("enabled")); + Assert.assertTrue(result.containsKey("results")); + Map results = (Map) result.get("results"); + Assert.assertTrue(results.containsKey("first")); + Assert.assertTrue(results.get("first") instanceof String); + Assert.assertTrue(results.containsKey("second")); + Assert.assertTrue(results.get("second") instanceof String); + + deleteCredentials(webauthn, Collections.singletonList(credDesc.getId())); + }); + } + } + + // this test is active only on devices without hmac-secret + public static void testPrfExtensionNoSupport(FidoTestState state) throws Throwable { + final String PRF_EXT = "prf"; + + // { prf: { enabled: false } } + state.withCtap2(session -> { + Assume.assumeFalse(session.getCachedInfo().getExtensions().contains("hmac-secret")); + BasicWebAuthnClient webauthn = new BasicWebAuthnClient(session); + PublicKeyCredential cred = new Builder(webauthn) + .extensions(Collections.singletonMap(PRF_EXT, Collections.emptyMap())) + .create(); + Map result = getResult(cred.getClientExtensionResults(), PRF_EXT); + Assert.assertNotNull(result); + Assert.assertEquals(Boolean.FALSE, result.get("enabled")); + }); + } static class Builder { final BasicWebAuthnClient client; + + List allowedCredentials = null; boolean residentKey = false; @Nullable @@ -113,6 +280,11 @@ Builder extensions(@Nullable Map extensions) { return this; } + Builder allowedCredentials(@Nullable List allowedCredentials) { + this.allowedCredentials = allowedCredentials; + return this; + } + PublicKeyCredential create() throws IOException, CommandException, ClientError { PublicKeyCredentialCreationOptions options = getCreateOptions( new PublicKeyCredentialUserEntity( @@ -139,7 +311,7 @@ PublicKeyCredential getAssertions() throws IOException, CommandException, Client TestData.CHALLENGE, (long) 90000, TestData.RP_ID, - null, + allowedCredentials, null, extensions );