1 /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
3 import { expect } from 'chai'
4 import { buildDigest } from '@server/helpers/peertube-crypto'
5 import { ACTIVITY_PUB, HTTP_SIGNATURE } from '@server/initializers/constants'
6 import { activityPubContextify } from '@server/lib/activitypub/context'
7 import { buildGlobalHeaders, signAndContextify } from '@server/lib/activitypub/send'
8 import { makePOSTAPRequest, SQLCommand } from '@server/tests/shared'
9 import { buildAbsoluteFixturePath, wait } from '@shared/core-utils'
10 import { HttpStatusCode } from '@shared/models'
11 import { cleanupTests, createMultipleServers, killallServers, PeerTubeServer } from '@shared/server-commands'
13 function setKeysOfServer (onServer: SQLCommand, ofServerUrl: string, publicKey: string, privateKey: string) {
14 const url = ofServerUrl + '/accounts/peertube'
17 onServer.setActorField(url, 'publicKey', publicKey),
18 onServer.setActorField(url, 'privateKey', privateKey)
22 function setUpdatedAtOfServer (onServer: SQLCommand, ofServerUrl: string, updatedAt: string) {
23 const url = ofServerUrl + '/accounts/peertube'
26 onServer.setActorField(url, 'createdAt', updatedAt),
27 onServer.setActorField(url, 'updatedAt', updatedAt)
31 function getAnnounceWithoutContext (server: PeerTubeServer) {
32 const json = require(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json'))
33 const result: typeof json = {}
35 for (const key of Object.keys(json)) {
36 if (Array.isArray(json[key])) {
37 result[key] = json[key].map(v => v.replace(':9002', `:${server.port}`))
39 result[key] = json[key].replace(':9002', `:${server.port}`)
46 async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) {
49 id: by.url + '/' + new Date().getTime(),
54 const body = await activityPubContextify(follow, 'Follow')
56 const httpSignature = {
57 algorithm: HTTP_SIGNATURE.ALGORITHM,
58 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
61 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
64 'digest': buildDigest(body),
65 'content-type': 'application/activity+json',
66 'accept': ACTIVITY_PUB.ACCEPT_HEADER
69 return makePOSTAPRequest(to.url + '/inbox', body, httpSignature, headers)
72 describe('Test ActivityPub security', function () {
73 let servers: PeerTubeServer[]
74 let sqlCommands: SQLCommand[] = []
78 const keys = require(buildAbsoluteFixturePath('./ap-json/peertube/keys.json'))
79 const invalidKeys = require(buildAbsoluteFixturePath('./ap-json/peertube/invalid-keys.json'))
80 const baseHttpSignature = () => ({
81 algorithm: HTTP_SIGNATURE.ALGORITHM,
82 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
83 keyId: 'acct:peertube@' + servers[1].host,
85 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
88 // ---------------------------------------------------------------
90 before(async function () {
93 servers = await createMultipleServers(3)
95 sqlCommands = servers.map(s => new SQLCommand(s))
97 url = servers[0].url + '/inbox'
99 await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, null)
100 await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey)
102 const to = { url: servers[0].url + '/accounts/peertube' }
103 const by = { url: servers[1].url + '/accounts/peertube', privateKey: keys.privateKey }
104 await makeFollowRequest(to, by)
107 describe('When checking HTTP signature', function () {
109 it('Should fail with an invalid digest', async function () {
110 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
112 Digest: buildDigest({ hello: 'coucou' })
116 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
117 expect(true, 'Did not throw').to.be.false
119 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
123 it('Should fail with an invalid date', async function () {
124 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
125 const headers = buildGlobalHeaders(body)
126 headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
129 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
130 expect(true, 'Did not throw').to.be.false
132 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
136 it('Should fail with bad keys', async function () {
137 await setKeysOfServer(sqlCommands[0], servers[1].url, invalidKeys.publicKey, invalidKeys.privateKey)
138 await setKeysOfServer(sqlCommands[1], servers[1].url, invalidKeys.publicKey, invalidKeys.privateKey)
140 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
141 const headers = buildGlobalHeaders(body)
144 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
145 expect(true, 'Did not throw').to.be.false
147 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
151 it('Should reject requests without appropriate signed headers', async function () {
152 await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, keys.privateKey)
153 await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey)
155 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
156 const headers = buildGlobalHeaders(body)
158 const signatureOptions = baseHttpSignature()
159 const badHeadersMatrix = [
160 [ '(request-target)', 'date', 'digest' ],
161 [ 'host', 'date', 'digest' ],
162 [ '(request-target)', 'host', 'digest' ]
165 for (const badHeaders of badHeadersMatrix) {
166 signatureOptions.headers = badHeaders
169 await makePOSTAPRequest(url, body, signatureOptions, headers)
170 expect(true, 'Did not throw').to.be.false
172 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
177 it('Should succeed with a valid HTTP signature draft 11 (without date but with (created))', async function () {
178 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
179 const headers = buildGlobalHeaders(body)
181 const signatureOptions = baseHttpSignature()
182 signatureOptions.headers = [ '(request-target)', '(created)', 'host', 'digest' ]
184 const { statusCode } = await makePOSTAPRequest(url, body, signatureOptions, headers)
185 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
188 it('Should succeed with a valid HTTP signature', async function () {
189 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
190 const headers = buildGlobalHeaders(body)
192 const { statusCode } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
193 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
196 it('Should refresh the actor keys', async function () {
199 // Update keys of server 2 to invalid keys
200 // Server 1 should refresh the actor and fail
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')
204 // Invalid peertube actor cache
205 await killallServers([ servers[1] ])
206 await servers[1].run()
208 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
209 const headers = buildGlobalHeaders(body)
212 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
213 expect(true, 'Did not throw').to.be.false
216 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
221 describe('When checking Linked Data Signature', function () {
222 before(async function () {
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)
229 const to = { url: servers[0].url + '/accounts/peertube' }
230 const by = { url: servers[2].url + '/accounts/peertube', privateKey: keys.privateKey }
231 await makeFollowRequest(to, by)
234 it('Should fail with bad keys', async function () {
237 await setKeysOfServer(sqlCommands[0], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey)
238 await setKeysOfServer(sqlCommands[2], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey)
240 const body = getAnnounceWithoutContext(servers[1])
241 body.actor = servers[2].url + '/accounts/peertube'
243 const signer: any = { privateKey: invalidKeys.privateKey, url: servers[2].url + '/accounts/peertube' }
244 const signedBody = await signAndContextify(signer, body, 'Announce')
246 const headers = buildGlobalHeaders(signedBody)
249 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
250 expect(true, 'Did not throw').to.be.false
252 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
256 it('Should fail with an altered body', async function () {
259 await setKeysOfServer(sqlCommands[0], servers[2].url, keys.publicKey, keys.privateKey)
260 await setKeysOfServer(sqlCommands[0], servers[2].url, keys.publicKey, keys.privateKey)
262 const body = getAnnounceWithoutContext(servers[1])
263 body.actor = servers[2].url + '/accounts/peertube'
265 const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
266 const signedBody = await signAndContextify(signer, body, 'Announce')
268 signedBody.actor = servers[2].url + '/account/peertube'
270 const headers = buildGlobalHeaders(signedBody)
273 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
274 expect(true, 'Did not throw').to.be.false
276 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
280 it('Should succeed with a valid signature', async function () {
283 const body = getAnnounceWithoutContext(servers[1])
284 body.actor = servers[2].url + '/accounts/peertube'
286 const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
287 const signedBody = await signAndContextify(signer, body, 'Announce')
289 const headers = buildGlobalHeaders(signedBody)
291 const { statusCode } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
292 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
295 it('Should refresh the actor keys', async function () {
298 // Wait refresh invalidation
301 // Update keys of server 3 to invalid keys
302 // Server 1 should refresh the actor and fail
303 await setKeysOfServer(sqlCommands[2], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey)
305 const body = getAnnounceWithoutContext(servers[1])
306 body.actor = servers[2].url + '/accounts/peertube'
308 const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
309 const signedBody = await signAndContextify(signer, body, 'Announce')
311 const headers = buildGlobalHeaders(signedBody)
314 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
315 expect(true, 'Did not throw').to.be.false
317 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
322 after(async function () {
323 for (const sql of sqlCommands) {
327 await cleanupTests(servers)