diff options
author | Matthew Miller <matthew@millerti.me> | 2020-05-19 14:30:25 -0700 |
---|---|---|
committer | Matthew Miller <matthew@millerti.me> | 2020-05-19 15:03:27 -0700 |
commit | fcddf9aab5c0498bdb8f5f3053af780ef32dbe26 (patch) | |
tree | ea94a35fd3ba5f67ac12ef493cc8b040e9974685 /src | |
parent | 7eade2a965aa79ffd199d04603b2f4dce2727616 (diff) |
Add verifyAssertionResponse
Diffstat (limited to 'src')
-rw-r--r-- | src/assertion/verifyAssertionResponse.test.ts | 25 | ||||
-rw-r--r-- | src/assertion/verifyAssertionResponse.ts | 90 | ||||
-rw-r--r-- | src/types.ts | 31 |
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, +}; |