1 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
4 import * as chai from 'chai'
5 import { activityPubContextify, buildSignedActivity } from '@server/helpers/activitypub'
6 import { buildDigest } from '@server/helpers/peertube-crypto'
7 import { HTTP_SIGNATURE } from '@server/initializers/constants'
8 import { buildGlobalHeaders } from '@server/lib/job-queue/handlers/utils/activitypub-http-utils'
9 import { makeFollowRequest, makePOSTAPRequest } from '@server/tests/shared'
10 import { buildAbsoluteFixturePath, wait } from '@shared/core-utils'
11 import { HttpStatusCode } from '@shared/models'
12 import { cleanupTests, createMultipleServers, killallServers, PeerTubeServer } from '@shared/server-commands'
14 const expect = chai.expect
16 function setKeysOfServer (onServer: PeerTubeServer, ofServer: PeerTubeServer, publicKey: string, privateKey: string) {
17 const url = 'http://localhost:' + ofServer.port + '/accounts/peertube'
20 onServer.sql.setActorField(url, 'publicKey', publicKey),
21 onServer.sql.setActorField(url, 'privateKey', privateKey)
25 function setUpdatedAtOfServer (onServer: PeerTubeServer, ofServer: PeerTubeServer, updatedAt: string) {
26 const url = 'http://localhost:' + ofServer.port + '/accounts/peertube'
29 onServer.sql.setActorField(url, 'createdAt', updatedAt),
30 onServer.sql.setActorField(url, 'updatedAt', updatedAt)
34 function getAnnounceWithoutContext (server: PeerTubeServer) {
35 const json = require(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json'))
36 const result: typeof json = {}
38 for (const key of Object.keys(json)) {
39 if (Array.isArray(json[key])) {
40 result[key] = json[key].map(v => v.replace(':9002', `:${server.port}`))
42 result[key] = json[key].replace(':9002', `:${server.port}`)
49 describe('Test ActivityPub security', function () {
50 let servers: PeerTubeServer[]
53 const keys = require(buildAbsoluteFixturePath('./ap-json/peertube/keys.json'))
54 const invalidKeys = require(buildAbsoluteFixturePath('./ap-json/peertube/invalid-keys.json'))
55 const baseHttpSignature = () => ({
56 algorithm: HTTP_SIGNATURE.ALGORITHM,
57 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
58 keyId: 'acct:peertube@localhost:' + servers[1].port,
60 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
63 // ---------------------------------------------------------------
65 before(async function () {
68 servers = await createMultipleServers(3)
70 url = servers[0].url + '/inbox'
72 await setKeysOfServer(servers[0], servers[1], keys.publicKey, null)
73 await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
75 const to = { url: 'http://localhost:' + servers[0].port + '/accounts/peertube' }
76 const by = { url: 'http://localhost:' + servers[1].port + '/accounts/peertube', privateKey: keys.privateKey }
77 await makeFollowRequest(to, by)
80 describe('When checking HTTP signature', function () {
82 it('Should fail with an invalid digest', async function () {
83 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
85 Digest: buildDigest({ hello: 'coucou' })
89 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
90 expect(true, 'Did not throw').to.be.false
92 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
96 it('Should fail with an invalid date', async function () {
97 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
98 const headers = buildGlobalHeaders(body)
99 headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
102 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
103 expect(true, 'Did not throw').to.be.false
105 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
109 it('Should fail with bad keys', async function () {
110 await setKeysOfServer(servers[0], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
111 await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
113 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
114 const headers = buildGlobalHeaders(body)
117 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
118 expect(true, 'Did not throw').to.be.false
120 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
124 it('Should reject requests without appropriate signed headers', async function () {
125 await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey)
126 await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
128 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
129 const headers = buildGlobalHeaders(body)
131 const signatureOptions = baseHttpSignature()
132 const badHeadersMatrix = [
133 [ '(request-target)', 'date', 'digest' ],
134 [ 'host', 'date', 'digest' ],
135 [ '(request-target)', 'host', 'digest' ]
138 for (const badHeaders of badHeadersMatrix) {
139 signatureOptions.headers = badHeaders
142 await makePOSTAPRequest(url, body, signatureOptions, headers)
143 expect(true, 'Did not throw').to.be.false
145 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
150 it('Should succeed with a valid HTTP signature', async function () {
151 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
152 const headers = buildGlobalHeaders(body)
154 const { statusCode } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
155 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
158 it('Should refresh the actor keys', async function () {
161 // Update keys of server 2 to invalid keys
162 // Server 1 should refresh the actor and fail
163 await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
164 await setUpdatedAtOfServer(servers[0], servers[1], '2015-07-17 22:00:00+00')
166 // Invalid peertube actor cache
167 await killallServers([ servers[1] ])
168 await servers[1].run()
170 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
171 const headers = buildGlobalHeaders(body)
174 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
175 expect(true, 'Did not throw').to.be.false
178 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
183 describe('When checking Linked Data Signature', function () {
184 before(async function () {
187 await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey)
188 await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
189 await setKeysOfServer(servers[2], servers[2], keys.publicKey, keys.privateKey)
191 const to = { url: 'http://localhost:' + servers[0].port + '/accounts/peertube' }
192 const by = { url: 'http://localhost:' + servers[2].port + '/accounts/peertube', privateKey: keys.privateKey }
193 await makeFollowRequest(to, by)
196 it('Should fail with bad keys', async function () {
199 await setKeysOfServer(servers[0], servers[2], invalidKeys.publicKey, invalidKeys.privateKey)
200 await setKeysOfServer(servers[2], servers[2], invalidKeys.publicKey, invalidKeys.privateKey)
202 const body = getAnnounceWithoutContext(servers[1])
203 body.actor = 'http://localhost:' + servers[2].port + '/accounts/peertube'
205 const signer: any = { privateKey: invalidKeys.privateKey, url: 'http://localhost:' + servers[2].port + '/accounts/peertube' }
206 const signedBody = await buildSignedActivity(signer, body)
208 const headers = buildGlobalHeaders(signedBody)
211 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
212 expect(true, 'Did not throw').to.be.false
214 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
218 it('Should fail with an altered body', async function () {
221 await setKeysOfServer(servers[0], servers[2], keys.publicKey, keys.privateKey)
222 await setKeysOfServer(servers[0], servers[2], keys.publicKey, keys.privateKey)
224 const body = getAnnounceWithoutContext(servers[1])
225 body.actor = 'http://localhost:' + servers[2].port + '/accounts/peertube'
227 const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:' + servers[2].port + '/accounts/peertube' }
228 const signedBody = await buildSignedActivity(signer, body)
230 signedBody.actor = 'http://localhost:' + servers[2].port + '/account/peertube'
232 const headers = buildGlobalHeaders(signedBody)
235 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
236 expect(true, 'Did not throw').to.be.false
238 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
242 it('Should succeed with a valid signature', async function () {
245 const body = getAnnounceWithoutContext(servers[1])
246 body.actor = 'http://localhost:' + servers[2].port + '/accounts/peertube'
248 const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:' + servers[2].port + '/accounts/peertube' }
249 const signedBody = await buildSignedActivity(signer, body)
251 const headers = buildGlobalHeaders(signedBody)
253 const { statusCode } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
254 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
257 it('Should refresh the actor keys', async function () {
260 // Wait refresh invalidation
263 // Update keys of server 3 to invalid keys
264 // Server 1 should refresh the actor and fail
265 await setKeysOfServer(servers[2], servers[2], invalidKeys.publicKey, invalidKeys.privateKey)
267 const body = getAnnounceWithoutContext(servers[1])
268 body.actor = 'http://localhost:' + servers[2].port + '/accounts/peertube'
270 const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:' + servers[2].port + '/accounts/peertube' }
271 const signedBody = await buildSignedActivity(signer, body)
273 const headers = buildGlobalHeaders(signedBody)
276 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
277 expect(true, 'Did not throw').to.be.false
279 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
284 after(async function () {
287 await cleanupTests(servers)