test_virtualAuthenticator_test.js

// 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('node:assert')
const virtualAuthenticatorCredential = require('selenium-webdriver/lib/virtual_authenticator').Credential
const virtualAuthenticatorOptions = require('selenium-webdriver/lib/virtual_authenticator').VirtualAuthenticatorOptions
const Protocol = require('selenium-webdriver/lib/virtual_authenticator').Protocol
const { ignore, suite } = require('../lib/test')
const { Browser } = require('selenium-webdriver/lib/capabilities')
const fileServer = require('../lib/test/fileserver')
const invalidArgumentError = require('selenium-webdriver/lib/error').InvalidArgumentError

const REGISTER_CREDENTIAL = 'registerCredential().then(arguments[arguments.length - 1]);'
const GET_CREDENTIAL = `getCredential([{
                          "type": "public-key",
                          "id": Int8Array.from(arguments[0]),
                        }]).then(arguments[arguments.length - 1]);`

async function createRkEnabledU2fAuthenticator(driver) {
  let options
  options = new virtualAuthenticatorOptions()
  options.setProtocol(Protocol['U2F'])
  options.setHasResidentKey(true)
  await driver.addVirtualAuthenticator(options)
  return driver
}

async function createRkDisabledU2fAuthenticator(driver) {
  let options
  options = new virtualAuthenticatorOptions()
  options.setProtocol(Protocol['U2F'])
  options.setHasResidentKey(false)
  await driver.addVirtualAuthenticator(options)
  return driver
}

async function createRkEnabledCTAP2Authenticator(driver) {
  let options
  options = new virtualAuthenticatorOptions()
  options.setProtocol(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(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(GET_CREDENTIAL, credentialId)
}

function extractRawIdFrom(response) {
  return response.credential.rawId
}

function extractIdFrom(response) {
  return response.credential.id
}

/**
 * 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.replace('127.0.0.1', 'localhost'))
    assert.strictEqual(await driver.getTitle(), 'Virtual Authenticator Tests')
  })

  afterEach(async function () {
    if (driver.virtualAuthenticatorId() != null) {
      await driver.removeVirtualAuthenticator()
    }
    await 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(REGISTER_CREDENTIAL)
      assert(response['status'] === 'OK')

      /**
       * Attempt to use the credential to get an assertion.
       */
      response = await getAssertionFor(driver, extractRawIdFrom(response))
      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 = 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 = 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 = 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 = 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(REGISTER_CREDENTIAL)
      assert(response2['status'] === 'OK')

      let credential1Id = extractRawIdFrom(response1)
      let credential2Id = extractRawIdFrom(response2)

      assert.notDeepStrictEqual(credential1Id.sort(), credential2Id.sort())

      /**
       * 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 {
          assert.fail(new Error('Unrecognized credential id'))
        }
      })

      assert.equal(credential1.isResidentCredential(), true)
      assert.notEqual(credential1.privateKey(), null)

      assert.equal(credential2.isResidentCredential(), false)
      assert.notEqual(credential2.privateKey(), null)
    })

    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(REGISTER_CREDENTIAL)
      assert(response['status'] === 'OK')

      /**
       * Remove a credential by its ID as an array of bytes.
       */
      let rawId = extractRawIdFrom(response)
      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(REGISTER_CREDENTIAL)
        assert(response['status'] === 'OK')

        let rawId = extractRawIdFrom(response)
        let credentialId = extractIdFrom(response)

        /**
         * 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(REGISTER_CREDENTIAL)
      assert(response1['status'] === 'OK')
      let rawId1 = extractRawIdFrom(response1)

      let response2 = await driver.executeAsyncScript(REGISTER_CREDENTIAL)
      assert(response2['status'] === 'OK')
      let rawId2 = extractRawIdFrom(response2)

      /**
       * 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 = extractRawIdFrom(response)

      /**
       * Getting an assertion requiring user verification should succeed.
       */
      response = await driver.executeAsyncScript(GET_CREDENTIAL, rawId)
      assert(response['status'] === 'OK')

      /**
       * Disable user verification.
       */
      await driver.setUserVerified(false)

      /**
       * Getting an assertion requiring user verification should fail.
       */
      response = await driver.executeAsyncScript(GET_CREDENTIAL, rawId)
      assert(response['status'].startsWith('NotAllowedError'))
    })
  })
})