chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// 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/);
});
});