From 0a387e2051621647257351870c11e26e425de315 Mon Sep 17 00:00:00 2001 From: TamsilAmani Date: Fri, 6 May 2022 18:06:17 +0530 Subject: [PATCH] [JS] feat: Added virtual authenticator Relates #10541 --- common/src/web/virtualAuthenticator.html | 73 +++ .../node/selenium-webdriver/lib/command.js | 9 + .../node/selenium-webdriver/lib/http.js | 11 + .../selenium-webdriver/lib/test/fileserver.js | 1 + .../lib/virtual_authenticator.js | 230 ++++++++ .../node/selenium-webdriver/lib/webdriver.js | 113 ++++ .../node/selenium-webdriver/net/index.js | 6 +- .../test/lib/credentials_test.js | 230 ++++++++ .../lib/virtualauthenticatoroptions_test.js | 118 ++++ .../test/virtualAuthenticator_test.js | 519 ++++++++++++++++++ 10 files changed, 1309 insertions(+), 1 deletion(-) create mode 100644 common/src/web/virtualAuthenticator.html create mode 100644 javascript/node/selenium-webdriver/lib/virtual_authenticator.js create mode 100644 javascript/node/selenium-webdriver/test/lib/credentials_test.js create mode 100644 javascript/node/selenium-webdriver/test/lib/virtualauthenticatoroptions_test.js create mode 100644 javascript/node/selenium-webdriver/test/virtualAuthenticator_test.js diff --git a/common/src/web/virtualAuthenticator.html b/common/src/web/virtualAuthenticator.html new file mode 100644 index 0000000000000..20e98044d8795 --- /dev/null +++ b/common/src/web/virtualAuthenticator.html @@ -0,0 +1,73 @@ + + + + + + + + Virtual Authenticator Tests + + + + +

Virtual Authenticator Tests

+ + + + + \ No newline at end of file diff --git a/javascript/node/selenium-webdriver/lib/command.js b/javascript/node/selenium-webdriver/lib/command.js index 290a77f5889f0..c34875f396b21 100644 --- a/javascript/node/selenium-webdriver/lib/command.js +++ b/javascript/node/selenium-webdriver/lib/command.js @@ -164,6 +164,15 @@ const Name = { FIND_ELEMENT_FROM_SHADOWROOT: 'findElementFromShadowRoot', FIND_ELEMENTS_FROM_SHADOWROOT: 'findElementsFromShadowRoot', + // Virtual Authenticator Commands + ADD_VIRTUAL_AUTHENTICATOR : 'addVirtualAuthenticator', + REMOVE_VIRTUAL_AUTHENTICATOR : 'removeVirtualAuthenticator', + ADD_CREDENTIAL : 'addCredential', + GET_CREDENTIALS : 'getCredentials', + REMOVE_CREDENTIAL : 'removeCredential', + REMOVE_ALL_CREDENTIALS : 'removeAllCredentials', + SET_USER_VERIFIED : 'setUserVerified', + GET_AVAILABLE_LOG_TYPES: 'getAvailableLogTypes', GET_LOG: 'getLog', diff --git a/javascript/node/selenium-webdriver/lib/http.js b/javascript/node/selenium-webdriver/lib/http.js index f96faed6e6794..178f3ff7737ab 100644 --- a/javascript/node/selenium-webdriver/lib/http.js +++ b/javascript/node/selenium-webdriver/lib/http.js @@ -347,6 +347,15 @@ const W3C_COMMAND_MAP = new Map([ // Server Extensions [cmd.Name.UPLOAD_FILE, post('/session/:sessionId/se/file')], + + // Virtual Authenticator + [cmd.Name.ADD_VIRTUAL_AUTHENTICATOR, post('/session/:sessionId/webauthn/authenticator')], + [cmd.Name.REMOVE_VIRTUAL_AUTHENTICATOR, del('/session/:sessionId/webauthn/authenticator/:authenticatorId')], + [cmd.Name.ADD_CREDENTIAL, post('/session/:sessionId/webauthn/authenticator/:authenticatorId/credential')], + [cmd.Name.GET_CREDENTIALS, get('/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials')], + [cmd.Name.REMOVE_CREDENTIAL, del('/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials/:credentialId')], + [cmd.Name.REMOVE_ALL_CREDENTIALS, del('/session/:sessionId/webauthn/authenticator/:authenticatorId/credentials')], + [cmd.Name.SET_USER_VERIFIED, post('/session/:sessionId/webauthn/authenticator/:authenticatorId/uv')], ]) /** @@ -472,6 +481,7 @@ class Executor { this.log_.finer(() => `>>>\n${request}\n<<<\n${response}`) let httpResponse = /** @type {!Response} */ (response) + let { isW3C, value } = parseHttpResponse(command, httpResponse) if (command.getName() === cmd.Name.NEW_SESSION) { @@ -530,6 +540,7 @@ function parseHttpResponse(command, httpResponse) { } let parsed = tryParse(httpResponse.body) + if (parsed && typeof parsed === 'object') { let value = parsed.value let isW3C = diff --git a/javascript/node/selenium-webdriver/lib/test/fileserver.js b/javascript/node/selenium-webdriver/lib/test/fileserver.js index 80cfb9e389659..9fb6f8a556e9b 100644 --- a/javascript/node/selenium-webdriver/lib/test/fileserver.js +++ b/javascript/node/selenium-webdriver/lib/test/fileserver.js @@ -106,6 +106,7 @@ const Pages = (function () { addPage('webComponents', 'webComponents.html') addPage('xhtmlTestPage', 'xhtmlTest.html') addPage('uploadInvisibleTestPage', 'upload_invisible.html') + addPage('virtualAuthenticator', 'virtualAuthenticator.html') return pages })() diff --git a/javascript/node/selenium-webdriver/lib/virtual_authenticator.js b/javascript/node/selenium-webdriver/lib/virtual_authenticator.js new file mode 100644 index 0000000000000..3bca125ac8128 --- /dev/null +++ b/javascript/node/selenium-webdriver/lib/virtual_authenticator.js @@ -0,0 +1,230 @@ +// 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. + +'use strict' + +/** + * Options for the creation of virtual authenticators. + * @see http://w3c.github.io/webauthn/#sctn-automation + */ +class VirtualAuthenticatorOptions { + + static Protocol = { + "CTAP2": 'ctap2', + "U2F": 'ctap1/u2f', + } + + static Transport = { + "BLE": 'ble', + "USB": 'usb', + "NFC": 'nfc', + "INTERNAL": 'internal', + } + + /** + * Constructor to initialise VirtualAuthenticatorOptions object. + */ + constructor() { + this._protocol = VirtualAuthenticatorOptions.Protocol["CTAP2"] + this._transport = VirtualAuthenticatorOptions.Transport["USB"] + this._hasResidentKey = false + this._hasUserVerification = false + this._isUserConsenting = true + this._isUserVerified = false + } + + getProtocol() { + return this._protocol + } + + setProtocol(protocol) { + this._protocol = protocol + } + + getTransport() { + return this._transport + } + + setTransport(transport) { + this._transport = transport + } + + getHasResidentKey() { + return this._hasResidentKey + } + + setHasResidentKey(value) { + this._hasResidentKey = value + } + + getHasUserVerification() { + return this._hasUserVerification + } + + setHasUserVerification(value) { + this._hasUserVerification = value + } + + getIsUserConsenting() { + return this._isUserConsenting + } + + setIsUserConsenting(value) { + this._isUserConsenting = value + } + + getIsUserVerified() { + return this._isUserVerified + } + + setIsUserVerified(value) { + this._isUserVerified = value + } + + toDict() { + return { + "protocol": this.getProtocol(), + "transport": this.getTransport(), + "hasResidentKey": this.getHasResidentKey(), + "hasUserVerification": this.getHasUserVerification(), + "isUserConsenting": this.getIsUserConsenting(), + "isUserVerified": this.getIsUserVerified(), + + } + } +} + +/** + * A credential stored in a virtual authenticator. + * @see https://w3c.github.io/webauthn/#credential-parameters + */ +class Credential { + constructor( + credentialId, + isResidentCredential, + rpId, + userHandle, + privateKey, + signCount + ) { + this._id = credentialId + this._isResidentCredential = isResidentCredential + this._rpId = rpId + this._userHandle = userHandle + this._privateKey = privateKey + this._signCount = signCount + } + + id() { + return this._id + } + + isResidentCredential() { + return this._isResidentCredential + } + + rpId() { + return this._rpId + } + + userHandle() { + if (this._userHandle != null) { + return this._userHandle + } + return null + } + + privateKey() { + return this._privateKey + } + + signCount() { + return this._signCount + } + + /** + * Creates a resident (i.e. stateless) credential. + * @param id Unique base64 encoded string. + * @param rpId Relying party identifier. + * @param userHandle userHandle associated to the credential. Must be Base64 encoded string. + * @param privateKey Base64 encoded PKCS + * @param signCount intital value for a signature counter. + * @returns A resident credential + */ + createResidentCredential(id, rpId, userHandle, privateKey, signCount) { + return new Credential(id, true, rpId, userHandle, privateKey, signCount) + } + + /** + * Creates a non resident (i.e. stateless) credential. + * @param id Unique base64 encoded string. + * @param rpId Relying party identifier. + * @param privateKey Base64 encoded PKCS + * @param signCount intital value for a signature counter. + * @returns A non-resident credential + */ + createNonResidentCredential(id, rpId, privateKey, signCount) { + return new Credential(id, false, rpId, null, privateKey, signCount) + } + + toDict() { + let credentialData = { + 'credentialId': Buffer.from(this._id).toString('base64url'), + 'isResidentCredential': this._isResidentCredential, + 'rpId': this._rpId, + 'privateKey': Buffer.from(this._privateKey, 'binary').toString('base64url'), + 'signCount': this._signCount, + } + + if (this.userHandle() != null) { + credentialData['userHandle'] = Buffer.from(this._userHandle).toString('base64url') + } + + return credentialData + } + + /** + * Creates a credential from a map. + */ + fromDict(data) { + let id = new Uint8Array(Buffer.from(data['credentialId'], 'base64url')) + let isResidentCredential = data['isResidentCredential'] + let rpId = data['rpId'] + let privateKey = Buffer.from(data['privateKey'], 'base64url').toString('binary') + let signCount = data['signCount'] + let userHandle + + if ('userHandle' in data) { + userHandle = new Uint8Array(Buffer.from(data['userHandle'], 'base64url')) + } else { + userHandle = null + } + return new Credential( + id, + isResidentCredential, + rpId, + userHandle, + privateKey, + signCount + ) + } +} + +module.exports = { + Credential, + VirtualAuthenticatorOptions, +} diff --git a/javascript/node/selenium-webdriver/lib/webdriver.js b/javascript/node/selenium-webdriver/lib/webdriver.js index a7340e2e3216a..a98d0b87e880f 100644 --- a/javascript/node/selenium-webdriver/lib/webdriver.js +++ b/javascript/node/selenium-webdriver/lib/webdriver.js @@ -37,6 +37,8 @@ const { Capabilities } = require('./capabilities') const path = require('path') const { NoSuchElementError } = require('./error') const cdpTargets = ['page', 'browser'] +const Credential = + require('./virtual_authenticator').Credential // Capability names that are defined in the W3C spec. const W3C_CAPABILITY_NAMES = new Set([ @@ -679,6 +681,9 @@ class WebDriver { /** @private @const {(function(this: void): ?|undefined)} */ this.onQuit_ = onQuit + + /** @private {./virtual_authenticator}*/ + this.authenticatorId_ = null } /** @@ -730,6 +735,7 @@ class WebDriver { /** @override */ async execute(command) { command.setParameter('sessionId', this.session_) + let parameters = await toWireValue(command.getParameters()) command.setParameters(parameters) let value = await this.executor_.execute(command) @@ -1515,6 +1521,113 @@ class WebDriver { } }) } + + /** + * + * @returns The value of authenticator ID added + */ + virtualAuthenticatorId() { + return this.authenticatorId_ + } + + /** + * Adds a virtual authenticator with the given options. + * @param options VirtualAuthenticatorOptions object to set authenticator optons. + */ + async addVirtualAuthenticator(options) { + this.authenticatorId_ = await this.execute( + new command.Command(command.Name.ADD_VIRTUAL_AUTHENTICATOR).setParameters( + options.toDict() + ) + ) + } + + /** + * Removes a previously added virtual authenticator. The authenticator is no + * longer valid after removal, so no methods may be called. + */ + async removeVirtualAuthenticator() { + await this.execute( + new command.Command( + command.Name.REMOVE_VIRTUAL_AUTHENTICATOR + ).setParameter('authenticatorId', this.authenticatorId_) + ) + this.authenticatorId_ = null + } + + /** + * Injects a credential into the authenticator. + * @param credential Credential to be added + */ + async addCredential(credential) { + credential = credential.toDict() + credential['authenticatorId'] = this.authenticatorId_ + await this.execute( + new command.Command(command.Name.ADD_CREDENTIAL).setParameters(credential) + ) + } + + /** + * + * @returns The list of credentials owned by the authenticator. + */ + async getCredentials() { + let credential_data = await this.execute( + new command.Command(command.Name.GET_CREDENTIALS).setParameter( + 'authenticatorId', + this.virtualAuthenticatorId() + ) + ) + var credential_list = [] + for(var i = 0; i < credential_data.length; i++) { + credential_list.push(new Credential().fromDict(credential_data[i])) + } + return credential_list + } + + /** + * Removes a credential from the authenticator. + * @param credential_id The ID of the credential to be removed. + */ + async removeCredential(credential_id) { + + // If credential_id is not a base64url, then convert it to base64url. + if (Array.isArray(credential_id)) { + credential_id = Buffer.from(credential_id).toString('base64url') + } + + await this.execute( + new command.Command(command.Name.REMOVE_CREDENTIAL) + .setParameter('credentialId', credential_id) + .setParameter('authenticatorId', this.authenticatorId_) + ) + } + + /** + * Removes all the credentials from the authenticator. + */ + async removeAllCredentials() { + await this.execute( + new command.Command(command.Name.REMOVE_ALL_CREDENTIALS).setParameter( + 'authenticatorId', + this.authenticatorId_ + ) + ) + } + + /** + * Sets whether the authenticator will simulate success or fail on user verification. + * @param verified true if the authenticator will pass user verification, false otherwise. + */ + async setUserVerified(verified) { + await this.execute( + new command.Command(command.Name.SET_USER_VERIFIED) + .setParameter( + 'authenticatorId', + this.authenticatorId_ + ).setParameter('isUserVerified', verified) + ) + } } /** diff --git a/javascript/node/selenium-webdriver/net/index.js b/javascript/node/selenium-webdriver/net/index.js index f87119bcb6ace..e3bab3ff44ec5 100644 --- a/javascript/node/selenium-webdriver/net/index.js +++ b/javascript/node/selenium-webdriver/net/index.js @@ -73,7 +73,11 @@ function getAddress(family = 'IPv4') { * @return {(string|undefined)} The IP address or undefined if not available. */ function getLoopbackAddress(family = 'IPv4') { - return getIPAddress(true, family) + let address = getIPAddress(true, family) + if (address === '127.0.0.1') { + address = 'localhost' + } + return address } /** diff --git a/javascript/node/selenium-webdriver/test/lib/credentials_test.js b/javascript/node/selenium-webdriver/test/lib/credentials_test.js new file mode 100644 index 0000000000000..0e09536fa719c --- /dev/null +++ b/javascript/node/selenium-webdriver/test/lib/credentials_test.js @@ -0,0 +1,230 @@ +// 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. + +'use strict' + +const assert = require('assert') +const virtualAuthenticatorCredential = + require('../../lib/virtual_authenticator').Credential + +describe('Credentials', function () { + const BASE64_ENCODED_PK = `MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDbBOu5Lhs4vpowbCnmCyLUpIE7JM9sm9QXzye2G+jr+Kr + MsinWohEce47BFPJlTaDzHSvOW2eeunBO89ZcvvVc8RLz4qyQ8rO98xS1jtgqi1NcBPETDrtzthODu/gd0sjB2Tk3TLuBGV + oPXt54a+Oo4JbBJ6h3s0+5eAfGplCbSNq6hN3Jh9YOTw5ZA6GCEy5l8zBaOgjXytd2v2OdSVoEDNiNQRkjJd2rmS2oi9AyQ + FR3B7BrPSiDlCcITZFOWgLF5C31Wp/PSHwQhlnh7/6YhnE2y9tzsUvzx0wJXrBADW13+oMxrneDK3WGbxTNYgIi1PvSqXlq + GjHtCK+R2QkXAgMBAAECggEAVc6bu7VAnP6v0gDOeX4razv4FX/adCao9ZsHZ+WPX8PQxtmWYqykH5CY4TSfsuizAgyPuQ0 + +j4Vjssr9VODLqFoanspT6YXsvaKanncUYbasNgUJnfnLnw3an2XpU2XdmXTNYckCPRX9nsAAURWT3/n9ljc/XYY22ecYxM + 8sDWnHu2uKZ1B7M3X60bQYL5T/lVXkKdD6xgSNLeP4AkRx0H4egaop68hoW8FIwmDPVWYVAvo8etzWCtibRXz5FcNld9MgD + /Ai7ycKy4Q1KhX5GBFI79MVVaHkSQfxPHpr7/XcmpQOEAr+BMPon4s4vnKqAGdGB3j/E3d/+4F2swykoQKBgQD8hCsp6FIQ + 5umJlk9/j/nGsMl85LgLaNVYpWlPRKPc54YNumtvj5vx1BG+zMbT7qIE3nmUPTCHP7qb5ERZG4CdMCS6S64/qzZEqijLCqe + pwj6j4fV5SyPWEcpxf6ehNdmcfgzVB3Wolfwh1ydhx/96L1jHJcTKchdJJzlfTvq8wwKBgQDeCnKws1t5GapfE1rmC/h4ol + L2qZTth9oQmbrXYohVnoqNFslDa43ePZwL9Jmd9kYb0axOTNMmyrP0NTj41uCfgDS0cJnNTc63ojKjegxHIyYDKRZNVUR/d + xAYB/vPfBYZUS7M89pO6LLsHhzS3qpu3/hppo/Uc/AM/r8PSflNHQKBgDnWgBh6OQncChPUlOLv9FMZPR1ZOfqLCYrjYEqi + uzGm6iKM13zXFO4AGAxu1P/IAd5BovFcTpg79Z8tWqZaUUwvscnl+cRlj+mMXAmdqCeO8VASOmqM1ml667axeZDIR867ZG8 + K5V029Wg+4qtX5uFypNAAi6GfHkxIKrD04yOHAoGACdh4wXESi0oiDdkz3KOHPwIjn6BhZC7z8mx+pnJODU3cYukxv3WTct + lUhAsyjJiQ/0bK1yX87ulqFVgO0Knmh+wNajrb9wiONAJTMICG7tiWJOm7fW5cfTJwWkBwYADmkfTRmHDvqzQSSvoC2S7aa + 9QulbC3C/qgGFNrcWgcT9kCgYAZTa1P9bFCDU7hJc2mHwJwAW7/FQKEJg8SL33KINpLwcR8fqaYOdAHWWz636osVEqosRrH + zJOGpf9x2RSWzQJ+dq8+6fACgfFZOVpN644+sAHfNPAI/gnNKU5OfUv+eav8fBnzlf1A3y3GIkyMyzFN3DE7e0n/lyqxE4H + BYGpI8g==` + + const data = { + _id: new Uint8Array([1, 2, 3, 4]), + rpId: 'localhost', + userHandle: new Uint8Array([1]), + privateKey: Buffer.from(BASE64_ENCODED_PK, 'base64').toString('binary'), + signCount: 0, + } + + it('can testRkEnabledCredential', function () { + const { _id, rpId, userHandle, privateKey, signCount } = data + const credential = + new virtualAuthenticatorCredential().createResidentCredential( + _id, + rpId, + userHandle, + privateKey, + signCount + ) + + let testCredentialId = new Uint8Array([1, 2, 3, 4]) + + /** + * Checking if credential.id() matches with testCredentialId. Both values are + * arrays so we check if the lengths of both are equal and if one array has + * all its elements in the other array and vice-versa. + */ + assert.equal( + credential.id().length == testCredentialId.length && + credential.id().every((item) => testCredentialId.includes(item)) && + testCredentialId.every((item) => credential.id().includes(item)), + true + ) + if (credential.isResidentCredential() == true) { + assert(true) + } else { + assert(false) + } + assert.equal(credential.rpId(), 'localhost') + + let testUserHandle = new Uint8Array([1]) + + /** + * Checking if credential.userHandle() matches with testUserHandle. Both values are + * arrays so we check if the lengths of both are equal and if one array has + * all its elements in the other array and vice-versa. + */ + assert.equal( + credential.userHandle().length == testUserHandle.length && + credential + .userHandle() + .every((item) => testUserHandle.includes(item)) && + testUserHandle.every((item) => credential.userHandle().includes(item)), + true + ) + assert.equal( + credential.privateKey(), + Buffer.from(BASE64_ENCODED_PK, 'base64url').toString('binary') + ) + assert.equal(credential.signCount(), 0) + }) + + it('can testRkDisabledCredential', function () { + const { _id, rpId, userHandle, privateKey, signCount } = data + const credential = + new virtualAuthenticatorCredential().createNonResidentCredential( + _id, + rpId, + privateKey, + signCount + ) + + let testCredentialId = new Uint8Array([1, 2, 3, 4]) + + /** + * Checking if credential.id() matches with testCredentialId. Both values are + * arrays so we check if the lengths of both are equal and if one array has + * all its elements in the other array and vice-versa. + */ + assert.equal( + credential.id().length == testCredentialId.length && + credential.id().every((item) => testCredentialId.includes(item)) && + testCredentialId.every((item) => credential.id().includes(item)), + true + ) + + if (credential.isResidentCredential() == false) { + assert(true) + } else { + assert(false) + } + + if (credential.userHandle() == null) { + assert(true) + } else { + assert(false) + } + }) + + it('can testToDict', function () { + const { _id, rpId, userHandle, privateKey, signCount } = data + const credential = + new virtualAuthenticatorCredential().createResidentCredential( + _id, + rpId, + userHandle, + privateKey, + signCount + ) + + let credential_dict = credential.toDict() + assert.equal( + credential_dict['credentialId'], + Buffer.from(new Uint8Array([1, 2, 3, 4])).toString('base64url') + ) + + if (credential_dict['isResidentCredential'] == true) { + assert(true) + } else { + assert(false) + } + + assert.equal(credential_dict['rpId'], 'localhost') + assert.equal( + credential_dict['userHandle'], + Buffer.from(new Uint8Array([1])).toString('base64url') + ) + assert.equal( + credential_dict['privateKey'], + Buffer.from(privateKey, 'binary').toString('base64url') + ) + assert.equal(credential_dict['signCount'], 0) + }) + + it('can testFromDict', function () { + let credential_data = { + credentialId: Buffer.from(new Uint8Array([1, 2, 3, 4])).toString( + 'base64url' + ), + isResidentCredential: true, + rpId: 'localhost', + userHandle: Buffer.from(new Uint8Array([1])).toString('base64url'), + privateKey: BASE64_ENCODED_PK, + signCount: 0, + } + + let credential = new virtualAuthenticatorCredential().fromDict( + credential_data + ) + let testCredentialId = new Uint8Array([1, 2, 3, 4]) + assert.equal( + credential.id().length == testCredentialId.length && + credential.id().every((item) => testCredentialId.includes(item)) && + testCredentialId.every((item) => credential.id().includes(item)), + true + ) + + if (credential.isResidentCredential() == true) { + assert(true) + } else { + assert(false) + } + + assert.equal(credential.rpId(), 'localhost') + + let testUserHandle = new Uint8Array([1]) + + /** + * Checking if credential.userHandle() matches with testUserHandle. Both values are + * arrays so we check if the lengths of both are equal and if one array has + * all its elements in the other array and vice-versa. + */ + assert.equal( + credential.userHandle().length == testUserHandle.length && + credential + .userHandle() + .every((item) => testUserHandle.includes(item)) && + testUserHandle.every((item) => credential.userHandle().includes(item)), + true + ) + + assert.equal( + credential.privateKey(), + Buffer.from(BASE64_ENCODED_PK, 'base64url').toString('binary') + ) + assert.equal(credential.signCount(), 0) + }) +}) diff --git a/javascript/node/selenium-webdriver/test/lib/virtualauthenticatoroptions_test.js b/javascript/node/selenium-webdriver/test/lib/virtualauthenticatoroptions_test.js new file mode 100644 index 0000000000000..852d57a94a61b --- /dev/null +++ b/javascript/node/selenium-webdriver/test/lib/virtualauthenticatoroptions_test.js @@ -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. + +'use strict' + +const assert = require('assert') +const virtualAuthenticatorOptions = + require('../../lib/virtual_authenticator').VirtualAuthenticatorOptions + +let options + +describe('VirtualAuthenticatorOptions', function () { + beforeEach(function () { + options = new virtualAuthenticatorOptions() + }) + + it('can testSetTransport', function () { + options.setTransport(virtualAuthenticatorOptions.Transport['USB']) + assert.equal( + options.getTransport(), + virtualAuthenticatorOptions.Transport['USB'] + ) + }) + + it('can testGetTransport', function () { + options._transport = virtualAuthenticatorOptions.Transport['NFC'] + assert.equal( + options.getTransport(), + virtualAuthenticatorOptions.Transport['NFC'] + ) + }) + + it('can testSetProtocol', function () { + options.setProtocol(virtualAuthenticatorOptions.Protocol['U2F']) + assert.equal( + options.getProtocol(), + virtualAuthenticatorOptions.Protocol['U2F'] + ) + }) + + it('can testGetProtocol', function () { + options._protocol = virtualAuthenticatorOptions.Protocol['CTAP2'] + assert.equal( + options.getProtocol(), + virtualAuthenticatorOptions.Protocol['CTAP2'] + ) + }) + + it('can testSetHasResidentKey', function () { + options.setHasResidentKey(true) + assert.equal(options.getHasResidentKey(), true) + }) + + it('can testGetHasResidentKey', function () { + options._hasResidentKey = false + assert.equal(options.getHasResidentKey(), false) + }) + + it('can testSetHasUserVerification', function () { + options.setHasUserVerification(true) + assert.equal(options.getHasUserVerification(), true) + }) + + it('can testGetHasUserVerification', function () { + options._hasUserVerification = false + assert.equal(options.getHasUserVerification(), false) + }) + + it('can testSetIsUserConsenting', function () { + options.setIsUserConsenting(true) + assert.equal(options.getIsUserConsenting(), true) + }) + + it('can testGetIsUserConsenting', function () { + options._isUserConsenting = false + assert.equal(options.getIsUserConsenting(), false) + }) + + it('can testSetIsUserVerified', function () { + options.setIsUserVerified(true) + assert.equal(options.getIsUserVerified(), true) + }) + + it('can testGetIsUserVerified', function () { + options._isUserVerified = false + assert.equal(options.getIsUserVerified(), false) + }) + + it('can testToDictWithDefaults', function () { + let default_options = options.toDict() + assert.equal( + default_options['transport'], + virtualAuthenticatorOptions.Transport['USB'] + ) + assert.equal( + default_options['protocol'], + virtualAuthenticatorOptions.Protocol['CTAP2'] + ) + assert.equal(default_options['hasResidentKey'], false) + assert.equal(default_options['hasUserVerification'], false) + assert.equal(default_options['isUserConsenting'], true) + assert.equal(default_options['isUserVerified'], false) + }) +}) \ No newline at end of file diff --git a/javascript/node/selenium-webdriver/test/virtualAuthenticator_test.js b/javascript/node/selenium-webdriver/test/virtualAuthenticator_test.js new file mode 100644 index 0000000000000..383c52ed1f759 --- /dev/null +++ b/javascript/node/selenium-webdriver/test/virtualAuthenticator_test.js @@ -0,0 +1,519 @@ +// 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. + +'use strict' + +const assert = require('assert') +const virtualAuthenticatorCredential = + require('../lib/virtual_authenticator').Credential +const virtualAuthenticatorOptions = + require('../lib/virtual_authenticator').VirtualAuthenticatorOptions +const { ignore, suite } = require('../lib/test') +const { Browser } = require('../lib/capabilities') +const fileserver = require('../lib/test/fileserver') +const invalidArgumentError = require('../lib/error').InvalidArgumentError + +async function createRkEnabledU2fAuthenticator(driver) { + let options + options = new virtualAuthenticatorOptions() + options.setProtocol(virtualAuthenticatorOptions.Protocol['U2F']) + options.setHasResidentKey(true) + await driver.addVirtualAuthenticator(options) + return driver +} + +async function createRkDisabledU2fAuthenticator(driver) { + let options + options = new virtualAuthenticatorOptions() + options.setProtocol(virtualAuthenticatorOptions.Protocol['U2F']) + options.setHasResidentKey(false) + await driver.addVirtualAuthenticator(options) + return driver +} + +async function createRkEnabledCTAP2Authenticator(driver) { + let options + options = new virtualAuthenticatorOptions() + options.setProtocol(virtualAuthenticatorOptions.Protocol['CTAP2']) + options.setHasResidentKey(true) + options.setHasUserVerification(true) + options.setIsUserVerified(true) + await driver.addVirtualAuthenticator(options) + return driver +} + +async function createRkDisabledCTAP2Authenticator(driver) { + let options + options = new virtualAuthenticatorOptions() + options.setProtocol(virtualAuthenticatorOptions.Protocol['CTAP2']) + options.setHasResidentKey(false) + options.setHasUserVerification(true) + options.setIsUserVerified(true) + await driver.addVirtualAuthenticator(options) + return driver +} + +async function getAssertionFor(driver, credentialId) { + return await driver.executeAsyncScript( + 'getCredential([{' + + ' "type": "public-key",' + + ' "id": Uint8Array.from(arguments[0]),' + + '}]).then(arguments[arguments.length - 1]);', + credentialId + ) +} + +/** + * Checks if the two arrays are equal or not. Conditions to check are: + * 1. If the length of both arrays is equal + * 2. If all elements of array1 are present in array2 + * 3. If all elements of array2 are present in array1 + * @param array1 First array to be checked for equality + * @param array2 Second array to be checked for equality + * @returns true if equal, otherwise false. + */ +function arraysEqual(array1, array2) { + return ( + array1.length == array2.length && + array1.every((item) => array2.includes(item)) && + array2.every((item) => array1.includes(item)) + ) +} + +/** + * * * * * * TESTS * * * * * + */ + +suite(function (env) { + /** + * A pkcs#8 encoded encrypted RSA private key as a base64url string. + */ + const BASE64_ENCODED_PK = + 'MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDbBOu5Lhs4vpowbCnmCyLUpIE7JM9sm9QXzye2G+jr+Kr' + + 'MsinWohEce47BFPJlTaDzHSvOW2eeunBO89ZcvvVc8RLz4qyQ8rO98xS1jtgqi1NcBPETDrtzthODu/gd0sjB2Tk3TLuBGV' + + 'oPXt54a+Oo4JbBJ6h3s0+5eAfGplCbSNq6hN3Jh9YOTw5ZA6GCEy5l8zBaOgjXytd2v2OdSVoEDNiNQRkjJd2rmS2oi9AyQ' + + 'FR3B7BrPSiDlCcITZFOWgLF5C31Wp/PSHwQhlnh7/6YhnE2y9tzsUvzx0wJXrBADW13+oMxrneDK3WGbxTNYgIi1PvSqXlq' + + 'GjHtCK+R2QkXAgMBAAECggEAVc6bu7VAnP6v0gDOeX4razv4FX/adCao9ZsHZ+WPX8PQxtmWYqykH5CY4TSfsuizAgyPuQ0' + + '+j4Vjssr9VODLqFoanspT6YXsvaKanncUYbasNgUJnfnLnw3an2XpU2XdmXTNYckCPRX9nsAAURWT3/n9ljc/XYY22ecYxM' + + '8sDWnHu2uKZ1B7M3X60bQYL5T/lVXkKdD6xgSNLeP4AkRx0H4egaop68hoW8FIwmDPVWYVAvo8etzWCtibRXz5FcNld9MgD' + + '/Ai7ycKy4Q1KhX5GBFI79MVVaHkSQfxPHpr7/XcmpQOEAr+BMPon4s4vnKqAGdGB3j/E3d/+4F2swykoQKBgQD8hCsp6FIQ' + + '5umJlk9/j/nGsMl85LgLaNVYpWlPRKPc54YNumtvj5vx1BG+zMbT7qIE3nmUPTCHP7qb5ERZG4CdMCS6S64/qzZEqijLCqe' + + 'pwj6j4fV5SyPWEcpxf6ehNdmcfgzVB3Wolfwh1ydhx/96L1jHJcTKchdJJzlfTvq8wwKBgQDeCnKws1t5GapfE1rmC/h4ol' + + 'L2qZTth9oQmbrXYohVnoqNFslDa43ePZwL9Jmd9kYb0axOTNMmyrP0NTj41uCfgDS0cJnNTc63ojKjegxHIyYDKRZNVUR/d' + + 'xAYB/vPfBYZUS7M89pO6LLsHhzS3qpu3/hppo/Uc/AM/r8PSflNHQKBgDnWgBh6OQncChPUlOLv9FMZPR1ZOfqLCYrjYEqi' + + 'uzGm6iKM13zXFO4AGAxu1P/IAd5BovFcTpg79Z8tWqZaUUwvscnl+cRlj+mMXAmdqCeO8VASOmqM1ml667axeZDIR867ZG8' + + 'K5V029Wg+4qtX5uFypNAAi6GfHkxIKrD04yOHAoGACdh4wXESi0oiDdkz3KOHPwIjn6BhZC7z8mx+pnJODU3cYukxv3WTct' + + 'lUhAsyjJiQ/0bK1yX87ulqFVgO0Knmh+wNajrb9wiONAJTMICG7tiWJOm7fW5cfTJwWkBwYADmkfTRmHDvqzQSSvoC2S7aa' + + '9QulbC3C/qgGFNrcWgcT9kCgYAZTa1P9bFCDU7hJc2mHwJwAW7/FQKEJg8SL33KINpLwcR8fqaYOdAHWWz636osVEqosRrH' + + 'zJOGpf9x2RSWzQJ+dq8+6fACgfFZOVpN644+sAHfNPAI/gnNKU5OfUv+eav8fBnzlf1A3y3GIkyMyzFN3DE7e0n/lyqxE4H' + + 'BYGpI8g==' + + const browsers = (...args) => env.browsers(...args) + let driver + + beforeEach(async function () { + driver = await env + .builder() + .build() + await driver.get(fileserver.Pages.virtualAuthenticator) + assert.strictEqual(await driver.getTitle(), 'Virtual Authenticator Tests') + }) + + afterEach(async function () { + // if (driver.virtualAuthenticatorId() != null) { + // await driver.removeVirtualAuthenticator() + // } + return driver.quit() + }) + + describe('VirtualAuthenticator Test Suit 2', function () { + ignore(browsers(Browser.SAFARI, Browser.FIREFOX)).it( + 'should test create authenticator', + async function () { + /** + * Register a credential on the Virtual Authenticator. + */ + driver = await createRkDisabledU2fAuthenticator(driver) + assert((await driver.virtualAuthenticatorId()) != null) + + let response = await driver.executeAsyncScript( + 'registerCredential().then(arguments[arguments.length - 1]);' + ) + assert(response['status'] === 'OK') + + /** + * Attempt to use the credential to get an assertion. + */ + response = await getAssertionFor(driver, response.credential.rawId) + assert(response['status'] === 'OK') + } + ) + + ignore(browsers(Browser.SAFARI, Browser.FIREFOX)).it( + 'should test remove authenticator', + async function () { + let options = new virtualAuthenticatorOptions() + await driver.addVirtualAuthenticator(options) + assert((await driver.virtualAuthenticatorId()) != null) + + await driver.removeVirtualAuthenticator() + assert((await driver.virtualAuthenticatorId()) == null) + } + ) + + ignore(browsers(Browser.SAFARI, Browser.FIREFOX)).it( + 'should test add non-resident credential', + async function () { + /** + * Add a non-resident credential using the testing API. + */ + driver = await createRkDisabledCTAP2Authenticator(driver) + let credential = + new virtualAuthenticatorCredential().createNonResidentCredential( + new Uint8Array([1, 2, 3, 4]), + 'localhost', + Buffer.from(BASE64_ENCODED_PK, 'base64').toString('binary'), + 0 + ) + await driver.addCredential(credential) + + /** + * Attempt to use the credential to generate an assertion. + */ + let response = await getAssertionFor(driver, [1, 2, 3, 4]) + assert(response['status'] === 'OK') + } + ) + + ignore(browsers(Browser.SAFARI, Browser.FIREFOX)).it( + 'should test add non-resident credential when authenticator uses U2F protocol', + async function () { + /** + * Add a non-resident credential using the testing API. + */ + driver = await createRkDisabledU2fAuthenticator(driver) + + /** + * A pkcs#8 encoded unencrypted EC256 private key as a base64url string. + */ + const base64EncodedPK = + 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8_zMDQDYAxlU-Q' + + 'hk1Dwkf0v18GZca1DMF3SaJ9HPdmShRANCAASNYX5lyVCOZLzFZzrIKmeZ2jwU' + + 'RmgsJYxGP__fWN_S-j5sN4tT15XEpN_7QZnt14YvI6uvAgO0uJEboFaZlOEB' + + let credential = + new virtualAuthenticatorCredential().createNonResidentCredential( + new Uint8Array([1, 2, 3, 4]), + 'localhost', + Buffer.from(base64EncodedPK, 'base64').toString('binary'), + 0 + ) + await driver.addCredential(credential) + + /** + * Attempt to use the credential to generate an assertion. + */ + let response = await getAssertionFor(driver, [1, 2, 3, 4]) + assert(response['status'] === 'OK') + } + ) + + ignore(browsers(Browser.SAFARI, Browser.FIREFOX)).it( + 'should test add resident credential', + async function () { + /** + * Add a resident credential using the testing API. + */ + driver = await createRkEnabledCTAP2Authenticator(driver) + + let credential = + new virtualAuthenticatorCredential().createResidentCredential( + new Uint8Array([1, 2, 3, 4]), + 'localhost', + new Uint8Array([1]), + Buffer.from(BASE64_ENCODED_PK, 'base64').toString('binary'), + 0 + ) + await driver.addCredential(credential) + + /** + * Attempt to use the credential to generate an assertion. Notice we use an + * empty allowCredentials array. + */ + let response = await driver.executeAsyncScript( + 'getCredential([]).then(arguments[arguments.length - 1]);' + ) + assert(response['status'] === 'OK') + assert(response.attestation.userHandle.includes(1)) + } + ) + + ignore(browsers(Browser.SAFARI, Browser.FIREFOX)).it( + 'should test add resident credential not supported when authenticator uses U2F protocol', + async function () { + /** + * Add a resident credential using the testing API. + */ + driver = await createRkEnabledU2fAuthenticator(driver) + + /** + * A pkcs#8 encoded unencrypted EC256 private key as a base64url string. + */ + const base64EncodedPK = + 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQg8_zMDQDYAxlU-Q' + + 'hk1Dwkf0v18GZca1DMF3SaJ9HPdmShRANCAASNYX5lyVCOZLzFZzrIKmeZ2jwU' + + 'RmgsJYxGP__fWN_S-j5sN4tT15XEpN_7QZnt14YvI6uvAgO0uJEboFaZlOEB' + + let credential = + new virtualAuthenticatorCredential().createResidentCredential( + new Uint8Array([1, 2, 3, 4]), + 'localhost', + new Uint8Array([1]), + Buffer.from(base64EncodedPK, 'base64').toString('binary'), + 0 + ) + + /** + * Throws InvalidArgumentError + */ + try { + await driver.addCredential(credential) + } catch (e) { + if (e instanceof invalidArgumentError) { + assert(true) + } else { + assert(false) + } + } + } + ) + + ignore(browsers(Browser.SAFARI, Browser.FIREFOX)).it( + 'should test get credentials', + async function () { + /** + * Create an authenticator and add two credentials. + */ + driver = await createRkEnabledCTAP2Authenticator(driver) + + /** + * Register a resident credential. + */ + let response1 = await driver.executeAsyncScript( + 'registerCredential({authenticatorSelection: {requireResidentKey: true}})' + + ' .then(arguments[arguments.length - 1]);' + ) + assert(response1['status'] === 'OK') + + /** + * Register a non resident credential. + */ + let response2 = await driver.executeAsyncScript( + 'registerCredential().then(arguments[arguments.length - 1]);' + ) + assert(response2['status'] === 'OK') + + let credential1Id = response1.credential.rawId + let credential2Id = response2.credential.rawId + + assert.equal(arraysEqual(credential1Id, credential2Id), false) + + /** + * Retrieve the two credentials. + */ + let credentials = await driver.getCredentials() + assert.equal(credentials.length, 2) + + let credential1 = null + let credential2 = null + + credentials.forEach(function (credential) { + if (arraysEqual(credential.id(), credential1Id)) { + credential1 = credential + } else if (arraysEqual(credential.id(), credential2Id)) { + credential2 = credential + } else { + done(new Error('Unrecognized credential id')) + } + }) + + assert.equal(credential1.isResidentCredential(), true) + assert.notEqual(credential1.privateKey(), null) + assert.equal(credential1.rpId(), 'localhost') + assert.equal( + arraysEqual(credential1.userHandle(), new Uint8Array([1])), + true + ) + assert.equal(credential1.signCount(), 1) + + assert.equal(credential2.isResidentCredential(), false) + assert.notEqual(credential2.privateKey(), null) + /** + * Non resident keys do not store raw RP IDs or user handles. + */ + assert.equal(credential2.rpId(), null) + assert.equal(credential2.userHandle(), null) + assert.equal(credential2.signCount(), 1) + } + ) + + ignore(browsers(Browser.SAFARI, Browser.FIREFOX)).it( + 'should test remove credential by rawID', + async function () { + driver = await createRkDisabledU2fAuthenticator(driver) + + /** + * Register credential. + */ + let response = await driver.executeAsyncScript( + 'registerCredential().then(arguments[arguments.length - 1]);' + ) + assert(response['status'] === 'OK') + + /** + * Remove a credential by its ID as an array of bytes. + */ + let rawId = response.credential.rawId + await driver.removeCredential(rawId) + + /** + * Trying to get an assertion should fail. + */ + response = await getAssertionFor(driver, rawId) + assert(response['status'].startsWith('NotAllowedError')) + } + ) + + ignore(browsers(Browser.SAFARI, Browser.FIREFOX)).it( + 'should test remove credential by base64url Id', + async function () { + driver = await createRkDisabledU2fAuthenticator(driver) + + /** + * Register credential. + */ + let response = await driver.executeAsyncScript( + 'registerCredential().then(arguments[arguments.length - 1]);' + ) + assert(response['status'] === 'OK') + + let rawId = response.credential.rawId + let credentialId = response.credential.id + + /** + * Remove a credential by its base64url ID. + */ + await driver.removeCredential(credentialId) + + /** + * Trying to get an assertion should fail. + */ + response = await getAssertionFor(driver, rawId) + assert(response['status'].startsWith('NotAllowedError')) + } + ) + + ignore(browsers(Browser.SAFARI, Browser.FIREFOX)).it( + 'should test remove all credentials', + async function () { + driver = await createRkDisabledU2fAuthenticator(driver) + + /** + * Register two credentials. + */ + let response1 = await driver.executeAsyncScript( + 'registerCredential().then(arguments[arguments.length - 1]);' + ) + assert(response1['status'] === 'OK') + let rawId1 = response1.credential.rawId + + let response2 = await driver.executeAsyncScript( + 'registerCredential().then(arguments[arguments.length - 1]);' + ) + assert(response2['status'] === 'OK') + let rawId2 = response2.credential.rawId + + /** + * Remove all credentials. + */ + await driver.removeAllCredentials() + + /** + * Trying to get an assertion allowing for any of both should fail. + */ + let response = await driver.executeAsyncScript( + 'getCredential([{' + + ' "type": "public-key",' + + ' "id": Int8Array.from(arguments[0]),' + + '}, {' + + ' "type": "public-key",' + + ' "id": Int8Array.from(arguments[1]),' + + '}]).then(arguments[arguments.length - 1]);', + rawId1, + rawId2 + ) + assert(response['status'].startsWith('NotAllowedError')) + } + ) + + ignore(browsers(Browser.SAFARI, Browser.FIREFOX)).it( + 'should test set user verified', + async function () { + driver = await createRkEnabledCTAP2Authenticator(driver) + + /** + * Register a credential requiring UV. + */ + let response = await driver.executeAsyncScript( + "registerCredential({authenticatorSelection: {userVerification: 'required'}})" + + ' .then(arguments[arguments.length - 1]);' + ) + assert(response['status'] === 'OK') + let rawId = response.credential.rawId + + /** + * Getting an assertion requiring user verification should succeed. + */ + response = await driver.executeAsyncScript( + 'getCredential([{' + + ' "type": "public-key",' + + ' "id": Int8Array.from(arguments[0]),' + + "}], {userVerification: 'required'}).then(arguments[arguments.length - 1]);", + rawId + ) + assert(response['status'] === 'OK') + + /** + * Disable user verification. + */ + await driver.setUserVerified(false) + + /** + * Getting an assertion requiring user verification should fail. + */ + response = await driver.executeAsyncScript( + 'getCredential([{' + + ' "type": "public-key",' + + ' "id": Int8Array.from(arguments[0]),' + + "}], {userVerification: 'required'}).then(arguments[arguments.length - 1]);", + rawId + ) + assert(response['status'].startsWith('NotAllowedError')) + } + ) + }) +})