chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
// Verifies a permissioned repo, MACs and space tokens produced by the reference
// implementation (bluesky-social/atproto), from fixtures committed under
// test/fixtures/space-reference-*. Exercises our decoder against their encoder;
// the opposite direction is scripts/space-reference/run.sh, which also
// regenerates these fixtures.
import { readFileSync } from 'node:fs';
import { cborDecode } from '@pdsjs/core/repo';
import { verifyCommit } from '@pdsjs/spaces/commit';
import { verifyCredentialSig } from '@pdsjs/spaces/http-signature';
import { decapsulate } from '@pdsjs/spaces/kem';
import { parseMacHeader } from '@pdsjs/spaces/mac';
import {
createMemoryBlobContext,
createMemorySpaceStorage,
} from '@pdsjs/spaces/memory-storage';
import { createSpaceRoutes } from '@pdsjs/spaces/routes';
import { verifyRecord, verifyRepoStar } from '@pdsjs/spaces/star';
import { parseSpaceToken } from '@pdsjs/spaces/token';
import { createVerifier } from '@pdsjs/spaces/verifier';
import { applyWrites } from '@pdsjs/spaces/writer';
import { afterEach, describe, expect, it, vi } from 'vitest';
// REF_ARTIFACTS_DIR overrides the committed fixtures with a freshly generated
// set; scripts/space-reference/run.sh uses it to check upstream has not drifted.
const DIR = process.env.REF_ARTIFACTS_DIR;
const manifestPath = DIR
? `${DIR}/manifest.json`
: new URL('./fixtures/space-reference-manifest.json', import.meta.url);
const starPath = DIR
? `${DIR}/repo.star`
: new URL('./fixtures/space-reference-repo.star', import.meta.url);
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
const star = new Uint8Array(readFileSync(starPath));
const fromB64 = (/** @type {string} */ s) =>
Uint8Array.from(atob(s), (c) => c.charCodeAt(0));
const kemPrivateKey = new Uint8Array(
Buffer.from(manifest.kemPrivateKeyHex, 'hex'),
);
/** The secret the reference MACed a read under, derived as a repo host. */
const secretFor = (/** @type {{keyShare: string}} */ read) =>
decapsulate(kemPrivateKey, read.keyShare);
async function repoParams() {
return {
space: manifest.space,
repo: manifest.author,
sharedSecret: await secretFor(manifest.getRepo),
mac: parseMacHeader(manifest.getRepo.mac, 'atproto-space-repo'),
};
}
describe('pds.js verifies reference output', () => {
it('accepts the reference STAR archive end to end', async () => {
const { commit, records } = await verifyRepoStar(star, await repoParams());
expect(commit.rev).toBe(manifest.rev);
expect(records.map((r) => `${r.collection}/${r.rkey}`)).toEqual(
manifest.expectedPaths,
);
const reply = records.find((r) => r.collection === 'com.example.reply');
expect(reply?.record).toEqual({
$type: 'com.example.reply',
deep: { nested: [1, 2, 3] },
});
// The blob ref arrives as a CID link, tag 42.
const pictured = records.find((r) => r.rkey === 'ccc');
expect(pictured?.cid).toBe(manifest.getRecord.record.cid);
});
it('rejects the archive for the wrong repo or space', async () => {
const params = await repoParams();
await expect(
verifyRepoStar(star, { ...params, repo: 'did:plc:wrong' }),
).rejects.toThrow(/commit failed verification/);
await expect(
verifyRepoStar(star, {
...params,
space: 'at://did:plc:refauthority/space/com.example.forum/other',
}),
).rejects.toThrow(/commit failed verification/);
});
it('verifies the JSON-shaped commit under the same MAC', async () => {
const commit = {
...manifest.commit,
data: fromB64(manifest.commit.data.$bytes),
};
expect(await verifyCommit(commit, await repoParams())).toBe(true);
});
it('verifies a record whose value carries a blob ref', async () => {
const { record, mac } = manifest.getRecord;
expect(
await verifyRecord(record, {
uri: record.uri,
sharedSecret: await secretFor(manifest.getRecord),
mac: parseMacHeader(mac, 'atproto-space-record'),
}),
).toBe(true);
});
});
describe('pds.js verifies reference tokens', () => {
const verifier = createVerifier();
// A committed token is always expired, and expiry is a local clock check
// rather than a cross-implementation property. These check what does cross
// the wire: that their JWT parses and their signature verifies.
/**
* @param {'delegation'|'credential'} type
* @param {string} jwt
* @param {string} didKey
*/
async function verifySignatureOnly(type, jwt, didKey) {
const { header, payload, signingInput, sig } = parseSpaceToken(type, jwt);
const ok = await verifier.verify(didKey, signingInput, sig);
return { header, payload, ok };
}
it('parses and verifies their delegation token', async () => {
const { payload, ok } = await verifySignatureOnly(
'delegation',
manifest.delegationToken,
manifest.userDidKey,
);
expect(ok).toBe(true);
expect(payload.iss).toBe(manifest.author);
expect(payload.sub).toBe(manifest.space);
expect(payload.aud).toBe(manifest.delegationAud);
});
it('parses and verifies their space credential', async () => {
const { header, payload, ok } = await verifySignatureOnly(
'credential',
manifest.credential,
manifest.authorityDidKey,
);
expect(ok).toBe(true);
expect(payload.iss).toBe(manifest.authority);
expect(header.kid).toBe('#atproto');
expect(payload.aud).toBeUndefined();
expect(payload.cnf?.kid).toBe(manifest.credentialKeyId);
});
it('verifies their credential signature for its audience', async () => {
const { audience } = await verifyCredentialSig(
new Headers(manifest.useSignature),
{ keyId: manifest.credentialKeyId, verifier },
);
expect(audience).toBe(manifest.author);
});
it('rejects a delegation token parsed as a credential', async () => {
expect(() =>
parseSpaceToken('credential', manifest.delegationToken),
).toThrow(/wrong token type/);
});
it('rejects their credential under the wrong key', async () => {
const { ok } = await verifySignatureOnly(
'credential',
manifest.credential,
manifest.userDidKey,
);
expect(ok).toBe(false);
});
});
describe('pds.js serves a reference syncer', () => {
afterEach(() => {
vi.useRealTimers();
});
// pds.js as the author's repo host, holding the reference's KEM key: their
// credential, signed with their createSpaceSigHeaders, is accepted, and the
// MAC it answers their key share with is the one the reference computed.
it('accepts their credential and MACs the record as they did', async () => {
const { iat } = parseSpaceToken('credential', manifest.credential).payload;
vi.useFakeTimers({ toFake: ['Date'] });
vi.setSystemTime((iat + 1) * 1000);
const spaceStorage = createMemorySpaceStorage();
const { record } = manifest.getRecord;
const [, , , , , , , collection, rkey] = record.uri.split('/');
await applyWrites(spaceStorage, {
space: manifest.space,
writes: [{ action: 'create', collection, rkey, record: record.value }],
});
const routes = /** @type {Record<string, any>} */ (
createSpaceRoutes({
spaceStorage,
...createMemoryBlobContext(),
getDid: async () => manifest.author,
getSigner: async () => {
throw new Error('not used');
},
getKemKey: async () => ({
privateKey: kemPrivateKey,
didKey: manifest.kemDidKey,
}),
resolveDid: async (/** @type {string} */ did) =>
did === manifest.authority
? {
id: did,
verificationMethod: [
{
id: '#atproto',
type: 'Multikey',
publicKeyMultibase: manifest.authorityDidKey.slice(
'did:key:'.length,
),
},
],
}
: null,
verifier: createVerifier(),
})
);
const path = '/xrpc/com.atproto.space.getRecord';
const url = new URL(`https://pds.example${path}`);
url.searchParams.set('space', manifest.space);
url.searchParams.set('collection', collection);
url.searchParams.set('rkey', rkey);
const res = await routes[path].handler(
new Request(url, {
headers: {
...manifest.useSignature,
'atproto-space-want-mac': manifest.getRecord.wantMac,
},
}),
url,
null,
);
expect(res.status).toBe(200);
const body = await res.json();
expect(body.cid).toBe(record.cid);
expect(body.value).toEqual(record.value);
expect(res.headers.get('atproto-space-mac')).toBe(manifest.getRecord.mac);
});
it('stores their record under the CID they computed', async () => {
const spaceStorage = createMemorySpaceStorage();
const { record } = manifest.getRecord;
const [, , , , , , , collection, rkey] = record.uri.split('/');
await applyWrites(spaceStorage, {
space: manifest.space,
writes: [{ action: 'create', collection, rkey, record: record.value }],
});
const row = await spaceStorage.getSpaceRecord(
manifest.space,
collection,
rkey,
);
if (!row) throw new Error('expected the record');
expect(row.cid).toBe(record.cid);
expect(cborDecode(row.value).image.ref).toBeInstanceOf(Uint8Array);
});
});