From b3d5ab44728e4785957c0733c02617d758c152bd Mon Sep 17 00:00:00 2001 From: Andreas Coroiu Date: Thu, 30 Mar 2023 10:55:59 +0200 Subject: [PATCH] [EC-598] feat: add signatures to attestation --- .../src/webauthn/abstractions/fido2-utils.ts | 9 +- .../fido2-authenticator.service.spec.ts | 28 ++++- .../services/fido2-authenticator.service.ts | 119 ++++++++++++------ 3 files changed, 115 insertions(+), 41 deletions(-) diff --git a/libs/common/src/webauthn/abstractions/fido2-utils.ts b/libs/common/src/webauthn/abstractions/fido2-utils.ts index dd1ab39789..b3e42de143 100644 --- a/libs/common/src/webauthn/abstractions/fido2-utils.ts +++ b/libs/common/src/webauthn/abstractions/fido2-utils.ts @@ -11,11 +11,16 @@ export class Fido2Utils { return Utils.fromUrlB64ToArray(str); } - private static bufferSourceToUint8Array(bufferSource: BufferSource) { - if (bufferSource instanceof ArrayBuffer) { + static bufferSourceToUint8Array(bufferSource: BufferSource) { + if (Fido2Utils.isArrayBuffer(bufferSource)) { return new Uint8Array(bufferSource); } else { return new Uint8Array(bufferSource.buffer); } } + + /** Utility function to identify type of bufferSource. Necessary because of differences between runtimes */ + static isArrayBuffer(bufferSource: BufferSource): bufferSource is ArrayBuffer { + return bufferSource instanceof ArrayBuffer || bufferSource.buffer === undefined; + } } 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 b30a955907..f81545d395 100644 --- a/libs/common/src/webauthn/services/fido2-authenticator.service.spec.ts +++ b/libs/common/src/webauthn/services/fido2-authenticator.service.spec.ts @@ -30,10 +30,23 @@ describe("FidoAuthenticatorService", () => { let userInterface!: MockProxy; let authenticator!: Fido2AuthenticatorService; - beforeEach(() => { + beforeEach(async () => { cipherService = mock(); userInterface = mock(); authenticator = new Fido2AuthenticatorService(cipherService, userInterface); + + // crypto.subtle.importKey doesn't work properly in jest, so we need to mock it with new keys. + const privateKey = ( + await crypto.subtle.generateKey( + { + name: "ECDSA", + namedCurve: "P-256", + }, + true, + ["sign"] + ) + ).privateKey; + crypto.subtle.importKey = jest.fn().mockResolvedValue(privateKey); }); describe("makeCredential", () => { @@ -696,6 +709,9 @@ describe("FidoAuthenticatorService", () => { const counter = encAuthData.slice(33, 37); expect(result.selectedCredential.id).toBe(selectedCredentialId); + expect(result.selectedCredential.userHandle).toEqual( + Fido2Utils.stringToBuffer(ciphers[0].fido2Key.userHandle) + ); expect(rpIdHash).toEqual( new Uint8Array([ 0x22, 0x6b, 0xb3, 0x92, 0x02, 0xff, 0xf9, 0x22, 0xdc, 0x74, 0x05, 0xcd, 0x28, 0xa8, @@ -705,6 +721,8 @@ describe("FidoAuthenticatorService", () => { ); expect(flags).toEqual(new Uint8Array([0b00000001])); // UP = true expect(counter).toEqual(new Uint8Array([0, 0, 0x23, 0x29])); // 9001 in hex + // Signatures are non-deterministic, and webcrypto can't verify DER signature format + // expect(result.signature).toMatchSnapshot(); }); }); } @@ -748,7 +766,13 @@ function createCipherView( cipher.fido2Key.nonDiscoverableId = fido2Key.nonDiscoverableId; cipher.fido2Key.rpId = fido2Key.rpId ?? RpId; cipher.fido2Key.counter = fido2Key.counter ?? 0; - cipher.fido2Key.userHandle = Fido2Utils.bufferToString(randomBytes(16)); + cipher.fido2Key.userHandle = fido2Key.userHandle ?? Fido2Utils.bufferToString(randomBytes(16)); + cipher.fido2Key.keyAlgorithm = fido2Key.keyAlgorithm ?? "ECDSA"; + cipher.fido2Key.keyCurve = fido2Key.keyCurve ?? "P-256"; + cipher.fido2Key.keyValue = + fido2Key.keyValue ?? + "MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgTC-7XDZipXbaVBlnkjlBgO16ZmqBZWejK2iYo6lV0dehRANCAASOcM2WduNq1DriRYN7ZekvZz-bRhA-qNT4v0fbp5suUFJyWmgOQ0bybZcLXHaerK5Ep1JiSrQcewtQNgLtry7f"; + return cipher; } diff --git a/libs/common/src/webauthn/services/fido2-authenticator.service.ts b/libs/common/src/webauthn/services/fido2-authenticator.service.ts index 793486ee64..f9be33981a 100644 --- a/libs/common/src/webauthn/services/fido2-authenticator.service.ts +++ b/libs/common/src/webauthn/services/fido2-authenticator.service.ts @@ -17,6 +17,8 @@ import { Fido2UserInterfaceService } from "../abstractions/fido2-user-interface. import { Fido2Utils } from "../abstractions/fido2-utils"; import { Fido2KeyView } from "../models/view/fido2-key.view"; +import { joseToDer } from "./ecdsa-utils"; + // AAGUID: 6e8248d5-b479-40db-a3d8-11116f7e8349 export const AAGUID = new Uint8Array([ 0xd5, 0x48, 0x82, 0x6e, 0x79, 0xb4, 0xdb, 0x40, 0xa3, 0xd8, 0x11, 0x11, 0x6f, 0x7e, 0x83, 0x49, @@ -79,12 +81,12 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr } try { - keyPair = await this.createKeyPair(); + keyPair = await createKeyPair(); cipher = new CipherView(); cipher.type = CipherType.Fido2Key; cipher.name = params.rpEntity.name; - cipher.fido2Key = await this.createKeyView(params, keyPair.privateKey); + cipher.fido2Key = await createKeyView(params, keyPair.privateKey); const encrypted = await this.cipherService.encrypt(cipher); await this.cipherService.createWithServer(encrypted); // encrypted.id is assigned inside here cipher.id = encrypted.id; @@ -102,11 +104,11 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr } try { - keyPair = await this.createKeyPair(); + keyPair = await createKeyPair(); const encrypted = await this.cipherService.get(cipherId); cipher = await encrypted.decrypt(); - cipher.fido2Key = await this.createKeyView(params, keyPair.privateKey); + cipher.fido2Key = await createKeyView(params, keyPair.privateKey); const reencrypted = await this.cipherService.encrypt(cipher); await this.cipherService.updateWithServer(reencrypted); } catch { @@ -189,11 +191,11 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr userVerification: false, }); - // const signature = await generateSignature({ - // authData, - // clientData, - // privateKey: credential.keyValue, - // }); + const signature = await generateSignature({ + authData: authenticatorData, + clientData: params.hash, + privateKey: await getPrivateKeyFromCipher(selectedCipher), + }); return { authenticatorData, @@ -201,7 +203,7 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr id: selectedCredentialId, userHandle: Fido2Utils.stringToBuffer(selectedCipher.fido2Key.userHandle), }, - signature: null, + signature, }; } @@ -264,38 +266,55 @@ export class Fido2AuthenticatorService implements Fido2AuthenticatorServiceAbstr (cipher) => cipher.type === CipherType.Fido2Key && cipher.fido2Key.rpId === rpId ); } +} - private async createKeyPair() { - return await crypto.subtle.generateKey( - { - name: "ECDSA", - namedCurve: "P-256", - }, - true, - KeyUsages - ); +async function createKeyPair() { + return await crypto.subtle.generateKey( + { + name: "ECDSA", + namedCurve: "P-256", + }, + true, + KeyUsages + ); +} + +async function createKeyView( + params: Fido2AuthenticatorMakeCredentialsParams, + keyValue: CryptoKey +): Promise { + if (keyValue.algorithm.name !== "ECDSA" && (keyValue.algorithm as any).namedCurve !== "P-256") { + throw new Fido2AutenticatorError(Fido2AutenticatorErrorCode.Unknown); } - private async createKeyView( - params: Fido2AuthenticatorMakeCredentialsParams, - keyValue: CryptoKey - ): Promise { - const pcks8Key = await crypto.subtle.exportKey("pkcs8", keyValue); + const pkcs8Key = await crypto.subtle.exportKey("pkcs8", keyValue); + const fido2Key = new Fido2KeyView(); + fido2Key.nonDiscoverableId = params.requireResidentKey ? null : Utils.newGuid(); + fido2Key.keyType = "public-key"; + fido2Key.keyAlgorithm = "ECDSA"; + fido2Key.keyCurve = "P-256"; + fido2Key.keyValue = Fido2Utils.bufferToString(pkcs8Key); + fido2Key.rpId = params.rpEntity.id; + fido2Key.userHandle = Fido2Utils.bufferToString(params.userEntity.id); + fido2Key.counter = 0; + fido2Key.rpName = params.rpEntity.name; + fido2Key.userName = params.userEntity.name; - const fido2Key = new Fido2KeyView(); - fido2Key.nonDiscoverableId = params.requireResidentKey ? null : Utils.newGuid(); - fido2Key.keyType = "public-key"; - fido2Key.keyAlgorithm = "ECDSA"; - fido2Key.keyCurve = "P-256"; - fido2Key.keyValue = Fido2Utils.bufferToString(pcks8Key); - fido2Key.rpId = params.rpEntity.id; - fido2Key.userHandle = Fido2Utils.bufferToString(params.userEntity.id); - fido2Key.counter = 0; - fido2Key.rpName = params.rpEntity.name; - fido2Key.userName = params.userEntity.name; + return fido2Key; +} - return fido2Key; - } +async function getPrivateKeyFromCipher(cipher: CipherView): Promise { + const keyBuffer = Fido2Utils.stringToBuffer(cipher.fido2Key.keyValue); + return await crypto.subtle.importKey( + "pkcs8", + keyBuffer, + { + name: cipher.fido2Key.keyType, + namedCurve: cipher.fido2Key.keyCurve, + } as EcKeyImportParams, + true, + KeyUsages + ); } interface AuthDataParams { @@ -366,6 +385,32 @@ async function generateAuthData(params: AuthDataParams) { return new Uint8Array(authData); } +interface SignatureParams { + authData: Uint8Array; + clientData: BufferSource; + privateKey: CryptoKey; +} + +async function generateSignature(params: SignatureParams) { + const clientData = Fido2Utils.bufferSourceToUint8Array(params.clientData); + const clientDataHash = await crypto.subtle.digest({ name: "SHA-256" }, clientData); + const sigBase = new Uint8Array([...params.authData, ...new Uint8Array(clientDataHash)]); + const p1336_signature = new Uint8Array( + await crypto.subtle.sign( + { + name: "ECDSA", + hash: { name: "SHA-256" }, + }, + params.privateKey, + sigBase + ) + ); + + const asn1Der_signature = joseToDer(p1336_signature, "ES256"); + + return asn1Der_signature; +} + interface Flags { extensionData: boolean; attestationData: boolean;