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