From 1bb72aaa4a08d0c36efe1f9fddf1d8775a9f80f7 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 11 Nov 2022 10:27:35 -0600 Subject: [PATCH 1/3] test coverage for security functions --- net/security.js | 1 - spec/functional/inbox.spec.js | 72 +++++++++++++++++++++++++++++++++- spec/functional/outbox.spec.js | 7 ++++ 3 files changed, 78 insertions(+), 2 deletions(-) diff --git a/net/security.js b/net/security.js index 0473fc6..6e8da2a 100644 --- a/net/security.js +++ b/net/security.js @@ -44,7 +44,6 @@ function verifyAuthorization (req, res, next) { async function verifySignature (req, res, next) { const apex = req.app.locals.apex try { - // support for apps not using signature extension to ActivityPub if (!req.get('authorization') && !req.get('signature')) { if (req.app.get('env') !== 'development') { apex.logger.warn('Request rejected: missing http signature') diff --git a/spec/functional/inbox.spec.js b/spec/functional/inbox.spec.js index cc4fd7b..da77fd3 100644 --- a/spec/functional/inbox.spec.js +++ b/spec/functional/inbox.spec.js @@ -1,4 +1,6 @@ -/* global describe, beforeAll, beforeEach, it, expect, spyOn */ +/* global describe, beforeAll, beforeEach, afterAll, it, expect, spyOn */ +const crypto = require('crypto') +const httpSignature = require('http-signature') const request = require('supertest') const merge = require('deepmerge') const nock = require('nock') @@ -1218,6 +1220,74 @@ describe('inbox', function () { .expect(200) }) }) + describe('signature verification', function () { + beforeAll(() => { + app.set('env', 'production') + }) + afterAll(function () { + app.set('env', 'development') + }) + it('rejects missing signature', function () { + return request(app) + .post('/inbox/test') + .set('Content-Type', 'application/activity+json') + .send(activity) + .expect(401) + }) + it('rejects invalid signature', function () { + return request(app) + .post('/inbox/test') + .set('Content-Type', 'application/activity+json') + .set('Date', new Date().toUTCString()) + .set('Signature', 'keyId="https://localhost/u/test",algorithm="rsa-sha256",headers="(request-target) host date",signature="asfdlajsflkjasklgja="') + .send(activity) + .expect(403) + }) + it('handles unverifiable delete', function () { + const act = merge({}, activity) + act.id = 'https://mocked.com/s/abc123' + act.actor = 'https://mocked.com/u/mocked' + act.object = act.actor + act.type = 'Delete' + nock('https://mocked.com') + .get('/u/mocked') + .reply(404) + return request(app) + .post('/inbox/test') + .set('Content-Type', 'application/activity+json') + .set('Date', new Date().toUTCString()) + .set('Signature', 'keyId="https://mocked.com/u/mocked",algorithm="rsa-sha256",headers="(request-target) host date",signature="asfdlajsflkjasklgja="') + .send(act) + .expect(200) + }) + it('validates valid signature', async function () { + const recip = await apex.createActor('recipient', 'recipient') + await apex.store.saveObject(recip) + const body = apex.stringifyPublicJSONLD(activity) + const headers = { + digest: crypto.createHash('sha256').update(body).digest('base64'), + host: 'localhost' + } + httpSignature.signRequest({ + getHeader: k => headers[k.toLowerCase()], + setHeader: (k, v) => (headers[k.toLowerCase()] = v), + method: 'POST', + path: '/inbox/recipient' + }, { + key: testUser._meta.privateKey, + keyId: testUser.id, + headers: ['(request-target)', 'host', 'date', 'digest'], + authorizationHeaderName: 'Signature' + }) + const signedReq = request(app) + .post('/inbox/recipient') + .set('Content-Type', 'application/activity+json') + Object.entries(headers).forEach(([k, v]) => signedReq.set(k, v)) + return signedReq + .send(body) + .expect(200) + }) + }) }) describe('get', function () { let inbox diff --git a/spec/functional/outbox.spec.js b/spec/functional/outbox.spec.js index fef587b..30ff736 100644 --- a/spec/functional/outbox.spec.js +++ b/spec/functional/outbox.spec.js @@ -107,6 +107,13 @@ describe('outbox', function () { .send({ actor: 'bob', '@context': 'https://www.w3.org/ns/activitystreams' }) .expect(400, 'Invalid activity', done) }) + it('rejects unauthorized requests', function () { + return request(app) + .post('/outbox/test') + .set('Content-Type', 'application/activity+json') + .send(activity) + .expect(403) + }) // activity getTargetActor it('errors on unknown actor', function (done) { request(app) From 01c20b6972c68299d876b61cca72f21c2987324b Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 11 Nov 2022 11:23:14 -0600 Subject: [PATCH 2/3] test uncommon branches in utils --- spec/unit/utils.spec.js | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/spec/unit/utils.spec.js b/spec/unit/utils.spec.js index 37442bb..a9ae7ad 100644 --- a/spec/unit/utils.spec.js +++ b/spec/unit/utils.spec.js @@ -19,6 +19,43 @@ describe('utils', function () { beforeEach(function () { return global.resetDb(apex, client, testUser) }) + describe('hasMeta util', function () { + it('returns false when object does not have metadata', function () { + const obj = { _meta: { collection: [] } } + expect(apex.hasMeta(obj, 'collection', testUser.inbox[0])).toBe(false) + }) + }) + describe('removeMeta', function () { + it('returns when object does not have the metadata', function () { + const obj = { _meta: { collection: [] } } + expect(apex.removeMeta(obj, 'collection', testUser.inbox[0])).toBe(undefined) + }) + it('removes the medata', function () { + const obj = { _meta: { collection: [testUser.inbox[0]] } } + apex.removeMeta(obj, 'collection', testUser.inbox[0]) + expect(obj._meta.collection).toEqual([]) + }) + }) + describe('actorIdFromActivity', function () { + it('returns id from object', function () { + expect(apex.actorIdFromActivity({ actor: [{ id: testUser.id }] })) + .toBe(testUser.id) + }) + it('returns href from link', function () { + expect(apex.actorIdFromActivity({ actor: [{ type: 'Link', href: [testUser.id] }] })) + .toBe(testUser.id) + }) + }) + describe('objectIdFromActivity', function () { + it('returns id from object', function () { + expect(apex.objectIdFromActivity({ object: [{ id: testUser.id }] })) + .toBe(testUser.id) + }) + it('returns href from link', function () { + expect(apex.objectIdFromActivity({ object: [{ type: 'Link', href: [testUser.id] }] })) + .toBe(testUser.id) + }) + }) describe('jsonld processing', function () { it('handles context arrays with language tag', async function () { const processed = await apex.fromJSONLD({ From 4546c02a365d03c508a1c511cdb6a3750a40bd49 Mon Sep 17 00:00:00 2001 From: Will Murphy Date: Fri, 11 Nov 2022 11:38:07 -0600 Subject: [PATCH 3/3] test c2s endpoints for internal collections --- spec/functional/collections.spec.js | 54 +++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/spec/functional/collections.spec.js b/spec/functional/collections.spec.js index 519fe71..f4346e1 100644 --- a/spec/functional/collections.spec.js +++ b/spec/functional/collections.spec.js @@ -24,6 +24,9 @@ describe('collections', function () { app.get('/followers/:actor', apex.net.followers.get) app.get('/following/:actor', apex.net.following.get) app.get('/liked/:actor', apex.net.liked.get) + app.get('/u/:actor/blocked', apex.net.blocked.get) + app.get('/u/:actor/rejected', apex.net.rejected.get) + app.get('/u/:actor/rejections', apex.net.rejections.get) app.get('/s/:id/shares', apex.net.shares.get) app.get('/s/:id/likes', apex.net.likes.get) app.get('/u/:actor/c/:id', apex.net.collections.get) @@ -340,5 +343,56 @@ describe('collections', function () { const rejected = await apex.getRejected(testUser, Infinity, true) expect(rejected.orderedItems).toEqual(follows.map(a => a.id).reverse()) }) + it('blocked c2s endpoint returns collection', async function (done) { + request(app) + .get('/u/test/blocked') + .set('Accept', 'application/activity+json') + .expect(200) + .end(function (err, res) { + const standard = { + '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'], + id: 'https://localhost/u/test/blocked', + type: 'OrderedCollection', + totalItems: 0, + first: 'https://localhost/u/test/blocked?page=true' + } + expect(res.body).toEqual(standard) + done(err) + }) + }) + it('rejected c2s endpoint returns collection', async function (done) { + request(app) + .get('/u/test/rejected') + .set('Accept', 'application/activity+json') + .expect(200) + .end(function (err, res) { + const standard = { + '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'], + id: 'https://localhost/u/test/rejected', + type: 'OrderedCollection', + totalItems: 0, + first: 'https://localhost/u/test/rejected?page=true' + } + expect(res.body).toEqual(standard) + done(err) + }) + }) + it('rejections c2s endpoint returns collection', async function (done) { + request(app) + .get('/u/test/rejections') + .set('Accept', 'application/activity+json') + .expect(200) + .end(function (err, res) { + const standard = { + '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'], + id: 'https://localhost/u/test/rejections', + type: 'OrderedCollection', + totalItems: 0, + first: 'https://localhost/u/test/rejections?page=true' + } + expect(res.body).toEqual(standard) + done(err) + }) + }) }) })