diff --git a/src/activitypub/actors.js b/src/activitypub/actors.js index baeba99c61..8fdbc543a5 100644 --- a/src/activitypub/actors.js +++ b/src/activitypub/actors.js @@ -31,6 +31,7 @@ Actors.assert = async (ids, options = {}) => { winston.verbose(`[activitypub/actors] Asserting ${ids.length} actor(s)`); const followersUrlMap = new Map(); + const pubKeysMap = new Map(); const actors = await Promise.all(ids.map(async (id) => { try { winston.verbose(`[activitypub/actors] Processing ${id}`); @@ -58,6 +59,9 @@ Actors.assert = async (ids, options = {}) => { followersUrlMap.set(actor.followers, actor.id); } + // Public keys + pubKeysMap.set(actor.id, actor.publicKey); + return actor; } catch (e) { return null; @@ -68,13 +72,14 @@ Actors.assert = async (ids, options = {}) => { const profiles = await activitypub.mocks.profile(actors); const now = Date.now(); - const bulkSet = profiles.map((profile) => { - if (!profile) { - return null; + const bulkSet = profiles.reduce((memo, profile) => { + if (profile) { + const key = `userRemote:${profile.uid}`; + memo.push([key, profile], [`${key}:keys`, pubKeysMap.get(profile.uid)]); } - const key = `userRemote:${profile.uid}`; - return [key, profile]; - }).filter(Boolean); + + return memo; + }, []); if (followersUrlMap.size) { bulkSet.push(['followersUrl:uid', Object.fromEntries(followersUrlMap)]); } diff --git a/src/middleware/activitypub.js b/src/middleware/activitypub.js index 79c3e19fe3..bdf28931fb 100644 --- a/src/middleware/activitypub.js +++ b/src/middleware/activitypub.js @@ -1,5 +1,6 @@ 'use strict'; +const db = require('../database'); const meta = require('../meta'); const activitypub = require('../activitypub'); @@ -39,11 +40,29 @@ middleware.validate = async function (req, res, next) { } // Sanity-check payload schema - const required = ['type']; + const required = ['type', 'actor', 'object']; if (!required.every(prop => req.body.hasOwnProperty(prop))) { return res.sendStatus(400); } + const { actor, object } = req.body; + + // Origin checking + const actorHostname = new URL(actor).hostname; + const objectHostname = new URL(typeof object === 'string' ? object : object.id).hostname; + if (actorHostname !== objectHostname) { + return res.sendStatus(403); + } + + // Cross-check key ownership against received actor + await activitypub.actors.assert(actor); + const compare = await db.getObjectField(`userRemote:${actor}:keys`, 'id'); + const { signature } = req.headers; + const keyId = new Map(signature.split(',').filter(Boolean).map(v => v.split('='))).get('keyId'); + if (`"${compare}"` !== keyId) { + return res.sendStatus(403); + } + next(); }; diff --git a/test/activitypub.js b/test/activitypub.js index e1c052e377..936a7b0add 100644 --- a/test/activitypub.js +++ b/test/activitypub.js @@ -88,7 +88,7 @@ describe('ActivityPub integration', () => { }); - describe.only('.resolveId()', () => { + describe('.resolveId()', () => { let url; let resolved;