~ knotless Search/
↑↓ move↵ open⇧↵ all results

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
110 lines · 4.2 KB Raw History
// 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/);
  });
});