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