diff --git a/ghost/ghost/src/core/activitypub/actor.entity.test.ts b/ghost/ghost/src/core/activitypub/actor.entity.test.ts new file mode 100644 index 0000000000..e3f97fb6b0 --- /dev/null +++ b/ghost/ghost/src/core/activitypub/actor.entity.test.ts @@ -0,0 +1,57 @@ +import crypto from 'node:crypto'; +import {Actor} from './actor.entity'; +import {HTTPSignature} from './http-signature.service'; +import assert from 'node:assert'; + +describe('Actor', function () { + describe('#sign', function () { + it('returns a request with a valid Signature header', async function () { + const keypair = crypto.generateKeyPairSync('rsa', { + modulusLength: 512 + }); + const baseUrl = new URL('https://example.com/ap'); + const actor = Actor.create({ + username: 'Testing', + outbox: [], + publicKey: keypair.publicKey + .export({type: 'pkcs1', format: 'pem'}) + .toString(), + privateKey: keypair.privateKey + .export({type: 'pkcs1', format: 'pem'}) + .toString() + }); + + const url = new URL('https://some-server.com/users/username/inbox'); + const date = new Date(); + const request = new Request(url, { + headers: { + Host: url.host, + Date: date.toISOString(), + Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' + } + }); + + const signedRequest = await actor.sign(request, baseUrl); + + const publicKey = actor.getJSONLD(baseUrl).publicKey; + + class MockHTTPSignature extends HTTPSignature { + protected static async getPublicKey() { + return crypto.createPublicKey(publicKey.publicKeyPem); + } + } + + const signedRequestURL = new URL(signedRequest.url); + + const actual = await MockHTTPSignature.validate( + signedRequest.method, + signedRequestURL.pathname, + signedRequest.headers + ); + + const expected = true; + + assert.equal(actual, expected, 'The signature should have been valid'); + }); + }); +}); diff --git a/ghost/ghost/src/core/activitypub/actor.entity.ts b/ghost/ghost/src/core/activitypub/actor.entity.ts index e08855ee9e..46dcfd130c 100644 --- a/ghost/ghost/src/core/activitypub/actor.entity.ts +++ b/ghost/ghost/src/core/activitypub/actor.entity.ts @@ -1,9 +1,11 @@ +import crypto from 'crypto'; import ObjectID from 'bson-objectid'; import {Entity} from '../../common/entity.base'; import {ActivityPub} from './types'; import {Activity} from './activity.object'; import {Article} from './article.object'; import {ActivityEvent} from './activity.event'; +import {HTTPSignature} from './http-signature.service'; type ActorData = { username: string; @@ -25,6 +27,12 @@ export class Actor extends Entity { return this.attr.outbox; } + async sign(request: Request, baseUrl: URL): Promise { + const keyId = new URL(this.getJSONLD(baseUrl).publicKey.id); + const key = crypto.createPrivateKey(this.attr.privateKey); + return HTTPSignature.sign(request, keyId, key); + } + private activities: Activity[] = []; static getActivitiesToSave(actor: Actor, fn: (activities: Activity[]) => void) {