feat: release authentication diagnostics 0.2.0
This commit is contained in:
@@ -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"]));
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user