security.ts 11 KB
Newer Older
Chocobozzz's avatar
Chocobozzz committed
1
/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3
4

import 'mocha'
import * as chai from 'chai'
Chocobozzz's avatar
Chocobozzz committed
5
import { buildDigest } from '@server/helpers/peertube-crypto'
6
import { HttpStatusCode } from '../../../../shared/core-utils/miscs/http-error-codes'
7
import {
Chocobozzz's avatar
Chocobozzz committed
8
  buildAbsoluteFixturePath,
9
  cleanupTests,
10
  createMultipleServers,
Chocobozzz's avatar
Chocobozzz committed
11
  killallServers,
12
  PeerTubeServer,
13
14
15
16
17
18
  wait
} from '../../../../shared/extra-utils'
import { makeFollowRequest, makePOSTAPRequest } from '../../../../shared/extra-utils/requests/activitypub'
import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub'
import { HTTP_SIGNATURE } from '../../../initializers/constants'
import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
19
20
21

const expect = chai.expect

22
function setKeysOfServer (onServer: PeerTubeServer, ofServer: PeerTubeServer, publicKey: string, privateKey: string) {
Chocobozzz's avatar
Chocobozzz committed
23
24
25
  const url = 'http://localhost:' + ofServer.port + '/accounts/peertube'

  return Promise.all([
Chocobozzz's avatar
Chocobozzz committed
26
27
    onServer.sql.setActorField(url, 'publicKey', publicKey),
    onServer.sql.setActorField(url, 'privateKey', privateKey)
Chocobozzz's avatar
Chocobozzz committed
28
29
30
  ])
}

31
function setUpdatedAtOfServer (onServer: PeerTubeServer, ofServer: PeerTubeServer, updatedAt: string) {
Chocobozzz's avatar
Chocobozzz committed
32
33
  const url = 'http://localhost:' + ofServer.port + '/accounts/peertube'

34
  return Promise.all([
Chocobozzz's avatar
Chocobozzz committed
35
36
    onServer.sql.setActorField(url, 'createdAt', updatedAt),
    onServer.sql.setActorField(url, 'updatedAt', updatedAt)
37
38
39
  ])
}

40
function getAnnounceWithoutContext (server: PeerTubeServer) {
Chocobozzz's avatar
Chocobozzz committed
41
  const json = require(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json'))
Chocobozzz's avatar
Chocobozzz committed
42
43
44
45
  const result: typeof json = {}

  for (const key of Object.keys(json)) {
    if (Array.isArray(json[key])) {
Chocobozzz's avatar
Chocobozzz committed
46
      result[key] = json[key].map(v => v.replace(':9002', `:${server.port}`))
Chocobozzz's avatar
Chocobozzz committed
47
    } else {
Chocobozzz's avatar
Chocobozzz committed
48
      result[key] = json[key].replace(':9002', `:${server.port}`)
Chocobozzz's avatar
Chocobozzz committed
49
50
51
52
    }
  }

  return result
53
54
55
}

describe('Test ActivityPub security', function () {
56
  let servers: PeerTubeServer[]
57
58
  let url: string

Chocobozzz's avatar
Chocobozzz committed
59
60
  const keys = require(buildAbsoluteFixturePath('./ap-json/peertube/keys.json'))
  const invalidKeys = require(buildAbsoluteFixturePath('./ap-json/peertube/invalid-keys.json'))
Chocobozzz's avatar
Chocobozzz committed
61
  const baseHttpSignature = () => ({
62
63
    algorithm: HTTP_SIGNATURE.ALGORITHM,
    authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
Chocobozzz's avatar
Chocobozzz committed
64
    keyId: 'acct:peertube@localhost:' + servers[1].port,
65
66
    key: keys.privateKey,
    headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
Chocobozzz's avatar
Chocobozzz committed
67
  })
68
69
70
71
72
73

  // ---------------------------------------------------------------

  before(async function () {
    this.timeout(60000)

74
    servers = await createMultipleServers(3)
75
76
77

    url = servers[0].url + '/inbox'

Chocobozzz's avatar
Chocobozzz committed
78
79
    await setKeysOfServer(servers[0], servers[1], keys.publicKey, null)
    await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
80

Chocobozzz's avatar
Chocobozzz committed
81
82
    const to = { url: 'http://localhost:' + servers[0].port + '/accounts/peertube' }
    const by = { url: 'http://localhost:' + servers[1].port + '/accounts/peertube', privateKey: keys.privateKey }
83
84
85
86
87
88
    await makeFollowRequest(to, by)
  })

  describe('When checking HTTP signature', function () {

    it('Should fail with an invalid digest', async function () {
Chocobozzz's avatar
Chocobozzz committed
89
      const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
90
91
92
93
      const headers = {
        Digest: buildDigest({ hello: 'coucou' })
      }

Chocobozzz's avatar
Chocobozzz committed
94
95
96
97
98
99
      try {
        await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
        expect(true, 'Did not throw').to.be.false
      } catch (err) {
        expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
      }
100
101
102
    })

    it('Should fail with an invalid date', async function () {
Chocobozzz's avatar
Chocobozzz committed
103
      const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
104
105
106
      const headers = buildGlobalHeaders(body)
      headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'

Chocobozzz's avatar
Chocobozzz committed
107
108
109
110
111
112
      try {
        await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
        expect(true, 'Did not throw').to.be.false
      } catch (err) {
        expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
      }
113
114
115
    })

    it('Should fail with bad keys', async function () {
Chocobozzz's avatar
Chocobozzz committed
116
117
      await setKeysOfServer(servers[0], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
      await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
118

Chocobozzz's avatar
Chocobozzz committed
119
      const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
120
121
      const headers = buildGlobalHeaders(body)

Chocobozzz's avatar
Chocobozzz committed
122
123
124
125
126
127
      try {
        await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
        expect(true, 'Did not throw').to.be.false
      } catch (err) {
        expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
      }
128
129
    })

130
    it('Should reject requests without appropriate signed headers', async function () {
Chocobozzz's avatar
Chocobozzz committed
131
132
      await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey)
      await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
133

Chocobozzz's avatar
Chocobozzz committed
134
      const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
135
      const headers = buildGlobalHeaders(body)
136
137
138
139
140
141
142
143
144
145
146

      const signatureOptions = baseHttpSignature()
      const badHeadersMatrix = [
        [ '(request-target)', 'date', 'digest' ],
        [ 'host', 'date', 'digest' ],
        [ '(request-target)', 'host', 'digest' ]
      ]

      for (const badHeaders of badHeadersMatrix) {
        signatureOptions.headers = badHeaders

Chocobozzz's avatar
Chocobozzz committed
147
148
149
150
151
152
        try {
          await makePOSTAPRequest(url, body, signatureOptions, headers)
          expect(true, 'Did not throw').to.be.false
        } catch (err) {
          expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
        }
153
154
155
156
157
158
      }
    })

    it('Should succeed with a valid HTTP signature', async function () {
      const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
      const headers = buildGlobalHeaders(body)
159

Chocobozzz's avatar
Chocobozzz committed
160
161
      const { statusCode } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
      expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
162
    })
163
164
165
166
167
168
169

    it('Should refresh the actor keys', async function () {
      this.timeout(20000)

      // Update keys of server 2 to invalid keys
      // Server 1 should refresh the actor and fail
      await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
Chocobozzz's avatar
Chocobozzz committed
170
171
172
      await setUpdatedAtOfServer(servers[0], servers[1], '2015-07-17 22:00:00+00')

      // Invalid peertube actor cache
Chocobozzz's avatar
Chocobozzz committed
173
      await killallServers([ servers[1] ])
174
      await servers[1].run()
175
176
177
178

      const body = activityPubContextify(getAnnounceWithoutContext(servers[1]))
      const headers = buildGlobalHeaders(body)

Chocobozzz's avatar
Chocobozzz committed
179
180
181
182
      try {
        await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
        expect(true, 'Did not throw').to.be.false
      } catch (err) {
Chocobozzz's avatar
Chocobozzz committed
183
        console.error(err)
Chocobozzz's avatar
Chocobozzz committed
184
185
        expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
      }
186
    })
187
188
189
  })

  describe('When checking Linked Data Signature', function () {
190
191
192
193
194
    before(async function () {
      this.timeout(10000)

      await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey)
      await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
Chocobozzz's avatar
Chocobozzz committed
195
      await setKeysOfServer(servers[2], servers[2], keys.publicKey, keys.privateKey)
196

Chocobozzz's avatar
Chocobozzz committed
197
198
      const to = { url: 'http://localhost:' + servers[0].port + '/accounts/peertube' }
      const by = { url: 'http://localhost:' + servers[2].port + '/accounts/peertube', privateKey: keys.privateKey }
199
200
201
202
203
204
      await makeFollowRequest(to, by)
    })

    it('Should fail with bad keys', async function () {
      this.timeout(10000)

Chocobozzz's avatar
Chocobozzz committed
205
206
      await setKeysOfServer(servers[0], servers[2], invalidKeys.publicKey, invalidKeys.privateKey)
      await setKeysOfServer(servers[2], servers[2], invalidKeys.publicKey, invalidKeys.privateKey)
207

Chocobozzz's avatar
Chocobozzz committed
208
209
      const body = getAnnounceWithoutContext(servers[1])
      body.actor = 'http://localhost:' + servers[2].port + '/accounts/peertube'
210

Chocobozzz's avatar
Chocobozzz committed
211
      const signer: any = { privateKey: invalidKeys.privateKey, url: 'http://localhost:' + servers[2].port + '/accounts/peertube' }
212
213
214
215
      const signedBody = await buildSignedActivity(signer, body)

      const headers = buildGlobalHeaders(signedBody)

Chocobozzz's avatar
Chocobozzz committed
216
217
218
219
220
221
      try {
        await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
        expect(true, 'Did not throw').to.be.false
      } catch (err) {
        expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
      }
222
223
224
225
226
    })

    it('Should fail with an altered body', async function () {
      this.timeout(10000)

Chocobozzz's avatar
Chocobozzz committed
227
228
      await setKeysOfServer(servers[0], servers[2], keys.publicKey, keys.privateKey)
      await setKeysOfServer(servers[0], servers[2], keys.publicKey, keys.privateKey)
229

Chocobozzz's avatar
Chocobozzz committed
230
231
      const body = getAnnounceWithoutContext(servers[1])
      body.actor = 'http://localhost:' + servers[2].port + '/accounts/peertube'
232

Chocobozzz's avatar
Chocobozzz committed
233
      const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:' + servers[2].port + '/accounts/peertube' }
234
235
      const signedBody = await buildSignedActivity(signer, body)

Chocobozzz's avatar
Chocobozzz committed
236
      signedBody.actor = 'http://localhost:' + servers[2].port + '/account/peertube'
237
238
239

      const headers = buildGlobalHeaders(signedBody)

Chocobozzz's avatar
Chocobozzz committed
240
241
242
243
244
245
      try {
        await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
        expect(true, 'Did not throw').to.be.false
      } catch (err) {
        expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
      }
246
247
248
249
250
    })

    it('Should succeed with a valid signature', async function () {
      this.timeout(10000)

Chocobozzz's avatar
Chocobozzz committed
251
252
      const body = getAnnounceWithoutContext(servers[1])
      body.actor = 'http://localhost:' + servers[2].port + '/accounts/peertube'
253

Chocobozzz's avatar
Chocobozzz committed
254
      const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:' + servers[2].port + '/accounts/peertube' }
255
256
257
258
      const signedBody = await buildSignedActivity(signer, body)

      const headers = buildGlobalHeaders(signedBody)

Chocobozzz's avatar
Chocobozzz committed
259
260
      const { statusCode } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
      expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
261
    })
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280

    it('Should refresh the actor keys', async function () {
      this.timeout(20000)

      // Wait refresh invalidation
      await wait(10000)

      // Update keys of server 3 to invalid keys
      // Server 1 should refresh the actor and fail
      await setKeysOfServer(servers[2], servers[2], invalidKeys.publicKey, invalidKeys.privateKey)

      const body = getAnnounceWithoutContext(servers[1])
      body.actor = 'http://localhost:' + servers[2].port + '/accounts/peertube'

      const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:' + servers[2].port + '/accounts/peertube' }
      const signedBody = await buildSignedActivity(signer, body)

      const headers = buildGlobalHeaders(signedBody)

Chocobozzz's avatar
Chocobozzz committed
281
282
283
284
285
286
      try {
        await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
        expect(true, 'Did not throw').to.be.false
      } catch (err) {
        expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
      }
287
    })
288
289
290
  })

  after(async function () {
Chocobozzz's avatar
Chocobozzz committed
291
292
293
    this.timeout(10000)

    await cleanupTests(servers)
294
295
  })
})