diff options
10 files changed, 149 insertions, 13 deletions
diff --git a/packages/server/package.json b/packages/server/package.json index ce9c622..92dfe78 100644 --- a/packages/server/package.json +++ b/packages/server/package.json @@ -46,9 +46,9 @@ "node" ], "dependencies": { - "@peculiar/asn1-android": "^2.0.38", - "@peculiar/asn1-schema": "^2.0.38", - "@peculiar/asn1-x509": "^2.0.38", + "@peculiar/asn1-android": "^2.1.7", + "@peculiar/asn1-schema": "^2.1.7", + "@peculiar/asn1-x509": "^2.1.7", "@simplewebauthn/typescript-types": "file:../typescript-types", "base64url": "^3.0.1", "cbor": "^5.1.0", diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts index 57d9613..1273e89 100644 --- a/packages/server/src/authentication/verifyAuthenticationResponse.test.ts +++ b/packages/server/src/authentication/verifyAuthenticationResponse.test.ts @@ -308,6 +308,19 @@ test('should fail verification if custom challenge verifier returns false', () = }).toThrow(/custom challenge verifier returned false/i); }); +test('should return credential backup info', async () => { + const verification = verifyAuthenticationResponse({ + credential: assertionResponse, + expectedChallenge: assertionChallenge, + expectedOrigin: assertionOrigin, + expectedRPID: 'dev.dontneeda.pw', + authenticator: authenticator, + }); + + expect(verification.authenticationInfo?.credentialDeviceType).toEqual('singleDevice'); + expect(verification.authenticationInfo?.credentialBackedUp).toEqual(false); +}); + /** * Assertion examples below */ diff --git a/packages/server/src/authentication/verifyAuthenticationResponse.ts b/packages/server/src/authentication/verifyAuthenticationResponse.ts index e7ec1ec..264a2f2 100644 --- a/packages/server/src/authentication/verifyAuthenticationResponse.ts +++ b/packages/server/src/authentication/verifyAuthenticationResponse.ts @@ -2,6 +2,7 @@ import base64url from 'base64url'; import { AuthenticationCredentialJSON, AuthenticatorDevice, + CredentialDeviceType, } from '@simplewebauthn/typescript-types'; import decodeClientDataJSON from '../helpers/decodeClientDataJSON'; @@ -10,6 +11,7 @@ import convertPublicKeyToPEM from '../helpers/convertPublicKeyToPEM'; import verifySignature from '../helpers/verifySignature'; import parseAuthenticatorData from '../helpers/parseAuthenticatorData'; import isBase64URLString from '../helpers/isBase64URLString'; +import { parseBackupFlags } from '../helpers/parseBackupFlags'; export type VerifyAuthenticationResponseOpts = { credential: AuthenticationCredentialJSON; @@ -178,11 +180,15 @@ export default function verifyAuthenticationResponse( ); } + const { credentialDeviceType, credentialBackedUp } = parseBackupFlags(flags); + const toReturn = { verified: verifySignature(signature, signatureBase, publicKey), authenticationInfo: { newCounter: counter, credentialID: authenticator.credentialID, + credentialDeviceType, + credentialBackedUp, }, }; @@ -199,11 +205,18 @@ export default function verifyAuthenticationResponse( * @param authenticationInfo.newCounter The number of times the authenticator identified above * reported it has been used. **Should be kept in a DB for later reference to help prevent replay * attacks!** + * @param authenticationInfo.credentialDeviceType Whether this is a single-device or multi-device + * credential. **Should be kept in a DB for later reference!** + * @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!** */ export type VerifiedAuthenticationResponse = { verified: boolean; authenticationInfo: { credentialID: Buffer; newCounter: number; + credentialDeviceType: CredentialDeviceType; + credentialBackedUp: boolean; }; }; diff --git a/packages/server/src/helpers/parseAuthenticatorData.test.ts b/packages/server/src/helpers/parseAuthenticatorData.test.ts index 3bba4b6..a815c85 100644 --- a/packages/server/src/helpers/parseAuthenticatorData.test.ts +++ b/packages/server/src/helpers/parseAuthenticatorData.test.ts @@ -20,6 +20,8 @@ test('should parse flags', () => { expect(flags.up).toEqual(true); expect(flags.uv).toEqual(false); + expect(flags.be).toEqual(false); + expect(flags.bs).toEqual(false); expect(flags.at).toEqual(false); expect(flags.ed).toEqual(true); }); diff --git a/packages/server/src/helpers/parseAuthenticatorData.ts b/packages/server/src/helpers/parseAuthenticatorData.ts index 911c9e0..6bf5b9a 100644 --- a/packages/server/src/helpers/parseAuthenticatorData.ts +++ b/packages/server/src/helpers/parseAuthenticatorData.ts @@ -18,11 +18,15 @@ export default function parseAuthenticatorData(authData: Buffer): ParsedAuthenti const flagsBuf = authData.slice(pointer, (pointer += 1)); const flagsInt = flagsBuf[0]; + // Bit positions can be referenced here: + // https://www.w3.org/TR/webauthn-2/#flags const flags = { - up: !!(flagsInt & 0x01), - uv: !!(flagsInt & 0x04), - at: !!(flagsInt & 0x40), - ed: !!(flagsInt & 0x80), + up: !!(flagsInt & 1 << 0), // User Presence + uv: !!(flagsInt & 1 << 2), // User Verified + be: !!(flagsInt & 1 << 3), // Backup Eligibility + bs: !!(flagsInt & 1 << 4), // Backup State + at: !!(flagsInt & 1 << 6), // Attested Credential Data Present + ed: !!(flagsInt & 1 << 7), // Extension Data Present flagsInt, }; @@ -80,6 +84,8 @@ export type ParsedAuthenticatorData = { flags: { up: boolean; uv: boolean; + be: boolean; + bs: boolean; at: boolean; ed: boolean; flagsInt: number; diff --git a/packages/server/src/helpers/parseBackupFlags.test.ts b/packages/server/src/helpers/parseBackupFlags.test.ts new file mode 100644 index 0000000..3133137 --- /dev/null +++ b/packages/server/src/helpers/parseBackupFlags.test.ts @@ -0,0 +1,34 @@ +import { parseBackupFlags } from './parseBackupFlags'; + +test('should return single-device cred, not backed up', () => { + const parsed = parseBackupFlags({ be: false, bs: false }); + + expect(parsed.credentialDeviceType).toEqual('singleDevice'); + expect(parsed.credentialBackedUp).toEqual(false); +}); + +test('should throw on single-device cred, backed up', () => { + expect.assertions(2); + + try { + parseBackupFlags({ be: false, bs: true }); + } catch (err) { + const _err: Error = err as Error; + expect(_err.message).toContain('impossible'); + expect(_err.name).toEqual('InvalidBackupFlags') + } +}); + +test('should return multi-device cred, not backed up', () => { + const parsed = parseBackupFlags({ be: true, bs: false }); + + expect(parsed.credentialDeviceType).toEqual('multiDevice'); + expect(parsed.credentialBackedUp).toEqual(false); +}); + +test('should return multi-device cred, backed up', () => { + const parsed = parseBackupFlags({ be: true, bs: true }); + + expect(parsed.credentialDeviceType).toEqual('multiDevice'); + expect(parsed.credentialBackedUp).toEqual(true); +}); diff --git a/packages/server/src/helpers/parseBackupFlags.ts b/packages/server/src/helpers/parseBackupFlags.ts new file mode 100644 index 0000000..c0a1e99 --- /dev/null +++ b/packages/server/src/helpers/parseBackupFlags.ts @@ -0,0 +1,36 @@ +import { CredentialDeviceType } from '@simplewebauthn/typescript-types'; + +/** + * Make sense of Bits 3 and 4 in authenticator indicating: + * + * - Whether the credential can be used on multiple devices + * - Whether the credential is backed up or not + * + * Invalid configurations will raise an `Error` + */ +export function parseBackupFlags({ be, bs }: { be: boolean, bs: boolean }): { + credentialDeviceType: CredentialDeviceType, + credentialBackedUp: boolean, +} { + const credentialBackedUp = bs; + let credentialDeviceType: CredentialDeviceType = 'singleDevice'; + + if (be) { + credentialDeviceType = 'multiDevice'; + } + + if (credentialDeviceType === 'singleDevice' && credentialBackedUp) { + throw new InvalidBackupFlags( + 'Single-device credential indicated that it was backed up, which should be impossible.' + ) + } + + return { credentialDeviceType, credentialBackedUp }; +} + +class InvalidBackupFlags extends Error { + constructor(message: string) { + super(message); + this.name = 'InvalidBackupFlags'; + } +} diff --git a/packages/server/src/registration/verifyRegistrationResponse.test.ts b/packages/server/src/registration/verifyRegistrationResponse.test.ts index f9074cc..03f74ef 100644 --- a/packages/server/src/registration/verifyRegistrationResponse.test.ts +++ b/packages/server/src/registration/verifyRegistrationResponse.test.ts @@ -568,6 +568,18 @@ test('should fail verification if custom challenge verifier returns false', asyn ).rejects.toThrow(/custom challenge verifier returned false/i); }); +test('should return credential backup info', async () => { + const verification = await verifyRegistrationResponse({ + credential: attestationNone, + expectedChallenge: attestationNoneChallenge, + expectedOrigin: 'https://dev.dontneeda.pw', + expectedRPID: 'dev.dontneeda.pw', + }); + + expect(verification.registrationInfo?.credentialDeviceType).toEqual('singleDevice'); + expect(verification.registrationInfo?.credentialBackedUp).toEqual(false); +}); + /** * Various Attestations Below */ diff --git a/packages/server/src/registration/verifyRegistrationResponse.ts b/packages/server/src/registration/verifyRegistrationResponse.ts index f9c111d..6fc6d86 100644 --- a/packages/server/src/registration/verifyRegistrationResponse.ts +++ b/packages/server/src/registration/verifyRegistrationResponse.ts @@ -2,6 +2,7 @@ import base64url from 'base64url'; import { RegistrationCredentialJSON, COSEAlgorithmIdentifier, + CredentialDeviceType, } from '@simplewebauthn/typescript-types'; import decodeAttestationObject, { @@ -14,6 +15,7 @@ import toHash from '../helpers/toHash'; import decodeCredentialPublicKey from '../helpers/decodeCredentialPublicKey'; import { COSEKEYS } from '../helpers/convertCOSEtoPKCS'; import convertAAGUIDToString from '../helpers/convertAAGUIDToString'; +import { parseBackupFlags } from '../helpers/parseBackupFlags'; import settingsService from '../services/settingsService'; import { supportedCOSEAlgorithmIdentifiers } from './generateRegistrationOptions'; @@ -233,15 +235,19 @@ export default async function verifyRegistrationResponse( }; if (toReturn.verified) { + const { credentialDeviceType, credentialBackedUp } = parseBackupFlags(flags); + toReturn.registrationInfo = { fmt, counter, aaguid: convertAAGUIDToString(aaguid), - credentialPublicKey, credentialID, + credentialPublicKey, credentialType, - userVerified: flags.uv, attestationObject, + userVerified: flags.uv, + credentialDeviceType, + credentialBackedUp, }; } @@ -254,7 +260,7 @@ export default async function verifyRegistrationResponse( * @param verified If the assertion response could be verified * @param registrationInfo.fmt Type of attestation * @param registrationInfo.counter The number of times the authenticator reported it has been used. - * Should be kept in a DB for later reference to help prevent replay attacks + * **Should be kept in a DB for later reference to help prevent replay attacks!** * @param registrationInfo.aaguid Authenticator's Attestation GUID indicating the type of the * authenticator * @param registrationInfo.credentialPublicKey The credential's public key @@ -263,6 +269,11 @@ export default async function verifyRegistrationResponse( * @param registrationInfo.userVerified Whether the user was uniquely identified during attestation * @param registrationInfo.attestationObject The raw `response.attestationObject` Buffer returned by * the authenticator + * @param registrationInfo.credentialDeviceType Whether this is a single-device or multi-device + * credential. **Should be kept in a DB for later reference!** + * @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!** */ export type VerifiedRegistrationResponse = { verified: boolean; @@ -270,11 +281,13 @@ export type VerifiedRegistrationResponse = { fmt: AttestationFormat; counter: number; aaguid: string; - credentialPublicKey: Buffer; credentialID: Buffer; - credentialType: string; - userVerified: boolean; + credentialPublicKey: Buffer; + credentialType: "public-key"; attestationObject: Buffer; + userVerified: boolean; + credentialDeviceType: CredentialDeviceType; + credentialBackedUp: boolean; }; }; diff --git a/packages/typescript-types/src/index.ts b/packages/typescript-types/src/index.ts index 751474a..6381b80 100644 --- a/packages/typescript-types/src/index.ts +++ b/packages/typescript-types/src/index.ts @@ -152,3 +152,10 @@ export interface AuthenticatorAttestationResponseFuture extends AuthenticatorAtt * registration and authentication. */ export type AuthenticatorTransport = "ble" | "internal" | "nfc" | "usb" | "cable"; + +/** + * The two types of credentials as defined by bit 3 ("Backup Eligibility") in authenticator data: + * - `"singleDevice"` credentials will never be backed up + * - `"multiDevice"` credentials can be backed up + */ +export type CredentialDeviceType = 'singleDevice' | 'multiDevice'; |