feat: release OTP and Passkey Tools 0.1.0
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
base64UrlToBytes,
|
||||
bytesToArrayBuffer,
|
||||
bytesToBase64Url,
|
||||
utf8ToBytes,
|
||||
} from "../../src/crypto/encoding";
|
||||
import { verifyAssertion } from "../../src/webauthn/verify";
|
||||
import type { CborValue } from "../../src/webauthn/cbor";
|
||||
|
||||
function rawEcdsaToDer(raw: Uint8Array): Uint8Array {
|
||||
const integer = (part: Uint8Array): number[] => {
|
||||
let offset = 0;
|
||||
while (offset < part.length - 1 && part[offset] === 0) offset += 1;
|
||||
const value = [...part.slice(offset)];
|
||||
if (value[0]! & 0x80) value.unshift(0);
|
||||
return [0x02, value.length, ...value];
|
||||
};
|
||||
const r = integer(raw.slice(0, 32));
|
||||
const s = integer(raw.slice(32));
|
||||
return Uint8Array.from([0x30, r.length + s.length, ...r, ...s]);
|
||||
}
|
||||
|
||||
describe("assertion verification", () => {
|
||||
it("checks ceremony bindings and a WebAuthn DER ECDSA signature", async () => {
|
||||
const pair = await crypto.subtle.generateKey(
|
||||
{ name: "ECDSA", namedCurve: "P-256" },
|
||||
true,
|
||||
["sign", "verify"],
|
||||
);
|
||||
const jwk = await crypto.subtle.exportKey("jwk", pair.publicKey);
|
||||
const challenge = bytesToBase64Url(
|
||||
crypto.getRandomValues(new Uint8Array(32)),
|
||||
);
|
||||
const origin = "https://auth.example.test";
|
||||
const rpId = "auth.example.test";
|
||||
const clientBytes = utf8ToBytes(
|
||||
JSON.stringify({
|
||||
type: "webauthn.get",
|
||||
challenge,
|
||||
origin,
|
||||
crossOrigin: false,
|
||||
}),
|
||||
);
|
||||
const rpHash = new Uint8Array(
|
||||
await crypto.subtle.digest(
|
||||
"SHA-256",
|
||||
bytesToArrayBuffer(utf8ToBytes(rpId)),
|
||||
),
|
||||
);
|
||||
const authenticator = Uint8Array.from([...rpHash, 0x05, 0, 0, 0, 7]);
|
||||
const clientHash = new Uint8Array(
|
||||
await crypto.subtle.digest("SHA-256", bytesToArrayBuffer(clientBytes)),
|
||||
);
|
||||
const signed = Uint8Array.from([...authenticator, ...clientHash]);
|
||||
const rawSignature = new Uint8Array(
|
||||
await crypto.subtle.sign(
|
||||
{ name: "ECDSA", hash: "SHA-256" },
|
||||
pair.privateKey,
|
||||
bytesToArrayBuffer(signed),
|
||||
),
|
||||
);
|
||||
const cose = new Map<CborValue, CborValue>([
|
||||
[1, 2],
|
||||
[3, -7],
|
||||
[-1, 1],
|
||||
[-2, base64UrlToBytes(jwk.x!)],
|
||||
[-3, base64UrlToBytes(jwk.y!)],
|
||||
]);
|
||||
const result = await verifyAssertion({
|
||||
clientDataJSON: bytesToBase64Url(clientBytes),
|
||||
authenticatorData: bytesToBase64Url(authenticator),
|
||||
signature: bytesToBase64Url(rawEcdsaToDer(rawSignature)),
|
||||
credentialPublicKey: cose,
|
||||
expectedChallenge: challenge,
|
||||
expectedOrigin: origin,
|
||||
expectedRpId: rpId,
|
||||
requireUserVerification: true,
|
||||
previousSignCount: 6,
|
||||
});
|
||||
expect(result.verified).toBe(true);
|
||||
expect(result.checks.every((check) => check.status !== "fail")).toBe(true);
|
||||
});
|
||||
|
||||
it("fails expected origin independently", async () => {
|
||||
await expect(
|
||||
verifyAssertion({
|
||||
clientDataJSON: bytesToBase64Url(
|
||||
utf8ToBytes(
|
||||
JSON.stringify({
|
||||
type: "webauthn.get",
|
||||
challenge: "YQ",
|
||||
origin: "https://wrong.test",
|
||||
}),
|
||||
),
|
||||
),
|
||||
authenticatorData: bytesToBase64Url(
|
||||
Uint8Array.from([...new Uint8Array(32), 1, 0, 0, 0, 0]),
|
||||
),
|
||||
signature: "",
|
||||
credentialPublicKey: "{}",
|
||||
expectedChallenge: "YQ",
|
||||
expectedOrigin: "https://right.test",
|
||||
expectedRpId: "right.test",
|
||||
}),
|
||||
).resolves.toMatchObject({ verified: false });
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user