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