123456789101112131415161718import { beforeAll, describe, expect, it } from "vitest";19import {20 base64urlDecode,21 ed25519RawToCose,22 generatePasskeyKeypair,23 publicKeyJwkToCose,24 signPasskeyAssertion,25} from "../../background/passkey-crypto";2627282930313233function decodeCoseKeyMap(bytes: Uint8Array): Map<number, Uint8Array | number> {34 const map = new Map<number, Uint8Array | number>();35 let pos = 0;3637 function readByte(): number {38 return bytes[pos++]!;39 }4041 function readUint(info: number): number {42 if (info < 24) return info;43 if (info === 24) return readByte();44 if (info === 25) { const v = (readByte() << 8) | readByte(); return v; }45 throw new Error("CBOR uint too wide for test");46 }4748 function readNegint(info: number): number {49 return -1 - readUint(info);50 }5152 function readItem(): Uint8Array | number {53 const initial = readByte();54 const majorType = initial >> 5;55 const info = initial & 0x1f;56 switch (majorType) {57 case 0: return readUint(info);58 case 1: return readNegint(info);59 case 2: {60 const len = readUint(info);61 const slice = bytes.slice(pos, pos + len);62 pos += len;63 return slice;64 }65 default:66 throw new Error(`Unsupported CBOR major type ${majorType} in test decoder`);67 }68 }6970 const initial = readByte();71 const majorType = initial >> 5;72 const count = initial & 0x1f; 73 if (majorType !== 5) throw new Error("Expected CBOR map");7475 for (let i = 0; i < count; i++) {76 const key = readItem() as number;77 const value = readItem();78 map.set(key, value);79 }80 return map;81}828384858687let keypair: {88 publicKeyCose: Uint8Array;89 publicKeySpki: Uint8Array;90 privateKeyJwk: JsonWebKey;91};9293describe("T-015: Ed25519 passkey key generation and COSE encoding", () => {94 beforeAll(async () => {95 keypair = await generatePasskeyKeypair(-8);96 });9798 99 100 101 it("T-015a: generatePasskeyKeypair(-8) returns publicKeyCose, publicKeySpki, privateKeyJwk", () => {102 expect(keypair.publicKeyCose).toBeInstanceOf(Uint8Array);103 expect(keypair.publicKeySpki).toBeInstanceOf(Uint8Array);104 expect(typeof keypair.privateKeyJwk).toBe("object");105 expect(keypair.publicKeyCose.length).toBeGreaterThan(0);106 expect(keypair.publicKeySpki.length).toBeGreaterThan(0);107 });108109 110 111 112 113 114 115 116 117 118 it("T-015b: COSE key structure matches Ed25519 COSE map (kty=1, alg=-8, crv=6, x=32 bytes)", () => {119 const map = decodeCoseKeyMap(keypair.publicKeyCose);120 expect(map.get(1)).toBe(1); 121 expect(map.get(3)).toBe(-8); 122 expect(map.get(-1)).toBe(6); 123 const x = map.get(-2);124 expect(x).toBeInstanceOf(Uint8Array);125 expect((x as Uint8Array).length).toBe(32);126 });127128 129 130 131 132 133 134 135 it("T-015c: publicKeyJwkToCose and ed25519RawToCose produce identical COSE bytes", () => {136 const fromJwk = publicKeyJwkToCose(keypair.privateKeyJwk, -8);137 138 const rawPub = base64urlDecode(keypair.privateKeyJwk.x!);139 const fromRaw = ed25519RawToCose(rawPub);140141 expect(fromJwk).toEqual(fromRaw);142 });143144 145 146 147 it("T-015d: ed25519RawToCose throws on non-32-byte input", () => {148 expect(() => ed25519RawToCose(new Uint8Array(31))).toThrow(/invalid/i);149 expect(() => ed25519RawToCose(new Uint8Array(33))).toThrow(/invalid/i);150 expect(() => ed25519RawToCose(new Uint8Array(64))).toThrow(/invalid/i);151 });152});153154describe("T-015: Ed25519 passkey signature round-trip", () => {155 beforeAll(async () => {156 keypair = await generatePasskeyKeypair(-8);157 });158159 160 161 162 163 164 it("T-015e: signPasskeyAssertion with Ed25519 returns a 64-byte signature", async () => {165 const authData = new Uint8Array(37).fill(0xaa);166 const clientDataHash = new Uint8Array(32).fill(0xbb);167 const sig = await signPasskeyAssertion(keypair.privateKeyJwk, -8, authData, clientDataHash);168 expect(sig).toBeInstanceOf(Uint8Array);169 expect(sig.length).toBe(64);170 });171172 173 174 175 176 it("T-015f: Ed25519 signature verifies against the generated public key", async () => {177 const authData = new Uint8Array(37).fill(0xcc);178 const clientDataHash = new Uint8Array(32).fill(0xdd);179 const sig = await signPasskeyAssertion(keypair.privateKeyJwk, -8, authData, clientDataHash);180181 182 const rawPub = base64urlDecode(keypair.privateKeyJwk.x!);183 const pubKey = await crypto.subtle.importKey(184 "raw",185 rawPub,186 { name: "Ed25519" },187 false,188 ["verify"],189 );190191 192 const signInput = new Uint8Array(authData.length + clientDataHash.length);193 signInput.set(authData, 0);194 signInput.set(clientDataHash, authData.length);195196 const valid = await crypto.subtle.verify("Ed25519", pubKey, sig, signInput);197 expect(valid).toBe(true);198 });199200 201 202 203 204 205 206 207 it("T-015g: signature does not verify against a different clientDataHash (replay check)", async () => {208 const authData = new Uint8Array(37).fill(0xee);209 const clientDataHash = new Uint8Array(32).fill(0xff);210 const sig = await signPasskeyAssertion(keypair.privateKeyJwk, -8, authData, clientDataHash);211212 const rawPub = base64urlDecode(keypair.privateKeyJwk.x!);213 const pubKey = await crypto.subtle.importKey(214 "raw",215 rawPub,216 { name: "Ed25519" },217 false,218 ["verify"],219 );220221 222 const differentClientDataHash = new Uint8Array(32).fill(0x01);223 const wrongSignInput = new Uint8Array(authData.length + differentClientDataHash.length);224 wrongSignInput.set(authData, 0);225 wrongSignInput.set(differentClientDataHash, authData.length);226227 const valid = await crypto.subtle.verify("Ed25519", pubKey, sig, wrongSignInput);228 expect(valid).toBe(false);229 });230});