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

chadtmiller.com/pds.js

An atproto PDS in JavaScript, for Cloudflare Workers and Node

0
0
main
194 lines · 6.4 KB Raw History
// Runs the reference implementation's verifier over artifacts pds.js produced.
// Copied into the atproto worktree by scripts/space-reference/run.sh as a
// scratch file; never committed there.

import { readFileSync } from 'node:fs';
import { P256Keypair } from '@atproto/crypto';
import { parseCid } from '@atproto/lex-data';
import { describe, expect, it } from 'vitest';
import {
  COMMIT_MAC_TYPE,
  decapsulate,
  parseMacHeader,
  parseWantMacHeader,
  RECORD_MAC_TYPE,
  verifyCommit,
  verifyCredentialSig,
  verifyRecord,
  verifyRepoStarFull,
  verifySpaceToken,
} from '../src/index.js';

const DIR = process.env.ARTIFACTS_DIR ?? '/tmp/pdsjs-space-artifacts';
const manifest = JSON.parse(readFileSync(`${DIR}/manifest.json`, 'utf8'));
const star = new Uint8Array(readFileSync(`${DIR}/repo.star`));

const fromB64 = (s: string) => new Uint8Array(Buffer.from(s, 'base64'));
const fromHex = (s: string) => new Uint8Array(Buffer.from(s, 'hex'));

// A record's JSON form in the data model, as the reference's XRPC client
// hands it over: `{$link}` is a CID and `{$bytes}` a byte string.
const jsonToLex = (value: any): any => {
  if (Array.isArray(value)) return value.map(jsonToLex);
  if (value && typeof value === 'object') {
    const keys = Object.keys(value);
    if (keys.length === 1 && typeof value.$link === 'string') {
      return parseCid(value.$link);
    }
    if (keys.length === 1 && typeof value.$bytes === 'string') {
      return fromB64(value.$bytes);
    }
    return Object.fromEntries(
      Object.entries(value).map(([k, v]) => [k, jsonToLex(v)]),
    );
  }
  return value;
};

const kem = await P256Keypair.import(manifest.kemPrivateKeyHex);

describe('reference verifies pds.js MACs', () => {
  it('derives the same secret from each key share', () => {
    expect(kem.did()).toBe(manifest.kemDidKey);
    for (const read of [
      manifest.getRepo,
      manifest.getLatestCommit,
      manifest.getRecord,
    ]) {
      const type =
        read === manifest.getRecord ? RECORD_MAC_TYPE : COMMIT_MAC_TYPE;
      expect(parseWantMacHeader(read.header, type)).toBe(read.keyShare);
      expect(Buffer.from(decapsulate(kem, read.keyShare)).toString('hex')).toBe(
        read.sharedSecretHex,
      );
    }
  });

  const repoParams = () => ({
    space: manifest.space,
    repo: manifest.author,
    sharedSecret: fromHex(manifest.getRepo.sharedSecretHex),
    mac: parseMacHeader(manifest.getRepo.mac, COMMIT_MAC_TYPE),
  });

  it('accepts the getRepo STAR archive', async () => {
    // Checks the commit's MAC, the archive's framing and record order, each
    // record's CID, and that the records' set hash is the commit's data.
    const verified = await verifyRepoStarFull([star], repoParams());
    expect(verified.commit.rev).toBe(manifest.rev);
    expect(verified.records).toHaveLength(manifest.recordCount);
    expect(verified.records.map((r) => `${r.collection}/${r.rkey}`)).toEqual([
      'com.e.p/x',
      'com.example.post/aaa',
      'com.example.post/bbb',
      'com.example.post/ccc',
      'com.example.reply/zz',
    ]);
    const reply = verified.records.find(
      (r) => r.collection === 'com.example.reply',
    );
    expect(reply?.record).toEqual({
      $type: 'com.example.reply',
      deep: { nested: [1, 2, 3] },
    });
  });

  it('rejects the archive for the wrong repo or space', async () => {
    await expect(
      verifyRepoStarFull([star], { ...repoParams(), repo: 'did:plc:other' }),
    ).rejects.toThrow();
    await expect(
      verifyRepoStarFull([star], {
        ...repoParams(),
        space: 'at://did:plc:conformauthority/space/com.example.forum/other',
      }),
    ).rejects.toThrow();
  });

  it('accepts the getLatestCommit commit', () => {
    const { commit, sharedSecretHex, mac } = manifest.getLatestCommit;
    const params = {
      space: manifest.space,
      repo: manifest.author,
      sharedSecret: fromHex(sharedSecretHex),
      mac: parseMacHeader(mac, COMMIT_MAC_TYPE),
    };
    const parsed = { ...commit, data: fromB64(commit.data.$bytes) };
    expect(verifyCommit(parsed, params)).toBe(true);
    expect(verifyCommit({ ...parsed, rev: '2222222222222' }, params)).toBe(
      false,
    );
  });

  it('accepts the getRecord record, blob ref and all', async () => {
    const { record, sharedSecretHex, mac } = manifest.getRecord;
    const params = {
      uri: `${manifest.space}/${manifest.author}/com.example.post/ccc`,
      sharedSecret: fromHex(sharedSecretHex),
      mac: parseMacHeader(mac, RECORD_MAC_TYPE),
    };
    const lex = { ...record, value: jsonToLex(record.value) };
    expect(await verifyRecord(lex, params)).toBe(true);
    expect(
      await verifyRecord(
        { ...lex, value: { ...lex.value, text: 'x' } },
        params,
      ),
    ).toBe(false);
  });
});

describe('reference verifies pds.js tokens', () => {
  it('accepts the delegation token', async () => {
    const { payload } = await verifySpaceToken(
      'delegation',
      manifest.delegationToken,
      {
        getSigningKey: () => manifest.userDidKey,
        aud: manifest.delegationAud,
        sub: manifest.space,
      },
    );
    expect(payload.iss).toBe(manifest.author);
    expect(payload.sub).toBe(manifest.space);
  });

  it('accepts the credential the authority issued', async () => {
    const { payload, header } = await verifySpaceToken(
      'credential',
      manifest.credential,
      { getSigningKey: () => manifest.authorityDidKey, sub: manifest.space },
    );
    expect(payload.iss).toBe(manifest.authority);
    expect(payload.cnf?.kid).toBe(manifest.credentialKeyId);
    expect(header.kid).toBe('#atproto');
  });

  it('accepts the signature presenting the credential', async () => {
    await verifyCredentialSig(manifest.useSignature, manifest.credentialKeyId);
    expect(manifest.useSignature['atproto-space-audience']).toBe(
      manifest.author,
    );
    expect(manifest.useSignature.authorization).toBe(
      `Atproto-Space-Credential ${manifest.credential}`,
    );
  });

  it('rejects the signature under another key', async () => {
    const other = await P256Keypair.create();
    await expect(
      verifyCredentialSig(
        manifest.useSignature,
        other.did() as `did:${string}`,
      ),
    ).rejects.toThrow();
  });

  it('rejects a delegation token presented as a credential', async () => {
    await expect(
      verifySpaceToken('credential', manifest.delegationToken, {
        getSigningKey: () => manifest.userDidKey,
      }),
    ).rejects.toThrow();
  });
});