summaryrefslogtreecommitdiffhomepage
path: root/packages/server/src
diff options
context:
space:
mode:
authorMatthew Miller <matthew@millerti.me>2022-07-22 19:11:46 -0700
committerGitHub <noreply@github.com>2022-07-22 19:11:46 -0700
commitea6ced40a0edbdd7c9be9270bb168b7f117547bd (patch)
tree23d64ae95264edecd19590c3612046d86e2e8ddc /packages/server/src
parentcfa689214f772a6375dcc385714982209ddf1f08 (diff)
parentc532f52e265ab272762f872f70346ce2f66f0199 (diff)
Merge pull request #230 from agektmr/dev
Return `AuthenticationExtensionsAuthenticatorOutputs` as part of registration and authentication
Diffstat (limited to 'packages/server/src')
-rw-r--r--packages/server/src/authentication/verifyAuthenticationResponse.test.ts39
-rw-r--r--packages/server/src/authentication/verifyAuthenticationResponse.ts7
-rw-r--r--packages/server/src/helpers/decodeAuthenticatorExtensions.test.ts22
-rw-r--r--packages/server/src/helpers/decodeAuthenticatorExtensions.ts37
-rw-r--r--packages/server/src/helpers/parseAuthenticatorData.test.ts7
-rw-r--r--packages/server/src/helpers/parseAuthenticatorData.ts6
-rw-r--r--packages/server/src/registration/verifyRegistrationResponse.test.ts38
-rw-r--r--packages/server/src/registration/verifyRegistrationResponse.ts15
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;
};
};