/** * The helper's dependency-free space auth, checked against @pdsjs/spaces: the * credential signature it sends, the KEM secret it derives and the record MAC * it verifies must be the ones a pds.js repo host accepts and produces. */ import { describe, expect, it } from 'vitest'; import { generateKemKey } from '../../core/src/kem.js'; import { CID, cborEncodeDagCbor, cidToBytes, cidToString, createCid, } from '../../core/src/repo.js'; import { verifyCredentialSig } from '../../spaces/src/http-signature.js'; import { decapsulate } from '../../spaces/src/kem.js'; import { computeRecordMac, formatMacHeader, parseWantMacHeader, } from '../../spaces/src/mac.js'; import { createVerifier } from '../../spaces/src/verifier.js'; import { encapsulate, formatWantMacHeader, p256PointFromDidKey, parseMacHeader, RECORD_MAC_TYPE, recordCid, verifyRecord, } from '../src/space-mac.js'; import { createSpaceSigHeaders, generateSpaceSigKey, } from '../src/space-signature.js'; const SPACE = 'at://did:plc:authority/space/dev.pdsjs.git.space/repo'; const REPO = 'did:plc:member'; describe('credential signature', () => { it('is accepted by a pds.js repo host', async () => { const key = await generateSpaceSigKey(); const headers = await createSpaceSigHeaders(key, { authorization: 'Atproto-Space-Credential a.b.c', audience: REPO, }); expect(headers['Signature-Input']).toBe( 'atproto-space-credential=("authorization" "atproto-space-audience");tag="atproto-space-credential"', ); expect(headers.Signature).toMatch(/^atproto-space-credential=:[^:]+=*:$/); const verified = await verifyCredentialSig(new Headers(headers), { keyId: key.did, verifier: createVerifier(), }); expect(verified.audience).toBe(REPO); }); }); describe('record MAC', () => { it('derives the secret the repo host derives', async () => { const kem = await generateKemKey(); const { keyShare, sharedSecret } = await encapsulate(kem.didKey); const header = formatWantMacHeader(RECORD_MAC_TYPE, keyShare); expect(parseWantMacHeader(header, RECORD_MAC_TYPE)).toBe(keyShare); expect(await decapsulate(kem.privateKey, keyShare)).toEqual(sharedSecret); }); it('decompresses both parities of point', async () => { for (let i = 0; i < 4; i++) { const pair = /** @type {CryptoKeyPair} */ ( await crypto.subtle.generateKey( { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits'], ) ); const raw = new Uint8Array( await crypto.subtle.exportKey('raw', pair.publicKey), ); const { publicKeyToDidKey } = await import('../../core/src/plc.js'); const { compressPublicKey } = await import('../../core/src/crypto.js'); const did = publicKeyToDidKey(compressPublicKey(raw), 'p256'); expect(p256PointFromDidKey(did)).toEqual(raw); } }); it('verifies a record the repo host MACed', async () => { const kem = await generateKemKey(); const { keyShare, sharedSecret } = await encapsulate(kem.didKey); const value = { $type: 'dev.pdsjs.git.repo', name: 'repo', refs: [{ name: 'refs/heads/main', sha: 'a'.repeat(40) }], size: 70000, negative: -3, ok: true, none: null, blob: { $type: 'blob', ref: { $link: 'bafkreigh2akiscaildcqabsyg3dfr6chu3fgpregiymsck7e7aqa4s52zy', }, mimeType: 'application/octet-stream', size: 12, }, raw: { $bytes: 'AAEC' }, }; // The same record as DAG-CBOR holds it: a link under tag 42, bytes as a // byte string. const lex = { ...value, blob: { ...value.blob, ref: new CID(cidToBytes(value.blob.ref.$link)) }, raw: new Uint8Array([0, 1, 2]), }; const cid = cidToString(await createCid(cborEncodeDagCbor(lex))); expect(await recordCid(value)).toBe(cid); const uri = `${SPACE}/${REPO}/dev.pdsjs.git.repo/repo`; const secret = await decapsulate(kem.privateKey, keyShare); const header = formatMacHeader( RECORD_MAC_TYPE, await computeRecordMac(secret, uri, cid), ); const mac = parseMacHeader(header, RECORD_MAC_TYPE); const record = { uri, cid, value }; expect(await verifyRecord(record, { uri, sharedSecret, mac })).toBe(true); expect( await verifyRecord( { ...record, value: { ...value, name: 'other' } }, { uri, sharedSecret, mac }, ), ).toBe(false); expect( await verifyRecord(record, { uri: `${uri}x`, sharedSecret, mac }), ).toBe(false); expect( await verifyRecord(record, { uri, sharedSecret: new Uint8Array(32), mac, }), ).toBe(false); }); it('encodes a plain record to the cid core computes', async () => { const value = { $type: 'dev.pdsjs.git.repo', zeta: 'z', a: [1, 23, 24, 255, 256, 65535, 65536, -1, -25], nested: { bb: 'x', a: { c: false } }, text: 'é'.repeat(30), }; const expected = cidToString(await createCid(cborEncodeDagCbor(value))); expect(await recordCid(value)).toBe(expected); }); it('refuses a MAC header of another type or version', () => { expect(() => parseMacHeader( 'type="atproto-space-repo", ver=1, mac=:AAEC:', RECORD_MAC_TYPE, ), ).toThrow(); expect(() => parseMacHeader( 'type="atproto-space-record", ver=2, mac=:AAEC:', RECORD_MAC_TYPE, ), ).toThrow(); expect(() => parseMacHeader(null, RECORD_MAC_TYPE)).toThrow(); }); it('refuses a key share that is not a P-256 did:key', async () => { await expect( encapsulate('did:key:z6MkhaXgBZDvotDkL5257faiztiGiC2QtKLGpbnnEGta2doK'), ).rejects.toThrow(); }); });