diff options
Diffstat (limited to 'packages/server/src')
8 files changed, 165 insertions, 6 deletions
diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts index 1273e89..dbaa946 100644 --- a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts +++ b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts @@ -308,6 +308,45 @@ test('should fail verification if custom challenge verifier returns false', () = }).toThrow(/custom challenge verifier returned false/i); }); +test('should return authenticator extension output', async () => { + const verification = verifyAuthenticationResponse({ + credential: { + response: { + clientDataJSON: "eyJ0eXBlIjoid2ViYXV0aG4uZ2V0IiwiY2hhbGxlbmdlIjoiaVpzVkN6dHJEVzdEMlVfR0hDSWxZS0x3VjJiQ3NCVFJxVlFVbkpYbjlUayIsIm9yaWdpbiI6ImFuZHJvaWQ6YXBrLWtleS1oYXNoOmd4N3NxX3B4aHhocklRZEx5ZkcwcHhLd2lKN2hPazJESlE0eHZLZDQzOFEiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uZmlkby5leGFtcGxlLmZpZG8yYXBpZXhhbXBsZSJ9", + authenticatorData:"DXX8xWP9p3nbLjQ-6kiYiHWLeFSdSTpP2-oc2WqjHMSFAAAAAKFvZGV2aWNlUHVibGljS2V5pWNkcGtYTaUBAgMmIAEhWCCZGqvtneQnGp7erYgG-dyW1tzNDEdiU6VRBInsg3m-WyJYIKCXPP3tu3nif-9O50gWc_szElBN3KVDTP0jQx1q0p7aY3NpZ1hHMEUCIElSbNKK72tOYhp9WTbStQSVL8CuIxOk8DV6r_-uqWR0AiEAnVE6yu-wsyx2Wq5v66jClGhe_2P_HL8R7PIQevT-uPhlbm9uY2VAZXNjb3BlQQBmYWFndWlkULk_2WHy5kYvsSKCACJH3ng=", + signature:"MEYCIQDlRuxY7cYre0sb3T6TovQdfYIUb72cRZYOQv_zS9wN_wIhAOvN-fwjtyIhWRceqJV4SX74-z6oALERbC7ohk8EdVPO", + userHandle:"b2FPajFxcmM4MWo3QkFFel9RN2lEakh5RVNlU2RLNDF0Sl92eHpQYWV5UQ==" + }, + id:"E_Pko4wN1BXE23S0ftN3eQ", + rawId:"E_Pko4wN1BXE23S0ftN3eQ", + type:"public-key", + clientExtensionResults: {} + }, + expectedOrigin: 'android:apk-key-hash:gx7sq_pxhxhrIQdLyfG0pxKwiJ7hOk2DJQ4xvKd438Q', + expectedRPID: 'try-webauthn.appspot.com', + expectedChallenge: 'iZsVCztrDW7D2U_GHCIlYKLwV2bCsBTRqVQUnJXn9Tk', + authenticator: { + credentialID: base64url.toBuffer( + 'AaIBxnYfL2pDWJmIii6CYgHBruhVvFGHheWamphVioG_TnEXxKA9MW4FWnJh21zsbmRpRJso9i2JmAtWOtXfVd4oXTgYVusXwhWWsA' + ), + credentialPublicKey: base64url.toBuffer( + 'pQECAyYgASFYILTrxTUQv3X4DRM6L_pk65FSMebenhCx3RMsTKoBm-AxIlggEf3qk5552QLNSh1T1oQs7_2C2qysDwN4r4fCp52Hsqs' + ), + counter: 0, + } + }); + + expect(verification.authenticationInfo?.authenticatorExtensionResults).toMatchObject({ + 'devicePublicKey': { + 'dpk': Buffer.from('A5010203262001215820991AABED9DE4271A9EDEAD8806F9DC96D6DCCD0C476253A5510489EC8379BE5B225820A0973CFDEDBB79E27FEF4EE7481673FB3312504DDCA5434CFD23431D6AD29EDA', 'hex'), + 'sig': Buffer.from('3045022049526CD28AEF6B4E621A7D5936D2B504952FC0AE2313A4F0357AAFFFAEA964740221009D513ACAEFB0B32C765AAE6FEBA8C294685EFF63FF1CBF11ECF2107AF4FEB8F8', 'hex'), + 'nonce': Buffer.from('', 'hex'), + 'scope': Buffer.from('00', 'hex'), + 'aaguid': Buffer.from('B93FD961F2E6462FB12282002247DE78', 'hex') + } + }); +}); + test('should return credential backup info', async () => { const verification = verifyAuthenticationResponse({ credential: assertionResponse, diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.ts b/packages/server/src/authentication/verifyAuthenticationResponse.ts index 264a2f2..ebc1dca 100644 --- a/packages/server/src/authentication/verifyAuthenticationResponse.ts +++ b/packages/server/src/authentication/verifyAuthenticationResponse.ts @@ -12,6 +12,7 @@ import verifySignature from '../helpers/verifySignature'; import parseAuthenticatorData from '../helpers/parseAuthenticatorData'; import isBase64URLString from '../helpers/isBase64URLString'; import { parseBackupFlags } from '../helpers/parseBackupFlags'; +import { AuthenticationExtensionsAuthenticatorOutputs } from '../helpers/decodeAuthenticatorExtensions'; export type VerifyAuthenticationResponseOpts = { credential: AuthenticationCredentialJSON; @@ -134,7 +135,7 @@ export default function verifyAuthenticationResponse( const authDataBuffer = base64url.toBuffer(response.authenticatorData); const parsedAuthData = parseAuthenticatorData(authDataBuffer); - const { rpIdHash, flags, counter } = parsedAuthData; + const { rpIdHash, flags, counter, extensionsData } = parsedAuthData; // Make sure the response's RP ID is ours if (typeof expectedRPID === 'string') { @@ -189,6 +190,7 @@ export default function verifyAuthenticationResponse( credentialID: authenticator.credentialID, credentialDeviceType, credentialBackedUp, + authenticatorExtensionResults: extensionsData, }, }; @@ -210,6 +212,8 @@ export default function verifyAuthenticationResponse( * @param authenticationInfo.credentialBackedUp Whether or not the multi-device credential has been * backed up. Always `false` for single-device credentials. **Should be kept in a DB for later * reference!** + * @param authenticationInfo?.authenticatorExtensionResults The authenticator extensions returned + * by the browser */ export type VerifiedAuthenticationResponse = { verified: boolean; @@ -218,5 +222,6 @@ export type VerifiedAuthenticationResponse = { newCounter: number; credentialDeviceType: CredentialDeviceType; credentialBackedUp: boolean; + authenticatorExtensionResults?: AuthenticationExtensionsAuthenticatorOutputs; }; }; diff --git a/packages/server/src/helpers/decodeAuthenticatorExtensions.test.ts b/packages/server/src/helpers/decodeAuthenticatorExtensions.test.ts new file mode 100644 index 0000000..5c184a8 --- /dev/null +++ b/packages/server/src/helpers/decodeAuthenticatorExtensions.test.ts @@ -0,0 +1,22 @@ +import { decodeAuthenticatorExtensions } from "./decodeAuthenticatorExtensions"; + +test('should decode authenticator extensions', () => { + const extensions = decodeAuthenticatorExtensions(Buffer.from( + 'A16F6465766963655075626C69634B6579A56364706B584DA5010203262001215820991A' + + 'ABED9DE4271A9EDEAD8806F9DC96D6DCCD0C476253A5510489EC8379BE5B225820A0973C' + + 'FDEDBB79E27FEF4EE7481673FB3312504DDCA5434CFD23431D6AD29EDA63736967584730' + + '45022100EFB38074BD15B8C82CF09F87FBC6FB3C7169EA4F1806B7E90937374302345B7A' + + '02202B7113040731A0E727D338D48542863CE65880AA79E5EA740AC8CCD94347988E656E' + + '6F6E6365406573636F706541006661616775696450000000000000000000000000000000' + + '00', 'hex' + )); + expect(extensions).toMatchObject({ + "devicePublicKey": { + "dpk": Buffer.from('A5010203262001215820991AABED9DE4271A9EDEAD8806F9DC96D6DCCD0C476253A5510489EC8379BE5B225820A0973CFDEDBB79E27FEF4EE7481673FB3312504DDCA5434CFD23431D6AD29EDA', 'hex'), + "sig": Buffer.from('3045022100EFB38074BD15B8C82CF09F87FBC6FB3C7169EA4F1806B7E90937374302345B7A02202B7113040731A0E727D338D48542863CE65880AA79E5EA740AC8CCD94347988E', 'hex'), + "nonce": Buffer.from('', 'hex'), + "scope": Buffer.from('00', 'hex'), + "aaguid": Buffer.from('00000000000000000000000000000000', 'hex') + } + }) +}); diff --git a/packages/server/src/helpers/decodeAuthenticatorExtensions.ts b/packages/server/src/helpers/decodeAuthenticatorExtensions.ts new file mode 100644 index 0000000..a0dc5c2 --- /dev/null +++ b/packages/server/src/helpers/decodeAuthenticatorExtensions.ts @@ -0,0 +1,37 @@ +import cbor from 'cbor'; + +/** + * Convert authenticator extension data buffer to a proper object + * + * @param extensionData Authenticator Extension Data buffer + */ +export function decodeAuthenticatorExtensions( + extensionData: Buffer +): AuthenticationExtensionsAuthenticatorOutputs | undefined { + let toCBOR: AuthenticationExtensionsAuthenticatorOutputs | undefined; + try { + toCBOR = cbor.decodeAllSync(extensionData)[0]; + } catch (err) { + const _err = err as Error; + throw new Error(`Error decoding authenticator extensions: ${_err.message}`); + } + return toCBOR; +} + +export type AuthenticationExtensionsAuthenticatorOutputs = { + devicePublicKey?: DevicePublicKeyAuthenticatorOutput; + uvm?: UVMAuthenticatorOutput; +} + +export type DevicePublicKeyAuthenticatorOutput = { + dpk?: Buffer; + scp?: Buffer; + sig?: string; + aaguid?: Buffer; +} + +// TODO: Need to verify this format +// https://w3c.github.io/webauthn/#sctn-uvm-extension. +export type UVMAuthenticatorOutput = { + uvm?: Buffer[] +} diff --git a/packages/server/src/helpers/parseAuthenticatorData.test.ts b/packages/server/src/helpers/parseAuthenticatorData.test.ts index a815c85..e199898 100644 --- a/packages/server/src/helpers/parseAuthenticatorData.test.ts +++ b/packages/server/src/helpers/parseAuthenticatorData.test.ts @@ -43,11 +43,10 @@ test('should parse extension data', () => { const parsed = parseAuthenticatorData(authDataWithED); - const { extensionsDataBuffer } = parsed; + const { extensionsData } = parsed; - if (extensionsDataBuffer) { - const decoded = cbor.decodeFirstSync(extensionsDataBuffer); - expect(decoded).toEqual({ + if (extensionsData) { + expect(extensionsData).toEqual({ 'example.extension': 'This is an example extension! If you read this message, you probably successfully passing conformance tests. Good job!', }); diff --git a/packages/server/src/helpers/parseAuthenticatorData.ts b/packages/server/src/helpers/parseAuthenticatorData.ts index 6bf5b9a..199513a 100644 --- a/packages/server/src/helpers/parseAuthenticatorData.ts +++ b/packages/server/src/helpers/parseAuthenticatorData.ts @@ -1,5 +1,6 @@ import cbor from 'cbor'; import { decodeCborFirst } from './decodeCbor'; +import { decodeAuthenticatorExtensions, AuthenticationExtensionsAuthenticatorOutputs } from './decodeAuthenticatorExtensions'; /** * Make sense of the authData buffer contained in an Attestation @@ -52,11 +53,14 @@ export default function parseAuthenticatorData(authData: Buffer): ParsedAuthenti pointer += firstEncoded.byteLength; } + let extensionsData: AuthenticationExtensionsAuthenticatorOutputs | undefined = undefined; let extensionsDataBuffer: Buffer | undefined = undefined; + if (flags.ed) { const firstDecoded = decodeCborFirst(authData.slice(pointer)); const firstEncoded = Buffer.from(cbor.encode(firstDecoded) as ArrayBuffer); extensionsDataBuffer = firstEncoded; + extensionsData = decodeAuthenticatorExtensions(extensionsDataBuffer); pointer += firstEncoded.byteLength; } @@ -74,6 +78,7 @@ export default function parseAuthenticatorData(authData: Buffer): ParsedAuthenti aaguid, credentialID, credentialPublicKey, + extensionsData, extensionsDataBuffer, }; } @@ -95,5 +100,6 @@ export type ParsedAuthenticatorData = { aaguid?: Buffer; credentialID?: Buffer; credentialPublicKey?: Buffer; + extensionsData?: AuthenticationExtensionsAuthenticatorOutputs; extensionsDataBuffer?: Buffer; }; diff --git a/packages/server/src/registration/verifyRegistrationResponse.test.ts b/packages/server/src/registration/verifyRegistrationResponse.test.ts index 03f74ef..d4c4f20 100644 --- a/packages/server/src/registration/verifyRegistrationResponse.test.ts +++ b/packages/server/src/registration/verifyRegistrationResponse.test.ts @@ -580,6 +580,44 @@ test('should return credential backup info', async () => { expect(verification.registrationInfo?.credentialBackedUp).toEqual(false); }); +test('should return authenticator extension output', async () => { + const verification = await verifyRegistrationResponse({ + credential: { + id: 'E_Pko4wN1BXE23S0ftN3eQ', + rawId: 'E_Pko4wN1BXE23S0ftN3eQ', + response: { + attestationObject: + 'o2NmbXRkbm9uZWdhdHRTdG10oGhhdXRoRGF0YVkBbQ11_MVj_ad52y40PupImIh1i3hUnUk6T9vqHNlqoxzExQAA' + + 'AAAAAAAAAAAAAAAAAAAAAAAAABAT8-SjjA3UFcTbdLR-03d5pQECAyYgASFYIJIkX8fs9wjKUv5HWBUop--6ig4S' + + 'zsxj8gBgJJmaX-_5IlggJ5XVdjUfCMlVlUZuHJRxCLFLzZCeK8Fg3l6OLfAIHnKhb2RldmljZVB1YmxpY0tleaVj' + + 'ZHBrWE2lAQIDJiABIVggmRqr7Z3kJxqe3q2IBvncltbczQxHYlOlUQSJ7IN5vlsiWCCglzz97bt54n_vTudIFnP7' + + 'MxJQTdylQ0z9I0MdatKe2mNzaWdYRzBFAiEA77OAdL0VuMgs8J-H-8b7PHFp6k8YBrfpCTc3QwI0W3oCICtxEwQH' + + 'MaDnJ9M41IVChjzmWICqeeXqdArIzNlDR5iOZW5vbmNlQGVzY29wZUEAZmFhZ3VpZFAAAAAAAAAAAAAAAAAAAAAA', + clientDataJSON: + 'eyJ0eXBlIjoid2ViYXV0aG4uY3JlYXRlIiwiY2hhbGxlbmdlIjoiQXJrcmxfRnhfTXZjSl9lSXFDVFE3LXRiRVNJ' + + 'U1IxNC1weVBSaDBLLTFBOCIsIm9yaWdpbiI6ImFuZHJvaWQ6YXBrLWtleS1oYXNoOmd4N3NxX3B4aHhocklRZEx5' + + 'ZkcwcHhLd2lKN2hPazJESlE0eHZLZDQzOFEiLCJhbmRyb2lkUGFja2FnZU5hbWUiOiJjb20uZmlkby5leGFtcGxl' + + 'LmZpZG8yYXBpZXhhbXBsZSJ9', + }, + clientExtensionResults: {}, + type: 'public-key', + }, + expectedChallenge: 'Arkrl_Fx_MvcJ_eIqCTQ7-tbESISR14-pyPRh0K-1A8', + expectedOrigin: 'android:apk-key-hash:gx7sq_pxhxhrIQdLyfG0pxKwiJ7hOk2DJQ4xvKd438Q', + expectedRPID: 'try-webauthn.appspot.com', + }); + + expect(verification.registrationInfo?.authenticatorExtensionResults).toMatchObject({ + 'devicePublicKey': { + "dpk": Buffer.from('A5010203262001215820991AABED9DE4271A9EDEAD8806F9DC96D6DCCD0C476253A5510489EC8379BE5B225820A0973CFDEDBB79E27FEF4EE7481673FB3312504DDCA5434CFD23431D6AD29EDA', 'hex'), + "sig": Buffer.from('3045022100EFB38074BD15B8C82CF09F87FBC6FB3C7169EA4F1806B7E90937374302345B7A02202B7113040731A0E727D338D48542863CE65880AA79E5EA740AC8CCD94347988E', 'hex'), + "nonce": Buffer.from('', 'hex'), + "scope": Buffer.from('00', 'hex'), + "aaguid": Buffer.from('00000000000000000000000000000000', 'hex') + } + }); +}); + /** * Various Attestations Below */ diff --git a/packages/server/src/registration/verifyRegistrationResponse.ts b/packages/server/src/registration/verifyRegistrationResponse.ts index 6fc6d86..8829db1 100644 --- a/packages/server/src/registration/verifyRegistrationResponse.ts +++ b/packages/server/src/registration/verifyRegistrationResponse.ts @@ -9,6 +9,7 @@ import decodeAttestationObject, { AttestationFormat, AttestationStatement, } from '../helpers/decodeAttestationObject'; +import { AuthenticationExtensionsAuthenticatorOutputs } from '../helpers/decodeAuthenticatorExtensions'; import decodeClientDataJSON from '../helpers/decodeClientDataJSON'; import parseAuthenticatorData from '../helpers/parseAuthenticatorData'; import toHash from '../helpers/toHash'; @@ -132,7 +133,15 @@ export default async function verifyRegistrationResponse( const { fmt, authData, attStmt } = decodedAttestationObject; const parsedAuthData = parseAuthenticatorData(authData); - const { aaguid, rpIdHash, flags, credentialID, counter, credentialPublicKey } = parsedAuthData; + const { + aaguid, + rpIdHash, + flags, + credentialID, + counter, + credentialPublicKey, + extensionsData, + } = parsedAuthData; // Make sure the response's RP ID is ours if (expectedRPID) { @@ -248,6 +257,7 @@ export default async function verifyRegistrationResponse( userVerified: flags.uv, credentialDeviceType, credentialBackedUp, + authenticatorExtensionResults: extensionsData, }; } @@ -274,6 +284,8 @@ export default async function verifyRegistrationResponse( * @param registrationInfo.credentialBackedUp Whether or not the multi-device credential has been * backed up. Always `false` for single-device credentials. **Should be kept in a DB for later * reference!** + * @param registrationInfo?.authenticatorExtensionResults The authenticator extensions returned + * by the browser */ export type VerifiedRegistrationResponse = { verified: boolean; @@ -288,6 +300,7 @@ export type VerifiedRegistrationResponse = { userVerified: boolean; credentialDeviceType: CredentialDeviceType; credentialBackedUp: boolean; + authenticatorExtensionResults?: AuthenticationExtensionsAuthenticatorOutputs; }; }; |