summaryrefslogtreecommitdiffhomepage
path: root/packages/server/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/server/src')
-rw-r--r--packages/server/src/helpers/decodeAttestationObject.ts9
-rw-r--r--packages/server/src/helpers/decodeCredentialPublicKey.ts9
-rw-r--r--packages/server/src/helpers/verifySignature.ts19
-rw-r--r--packages/server/src/registration/verifyRegistrationResponse.test.ts773
4 files changed, 473 insertions, 337 deletions
diff --git a/packages/server/src/helpers/decodeAttestationObject.ts b/packages/server/src/helpers/decodeAttestationObject.ts
index 8cf036d..ebfe42b 100644
--- a/packages/server/src/helpers/decodeAttestationObject.ts
+++ b/packages/server/src/helpers/decodeAttestationObject.ts
@@ -8,7 +8,9 @@ import { isoCBOR } from "./iso/index.ts";
export function decodeAttestationObject(
attestationObject: Uint8Array,
): AttestationObject {
- return isoCBOR.decodeFirst<AttestationObject>(attestationObject);
+ return _decodeAttestationObjectInternals.stubThis(
+ isoCBOR.decodeFirst<AttestationObject>(attestationObject),
+ );
}
export type AttestationFormat =
@@ -41,3 +43,8 @@ export type AttestationStatement = {
// `Map` properties
readonly size: number;
};
+
+// Make it possible to stub the return value during testing
+export const _decodeAttestationObjectInternals = {
+ stubThis: (value: AttestationObject) => value,
+};
diff --git a/packages/server/src/helpers/decodeCredentialPublicKey.ts b/packages/server/src/helpers/decodeCredentialPublicKey.ts
index b6d0f21..bb5dab4 100644
--- a/packages/server/src/helpers/decodeCredentialPublicKey.ts
+++ b/packages/server/src/helpers/decodeCredentialPublicKey.ts
@@ -4,5 +4,12 @@ import { isoCBOR } from "./iso/index.ts";
export function decodeCredentialPublicKey(
publicKey: Uint8Array,
): COSEPublicKey {
- return isoCBOR.decodeFirst<COSEPublicKey>(publicKey);
+ return _decodeCredentialPublicKeyInternals.stubThis(
+ isoCBOR.decodeFirst<COSEPublicKey>(publicKey),
+ );
}
+
+// Make it possible to stub the return value during testing
+export const _decodeCredentialPublicKeyInternals = {
+ stubThis: (value: COSEPublicKey) => value,
+};
diff --git a/packages/server/src/helpers/verifySignature.ts b/packages/server/src/helpers/verifySignature.ts
index 613436b..593c2e4 100644
--- a/packages/server/src/helpers/verifySignature.ts
+++ b/packages/server/src/helpers/verifySignature.ts
@@ -39,10 +39,17 @@ export function verifySignature(opts: {
cosePublicKey = convertX509PublicKeyToCOSE(x509Certificate);
}
- return isoCrypto.verify({
- cosePublicKey,
- signature,
- data,
- shaHashOverride: hashAlgorithm,
- });
+ return _verifySignatureInternals.stubThis(
+ isoCrypto.verify({
+ cosePublicKey,
+ signature,
+ data,
+ shaHashOverride: hashAlgorithm,
+ }),
+ );
}
+
+// Make it possible to stub the return value during testing
+export const _verifySignatureInternals = {
+ stubThis: (value: Promise<boolean>) => value,
+};
diff --git a/packages/server/src/registration/verifyRegistrationResponse.test.ts b/packages/server/src/registration/verifyRegistrationResponse.test.ts
index 5d8ae11..f353e06 100644
--- a/packages/server/src/registration/verifyRegistrationResponse.test.ts
+++ b/packages/server/src/registration/verifyRegistrationResponse.test.ts
@@ -1,17 +1,32 @@
-import { RegistrationResponseJSON } from "@simplewebauthn/typescript-types";
-
+import {
+ assert,
+ assertEquals,
+ assertFalse,
+ assertRejects,
+} from "https://deno.land/std@0.198.0/assert/mod.ts";
+import {
+ returnsNext,
+ stub,
+} from "https://deno.land/std@0.198.0/testing/mock.ts";
+
+import { RegistrationResponseJSON } from "../deps.ts";
import { verifyRegistrationResponse } from "./verifyRegistrationResponse.ts";
-
-import * as esmDecodeAttestationObject from "../helpers/decodeAttestationObject.ts";
-import * as esmDecodeClientDataJSON from "../helpers/decodeClientDataJSON.ts";
-import * as esmParseAuthenticatorData from "../helpers/parseAuthenticatorData.ts";
-import * as esmDecodeCredentialPublicKey from "../helpers/decodeCredentialPublicKey.ts";
+import {
+ _decodeAttestationObjectInternals,
+ decodeAttestationObject,
+} from "../helpers/decodeAttestationObject.ts";
+import { _decodeClientDataJSONInternals } from "../helpers/decodeClientDataJSON.ts";
+import {
+ _parseAuthenticatorDataInternals,
+ parseAuthenticatorData,
+} from "../helpers/parseAuthenticatorData.ts";
+import { _decodeCredentialPublicKeyInternals } from "../helpers/decodeCredentialPublicKey.ts";
+import { _verifySignatureInternals } from "../helpers/verifySignature.ts";
import { toHash } from "../helpers/toHash.ts";
import { isoBase64URL, isoUint8Array } from "../helpers/iso/index.ts";
-import { COSEKEYS, COSEPublicKey } from "../helpers/cose.ts";
+import { COSEKEYS } from "../helpers/cose.ts";
import { SettingsService } from "../services/settingsService.ts";
-
-import * as esmVerifyAttestationFIDOU2F from "./verifications/verifyAttestationFIDOU2F.ts";
+import { assertObjectMatch } from "https://deno.land/std@0.198.0/assert/assert_object_match.ts";
/**
* Clear out root certs for android-key since responses were captured from FIDO Conformance testing
@@ -22,46 +37,7 @@ SettingsService.setRootCertificates({
certificates: [],
});
-let mockDecodeAttestation: jest.SpyInstance<
- esmDecodeAttestationObject.AttestationObject
->;
-let mockDecodeClientData: jest.SpyInstance;
-let mockParseAuthData: jest.SpyInstance;
-let mockDecodePubKey: jest.SpyInstance<COSEPublicKey>;
-let mockVerifyFIDOU2F: jest.SpyInstance;
-
-beforeEach(() => {
- mockDecodeAttestation = jest.spyOn(
- esmDecodeAttestationObject,
- "decodeAttestationObject",
- );
- mockDecodeClientData = jest.spyOn(
- esmDecodeClientDataJSON,
- "decodeClientDataJSON",
- );
- mockParseAuthData = jest.spyOn(
- esmParseAuthenticatorData,
- "parseAuthenticatorData",
- );
- mockDecodePubKey = jest.spyOn(
- esmDecodeCredentialPublicKey,
- "decodeCredentialPublicKey",
- );
- mockVerifyFIDOU2F = jest.spyOn(
- esmVerifyAttestationFIDOU2F,
- "verifyAttestationFIDOU2F",
- );
-});
-
-afterEach(() => {
- mockDecodeAttestation.mockRestore();
- mockDecodeClientData.mockRestore();
- mockParseAuthData.mockRestore();
- mockDecodePubKey.mockRestore();
- mockVerifyFIDOU2F.mockRestore();
-});
-
-test("should verify FIDO U2F attestation", async () => {
+Deno.test("should verify FIDO U2F attestation", async () => {
const verification = await verifyRegistrationResponse({
response: attestationFIDOU2F,
expectedChallenge: attestationFIDOU2FChallenge,
@@ -70,34 +46,33 @@ test("should verify FIDO U2F attestation", async () => {
requireUserVerification: false,
});
- expect(verification.verified).toEqual(true);
- expect(verification.registrationInfo?.fmt).toEqual("fido-u2f");
- expect(verification.registrationInfo?.counter).toEqual(0);
- expect(verification.registrationInfo?.credentialPublicKey).toEqual(
+ assert(verification.verified);
+ assertEquals(verification.registrationInfo?.fmt, "fido-u2f");
+ assertEquals(verification.registrationInfo?.counter, 0);
+ assertEquals(
+ verification.registrationInfo?.credentialPublicKey,
isoBase64URL.toBuffer(
"pQECAyYgASFYIMiRyw5pUoMhBjCrcQND6lJPaRHA0f-XWcKBb5ZwWk1eIlggFJu6aan4o7epl6qa9n9T-6KsIMvZE2PcTnLj8rN58is",
),
);
- expect(verification.registrationInfo?.credentialID).toEqual(
- isoBase64URL.toBuffer(
- "VHzbxaYaJu2P8m1Y2iHn2gRNHrgK0iYbn9E978L3Qi7Q-chFeicIHwYCRophz5lth2nCgEVKcgWirxlgidgbUQ",
- ),
- );
- expect(verification.registrationInfo?.aaguid).toEqual(
+ assertEquals(
+ verification.registrationInfo?.aaguid,
"00000000-0000-0000-0000-000000000000",
);
- expect(verification.registrationInfo?.credentialType).toEqual("public-key");
- expect(verification.registrationInfo?.userVerified).toEqual(false);
- expect(verification.registrationInfo?.attestationObject).toEqual(
+ assertEquals(verification.registrationInfo?.credentialType, "public-key");
+ assertEquals(verification.registrationInfo?.userVerified, false);
+ assertEquals(
+ verification.registrationInfo?.attestationObject,
isoBase64URL.toBuffer(attestationFIDOU2F.response.attestationObject),
);
- expect(verification.registrationInfo?.origin).toEqual(
+ assertEquals(
+ verification.registrationInfo?.origin,
"https://dev.dontneeda.pw",
);
- expect(verification.registrationInfo?.rpID).toEqual("dev.dontneeda.pw");
+ assertEquals(verification.registrationInfo?.rpID, "dev.dontneeda.pw");
});
-test("should verify Packed (EC2) attestation", async () => {
+Deno.test("should verify Packed (EC2) attestation", async () => {
const verification = await verifyRegistrationResponse({
response: attestationPacked,
expectedChallenge: attestationPackedChallenge,
@@ -105,15 +80,17 @@ test("should verify Packed (EC2) attestation", async () => {
expectedRPID: "dev.dontneeda.pw",
});
- expect(verification.verified).toEqual(true);
- expect(verification.registrationInfo?.fmt).toEqual("packed");
- expect(verification.registrationInfo?.counter).toEqual(1589874425);
- expect(verification.registrationInfo?.credentialPublicKey).toEqual(
+ assert(verification.verified);
+ assertEquals(verification.registrationInfo?.fmt, "packed");
+ assertEquals(verification.registrationInfo?.counter, 1589874425);
+ assertEquals(
+ verification.registrationInfo?.credentialPublicKey,
isoBase64URL.toBuffer(
"pQECAyYgASFYIEoxVVqK-oIGmqoDEyO4KjmMx5R2HeMM4LQQXh8sE01PIlggtzuuoMN5fWnAIuuXdlfshOGu1k3ApBUtDJ8eKiuo_6c",
),
);
- expect(verification.registrationInfo?.credentialID).toEqual(
+ assertEquals(
+ verification.registrationInfo?.credentialID,
isoBase64URL.toBuffer(
"AYThY1csINY4JrbHyGmqTl1nL_F1zjAF3hSAIngz8kAcjugmAMNVvxZRwqpEH-bNHHAIv291OX5ko9eDf_5mu3U" +
"B2BvsScr2K-ppM4owOpGsqwg5tZglqqmxIm1Q",
@@ -121,7 +98,7 @@ test("should verify Packed (EC2) attestation", async () => {
);
});
-test("should verify Packed (X5C) attestation", async () => {
+Deno.test("should verify Packed (X5C) attestation", async () => {
const verification = await verifyRegistrationResponse({
response: attestationPackedX5C,
expectedChallenge: attestationPackedX5CChallenge,
@@ -130,22 +107,24 @@ test("should verify Packed (X5C) attestation", async () => {
requireUserVerification: false,
});
- expect(verification.verified).toEqual(true);
- expect(verification.registrationInfo?.fmt).toEqual("packed");
- expect(verification.registrationInfo?.counter).toEqual(28);
- expect(verification.registrationInfo?.credentialPublicKey).toEqual(
+ assert(verification.verified);
+ assertEquals(verification.registrationInfo?.fmt, "packed");
+ assertEquals(verification.registrationInfo?.counter, 28);
+ assertEquals(
+ verification.registrationInfo?.credentialPublicKey,
isoBase64URL.toBuffer(
"pQECAyYgASFYIGwlsYCNyRb4AD9cyTw6cH5VS-uzflmmO1UldGGe9eIaIlggvadzKD8p6wKLjgYfxRxldjCMGRV0YyM13osWbKIPrF8",
),
);
- expect(verification.registrationInfo?.credentialID).toEqual(
+ assertEquals(
+ verification.registrationInfo?.credentialID,
isoBase64URL.toBuffer(
"4rrvMciHCkdLQ2HghazIp1sMc8TmV8W8RgoX-x8tqV_1AmlqWACqUK8mBGLandr-htduQKPzgb2yWxOFV56Tlg",
),
);
});
-test("should verify None attestation", async () => {
+Deno.test("should verify None attestation", async () => {
const verification = await verifyRegistrationResponse({
response: attestationNone,
expectedChallenge: attestationNoneChallenge,
@@ -153,25 +132,28 @@ test("should verify None attestation", async () => {
expectedRPID: "dev.dontneeda.pw",
});
- expect(verification.verified).toEqual(true);
- expect(verification.registrationInfo?.fmt).toEqual("none");
- expect(verification.registrationInfo?.counter).toEqual(0);
- expect(verification.registrationInfo?.credentialPublicKey).toEqual(
+ assert(verification.verified);
+ assertEquals(verification.registrationInfo?.fmt, "none");
+ assertEquals(verification.registrationInfo?.counter, 0);
+ assertEquals(
+ verification.registrationInfo?.credentialPublicKey,
isoBase64URL.toBuffer(
"pQECAyYgASFYID5PQTZQQg6haZFQWFzqfAOyQ_ENsMH8xxQ4GRiNPsqrIlggU8IVUOV8qpgk_Jh-OTaLuZL52KdX1fTht07X4DiQPow",
),
);
- expect(verification.registrationInfo?.credentialID).toEqual(
+ assertEquals(
+ verification.registrationInfo?.credentialID,
isoBase64URL.toBuffer(
"AdKXJEch1aV5Wo7bj7qLHskVY4OoNaj9qu8TPdJ7kSAgUeRxWNngXlcNIGt4gexZGKVGcqZpqqWordXb_he1izY",
),
);
- expect(verification.registrationInfo?.origin).toEqual(
+ assertEquals(
+ verification.registrationInfo?.origin,
"https://dev.dontneeda.pw",
);
});
-test("should verify None attestation w/RSA public key", async () => {
+Deno.test("should verify None attestation w/RSA public key", async () => {
const expectedChallenge = "pYZ3VX2yb8dS9yplNxJChiXhPGBk8gZzTAyJ2iU5x1k";
const verification = await verifyRegistrationResponse({
response: {
@@ -192,200 +174,299 @@ test("should verify None attestation w/RSA public key", async () => {
expectedRPID: "dev.dontneeda.pw",
});
- expect(verification.verified).toEqual(true);
- expect(verification.registrationInfo?.fmt).toEqual("none");
- expect(verification.registrationInfo?.counter).toEqual(0);
- expect(verification.registrationInfo?.credentialPublicKey).toEqual(
+ assert(verification.verified);
+ assertEquals(verification.registrationInfo?.fmt, "none");
+ assertEquals(verification.registrationInfo?.counter, 0);
+ assertEquals(
+ verification.registrationInfo?.credentialPublicKey,
isoBase64URL.toBuffer(
"pAEDAzkBACBZAQDxfpXrj0ba_AH30JJ_-W7BHSOPugOD8aEDdNBKc1gjB9AmV3FPl2aL0fwiOMKtM_byI24qXb2FzcyjC7HUVkHRtzkAQnahXckI4wY_01koaY6iwXuIE3Ya0Zjs2iZyz6u4G_abGnWdObqa_kHxc3CHR7Xy5MDkAkKyX6TqU0tgHZcEhDd_Lb5ONJDwg4wvKlZBtZYElfMuZ6lonoRZ7qR_81rGkDZyFaxp6RlyvzEbo4ijeIaHQylqCz-oFm03ifZMOfRHYuF4uTjJDRH-g4BW1f3rdi7DTHk1hJnIw1IyL_VFIQ9NifkAguYjNCySCUNpYli2eMrPhAu5dYJFFjINIUMBAAE",
),
);
- expect(verification.registrationInfo?.credentialID).toEqual(
+ assertEquals(
+ verification.registrationInfo?.credentialID,
isoBase64URL.toBuffer("kGXv4RJWLeXRw8Yf3T22K3Gq_GGeDv9OKYmAHLm0Ylo"),
);
- expect(verification.registrationInfo?.origin).toEqual(
+ assertEquals(
+ verification.registrationInfo?.origin,
"https://dev.dontneeda.pw",
);
- expect(verification.registrationInfo?.rpID).toEqual("dev.dontneeda.pw");
+ assertEquals(verification.registrationInfo?.rpID, "dev.dontneeda.pw");
});
-test("should throw when response challenge is not expected value", async () => {
- await expect(
- verifyRegistrationResponse({
- response: attestationNone,
- expectedChallenge: "shouldhavebeenthisvalue",
- expectedOrigin: "https://dev.dontneeda.pw",
- expectedRPID: "dev.dontneeda.pw",
- }),
- ).rejects.toThrow(/registration response challenge/i);
+Deno.test("should throw when response challenge is not expected value", async () => {
+ await assertRejects(
+ () =>
+ verifyRegistrationResponse({
+ response: attestationNone,
+ expectedChallenge: "shouldhavebeenthisvalue",
+ expectedOrigin: "https://dev.dontneeda.pw",
+ expectedRPID: "dev.dontneeda.pw",
+ }),
+ Error,
+ "registration response challenge",
+ );
});
-test("should throw when response origin is not expected value", async () => {
- await expect(
- verifyRegistrationResponse({
- response: attestationNone,
- expectedChallenge: attestationNoneChallenge,
- expectedOrigin: "https://different.address",
- expectedRPID: "dev.dontneeda.pw",
- }),
- ).rejects.toThrow(/registration response origin/i);
+Deno.test("should throw when response origin is not expected value", async () => {
+ await assertRejects(
+ () =>
+ verifyRegistrationResponse({
+ response: attestationNone,
+ expectedChallenge: attestationNoneChallenge,
+ expectedOrigin: "https://different.address",
+ expectedRPID: "dev.dontneeda.pw",
+ }),
+ Error,
+ "registration response origin",
+ );
});
-test("should throw when attestation type is not webauthn.create", async () => {
+Deno.test("should throw when attestation type is not webauthn.create", async () => {
const origin = "https://dev.dontneeda.pw";
const challenge = attestationNoneChallenge;
- // @ts-ignore 2345
- mockDecodeClientData.mockReturnValue({
- origin,
- type: "webauthn.badtype",
- challenge: attestationNoneChallenge,
- });
+ const mockDecodeClientData = stub(
+ _decodeClientDataJSONInternals,
+ "stubThis",
+ returnsNext([
+ {
+ origin,
+ type: "webauthn.badtype",
+ challenge: attestationNoneChallenge,
+ },
+ ]),
+ );
+
+ await assertRejects(
+ () =>
+ verifyRegistrationResponse({
+ response: attestationNone,
+ expectedChallenge: challenge,
+ expectedOrigin: origin,
+ expectedRPID: "dev.dontneeda.pw",
+ }),
+ Error,
+ "registration response type",
+ );
- await expect(
- verifyRegistrationResponse({
- response: attestationNone,
- expectedChallenge: challenge,
- expectedOrigin: origin,
- expectedRPID: "dev.dontneeda.pw",
- }),
- ).rejects.toThrow(/registration response type/i);
+ mockDecodeClientData.restore();
});
-test("should throw if an unexpected attestation format is specified", async () => {
- const realAtteObj = esmDecodeAttestationObject.decodeAttestationObject(
+Deno.test("should throw if an unexpected attestation format is specified", async () => {
+ const realAtteObj = decodeAttestationObject(
isoBase64URL.toBuffer(attestationNone.response.attestationObject),
);
// Mangle the fmt
(realAtteObj as Map<unknown, unknown>).set("fmt", "fizzbuzz");
- mockDecodeAttestation.mockReturnValue(realAtteObj);
+ const mockDecodeAttestation = stub(
+ _decodeAttestationObjectInternals,
+ "stubThis",
+ returnsNext([realAtteObj]),
+ );
+
+ await assertRejects(
+ () =>
+ verifyRegistrationResponse({
+ response: attestationNone,
+ expectedChallenge: attestationNoneChallenge,
+ expectedOrigin: "https://dev.dontneeda.pw",
+ expectedRPID: "dev.dontneeda.pw",
+ }),
+ Error,
+ "Unsupported Attestation Format",
+ );
- await expect(
- verifyRegistrationResponse({
- response: attestationNone,
- expectedChallenge: attestationNoneChallenge,
- expectedOrigin: "https://dev.dontneeda.pw",
- expectedRPID: "dev.dontneeda.pw",
- }),
- ).rejects.toThrow(/unsupported attestation format/i);
+ mockDecodeAttestation.restore();
});
-test("should throw error if assertion RP ID is unexpected value", async () => {
- const authData = esmDecodeAttestationObject
- .decodeAttestationObject(
- isoBase64URL.toBuffer(attestationNone.response.attestationObject),
- )
- .get("authData");
- const actualAuthData = esmParseAuthenticatorData.parseAuthenticatorData(
- authData,
+Deno.test("should throw error if assertion RP ID is unexpected value", async () => {
+ const authData = decodeAttestationObject(
+ isoBase64URL.toBuffer(attestationNone.response.attestationObject),
+ ).get("authData");
+ const actualAuthData = parseAuthenticatorData(authData);
+
+ const mockParseAuthData = stub(
+ _parseAuthenticatorDataInternals,
+ "stubThis",
+ returnsNext([
+ {
+ ...actualAuthData,
+ rpIdHash: await toHash(isoUint8Array.fromASCIIString("bad.url")),
+ },
+ ]),
);
- mockParseAuthData.mockReturnValue({
- ...actualAuthData,
- rpIdHash: await toHash(Buffer.from("bad.url", "ascii")),
- });
+ await assertRejects(
+ () =>
+ verifyRegistrationResponse({
+ response: attestationNone,
+ expectedChallenge: attestationNoneChallenge,
+ expectedOrigin: "https://dev.dontneeda.pw",
+ expectedRPID: "dev.dontneeda.pw",
+ }),
+ Error,
+ "RP ID",
+ );
- await expect(
- verifyRegistrationResponse({
- response: attestationNone,
- expectedChallenge: attestationNoneChallenge,
- expectedOrigin: "https://dev.dontneeda.pw",
- expectedRPID: "dev.dontneeda.pw",
- }),
- ).rejects.toThrow(/rp id/i);
-});
-
-test("should throw error if user was not present", async () => {
- mockParseAuthData.mockReturnValue({
- rpIdHash: await toHash(Buffer.from("dev.dontneeda.pw", "ascii")),
- flags: {
- up: false,
- },
- });
+ mockParseAuthData.restore();
+});
- await expect(
- verifyRegistrationResponse({
- response: attestationNone,
- expectedChallenge: attestationNoneChallenge,
- expectedOrigin: "https://dev.dontneeda.pw",
- expectedRPID: "dev.dontneeda.pw",
- }),
- ).rejects.toThrow(/not present/i);
-});
-
-test("should throw if the authenticator does not give back credential ID", async () => {
- mockParseAuthData.mockReturnValue({
- rpIdHash: await toHash(Buffer.from("dev.dontneeda.pw", "ascii")),
- flags: {
- up: true,
- },
- credentialID: undefined,
- });
+Deno.test("should throw error if user was not present", async () => {
+ const mockParseAuthData = stub(
+ _parseAuthenticatorDataInternals,
+ "stubThis",
+ // @ts-ignore: Only return the values that matter
+ returnsNext([
+ {
+ rpIdHash: await toHash(
+ isoUint8Array.fromASCIIString("dev.dontneeda.pw"),
+ ),
+ flags: {
+ up: false,
+ },
+ },
+ ]),
+ );
- await expect(
- verifyRegistrationResponse({
- response: attestationNone,
- expectedChallenge: attestationNoneChallenge,
- expectedOrigin: "https://dev.dontneeda.pw",
- expectedRPID: "dev.dontneeda.pw",
- requireUserVerification: false,
- }),
- ).rejects.toThrow(/credential id/i);
-});
-
-test("should throw if the authenticator does not give back credential public key", async () => {
- mockParseAuthData.mockReturnValue({
- rpIdHash: await toHash(Buffer.from("dev.dontneeda.pw", "ascii")),
- flags: {
- up: true,
- },
- credentialID: "aaa",
- credentialPublicKey: undefined,
- });
+ await assertRejects(
+ () =>
+ verifyRegistrationResponse({
+ response: attestationNone,
+ expectedChallenge: attestationNoneChallenge,
+ expectedOrigin: "https://dev.dontneeda.pw",
+ expectedRPID: "dev.dontneeda.pw",
+ }),
+ Error,
+ "not present",
+ );
+
+ mockParseAuthData.restore();
+});
+
+Deno.test("should throw if the authenticator does not give back credential ID", async () => {
+ const mockParseAuthData = stub(
+ _parseAuthenticatorDataInternals,
+ "stubThis",
+ // @ts-ignore: Only return the values that matter
+ returnsNext([
+ {
+ rpIdHash: await toHash(
+ isoUint8Array.fromASCIIString("dev.dontneeda.pw"),
+ ),
+ flags: {
+ up: true,
+ },
+ credentialID: undefined,
+ },
+ ]),
+ );
+
+ await assertRejects(
+ () =>
+ verifyRegistrationResponse({
+ response: attestationNone,
+ expectedChallenge: attestationNoneChallenge,
+ expectedOrigin: "https://dev.dontneeda.pw",
+ expectedRPID: "dev.dontneeda.pw",
+ requireUserVerification: false,
+ }),
+ Error,
+ "credential ID",
+ );
+
+ mockParseAuthData.restore();
+});
+
+Deno.test("should throw if the authenticator does not give back credential public key", async () => {
+ const mockParseAuthData = stub(
+ _parseAuthenticatorDataInternals,
+ "stubThis",
+ // @ts-ignore: Only return the values that matter
+ returnsNext([
+ {
+ rpIdHash: await toHash(
+ isoUint8Array.fromASCIIString("dev.dontneeda.pw"),
+ ),
+ flags: {
+ up: true,
+ },
+ credentialID: "aaa",
+ credentialPublicKey: undefined,
+ },
+ ]),
+ );
- await expect(
- verifyRegistrationResponse({
- response: attestationNone,
- expectedChallenge: attestationNoneChallenge,
- expectedOrigin: "https://dev.dontneeda.pw",
- expectedRPID: "dev.dontneeda.pw",
- requireUserVerification: false,
- }),
- ).rejects.toThrow(/public key/i);
+ await assertRejects(
+ () =>
+ verifyRegistrationResponse({
+ response: attestationNone,
+ expectedChallenge: attestationNoneChallenge,
+ expectedOrigin: "https://dev.dontneeda.pw",
+ expectedRPID: "dev.dontneeda.pw",
+ requireUserVerification: false,
+ }),
+ Error,
+ "public key",
+ );
+
+ mockParseAuthData.restore();
});
-test("should throw error if no alg is specified in public key", async () => {
+Deno.test("should throw error if no alg is specified in public key", async () => {
const pubKey = new Map();
- mockDecodePubKey.mockReturnValue(pubKey);
+ const mockDecodePubKey = stub(
+ _decodeCredentialPublicKeyInternals,
+ "stubThis",
+ returnsNext([pubKey]),
+ );
- await expect(
- verifyRegistrationResponse({
- response: attestationNone,
- expectedChallenge: attestationNoneChallenge,
- expectedOrigin: "https://dev.dontneeda.pw",
- expectedRPID: "dev.dontneeda.pw",
- }),
- ).rejects.toThrow(/missing numeric alg/i);
+ await assertRejects(
+ () =>
+ verifyRegistrationResponse({
+ response: attestationNone,
+ expectedChallenge: attestationNoneChallenge,
+ expectedOrigin: "https://dev.dontneeda.pw",
+ expectedRPID: "dev.dontneeda.pw",
+ }),
+ Error,
+ "missing numeric alg",
+ );
+
+ mockDecodePubKey.restore();
});
-test("should throw error if unsupported alg is used", async () => {
+Deno.test("should throw error if unsupported alg is used", async () => {
const pubKey = new Map();
pubKey.set(COSEKEYS.alg, -999);
- mockDecodePubKey.mockReturnValue(pubKey);
+ const mockDecodePubKey = stub(
+ _decodeCredentialPublicKeyInternals,
+ "stubThis",
+ returnsNext([pubKey]),
+ );
- await expect(
- verifyRegistrationResponse({
- response: attestationNone,
- expectedChallenge: attestationNoneChallenge,
- expectedOrigin: "https://dev.dontneeda.pw",
- expectedRPID: "dev.dontneeda.pw",
- }),
- ).rejects.toThrow(/unexpected public key/i);
+ await assertRejects(
+ () =>
+ verifyRegistrationResponse({
+ response: attestationNone,
+ expectedChallenge: attestationNoneChallenge,
+ expectedOrigin: "https://dev.dontneeda.pw",
+ expectedRPID: "dev.dontneeda.pw",
+ }),
+ Error,
+ "Unexpected public key",
+ );
+
+ mockDecodePubKey.restore();
});
-test("should not include authenticator info if not verified", async () => {
- mockVerifyFIDOU2F.mockReturnValue(false);
+Deno.test("should not include authenticator info if not verified", async () => {
+ const mockVerifySignature = stub(
+ _verifySignatureInternals,
+ "stubThis",
+ returnsNext([new Promise((resolve) => resolve(false))]),
+ );
const verification = await verifyRegistrationResponse({
response: attestationFIDOU2F,
@@ -395,31 +476,43 @@ test("should not include authenticator info if not verified", async () => {
requireUserVerification: false,
});
- expect(verification.verified).toBe(false);
- expect(verification.registrationInfo).toBeUndefined();
+ assertFalse(verification.verified);
+ assertEquals(verification.registrationInfo, undefined);
+
+ mockVerifySignature.restore();
});
-test("should throw an error if user verification is required but user was not verified", async () => {
- mockParseAuthData.mockReturnValue({
- rpIdHash: await toHash(Buffer.from("dev.dontneeda.pw", "ascii")),
- flags: {
- up: true,
- uv: false,
- },
- });
+Deno.test("should throw an error if user verification is required but user was not verified", async () => {
+ const mockParseAuthData = stub(
+ _parseAuthenticatorDataInternals,
+ "stubThis",
+ // @ts-ignore: Only return the values that matter
+ returnsNext([{
+ rpIdHash: await toHash(isoUint8Array.fromASCIIString("dev.dontneeda.pw")),
+ flags: {
+ up: true,
+ uv: false,
+ },
+ }]),
+ );
+
+ await assertRejects(
+ () =>
+ verifyRegistrationResponse({
+ response: attestationFIDOU2F,
+ expectedChallenge: attestationFIDOU2FChallenge,
+ expectedOrigin: "https://dev.dontneeda.pw",
+ expectedRPID: "dev.dontneeda.pw",
+ requireUserVerification: true,
+ }),
+ Error,
+ "user could not be verified",
+ );
- await expect(
- verifyRegistrationResponse({
- response: attestationFIDOU2F,
- expectedChallenge: attestationFIDOU2FChallenge,
- expectedOrigin: "https://dev.dontneeda.pw",
- expectedRPID: "dev.dontneeda.pw",
- requireUserVerification: true,
- }),
- ).rejects.toThrow(/user could not be verified/i);
+ mockParseAuthData.restore();
});
-test("should validate TPM RSA response (SHA256)", async () => {
+Deno.test("should validate TPM RSA response (SHA256)", async () => {
const expectedChallenge = "3a07cf85-e7b6-447f-8270-b25433f6018e";
const verification = await verifyRegistrationResponse({
response: {
@@ -441,24 +534,27 @@ test("should validate TPM RSA response (SHA256)", async () => {
requireUserVerification: false,
});
- expect(verification.verified).toEqual(true);
- expect(verification.registrationInfo?.fmt).toEqual("tpm");
- expect(verification.registrationInfo?.counter).toEqual(30);
- expect(verification.registrationInfo?.credentialPublicKey).toEqual(
+ assert(verification.verified);
+ assertEquals(verification.registrationInfo?.fmt, "tpm");
+ assertEquals(verification.registrationInfo?.counter, 30);
+ assertEquals(
+ verification.registrationInfo?.credentialPublicKey,
isoBase64URL.toBuffer(
"pAEDAzkBACBZAQCtxzw59Wsl8xWP97wPTu2TSDlushwshL8GedHAHO1R62m3nNy21hCLJlQabfLepRUQ_v9mq3PCmV81tBSqtRGU5_YlK0R2yeu756SnT39c6hKC3PBPt_xdjL_ccz4H_73DunfB63QZOtdeAsswV7WPLqMARofuM-LQ_LHnNguCypDcxhADuUqQtogfwZsknTVIPxzGcfqnQ7ERF9D9AOWIQ8YjOsTi_B2zS8SOySKIFUGwwYcPG7DiCE-QJcI-fpydRDnEq6UxbkYgB7XK4BlmPKlwuXkBDX9egl_Ma4B7W2WJvYbKevu6Z8Kc5y-OITpNVDYKbBK3qKyh4yIUpB1NIUMBAAE",
),
);
- expect(verification.registrationInfo?.credentialID).toEqual(
+ assertEquals(
+ verification.registrationInfo?.credentialID,
isoBase64URL.toBuffer("lGkWHPe88VpnNYgVBxzon_MRR9-gmgODveQ16uM_bPM"),
);
- expect(verification.registrationInfo?.origin).toEqual(
+ assertEquals(
+ verification.registrationInfo?.origin,
"https://dev.dontneeda.pw",
);
- expect(verification.registrationInfo?.rpID).toEqual("dev.dontneeda.pw");
+ assertEquals(verification.registrationInfo?.rpID, "dev.dontneeda.pw");
});
-test("should validate TPM RSA response (SHA1)", async () => {
+Deno.test("should validate TPM RSA response (SHA1)", async () => {
const expectedChallenge = "f4e8d87b-d363-47cc-ab4d-1a84647bf245";
const verification = await verifyRegistrationResponse({
response: {
@@ -480,24 +576,27 @@ test("should validate TPM RSA response (SHA1)", async () => {
requireUserVerification: false,
});
- expect(verification.verified).toEqual(true);
- expect(verification.registrationInfo?.fmt).toEqual("tpm");
- expect(verification.registrationInfo?.counter).toEqual(97);
- expect(verification.registrationInfo?.credentialPublicKey).toEqual(
+ assert(verification.verified);
+ assertEquals(verification.registrationInfo?.fmt, "tpm");
+ assertEquals(verification.registrationInfo?.counter, 97);
+ assertEquals(
+ verification.registrationInfo?.credentialPublicKey,
isoBase64URL.toBuffer(
"pAEDAzn__iBZAQCzl_wD24PZ5z-po2FrwoQVdd13got_CkL8p4B_NvJBC5OwAYKDilii_wj-0CA8ManbpSInx9Tdnz6t91OhudwUT0-W_BHSLK_MqFcjZWrR5LYVmVpz1EgH3DrOTra4AlogEq2D2CYktPrPe7joE-oT3vAYXK8vzQDLRyaxI_Z1qS4KLlLCdajW8PGpw1YRjMDw6s69GZU8mXkgNPMCUh1TZ1bnCvJTO9fnmLjDjqdQGRU4bWo8tFjCL8g1-2WD_2n0-twt6n-Uox5VnR1dQJG4awMlanBCkGGpOb3WBDQ8K10YJJ2evPhJKGJahBvu2Dxmq6pLCAXCv0ma3EHj-PmDIUMBAAE",
),
);
- expect(verification.registrationInfo?.credentialID).toEqual(
+ assertEquals(
+ verification.registrationInfo?.credentialID,
isoBase64URL.toBuffer("oELnad0f6-g2BtzEn_78iLNoubarlq0xFtOtAMXnflU"),
);
- expect(verification.registrationInfo?.origin).toEqual(
+ assertEquals(
+ verification.registrationInfo?.origin,
"https://dev.dontneeda.pw",
);
- expect(verification.registrationInfo?.rpID).toEqual("dev.dontneeda.pw");
+ assertEquals(verification.registrationInfo?.rpID, "dev.dontneeda.pw");
});
-test("should validate Android-Key response", async () => {
+Deno.test("should validate Android-Key response", async () => {
const expectedChallenge = "14e0d1b6-9c36-4849-aeec-ea64676449ef";
const verification = await verifyRegistrationResponse({
response: {
@@ -519,24 +618,27 @@ test("should validate Android-Key response", async () => {
requireUserVerification: false,
});
- expect(verification.verified).toEqual(true);
- expect(verification.registrationInfo?.fmt).toEqual("android-key");
- expect(verification.registrationInfo?.counter).toEqual(108);
- expect(verification.registrationInfo?.credentialPublicKey).toEqual(
+ assert(verification.verified);
+ assertEquals(verification.registrationInfo?.fmt, "android-key");
+ assertEquals(verification.registrationInfo?.counter, 108);
+ assertEquals(
+ verification.registrationInfo?.credentialPublicKey,
isoBase64URL.toBuffer(
"pQECAyYgASFYIEjCq7woGNN_42rbaqMgJvz0nuKTWNRrR29lMX3J239oIlgg6IcAXqPJPIjSrClHDAmbJv_EShYhYq0R9-G3k744n7Y",
),
);
- expect(verification.registrationInfo?.credentialID).toEqual(
+ assertEquals(
+ verification.registrationInfo?.credentialID,
isoBase64URL.toBuffer("PPa1spYTB680cQq5q6qBtFuPLLdG1FQ73EastkT8n0o"),
);
- expect(verification.registrationInfo?.origin).toEqual(
+ assertEquals(
+ verification.registrationInfo?.origin,
"https://dev.dontneeda.pw",
);
- expect(verification.registrationInfo?.rpID).toEqual("dev.dontneeda.pw");
+ assertEquals(verification.registrationInfo?.rpID, "dev.dontneeda.pw");
});
-test("should support multiple possible origins", async () => {
+Deno.test("should support multiple possible origins", async () => {
const verification = await verifyRegistrationResponse({
response: attestationNone,
expectedChallenge: attestationNoneChallenge,
@@ -544,14 +646,15 @@ test("should support multiple possible origins", async () => {
expectedRPID: "dev.dontneeda.pw",
});
- expect(verification.verified).toBe(true);
- expect(verification.registrationInfo?.origin).toEqual(
+ assert(verification.verified);
+ assertEquals(
+ verification.registrationInfo?.origin,
"https://dev.dontneeda.pw",
);
- expect(verification.registrationInfo?.rpID).toEqual("dev.dontneeda.pw");
+ assertEquals(verification.registrationInfo?.rpID, "dev.dontneeda.pw");
});
-test("should not set RPID in registrationInfo when not expected", async () => {
+Deno.test("should not set RPID in registrationInfo when not expected", async () => {
const verification = await verifyRegistrationResponse({
response: attestationNone,
expectedChallenge: attestationNoneChallenge,
@@ -559,22 +662,25 @@ test("should not set RPID in registrationInfo when not expected", async () => {
expectedRPID: undefined,
});
- expect(verification.verified).toBe(true);
- expect(verification.registrationInfo?.rpID).toBeUndefined();
+ assert(verification.verified);
+ assertEquals(verification.registrationInfo?.rpID, undefined);
});
-test("should throw an error if origin not in list of expected origins", async () => {
- await expect(
- verifyRegistrationResponse({
- response: attestationNone,
- expectedChallenge: attestationNoneChallenge,
- expectedOrigin: ["https://different.address"],
- expectedRPID: "dev.dontneeda.pw",
- }),
- ).rejects.toThrow(/unexpected registration response origin/i);
+Deno.test("should throw an error if origin not in list of expected origins", async () => {
+ await assertRejects(
+ () =>
+ verifyRegistrationResponse({
+ response: attestationNone,
+ expectedChallenge: attestationNoneChallenge,
+ expectedOrigin: ["https://different.address"],
+ expectedRPID: "dev.dontneeda.pw",
+ }),
+ Error,
+ "Unexpected registration response origin",
+ );
});
-test("should support multiple possible RP IDs", async () => {
+Deno.test("should support multiple possible RP IDs", async () => {
const verification = await verifyRegistrationResponse({
response: attestationNone,
expectedChallenge: attestationNoneChallenge,
@@ -582,21 +688,24 @@ test("should support multiple possible RP IDs", async () => {
expectedRPID: ["dev.dontneeda.pw", "simplewebauthn.dev"],
});
- expect(verification.verified).toBe(true);
+ assert(verification.verified);
});
-test("should throw an error if RP ID not in list of possible RP IDs", async () => {
- await expect(
- verifyRegistrationResponse({
- response: attestationNone,
- expectedChallenge: attestationNoneChallenge,
- expectedOrigin: "https://dev.dontneeda.pw",
- expectedRPID: ["simplewebauthn.dev"],
- }),
- ).rejects.toThrow(/unexpected rp id/i);
+Deno.test("should throw an error if RP ID not in list of possible RP IDs", async () => {
+ await assertRejects(
+ () =>
+ verifyRegistrationResponse({
+ response: attestationNone,
+ expectedChallenge: attestationNoneChallenge,
+ expectedOrigin: "https://dev.dontneeda.pw",
+ expectedRPID: ["simplewebauthn.dev"],
+ }),
+ Error,
+ "Unexpected RP ID",
+ );
});
-test("should pass verification if custom challenge verifier returns true", async () => {
+Deno.test("should pass verification if custom challenge verifier returns true", async () => {
const verification = await verifyRegistrationResponse({
response: {
id:
@@ -627,22 +736,25 @@ test("should pass verification if custom challenge verifier returns true", async
expectedRPID: "localhost",
});
- expect(verification.verified).toBe(true);
+ assert(verification.verified);
});
-test("should fail verification if custom challenge verifier returns false", async () => {
- await expect(
- verifyRegistrationResponse({
- response: attestationNone,
- expectedChallenge: (challenge: string) =>
- challenge === "thisWillneverMatch",
- expectedOrigin: "https://dev.dontneeda.pw",
- expectedRPID: "dev.dontneeda.pw",
- }),
- ).rejects.toThrow(/custom challenge verifier returned false/i);
+Deno.test("should fail verification if custom challenge verifier returns false", async () => {
+ await assertRejects(
+ () =>
+ verifyRegistrationResponse({
+ response: attestationNone,
+ expectedChallenge: (challenge: string) =>
+ challenge === "thisWillneverMatch",
+ expectedOrigin: "https://dev.dontneeda.pw",
+ expectedRPID: "dev.dontneeda.pw",
+ }),
+ Error,
+ "Custom challenge verifier returned false",
+ );
});
-test("should return credential backup info", async () => {
+Deno.test("should return credential backup info", async () => {
const verification = await verifyRegistrationResponse({
response: attestationNone,
expectedChallenge: attestationNoneChallenge,
@@ -650,13 +762,14 @@ test("should return credential backup info", async () => {
expectedRPID: "dev.dontneeda.pw",
});
- expect(verification.registrationInfo?.credentialDeviceType).toEqual(
+ assertEquals(
+ verification.registrationInfo?.credentialDeviceType,
"singleDevice",
);
- expect(verification.registrationInfo?.credentialBackedUp).toEqual(false);
+ assertEquals(verification.registrationInfo?.credentialBackedUp, false);
});
-test("should return authenticator extension output", async () => {
+Deno.test("should return authenticator extension output", async () => {
const verification = await verifyRegistrationResponse({
response: {
id: "E_Pko4wN1BXE23S0ftN3eQ",
@@ -680,8 +793,9 @@ test("should return authenticator extension output", async () => {
expectedRPID: "try-webauthn.appspot.com",
});
- expect(verification.registrationInfo?.authenticatorExtensionResults)
- .toMatchObject({
+ assertObjectMatch(
+ verification.registrationInfo!.authenticatorExtensionResults!,
+ {
devicePubKey: {
dpk: isoUint8Array.fromHex(
"A5010203262001215820991AABED9DE4271A9EDEAD8806F9DC96D6DCCD0C476253A5510489EC8379BE5B225820A0973CFDEDBB79E27FEF4EE7481673FB3312504DDCA5434CFD23431D6AD29EDA",
@@ -693,10 +807,11 @@ test("should return authenticator extension output", async () => {
scope: isoUint8Array.fromHex("00"),
aaguid: isoUint8Array.fromHex("00000000000000000000000000000000"),
},
- });
+ },
+ );
});
-test("should verify FIDO U2F attestation that specifies SHA-1 in its leaf cert public key", async () => {
+Deno.test("should verify FIDO U2F attestation that specifies SHA-1 in its leaf cert public key", async () => {
const verification = await verifyRegistrationResponse({
response: {
id: "7wQcUWO9gG6mi2IktoZUogs8opnghY01DPYwaerMZms",
@@ -717,10 +832,10 @@ test("should verify FIDO U2F attestation that specifies SHA-1 in its leaf cert p
requireUserVerification: false,
});
- expect(verification.verified).toBe(true);
+ assert(verification.verified);
});
-test("should verify Packed attestation with RSA-PSS SHA-256 public key", async () => {
+Deno.test("should verify Packed attestation with RSA-PSS SHA-256 public key", async () => {
const verification = await verifyRegistrationResponse({
response: {
id: "n_dmFmW9UL7678vS4A3XSQLXvxWjefEkYVzEB5cNc_Q",
@@ -741,10 +856,10 @@ test("should verify Packed attestation with RSA-PSS SHA-256 public key", async (
requireUserVerification: false,
});
- expect(verification.verified).toBe(true);
+ assert(verification.verified);
});
-test("should verify Packed attestation with RSA-PSS SHA-384 public key", async () => {
+Deno.test("should verify Packed attestation with RSA-PSS SHA-384 public key", async () => {
const verification = await verifyRegistrationResponse({
response: {
id: "BCwirFmTkTdTUjVqn_uSy-UOSK-iMBgzpfFunE-Hnb0",
@@ -765,7 +880,7 @@ test("should verify Packed attestation with RSA-PSS SHA-384 public key", async (
requireUserVerification: false,
});
- expect(verification.verified).toBe(true);
+ assert(verification.verified);
});
/**