From df66d81583e07ce049daeeef1edc6a87b57b3684 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Tue, 23 Oct 2018 11:38:48 +0200 Subject: [PATCH] Add compatibility with other Linked Signature algorithms --- scripts/travis.sh | 1 - server.ts | 9 +- server/helpers/custom-jsonld-signature.ts | 4 +- server/helpers/peertube-crypto.ts | 71 ++++++- server/initializers/constants.ts | 2 +- .../handlers/utils/activitypub-http-utils.ts | 13 +- server/middlewares/activitypub.ts | 5 +- .../activitypub/client.ts} | 2 +- server/tests/api/activitypub/helpers.ts | 182 ++++++++++++++++++ server/tests/api/activitypub/index.ts | 3 + .../mastodon/bad-body-http-signature.json | 93 +++++++++ .../json/mastodon/bad-http-signature.json | 93 +++++++++ .../json/mastodon/bad-public-key.json | 3 + .../json/mastodon/create-bad-signature.json | 81 ++++++++ .../api/activitypub/json/mastodon/create.json | 81 ++++++++ .../json/mastodon/http-signature.json | 93 +++++++++ .../activitypub/json/mastodon/public-key.json | 3 + .../peertube/announce-without-context.json | 13 ++ .../json/peertube/invalid-keys.json | 6 + .../api/activitypub/json/peertube/keys.json | 4 + server/tests/api/activitypub/security.ts | 180 +++++++++++++++++ server/tests/api/index-4.ts | 1 + server/tests/index.ts | 1 - server/tests/utils/index.ts | 2 + server/tests/utils/miscs/sql.ts | 29 +++ server/tests/utils/miscs/stubs.ts | 14 ++ server/tests/utils/requests/activitypub.ts | 43 +++++ yarn.lock | 2 +- 28 files changed, 1012 insertions(+), 22 deletions(-) rename server/tests/{activitypub.ts => api/activitypub/client.ts} (92%) create mode 100644 server/tests/api/activitypub/helpers.ts create mode 100644 server/tests/api/activitypub/index.ts create mode 100644 server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json create mode 100644 server/tests/api/activitypub/json/mastodon/bad-http-signature.json create mode 100644 server/tests/api/activitypub/json/mastodon/bad-public-key.json create mode 100644 server/tests/api/activitypub/json/mastodon/create-bad-signature.json create mode 100644 server/tests/api/activitypub/json/mastodon/create.json create mode 100644 server/tests/api/activitypub/json/mastodon/http-signature.json create mode 100644 server/tests/api/activitypub/json/mastodon/public-key.json create mode 100644 server/tests/api/activitypub/json/peertube/announce-without-context.json create mode 100644 server/tests/api/activitypub/json/peertube/invalid-keys.json create mode 100644 server/tests/api/activitypub/json/peertube/keys.json create mode 100644 server/tests/api/activitypub/security.ts create mode 100644 server/tests/utils/miscs/sql.ts create mode 100644 server/tests/utils/miscs/stubs.ts create mode 100644 server/tests/utils/requests/activitypub.ts diff --git a/scripts/travis.sh b/scripts/travis.sh index ae4a9f926..8f0c4a40d 100755 --- a/scripts/travis.sh +++ b/scripts/travis.sh @@ -12,7 +12,6 @@ killall -q peertube || true if [ "$1" = "misc" ]; then npm run build -- --light-fr mocha --timeout 5000 --exit --require ts-node/register/type-check --bail server/tests/client.ts \ - server/tests/activitypub.ts \ server/tests/feeds/index.ts \ server/tests/misc-endpoints.ts \ server/tests/helpers/index.ts diff --git a/server.ts b/server.ts index 51aa67638..f3514cf9c 100644 --- a/server.ts +++ b/server.ts @@ -28,7 +28,7 @@ import { checkMissedConfig, checkFFmpeg } from './server/initializers/checker-be // Do not use barrels because we don't want to load all modules here (we need to initialize database first) import { logger } from './server/helpers/logger' -import { API_VERSION, CONFIG, CACHE } from './server/initializers/constants' +import { API_VERSION, CONFIG, CACHE, HTTP_SIGNATURE } from './server/initializers/constants' const missed = checkMissedConfig() if (missed.length !== 0) { @@ -96,6 +96,7 @@ import { RemoveOldJobsScheduler } from './server/lib/schedulers/remove-old-jobs- import { UpdateVideosScheduler } from './server/lib/schedulers/update-videos-scheduler' import { YoutubeDlUpdateScheduler } from './server/lib/schedulers/youtube-dl-update-scheduler' import { VideosRedundancyScheduler } from './server/lib/schedulers/videos-redundancy-scheduler' +import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto' // ----------- Command line ----------- @@ -131,7 +132,11 @@ app.use(morgan('combined', { app.use(bodyParser.urlencoded({ extended: false })) app.use(bodyParser.json({ type: [ 'application/json', 'application/*+json' ], - limit: '500kb' + limit: '500kb', + verify: (req: express.Request, _, buf: Buffer, encoding: string) => { + const valid = isHTTPSignatureDigestValid(buf, req) + if (valid !== true) throw new Error('Invalid digest') + } })) // Cookies app.use(cookieParser()) diff --git a/server/helpers/custom-jsonld-signature.ts b/server/helpers/custom-jsonld-signature.ts index e4f28018e..27a187db1 100644 --- a/server/helpers/custom-jsonld-signature.ts +++ b/server/helpers/custom-jsonld-signature.ts @@ -1,5 +1,5 @@ import * as AsyncLRU from 'async-lru' -import * as jsonld from 'jsonld/' +import * as jsonld from 'jsonld' import * as jsig from 'jsonld-signatures' const nodeDocumentLoader = jsonld.documentLoaders.node() @@ -17,4 +17,4 @@ jsonld.documentLoader = (url, cb) => { jsig.use('jsonld', jsonld) -export { jsig } +export { jsig, jsonld } diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts index 8ef7b1359..ab9ec077e 100644 --- a/server/helpers/peertube-crypto.ts +++ b/server/helpers/peertube-crypto.ts @@ -1,9 +1,12 @@ import { Request } from 'express' import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers' import { ActorModel } from '../models/activitypub/actor' -import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey } from './core-utils' -import { jsig } from './custom-jsonld-signature' +import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey, sha256 } from './core-utils' +import { jsig, jsonld } from './custom-jsonld-signature' import { logger } from './logger' +import { cloneDeep } from 'lodash' +import { createVerify } from 'crypto' +import { buildDigest } from '../lib/job-queue/handlers/utils/activitypub-http-utils' const httpSignature = require('http-signature') @@ -30,21 +33,36 @@ async function cryptPassword (password: string) { // HTTP Signature -function isHTTPSignatureVerified (httpSignatureParsed: any, actor: ActorModel) { +function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean { + if (req.headers[HTTP_SIGNATURE.HEADER_NAME] && req.headers['digest']) { + return buildDigest(rawBody.toString()) === req.headers['digest'] + } + + return true +} + +function isHTTPSignatureVerified (httpSignatureParsed: any, actor: ActorModel): boolean { return httpSignature.verifySignature(httpSignatureParsed, actor.publicKey) === true } -function parseHTTPSignature (req: Request) { - return httpSignature.parse(req, { authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME }) +function parseHTTPSignature (req: Request, clockSkew?: number) { + return httpSignature.parse(req, { authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, clockSkew }) } // JSONLD -function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any) { +async function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any): Promise { + if (signedDocument.signature.type === 'RsaSignature2017') { + // Mastodon algorithm + const res = await isJsonLDRSA2017Verified(fromActor, signedDocument) + // Success? If no, try with our library + if (res === true) return true + } + const publicKeyObject = { '@context': jsig.SECURITY_CONTEXT_URL, id: fromActor.url, - type: 'CryptographicKey', + type: 'CryptographicKey', owner: fromActor.url, publicKeyPem: fromActor.publicKey } @@ -69,6 +87,44 @@ function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any) }) } +// Backward compatibility with "other" implementations +async function isJsonLDRSA2017Verified (fromActor: ActorModel, signedDocument: any) { + function hash (obj: any): Promise { + return jsonld.promises + .normalize(obj, { + algorithm: 'URDNA2015', + format: 'application/n-quads' + }) + .then(res => sha256(res)) + } + + const signatureCopy = cloneDeep(signedDocument.signature) + Object.assign(signatureCopy, { + '@context': [ + 'https://w3id.org/security/v1', + { RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' } + ] + }) + delete signatureCopy.type + delete signatureCopy.id + delete signatureCopy.signatureValue + + const docWithoutSignature = cloneDeep(signedDocument) + delete docWithoutSignature.signature + + const [ documentHash, optionsHash ] = await Promise.all([ + hash(docWithoutSignature), + hash(signatureCopy) + ]) + + const toVerify = optionsHash + documentHash + + const verify = createVerify('RSA-SHA256') + verify.update(toVerify, 'utf8') + + return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64') +} + function signJsonLDObject (byActor: ActorModel, data: any) { const options = { privateKeyPem: byActor.privateKey, @@ -82,6 +138,7 @@ function signJsonLDObject (byActor: ActorModel, data: any) { // --------------------------------------------------------------------------- export { + isHTTPSignatureDigestValid, parseHTTPSignature, isHTTPSignatureVerified, isJsonLDSignatureVerified, diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index 28d51068b..9aadbe824 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -535,7 +535,7 @@ const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = { const HTTP_SIGNATURE = { HEADER_NAME: 'signature', ALGORITHM: 'rsa-sha256', - HEADERS_TO_SIGN: [ 'date', 'host', 'digest', '(request-target)' ] + HEADERS_TO_SIGN: [ '(request-target)', 'host', 'date', 'digest' ] } // --------------------------------------------------------------------------- diff --git a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts index fd9c74341..4961d4502 100644 --- a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts +++ b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts @@ -38,15 +38,20 @@ async function buildSignedRequestOptions (payload: Payload) { } } -function buildGlobalHeaders (body: object) { - const digest = 'SHA-256=' + sha256(JSON.stringify(body), 'base64') - +function buildGlobalHeaders (body: any) { return { - 'Digest': digest + 'Digest': buildDigest(body) } } +function buildDigest (body: any) { + const rawBody = typeof body === 'string' ? body : JSON.stringify(body) + + return 'SHA-256=' + sha256(rawBody, 'base64') +} + export { + buildDigest, buildGlobalHeaders, computeBody, buildSignedRequestOptions diff --git a/server/middlewares/activitypub.ts b/server/middlewares/activitypub.ts index 1ec888477..01e5dd24e 100644 --- a/server/middlewares/activitypub.ts +++ b/server/middlewares/activitypub.ts @@ -53,7 +53,8 @@ function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) { export { checkSignature, - executeIfActivityPub + executeIfActivityPub, + checkHttpSignature } // --------------------------------------------------------------------------- @@ -94,7 +95,7 @@ async function checkHttpSignature (req: Request, res: Response) { async function checkJsonLDSignature (req: Request, res: Response) { const signatureObject: ActivityPubSignature = req.body.signature - if (!signatureObject.creator) { + if (!signatureObject || !signatureObject.creator) { res.sendStatus(403) return false } diff --git a/server/tests/activitypub.ts b/server/tests/api/activitypub/client.ts similarity index 92% rename from server/tests/activitypub.ts rename to server/tests/api/activitypub/client.ts index 53a04d363..5ca8bdfd3 100644 --- a/server/tests/activitypub.ts +++ b/server/tests/api/activitypub/client.ts @@ -2,7 +2,7 @@ import * as chai from 'chai' import 'mocha' -import { flushTests, killallServers, makeActivityPubGetRequest, runServer, ServerInfo, setAccessTokensToServers } from './utils' +import { flushTests, killallServers, makeActivityPubGetRequest, runServer, ServerInfo, setAccessTokensToServers } from '../../utils' const expect = chai.expect diff --git a/server/tests/api/activitypub/helpers.ts b/server/tests/api/activitypub/helpers.ts new file mode 100644 index 000000000..610846247 --- /dev/null +++ b/server/tests/api/activitypub/helpers.ts @@ -0,0 +1,182 @@ +/* tslint:disable:no-unused-expression */ + +import 'mocha' +import { expect } from 'chai' +import { buildRequestStub } from '../../utils' +import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto' +import { cloneDeep } from 'lodash' +import { buildSignedActivity } from '../../../helpers/activitypub' + +describe('Test activity pub helpers', function () { + describe('When checking the Linked Signature', function () { + + it('Should fail with an invalid Mastodon signature', async function () { + const body = require('./json/mastodon/create-bad-signature.json') + const publicKey = require('./json/mastodon/public-key.json').publicKey + const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' } + + const result = await isJsonLDSignatureVerified(fromActor as any, body) + + expect(result).to.be.false + }) + + it('Should fail with an invalid public key', async function () { + const body = require('./json/mastodon/create.json') + const publicKey = require('./json/mastodon/bad-public-key.json').publicKey + const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' } + + const result = await isJsonLDSignatureVerified(fromActor as any, body) + + expect(result).to.be.false + }) + + it('Should succeed with a valid Mastodon signature', async function () { + const body = require('./json/mastodon/create.json') + const publicKey = require('./json/mastodon/public-key.json').publicKey + const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' } + + const result = await isJsonLDSignatureVerified(fromActor as any, body) + + expect(result).to.be.true + }) + + it('Should fail with an invalid PeerTube signature', async function () { + const keys = require('./json/peertube/invalid-keys.json') + const body = require('./json/peertube/announce-without-context.json') + + const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey } + const signedBody = await buildSignedActivity(actorSignature as any, body) + + const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' } + const result = await isJsonLDSignatureVerified(fromActor as any, signedBody) + + expect(result).to.be.false + }) + + it('Should fail with an invalid PeerTube URL', async function () { + const keys = require('./json/peertube/keys.json') + const body = require('./json/peertube/announce-without-context.json') + + const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey } + const signedBody = await buildSignedActivity(actorSignature as any, body) + + const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9003/accounts/peertube' } + const result = await isJsonLDSignatureVerified(fromActor as any, signedBody) + + expect(result).to.be.false + }) + + it('Should succeed with a valid PeerTube signature', async function () { + const keys = require('./json/peertube/keys.json') + const body = require('./json/peertube/announce-without-context.json') + + const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey } + const signedBody = await buildSignedActivity(actorSignature as any, body) + + const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' } + const result = await isJsonLDSignatureVerified(fromActor as any, signedBody) + + expect(result).to.be.true + }) + }) + + describe('When checking HTTP signature', function () { + it('Should fail with an invalid http signature', async function () { + const req = buildRequestStub() + req.method = 'POST' + req.url = '/accounts/ronan/inbox' + + const mastodonObject = cloneDeep(require('./json/mastodon/bad-http-signature.json')) + req.body = mastodonObject.body + req.headers = mastodonObject.headers + req.headers.signature = 'Signature ' + req.headers.signature + + const parsed = parseHTTPSignature(req, 3600 * 365 * 3) + const publicKey = require('./json/mastodon/public-key.json').publicKey + + const actor = { publicKey } + const verified = isHTTPSignatureVerified(parsed, actor as any) + + expect(verified).to.be.false + }) + + it('Should fail with an invalid public key', async function () { + const req = buildRequestStub() + req.method = 'POST' + req.url = '/accounts/ronan/inbox' + + const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json')) + req.body = mastodonObject.body + req.headers = mastodonObject.headers + req.headers.signature = 'Signature ' + req.headers.signature + + const parsed = parseHTTPSignature(req, 3600 * 365 * 3) + const publicKey = require('./json/mastodon/bad-public-key.json').publicKey + + const actor = { publicKey } + const verified = isHTTPSignatureVerified(parsed, actor as any) + + expect(verified).to.be.false + }) + + it('Should fail because of clock skew', async function () { + const req = buildRequestStub() + req.method = 'POST' + req.url = '/accounts/ronan/inbox' + + const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json')) + req.body = mastodonObject.body + req.headers = mastodonObject.headers + req.headers.signature = 'Signature ' + req.headers.signature + + let errored = false + try { + parseHTTPSignature(req) + } catch { + errored = true + } + + expect(errored).to.be.true + }) + + it('Should fail without scheme', async function () { + const req = buildRequestStub() + req.method = 'POST' + req.url = '/accounts/ronan/inbox' + + const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json')) + req.body = mastodonObject.body + req.headers = mastodonObject.headers + + let errored = false + try { + parseHTTPSignature(req, 3600 * 365 * 3) + } catch { + errored = true + } + + expect(errored).to.be.true + }) + + it('Should succeed with a valid signature', async function () { + const req = buildRequestStub() + req.method = 'POST' + req.url = '/accounts/ronan/inbox' + + const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json')) + req.body = mastodonObject.body + req.headers = mastodonObject.headers + req.headers.signature = 'Signature ' + req.headers.signature + + const parsed = parseHTTPSignature(req, 3600 * 365 * 3) + const publicKey = require('./json/mastodon/public-key.json').publicKey + + const actor = { publicKey } + const verified = isHTTPSignatureVerified(parsed, actor as any) + + expect(verified).to.be.true + }) + + }) + +}) diff --git a/server/tests/api/activitypub/index.ts b/server/tests/api/activitypub/index.ts new file mode 100644 index 000000000..de8a59978 --- /dev/null +++ b/server/tests/api/activitypub/index.ts @@ -0,0 +1,3 @@ +import './client' +import './helpers' +import './security' diff --git a/server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json b/server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json new file mode 100644 index 000000000..4e7bc3af5 --- /dev/null +++ b/server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json @@ -0,0 +1,93 @@ +{ + "headers": { + "user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)", + "host": "localhost", + "date": "Mon, 22 Oct 2018 13:34:22 GMT", + "accept-encoding": "gzip", + "digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=", + "content-type": "application/activity+json", + "signature": "keyId=\"http://localhost:3000/users/ronan2#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"oLKbgxdFXdXsHJ3x/UsG9Svu7oa8Dyqiy6Jif4wqNuhAqRVMRaG18f+dd2OcfFX3XRGF8p8flZkU6vvoEQBauTwGRGcgXAJuKC1zYIWGk+PeiW8lNUnE4qGapWcTiFnIo7FKauNdsgqg/tvgs1pQIdHkDDjZMI64twP7sTN/4vG1PCq+kyqi/DM+ORLi/W7vFuLVHt2Iz7ikfw/R3/mMtS4FwLops+tVYBQ2iQ9DVRhTwLKVbeL/LLVB/tdGzNZ4F4nImBAQQ9I7WpPM6J/k+cBmoEbrUKs8ptx9gbX3OSsl5wlvPVMNzU9F9yb2MrB/Y/J4qssKz+LbiaktKGj7OQ==\"", + "content-length": "2815" + }, + "body": { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "movedTo": { + "@id": "as:movedTo", + "@type": "@id" + }, + "Hashtag": "as:Hashtag", + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + }, + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value" + } + ], + "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity", + "type": "Create", + "actor": "http://localhost:3000/users/ronan2", + "published": "2018-10-22T13:34:18Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "http://localhost:3000/users/ronan2/followers", + "http://localhost:9000/accounts/ronan" + ], + "object": { + "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948", + "type": "Note", + "summary": null, + "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752", + "published": "2018-10-22T13:34:18Z", + "url": "http://localhost:3000/@ronan2/100939547203370948", + "attributedTo": "http://localhost:3000/users/ronan2", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "http://localhost:3000/users/ronan2/followers", + "http://localhost:9000/accounts/ronan" + ], + "sensitive": false, + "atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948", + "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752", + "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation", + "content": "

@ronan zergzerg

", + "contentMap": { + "en": "

@ronan zergzerg

" + }, + "attachment": [], + "tag": [ + { + "type": "Mention", + "href": "http://localhost:9000/accounts/ronan", + "name": "@ronan@localhost:9000" + } + ] + }, + "signature": { + "type": "RsaSignature2017", + "creator": "http://localhost:3000/users/ronan2#main-key", + "created": "2018-10-22T13:34:19Z", + "signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q==" + } + } +} diff --git a/server/tests/api/activitypub/json/mastodon/bad-http-signature.json b/server/tests/api/activitypub/json/mastodon/bad-http-signature.json new file mode 100644 index 000000000..098597db0 --- /dev/null +++ b/server/tests/api/activitypub/json/mastodon/bad-http-signature.json @@ -0,0 +1,93 @@ +{ + "headers": { + "user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)", + "host": "localhost", + "date": "Mon, 22 Oct 2018 13:34:22 GMT", + "accept-encoding": "gzip", + "digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=", + "content-type": "application/activity+json", + "signature": "keyId=\"http://localhost:3000/users/ronan2#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"oLKbgxdFXdXsHJ3x/UsG9Svu7oa8Dyqiy6Jif4wqNuhAqRVMRaG18f+dd2OcfFX3XRGF8p8flZkU6vvoEQBauTwGRGcgXAJuKC1zYIWGk+PeiW8lNUnE4qGapWcTiFnIo7FKauNdsgqg/tvgs1pQIdHkDDjZMI64twP7sTN/4vG1PCq+kyqi/DM+ORLi/W7vFuLVHt2Iz7ikfw/R3/mMtS4FwLops+tVYBQ2iQ9DVRhTwLKVbeL/LLVB/tdGzNZ4F4nImBAQQ9I7WpPM6J/k+cBmoEbrUKs8ptx9gbX3OSsl4wlvPVMNzU9F9yb2MrB/Y/J4qssKz+LbiaktKGj7OQ==\"", + "content-length": "2815" + }, + "body": { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "movedTo": { + "@id": "as:movedTo", + "@type": "@id" + }, + "Hashtag": "as:Hashtag", + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + }, + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value" + } + ], + "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity", + "type": "Create", + "actor": "http://localhost:3000/users/ronan2", + "published": "2018-10-22T13:34:18Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "http://localhost:3000/users/ronan2/followers", + "http://localhost:9000/accounts/ronan" + ], + "object": { + "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948", + "type": "Note", + "summary": null, + "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752", + "published": "2018-10-22T13:34:18Z", + "url": "http://localhost:3000/@ronan2/100939547203370948", + "attributedTo": "http://localhost:3000/users/ronan2", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "http://localhost:3000/users/ronan2/followers", + "http://localhost:9000/accounts/ronan" + ], + "sensitive": false, + "atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948", + "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752", + "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation", + "content": "

@ronan zergzerg

", + "contentMap": { + "en": "

@ronan zergzerg

" + }, + "attachment": [], + "tag": [ + { + "type": "Mention", + "href": "http://localhost:9000/accounts/ronan", + "name": "@ronan@localhost:9000" + } + ] + }, + "signature": { + "type": "RsaSignature2017", + "creator": "http://localhost:3000/users/ronan2#main-key", + "created": "2018-10-22T13:34:19Z", + "signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q==" + } + } +} diff --git a/server/tests/api/activitypub/json/mastodon/bad-public-key.json b/server/tests/api/activitypub/json/mastodon/bad-public-key.json new file mode 100644 index 000000000..73d18b3ad --- /dev/null +++ b/server/tests/api/activitypub/json/mastodon/bad-public-key.json @@ -0,0 +1,3 @@ +{ + "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0YyuthHtWWgDe0Fdgdp2\ndC5dTJsRqW6pFw5omIYYYjoES/WRewhVxEA54BhmxD3L1zChfx131N1TS8jVowhW\nm999jpUffKCCvLgYKIXETJDHiDeMONVx8wp7v9fS1HiFXo/E5und39gUMs14CMFZ\n6PE5jRV3r4XIKQJHQl7/X5n5FOb2934K+1TKUeBkbft/AushlKatYQakt3qHxpwx\nFvE+JjGo7QTnzdjaOx/e5QvojdGi2Kx4+jl77j2WVcSo5lOBz04OAVJtChtn82vS\nulPdDh3hZcDn+WK67yAhGP6AnzvOybZZS4zowlKiQ3kqjVVXKdl8gAsL4Y7MZ40R\nJQIDAQAB\n-----END PUBLIC KEY-----\n" +} diff --git a/server/tests/api/activitypub/json/mastodon/create-bad-signature.json b/server/tests/api/activitypub/json/mastodon/create-bad-signature.json new file mode 100644 index 000000000..2cd037241 --- /dev/null +++ b/server/tests/api/activitypub/json/mastodon/create-bad-signature.json @@ -0,0 +1,81 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "movedTo": { + "@id": "as:movedTo", + "@type": "@id" + }, + "Hashtag": "as:Hashtag", + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + }, + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value" + } + ], + "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698/activity", + "type": "Create", + "actor": "http://localhost:3000/users/ronan2", + "published": "2018-10-22T12:43:07Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "http://localhost:3000/users/ronan2/followers", + "http://localhost:9000/accounts/ronan" + ], + "object": { + "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698", + "type": "Note", + "summary": null, + "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752", + "published": "2018-10-22T12:43:07Z", + "url": "http://localhost:3000/@ronan2/100939345950887698", + "attributedTo": "http://localhost:3000/users/ronan2", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "http://localhost:3000/users/ronan2/followers", + "http://localhost:9000/accounts/ronan" + ], + "sensitive": false, + "atomUri": "http://localhost:3000/users/ronan2/statuses/100939345950887698", + "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752", + "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation", + "content": "

@ronan zerg

", + "contentMap": { + "en": "

@ronan zerg

" + }, + "attachment": [], + "tag": [ + { + "type": "Mention", + "href": "http://localhost:9000/accounts/ronan", + "name": "@ronan@localhost:9000" + } + ] + }, + "signature": { + "type": "RsaSignature2017", + "creator": "http://localhost:3000/users/ronan2#main-key", + "created": "2018-10-22T12:43:08Z", + "signatureValue": "Vgr8nA0agPr9TcA4BlX+MWhmuE+rBcoIJLpnPbm3E5SnOCXbgjEfEaTLqfuzzkKNsR3PBbkvi3YWK4/DxJ0zmpzSB7yy4NRzluQMVQHqJiFKXAX3Sr3fIrK24xkWW9/F207c1NpFajSGbgnFKBdtFE0e5VqwSrSoOJkZukZW/2ATSnsyzblieuUmvTWpD0PqpUOsynPjw+RqZnqPn0cjw1z2Dm7ZRt3trnyMTXFYZw5U/YuqMY2kpadD6vq780md8kXlJIylxG6ZrlO2jz9fJdnfuVq43d4QFNsBm1K1r2WtNqX+i+wiqh+u3PjF4pzXtl/a3hJOH18IfZnK7I21mQ==" + } +} diff --git a/server/tests/api/activitypub/json/mastodon/create.json b/server/tests/api/activitypub/json/mastodon/create.json new file mode 100644 index 000000000..0be271bb8 --- /dev/null +++ b/server/tests/api/activitypub/json/mastodon/create.json @@ -0,0 +1,81 @@ +{ + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "movedTo": { + "@id": "as:movedTo", + "@type": "@id" + }, + "Hashtag": "as:Hashtag", + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + }, + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value" + } + ], + "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698/activity", + "type": "Create", + "actor": "http://localhost:3000/users/ronan2", + "published": "2018-10-22T12:43:07Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "http://localhost:3000/users/ronan2/followers", + "http://localhost:9000/accounts/ronan" + ], + "object": { + "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698", + "type": "Note", + "summary": null, + "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752", + "published": "2018-10-22T12:43:07Z", + "url": "http://localhost:3000/@ronan2/100939345950887698", + "attributedTo": "http://localhost:3000/users/ronan2", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "http://localhost:3000/users/ronan2/followers", + "http://localhost:9000/accounts/ronan" + ], + "sensitive": false, + "atomUri": "http://localhost:3000/users/ronan2/statuses/100939345950887698", + "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752", + "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation", + "content": "

@ronan zerg

", + "contentMap": { + "en": "

@ronan zerg

" + }, + "attachment": [], + "tag": [ + { + "type": "Mention", + "href": "http://localhost:9000/accounts/ronan", + "name": "@ronan@localhost:9000" + } + ] + }, + "signature": { + "type": "RsaSignature2017", + "creator": "http://localhost:3000/users/ronan2#main-key", + "created": "2018-10-22T12:43:08Z", + "signatureValue": "VgR8nA0agPr9TcA4BlX+MWhmuE+rBcoIJLpnPbm3E5SnOCXbgjEfEaTLqfuzzkKNsR3PBbkvi3YWK4/DxJ0zmpzSB7yy4NRzluQMVQHqJiFKXAX3Sr3fIrK24xkWW9/F207c1NpFajSGbgnFKBdtFE0e5VqwSrSoOJkZukZW/2ATSnsyzblieuUmvTWpD0PqpUOsynPjw+RqZnqPn0cjw1z2Dm7ZRt3trnyMTXFYZw5U/YuqMY2kpadD6vq780md8kXlJIylxG6ZrlO2jz9fJdnfuVq43d4QFNsBm1K1r2WtNqX+i+wiqh+u3PjF4pzXtl/a3hJOH18IfZnK7I21mQ==" + } +} diff --git a/server/tests/api/activitypub/json/mastodon/http-signature.json b/server/tests/api/activitypub/json/mastodon/http-signature.json new file mode 100644 index 000000000..4e7bc3af5 --- /dev/null +++ b/server/tests/api/activitypub/json/mastodon/http-signature.json @@ -0,0 +1,93 @@ +{ + "headers": { + "user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)", + "host": "localhost", + "date": "Mon, 22 Oct 2018 13:34:22 GMT", + "accept-encoding": "gzip", + "digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=", + "content-type": "application/activity+json", + "signature": "keyId=\"http://localhost:3000/users/ronan2#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"oLKbgxdFXdXsHJ3x/UsG9Svu7oa8Dyqiy6Jif4wqNuhAqRVMRaG18f+dd2OcfFX3XRGF8p8flZkU6vvoEQBauTwGRGcgXAJuKC1zYIWGk+PeiW8lNUnE4qGapWcTiFnIo7FKauNdsgqg/tvgs1pQIdHkDDjZMI64twP7sTN/4vG1PCq+kyqi/DM+ORLi/W7vFuLVHt2Iz7ikfw/R3/mMtS4FwLops+tVYBQ2iQ9DVRhTwLKVbeL/LLVB/tdGzNZ4F4nImBAQQ9I7WpPM6J/k+cBmoEbrUKs8ptx9gbX3OSsl5wlvPVMNzU9F9yb2MrB/Y/J4qssKz+LbiaktKGj7OQ==\"", + "content-length": "2815" + }, + "body": { + "@context": [ + "https://www.w3.org/ns/activitystreams", + "https://w3id.org/security/v1", + { + "manuallyApprovesFollowers": "as:manuallyApprovesFollowers", + "sensitive": "as:sensitive", + "movedTo": { + "@id": "as:movedTo", + "@type": "@id" + }, + "Hashtag": "as:Hashtag", + "ostatus": "http://ostatus.org#", + "atomUri": "ostatus:atomUri", + "inReplyToAtomUri": "ostatus:inReplyToAtomUri", + "conversation": "ostatus:conversation", + "toot": "http://joinmastodon.org/ns#", + "Emoji": "toot:Emoji", + "focalPoint": { + "@container": "@list", + "@id": "toot:focalPoint" + }, + "featured": { + "@id": "toot:featured", + "@type": "@id" + }, + "schema": "http://schema.org#", + "PropertyValue": "schema:PropertyValue", + "value": "schema:value" + } + ], + "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity", + "type": "Create", + "actor": "http://localhost:3000/users/ronan2", + "published": "2018-10-22T13:34:18Z", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "http://localhost:3000/users/ronan2/followers", + "http://localhost:9000/accounts/ronan" + ], + "object": { + "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948", + "type": "Note", + "summary": null, + "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752", + "published": "2018-10-22T13:34:18Z", + "url": "http://localhost:3000/@ronan2/100939547203370948", + "attributedTo": "http://localhost:3000/users/ronan2", + "to": [ + "https://www.w3.org/ns/activitystreams#Public" + ], + "cc": [ + "http://localhost:3000/users/ronan2/followers", + "http://localhost:9000/accounts/ronan" + ], + "sensitive": false, + "atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948", + "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752", + "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation", + "content": "

@ronan zergzerg

", + "contentMap": { + "en": "

@ronan zergzerg

" + }, + "attachment": [], + "tag": [ + { + "type": "Mention", + "href": "http://localhost:9000/accounts/ronan", + "name": "@ronan@localhost:9000" + } + ] + }, + "signature": { + "type": "RsaSignature2017", + "creator": "http://localhost:3000/users/ronan2#main-key", + "created": "2018-10-22T13:34:19Z", + "signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q==" + } + } +} diff --git a/server/tests/api/activitypub/json/mastodon/public-key.json b/server/tests/api/activitypub/json/mastodon/public-key.json new file mode 100644 index 000000000..b7b9b8308 --- /dev/null +++ b/server/tests/api/activitypub/json/mastodon/public-key.json @@ -0,0 +1,3 @@ +{ + "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0YyuthHtWWgDe0Fdgdp2\ndC5dTJsRqW6pFw5omIYYYjoES/WRewhVxEA54BhmxD3L1zChfx131N1TS8jVowhW\nm999jpUffKCCvLgYKIXETJDHiDeMONVx8wp7v9fS1HiFXo/E5und39gUMs14CMFZ\n6PE5jRV3r4XIKQJHQl7/X5n5FOb2934K+1TKUeBkbft/AushlKatYQakt3qHxpwx\nFvE+JjGo7QTnzdjaOx/e5QvojdGi2Kx4+jl87j2WVcSo5lOBz04OAVJtChtn82vS\nulPdDh3hZcDn+WK67yAhGP6AnzvOybZZS4zowlKiQ3kqjVVXKdl8gAsL4Y7MZ40R\nJQIDAQAB\n-----END PUBLIC KEY-----\n" +} diff --git a/server/tests/api/activitypub/json/peertube/announce-without-context.json b/server/tests/api/activitypub/json/peertube/announce-without-context.json new file mode 100644 index 000000000..5f2af0cde --- /dev/null +++ b/server/tests/api/activitypub/json/peertube/announce-without-context.json @@ -0,0 +1,13 @@ +{ + "type": "Announce", + "id": "http://localhost:9002/videos/watch/997111d4-e8d8-4f45-99d3-857905785d05/announces/1", + "actor": "http://localhost:9002/accounts/peertube", + "object": "http://localhost:9002/videos/watch/997111d4-e8d8-4f45-99d3-857905785d05", + "to": [ + "https://www.w3.org/ns/activitystreams#Public", + "http://localhost:9002/accounts/peertube/followers", + "http://localhost:9002/video-channels/root_channel/followers", + "http://localhost:9002/accounts/root/followers" + ], + "cc": [] +} diff --git a/server/tests/api/activitypub/json/peertube/invalid-keys.json b/server/tests/api/activitypub/json/peertube/invalid-keys.json new file mode 100644 index 000000000..0544e96b9 --- /dev/null +++ b/server/tests/api/activitypub/json/peertube/invalid-keys.json @@ -0,0 +1,6 @@ +{ + "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqjQGdH6D3naKmSbbr/Df\nEh1H42F3WlHYXuxKLkm5Bemjdde+GwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYO\nwAyc3Zoy7afPNa4bZXqhJ1Im41rMGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55s\nIkczDkseJuadTvG+A1e4uNY2lnRmVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/F\npP5S75TS5l1DfJQIq2lp8RwrH6FvGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM\n7mS7eP8zF8lKXYUu8cjIscKm+XqGmyRoPyw2Pp53tew29idRUocVQHGBnlNbpKdd\naQIDAQAB\n-----END PUBLIC KEY-----\n", + "privateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAqjQGdH6D3naKmSbbr/DfEh1H42F3WlHYXuxKLkm5Bemjdde+\nGwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYOwAyc3Zoy7afPNa4bZXqhJ1Im41rM\nGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55sIkczDkseJuadTvG+A1e4uNY2lnRm\nVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/FpP5S75TS5l1DfJQIq2lp8RwrH6Fv\nGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM7mS7eP8zF8lKXYUu8cjIscKm+XqG\nmyRoPyw3Pp53tew29idRUocVQHGBnlNbpKddaQIDAQABAoIBAQCnBZawCtbtH/ay\ng+dhqEW/SOyavbKZ92cU/1tsQPxISRYXNjdf2VfK7HmVqC2S7NqBanz+AVZPHmda\n7OfamkSvQbFN5VvEy8ATNV+9HbG3HG78/MT9hZcGigmyJkcZuy4wILgoXCxfpxlD\netla60PB/4yioiRcmEIWjjOgpByphDJ7RuuuptyEvgjUjpPtvHK47O/loaD2HFJk\nbIYbRirbjUjITRjQxGVIvanqiwPG9pB26YDLxDOoXEumcnzRcEFWNdvoleaLgquS\nn/zVsXWEq4+1i7t44DDstWUt/2Bw5ksIkSdayQ6oy3vzre3YFHwvbVZ7qtQQgpru\nx+NIolZhAoGBAN1RgNj8zy9Py3SJdsoXtnuCItfD7eo7LWXUa06cM/NS695Q+/to\naa5i3cJnRlv+b+b3VvnhkhIBLfFQW+hWwPnnxJEehcm09ddN9zbWrZ4Yv9yYu+8d\nTLGyWL8kPFF1dz+29DcrSv3tXEOwxByX/O4U/X/i3wl2WhkybxVFnCuvAoGBAMTf\n91BgLzvcYKOxH+vRPOJY7g2HKGFe35R91M4E+9Eq1rq4LUQHBb3fhRh4+scNu0yb\nNfN1Zdx2nbgCXdTKomF1Ahxp58/A2iU65vVzL6hYfWXEGSmoBqsGCIpIxQ9jgB9k\nCl7t/Ban8Z/ORHTjI9fpHlSZyCWJ3ajepiM2a1ZnAoGAPpDO6wi1DXvyWVSPF1yS\nwuGsNfD2rjPihpoBZ+yypwP3GBcu1QjUb28Vn+KQOmt4eQPNO8DwCVT6BvEfulPk\nJAHISPom+jnFEgPBcmhIFpyKiLNI1bUjvExd2FNHFgQuHP38ligQAC782Un8dtTk\ntO2MKH4bbVJe8CaYzpuqJZMCgYABZyMpBHZxs8FQiUuT75rCdiXEHOlxwC5RrY/d\no/VzaR28mOFhsbcdwkD9iqcm0fc6tYRt5rFCH+pBzGqEwKjljuLj9vE67sHfMAtD\nRn3Zcj/6gKo5PMRHZbSb36bf1DKuhpT4VjPMqYe0PtEIEDJKMJQRwELH2bKlqGiA\nqbucEwKBgQCkS85JnpHEV/tSylsEEn2W3CQCx58zl7iZNV7h/tWMR4AyrcI0HqP6\nllJ7V/Cfw66MgelPnosKgagwLVI6gsqDtjnzYo3XuMRVlYIySJ/jV3eiUNkV2Ky2\nfp/gA9sVgp38QSr+xB9E0LNStcbqDzoCCcDRws/SK7PbkQH9KV47tQ==\n-----END RSA PRIVATE KEY-----" +} + + diff --git a/server/tests/api/activitypub/json/peertube/keys.json b/server/tests/api/activitypub/json/peertube/keys.json new file mode 100644 index 000000000..1a7700865 --- /dev/null +++ b/server/tests/api/activitypub/json/peertube/keys.json @@ -0,0 +1,4 @@ +{ + "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqjQGdH6D3naKmSbbr/Df\nEh1H42F3WlHYXuxKLkm5Bemjdde+GwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYO\nwAyc3Zoy7afPNa4bZXqhJ1Im41rMGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55s\nIkczDkseJuadTvG+A1e4uNY2lnRmVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/F\npP5S75TS5l1DfJQIq2lp8RwrH6FvGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM\n7mS7eP8zF8lKXYUu8cjIscKm+XqGmyRoPyw3Pp53tew29idRUocVQHGBnlNbpKdd\naQIDAQAB\n-----END PUBLIC KEY-----\n", + "privateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAqjQGdH6D3naKmSbbr/DfEh1H42F3WlHYXuxKLkm5Bemjdde+\nGwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYOwAyc3Zoy7afPNa4bZXqhJ1Im41rM\nGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55sIkczDkseJuadTvG+A1e4uNY2lnRm\nVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/FpP5S75TS5l1DfJQIq2lp8RwrH6Fv\nGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM7mS7eP8zF8lKXYUu8cjIscKm+XqG\nmyRoPyw3Pp53tew29idRUocVQHGBnlNbpKddaQIDAQABAoIBAQCnBZawCtbtH/ay\ng+dhqEW/SOyavbKZ92cU/1tsQPxISRYXNjdf2VfK7HmVqC2S7NqBanz+AVZPHmda\n7OfamkSvQbFN5VvEy8ATNV+9HbG3HG78/MT9hZcGigmyJkcZuy4wILgoXCxfpxlD\netla60PB/4yioiRcmEIWjjOgpByphDJ7RuuuptyEvgjUjpPtvHK47O/loaD2HFJk\nbIYbRirbjUjITRjQxGVIvanqiwPG9pB26YDLxDOoXEumcnzRcEFWNdvoleaLgquS\nn/zVsXWEq4+1i7t44DDstWUt/2Bw5ksIkSdayQ6oy3vzre3YFHwvbVZ7qtQQgpru\nx+NIolZhAoGBAN1RgNj8zy9Py3SJdsoXtnuCItfD7eo7LWXUa06cM/NS695Q+/to\naa5i3cJnRlv+b+b3VvnhkhIBLfFQW+hWwPnnxJEehcm09ddN9zbWrZ4Yv9yYu+8d\nTLGyWL8kPFF1dz+29DcrSv3tXEOwxByX/O4U/X/i3wl2WhkybxVFnCuvAoGBAMTf\n91BgLzvcYKOxH+vRPOJY7g2HKGFe35R91M4E+9Eq1rq4LUQHBb3fhRh4+scNu0yb\nNfN1Zdx2nbgCXdTKomF1Ahxp58/A2iU65vVzL6hYfWXEGSmoBqsGCIpIxQ9jgB9k\nCl7t/Ban8Z/ORHTjI9fpHlSZyCWJ3ajepiM2a1ZnAoGAPpDO6wi1DXvyWVSPF1yS\nwuGsNfD2rjPihpoBZ+yypwP3GBcu1QjUb28Vn+KQOmt4eQPNO8DwCVT6BvEfulPk\nJAHISPom+jnFEgPBcmhIFpyKiLNI1bUjvExd2FNHFgQuHP38ligQAC782Un8dtTk\ntO2MKH4bbVJe8CaYzpuqJZMCgYABZyMpBHZxs8FQiUuT75rCdiXEHOlxwC5RrY/d\no/VzaR28mOFhsbcdwkD9iqcm0fc6tYRt5rFCH+pBzGqEwKjljuLj9vE67sHfMAtD\nRn3Zcj/6gKo5PMRHZbSb36bf1DKuhpT4VjPMqYe0PtEIEDJKMJQRwELH2bKlqGiA\nqbucEwKBgQCkS85JnpHEV/tSylsEEn2W3CQCx58zl7iZNV7h/tWMR4AyrcI0HqP6\nllJ7V/Cfw66MgelPnosKgagwLVI6gsqDtjnzYo3XuMRVlYIySJ/jV3eiUNkV2Ky2\nfp/gA9sVgp38QSr+xB9E0LNStcbqDzoCCcDRws/SK7PbkQH9KV47tQ==\n-----END RSA PRIVATE KEY-----" +} diff --git a/server/tests/api/activitypub/security.ts b/server/tests/api/activitypub/security.ts new file mode 100644 index 000000000..c5428abbb --- /dev/null +++ b/server/tests/api/activitypub/security.ts @@ -0,0 +1,180 @@ +/* tslint:disable:no-unused-expression */ + +import 'mocha' + +import { flushAndRunMultipleServers, flushTests, killallServers, makeAPRequest, makeFollowRequest, ServerInfo } from '../../utils' +import { HTTP_SIGNATURE } from '../../../initializers' +import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils' +import * as chai from 'chai' +import { setActorField } from '../../utils/miscs/sql' +import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub' + +const expect = chai.expect + +function setKeysOfServer2 (serverNumber: number, publicKey: string, privateKey: string) { + return Promise.all([ + setActorField(serverNumber, 'http://localhost:9002/accounts/peertube', 'publicKey', publicKey), + setActorField(serverNumber, 'http://localhost:9002/accounts/peertube', 'privateKey', privateKey) + ]) +} + +function setKeysOfServer3 (serverNumber: number, publicKey: string, privateKey: string) { + return Promise.all([ + setActorField(serverNumber, 'http://localhost:9003/accounts/peertube', 'publicKey', publicKey), + setActorField(serverNumber, 'http://localhost:9003/accounts/peertube', 'privateKey', privateKey) + ]) +} + +describe('Test ActivityPub security', function () { + let servers: ServerInfo[] + let url: string + + const keys = require('./json/peertube/keys.json') + const invalidKeys = require('./json/peertube/invalid-keys.json') + const baseHttpSignature = { + algorithm: HTTP_SIGNATURE.ALGORITHM, + authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, + keyId: 'acct:peertube@localhost:9002', + key: keys.privateKey, + headers: HTTP_SIGNATURE.HEADERS_TO_SIGN + } + + // --------------------------------------------------------------- + + before(async function () { + this.timeout(60000) + + servers = await flushAndRunMultipleServers(3) + + url = servers[0].url + '/inbox' + + await setKeysOfServer2(1, keys.publicKey, keys.privateKey) + + const to = { url: 'http://localhost:9001/accounts/peertube' } + const by = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey } + await makeFollowRequest(to, by) + }) + + describe('When checking HTTP signature', function () { + + it('Should fail with an invalid digest', async function () { + const body = activityPubContextify(require('./json/peertube/announce-without-context.json')) + const headers = { + Digest: buildDigest({ hello: 'coucou' }) + } + + const { response } = await makeAPRequest(url, body, baseHttpSignature, headers) + + expect(response.statusCode).to.equal(403) + }) + + it('Should fail with an invalid date', async function () { + const body = activityPubContextify(require('./json/peertube/announce-without-context.json')) + const headers = buildGlobalHeaders(body) + headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT' + + const { response } = await makeAPRequest(url, body, baseHttpSignature, headers) + + expect(response.statusCode).to.equal(403) + }) + + it('Should fail with bad keys', async function () { + await setKeysOfServer2(1, invalidKeys.publicKey, invalidKeys.privateKey) + await setKeysOfServer2(2, invalidKeys.publicKey, invalidKeys.privateKey) + + const body = activityPubContextify(require('./json/peertube/announce-without-context.json')) + const headers = buildGlobalHeaders(body) + + const { response } = await makeAPRequest(url, body, baseHttpSignature, headers) + + expect(response.statusCode).to.equal(403) + }) + + it('Should succeed with a valid HTTP signature', async function () { + await setKeysOfServer2(1, keys.publicKey, keys.privateKey) + await setKeysOfServer2(2, keys.publicKey, keys.privateKey) + + const body = activityPubContextify(require('./json/peertube/announce-without-context.json')) + const headers = buildGlobalHeaders(body) + + const { response } = await makeAPRequest(url, body, baseHttpSignature, headers) + + expect(response.statusCode).to.equal(204) + }) + }) + + describe('When checking Linked Data Signature', function () { + before(async () => { + await setKeysOfServer3(3, keys.publicKey, keys.privateKey) + + const to = { url: 'http://localhost:9001/accounts/peertube' } + const by = { url: 'http://localhost:9003/accounts/peertube', privateKey: keys.privateKey } + await makeFollowRequest(to, by) + }) + + it('Should fail with bad keys', async function () { + this.timeout(10000) + + await setKeysOfServer3(1, invalidKeys.publicKey, invalidKeys.privateKey) + await setKeysOfServer3(3, invalidKeys.publicKey, invalidKeys.privateKey) + + const body = require('./json/peertube/announce-without-context.json') + body.actor = 'http://localhost:9003/accounts/peertube' + + const signer: any = { privateKey: invalidKeys.privateKey, url: 'http://localhost:9003/accounts/peertube' } + const signedBody = await buildSignedActivity(signer, body) + + const headers = buildGlobalHeaders(signedBody) + + const { response } = await makeAPRequest(url, signedBody, baseHttpSignature, headers) + + expect(response.statusCode).to.equal(403) + }) + + it('Should fail with an altered body', async function () { + this.timeout(10000) + + await setKeysOfServer3(1, keys.publicKey, keys.privateKey) + await setKeysOfServer3(3, keys.publicKey, keys.privateKey) + + const body = require('./json/peertube/announce-without-context.json') + body.actor = 'http://localhost:9003/accounts/peertube' + + const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:9003/accounts/peertube' } + const signedBody = await buildSignedActivity(signer, body) + + signedBody.actor = 'http://localhost:9003/account/peertube' + + const headers = buildGlobalHeaders(signedBody) + + const { response } = await makeAPRequest(url, signedBody, baseHttpSignature, headers) + + expect(response.statusCode).to.equal(403) + }) + + it('Should succeed with a valid signature', async function () { + this.timeout(10000) + + const body = require('./json/peertube/announce-without-context.json') + body.actor = 'http://localhost:9003/accounts/peertube' + + const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:9003/accounts/peertube' } + const signedBody = await buildSignedActivity(signer, body) + + const headers = buildGlobalHeaders(signedBody) + + const { response } = await makeAPRequest(url, signedBody, baseHttpSignature, headers) + + expect(response.statusCode).to.equal(204) + }) + }) + + after(async function () { + killallServers(servers) + + // Keep the logs if the test failed + if (this['ok']) { + await flushTests() + } + }) +}) diff --git a/server/tests/api/index-4.ts b/server/tests/api/index-4.ts index 8e69b95a6..7d8be2b3d 100644 --- a/server/tests/api/index-4.ts +++ b/server/tests/api/index-4.ts @@ -1 +1,2 @@ import './redundancy' +import './activitypub' diff --git a/server/tests/index.ts b/server/tests/index.ts index e659fd3df..ed16d65dd 100644 --- a/server/tests/index.ts +++ b/server/tests/index.ts @@ -1,6 +1,5 @@ // Order of the tests we want to execute import './client' -import './activitypub' import './feeds/' import './cli/' import './api/' diff --git a/server/tests/utils/index.ts b/server/tests/utils/index.ts index 897389824..905d93823 100644 --- a/server/tests/utils/index.ts +++ b/server/tests/utils/index.ts @@ -4,8 +4,10 @@ export * from './server/clients' export * from './server/config' export * from './users/login' export * from './miscs/miscs' +export * from './miscs/stubs' export * from './server/follows' export * from './requests/requests' +export * from './requests/activitypub' export * from './server/servers' export * from './videos/services' export * from './users/users' diff --git a/server/tests/utils/miscs/sql.ts b/server/tests/utils/miscs/sql.ts new file mode 100644 index 000000000..204ff5163 --- /dev/null +++ b/server/tests/utils/miscs/sql.ts @@ -0,0 +1,29 @@ +import * as Sequelize from 'sequelize' + +function getSequelize (serverNumber: number) { + const dbname = 'peertube_test' + serverNumber + const username = 'peertube' + const password = 'peertube' + const host = 'localhost' + const port = 5432 + + return new Sequelize(dbname, username, password, { + dialect: 'postgres', + host, + port, + operatorsAliases: false, + logging: false + }) +} + +function setActorField (serverNumber: number, to: string, field: string, value: string) { + const seq = getSequelize(serverNumber) + + const options = { type: Sequelize.QueryTypes.UPDATE } + + return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options) +} + +export { + setActorField +} diff --git a/server/tests/utils/miscs/stubs.ts b/server/tests/utils/miscs/stubs.ts new file mode 100644 index 000000000..d1eb0e3b2 --- /dev/null +++ b/server/tests/utils/miscs/stubs.ts @@ -0,0 +1,14 @@ +function buildRequestStub (): any { + return { } +} + +function buildResponseStub (): any { + return { + locals: {} + } +} + +export { + buildResponseStub, + buildRequestStub +} diff --git a/server/tests/utils/requests/activitypub.ts b/server/tests/utils/requests/activitypub.ts new file mode 100644 index 000000000..e3e08ce67 --- /dev/null +++ b/server/tests/utils/requests/activitypub.ts @@ -0,0 +1,43 @@ +import { doRequest } from '../../../helpers/requests' +import { HTTP_SIGNATURE } from '../../../initializers' +import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils' +import { activityPubContextify } from '../../../helpers/activitypub' + +function makeAPRequest (url: string, body: any, httpSignature: any, headers: any) { + const options = { + method: 'POST', + uri: url, + json: body, + httpSignature, + headers + } + + return doRequest(options) +} + +async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) { + const follow = { + type: 'Follow', + id: by.url + '/toto', + actor: by.url, + object: to.url + } + + const body = activityPubContextify(follow) + + const httpSignature = { + algorithm: HTTP_SIGNATURE.ALGORITHM, + authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, + keyId: by.url, + key: by.privateKey, + headers: HTTP_SIGNATURE.HEADERS_TO_SIGN + } + const headers = buildGlobalHeaders(body) + + return makeAPRequest(to.url, body, httpSignature, headers) +} + +export { + makeAPRequest, + makeFollowRequest +} diff --git a/yarn.lock b/yarn.lock index 2478a0664..6aeb87e3f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5054,7 +5054,7 @@ jsonify@~0.0.0: jsonld@^0.5.12: version "0.5.21" - resolved "http://registry.npmjs.org/jsonld/-/jsonld-0.5.21.tgz#4d5b78d717eb92bcd1ac9d88e34efad95370c0bf" + resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-0.5.21.tgz#4d5b78d717eb92bcd1ac9d88e34efad95370c0bf" integrity sha512-1dQhaw1Eb3p7Cz5ECE2DNPwLvTmK+f6D45hACBdonJaFKP1bN9zlKLZWbPZQeZtduAc/LNv10J4ML0IiTBVahw== dependencies: rdf-canonize "^0.2.1" -- 2.41.0