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