feat: release authentication laboratories 0.3.0
This commit is contained in:
@@ -0,0 +1,40 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
authenticationExtensions,
|
||||
extensionOutputToJson,
|
||||
registrationExtensions,
|
||||
} from "../../src/webauthn/extensions";
|
||||
|
||||
describe("WebAuthn extension inputs", () => {
|
||||
it("builds registration inputs without silently changing binary values", () => {
|
||||
const result = registrationExtensions({
|
||||
credProps: true,
|
||||
appidExclude: "https://legacy.example.test/app-id.json",
|
||||
prf: { enabled: true, first: "base64url:AQID", second: "label" },
|
||||
largeBlob: { registrationSupport: "required" },
|
||||
}) as AuthenticationExtensionsClientInputs & { appidExclude: string };
|
||||
expect(result.credProps).toBe(true);
|
||||
expect(result.appidExclude).toBe("https://legacy.example.test/app-id.json");
|
||||
expect(new Uint8Array(result.prf!.eval!.first as ArrayBuffer)).toEqual(
|
||||
Uint8Array.of(1, 2, 3),
|
||||
);
|
||||
expect(result.largeBlob).toEqual({ support: "required" });
|
||||
});
|
||||
|
||||
it("rejects invalid AppID and conflicting largeBlob operations", () => {
|
||||
expect(() =>
|
||||
registrationExtensions({ appidExclude: "http://unsafe" }),
|
||||
).toThrow(/HTTPS/u);
|
||||
expect(() =>
|
||||
authenticationExtensions({
|
||||
largeBlob: { read: true, write: "payload" },
|
||||
}),
|
||||
).toThrow(/cannot be requested together/u);
|
||||
});
|
||||
|
||||
it("serializes extension buffers for inspection", () => {
|
||||
expect(extensionOutputToJson({ result: Uint8Array.of(1, 2, 3) })).toEqual({
|
||||
result: { base64url: "AQID", bytes: 3 },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { liveLabAvailability } from "../../src/webauthn/live";
|
||||
|
||||
describe("live WebAuthn origin isolation", () => {
|
||||
afterEach(() => vi.unstubAllGlobals());
|
||||
|
||||
it("rejects the shared Portal origin", () => {
|
||||
vi.stubGlobal("isSecureContext", true);
|
||||
expect(
|
||||
liveLabAvailability({
|
||||
hostname: "toolbox.add-ideas.de",
|
||||
origin: "https://toolbox.add-ideas.de",
|
||||
}),
|
||||
).toMatchObject({ available: false, rpId: "toolbox.add-ideas.de" });
|
||||
});
|
||||
|
||||
it("accepts only the dedicated production host and local development", () => {
|
||||
vi.stubGlobal("isSecureContext", true);
|
||||
expect(
|
||||
liveLabAvailability({
|
||||
hostname: "auth.toolbox.add-ideas.de",
|
||||
origin: "https://auth.toolbox.add-ideas.de",
|
||||
}).available,
|
||||
).toBe(true);
|
||||
expect(
|
||||
liveLabAvailability({
|
||||
hostname: "127.0.0.1",
|
||||
origin: "http://127.0.0.1:4173",
|
||||
}).available,
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
it("requires a secure context on every host", () => {
|
||||
vi.stubGlobal("isSecureContext", false);
|
||||
expect(
|
||||
liveLabAvailability({
|
||||
hostname: "auth.toolbox.add-ideas.de",
|
||||
origin: "http://auth.toolbox.add-ideas.de",
|
||||
}),
|
||||
).toMatchObject({
|
||||
available: false,
|
||||
reason: "WebAuthn requires a secure context.",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,73 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
compareTraces,
|
||||
parseTrace,
|
||||
redactTrace,
|
||||
type AuthenticationTrace,
|
||||
} from "../../src/webauthn/trace";
|
||||
|
||||
function trace(): AuthenticationTrace {
|
||||
return {
|
||||
schema: "de.add-ideas.auth-tools.webauthn-trace",
|
||||
version: 1,
|
||||
kind: "authentication",
|
||||
recordedAt: "2026-08-19T12:00:00.000Z",
|
||||
request: {
|
||||
user: { id: "user-id", name: "Alice", displayName: "Alice Example" },
|
||||
extensions: { prf: true },
|
||||
},
|
||||
expectations: {
|
||||
challenge: "AQID",
|
||||
origin: "https://auth.example.test",
|
||||
rpId: "auth.example.test",
|
||||
},
|
||||
credentialPublicKey: { "1": 2, "3": -7, "-1": 1 },
|
||||
response: {
|
||||
id: "credential-id",
|
||||
rawId: "credential-id",
|
||||
clientDataJSON: "e30",
|
||||
authenticatorData: "AQID",
|
||||
signature: "BAUG",
|
||||
userHandle: "user-handle",
|
||||
clientExtensionResults: { prf: { enabled: true } },
|
||||
},
|
||||
privacy: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe("WebAuthn ceremony traces", () => {
|
||||
it("parses the bounded versioned schema", () => {
|
||||
expect(parseTrace(JSON.stringify(trace()))).toMatchObject({
|
||||
kind: "authentication",
|
||||
version: 1,
|
||||
});
|
||||
expect(() => parseTrace('{"version":2}')).toThrow(/schema/u);
|
||||
let nested: Record<string, unknown> = {};
|
||||
for (let depth = 0; depth < 70; depth += 1) nested = { nested };
|
||||
expect(() =>
|
||||
parseTrace(JSON.stringify({ ...trace(), request: nested })),
|
||||
).toThrow(/depth limit/u);
|
||||
});
|
||||
|
||||
it("redacts labels and top-level identifiers while preserving evidence", async () => {
|
||||
const redacted = await redactTrace(trace());
|
||||
expect(redacted.response.id).toMatch(/^sha256:/u);
|
||||
expect(redacted.response.clientDataJSON).toBe("e30");
|
||||
expect(
|
||||
redacted.kind === "authentication" && redacted.response.userHandle,
|
||||
).toBe("[redacted]");
|
||||
expect((redacted.request.user as Record<string, string>).displayName).toBe(
|
||||
"[redacted]",
|
||||
);
|
||||
});
|
||||
|
||||
it("compares nested trace state", () => {
|
||||
const right = trace();
|
||||
right.expectations.rpId = "other.example.test";
|
||||
right.response.clientExtensionResults = { prf: { enabled: false } };
|
||||
expect(compareTraces(trace(), right).map(({ path }) => path)).toEqual([
|
||||
"expectations.rpId",
|
||||
"response.clientExtensionResults.prf.enabled",
|
||||
]);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import type { FidoMetadataSnapshot } from "../../src/webauthn/metadata";
|
||||
import {
|
||||
DEFAULT_ATTESTATION_TRUST_POLICY,
|
||||
evaluateMetadataPolicy,
|
||||
} from "../../src/webauthn/trust-policy";
|
||||
|
||||
const AAGUID = "00000000-0000-0000-0000-000000000001";
|
||||
|
||||
function snapshot(): FidoMetadataSnapshot {
|
||||
return {
|
||||
sequenceNumber: 42,
|
||||
nextUpdate: "2027-01-01",
|
||||
entries: [
|
||||
{
|
||||
aaguid: AAGUID,
|
||||
statusReports: [
|
||||
{ status: "FIDO_CERTIFIED", effectiveDate: "2025-01-01" },
|
||||
{
|
||||
status: "USER_VERIFICATION_BYPASS",
|
||||
effectiveDate: "2026-04-01",
|
||||
authenticatorVersion: 7,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
jwtAlgorithm: "RS256",
|
||||
signerCertificates: 1,
|
||||
signerCertificateChain: ["AA"],
|
||||
signatureVerified: true,
|
||||
trustEstablished: false,
|
||||
warnings: [],
|
||||
};
|
||||
}
|
||||
|
||||
describe("attestation metadata policy", () => {
|
||||
it("evaluates historical effective dates", () => {
|
||||
const result = evaluateMetadataPolicy({
|
||||
snapshot: snapshot(),
|
||||
aaguid: AAGUID,
|
||||
asOf: "2026-03-01",
|
||||
trustEstablished: true,
|
||||
});
|
||||
expect(result.accepted).toBe(true);
|
||||
expect(result.activeStatusReports.map(({ status }) => status)).toEqual([
|
||||
"FIDO_CERTIFIED",
|
||||
]);
|
||||
});
|
||||
|
||||
it("blocks severe statuses for the selected firmware", () => {
|
||||
const result = evaluateMetadataPolicy({
|
||||
snapshot: snapshot(),
|
||||
aaguid: AAGUID,
|
||||
asOf: "2026-08-19",
|
||||
authenticatorVersion: 7,
|
||||
trustEstablished: true,
|
||||
});
|
||||
expect(result.accepted).toBe(false);
|
||||
expect(result.checks).toContainEqual(
|
||||
expect.objectContaining({
|
||||
name: "Metadata status: USER_VERIFICATION_BYPASS",
|
||||
status: "fail",
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("allows transparent policy overrides", () => {
|
||||
const result = evaluateMetadataPolicy({
|
||||
snapshot: { ...snapshot(), signatureVerified: false },
|
||||
aaguid: "ffffffff-ffff-ffff-ffff-ffffffffffff",
|
||||
asOf: "2028-01-01",
|
||||
policy: {
|
||||
...DEFAULT_ATTESTATION_TRUST_POLICY,
|
||||
requireValidBlobSignature: false,
|
||||
requirePinnedTrustRoot: false,
|
||||
requireCurrentSnapshot: false,
|
||||
requireMetadataEntry: false,
|
||||
},
|
||||
});
|
||||
expect(result.accepted).toBe(true);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user