]> git.immae.eu Git - github/Chocobozzz/PeerTube.git/blame - server/tests/api/activitypub/security.ts
Fix tests
[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'
d102de1b 8import { makePOSTAPRequest, SQLCommand } 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
d102de1b
C
13function setKeysOfServer (onServer: SQLCommand, ofServerUrl: string, publicKey: string, privateKey: string) {
14 const url = ofServerUrl + '/accounts/peertube'
e7053b1d
C
15
16 return Promise.all([
d102de1b
C
17 onServer.setActorField(url, 'publicKey', publicKey),
18 onServer.setActorField(url, 'privateKey', privateKey)
e7053b1d
C
19 ])
20}
21
d102de1b
C
22function setUpdatedAtOfServer (onServer: SQLCommand, ofServerUrl: string, updatedAt: string) {
23 const url = ofServerUrl + '/accounts/peertube'
e7053b1d 24
df66d815 25 return Promise.all([
d102de1b
C
26 onServer.setActorField(url, 'createdAt', updatedAt),
27 onServer.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[]
c106db14 74 let sqlCommands: SQLCommand[] = []
d102de1b 75
df66d815
C
76 let url: string
77
3d470a53
C
78 const keys = require(buildAbsoluteFixturePath('./ap-json/peertube/keys.json'))
79 const invalidKeys = require(buildAbsoluteFixturePath('./ap-json/peertube/invalid-keys.json'))
48f07b4a 80 const baseHttpSignature = () => ({
df66d815
C
81 algorithm: HTTP_SIGNATURE.ALGORITHM,
82 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
2732eeff 83 keyId: 'acct:peertube@' + servers[1].host,
df66d815
C
84 key: keys.privateKey,
85 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
48f07b4a 86 })
df66d815
C
87
88 // ---------------------------------------------------------------
89
90 before(async function () {
91 this.timeout(60000)
92
254d3579 93 servers = await createMultipleServers(3)
df66d815 94
d102de1b
C
95 sqlCommands = servers.map(s => new SQLCommand(s))
96
df66d815
C
97 url = servers[0].url + '/inbox'
98
d102de1b
C
99 await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, null)
100 await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey)
df66d815 101
2732eeff
C
102 const to = { url: servers[0].url + '/accounts/peertube' }
103 const by = { url: servers[1].url + '/accounts/peertube', privateKey: keys.privateKey }
df66d815
C
104 await makeFollowRequest(to, by)
105 })
106
107 describe('When checking HTTP signature', function () {
108
109 it('Should fail with an invalid digest', async function () {
3b504f6e 110 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
df66d815
C
111 const headers = {
112 Digest: buildDigest({ hello: 'coucou' })
113 }
114
b5c36108
C
115 try {
116 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
117 expect(true, 'Did not throw').to.be.false
118 } catch (err) {
119 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
120 }
df66d815
C
121 })
122
123 it('Should fail with an invalid date', async function () {
3b504f6e 124 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
df66d815
C
125 const headers = buildGlobalHeaders(body)
126 headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
127
b5c36108
C
128 try {
129 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
130 expect(true, 'Did not throw').to.be.false
131 } catch (err) {
132 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
133 }
df66d815
C
134 })
135
136 it('Should fail with bad keys', async function () {
d102de1b
C
137 await setKeysOfServer(sqlCommands[0], servers[1].url, invalidKeys.publicKey, invalidKeys.privateKey)
138 await setKeysOfServer(sqlCommands[1], servers[1].url, invalidKeys.publicKey, invalidKeys.privateKey)
df66d815 139
3b504f6e 140 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
df66d815
C
141 const headers = buildGlobalHeaders(body)
142
b5c36108
C
143 try {
144 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
145 expect(true, 'Did not throw').to.be.false
146 } catch (err) {
147 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
148 }
df66d815
C
149 })
150
797d05bd 151 it('Should reject requests without appropriate signed headers', async function () {
d102de1b
C
152 await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, keys.privateKey)
153 await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey)
df66d815 154
3b504f6e 155 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
df66d815 156 const headers = buildGlobalHeaders(body)
797d05bd
C
157
158 const signatureOptions = baseHttpSignature()
159 const badHeadersMatrix = [
160 [ '(request-target)', 'date', 'digest' ],
161 [ 'host', 'date', 'digest' ],
162 [ '(request-target)', 'host', 'digest' ]
163 ]
164
165 for (const badHeaders of badHeadersMatrix) {
166 signatureOptions.headers = badHeaders
167
b5c36108
C
168 try {
169 await makePOSTAPRequest(url, body, signatureOptions, headers)
170 expect(true, 'Did not throw').to.be.false
171 } catch (err) {
172 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
173 }
797d05bd
C
174 }
175 })
176
e08ec7a7 177 it('Should succeed with a valid HTTP signature draft 11 (without date but with (created))', async function () {
3b504f6e 178 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
e08ec7a7
C
179 const headers = buildGlobalHeaders(body)
180
181 const signatureOptions = baseHttpSignature()
182 signatureOptions.headers = [ '(request-target)', '(created)', 'host', 'digest' ]
183
184 const { statusCode } = await makePOSTAPRequest(url, body, signatureOptions, headers)
185 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
186 })
187
797d05bd 188 it('Should succeed with a valid HTTP signature', async function () {
3b504f6e 189 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
797d05bd 190 const headers = buildGlobalHeaders(body)
df66d815 191
db4b15f2 192 const { statusCode } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
db4b15f2 193 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
df66d815 194 })
095e2258
C
195
196 it('Should refresh the actor keys', async function () {
197 this.timeout(20000)
198
095e2258
C
199 // Update keys of server 2 to invalid keys
200 // Server 1 should refresh the actor and fail
d102de1b
C
201 await setKeysOfServer(sqlCommands[1], servers[1].url, invalidKeys.publicKey, invalidKeys.privateKey)
202 await setUpdatedAtOfServer(sqlCommands[0], servers[1].url, '2015-07-17 22:00:00+00')
e7053b1d
C
203
204 // Invalid peertube actor cache
9293139f 205 await killallServers([ servers[1] ])
254d3579 206 await servers[1].run()
095e2258 207
3b504f6e 208 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
095e2258
C
209 const headers = buildGlobalHeaders(body)
210
b5c36108
C
211 try {
212 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
213 expect(true, 'Did not throw').to.be.false
214 } catch (err) {
e7053b1d 215 console.error(err)
b5c36108
C
216 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
217 }
095e2258 218 })
df66d815
C
219 })
220
221 describe('When checking Linked Data Signature', function () {
095e2258
C
222 before(async function () {
223 this.timeout(10000)
224
d102de1b
C
225 await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, keys.privateKey)
226 await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey)
227 await setKeysOfServer(sqlCommands[2], servers[2].url, keys.publicKey, keys.privateKey)
df66d815 228
2732eeff
C
229 const to = { url: servers[0].url + '/accounts/peertube' }
230 const by = { url: servers[2].url + '/accounts/peertube', privateKey: keys.privateKey }
df66d815
C
231 await makeFollowRequest(to, by)
232 })
233
234 it('Should fail with bad keys', async function () {
235 this.timeout(10000)
236
d102de1b
C
237 await setKeysOfServer(sqlCommands[0], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey)
238 await setKeysOfServer(sqlCommands[2], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey)
df66d815 239
48f07b4a 240 const body = getAnnounceWithoutContext(servers[1])
2732eeff 241 body.actor = servers[2].url + '/accounts/peertube'
df66d815 242
2732eeff 243 const signer: any = { privateKey: invalidKeys.privateKey, url: servers[2].url + '/accounts/peertube' }
a219c910 244 const signedBody = await signAndContextify(signer, body, 'Announce')
df66d815
C
245
246 const headers = buildGlobalHeaders(signedBody)
247
b5c36108
C
248 try {
249 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
250 expect(true, 'Did not throw').to.be.false
251 } catch (err) {
252 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
253 }
df66d815
C
254 })
255
256 it('Should fail with an altered body', async function () {
257 this.timeout(10000)
258
d102de1b
C
259 await setKeysOfServer(sqlCommands[0], servers[2].url, keys.publicKey, keys.privateKey)
260 await setKeysOfServer(sqlCommands[0], servers[2].url, keys.publicKey, keys.privateKey)
df66d815 261
48f07b4a 262 const body = getAnnounceWithoutContext(servers[1])
2732eeff 263 body.actor = servers[2].url + '/accounts/peertube'
df66d815 264
2732eeff 265 const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
a219c910 266 const signedBody = await signAndContextify(signer, body, 'Announce')
df66d815 267
2732eeff 268 signedBody.actor = servers[2].url + '/account/peertube'
df66d815
C
269
270 const headers = buildGlobalHeaders(signedBody)
271
b5c36108
C
272 try {
273 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
274 expect(true, 'Did not throw').to.be.false
275 } catch (err) {
276 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
277 }
df66d815
C
278 })
279
280 it('Should succeed with a valid signature', async function () {
281 this.timeout(10000)
282
48f07b4a 283 const body = getAnnounceWithoutContext(servers[1])
2732eeff 284 body.actor = servers[2].url + '/accounts/peertube'
df66d815 285
2732eeff 286 const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
a219c910 287 const signedBody = await signAndContextify(signer, body, 'Announce')
df66d815
C
288
289 const headers = buildGlobalHeaders(signedBody)
290
db4b15f2 291 const { statusCode } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
db4b15f2 292 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
df66d815 293 })
095e2258
C
294
295 it('Should refresh the actor keys', async function () {
296 this.timeout(20000)
297
298 // Wait refresh invalidation
299 await wait(10000)
300
301 // Update keys of server 3 to invalid keys
302 // Server 1 should refresh the actor and fail
d102de1b 303 await setKeysOfServer(sqlCommands[2], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey)
095e2258
C
304
305 const body = getAnnounceWithoutContext(servers[1])
2732eeff 306 body.actor = servers[2].url + '/accounts/peertube'
095e2258 307
2732eeff 308 const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
a219c910 309 const signedBody = await signAndContextify(signer, body, 'Announce')
095e2258
C
310
311 const headers = buildGlobalHeaders(signedBody)
312
b5c36108
C
313 try {
314 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
315 expect(true, 'Did not throw').to.be.false
316 } catch (err) {
317 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
318 }
095e2258 319 })
df66d815
C
320 })
321
322 after(async function () {
d102de1b
C
323 for (const sql of sqlCommands) {
324 await sql.cleanup()
325 }
48f07b4a
C
326
327 await cleanupTests(servers)
df66d815
C
328 })
329})