aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/helpers/custom-jsonld-signature.ts4
-rw-r--r--server/helpers/peertube-crypto.ts71
-rw-r--r--server/initializers/constants.ts2
-rw-r--r--server/lib/job-queue/handlers/utils/activitypub-http-utils.ts13
-rw-r--r--server/middlewares/activitypub.ts5
-rw-r--r--server/tests/api/activitypub/client.ts (renamed from server/tests/activitypub.ts)2
-rw-r--r--server/tests/api/activitypub/helpers.ts182
-rw-r--r--server/tests/api/activitypub/index.ts3
-rw-r--r--server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json93
-rw-r--r--server/tests/api/activitypub/json/mastodon/bad-http-signature.json93
-rw-r--r--server/tests/api/activitypub/json/mastodon/bad-public-key.json3
-rw-r--r--server/tests/api/activitypub/json/mastodon/create-bad-signature.json81
-rw-r--r--server/tests/api/activitypub/json/mastodon/create.json81
-rw-r--r--server/tests/api/activitypub/json/mastodon/http-signature.json93
-rw-r--r--server/tests/api/activitypub/json/mastodon/public-key.json3
-rw-r--r--server/tests/api/activitypub/json/peertube/announce-without-context.json13
-rw-r--r--server/tests/api/activitypub/json/peertube/invalid-keys.json6
-rw-r--r--server/tests/api/activitypub/json/peertube/keys.json4
-rw-r--r--server/tests/api/activitypub/security.ts180
-rw-r--r--server/tests/api/index-4.ts1
-rw-r--r--server/tests/index.ts1
-rw-r--r--server/tests/utils/index.ts2
-rw-r--r--server/tests/utils/miscs/sql.ts29
-rw-r--r--server/tests/utils/miscs/stubs.ts14
-rw-r--r--server/tests/utils/requests/activitypub.ts43
25 files changed, 1004 insertions, 18 deletions
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 @@
1import * as AsyncLRU from 'async-lru' 1import * as AsyncLRU from 'async-lru'
2import * as jsonld from 'jsonld/' 2import * as jsonld from 'jsonld'
3import * as jsig from 'jsonld-signatures' 3import * as jsig from 'jsonld-signatures'
4 4
5const nodeDocumentLoader = jsonld.documentLoaders.node() 5const nodeDocumentLoader = jsonld.documentLoaders.node()
@@ -17,4 +17,4 @@ jsonld.documentLoader = (url, cb) => {
17 17
18jsig.use('jsonld', jsonld) 18jsig.use('jsonld', jsonld)
19 19
20export { jsig } 20export { 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 @@
1import { Request } from 'express' 1import { Request } from 'express'
2import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers' 2import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers'
3import { ActorModel } from '../models/activitypub/actor' 3import { ActorModel } from '../models/activitypub/actor'
4import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey } from './core-utils' 4import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey, sha256 } from './core-utils'
5import { jsig } from './custom-jsonld-signature' 5import { jsig, jsonld } from './custom-jsonld-signature'
6import { logger } from './logger' 6import { logger } from './logger'
7import { cloneDeep } from 'lodash'
8import { createVerify } from 'crypto'
9import { buildDigest } from '../lib/job-queue/handlers/utils/activitypub-http-utils'
7 10
8const httpSignature = require('http-signature') 11const httpSignature = require('http-signature')
9 12
@@ -30,21 +33,36 @@ async function cryptPassword (password: string) {
30 33
31// HTTP Signature 34// HTTP Signature
32 35
33function isHTTPSignatureVerified (httpSignatureParsed: any, actor: ActorModel) { 36function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean {
37 if (req.headers[HTTP_SIGNATURE.HEADER_NAME] && req.headers['digest']) {
38 return buildDigest(rawBody.toString()) === req.headers['digest']
39 }
40
41 return true
42}
43
44function isHTTPSignatureVerified (httpSignatureParsed: any, actor: ActorModel): boolean {
34 return httpSignature.verifySignature(httpSignatureParsed, actor.publicKey) === true 45 return httpSignature.verifySignature(httpSignatureParsed, actor.publicKey) === true
35} 46}
36 47
37function parseHTTPSignature (req: Request) { 48function parseHTTPSignature (req: Request, clockSkew?: number) {
38 return httpSignature.parse(req, { authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME }) 49 return httpSignature.parse(req, { authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, clockSkew })
39} 50}
40 51
41// JSONLD 52// JSONLD
42 53
43function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any) { 54async function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any): Promise<boolean> {
55 if (signedDocument.signature.type === 'RsaSignature2017') {
56 // Mastodon algorithm
57 const res = await isJsonLDRSA2017Verified(fromActor, signedDocument)
58 // Success? If no, try with our library
59 if (res === true) return true
60 }
61
44 const publicKeyObject = { 62 const publicKeyObject = {
45 '@context': jsig.SECURITY_CONTEXT_URL, 63 '@context': jsig.SECURITY_CONTEXT_URL,
46 id: fromActor.url, 64 id: fromActor.url,
47 type: 'CryptographicKey', 65 type: 'CryptographicKey',
48 owner: fromActor.url, 66 owner: fromActor.url,
49 publicKeyPem: fromActor.publicKey 67 publicKeyPem: fromActor.publicKey
50 } 68 }
@@ -69,6 +87,44 @@ function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any)
69 }) 87 })
70} 88}
71 89
90// Backward compatibility with "other" implementations
91async function isJsonLDRSA2017Verified (fromActor: ActorModel, signedDocument: any) {
92 function hash (obj: any): Promise<any> {
93 return jsonld.promises
94 .normalize(obj, {
95 algorithm: 'URDNA2015',
96 format: 'application/n-quads'
97 })
98 .then(res => sha256(res))
99 }
100
101 const signatureCopy = cloneDeep(signedDocument.signature)
102 Object.assign(signatureCopy, {
103 '@context': [
104 'https://w3id.org/security/v1',
105 { RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' }
106 ]
107 })
108 delete signatureCopy.type
109 delete signatureCopy.id
110 delete signatureCopy.signatureValue
111
112 const docWithoutSignature = cloneDeep(signedDocument)
113 delete docWithoutSignature.signature
114
115 const [ documentHash, optionsHash ] = await Promise.all([
116 hash(docWithoutSignature),
117 hash(signatureCopy)
118 ])
119
120 const toVerify = optionsHash + documentHash
121
122 const verify = createVerify('RSA-SHA256')
123 verify.update(toVerify, 'utf8')
124
125 return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64')
126}
127
72function signJsonLDObject (byActor: ActorModel, data: any) { 128function signJsonLDObject (byActor: ActorModel, data: any) {
73 const options = { 129 const options = {
74 privateKeyPem: byActor.privateKey, 130 privateKeyPem: byActor.privateKey,
@@ -82,6 +138,7 @@ function signJsonLDObject (byActor: ActorModel, data: any) {
82// --------------------------------------------------------------------------- 138// ---------------------------------------------------------------------------
83 139
84export { 140export {
141 isHTTPSignatureDigestValid,
85 parseHTTPSignature, 142 parseHTTPSignature,
86 isHTTPSignatureVerified, 143 isHTTPSignatureVerified,
87 isJsonLDSignatureVerified, 144 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 } = {
535const HTTP_SIGNATURE = { 535const HTTP_SIGNATURE = {
536 HEADER_NAME: 'signature', 536 HEADER_NAME: 'signature',
537 ALGORITHM: 'rsa-sha256', 537 ALGORITHM: 'rsa-sha256',
538 HEADERS_TO_SIGN: [ 'date', 'host', 'digest', '(request-target)' ] 538 HEADERS_TO_SIGN: [ '(request-target)', 'host', 'date', 'digest' ]
539} 539}
540 540
541// --------------------------------------------------------------------------- 541// ---------------------------------------------------------------------------
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) {
38 } 38 }
39} 39}
40 40
41function buildGlobalHeaders (body: object) { 41function buildGlobalHeaders (body: any) {
42 const digest = 'SHA-256=' + sha256(JSON.stringify(body), 'base64')
43
44 return { 42 return {
45 'Digest': digest 43 'Digest': buildDigest(body)
46 } 44 }
47} 45}
48 46
47function buildDigest (body: any) {
48 const rawBody = typeof body === 'string' ? body : JSON.stringify(body)
49
50 return 'SHA-256=' + sha256(rawBody, 'base64')
51}
52
49export { 53export {
54 buildDigest,
50 buildGlobalHeaders, 55 buildGlobalHeaders,
51 computeBody, 56 computeBody,
52 buildSignedRequestOptions 57 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[]) {
53 53
54export { 54export {
55 checkSignature, 55 checkSignature,
56 executeIfActivityPub 56 executeIfActivityPub,
57 checkHttpSignature
57} 58}
58 59
59// --------------------------------------------------------------------------- 60// ---------------------------------------------------------------------------
@@ -94,7 +95,7 @@ async function checkHttpSignature (req: Request, res: Response) {
94async function checkJsonLDSignature (req: Request, res: Response) { 95async function checkJsonLDSignature (req: Request, res: Response) {
95 const signatureObject: ActivityPubSignature = req.body.signature 96 const signatureObject: ActivityPubSignature = req.body.signature
96 97
97 if (!signatureObject.creator) { 98 if (!signatureObject || !signatureObject.creator) {
98 res.sendStatus(403) 99 res.sendStatus(403)
99 return false 100 return false
100 } 101 }
diff --git a/server/tests/activitypub.ts b/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 @@
2 2
3import * as chai from 'chai' 3import * as chai from 'chai'
4import 'mocha' 4import 'mocha'
5import { flushTests, killallServers, makeActivityPubGetRequest, runServer, ServerInfo, setAccessTokensToServers } from './utils' 5import { flushTests, killallServers, makeActivityPubGetRequest, runServer, ServerInfo, setAccessTokensToServers } from '../../utils'
6 6
7const expect = chai.expect 7const expect = chai.expect
8 8
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 @@
1/* tslint:disable:no-unused-expression */
2
3import 'mocha'
4import { expect } from 'chai'
5import { buildRequestStub } from '../../utils'
6import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto'
7import { cloneDeep } from 'lodash'
8import { buildSignedActivity } from '../../../helpers/activitypub'
9
10describe('Test activity pub helpers', function () {
11 describe('When checking the Linked Signature', function () {
12
13 it('Should fail with an invalid Mastodon signature', async function () {
14 const body = require('./json/mastodon/create-bad-signature.json')
15 const publicKey = require('./json/mastodon/public-key.json').publicKey
16 const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
17
18 const result = await isJsonLDSignatureVerified(fromActor as any, body)
19
20 expect(result).to.be.false
21 })
22
23 it('Should fail with an invalid public key', async function () {
24 const body = require('./json/mastodon/create.json')
25 const publicKey = require('./json/mastodon/bad-public-key.json').publicKey
26 const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
27
28 const result = await isJsonLDSignatureVerified(fromActor as any, body)
29
30 expect(result).to.be.false
31 })
32
33 it('Should succeed with a valid Mastodon signature', async function () {
34 const body = require('./json/mastodon/create.json')
35 const publicKey = require('./json/mastodon/public-key.json').publicKey
36 const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
37
38 const result = await isJsonLDSignatureVerified(fromActor as any, body)
39
40 expect(result).to.be.true
41 })
42
43 it('Should fail with an invalid PeerTube signature', async function () {
44 const keys = require('./json/peertube/invalid-keys.json')
45 const body = require('./json/peertube/announce-without-context.json')
46
47 const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
48 const signedBody = await buildSignedActivity(actorSignature as any, body)
49
50 const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' }
51 const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
52
53 expect(result).to.be.false
54 })
55
56 it('Should fail with an invalid PeerTube URL', async function () {
57 const keys = require('./json/peertube/keys.json')
58 const body = require('./json/peertube/announce-without-context.json')
59
60 const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
61 const signedBody = await buildSignedActivity(actorSignature as any, body)
62
63 const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9003/accounts/peertube' }
64 const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
65
66 expect(result).to.be.false
67 })
68
69 it('Should succeed with a valid PeerTube signature', async function () {
70 const keys = require('./json/peertube/keys.json')
71 const body = require('./json/peertube/announce-without-context.json')
72
73 const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
74 const signedBody = await buildSignedActivity(actorSignature as any, body)
75
76 const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' }
77 const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
78
79 expect(result).to.be.true
80 })
81 })
82
83 describe('When checking HTTP signature', function () {
84 it('Should fail with an invalid http signature', async function () {
85 const req = buildRequestStub()
86 req.method = 'POST'
87 req.url = '/accounts/ronan/inbox'
88
89 const mastodonObject = cloneDeep(require('./json/mastodon/bad-http-signature.json'))
90 req.body = mastodonObject.body
91 req.headers = mastodonObject.headers
92 req.headers.signature = 'Signature ' + req.headers.signature
93
94 const parsed = parseHTTPSignature(req, 3600 * 365 * 3)
95 const publicKey = require('./json/mastodon/public-key.json').publicKey
96
97 const actor = { publicKey }
98 const verified = isHTTPSignatureVerified(parsed, actor as any)
99
100 expect(verified).to.be.false
101 })
102
103 it('Should fail with an invalid public key', async function () {
104 const req = buildRequestStub()
105 req.method = 'POST'
106 req.url = '/accounts/ronan/inbox'
107
108 const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json'))
109 req.body = mastodonObject.body
110 req.headers = mastodonObject.headers
111 req.headers.signature = 'Signature ' + req.headers.signature
112
113 const parsed = parseHTTPSignature(req, 3600 * 365 * 3)
114 const publicKey = require('./json/mastodon/bad-public-key.json').publicKey
115
116 const actor = { publicKey }
117 const verified = isHTTPSignatureVerified(parsed, actor as any)
118
119 expect(verified).to.be.false
120 })
121
122 it('Should fail because of clock skew', async function () {
123 const req = buildRequestStub()
124 req.method = 'POST'
125 req.url = '/accounts/ronan/inbox'
126
127 const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json'))
128 req.body = mastodonObject.body
129 req.headers = mastodonObject.headers
130 req.headers.signature = 'Signature ' + req.headers.signature
131
132 let errored = false
133 try {
134 parseHTTPSignature(req)
135 } catch {
136 errored = true
137 }
138
139 expect(errored).to.be.true
140 })
141
142 it('Should fail without scheme', async function () {
143 const req = buildRequestStub()
144 req.method = 'POST'
145 req.url = '/accounts/ronan/inbox'
146
147 const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json'))
148 req.body = mastodonObject.body
149 req.headers = mastodonObject.headers
150
151 let errored = false
152 try {
153 parseHTTPSignature(req, 3600 * 365 * 3)
154 } catch {
155 errored = true
156 }
157
158 expect(errored).to.be.true
159 })
160
161 it('Should succeed with a valid signature', async function () {
162 const req = buildRequestStub()
163 req.method = 'POST'
164 req.url = '/accounts/ronan/inbox'
165
166 const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json'))
167 req.body = mastodonObject.body
168 req.headers = mastodonObject.headers
169 req.headers.signature = 'Signature ' + req.headers.signature
170
171 const parsed = parseHTTPSignature(req, 3600 * 365 * 3)
172 const publicKey = require('./json/mastodon/public-key.json').publicKey
173
174 const actor = { publicKey }
175 const verified = isHTTPSignatureVerified(parsed, actor as any)
176
177 expect(verified).to.be.true
178 })
179
180 })
181
182})
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 @@
1import './client'
2import './helpers'
3import './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 @@
1{
2 "headers": {
3 "user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)",
4 "host": "localhost",
5 "date": "Mon, 22 Oct 2018 13:34:22 GMT",
6 "accept-encoding": "gzip",
7 "digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=",
8 "content-type": "application/activity+json",
9 "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==\"",
10 "content-length": "2815"
11 },
12 "body": {
13 "@context": [
14 "https://www.w3.org/ns/activitystreams",
15 "https://w3id.org/security/v1",
16 {
17 "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
18 "sensitive": "as:sensitive",
19 "movedTo": {
20 "@id": "as:movedTo",
21 "@type": "@id"
22 },
23 "Hashtag": "as:Hashtag",
24 "ostatus": "http://ostatus.org#",
25 "atomUri": "ostatus:atomUri",
26 "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
27 "conversation": "ostatus:conversation",
28 "toot": "http://joinmastodon.org/ns#",
29 "Emoji": "toot:Emoji",
30 "focalPoint": {
31 "@container": "@list",
32 "@id": "toot:focalPoint"
33 },
34 "featured": {
35 "@id": "toot:featured",
36 "@type": "@id"
37 },
38 "schema": "http://schema.org#",
39 "PropertyValue": "schema:PropertyValue",
40 "value": "schema:value"
41 }
42 ],
43 "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity",
44 "type": "Create",
45 "actor": "http://localhost:3000/users/ronan2",
46 "published": "2018-10-22T13:34:18Z",
47 "to": [
48 "https://www.w3.org/ns/activitystreams#Public"
49 ],
50 "cc": [
51 "http://localhost:3000/users/ronan2/followers",
52 "http://localhost:9000/accounts/ronan"
53 ],
54 "object": {
55 "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
56 "type": "Note",
57 "summary": null,
58 "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
59 "published": "2018-10-22T13:34:18Z",
60 "url": "http://localhost:3000/@ronan2/100939547203370948",
61 "attributedTo": "http://localhost:3000/users/ronan2",
62 "to": [
63 "https://www.w3.org/ns/activitystreams#Public"
64 ],
65 "cc": [
66 "http://localhost:3000/users/ronan2/followers",
67 "http://localhost:9000/accounts/ronan"
68 ],
69 "sensitive": false,
70 "atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
71 "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
72 "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
73 "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>",
74 "contentMap": {
75 "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>"
76 },
77 "attachment": [],
78 "tag": [
79 {
80 "type": "Mention",
81 "href": "http://localhost:9000/accounts/ronan",
82 "name": "@ronan@localhost:9000"
83 }
84 ]
85 },
86 "signature": {
87 "type": "RsaSignature2017",
88 "creator": "http://localhost:3000/users/ronan2#main-key",
89 "created": "2018-10-22T13:34:19Z",
90 "signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q=="
91 }
92 }
93}
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 @@
1{
2 "headers": {
3 "user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)",
4 "host": "localhost",
5 "date": "Mon, 22 Oct 2018 13:34:22 GMT",
6 "accept-encoding": "gzip",
7 "digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=",
8 "content-type": "application/activity+json",
9 "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==\"",
10 "content-length": "2815"
11 },
12 "body": {
13 "@context": [
14 "https://www.w3.org/ns/activitystreams",
15 "https://w3id.org/security/v1",
16 {
17 "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
18 "sensitive": "as:sensitive",
19 "movedTo": {
20 "@id": "as:movedTo",
21 "@type": "@id"
22 },
23 "Hashtag": "as:Hashtag",
24 "ostatus": "http://ostatus.org#",
25 "atomUri": "ostatus:atomUri",
26 "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
27 "conversation": "ostatus:conversation",
28 "toot": "http://joinmastodon.org/ns#",
29 "Emoji": "toot:Emoji",
30 "focalPoint": {
31 "@container": "@list",
32 "@id": "toot:focalPoint"
33 },
34 "featured": {
35 "@id": "toot:featured",
36 "@type": "@id"
37 },
38 "schema": "http://schema.org#",
39 "PropertyValue": "schema:PropertyValue",
40 "value": "schema:value"
41 }
42 ],
43 "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity",
44 "type": "Create",
45 "actor": "http://localhost:3000/users/ronan2",
46 "published": "2018-10-22T13:34:18Z",
47 "to": [
48 "https://www.w3.org/ns/activitystreams#Public"
49 ],
50 "cc": [
51 "http://localhost:3000/users/ronan2/followers",
52 "http://localhost:9000/accounts/ronan"
53 ],
54 "object": {
55 "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
56 "type": "Note",
57 "summary": null,
58 "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
59 "published": "2018-10-22T13:34:18Z",
60 "url": "http://localhost:3000/@ronan2/100939547203370948",
61 "attributedTo": "http://localhost:3000/users/ronan2",
62 "to": [
63 "https://www.w3.org/ns/activitystreams#Public"
64 ],
65 "cc": [
66 "http://localhost:3000/users/ronan2/followers",
67 "http://localhost:9000/accounts/ronan"
68 ],
69 "sensitive": false,
70 "atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
71 "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
72 "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
73 "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>",
74 "contentMap": {
75 "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>"
76 },
77 "attachment": [],
78 "tag": [
79 {
80 "type": "Mention",
81 "href": "http://localhost:9000/accounts/ronan",
82 "name": "@ronan@localhost:9000"
83 }
84 ]
85 },
86 "signature": {
87 "type": "RsaSignature2017",
88 "creator": "http://localhost:3000/users/ronan2#main-key",
89 "created": "2018-10-22T13:34:19Z",
90 "signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q=="
91 }
92 }
93}
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 @@
1{
2 "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"
3}
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 @@
1{
2 "@context": [
3 "https://www.w3.org/ns/activitystreams",
4 "https://w3id.org/security/v1",
5 {
6 "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
7 "sensitive": "as:sensitive",
8 "movedTo": {
9 "@id": "as:movedTo",
10 "@type": "@id"
11 },
12 "Hashtag": "as:Hashtag",
13 "ostatus": "http://ostatus.org#",
14 "atomUri": "ostatus:atomUri",
15 "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
16 "conversation": "ostatus:conversation",
17 "toot": "http://joinmastodon.org/ns#",
18 "Emoji": "toot:Emoji",
19 "focalPoint": {
20 "@container": "@list",
21 "@id": "toot:focalPoint"
22 },
23 "featured": {
24 "@id": "toot:featured",
25 "@type": "@id"
26 },
27 "schema": "http://schema.org#",
28 "PropertyValue": "schema:PropertyValue",
29 "value": "schema:value"
30 }
31 ],
32 "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698/activity",
33 "type": "Create",
34 "actor": "http://localhost:3000/users/ronan2",
35 "published": "2018-10-22T12:43:07Z",
36 "to": [
37 "https://www.w3.org/ns/activitystreams#Public"
38 ],
39 "cc": [
40 "http://localhost:3000/users/ronan2/followers",
41 "http://localhost:9000/accounts/ronan"
42 ],
43 "object": {
44 "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698",
45 "type": "Note",
46 "summary": null,
47 "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
48 "published": "2018-10-22T12:43:07Z",
49 "url": "http://localhost:3000/@ronan2/100939345950887698",
50 "attributedTo": "http://localhost:3000/users/ronan2",
51 "to": [
52 "https://www.w3.org/ns/activitystreams#Public"
53 ],
54 "cc": [
55 "http://localhost:3000/users/ronan2/followers",
56 "http://localhost:9000/accounts/ronan"
57 ],
58 "sensitive": false,
59 "atomUri": "http://localhost:3000/users/ronan2/statuses/100939345950887698",
60 "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
61 "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
62 "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>",
63 "contentMap": {
64 "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>"
65 },
66 "attachment": [],
67 "tag": [
68 {
69 "type": "Mention",
70 "href": "http://localhost:9000/accounts/ronan",
71 "name": "@ronan@localhost:9000"
72 }
73 ]
74 },
75 "signature": {
76 "type": "RsaSignature2017",
77 "creator": "http://localhost:3000/users/ronan2#main-key",
78 "created": "2018-10-22T12:43:08Z",
79 "signatureValue": "Vgr8nA0agPr9TcA4BlX+MWhmuE+rBcoIJLpnPbm3E5SnOCXbgjEfEaTLqfuzzkKNsR3PBbkvi3YWK4/DxJ0zmpzSB7yy4NRzluQMVQHqJiFKXAX3Sr3fIrK24xkWW9/F207c1NpFajSGbgnFKBdtFE0e5VqwSrSoOJkZukZW/2ATSnsyzblieuUmvTWpD0PqpUOsynPjw+RqZnqPn0cjw1z2Dm7ZRt3trnyMTXFYZw5U/YuqMY2kpadD6vq780md8kXlJIylxG6ZrlO2jz9fJdnfuVq43d4QFNsBm1K1r2WtNqX+i+wiqh+u3PjF4pzXtl/a3hJOH18IfZnK7I21mQ=="
80 }
81}
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 @@
1{
2 "@context": [
3 "https://www.w3.org/ns/activitystreams",
4 "https://w3id.org/security/v1",
5 {
6 "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
7 "sensitive": "as:sensitive",
8 "movedTo": {
9 "@id": "as:movedTo",
10 "@type": "@id"
11 },
12 "Hashtag": "as:Hashtag",
13 "ostatus": "http://ostatus.org#",
14 "atomUri": "ostatus:atomUri",
15 "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
16 "conversation": "ostatus:conversation",
17 "toot": "http://joinmastodon.org/ns#",
18 "Emoji": "toot:Emoji",
19 "focalPoint": {
20 "@container": "@list",
21 "@id": "toot:focalPoint"
22 },
23 "featured": {
24 "@id": "toot:featured",
25 "@type": "@id"
26 },
27 "schema": "http://schema.org#",
28 "PropertyValue": "schema:PropertyValue",
29 "value": "schema:value"
30 }
31 ],
32 "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698/activity",
33 "type": "Create",
34 "actor": "http://localhost:3000/users/ronan2",
35 "published": "2018-10-22T12:43:07Z",
36 "to": [
37 "https://www.w3.org/ns/activitystreams#Public"
38 ],
39 "cc": [
40 "http://localhost:3000/users/ronan2/followers",
41 "http://localhost:9000/accounts/ronan"
42 ],
43 "object": {
44 "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698",
45 "type": "Note",
46 "summary": null,
47 "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
48 "published": "2018-10-22T12:43:07Z",
49 "url": "http://localhost:3000/@ronan2/100939345950887698",
50 "attributedTo": "http://localhost:3000/users/ronan2",
51 "to": [
52 "https://www.w3.org/ns/activitystreams#Public"
53 ],
54 "cc": [
55 "http://localhost:3000/users/ronan2/followers",
56 "http://localhost:9000/accounts/ronan"
57 ],
58 "sensitive": false,
59 "atomUri": "http://localhost:3000/users/ronan2/statuses/100939345950887698",
60 "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
61 "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
62 "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>",
63 "contentMap": {
64 "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>"
65 },
66 "attachment": [],
67 "tag": [
68 {
69 "type": "Mention",
70 "href": "http://localhost:9000/accounts/ronan",
71 "name": "@ronan@localhost:9000"
72 }
73 ]
74 },
75 "signature": {
76 "type": "RsaSignature2017",
77 "creator": "http://localhost:3000/users/ronan2#main-key",
78 "created": "2018-10-22T12:43:08Z",
79 "signatureValue": "VgR8nA0agPr9TcA4BlX+MWhmuE+rBcoIJLpnPbm3E5SnOCXbgjEfEaTLqfuzzkKNsR3PBbkvi3YWK4/DxJ0zmpzSB7yy4NRzluQMVQHqJiFKXAX3Sr3fIrK24xkWW9/F207c1NpFajSGbgnFKBdtFE0e5VqwSrSoOJkZukZW/2ATSnsyzblieuUmvTWpD0PqpUOsynPjw+RqZnqPn0cjw1z2Dm7ZRt3trnyMTXFYZw5U/YuqMY2kpadD6vq780md8kXlJIylxG6ZrlO2jz9fJdnfuVq43d4QFNsBm1K1r2WtNqX+i+wiqh+u3PjF4pzXtl/a3hJOH18IfZnK7I21mQ=="
80 }
81}
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 @@
1{
2 "headers": {
3 "user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)",
4 "host": "localhost",
5 "date": "Mon, 22 Oct 2018 13:34:22 GMT",
6 "accept-encoding": "gzip",
7 "digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=",
8 "content-type": "application/activity+json",
9 "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==\"",
10 "content-length": "2815"
11 },
12 "body": {
13 "@context": [
14 "https://www.w3.org/ns/activitystreams",
15 "https://w3id.org/security/v1",
16 {
17 "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
18 "sensitive": "as:sensitive",
19 "movedTo": {
20 "@id": "as:movedTo",
21 "@type": "@id"
22 },
23 "Hashtag": "as:Hashtag",
24 "ostatus": "http://ostatus.org#",
25 "atomUri": "ostatus:atomUri",
26 "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
27 "conversation": "ostatus:conversation",
28 "toot": "http://joinmastodon.org/ns#",
29 "Emoji": "toot:Emoji",
30 "focalPoint": {
31 "@container": "@list",
32 "@id": "toot:focalPoint"
33 },
34 "featured": {
35 "@id": "toot:featured",
36 "@type": "@id"
37 },
38 "schema": "http://schema.org#",
39 "PropertyValue": "schema:PropertyValue",
40 "value": "schema:value"
41 }
42 ],
43 "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity",
44 "type": "Create",
45 "actor": "http://localhost:3000/users/ronan2",
46 "published": "2018-10-22T13:34:18Z",
47 "to": [
48 "https://www.w3.org/ns/activitystreams#Public"
49 ],
50 "cc": [
51 "http://localhost:3000/users/ronan2/followers",
52 "http://localhost:9000/accounts/ronan"
53 ],
54 "object": {
55 "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
56 "type": "Note",
57 "summary": null,
58 "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
59 "published": "2018-10-22T13:34:18Z",
60 "url": "http://localhost:3000/@ronan2/100939547203370948",
61 "attributedTo": "http://localhost:3000/users/ronan2",
62 "to": [
63 "https://www.w3.org/ns/activitystreams#Public"
64 ],
65 "cc": [
66 "http://localhost:3000/users/ronan2/followers",
67 "http://localhost:9000/accounts/ronan"
68 ],
69 "sensitive": false,
70 "atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
71 "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
72 "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
73 "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>",
74 "contentMap": {
75 "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>"
76 },
77 "attachment": [],
78 "tag": [
79 {
80 "type": "Mention",
81 "href": "http://localhost:9000/accounts/ronan",
82 "name": "@ronan@localhost:9000"
83 }
84 ]
85 },
86 "signature": {
87 "type": "RsaSignature2017",
88 "creator": "http://localhost:3000/users/ronan2#main-key",
89 "created": "2018-10-22T13:34:19Z",
90 "signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q=="
91 }
92 }
93}
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 @@
1{
2 "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"
3}
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 @@
1{
2 "type": "Announce",
3 "id": "http://localhost:9002/videos/watch/997111d4-e8d8-4f45-99d3-857905785d05/announces/1",
4 "actor": "http://localhost:9002/accounts/peertube",
5 "object": "http://localhost:9002/videos/watch/997111d4-e8d8-4f45-99d3-857905785d05",
6 "to": [
7 "https://www.w3.org/ns/activitystreams#Public",
8 "http://localhost:9002/accounts/peertube/followers",
9 "http://localhost:9002/video-channels/root_channel/followers",
10 "http://localhost:9002/accounts/root/followers"
11 ],
12 "cc": []
13}
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 @@
1{
2 "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqjQGdH6D3naKmSbbr/Df\nEh1H42F3WlHYXuxKLkm5Bemjdde+GwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYO\nwAyc3Zoy7afPNa4bZXqhJ1Im41rMGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55s\nIkczDkseJuadTvG+A1e4uNY2lnRmVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/F\npP5S75TS5l1DfJQIq2lp8RwrH6FvGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM\n7mS7eP8zF8lKXYUu8cjIscKm+XqGmyRoPyw2Pp53tew29idRUocVQHGBnlNbpKdd\naQIDAQAB\n-----END PUBLIC KEY-----\n",
3 "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-----"
4}
5
6
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 @@
1{
2 "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqjQGdH6D3naKmSbbr/Df\nEh1H42F3WlHYXuxKLkm5Bemjdde+GwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYO\nwAyc3Zoy7afPNa4bZXqhJ1Im41rMGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55s\nIkczDkseJuadTvG+A1e4uNY2lnRmVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/F\npP5S75TS5l1DfJQIq2lp8RwrH6FvGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM\n7mS7eP8zF8lKXYUu8cjIscKm+XqGmyRoPyw3Pp53tew29idRUocVQHGBnlNbpKdd\naQIDAQAB\n-----END PUBLIC KEY-----\n",
3 "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-----"
4}
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 @@
1/* tslint:disable:no-unused-expression */
2
3import 'mocha'
4
5import { flushAndRunMultipleServers, flushTests, killallServers, makeAPRequest, makeFollowRequest, ServerInfo } from '../../utils'
6import { HTTP_SIGNATURE } from '../../../initializers'
7import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
8import * as chai from 'chai'
9import { setActorField } from '../../utils/miscs/sql'
10import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub'
11
12const expect = chai.expect
13
14function setKeysOfServer2 (serverNumber: number, publicKey: string, privateKey: string) {
15 return Promise.all([
16 setActorField(serverNumber, 'http://localhost:9002/accounts/peertube', 'publicKey', publicKey),
17 setActorField(serverNumber, 'http://localhost:9002/accounts/peertube', 'privateKey', privateKey)
18 ])
19}
20
21function setKeysOfServer3 (serverNumber: number, publicKey: string, privateKey: string) {
22 return Promise.all([
23 setActorField(serverNumber, 'http://localhost:9003/accounts/peertube', 'publicKey', publicKey),
24 setActorField(serverNumber, 'http://localhost:9003/accounts/peertube', 'privateKey', privateKey)
25 ])
26}
27
28describe('Test ActivityPub security', function () {
29 let servers: ServerInfo[]
30 let url: string
31
32 const keys = require('./json/peertube/keys.json')
33 const invalidKeys = require('./json/peertube/invalid-keys.json')
34 const baseHttpSignature = {
35 algorithm: HTTP_SIGNATURE.ALGORITHM,
36 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
37 keyId: 'acct:peertube@localhost:9002',
38 key: keys.privateKey,
39 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
40 }
41
42 // ---------------------------------------------------------------
43
44 before(async function () {
45 this.timeout(60000)
46
47 servers = await flushAndRunMultipleServers(3)
48
49 url = servers[0].url + '/inbox'
50
51 await setKeysOfServer2(1, keys.publicKey, keys.privateKey)
52
53 const to = { url: 'http://localhost:9001/accounts/peertube' }
54 const by = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
55 await makeFollowRequest(to, by)
56 })
57
58 describe('When checking HTTP signature', function () {
59
60 it('Should fail with an invalid digest', async function () {
61 const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
62 const headers = {
63 Digest: buildDigest({ hello: 'coucou' })
64 }
65
66 const { response } = await makeAPRequest(url, body, baseHttpSignature, headers)
67
68 expect(response.statusCode).to.equal(403)
69 })
70
71 it('Should fail with an invalid date', async function () {
72 const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
73 const headers = buildGlobalHeaders(body)
74 headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
75
76 const { response } = await makeAPRequest(url, body, baseHttpSignature, headers)
77
78 expect(response.statusCode).to.equal(403)
79 })
80
81 it('Should fail with bad keys', async function () {
82 await setKeysOfServer2(1, invalidKeys.publicKey, invalidKeys.privateKey)
83 await setKeysOfServer2(2, invalidKeys.publicKey, invalidKeys.privateKey)
84
85 const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
86 const headers = buildGlobalHeaders(body)
87
88 const { response } = await makeAPRequest(url, body, baseHttpSignature, headers)
89
90 expect(response.statusCode).to.equal(403)
91 })
92
93 it('Should succeed with a valid HTTP signature', async function () {
94 await setKeysOfServer2(1, keys.publicKey, keys.privateKey)
95 await setKeysOfServer2(2, keys.publicKey, keys.privateKey)
96
97 const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
98 const headers = buildGlobalHeaders(body)
99
100 const { response } = await makeAPRequest(url, body, baseHttpSignature, headers)
101
102 expect(response.statusCode).to.equal(204)
103 })
104 })
105
106 describe('When checking Linked Data Signature', function () {
107 before(async () => {
108 await setKeysOfServer3(3, keys.publicKey, keys.privateKey)
109
110 const to = { url: 'http://localhost:9001/accounts/peertube' }
111 const by = { url: 'http://localhost:9003/accounts/peertube', privateKey: keys.privateKey }
112 await makeFollowRequest(to, by)
113 })
114
115 it('Should fail with bad keys', async function () {
116 this.timeout(10000)
117
118 await setKeysOfServer3(1, invalidKeys.publicKey, invalidKeys.privateKey)
119 await setKeysOfServer3(3, invalidKeys.publicKey, invalidKeys.privateKey)
120
121 const body = require('./json/peertube/announce-without-context.json')
122 body.actor = 'http://localhost:9003/accounts/peertube'
123
124 const signer: any = { privateKey: invalidKeys.privateKey, url: 'http://localhost:9003/accounts/peertube' }
125 const signedBody = await buildSignedActivity(signer, body)
126
127 const headers = buildGlobalHeaders(signedBody)
128
129 const { response } = await makeAPRequest(url, signedBody, baseHttpSignature, headers)
130
131 expect(response.statusCode).to.equal(403)
132 })
133
134 it('Should fail with an altered body', async function () {
135 this.timeout(10000)
136
137 await setKeysOfServer3(1, keys.publicKey, keys.privateKey)
138 await setKeysOfServer3(3, keys.publicKey, keys.privateKey)
139
140 const body = require('./json/peertube/announce-without-context.json')
141 body.actor = 'http://localhost:9003/accounts/peertube'
142
143 const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:9003/accounts/peertube' }
144 const signedBody = await buildSignedActivity(signer, body)
145
146 signedBody.actor = 'http://localhost:9003/account/peertube'
147
148 const headers = buildGlobalHeaders(signedBody)
149
150 const { response } = await makeAPRequest(url, signedBody, baseHttpSignature, headers)
151
152 expect(response.statusCode).to.equal(403)
153 })
154
155 it('Should succeed with a valid signature', async function () {
156 this.timeout(10000)
157
158 const body = require('./json/peertube/announce-without-context.json')
159 body.actor = 'http://localhost:9003/accounts/peertube'
160
161 const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:9003/accounts/peertube' }
162 const signedBody = await buildSignedActivity(signer, body)
163
164 const headers = buildGlobalHeaders(signedBody)
165
166 const { response } = await makeAPRequest(url, signedBody, baseHttpSignature, headers)
167
168 expect(response.statusCode).to.equal(204)
169 })
170 })
171
172 after(async function () {
173 killallServers(servers)
174
175 // Keep the logs if the test failed
176 if (this['ok']) {
177 await flushTests()
178 }
179 })
180})
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 @@
1import './redundancy' 1import './redundancy'
2import './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 @@
1// Order of the tests we want to execute 1// Order of the tests we want to execute
2import './client' 2import './client'
3import './activitypub'
4import './feeds/' 3import './feeds/'
5import './cli/' 4import './cli/'
6import './api/' 5import './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'
4export * from './server/config' 4export * from './server/config'
5export * from './users/login' 5export * from './users/login'
6export * from './miscs/miscs' 6export * from './miscs/miscs'
7export * from './miscs/stubs'
7export * from './server/follows' 8export * from './server/follows'
8export * from './requests/requests' 9export * from './requests/requests'
10export * from './requests/activitypub'
9export * from './server/servers' 11export * from './server/servers'
10export * from './videos/services' 12export * from './videos/services'
11export * from './users/users' 13export * 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 @@
1import * as Sequelize from 'sequelize'
2
3function getSequelize (serverNumber: number) {
4 const dbname = 'peertube_test' + serverNumber
5 const username = 'peertube'
6 const password = 'peertube'
7 const host = 'localhost'
8 const port = 5432
9
10 return new Sequelize(dbname, username, password, {
11 dialect: 'postgres',
12 host,
13 port,
14 operatorsAliases: false,
15 logging: false
16 })
17}
18
19function setActorField (serverNumber: number, to: string, field: string, value: string) {
20 const seq = getSequelize(serverNumber)
21
22 const options = { type: Sequelize.QueryTypes.UPDATE }
23
24 return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options)
25}
26
27export {
28 setActorField
29}
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 @@
1function buildRequestStub (): any {
2 return { }
3}
4
5function buildResponseStub (): any {
6 return {
7 locals: {}
8 }
9}
10
11export {
12 buildResponseStub,
13 buildRequestStub
14}
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 @@
1import { doRequest } from '../../../helpers/requests'
2import { HTTP_SIGNATURE } from '../../../initializers'
3import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
4import { activityPubContextify } from '../../../helpers/activitypub'
5
6function makeAPRequest (url: string, body: any, httpSignature: any, headers: any) {
7 const options = {
8 method: 'POST',
9 uri: url,
10 json: body,
11 httpSignature,
12 headers
13 }
14
15 return doRequest(options)
16}
17
18async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) {
19 const follow = {
20 type: 'Follow',
21 id: by.url + '/toto',
22 actor: by.url,
23 object: to.url
24 }
25
26 const body = activityPubContextify(follow)
27
28 const httpSignature = {
29 algorithm: HTTP_SIGNATURE.ALGORITHM,
30 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
31 keyId: by.url,
32 key: by.privateKey,
33 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
34 }
35 const headers = buildGlobalHeaders(body)
36
37 return makeAPRequest(to.url, body, httpSignature, headers)
38}
39
40export {
41 makeAPRequest,
42 makeFollowRequest
43}