feat: release OTP and Passkey Tools 0.1.0

This commit is contained in:
2026-08-19 12:19:35 +02:00
commit f1cb2d7151
67 changed files with 12802 additions and 0 deletions
+108
View File
@@ -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 });
});
});