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} 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} 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; }