// 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} */ ( 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); }); });