summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorMatthew Miller <matthew@millerti.me>2022-05-16 22:52:53 -0700
committerGitHub <noreply@github.com>2022-05-16 22:52:53 -0700
commit39a41ae7e86a4782017c2f489f7c66a9effdcaf3 (patch)
treeccc0cb0708cce781f7d6e189528b37bbd9a1a503
parent5c189bca1dc480e919bb82077b51829f29123375 (diff)
parent190a746b89e2cf3238c0070c08f8458e7751c5ea (diff)
Merge pull request #195 from MasterKale/feat/backup-state
feat/backup-state
-rw-r--r--packages/server/package.json6
-rw-r--r--packages/server/src/authentication/verifyAuthenticationResponse.test.ts13
-rw-r--r--packages/server/src/authentication/verifyAuthenticationResponse.ts13
-rw-r--r--packages/server/src/helpers/parseAuthenticatorData.test.ts2
-rw-r--r--packages/server/src/helpers/parseAuthenticatorData.ts14
-rw-r--r--packages/server/src/helpers/parseBackupFlags.test.ts34
-rw-r--r--packages/server/src/helpers/parseBackupFlags.ts36
-rw-r--r--packages/server/src/registration/verifyRegistrationResponse.test.ts12
-rw-r--r--packages/server/src/registration/verifyRegistrationResponse.ts25
-rw-r--r--packages/typescript-types/src/index.ts7
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';