Skip to content

Commit

Permalink
Merge pull request #230 from agektmr/dev
Browse files Browse the repository at this point in the history
Return `AuthenticationExtensionsAuthenticatorOutputs` as part of registration and authentication
  • Loading branch information
MasterKale committed Jul 23, 2022
2 parents cfa6892 + c532f52 commit ea6ced4
Show file tree
Hide file tree
Showing 8 changed files with 165 additions and 6 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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') {
Expand Down Expand Up @@ -189,6 +190,7 @@ export default function verifyAuthenticationResponse(
credentialID: authenticator.credentialID,
credentialDeviceType,
credentialBackedUp,
authenticatorExtensionResults: extensionsData,
},
};

Expand All @@ -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;
Expand All @@ -218,5 +222,6 @@ export type VerifiedAuthenticationResponse = {
newCounter: number;
credentialDeviceType: CredentialDeviceType;
credentialBackedUp: boolean;
authenticatorExtensionResults?: AuthenticationExtensionsAuthenticatorOutputs;
};
};
22 changes: 22 additions & 0 deletions packages/server/src/helpers/decodeAuthenticatorExtensions.test.ts
Original file line number Diff line number Diff line change
@@ -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')
}
})
});
37 changes: 37 additions & 0 deletions packages/server/src/helpers/decodeAuthenticatorExtensions.ts
Original file line number Diff line number Diff line change
@@ -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[]
}
7 changes: 3 additions & 4 deletions packages/server/src/helpers/parseAuthenticatorData.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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!',
});
Expand Down
6 changes: 6 additions & 0 deletions packages/server/src/helpers/parseAuthenticatorData.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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;
}

Expand All @@ -74,6 +78,7 @@ export default function parseAuthenticatorData(authData: Buffer): ParsedAuthenti
aaguid,
credentialID,
credentialPublicKey,
extensionsData,
extensionsDataBuffer,
};
}
Expand All @@ -95,5 +100,6 @@ export type ParsedAuthenticatorData = {
aaguid?: Buffer;
credentialID?: Buffer;
credentialPublicKey?: Buffer;
extensionsData?: AuthenticationExtensionsAuthenticatorOutputs;
extensionsDataBuffer?: Buffer;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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
*/
Expand Down
15 changes: 14 additions & 1 deletion packages/server/src/registration/verifyRegistrationResponse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -248,6 +257,7 @@ export default async function verifyRegistrationResponse(
userVerified: flags.uv,
credentialDeviceType,
credentialBackedUp,
authenticatorExtensionResults: extensionsData,
};
}

Expand All @@ -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;
Expand All @@ -288,6 +300,7 @@ export type VerifiedRegistrationResponse = {
userVerified: boolean;
credentialDeviceType: CredentialDeviceType;
credentialBackedUp: boolean;
authenticatorExtensionResults?: AuthenticationExtensionsAuthenticatorOutputs;
};
};

Expand Down

0 comments on commit ea6ced4

Please sign in to comment.