diff --git a/ghost/ghost/src/core/activitypub/http-signature.service.test.ts b/ghost/ghost/src/core/activitypub/http-signature.service.test.ts index c327196623..0b969cb0cc 100644 --- a/ghost/ghost/src/core/activitypub/http-signature.service.test.ts +++ b/ghost/ghost/src/core/activitypub/http-signature.service.test.ts @@ -1,4 +1,5 @@ import assert from 'assert'; +import crypto from 'crypto'; import {HTTPSignature} from './http-signature.service'; describe('HTTPSignature', function () { @@ -50,4 +51,50 @@ describe('HTTPSignature', function () { assert.equal(actual, expected, 'The signature should have been validated'); }); }); + + describe('#sign', function () { + it('Can sign a request that does not have explicit Date or Host headers', async function () { + const keypair = crypto.generateKeyPairSync('rsa', { + modulusLength: 512 + }); + const request = new Request('https://example.com:2368/blah'); + const signed = await HTTPSignature.sign(request, new URL('https://keyid.com'), keypair.privateKey); + + assert.ok(signed); + }); + + it('Can sign a post request which is valid', async function () { + const keypair = crypto.generateKeyPairSync('rsa', { + modulusLength: 512 + }); + class MockHTTPSignature extends HTTPSignature { + protected static async getPublicKey() { + return keypair.publicKey; + } + } + + const request = new Request('https://example.com:2368/blah', { + method: 'POST', + headers: { + 'Content-Type': 'application/ld+json' + }, + body: JSON.stringify({ + id: 'https://mastodon.social/users/testingshtuff', + type: 'Accept', + actor: 'https://mastodon.social/users/testingshtuff', + object: 'https://mastodon.social/79f89120-fd13-43e8-aa6d-3bd03652cfad' + }) + }); + + const signed = await MockHTTPSignature.sign(request, new URL('https://keyid.com'), keypair.privateKey); + + assert.ok(signed); + + const url = new URL(signed.url); + const body = Buffer.from(await signed.text()); + const result = await MockHTTPSignature.validate(signed.method, url.pathname, signed.headers, body); + + assert.equal(result, true); + }); + }); }); diff --git a/ghost/ghost/src/core/activitypub/http-signature.service.ts b/ghost/ghost/src/core/activitypub/http-signature.service.ts index ba9824dc83..48920e2918 100644 --- a/ghost/ghost/src/core/activitypub/http-signature.service.ts +++ b/ghost/ghost/src/core/activitypub/http-signature.service.ts @@ -106,7 +106,9 @@ export class HTTPSignature { .update(requestBody) .digest('base64'); - const remoteDigest = requestHeaders.get('digest')?.split('SHA-256=')[1]; + const parts = requestHeaders.get('digest')?.split('='); + parts?.shift(); + const remoteDigest = parts?.join('='); return digest === remoteDigest; } @@ -168,9 +170,24 @@ export class HTTPSignature { algorithm: 'rsa-sha256' }; const url = new URL(request.url); + const requestHeaders = new Headers(request.headers); + if (!requestHeaders.has('host')) { + requestHeaders.set('host', url.host); + } + if (!requestHeaders.has('date')) { + requestHeaders.set('date', (new Date()).toUTCString()); + } + if (request.method.toLowerCase() === 'post') { + const digest = crypto + .createHash(signatureData.algorithm) + .update(Buffer.from(await request.clone().text(), 'utf8')) + .digest('base64'); + + requestHeaders.set('digest', `${signatureData.algorithm}=${digest}`); + } const signatureString = this.generateSignatureString( signatureData, - request.headers, + requestHeaders, request.method, url.pathname ); @@ -180,14 +197,13 @@ export class HTTPSignature { .sign(privateKey) .toString('base64'); - const newHeaders = new Headers(request.headers); - newHeaders.set( + requestHeaders.set( 'Signature', `keyId="${keyId}",headers="${headers.join(' ')}",signature="${signature}",algorithm="${signatureData.algorithm}"` ); return new Request(request, { - headers: newHeaders + headers: requestHeaders }); } }