]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blob - server/tests/api/activitypub/security.ts
c173648b30eaff5887ccc99aeb3953e87ef737d0
[github/Chocobozzz/PeerTube.git] / server / tests / api / activitypub / security.ts
1 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3 import 'mocha'
4 import * as chai from 'chai'
5 import { buildDigest } from '@server/helpers/peertube-crypto'
6 import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
7 import {
8 buildAbsoluteFixturePath,
9 cleanupTests,
10 createMultipleServers,
11 killallServers,
12 PeerTubeServer,
13 wait
14 } from '../../../../shared/extra-utils'
15 import { makeFollowRequest, makePOSTAPRequest } from '../../../../shared/extra-utils/requests/activitypub'
16 import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub'
17 import { HTTP_SIGNATURE } from '../../../initializers/constants'
18 import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
19
20 const expect = chai.expect
21
22 function setKeysOfServer (onServer: PeerTubeServer, ofServer: PeerTubeServer, publicKey: string, privateKey: string) {
23 const url = 'http://localhost:' + ofServer.port + '/accounts/peertube'
24
25 return Promise.all([
26 onServer.sql.setActorField(url, 'publicKey', publicKey),
27 onServer.sql.setActorField(url, 'privateKey', privateKey)
28 ])
29 }
30
31 function setUpdatedAtOfServer (onServer: PeerTubeServer, ofServer: PeerTubeServer, updatedAt: string) {
32 const url = 'http://localhost:' + ofServer.port + '/accounts/peertube'
33
34 return Promise.all([
35 onServer.sql.setActorField(url, 'createdAt', updatedAt),
36 onServer.sql.setActorField(url, 'updatedAt', updatedAt)
37 ])
38 }
39
40 function getAnnounceWithoutContext (server: PeerTubeServer) {
41 const json = require(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json'))
42 const result: typeof json = {}
43
44 for (const key of Object.keys(json)) {
45 if (Array.isArray(json[key])) {
46 result[key] = json[key].map(v => v.replace(':9002', `:${server.port}`))
47 } else {
48 result[key] = json[key].replace(':9002', `:${server.port}`)
49 }
50 }
51
52 return result
53 }
54
55 describe('Test ActivityPub security', function () {
56 let servers: PeerTubeServer[]
57 let url: string
58
59 const keys = require(buildAbsoluteFixturePath('./ap-json/peertube/keys.json'))
60 const invalidKeys = require(buildAbsoluteFixturePath('./ap-json/peertube/invalid-keys.json'))
61 const baseHttpSignature = () => ({
62 algorithm: HTTP_SIGNATURE.ALGORITHM,
63 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
64 keyId: 'acct:peertube@localhost:' + servers[1].port,
65 key: keys.privateKey,
66 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
67 })
68
69 // ---------------------------------------------------------------
70
71 before(async function () {
72 this.timeout(60000)
73
74 servers = await createMultipleServers(3)
75
76 url = servers[0].url + '/inbox'
77
78 await setKeysOfServer(servers[0], servers[1], keys.publicKey, null)
79 await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
80
81 const to = { url: 'http://localhost:' + servers[0].port + '/accounts/peertube' }
82 const by = { url: 'http://localhost:' + servers[1].port + '/accounts/peertube', privateKey: keys.privateKey }
83 await makeFollowRequest(to, by)
84 })
85
86 describe('When checking HTTP signature', function () {
87
88 it('Should fail with an invalid digest', async function () {
89 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
90 const headers = {
91 Digest: buildDigest({ hello: 'coucou' })
92 }
93
94 try {
95 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
96 expect(true, 'Did not throw').to.be.false
97 } catch (err) {
98 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
99 }
100 })
101
102 it('Should fail with an invalid date', async function () {
103 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
104 const headers = buildGlobalHeaders(body)
105 headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
106
107 try {
108 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
109 expect(true, 'Did not throw').to.be.false
110 } catch (err) {
111 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
112 }
113 })
114
115 it('Should fail with bad keys', async function () {
116 await setKeysOfServer(servers[0], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
117 await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
118
119 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
120 const headers = buildGlobalHeaders(body)
121
122 try {
123 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
124 expect(true, 'Did not throw').to.be.false
125 } catch (err) {
126 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
127 }
128 })
129
130 it('Should reject requests without appropriate signed headers', async function () {
131 await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey)
132 await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
133
134 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
135 const headers = buildGlobalHeaders(body)
136
137 const signatureOptions = baseHttpSignature()
138 const badHeadersMatrix = [
139 [ '(request-target)', 'date', 'digest' ],
140 [ 'host', 'date', 'digest' ],
141 [ '(request-target)', 'host', 'digest' ]
142 ]
143
144 for (const badHeaders of badHeadersMatrix) {
145 signatureOptions.headers = badHeaders
146
147 try {
148 await makePOSTAPRequest(url, body, signatureOptions, headers)
149 expect(true, 'Did not throw').to.be.false
150 } catch (err) {
151 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
152 }
153 }
154 })
155
156 it('Should succeed with a valid HTTP signature', async function () {
157 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
158 const headers = buildGlobalHeaders(body)
159
160 const { statusCode } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
161 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
162 })
163
164 it('Should refresh the actor keys', async function () {
165 this.timeout(20000)
166
167 // Update keys of server 2 to invalid keys
168 // Server 1 should refresh the actor and fail
169 await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
170 await setUpdatedAtOfServer(servers[0], servers[1], '2015-07-17 22:00:00+00')
171
172 // Invalid peertube actor cache
173 await killallServers([ servers[1] ])
174 await servers[1].run()
175
176 const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
177 const headers = buildGlobalHeaders(body)
178
179 try {
180 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
181 expect(true, 'Did not throw').to.be.false
182 } catch (err) {
183 console.error(err)
184 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
185 }
186 })
187 })
188
189 describe('When checking Linked Data Signature', function () {
190 before(async function () {
191 this.timeout(10000)
192
193 await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey)
194 await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
195 await setKeysOfServer(servers[2], servers[2], keys.publicKey, keys.privateKey)
196
197 const to = { url: 'http://localhost:' + servers[0].port + '/accounts/peertube' }
198 const by = { url: 'http://localhost:' + servers[2].port + '/accounts/peertube', privateKey: keys.privateKey }
199 await makeFollowRequest(to, by)
200 })
201
202 it('Should fail with bad keys', async function () {
203 this.timeout(10000)
204
205 await setKeysOfServer(servers[0], servers[2], invalidKeys.publicKey, invalidKeys.privateKey)
206 await setKeysOfServer(servers[2], servers[2], invalidKeys.publicKey, invalidKeys.privateKey)
207
208 const body = getAnnounceWithoutContext(servers[1])
209 body.actor = 'http://localhost:' + servers[2].port + '/accounts/peertube'
210
211 const signer: any = { privateKey: invalidKeys.privateKey, url: 'http://localhost:' + servers[2].port + '/accounts/peertube' }
212 const signedBody = await buildSignedActivity(signer, body)
213
214 const headers = buildGlobalHeaders(signedBody)
215
216 try {
217 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
218 expect(true, 'Did not throw').to.be.false
219 } catch (err) {
220 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
221 }
222 })
223
224 it('Should fail with an altered body', async function () {
225 this.timeout(10000)
226
227 await setKeysOfServer(servers[0], servers[2], keys.publicKey, keys.privateKey)
228 await setKeysOfServer(servers[0], servers[2], keys.publicKey, keys.privateKey)
229
230 const body = getAnnounceWithoutContext(servers[1])
231 body.actor = 'http://localhost:' + servers[2].port + '/accounts/peertube'
232
233 const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:' + servers[2].port + '/accounts/peertube' }
234 const signedBody = await buildSignedActivity(signer, body)
235
236 signedBody.actor = 'http://localhost:' + servers[2].port + '/account/peertube'
237
238 const headers = buildGlobalHeaders(signedBody)
239
240 try {
241 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
242 expect(true, 'Did not throw').to.be.false
243 } catch (err) {
244 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
245 }
246 })
247
248 it('Should succeed with a valid signature', async function () {
249 this.timeout(10000)
250
251 const body = getAnnounceWithoutContext(servers[1])
252 body.actor = 'http://localhost:' + servers[2].port + '/accounts/peertube'
253
254 const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:' + servers[2].port + '/accounts/peertube' }
255 const signedBody = await buildSignedActivity(signer, body)
256
257 const headers = buildGlobalHeaders(signedBody)
258
259 const { statusCode } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
260 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
261 })
262
263 it('Should refresh the actor keys', async function () {
264 this.timeout(20000)
265
266 // Wait refresh invalidation
267 await wait(10000)
268
269 // Update keys of server 3 to invalid keys
270 // Server 1 should refresh the actor and fail
271 await setKeysOfServer(servers[2], servers[2], invalidKeys.publicKey, invalidKeys.privateKey)
272
273 const body = getAnnounceWithoutContext(servers[1])
274 body.actor = 'http://localhost:' + servers[2].port + '/accounts/peertube'
275
276 const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:' + servers[2].port + '/accounts/peertube' }
277 const signedBody = await buildSignedActivity(signer, body)
278
279 const headers = buildGlobalHeaders(signedBody)
280
281 try {
282 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
283 expect(true, 'Did not throw').to.be.false
284 } catch (err) {
285 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
286 }
287 })
288 })
289
290 after(async function () {
291 this.timeout(10000)
292
293 await cleanupTests(servers)
294 })
295 })