feat: release authentication diagnostics 0.2.0

This commit is contained in:
2026-08-19 14:13:48 +02:00
parent 603559c540
commit 53cc91f2a5
41 changed files with 3472 additions and 154 deletions
+124
View File
@@ -0,0 +1,124 @@
import { describe, expect, it } from "vitest";
import {
bytesToArrayBuffer,
bytesToBase64Url,
utf8ToBytes,
} from "../../src/crypto/encoding";
import { verifyAttestation } from "../../src/webauthn/attestation";
function bytesValue(value: Uint8Array): number[] {
if (value.length < 24) return [0x40 | value.length, ...value];
if (value.length < 256) return [0x58, value.length, ...value];
return [0x59, value.length >>> 8, value.length & 0xff, ...value];
}
function textValue(value: string): number[] {
const encoded = utf8ToBytes(value);
return [0x60 | encoded.length, ...encoded];
}
function noneAttestation(
authenticatorData: Uint8Array,
format = "none",
): Uint8Array {
return Uint8Array.from([
0xa3,
...textValue("fmt"),
...textValue(format),
...textValue("authData"),
...bytesValue(authenticatorData),
...textValue("attStmt"),
0xa0,
]);
}
async function registrationData(): Promise<{
attestationObject: string;
clientDataJSON: string;
challenge: string;
}> {
const challenge = bytesToBase64Url(Uint8Array.of(1, 2, 3, 4));
const client = utf8ToBytes(
JSON.stringify({
type: "webauthn.create",
challenge,
origin: "https://example.test",
crossOrigin: false,
}),
);
const rpHash = new Uint8Array(
await crypto.subtle.digest(
"SHA-256",
bytesToArrayBuffer(utf8ToBytes("example.test")),
),
);
const cose = Uint8Array.from([
0xa5,
0x01,
0x02,
0x03,
0x26,
0x20,
0x01,
0x21,
0x58,
0x20,
...new Uint8Array(32).fill(1),
0x22,
0x58,
0x20,
...new Uint8Array(32).fill(2),
]);
const authData = Uint8Array.from([
...rpHash,
0x41,
0,
0,
0,
0,
...new Uint8Array(16),
0,
1,
7,
...cose,
]);
return {
attestationObject: bytesToBase64Url(noneAttestation(authData)),
clientDataJSON: bytesToBase64Url(client),
challenge,
};
}
describe("WebAuthn attestation verification", () => {
it("verifies a registration with none attestation without claiming trust", async () => {
const data = await registrationData();
const result = await verifyAttestation({
...data,
expectedChallenge: data.challenge,
expectedOrigin: "https://example.test",
expectedRpId: "example.test",
});
expect(result).toMatchObject({
verified: true,
format: "none",
attestationType: "none",
trustEstablished: false,
});
});
it("rejects mismatched ceremony state", async () => {
const data = await registrationData();
const result = await verifyAttestation({
...data,
expectedChallenge: "different",
expectedOrigin: "https://evil.test",
expectedRpId: "evil.test",
});
expect(result.verified).toBe(false);
expect(
result.checks
.filter((check) => check.status === "fail")
.map((check) => check.name),
).toEqual(expect.arrayContaining(["Challenge", "Origin", "RP ID hash"]));
});
});