// Exercises the secp256k1 verifier the node package injects into the core // signature verifier. secp256k1 is the curve the reference PDS gives every // account, so this is the path a real migration service token takes. import { secp256k1 } from '@noble/curves/secp256k1'; import { base58btcEncode } from '@pdsjs/core/plc'; import { createVerifier } from '@pdsjs/core/verify'; import { describe, expect, it } from 'vitest'; import { secp256k1Verify } from '../src/index.js'; const SECP256K1_MULTICODEC = [0xe7, 0x01]; /** Encode a compressed secp256k1 public key as a did:key. */ function secpDidKey(/** @type {Uint8Array} */ compressedPub) { const prefixed = new Uint8Array( SECP256K1_MULTICODEC.length + compressedPub.length, ); prefixed.set(SECP256K1_MULTICODEC); prefixed.set(compressedPub, SECP256K1_MULTICODEC.length); return `did:key:z${base58btcEncode(prefixed)}`; } async function sha256(/** @type {Uint8Array} */ data) { return new Uint8Array( await crypto.subtle.digest('SHA-256', /** @type {BufferSource} */ (data)), ); } describe('secp256k1 verification', () => { it('verifies a genuine secp256k1 signature through createVerifier', async () => { const priv = secp256k1.utils.randomPrivateKey(); const pub = secp256k1.getPublicKey(priv, true); const didKey = secpDidKey(pub); const data = new TextEncoder().encode('header.payload'); const sig = secp256k1 .sign(await sha256(data), priv, { lowS: true }) .toCompactRawBytes(); const verifier = createVerifier({ secp256k1: secp256k1Verify }); expect(await verifier.verify(didKey, data, sig)).toBe(true); }); it('rejects a signature over different data', async () => { const priv = secp256k1.utils.randomPrivateKey(); const pub = secp256k1.getPublicKey(priv, true); const didKey = secpDidKey(pub); const sig = secp256k1 .sign(await sha256(new TextEncoder().encode('header.payload')), priv, { lowS: true, }) .toCompactRawBytes(); const verifier = createVerifier({ secp256k1: secp256k1Verify }); expect( await verifier.verify(didKey, new TextEncoder().encode('tampered'), sig), ).toBe(false); }); it('rejects a signature from a different key', async () => { const signer = secp256k1.utils.randomPrivateKey(); const other = secp256k1.utils.randomPrivateKey(); const didKey = secpDidKey(secp256k1.getPublicKey(other, true)); const data = new TextEncoder().encode('header.payload'); const sig = secp256k1 .sign(await sha256(data), signer, { lowS: true }) .toCompactRawBytes(); const verifier = createVerifier({ secp256k1: secp256k1Verify }); expect(await verifier.verify(didKey, data, sig)).toBe(false); }); it('rejects a high-S (malleable) signature', async () => { const priv = secp256k1.utils.randomPrivateKey(); const pub = secp256k1.getPublicKey(priv, true); const didKey = secpDidKey(pub); const data = new TextEncoder().encode('header.payload'); const hash = await sha256(data); // Take a canonical low-S signature and flip S to its high-S counterpart // (N - s). That's the same point, so it would verify without the low-S // rule; our verifier must reject it as non-canonical. const low = secp256k1.sign(hash, priv, { lowS: true }).toCompactRawBytes(); const toBig = (/** @type {Uint8Array} */ b) => b.reduce((acc, byte) => (acc << 8n) | BigInt(byte), 0n); const s = toBig(low.slice(32)); const highS = secp256k1.CURVE.n - s; const highSBytes = new Uint8Array(32); for (let i = 31, v = highS; i >= 0; i--, v >>= 8n) { highSBytes[i] = Number(v & 0xffn); } const malleable = new Uint8Array(64); malleable.set(low.slice(0, 32), 0); malleable.set(highSBytes, 32); const verifier = createVerifier({ secp256k1: secp256k1Verify }); expect(await verifier.verify(didKey, data, malleable)).toBe(false); }); it('refuses secp256k1 when no implementation is injected', async () => { const priv = secp256k1.utils.randomPrivateKey(); const didKey = secpDidKey(secp256k1.getPublicKey(priv, true)); const verifier = createVerifier(); // P-256 only await expect( verifier.verify(didKey, new Uint8Array([1]), new Uint8Array(64)), ).rejects.toThrow(/secp256k1/); }); });