chadtmiller.com/pds.js
An atproto PDS in JavaScript, for Cloudflare Workers and Node
0
0
import { describe, expect, it } from 'vitest';
import {
createSpaceSigHeaders,
generateSpaceSigKey,
isP256DidKey,
verifyCredentialSig,
} from '../src/http-signature.js';
import { createVerifier } from '../src/verifier.js';
import { vectors } from './helpers/vectors.js';
const verifier = createVerifier();
const AUDIENCE = 'did:plc:bbbb';
/** @param {Record<string, string>} h */
const headers = (h) => new Headers(h);
describe('createSpaceSigHeaders', () => {
it('writes the reference header shapes', async () => {
const key = await generateSpaceSigKey();
const out = await createSpaceSigHeaders(key, {
authorization: 'Atproto-Space-Credential a.b.c',
audience: AUDIENCE,
});
expect(out['signature-input']).toBe(
vectors.httpSignature.headers['signature-input'],
);
expect(out.signature).toMatch(
/^atproto-space-credential=:[A-Za-z0-9+/]{86}==:$/,
);
expect(out['atproto-space-audience']).toBe(AUDIENCE);
});
it('round-trips through verifyCredentialSig', async () => {
const key = await generateSpaceSigKey();
const out = await createSpaceSigHeaders(key, {
authorization: 'Atproto-Space-Credential a.b.c',
audience: AUDIENCE,
});
expect(
await verifyCredentialSig(headers(out), { keyId: key.did, verifier }),
).toEqual({ audience: AUDIENCE });
});
});
describe('verifyCredentialSig', () => {
const ref = vectors.httpSignature;
it('accepts the reference signature', async () => {
expect(
await verifyCredentialSig(headers(ref.headers), {
keyId: ref.keyId,
verifier,
}),
).toEqual({ audience: ref.headers['atproto-space-audience'] });
});
it('accepts a matching keyid and algorithm', async () => {
// The signature covers the parameters, so add them by re-signing.
const key = await generateSpaceSigKey();
const out = await createSpaceSigHeaders(key, {
authorization: 'Atproto-Space-Credential a.b.c',
audience: AUDIENCE,
});
await expect(
verifyCredentialSig(
headers({
...out,
'signature-input': `${out['signature-input']};keyid="${key.did}"`,
}),
{ keyId: key.did, verifier },
),
).rejects.toThrow(/invalid HTTP message signature/);
});
/** @param {Record<string, string>} changes */
async function refuse(changes, pattern = /./) {
const h = { ...ref.headers, ...changes };
for (const [k, v] of Object.entries(h)) if (v === '') delete h[k];
const err = await verifyCredentialSig(headers(h), {
keyId: ref.keyId,
verifier,
}).catch((e) => e);
expect(err?.code).toBe('BadSpaceSignature');
expect(err.message).toMatch(pattern);
}
it('refuses a missing or repeated field', async () => {
await refuse({ signature: '' }, /missing Signature/);
await refuse({ 'signature-input': '' }, /missing Signature-Input/);
await refuse({ 'atproto-space-audience': '' }, /exactly one/);
await refuse(
{ 'atproto-space-audience': `${AUDIENCE}, ${AUDIENCE}` },
/exactly one/,
);
});
it('refuses another label, components, tag, alg or keyid', async () => {
const input = ref.headers['signature-input'];
await refuse(
{ 'signature-input': input.replace('atproto-space-credential=', 'sig=') },
/no atproto-space-credential member/,
);
await refuse(
{
'signature-input':
'atproto-space-credential=("authorization");tag="atproto-space-credential"',
},
/cover exactly/,
);
await refuse(
{
'signature-input': input.replace(
'tag="atproto-space-credential"',
'tag="other"',
),
},
/tag/,
);
await refuse({ 'signature-input': `${input};alg="ed25519"` }, /algorithm/);
await refuse(
{ 'signature-input': `${input};keyid="did:key:zDnaother"` },
/keyid/,
);
await refuse(
{ 'signature-input': 'atproto-space-credential=:AAAA:' },
/inner list/,
);
await refuse(
{ signature: 'atproto-space-credential=("x")' },
/byte sequence/,
);
await refuse({ signature: '(((' }, /malformed Signature/);
});
it('refuses a signature over other values', async () => {
await refuse(
{ authorization: 'Atproto-Space-Credential x.y.z' },
/invalid HTTP message signature/,
);
await refuse(
{ signature: 'atproto-space-credential=:AAAA:' },
/invalid HTTP message signature/,
);
});
it('refuses a key that is not P-256', async () => {
const err = await verifyCredentialSig(headers(ref.headers), {
keyId: 'did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme',
verifier,
}).catch((e) => e);
expect(err.message).toMatch(/P-256/);
});
});
describe('isP256DidKey', () => {
it('accepts P-256 only', () => {
expect(isP256DidKey(ref())).toBe(true);
expect(
isP256DidKey('did:key:zQ3shokFTS3brHcDQrn82RUDfCZESWL1ZdCEJwekUDPQiYBme'),
).toBe(false);
expect(isP256DidKey('did:plc:aaaa')).toBe(false);
expect(isP256DidKey(undefined)).toBe(false);
expect(isP256DidKey('did:key:zzzz')).toBe(false);
});
});
function ref() {
return vectors.httpSignature.keyId;
}