summaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorMatthew Miller <matthew@millerti.me>2020-05-19 14:30:25 -0700
committerMatthew Miller <matthew@millerti.me>2020-05-19 15:03:27 -0700
commitfcddf9aab5c0498bdb8f5f3053af780ef32dbe26 (patch)
treeea94a35fd3ba5f67ac12ef493cc8b040e9974685 /src
parent7eade2a965aa79ffd199d04603b2f4dce2727616 (diff)
Add verifyAssertionResponse
Diffstat (limited to 'src')
-rw-r--r--src/assertion/verifyAssertionResponse.test.ts25
-rw-r--r--src/assertion/verifyAssertionResponse.ts90
-rw-r--r--src/types.ts31
3 files changed, 146 insertions, 0 deletions
diff --git a/src/assertion/verifyAssertionResponse.test.ts b/src/assertion/verifyAssertionResponse.test.ts
new file mode 100644
index 0000000..ba76943
--- /dev/null
+++ b/src/assertion/verifyAssertionResponse.test.ts
@@ -0,0 +1,25 @@
+import verifyAssertionResponse from './verifyAssertionResponse';
+
+test('', () => {
+ const verification = verifyAssertionResponse(
+ {
+ base64AuthenticatorData: 'PdxHEOnAiLIp26idVjIguzn3Ipr_RlsKZWsa-5qK-KABAAAAhw',
+ base64ClientDataJSON: 'eyJjaGFsbGVuZ2UiOiJXRzVRU21RM1oyOTROR2gyTVROUk56WnViVmhMTlZZMWMwOHRP' +
+ 'V3BLVG5JIiwiY2xpZW50RXh0ZW5zaW9ucyI6e30sImhhc2hBbGdvcml0aG0iOiJTSEEtMjU2Iiwib3JpZ2luIjoi' +
+ 'aHR0cHM6Ly9kZXYuZG9udG5lZWRhLnB3IiwidHlwZSI6IndlYmF1dGhuLmdldCJ9',
+ base64Signature: 'MEQCIHZYFY3LsKzI0T9XRwEACl7YsYZysZ2HUw3q9f7tlq3wAiBNbyBbQMNM56P6Z00tBEZ6v' +
+ 'II4f9Al-p4pZw7OBpSaog',
+ userHandle: null,
+ },
+ 'https://dev.dontneeda.pw',
+ {
+ base64PublicKey: 'BBMQEnZRfg4ASys9kfGUj99Xlsa028wqYJZw8xuGahPQJWN3K9D9DajLxzKlY7uf_ulA5D6gh' +
+ 'UJ9hrouDX84S_I',
+ base64CredentialID: 'wJZRtQbYjKlpiRnzet7yyVizdsj_oUhi11kFbKyO0hc5gIg-4xeaTC9YC9y9sfow6gO3jE' +
+ 'MoONBKNX4SmSclmQ',
+ counter: 134,
+ },
+ );
+
+ expect(verification.verified).toEqual(true);
+});
diff --git a/src/assertion/verifyAssertionResponse.ts b/src/assertion/verifyAssertionResponse.ts
new file mode 100644
index 0000000..a6f77c1
--- /dev/null
+++ b/src/assertion/verifyAssertionResponse.ts
@@ -0,0 +1,90 @@
+import base64url from 'base64url';
+
+import {
+ EncodedAuthenticatorAssertionResponse,
+ U2F_USER_PRESENTED,
+ AuthenticatorDevice,
+ VerifiedAssertion,
+} from "@types";
+import decodeClientDataJSON from "@helpers/decodeClientDataJSON";
+
+import parseAssertionAuthData from './parseAssertionAuthData';
+import toHash from '@helpers/toHash';
+import convertASN1toPEM from '@helpers/convertASN1toPEM';
+import verifySignature from '@helpers/verifySignature';
+
+/**
+ * Verify that the user has legitimately completed the login process
+ *
+ * @param response Authenticator attestation response with base64-encoded values
+ * @param expectedOrigin Expected URL of website attestation should have occurred on
+ */
+export default function verifyAssertionResponse(
+ response: EncodedAuthenticatorAssertionResponse,
+ expectedOrigin: string,
+ authenticator: AuthenticatorDevice,
+): VerifiedAssertion {
+ const { base64AuthenticatorData, base64ClientDataJSON, base64Signature } = response;
+ const clientDataJSON = decodeClientDataJSON(base64ClientDataJSON);
+
+ console.debug('decodedClientDataJSON:', clientDataJSON);
+
+ const { type, origin } = clientDataJSON;
+
+ // Check that the origin is our site
+ if (origin !== expectedOrigin) {
+ console.error('client origin did not equal our origin');
+ console.debug('expectedOrigin:', expectedOrigin);
+ console.debug('assertion\'s origin:', origin);
+ throw new Error('Assertion origin was an unexpected value');
+ }
+
+ // Make sure we're handling an assertion
+ if (type !== 'webauthn.get') {
+ console.error('type did not equal "webauthn.get"');
+ console.debug('attestation\'s type:', type);
+ throw new Error('Attestation type was an unexpected value');
+ }
+
+ const authDataBuffer = base64url.toBuffer(base64AuthenticatorData);
+ const authData = parseAssertionAuthData(authDataBuffer);
+ console.log('parsed authData:', authData);
+
+ if (!(authData.flags & U2F_USER_PRESENTED)) {
+ throw new Error('User was NOT present during authentication!');
+ }
+
+ const {
+ rpIdHash,
+ flagsBuf,
+ counterBuf,
+ counter,
+ } = authData;
+
+ const clientDataHash = toHash(base64url.toBuffer(base64ClientDataJSON));
+ const signatureBase = Buffer.concat([
+ rpIdHash,
+ flagsBuf,
+ counterBuf,
+ clientDataHash,
+ ]);
+
+ const publicKey = convertASN1toPEM(base64url.toBuffer(authenticator.base64PublicKey));
+ const signature = base64url.toBuffer(base64Signature);
+
+ const toReturn = {
+ verified: verifySignature(signature, signatureBase, publicKey),
+ };
+
+ if (toReturn.verified) {
+ if (counter <= authenticator.counter) {
+ // Error out when the counter in the DB is greater than or equal to the counter in the
+ // dataStruct. It's related to how the authenticator maintains the number of times its been
+ // used for this client. If this happens, then someone's somehow increased the counter
+ // on the device without going through this site
+ throw new Error(`Device's counter ${counter} isn't greater than ${authenticator.counter}!`);
+ }
+ }
+
+ return toReturn;
+}
diff --git a/src/types.ts b/src/types.ts
index b528978..cccd755 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -23,6 +23,18 @@ AuthenticatorAttestationResponse, 'clientDataJSON' | 'attestationObject'
base64AttestationObject: string;
}
+/**
+ * A slightly-modified AuthenticatorAttestationResponse to simplify working with ArrayBuffers that
+ * are base64-encoded in the browser so that they can be sent as JSON to the server.
+ */
+export interface EncodedAuthenticatorAssertionResponse extends Omit<
+AuthenticatorAssertionResponse, 'clientDataJSON' | 'authenticatorData' | 'signature'
+> {
+ base64AuthenticatorData: string;
+ base64ClientDataJSON: string;
+ base64Signature: string;
+}
+
export enum ATTESTATION_FORMATS {
FIDO_U2F = 'fido-u2f',
PACKED = 'packed',
@@ -64,6 +76,9 @@ export type ClientDataJSON = {
origin: string,
};
+/**
+ * Result of attestation verification
+ */
export type VerifiedAttestation = {
verified: boolean,
authenticatorInfo?: {
@@ -74,6 +89,13 @@ export type VerifiedAttestation = {
},
};
+/**
+ * Result of assertion verification
+ */
+export type VerifiedAssertion = {
+ verified: boolean;
+};
+
export type CertificateInfo = {
subject: { [key: string]: string },
version: number,
@@ -122,3 +144,12 @@ export type ParsedAssertionAuthData = {
*/
export const U2F_USER_PRESENTED = 0x01;
+/**
+ * A WebAuthn-compatible device and the information needed to verify assertions by it
+ */
+export type AuthenticatorDevice = {
+ base64PublicKey: string,
+ base64CredentialID: string,
+ // Number of times this device is expected to have been used
+ counter: number,
+};