From a9963fb1a9c4a948a77dedc261bb266b808f54f0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Tue, 19 Mar 2019 21:24:11 +0100 Subject: [PATCH 1/4] Bundle all activityPub in middleware This way we can disable it for the seeder server which does not need to create RSA keys for all created users --- src/middleware/activityPubMiddleware.js | 18 ++++++++++++++++++ src/middleware/index.js | 2 ++ src/middleware/userMiddleware.js | 4 ---- src/resolvers/posts.js | 3 --- 4 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 src/middleware/activityPubMiddleware.js diff --git a/src/middleware/activityPubMiddleware.js b/src/middleware/activityPubMiddleware.js new file mode 100644 index 000000000..2fcd04729 --- /dev/null +++ b/src/middleware/activityPubMiddleware.js @@ -0,0 +1,18 @@ +import { generateRsaKeyPair } from '../activitypub/security' +import { activityPub } from '../activitypub/ActivityPub' + +export default { + Mutation: { + CreatePost: async (resolve, root, args, context, info) => { + args.activityId = activityPub.generateStatusId(context.user.slug) + args.objectId = activityPub.generateStatusId(context.user.slug) + return resolve(root, args, context, info) + }, + CreateUser: async (resolve, root, args, context, info) => { + const keys = generateRsaKeyPair() + Object.assign(args, keys) + args.actorId = `${process.env.GRAPHQL_URI}/activitypub/users/${args.slug}` + return resolve(root, args, context, info) + } + } +} diff --git a/src/middleware/index.js b/src/middleware/index.js index 6f95c7451..6ed0955e8 100644 --- a/src/middleware/index.js +++ b/src/middleware/index.js @@ -1,3 +1,4 @@ +import activityPubMiddleware from './activityPubMiddleware' import passwordMiddleware from './passwordMiddleware' import softDeleteMiddleware from './softDeleteMiddleware' import sluggifyMiddleware from './sluggifyMiddleware' @@ -25,6 +26,7 @@ export default schema => { // add permisions middleware at the first position (unless we're seeding) // NOTE: DO NOT SET THE PERMISSION FLAT YOUR SELF if (process.env.PERMISSIONS !== 'disabled' && process.env.NODE_ENV !== 'production') { + middleware.unshift(activityPubMiddleware) middleware.unshift(permissionsMiddleware.generate(schema)) } return middleware diff --git a/src/middleware/userMiddleware.js b/src/middleware/userMiddleware.js index a85bd1244..2979fdadf 100644 --- a/src/middleware/userMiddleware.js +++ b/src/middleware/userMiddleware.js @@ -1,5 +1,4 @@ import createOrUpdateLocations from './nodes/locations' -import { generateRsaKeyPair } from '../activitypub/security' import dotenv from 'dotenv' dotenv.config() @@ -7,9 +6,6 @@ dotenv.config() export default { Mutation: { CreateUser: async (resolve, root, args, context, info) => { - const keys = generateRsaKeyPair() - Object.assign(args, keys) - args.actorId = `${process.env.GRAPHQL_URI}/activitypub/users/${args.slug}` const result = await resolve(root, args, context, info) await createOrUpdateLocations(args.id, args.locationName, context.driver) return result diff --git a/src/resolvers/posts.js b/src/resolvers/posts.js index b7a6d8a2a..de5cd8282 100644 --- a/src/resolvers/posts.js +++ b/src/resolvers/posts.js @@ -1,5 +1,4 @@ import { neo4jgraphql } from 'neo4j-graphql-js' -import { activityPub } from '../activitypub/ActivityPub' import as from 'activitystrea.ms' import dotenv from 'dotenv' /* @@ -13,8 +12,6 @@ dotenv.config() export default { Mutation: { CreatePost: async (object, params, context, resolveInfo) => { - params.activityId = activityPub.generateStatusId(context.user.slug) - params.objectId = activityPub.generateStatusId(context.user.slug) const result = await neo4jgraphql(object, params, context, resolveInfo, false) const session = context.driver.session() From c2c9e98787b55f02b7a0c004193bac4492483c58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 20 Mar 2019 12:33:10 +0100 Subject: [PATCH 2/4] Fix http signature unit test --- .../security/httpSignature.spec.js | 131 ++++++++++-------- src/activitypub/security/index.js | 15 +- src/activitypub/utils/index.js | 7 +- 3 files changed, 88 insertions(+), 65 deletions(-) diff --git a/src/activitypub/security/httpSignature.spec.js b/src/activitypub/security/httpSignature.spec.js index fe09eda8a..bddb05ea9 100644 --- a/src/activitypub/security/httpSignature.spec.js +++ b/src/activitypub/security/httpSignature.spec.js @@ -1,69 +1,84 @@ -import { createSignature, verifySignature } from '.' -import Factory from '../../seed/factories' -import { host, login } from '../../jest/helpers' -import { GraphQLClient } from 'graphql-request' +import { generateRsaKeyPair, createSignature, verifySignature } from '.' import crypto from 'crypto' -import { expect } from 'chai' -const factory = Factory() +jest.mock('request') +import request from 'request' -describe('Signature creation and verification', () => { - let user = null - let client = null - const headers = { - 'Date': '2019-03-08T14:35:45.759Z', - 'Host': 'democracy-app.de', - 'Content-Type': 'application/json' - } +let privateKey +let publicKey +let headers +const passphrase = "a7dsf78sadg87ad87sfagsadg78" - beforeEach(async () => { - await factory.create('User', { - 'slug': 'test-user', - 'name': 'Test User', - 'email': 'user@example.org', - 'password': 'swordfish' +describe('activityPub/security', () => { + beforeEach(() => { + const pair = generateRsaKeyPair({passphrase}) + privateKey = pair.privateKey + publicKey = pair.publicKey + headers = { + 'Date': '2019-03-08T14:35:45.759Z', + 'Host': 'democracy-app.de', + 'Content-Type': 'application/json' + } + }) + + describe('createSignature', () => { + describe('returned http signature', () => { + let signatureB64 + let httpSignature + + beforeEach(() => { + const signer = crypto.createSign('rsa-sha256') + signer.update('(request-target): post /activitypub/users/max/inbox\ndate: 2019-03-08T14:35:45.759Z\nhost: democracy-app.de\ncontent-type: application/json') + signatureB64 = signer.sign({ key: privateKey, passphrase: 'a7dsf78sadg87ad87sfagsadg78' }, 'base64') + httpSignature = createSignature({privateKey, keyId: 'https://human-connection.org/activitypub/users/lea#main-key', url: 'https://democracy-app.de/activitypub/users/max/inbox', headers, passphrase}) + }) + + it('contains keyId', () => { + expect(httpSignature).toContain('keyId="https://human-connection.org/activitypub/users/lea#main-key"') + }) + + it('contains default algorithm "rsa-sha256"', () => { + expect(httpSignature).toContain('algorithm="rsa-sha256"') + }) + + it('contains headers', () => { + expect(httpSignature).toContain('headers="(request-target) date host content-type"') + }) + + it('contains signature', () => { + expect(httpSignature).toContain('signature="' + signatureB64 + '"') + }) }) - const headers = await login({ email: 'user@example.org', password: 'swordfish' }) - client = new GraphQLClient(host, { headers }) - const result = await client.request(`query { - User(slug: "test-user") { - privateKey - publicKey + }) + + describe('verifySignature', () => { + let httpSignature + + beforeEach(() => { + httpSignature = createSignature({privateKey, keyId: 'http://localhost:4001/activitypub/users/test-user#main-key', url: 'https://democracy-app.de/activitypub/users/max/inbox', headers, passphrase}) + const body = { + "publicKey":{ + "id":"https://localhost:4001/activitypub/users/test-user#main-key", + "owner":"https://localhost:4001/activitypub/users/test-user", + "publicKeyPem": publicKey + } } - }`) - user = result.User[0] - }) - afterEach(async () => { - await factory.cleanDatabase() - }) - - describe('Signature creation', () => { - let signatureB64 = '' - beforeEach(() => { - const signer = crypto.createSign('rsa-sha256') - signer.update('(request-target): post /activitypub/users/max/inbox\ndate: 2019-03-08T14:35:45.759Z\nhost: democracy-app.de\ncontent-type: application/json') - signatureB64 = signer.sign({ key: user.privateKey, passphrase: 'a7dsf78sadg87ad87sfagsadg78' }, 'base64') - }) - it('creates a Signature with given privateKey, keyId, url and headers (default algorithm: "rsa-sha256")', () => { - const httpSignature = createSignature(user.privateKey, 'https://human-connection.org/activitypub/users/lea#main-key', 'https://democracy-app.de/activitypub/users/max/inbox', headers) - - expect(httpSignature).to.contain('keyId="https://human-connection.org/activitypub/users/lea#main-key"') - expect(httpSignature).to.contain('algorithm="rsa-sha256"') - expect(httpSignature).to.contain('headers="(request-target) date host content-type"') - expect(httpSignature).to.contain('signature="' + signatureB64 + '"') - }) - }) - - describe('Signature verification', () => { - let httpSignature = '' - beforeEach(() => { - httpSignature = createSignature(user.privateKey, 'http://localhost:4001/activitypub/users/test-user#main-key', 'https://democracy-app.de/activitypub/users/max/inbox', headers) + const mockedRequest = jest.fn((_, callback) => callback(null, null, JSON.stringify(body))) + request.mockImplementation(mockedRequest) }) - it('verifies a Signature correct', async () => { - headers['Signature'] = httpSignature - const isVerified = await verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers) - expect(isVerified).to.equal(true) + it('resolves false', async () => { + await expect(verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers)).resolves.toEqual(false) + }) + + describe('valid signature', () => { + beforeEach(() => { + headers.Signature = httpSignature + }) + + it('resolves true', async () => { + await expect(verifySignature('https://democracy-app.de/activitypub/users/max/inbox', headers)).resolves.toEqual(true) + }) }) }) }) diff --git a/src/activitypub/security/index.js b/src/activitypub/security/index.js index 412084022..fdb1e27c6 100644 --- a/src/activitypub/security/index.js +++ b/src/activitypub/security/index.js @@ -6,7 +6,8 @@ const debug = require('debug')('ea:security') dotenv.config({ path: resolve('src', 'activitypub', '.env') }) -export function generateRsaKeyPair () { +export function generateRsaKeyPair (options = {}) { + const { passphrase = process.env.PRIVATE_KEY_PASSPHRASE } = options return crypto.generateKeyPairSync('rsa', { modulusLength: 4096, publicKeyEncoding: { @@ -17,18 +18,24 @@ export function generateRsaKeyPair () { type: 'pkcs8', format: 'pem', cipher: 'aes-256-cbc', - passphrase: process.env.PRIVATE_KEY_PASSPHRASE + passphrase } }) } // signing -export function createSignature (privKey, keyId, url, headers = {}, algorithm = 'rsa-sha256') { +export function createSignature (options) { + const { + privateKey, keyId, url, + headers = {}, + algorithm = 'rsa-sha256', + passphrase = process.env.PRIVATE_KEY_PASSPHRASE + } = options if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) { throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`) } const signer = crypto.createSign(algorithm) const signingString = constructSigningString(url, headers) signer.update(signingString) - const signatureB64 = signer.sign({ key: privKey, passphrase: process.env.PRIVATE_KEY_PASSPHRASE }, 'base64') + const signatureB64 = signer.sign({ key: privateKey, passphrase }, 'base64') const headersString = Object.keys(headers).reduce((result, key) => { return result + ' ' + key.toLowerCase() }, '') return `keyId="${keyId}",algorithm="${algorithm}",headers="(request-target)${headersString}",signature="${signatureB64}"` } diff --git a/src/activitypub/utils/index.js b/src/activitypub/utils/index.js index 0cb4b7d0d..6940ca082 100644 --- a/src/activitypub/utils/index.js +++ b/src/activitypub/utils/index.js @@ -75,12 +75,13 @@ export function signAndSend (activity, fromName, targetDomain, url) { headers: { 'Host': targetDomain, 'Date': date, - 'Signature': createSignature(privateKey, `http://${activityPub.domain}/activitypub/users/${fromName}#main-key`, url, - { + 'Signature': createSignature({privateKey, keyId: `http://${activityPub.domain}/activitypub/users/${fromName}#main-key`, url, + headers: { 'Host': targetDomain, 'Date': date, 'Content-Type': 'application/activity+json' - }), + } + }), 'Content-Type': 'application/activity+json' }, method: 'POST', From 27e8b70412487c6812369d80e331764d90c97500 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 20 Mar 2019 15:09:38 +0100 Subject: [PATCH 3/4] Fix lint and refactor cc @Mastercuber Send out an activity should be the responsibility of the middleware. --- .../security/httpSignature.spec.js | 20 ++++----- src/activitypub/utils/index.js | 4 +- src/jwt/decode.js | 2 +- src/middleware/activityPubMiddleware.js | 40 ++++++++++++++++- src/resolvers/posts.js | 43 +------------------ 5 files changed, 54 insertions(+), 55 deletions(-) diff --git a/src/activitypub/security/httpSignature.spec.js b/src/activitypub/security/httpSignature.spec.js index bddb05ea9..fc7e69479 100644 --- a/src/activitypub/security/httpSignature.spec.js +++ b/src/activitypub/security/httpSignature.spec.js @@ -1,16 +1,16 @@ import { generateRsaKeyPair, createSignature, verifySignature } from '.' import crypto from 'crypto' -jest.mock('request') import request from 'request' +jest.mock('request') let privateKey let publicKey let headers -const passphrase = "a7dsf78sadg87ad87sfagsadg78" +const passphrase = 'a7dsf78sadg87ad87sfagsadg78' describe('activityPub/security', () => { beforeEach(() => { - const pair = generateRsaKeyPair({passphrase}) + const pair = generateRsaKeyPair({ passphrase }) privateKey = pair.privateKey publicKey = pair.publicKey headers = { @@ -29,7 +29,7 @@ describe('activityPub/security', () => { const signer = crypto.createSign('rsa-sha256') signer.update('(request-target): post /activitypub/users/max/inbox\ndate: 2019-03-08T14:35:45.759Z\nhost: democracy-app.de\ncontent-type: application/json') signatureB64 = signer.sign({ key: privateKey, passphrase: 'a7dsf78sadg87ad87sfagsadg78' }, 'base64') - httpSignature = createSignature({privateKey, keyId: 'https://human-connection.org/activitypub/users/lea#main-key', url: 'https://democracy-app.de/activitypub/users/max/inbox', headers, passphrase}) + httpSignature = createSignature({ privateKey, keyId: 'https://human-connection.org/activitypub/users/lea#main-key', url: 'https://democracy-app.de/activitypub/users/max/inbox', headers, passphrase }) }) it('contains keyId', () => { @@ -54,12 +54,12 @@ describe('activityPub/security', () => { let httpSignature beforeEach(() => { - httpSignature = createSignature({privateKey, keyId: 'http://localhost:4001/activitypub/users/test-user#main-key', url: 'https://democracy-app.de/activitypub/users/max/inbox', headers, passphrase}) - const body = { - "publicKey":{ - "id":"https://localhost:4001/activitypub/users/test-user#main-key", - "owner":"https://localhost:4001/activitypub/users/test-user", - "publicKeyPem": publicKey + httpSignature = createSignature({ privateKey, keyId: 'http://localhost:4001/activitypub/users/test-user#main-key', url: 'https://democracy-app.de/activitypub/users/max/inbox', headers, passphrase }) + const body = { + 'publicKey': { + 'id': 'https://localhost:4001/activitypub/users/test-user#main-key', + 'owner': 'https://localhost:4001/activitypub/users/test-user', + 'publicKeyPem': publicKey } } diff --git a/src/activitypub/utils/index.js b/src/activitypub/utils/index.js index 6940ca082..c48e15e3d 100644 --- a/src/activitypub/utils/index.js +++ b/src/activitypub/utils/index.js @@ -75,7 +75,9 @@ export function signAndSend (activity, fromName, targetDomain, url) { headers: { 'Host': targetDomain, 'Date': date, - 'Signature': createSignature({privateKey, keyId: `http://${activityPub.domain}/activitypub/users/${fromName}#main-key`, url, + 'Signature': createSignature({ privateKey, + keyId: `http://${activityPub.domain}/activitypub/users/${fromName}#main-key`, + url, headers: { 'Host': targetDomain, 'Date': date, diff --git a/src/jwt/decode.js b/src/jwt/decode.js index 6abc06dc1..e8305a83b 100644 --- a/src/jwt/decode.js +++ b/src/jwt/decode.js @@ -13,7 +13,7 @@ export default async (driver, authorizationHeader) => { const session = driver.session() const query = ` MATCH (user:User {id: {id} }) - RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled} + RETURN user {.id, .slug, .name, .avatar, .email, .role, .disabled, .actorId} LIMIT 1 ` const result = await session.run(query, { id }) diff --git a/src/middleware/activityPubMiddleware.js b/src/middleware/activityPubMiddleware.js index 2fcd04729..6c737faff 100644 --- a/src/middleware/activityPubMiddleware.js +++ b/src/middleware/activityPubMiddleware.js @@ -1,12 +1,50 @@ import { generateRsaKeyPair } from '../activitypub/security' import { activityPub } from '../activitypub/ActivityPub' +import as from 'activitystrea.ms' +import dotenv from 'dotenv' + +const debug = require('debug')('backend:schema') +dotenv.config() export default { Mutation: { CreatePost: async (resolve, root, args, context, info) => { args.activityId = activityPub.generateStatusId(context.user.slug) args.objectId = activityPub.generateStatusId(context.user.slug) - return resolve(root, args, context, info) + + const post = await resolve(root, args, context, info) + + const { user: author } = context + const actorId = author.actorId + debug(`actorId = ${actorId}`) + const createActivity = await new Promise((resolve, reject) => { + as.create() + .id(`${actorId}/status/${args.activityId}`) + .actor(`${actorId}`) + .object( + as.article() + .id(`${actorId}/status/${post.id}`) + .content(post.content) + .to('https://www.w3.org/ns/activitystreams#Public') + .publishedNow() + .attributedTo(`${actorId}`) + ).prettyWrite((err, doc) => { + if (err) { + reject(err) + } else { + debug(doc) + const parsedDoc = JSON.parse(doc) + parsedDoc.send = true + resolve(JSON.stringify(parsedDoc)) + } + }) + }) + try { + await activityPub.sendActivity(createActivity) + } catch (e) { + debug(`error sending post activity\n${e}`) + } + return post }, CreateUser: async (resolve, root, args, context, info) => { const keys = generateRsaKeyPair() diff --git a/src/resolvers/posts.js b/src/resolvers/posts.js index de5cd8282..5b06c38fa 100644 --- a/src/resolvers/posts.js +++ b/src/resolvers/posts.js @@ -1,13 +1,4 @@ import { neo4jgraphql } from 'neo4j-graphql-js' -import as from 'activitystrea.ms' -import dotenv from 'dotenv' -/* -import as from 'activitystrea.ms' -import request from 'request' -*/ - -const debug = require('debug')('backend:schema') -dotenv.config() export default { Mutation: { @@ -15,7 +6,7 @@ export default { const result = await neo4jgraphql(object, params, context, resolveInfo, false) const session = context.driver.session() - const author = await session.run( + await session.run( 'MATCH (author:User {id: $userId}), (post:Post {id: $postId}) ' + 'MERGE (post)<-[:WROTE]-(author) ' + 'RETURN author', { @@ -25,38 +16,6 @@ export default { ) session.close() - debug(`actorId = ${author.records[0]._fields[0].properties.actorId}`) - if (Array.isArray(author.records) && author.records.length > 0) { - const actorId = author.records[0]._fields[0].properties.actorId - const createActivity = await new Promise((resolve, reject) => { - as.create() - .id(`${actorId}/status/${params.activityId}`) - .actor(`${actorId}`) - .object( - as.article() - .id(`${actorId}/status/${result.id}`) - .content(result.content) - .to('https://www.w3.org/ns/activitystreams#Public') - .publishedNow() - .attributedTo(`${actorId}`) - ).prettyWrite((err, doc) => { - if (err) { - reject(err) - } else { - debug(doc) - const parsedDoc = JSON.parse(doc) - parsedDoc.send = true - resolve(JSON.stringify(parsedDoc)) - } - }) - }) - try { - await activityPub.sendActivity(createActivity) - } catch (e) { - debug(`error sending post activity\n${e}`) - } - } - return result } } From 431030f05b75c57bf4c7e797696cad802e8c7ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Sch=C3=A4fer?= Date: Wed, 20 Mar 2019 19:24:22 +0100 Subject: [PATCH 4/4] Follow review of @Mastercuber --- src/activitypub/security/httpSignature.spec.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/activitypub/security/httpSignature.spec.js b/src/activitypub/security/httpSignature.spec.js index fc7e69479..d40c38242 100644 --- a/src/activitypub/security/httpSignature.spec.js +++ b/src/activitypub/security/httpSignature.spec.js @@ -28,7 +28,7 @@ describe('activityPub/security', () => { beforeEach(() => { const signer = crypto.createSign('rsa-sha256') signer.update('(request-target): post /activitypub/users/max/inbox\ndate: 2019-03-08T14:35:45.759Z\nhost: democracy-app.de\ncontent-type: application/json') - signatureB64 = signer.sign({ key: privateKey, passphrase: 'a7dsf78sadg87ad87sfagsadg78' }, 'base64') + signatureB64 = signer.sign({ key: privateKey, passphrase }, 'base64') httpSignature = createSignature({ privateKey, keyId: 'https://human-connection.org/activitypub/users/lea#main-key', url: 'https://democracy-app.de/activitypub/users/max/inbox', headers, passphrase }) })