From 6d90489acea7889dfe72b5bbc4f2e3783caf65ba Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Tue, 28 Mar 2023 10:38:25 +0200 Subject: [PATCH] [EC-598] feat: start implementing `getAssertion` --- ...fido2-authenticator.service.abstraction.ts | 45 ++++- .../fido2-authenticator.service.spec.ts | 168 ++++++++++++------ .../services/fido2-authenticator.service.ts | 20 ++- 3 files changed, 167 insertions(+), 66 deletions(-) diff --git a/libs/common/src/webauthn/abstractions/fido2-authenticator.service.abstraction.ts b/libs/common/src/webauthn/abstractions/fido2-authenticator.service.abstraction.ts index 5315cdac8c..f3619a9953 100644 --- a/libs/common/src/webauthn/abstractions/fido2-authenticator.service.abstraction.ts +++ b/libs/common/src/webauthn/abstractions/fido2-authenticator.service.abstraction.ts @@ -1,10 +1,17 @@ export abstract class Fido2AuthenticatorService { /** - * This method triggers the generation of a new credential in the authenticator + * Create and save a new credential * * @return {Uint8Array} Attestation object **/ makeCredential: (params: Fido2AuthenticatorMakeCredentialsParams) => Promise; + + /** + * Generate an assertion using an existing credential + */ + getAssertion: ( + params: Fido2AuthenticatorGetAssertionParams + ) => Promise; } export enum Fido2AlgorithmIdentifier { @@ -26,6 +33,12 @@ export class Fido2AutenticatorError extends Error { } } +export interface PublicKeyCredentialDescriptor { + id: BufferSource; + transports?: ("ble" | "internal" | "nfc" | "usb")[]; + type: "public-key"; +} + /** * Parameters for {@link Fido2AuthenticatorService.makeCredential} * @@ -54,11 +67,7 @@ export interface Fido2AuthenticatorMakeCredentialsParams { type: "public-key"; // not used }[]; /** An OPTIONAL list of PublicKeyCredentialDescriptor objects provided by the Relying Party with the intention that, if any of these are known to the authenticator, it SHOULD NOT create a new credential. excludeCredentialDescriptorList contains a list of known credentials. */ - excludeCredentialDescriptorList?: { - id: BufferSource; - transports?: ("ble" | "internal" | "nfc" | "usb")[]; - type: "public-key"; // not used - }[]; + excludeCredentialDescriptorList?: PublicKeyCredentialDescriptor[]; /** A map from extension identifiers to their authenticator extension inputs, created by the client based on the extensions requested by the Relying Party, if any. */ extensions?: { appid?: string; @@ -72,5 +81,27 @@ export interface Fido2AuthenticatorMakeCredentialsParams { requireResidentKey: boolean; requireUserVerification: boolean; /** The constant Boolean value true. It is included here as a pseudo-parameter to simplify applying this abstract authenticator model to implementations that may wish to make a test of user presence optional although WebAuthn does not. */ - // requireUserPresence: true; // Always performed + // requireUserPresence: true; // Always required +} + +export interface Fido2AuthenticatorGetAssertionParams { + /** The caller’s RP ID, as determined by the user agent and the client. */ + rpId: string; + /** The hash of the serialized client data, provided by the client. */ + hash: BufferSource; + allowCredentialDescriptorList: PublicKeyCredentialDescriptor[]; + /** The effective user verification requirement for assertion, a Boolean value provided by the client. */ + requireUserVerification: boolean; + /** The constant Boolean value true. It is included here as a pseudo-parameter to simplify applying this abstract authenticator model to implementations that may wish to make a test of user presence optional although WebAuthn does not. */ + // requireUserPresence: boolean; // Always required + extensions: unknown; +} + +export interface Fido2AuthenticatorGetAssertionResult { + selectedCredential?: { + id: string; + userHandle: Uint8Array; + }; + authenticatorData: Uint8Array; + signature: Uint8Array; } diff --git a/libs/common/src/webauthn/services/fido2-authenticator.service.spec.ts b/libs/common/src/webauthn/services/fido2-authenticator.service.spec.ts index 3ff0862b7d..3651b541b4 100644 --- a/libs/common/src/webauthn/services/fido2-authenticator.service.spec.ts +++ b/libs/common/src/webauthn/services/fido2-authenticator.service.spec.ts @@ -11,6 +11,7 @@ import { Login } from "../../vault/models/domain/login"; import { CipherView } from "../../vault/models/view/cipher.view"; import { Fido2AutenticatorErrorCode, + Fido2AuthenticatorGetAssertionParams, Fido2AuthenticatorMakeCredentialsParams, } from "../abstractions/fido2-authenticator.service.abstraction"; import { @@ -35,7 +36,7 @@ describe("FidoAuthenticatorService", () => { authenticator = new Fido2AuthenticatorService(cipherService, userInterface); }); - describe("authenticatorMakeCredential", () => { + describe("makeCredential", () => { let invalidParams!: InvalidParams; beforeEach(async () => { @@ -68,7 +69,7 @@ describe("FidoAuthenticatorService", () => { * Deviation: User verification is checked before checking for excluded credentials * */ it("should throw error if requireUserVerification is set to true", async () => { - const params = await createCredentialParams({ requireUserVerification: true }); + const params = await createParams({ requireUserVerification: true }); const result = async () => await authenticator.makeCredential(params); @@ -98,7 +99,7 @@ describe("FidoAuthenticatorService", () => { beforeEach(async () => { const excludedCipher = createCipher(); excludedCipherView = await excludedCipher.decrypt(); - params = await createCredentialParams({ + params = await createParams({ excludeCredentialDescriptorList: [ { id: Fido2Utils.stringToBuffer(excludedCipher.id), type: "public-key" }, ], @@ -151,7 +152,7 @@ describe("FidoAuthenticatorService", () => { let params: Fido2AuthenticatorMakeCredentialsParams; beforeEach(async () => { - params = await createCredentialParams({ requireResidentKey: true }); + params = await createParams({ requireResidentKey: true }); }); /** @@ -237,7 +238,7 @@ describe("FidoAuthenticatorService", () => { existingCipher.login = new Login(); existingCipher.fido2Key = undefined; existingCipherView = await existingCipher.decrypt(); - params = await createCredentialParams(); + params = await createParams(); cipherService.get.mockImplementation(async (id) => id === existingCipher.id ? existingCipher : undefined ); @@ -290,7 +291,7 @@ describe("FidoAuthenticatorService", () => { /** Spec: If the user does not consent or if user verification fails, return an error code equivalent to "NotAllowedError" and terminate the operation. */ it("should throw error if user denies creation request", async () => { userInterface.confirmNewNonDiscoverableCredential.mockResolvedValue(undefined); - const params = await createCredentialParams(); + const params = await createParams(); const result = async () => await authenticator.makeCredential(params); @@ -319,7 +320,7 @@ describe("FidoAuthenticatorService", () => { let params: Fido2AuthenticatorMakeCredentialsParams; beforeEach(async () => { - params = await createCredentialParams({ requireResidentKey: true }); + params = await createParams({ requireResidentKey: true }); userInterface.confirmNewCredential.mockResolvedValue(true); cipherService.encrypt.mockResolvedValue({} as unknown as Cipher); cipherService.createWithServer.mockImplementation(async (cipher) => { @@ -328,7 +329,7 @@ describe("FidoAuthenticatorService", () => { }); }); - it.only("should throw error if user denies creation request", async () => { + it("should throw error if user denies creation request", async () => { const result = await authenticator.makeCredential(params); const attestationObject = CBOR.decode(result.buffer); @@ -360,59 +361,110 @@ describe("FidoAuthenticatorService", () => { expect(credentialId).toEqual(cipherIdBytes); }); }); + + async function createParams( + params: Partial = {} + ): Promise { + return { + hash: params.hash ?? (await createClientDataHash()), + rpEntity: params.rpEntity ?? { + name: "Bitwarden", + id: RpId, + }, + userEntity: params.userEntity ?? { + id: randomBytes(64), + name: "jane.doe@bitwarden.com", + displayName: "Jane Doe", + icon: " data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAOhJREFUeNpiFI+9E8DAwDAfiAUYSAMfgDiQBVmzlSYnUTqPXf/OANWzngVZ87pKKaIMCGp/BjeEhRjFMKAjx8bQFC2CIs9CpHNxAiYGCsEQM4Cfiwm3AY9f/yZogIcRN4ZahAFv/jAcu4E7xMNtecEYpAakFqsX8me9Yvj07R+G5jR3foaJqWJgOZAaZMAIzAv/kQV05NgZ5hdIMMiKQJIIyEYrDU6wrYkTXjBcefQTvwGwwCoJFGJIBdoMArN3fmToWf+O4SMW14EMeI8rJ8Jcgexn9BwJCoNEaNbEACCN+DSDsjNAgAEAri9Zii/uDMsAAAAASUVORK5CYII=", + }, + credTypesAndPubKeyAlgs: params.credTypesAndPubKeyAlgs ?? [ + { + alg: -7, // ES256 + type: "public-key", + }, + ], + excludeCredentialDescriptorList: params.excludeCredentialDescriptorList ?? [ + { + id: randomBytes(16), + transports: ["internal"], + type: "public-key", + }, + ], + requireResidentKey: params.requireResidentKey ?? false, + requireUserVerification: params.requireUserVerification ?? false, + extensions: params.extensions ?? { + appid: undefined, + appidExclude: undefined, + credProps: undefined, + uvm: false as boolean, + }, + }; + } + + type InvalidParams = Awaited>; + async function createInvalidParams() { + return { + unsupportedAlgorithm: await createParams({ + credTypesAndPubKeyAlgs: [{ alg: 9001, type: "public-key" }], + }), + invalidRk: await createParams({ requireResidentKey: "invalid-value" as any }), + invalidUv: await createParams({ + requireUserVerification: "invalid-value" as any, + }), + }; + } + }); + + describe("getAssertion", () => { + let invalidParams!: InvalidParams; + + beforeEach(async () => { + invalidParams = await createInvalidParams(); + }); + + describe("invalid input parameters", () => { + it("should throw error when requireUserVerification has invalid value", async () => { + const result = async () => await authenticator.getAssertion(invalidParams.invalidUv); + + await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.Unknown); + }); + + /** Deviation: User verification is checked before checking for credentials */ + it("should throw error if requireUserVerification is set to true", async () => { + const params = await createParams({ requireUserVerification: true }); + + const result = async () => await authenticator.getAssertion(params); + + await expect(result).rejects.toThrowError(Fido2AutenticatorErrorCode.Constraint); + }); + }); + + async function createParams( + params: Partial = {} + ): Promise { + return { + rpId: params.rpId ?? RpId, + hash: params.hash ?? (await createClientDataHash()), + allowCredentialDescriptorList: params.allowCredentialDescriptorList ?? [], + requireUserVerification: params.requireUserVerification ?? false, + extensions: params.extensions ?? {}, + }; + } + + type InvalidParams = Awaited>; + async function createInvalidParams() { + const emptyRpId = await createParams(); + emptyRpId.rpId = undefined as any; + return { + emptyRpId, + invalidUv: await createParams({ + requireUserVerification: "invalid-value" as any, + }), + }; + } }); }); -async function createCredentialParams( - params: Partial = {} -): Promise { - return { - hash: params.hash ?? (await createClientDataHash()), - rpEntity: params.rpEntity ?? { - name: "Bitwarden", - id: RpId, - }, - userEntity: params.userEntity ?? { - id: randomBytes(64), - name: "jane.doe@bitwarden.com", - displayName: "Jane Doe", - icon: " data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAOhJREFUeNpiFI+9E8DAwDAfiAUYSAMfgDiQBVmzlSYnUTqPXf/OANWzngVZ87pKKaIMCGp/BjeEhRjFMKAjx8bQFC2CIs9CpHNxAiYGCsEQM4Cfiwm3AY9f/yZogIcRN4ZahAFv/jAcu4E7xMNtecEYpAakFqsX8me9Yvj07R+G5jR3foaJqWJgOZAaZMAIzAv/kQV05NgZ5hdIMMiKQJIIyEYrDU6wrYkTXjBcefQTvwGwwCoJFGJIBdoMArN3fmToWf+O4SMW14EMeI8rJ8Jcgexn9BwJCoNEaNbEACCN+DSDsjNAgAEAri9Zii/uDMsAAAAASUVORK5CYII=", - }, - credTypesAndPubKeyAlgs: params.credTypesAndPubKeyAlgs ?? [ - { - alg: -7, // ES256 - type: "public-key", - }, - ], - excludeCredentialDescriptorList: params.excludeCredentialDescriptorList ?? [ - { - id: randomBytes(16), - transports: ["internal"], - type: "public-key", - }, - ], - requireResidentKey: params.requireResidentKey ?? false, - requireUserVerification: params.requireUserVerification ?? false, - extensions: params.extensions ?? { - appid: undefined, - appidExclude: undefined, - credProps: undefined, - uvm: false as boolean, - }, - }; -} - -type InvalidParams = Awaited>; -async function createInvalidParams() { - return { - unsupportedAlgorithm: await createCredentialParams({ - credTypesAndPubKeyAlgs: [{ alg: 9001, type: "public-key" }], - }), - invalidRk: await createCredentialParams({ requireResidentKey: "invalid-value" as any }), - invalidUv: await createCredentialParams({ requireUserVerification: "invalid-value" as any }), - }; -} - function createCipher(data: Partial = {}): Cipher { const cipher = new Cipher(); cipher.id = data.id ?? Utils.newGuid(); diff --git a/libs/common/src/webauthn/services/fido2-authenticator.service.ts b/libs/common/src/webauthn/services/fido2-authenticator.service.ts index 84e5557ec2..f5ac35a379 100644 --- a/libs/common/src/webauthn/services/fido2-authenticator.service.ts +++ b/libs/common/src/webauthn/services/fido2-authenticator.service.ts @@ -8,6 +8,8 @@ import { Fido2AlgorithmIdentifier, Fido2AutenticatorError, Fido2AutenticatorErrorCode, + Fido2AuthenticatorGetAssertionParams, + Fido2AuthenticatorGetAssertionResult, Fido2AuthenticatorMakeCredentialsParams, Fido2AuthenticatorService as Fido2AuthenticatorServiceAbstraction, } from "../abstractions/fido2-authenticator.service.abstraction"; @@ -31,7 +33,6 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr private cipherService: CipherService, private userInterface: Fido2UserInterfaceService ) {} - async makeCredential(params: Fido2AuthenticatorMakeCredentialsParams): Promise { if (params.credTypesAndPubKeyAlgs.every((p) => p.alg !== Fido2AlgorithmIdentifier.ES256)) { throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.NotSupported); @@ -134,6 +135,23 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr return attestationObject; } + async getAssertion( + params: Fido2AuthenticatorGetAssertionParams + ): Promise { + if ( + params.requireUserVerification != undefined && + typeof params.requireUserVerification !== "boolean" + ) { + throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown); + } + + if (params.requireUserVerification) { + throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Constraint); + } + + throw new Error("Not implemented"); + } + private async vaultContainsId(ids: string[]): Promise { for (const id of ids) { if ((await this.cipherService.get(id)) != undefined) {