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 } 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: PeerTubeServer, ofServer: PeerTubeServer, publicKey: string, privateKey: string) {
14 const url = ofServer.url + '/accounts/peertube'
17 onServer.sql.setActorField(url, 'publicKey', publicKey),
18 onServer.sql.setActorField(url, 'privateKey', privateKey)
22 function setUpdatedAtOfServer (onServer: PeerTubeServer, ofServer: PeerTubeServer, updatedAt: string) {
23 const url = ofServer.url + '/accounts/peertube'
26 onServer.sql.setActorField(url, 'createdAt', updatedAt),
27 onServer.sql.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[]
76 const keys = require(buildAbsoluteFixturePath('./ap-json/peertube/keys.json'))
77 const invalidKeys = require(buildAbsoluteFixturePath('./ap-json/peertube/invalid-keys.json'))
78 const baseHttpSignature = () => ({
79 algorithm: HTTP_SIGNATURE.ALGORITHM,
80 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
81 keyId: 'acct:peertube@' + servers[1].host,
83 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
86 // ---------------------------------------------------------------
88 before(async function () {
91 servers = await createMultipleServers(3)
93 url = servers[0].url + '/inbox'
95 await setKeysOfServer(servers[0], servers[1], keys.publicKey, null)
96 await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
98 const to = { url: servers[0].url + '/accounts/peertube' }
99 const by = { url: servers[1].url + '/accounts/peertube', privateKey: keys.privateKey }
100 await makeFollowRequest(to, by)
103 describe('When checking HTTP signature', function () {
105 it('Should fail with an invalid digest', async function () {
106 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
108 Digest: buildDigest({ hello: 'coucou' })
112 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
113 expect(true, 'Did not throw').to.be.false
115 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
119 it('Should fail with an invalid date', async function () {
120 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
121 const headers = buildGlobalHeaders(body)
122 headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
125 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
126 expect(true, 'Did not throw').to.be.false
128 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
132 it('Should fail with bad keys', async function () {
133 await setKeysOfServer(servers[0], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
134 await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
136 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
137 const headers = buildGlobalHeaders(body)
140 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
141 expect(true, 'Did not throw').to.be.false
143 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
147 it('Should reject requests without appropriate signed headers', async function () {
148 await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey)
149 await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
151 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
152 const headers = buildGlobalHeaders(body)
154 const signatureOptions = baseHttpSignature()
155 const badHeadersMatrix = [
156 [ '(request-target)', 'date', 'digest' ],
157 [ 'host', 'date', 'digest' ],
158 [ '(request-target)', 'host', 'digest' ]
161 for (const badHeaders of badHeadersMatrix) {
162 signatureOptions.headers = badHeaders
165 await makePOSTAPRequest(url, body, signatureOptions, headers)
166 expect(true, 'Did not throw').to.be.false
168 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
173 it('Should succeed with a valid HTTP signature draft 11 (without date but with (created))', async function () {
174 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
175 const headers = buildGlobalHeaders(body)
177 const signatureOptions = baseHttpSignature()
178 signatureOptions.headers = [ '(request-target)', '(created)', 'host', 'digest' ]
180 const { statusCode } = await makePOSTAPRequest(url, body, signatureOptions, headers)
181 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
184 it('Should succeed with a valid HTTP signature', async function () {
185 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
186 const headers = buildGlobalHeaders(body)
188 const { statusCode } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
189 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
192 it('Should refresh the actor keys', async function () {
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)
198 await setUpdatedAtOfServer(servers[0], servers[1], '2015-07-17 22:00:00+00')
200 // Invalid peertube actor cache
201 await killallServers([ servers[1] ])
202 await servers[1].run()
204 const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
205 const headers = buildGlobalHeaders(body)
208 await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
209 expect(true, 'Did not throw').to.be.false
212 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
217 describe('When checking Linked Data Signature', function () {
218 before(async function () {
221 await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey)
222 await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
223 await setKeysOfServer(servers[2], servers[2], keys.publicKey, keys.privateKey)
225 const to = { url: servers[0].url + '/accounts/peertube' }
226 const by = { url: servers[2].url + '/accounts/peertube', privateKey: keys.privateKey }
227 await makeFollowRequest(to, by)
230 it('Should fail with bad keys', async function () {
233 await setKeysOfServer(servers[0], servers[2], invalidKeys.publicKey, invalidKeys.privateKey)
234 await setKeysOfServer(servers[2], servers[2], invalidKeys.publicKey, invalidKeys.privateKey)
236 const body = getAnnounceWithoutContext(servers[1])
237 body.actor = servers[2].url + '/accounts/peertube'
239 const signer: any = { privateKey: invalidKeys.privateKey, url: servers[2].url + '/accounts/peertube' }
240 const signedBody = await signAndContextify(signer, body, 'Announce')
242 const headers = buildGlobalHeaders(signedBody)
245 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
246 expect(true, 'Did not throw').to.be.false
248 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
252 it('Should fail with an altered body', async function () {
255 await setKeysOfServer(servers[0], servers[2], keys.publicKey, keys.privateKey)
256 await setKeysOfServer(servers[0], servers[2], keys.publicKey, keys.privateKey)
258 const body = getAnnounceWithoutContext(servers[1])
259 body.actor = servers[2].url + '/accounts/peertube'
261 const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
262 const signedBody = await signAndContextify(signer, body, 'Announce')
264 signedBody.actor = servers[2].url + '/account/peertube'
266 const headers = buildGlobalHeaders(signedBody)
269 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
270 expect(true, 'Did not throw').to.be.false
272 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
276 it('Should succeed with a valid signature', async function () {
279 const body = getAnnounceWithoutContext(servers[1])
280 body.actor = servers[2].url + '/accounts/peertube'
282 const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
283 const signedBody = await signAndContextify(signer, body, 'Announce')
285 const headers = buildGlobalHeaders(signedBody)
287 const { statusCode } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
288 expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
291 it('Should refresh the actor keys', async function () {
294 // Wait refresh invalidation
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)
301 const body = getAnnounceWithoutContext(servers[1])
302 body.actor = servers[2].url + '/accounts/peertube'
304 const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
305 const signedBody = await signAndContextify(signer, body, 'Announce')
307 const headers = buildGlobalHeaders(signedBody)
310 await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
311 expect(true, 'Did not throw').to.be.false
313 expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
318 after(async function () {
321 await cleanupTests(servers)