diff --git a/java/client/src/org/openqa/selenium/remote/DriverCommand.java b/java/client/src/org/openqa/selenium/remote/DriverCommand.java index a4dd43aee1cc1..2cfc67947d229 100644 --- a/java/client/src/org/openqa/selenium/remote/DriverCommand.java +++ b/java/client/src/org/openqa/selenium/remote/DriverCommand.java @@ -323,4 +323,9 @@ static CommandPayload SET_CURRENT_WINDOW_SIZE(Dimension targetSize) { // http://w3c.github.io/webauthn#sctn-automation String ADD_VIRTUAL_AUTHENTICATOR = "addVirtualAuthenticator"; String REMOVE_VIRTUAL_AUTHENTICATOR = "removeVirtualAuthenticator"; + String ADD_CREDENTIAL = "addCredential"; + String GET_CREDENTIALS = "getCredentials"; + String REMOVE_CREDENTIAL = "removeCredential"; + String REMOVE_ALL_CREDENTIALS = "removeAllCredentials"; + String SET_USER_VERIFIED = "setUserVerified"; } diff --git a/java/client/src/org/openqa/selenium/remote/RemoteWebDriver.java b/java/client/src/org/openqa/selenium/remote/RemoteWebDriver.java index cf2ebd34d0229..1154fba74df22 100644 --- a/java/client/src/org/openqa/selenium/remote/RemoteWebDriver.java +++ b/java/client/src/org/openqa/selenium/remote/RemoteWebDriver.java @@ -67,11 +67,13 @@ import org.openqa.selenium.logging.Logs; import org.openqa.selenium.logging.NeedsLocalLogs; import org.openqa.selenium.remote.internal.WebElementToJsonConverter; +import org.openqa.selenium.virtualauthenticator.Credential; import org.openqa.selenium.virtualauthenticator.HasVirtualAuthenticator; import org.openqa.selenium.virtualauthenticator.VirtualAuthenticator; import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions; import java.net.URL; +import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.Date; @@ -669,7 +671,7 @@ public Mouse getMouse() { public VirtualAuthenticator addVirtualAuthenticator(VirtualAuthenticatorOptions options) { String authenticatorId = (String) execute(DriverCommand.ADD_VIRTUAL_AUTHENTICATOR, options.toMap()).getValue(); - return new VirtualAuthenticator(authenticatorId); + return new RemoteVirtualAuthenticator(authenticatorId); } @Override @@ -1058,6 +1060,57 @@ public void sendKeys(String keysToSend) { } } + private class RemoteVirtualAuthenticator implements VirtualAuthenticator { + private final String id; + + public RemoteVirtualAuthenticator(final String id) { + this.id = Objects.requireNonNull(id); + } + + @Override + public String getId() { + return id; + } + + @Override + public void addCredential(Credential credential) { + execute(DriverCommand.ADD_CREDENTIAL, + new ImmutableMap.Builder() + .putAll(credential.toMap()) + .put("authenticatorId", id) + .build()); + } + + @Override + public List getCredentials() { + List> response = (List>) + execute(DriverCommand.GET_CREDENTIALS, ImmutableMap.of("authenticatorId", id)).getValue(); + return response.stream().map(Credential::fromMap).collect(Collectors.toList()); + } + + @Override + public void removeCredential(byte[] credentialId) { + removeCredential(Base64.getUrlEncoder().encodeToString(credentialId)); + } + + @Override + public void removeCredential(String credentialId) { + execute(DriverCommand.REMOVE_CREDENTIAL, + ImmutableMap.of("authenticatorId", id, "credentialId", credentialId)).getValue(); + } + + @Override + public void removeAllCredentials() { + execute(DriverCommand.REMOVE_ALL_CREDENTIALS, ImmutableMap.of("authenticatorId", id)); + } + + @Override + public void setUserVerified(boolean verified) { + execute(DriverCommand.SET_USER_VERIFIED, + ImmutableMap.of("authenticatorId", id, "isUserVerified", verified)); + } + } + public enum When { BEFORE, AFTER, diff --git a/java/client/src/org/openqa/selenium/remote/codec/AbstractHttpCommandCodec.java b/java/client/src/org/openqa/selenium/remote/codec/AbstractHttpCommandCodec.java index 17d8e5e857c41..a39924cefc193 100644 --- a/java/client/src/org/openqa/selenium/remote/codec/AbstractHttpCommandCodec.java +++ b/java/client/src/org/openqa/selenium/remote/codec/AbstractHttpCommandCodec.java @@ -26,6 +26,7 @@ import static java.nio.charset.StandardCharsets.UTF_8; import static org.openqa.selenium.json.Json.MAP_TYPE; import static org.openqa.selenium.remote.DriverCommand.ADD_COOKIE; +import static org.openqa.selenium.remote.DriverCommand.ADD_CREDENTIAL; import static org.openqa.selenium.remote.DriverCommand.ADD_VIRTUAL_AUTHENTICATOR; import static org.openqa.selenium.remote.DriverCommand.CLEAR_ELEMENT; import static org.openqa.selenium.remote.DriverCommand.CLICK_ELEMENT; @@ -47,6 +48,7 @@ import static org.openqa.selenium.remote.DriverCommand.GET_CAPABILITIES; import static org.openqa.selenium.remote.DriverCommand.GET_CONTEXT_HANDLES; import static org.openqa.selenium.remote.DriverCommand.GET_COOKIE; +import static org.openqa.selenium.remote.DriverCommand.GET_CREDENTIALS; import static org.openqa.selenium.remote.DriverCommand.GET_CURRENT_CONTEXT_HANDLE; import static org.openqa.selenium.remote.DriverCommand.GET_CURRENT_URL; import static org.openqa.selenium.remote.DriverCommand.GET_ELEMENT_LOCATION; @@ -77,6 +79,8 @@ import static org.openqa.selenium.remote.DriverCommand.NEW_SESSION; import static org.openqa.selenium.remote.DriverCommand.QUIT; import static org.openqa.selenium.remote.DriverCommand.REFRESH; +import static org.openqa.selenium.remote.DriverCommand.REMOVE_ALL_CREDENTIALS; +import static org.openqa.selenium.remote.DriverCommand.REMOVE_CREDENTIAL; import static org.openqa.selenium.remote.DriverCommand.REMOVE_VIRTUAL_AUTHENTICATOR; import static org.openqa.selenium.remote.DriverCommand.SCREENSHOT; import static org.openqa.selenium.remote.DriverCommand.SEND_KEYS_TO_ELEMENT; @@ -88,6 +92,7 @@ import static org.openqa.selenium.remote.DriverCommand.SET_SCREEN_ROTATION; import static org.openqa.selenium.remote.DriverCommand.SET_SCRIPT_TIMEOUT; import static org.openqa.selenium.remote.DriverCommand.SET_TIMEOUT; +import static org.openqa.selenium.remote.DriverCommand.SET_USER_VERIFIED; import static org.openqa.selenium.remote.DriverCommand.STATUS; import static org.openqa.selenium.remote.DriverCommand.SWITCH_TO_CONTEXT; import static org.openqa.selenium.remote.DriverCommand.SWITCH_TO_FRAME; @@ -221,6 +226,16 @@ public AbstractHttpCommandCodec() { defineCommand(ADD_VIRTUAL_AUTHENTICATOR, post("/session/:sessionId/webauthn/authenticator")); defineCommand(REMOVE_VIRTUAL_AUTHENTICATOR, delete("/session/:sessionId/webauthn/authenticator/:authenticatorId")); + defineCommand(ADD_CREDENTIAL, + post("/session/:sessionId/webauthn/authenticator/:authenticatorId/credential")); + defineCommand(GET_CREDENTIALS, + get("/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials")); + defineCommand(REMOVE_CREDENTIAL, + delete("/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials/:credentialId")); + defineCommand(REMOVE_ALL_CREDENTIALS, + delete("/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials")); + defineCommand(SET_USER_VERIFIED, + post("/session/:sessionId/webauthn/authenticator/:authenticatorId/uv")); } @Override diff --git a/java/client/src/org/openqa/selenium/virtualauthenticator/Credential.java b/java/client/src/org/openqa/selenium/virtualauthenticator/Credential.java new file mode 100644 index 0000000000000..94c4c65f8f70e --- /dev/null +++ b/java/client/src/org/openqa/selenium/virtualauthenticator/Credential.java @@ -0,0 +1,118 @@ +// Licensed to the Software Freedom Conservancy (SFC) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The SFC licenses this file +// to you 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 org.openqa.selenium.virtualauthenticator; + +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; + +/** + * A credential stored in a virtual authenticator. + * @see https://w3c.github.io/webauthn/#credential-parameters + */ +public class Credential { + + private final byte[] id; + private final boolean isResidentCredential; + private final String rpId; + private final PKCS8EncodedKeySpec privateKey; + private final byte[] userHandle; + private final int signCount; + + /** + * Creates a non resident (i.e. stateless) credential. + */ + public static Credential createNonResidentCredential(byte[] id, String rpId, + PKCS8EncodedKeySpec privateKey, int signCount) { + return new Credential(id, /*isResidentCredential=*/false, Objects.requireNonNull(rpId), + privateKey, /*userHandle=*/null, signCount); + } + + /** + * Creates a resident (i.e. stateful) credential. + */ + public static Credential createResidentCredential(byte[] id, String rpId, + PKCS8EncodedKeySpec privateKey, byte[] userHandle, int signCount) { + return new Credential(id, /*isResidentCredential=*/true, Objects.requireNonNull(rpId), + privateKey, Objects.requireNonNull(userHandle), signCount); + } + + /** + * Creates a credential from a map. + */ + public static Credential fromMap(Map map) { + Base64.Decoder decoder = Base64.getUrlDecoder(); + return new Credential(decoder.decode((String) map.get("credentialId")), + (boolean) map.get("isResidentCredential"), + (String) map.get("rpId"), + new PKCS8EncodedKeySpec(decoder.decode((String) map.get("privateKey"))), + map.get("userHandle") == null ? null : decoder.decode((String) map.get("userHandle")), + ((Long) map.get("signCount")).intValue()); + } + + private Credential(byte[] id, boolean isResidentCredential, String rpId, + PKCS8EncodedKeySpec privateKey, byte[] userHandle, int signCount) { + this.id = Objects.requireNonNull(id); + this.isResidentCredential = isResidentCredential; + this.rpId = rpId; + this.privateKey = Objects.requireNonNull(privateKey); + this.userHandle = userHandle; + this.signCount = signCount; + } + + public byte[] getId() { + return id; + } + + public boolean isResidentCredential() { + return isResidentCredential; + } + + public String getRpId() { + return rpId; + } + + public PKCS8EncodedKeySpec getPrivateKey() { + return privateKey; + } + + public byte[] getUserHandle() { + return userHandle; + } + + public int getSignCount() { + return signCount; + } + + public Map toMap() { + Base64.Encoder encoder = Base64.getUrlEncoder(); + Map map = new HashMap(); + map.put("credentialId", encoder.encodeToString(id)); + map.put("isResidentCredential", isResidentCredential); + map.put("rpId", rpId); + map.put("privateKey", encoder.encodeToString(privateKey.getEncoded())); + map.put("signCount", signCount); + if (userHandle != null) { + map.put("userHandle", encoder.encodeToString(userHandle)); + } + return Collections.unmodifiableMap(map); + } +} diff --git a/java/client/src/org/openqa/selenium/virtualauthenticator/HasVirtualAuthenticator.java b/java/client/src/org/openqa/selenium/virtualauthenticator/HasVirtualAuthenticator.java index 9a18c27b58a3b..780fdd76fb358 100644 --- a/java/client/src/org/openqa/selenium/virtualauthenticator/HasVirtualAuthenticator.java +++ b/java/client/src/org/openqa/selenium/virtualauthenticator/HasVirtualAuthenticator.java @@ -24,7 +24,15 @@ * Interface implemented by each driver that allows access to the virtual authenticator API. */ public interface HasVirtualAuthenticator { + /** + * Adds a virtual authenticator with the given options. + * @return the new virtual authenticator. + */ public VirtualAuthenticator addVirtualAuthenticator(VirtualAuthenticatorOptions options); + /** + * Removes a previously added virtual authenticator. The authenticator is no + * longer valid after removal, so no methods may be called. + */ public void removeVirtualAuthenticator(VirtualAuthenticator authenticator); } diff --git a/java/client/src/org/openqa/selenium/virtualauthenticator/VirtualAuthenticator.java b/java/client/src/org/openqa/selenium/virtualauthenticator/VirtualAuthenticator.java index 17ab5d1cb6b6a..216b37ee69ea7 100644 --- a/java/client/src/org/openqa/selenium/virtualauthenticator/VirtualAuthenticator.java +++ b/java/client/src/org/openqa/selenium/virtualauthenticator/VirtualAuthenticator.java @@ -17,20 +17,53 @@ package org.openqa.selenium.virtualauthenticator; +import org.openqa.selenium.virtualauthenticator.Credential; + +import java.util.List; import java.util.Objects; /** * Represents a virtual authenticator. */ -public class VirtualAuthenticator { +public interface VirtualAuthenticator { + + /** + * @return the authenticator unique identifier. + */ + public String getId(); + + /** + * Injects a credential into the authenticator. + */ + public void addCredential(Credential credential); + + /** + * @return the list of credentials owned by the authenticator. + */ + public List getCredentials(); + + /** + * Removes a credential from the authenticator. + * @param credentialId the ID of the credential to be removed. + */ + public void removeCredential(byte[] credentialId); - private final String id; + /** + * Removes a credential from the authenticator. + * @param credentialId the ID of the credential to be removed as a base64url + * string. + */ + public void removeCredential(String credentialId); - public VirtualAuthenticator(final String id) { - this.id = Objects.requireNonNull(id); - } + /** + * Removes all the credentials from the authenticator. + */ + public void removeAllCredentials(); - public String getId() { - return id; - } + /** + * Sets whether the authenticator will simulate success or fail on user verification. + * @param verified true if the authenticator will pass user verification, + * false otherwise. + */ + public void setUserVerified(boolean verified); } diff --git a/java/client/src/org/openqa/selenium/virtualauthenticator/VirtualAuthenticatorOptions.java b/java/client/src/org/openqa/selenium/virtualauthenticator/VirtualAuthenticatorOptions.java index 0361073786991..7aeb5939fd24c 100644 --- a/java/client/src/org/openqa/selenium/virtualauthenticator/VirtualAuthenticatorOptions.java +++ b/java/client/src/org/openqa/selenium/virtualauthenticator/VirtualAuthenticatorOptions.java @@ -17,12 +17,13 @@ package org.openqa.selenium.virtualauthenticator; +import java.util.Collections; import java.util.HashMap; import java.util.Map; /** * Options for the creation of virtual authenticators. - * @see http://w3c.github.io/webauthn/#sctn-automation + * @see https://w3c.github.io/webauthn/#sctn-automation */ public class VirtualAuthenticatorOptions { @@ -90,13 +91,13 @@ public VirtualAuthenticatorOptions setIsUserVerified(boolean isUserVerified) { } public Map toMap() { - HashMap map = new HashMap(); + Map map = new HashMap(); map.put("protocol", protocol.id); map.put("transport", transport.id); map.put("hasResidentKey", hasResidentKey); map.put("hasUserVerification", hasUserVerification); map.put("isUserConsenting", isUserConsenting); map.put("isUserVerified", isUserVerified); - return map; + return Collections.unmodifiableMap(map); } } diff --git a/java/client/test/org/openqa/selenium/virtualauthenticator/VirtualAuthenticatorTest.java b/java/client/test/org/openqa/selenium/virtualauthenticator/VirtualAuthenticatorTest.java index 7a2326e0ca2d4..013f091f346a7 100644 --- a/java/client/test/org/openqa/selenium/virtualauthenticator/VirtualAuthenticatorTest.java +++ b/java/client/test/org/openqa/selenium/virtualauthenticator/VirtualAuthenticatorTest.java @@ -19,7 +19,11 @@ import static org.assertj.core.api.Assumptions.assumeThat; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Fail.fail; +import java.security.spec.PKCS8EncodedKeySpec; +import java.util.Base64; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.openqa.selenium.By; @@ -27,14 +31,30 @@ import org.openqa.selenium.environment.webserver.Page; import org.openqa.selenium.testing.JUnit4TestBase; import org.openqa.selenium.testing.NotYetImplemented; +import org.openqa.selenium.virtualauthenticator.Credential; import org.openqa.selenium.virtualauthenticator.HasVirtualAuthenticator; import org.openqa.selenium.virtualauthenticator.VirtualAuthenticator; import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions; +import org.openqa.selenium.virtualauthenticator.VirtualAuthenticatorOptions.Protocol; +import java.util.Arrays; +import java.util.ArrayList; import java.util.Map; +import java.util.List; public class VirtualAuthenticatorTest extends JUnit4TestBase { + /** + * A pkcs#8 encoded unencrypted EC256 private key as a base64url string. + */ + private final String base64EncodedPK = + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8_zMDQDYAxlU-Q" + + "hk1Dwkf0v18GZca1DMF3SaJ9HPdmShRANCAASNYX5lyVCOZLzFZzrIKmeZ2jwU" + + "RmgsJYxGP__fWN_S-j5sN4tT15XEpN_7QZnt14YvI6uvAgO0uJEboFaZlOEB"; + + private final PKCS8EncodedKeySpec privateKey = + new PKCS8EncodedKeySpec(Base64.getUrlDecoder().decode(base64EncodedPK)); + private final String script = "async function registerCredential(options = {}) {" + " options = Object.assign({" @@ -45,14 +65,14 @@ public class VirtualAuthenticatorTest extends JUnit4TestBase { + " id: \"localhost\"," + " name: \"Selenium WebDriver Test\"," + " }," - + " challenge: Uint8Array.from(\"challenge\")," + + " challenge: Int8Array.from(\"challenge\")," + " pubKeyCredParams: [" + " {type: \"public-key\", alg: -7}," + " ]," + " user: {" + " name: \"name\"," + " displayName: \"displayName\"," - + " id: Uint8Array.from([1])," + + " id: Int8Array.from([1])," + " }," + " }, options);" @@ -62,7 +82,7 @@ public class VirtualAuthenticatorTest extends JUnit4TestBase { + " status: \"OK\"," + " credential: {" + " id: credential.id," - + " rawId: Array.from(new Uint8Array(credential.rawId))," + + " rawId: Array.from(new Int8Array(credential.rawId))," + " transports: credential.response.getTransports()," + " }" + " };" @@ -71,11 +91,11 @@ public class VirtualAuthenticatorTest extends JUnit4TestBase { + " }" + "}" - + "async function getCredential(credential, options = {}) {" + + "async function getCredential(credentials, options = {}) {" + " options = Object.assign({" - + " challenge: Uint8Array.from(\"Winter is Coming\")," + + " challenge: Int8Array.from(\"Winter is Coming\")," + " rpId: \"localhost\"," - + " allowCredentials: [credential]," + + " allowCredentials: credentials," + " userVerification: \"preferred\"," + " }, options);" @@ -83,13 +103,17 @@ public class VirtualAuthenticatorTest extends JUnit4TestBase { + " const attestation = await navigator.credentials.get({publicKey: options});" + " return {" + " status: \"OK\"," - + " attestation," + + " attestation: {" + + " userHandle: new Int8Array(attestation.response.userHandle)," + + " }," + " };" + " } catch (error) {" + " return {status: error.toString()};" + " }" + "}"; + private VirtualAuthenticator authenticator; + @Before public void setup() { assumeThat(driver).isInstanceOf(HasVirtualAuthenticator.class); @@ -98,25 +122,59 @@ public void setup() { .withScripts(script))); } + private void createSimpleU2FAuthenticator() { + VirtualAuthenticatorOptions options = new VirtualAuthenticatorOptions() + .setProtocol(Protocol.U2F); + authenticator = ((HasVirtualAuthenticator) driver).addVirtualAuthenticator(options); + } + + private void createRKEnabledAuthenticator() { + VirtualAuthenticatorOptions options = new VirtualAuthenticatorOptions() + .setProtocol(Protocol.CTAP2) + .setHasResidentKey(true) + .setHasUserVerification(true) + .setIsUserVerified(true); + authenticator = ((HasVirtualAuthenticator) driver).addVirtualAuthenticator(options); + } + + /** + * @param list a list of numbers between -128 and 127. + * @return a byte array containing the list. + */ + private byte[] convertListIntoArrayOfBytes(List list) { + byte[] ret = new byte[list.size()]; + for (int i = 0; i < list.size(); ++i) + ret[i] = list.get(i).byteValue(); + return ret; + } + + private Map getAssertionFor(Object credentialId) { + return (Map) + ((JavascriptExecutor) driver).executeAsyncScript( + "getCredential([{" + + " \"type\": \"public-key\"," + + " \"id\": Int8Array.from(arguments[0])," + + "}]).then(arguments[arguments.length - 1]);", credentialId); + } + + @After + public void tearDown() { + if (authenticator != null) { + ((HasVirtualAuthenticator) driver).removeVirtualAuthenticator(authenticator); + } + } + @Test public void testCreateAuthenticator() { // Register a credential on the Virtual Authenticator. - VirtualAuthenticatorOptions options = new VirtualAuthenticatorOptions(); - ((HasVirtualAuthenticator) driver).addVirtualAuthenticator(options); + createSimpleU2FAuthenticator(); Map response = (Map) ((JavascriptExecutor) driver).executeAsyncScript( "registerCredential().then(arguments[arguments.length - 1]);"); assertThat(response.get("status")).isEqualTo("OK"); // Attempt to use the credential to get an assertion. - Object credentialId = ((Map) response.get("credential")).get("rawId"); - response = (Map) - ((JavascriptExecutor) driver).executeAsyncScript( - "getCredential({" - + " \"type\": \"public-key\"," - + " \"id\": Uint8Array.from(arguments[0])," - + "}).then(arguments[arguments.length - 1]);", credentialId); - + response = getAssertionFor(((Map) response.get("credential")).get("rawId")); assertThat(response.get("status")).isEqualTo("OK"); } @@ -128,4 +186,205 @@ public void testRemoveAuthenticator() { ((HasVirtualAuthenticator) driver).removeVirtualAuthenticator(authenticator); // no exceptions. } + + @Test + public void testAddNonResidentCredential() { + // Add a non-resident credential using the testing API. + createSimpleU2FAuthenticator(); + byte[] credentialId = {1, 2, 3, 4}; + Credential credential = Credential.createNonResidentCredential( + credentialId, "localhost", privateKey, /*signCount=*/0); + authenticator.addCredential(credential); + + // Attempt to use the credential to generate an assertion. + Map response = getAssertionFor(Arrays.asList(1, 2, 3, 4)); + assertThat(response.get("status")).isEqualTo("OK"); + } + + @Test + public void testAddResidentCredential() { + // Add a resident credential using the testing API. + createRKEnabledAuthenticator(); + byte[] credentialId = {1, 2, 3, 4}; + byte[] userHandle = {1}; + Credential credential = Credential.createResidentCredential( + credentialId, "localhost", privateKey, userHandle, /*signCount=*/0); + authenticator.addCredential(credential); + + // Attempt to use the credential to generate an assertion. Notice we use an + // empty allowCredentials array. + Map response = (Map) + ((JavascriptExecutor) driver).executeAsyncScript( + "getCredential([]).then(arguments[arguments.length - 1]);"); + + assertThat(response.get("status")).isEqualTo("OK"); + + Map attestation = (Map) response.get("attestation"); + assertThat((List) attestation.get("userHandle")).containsExactly(1L); + } + + @Test + public void testGetCredentials() { + // Create an authenticator and add two credentials. + createRKEnabledAuthenticator(); + + // Register a resident credential. + Map response1 = (Map) + ((JavascriptExecutor) driver).executeAsyncScript( + "registerCredential({authenticatorSelection: {requireResidentKey: true}})" + + " .then(arguments[arguments.length - 1]);"); + assertThat(response1.get("status")).isEqualTo("OK"); + Map credential1Json = (Map) response1.get("credential"); + byte[] credential1Id = convertListIntoArrayOfBytes((ArrayList) credential1Json.get("rawId")); + + // Register a non resident credential. + Map response2 = (Map) + ((JavascriptExecutor) driver).executeAsyncScript( + "registerCredential().then(arguments[arguments.length - 1]);"); + assertThat(response2.get("status")).isEqualTo("OK"); + Map credential2Json = (Map) response2.get("credential"); + byte[] credential2Id = convertListIntoArrayOfBytes((ArrayList) credential2Json.get("rawId")); + + assertThat(credential1Id).isNotEqualTo(credential2Id); + + // Retrieve the two credentials. + List credentials = authenticator.getCredentials(); + assertThat(credentials.size()).isEqualTo(2); + + Credential credential1 = null; + Credential credential2 = null; + for (Credential credential : credentials) { + if (Arrays.equals(credential.getId(), credential1Id)) { + credential1 = credential; + } else if (Arrays.equals(credential.getId(), credential2Id)) { + credential2 = credential; + } else { + fail("Unrecognized credential id"); + } + } + + assertThat(credential1.isResidentCredential()).isTrue(); + assertThat(credential1.getPrivateKey()).isNotNull(); + assertThat(credential1.getRpId()).isEqualTo("localhost"); + assertThat(credential1.getUserHandle()).isEqualTo(new byte[] {1}); + assertThat(credential1.getSignCount()).isEqualTo(1); + + assertThat(credential2.isResidentCredential()).isFalse(); + assertThat(credential2.getPrivateKey()).isNotNull(); + // Non resident keys do not store raw RP IDs or user handles. + assertThat(credential2.getRpId()).isNull(); + assertThat(credential2.getUserHandle()).isNull(); + assertThat(credential2.getSignCount()).isEqualTo(1); + } + + @Test + public void testRemoveCredentialByRawId() { + createSimpleU2FAuthenticator(); + + // Register credential. + Map response = (Map) + ((JavascriptExecutor) driver).executeAsyncScript( + "registerCredential().then(arguments[arguments.length - 1]);"); + assertThat(response.get("status")).isEqualTo("OK"); + Map credentialJson = (Map) response.get("credential"); + + // Remove a credential by its ID as an array of bytes. + byte[] rawCredentialId = + convertListIntoArrayOfBytes((ArrayList) credentialJson.get("rawId")); + authenticator.removeCredential(rawCredentialId); + + // Trying to get an assertion should fail. + response = getAssertionFor(credentialJson.get("rawId")); + assertThat((String) response.get("status")).startsWith("NotAllowedError"); + } + + @Test + public void testRemoveCredentialByBase64UrlId() { + createSimpleU2FAuthenticator(); + + // Register credential. + Map response = (Map) + ((JavascriptExecutor) driver).executeAsyncScript( + "registerCredential().then(arguments[arguments.length - 1]);"); + assertThat(response.get("status")).isEqualTo("OK"); + Map credentialJson = (Map) response.get("credential"); + + // Remove a credential by its base64url ID. + String credentialId = (String) credentialJson.get("id"); + authenticator.removeCredential(credentialId); + + // Trying to get an assertion should fail. + response = getAssertionFor(credentialJson.get("rawId")); + assertThat((String) response.get("status")).startsWith("NotAllowedError"); + } + + @Test + public void testRemoveAllCredentials() { + createSimpleU2FAuthenticator(); + + // Register two credentials. + Map response1 = (Map) + ((JavascriptExecutor) driver).executeAsyncScript( + "registerCredential().then(arguments[arguments.length - 1]);"); + assertThat(response1.get("status")).isEqualTo("OK"); + Map credential1Json = (Map) response1.get("credential"); + + Map response2 = (Map) + ((JavascriptExecutor) driver).executeAsyncScript( + "registerCredential().then(arguments[arguments.length - 1]);"); + assertThat(response2.get("status")).isEqualTo("OK"); + Map credential2Json = (Map) response2.get("credential"); + + // Remove all credentials. + authenticator.removeAllCredentials(); + + // Trying to get an assertion allowing for any of both should fail. + Map response = (Map) + ((JavascriptExecutor) driver).executeAsyncScript( + "getCredential([{" + + " \"type\": \"public-key\"," + + " \"id\": Int8Array.from(arguments[0])," + + "}, {" + + " \"type\": \"public-key\"," + + " \"id\": Int8Array.from(arguments[1])," + + "}]).then(arguments[arguments.length - 1]);", + credential1Json.get("rawId"), credential2Json.get("rawId")); + assertThat((String) response.get("status")).startsWith("NotAllowedError"); + } + + @Test + public void testSetUserVerified() { + createRKEnabledAuthenticator(); + + // Register a credential requiring UV. + Map response = (Map) + ((JavascriptExecutor) driver).executeAsyncScript( + "registerCredential({authenticatorSelection: {userVerification: 'required'}})" + + " .then(arguments[arguments.length - 1]);"); + assertThat(response.get("status")).isEqualTo("OK"); + Map credentialJson = (Map) response.get("credential"); + + // Getting an assertion requiring user verification should succeed. + response = (Map) + ((JavascriptExecutor) driver).executeAsyncScript( + "getCredential([{" + + " \"type\": \"public-key\"," + + " \"id\": Int8Array.from(arguments[0])," + + "}], {userVerification: 'required'}).then(arguments[arguments.length - 1]);", + credentialJson.get("rawId")); + assertThat(response.get("status")).isEqualTo("OK"); + + // Disable user verification. + authenticator.setUserVerified(false); + + // Getting an assertion requiring user verification should fail. + response = (Map) + ((JavascriptExecutor) driver).executeAsyncScript( + "getCredential([{" + + " \"type\": \"public-key\"," + + " \"id\": Int8Array.from(arguments[0])," + + "}], {userVerification: 'required'}).then(arguments[arguments.length - 1]);", + credentialJson.get("rawId")); + assertThat((String) response.get("status")).startsWith("NotAllowedError"); + } }