diff options
author | Chocobozzz <me@florianbigard.com> | 2023-07-31 14:34:36 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2023-08-11 15:02:33 +0200 |
commit | 3a4992633ee62d5edfbb484d9c6bcb3cf158489d (patch) | |
tree | e4510b39bdac9c318fdb4b47018d08f15368b8f0 /server/tests/api/activitypub/security.ts | |
parent | 04d1da5621d25d59bd5fa1543b725c497bf5d9a8 (diff) | |
download | PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.gz PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.tar.zst PeerTube-3a4992633ee62d5edfbb484d9c6bcb3cf158489d.zip |
Migrate server to ESM
Sorry for the very big commit that may lead to git log issues and merge
conflicts, but it's a major step forward:
* Server can be faster at startup because imports() are async and we can
easily lazy import big modules
* Angular doesn't seem to support ES import (with .js extension), so we
had to correctly organize peertube into a monorepo:
* Use yarn workspace feature
* Use typescript reference projects for dependencies
* Shared projects have been moved into "packages", each one is now a
node module (with a dedicated package.json/tsconfig.json)
* server/tools have been moved into apps/ and is now a dedicated app
bundled and published on NPM so users don't have to build peertube
cli tools manually
* server/tests have been moved into packages/ so we don't compile
them every time we want to run the server
* Use isolatedModule option:
* Had to move from const enum to const
(https://www.typescriptlang.org/docs/handbook/enums.html#objects-vs-enums)
* Had to explictely specify "type" imports when used in decorators
* Prefer tsx (that uses esbuild under the hood) instead of ts-node to
load typescript files (tests with mocha or scripts):
* To reduce test complexity as esbuild doesn't support decorator
metadata, we only test server files that do not import server
models
* We still build tests files into js files for a faster CI
* Remove unmaintained peertube CLI import script
* Removed some barrels to speed up execution (less imports)
Diffstat (limited to 'server/tests/api/activitypub/security.ts')
-rw-r--r-- | server/tests/api/activitypub/security.ts | 321 |
1 files changed, 0 insertions, 321 deletions
diff --git a/server/tests/api/activitypub/security.ts b/server/tests/api/activitypub/security.ts deleted file mode 100644 index 8e87361a9..000000000 --- a/server/tests/api/activitypub/security.ts +++ /dev/null | |||
@@ -1,321 +0,0 @@ | |||
1 | /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */ | ||
2 | |||
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' | ||
12 | |||
13 | function setKeysOfServer (onServer: SQLCommand, ofServerUrl: string, publicKey: string, privateKey: string) { | ||
14 | const url = ofServerUrl + '/accounts/peertube' | ||
15 | |||
16 | return Promise.all([ | ||
17 | onServer.setActorField(url, 'publicKey', publicKey), | ||
18 | onServer.setActorField(url, 'privateKey', privateKey) | ||
19 | ]) | ||
20 | } | ||
21 | |||
22 | function setUpdatedAtOfServer (onServer: SQLCommand, ofServerUrl: string, updatedAt: string) { | ||
23 | const url = ofServerUrl + '/accounts/peertube' | ||
24 | |||
25 | return Promise.all([ | ||
26 | onServer.setActorField(url, 'createdAt', updatedAt), | ||
27 | onServer.setActorField(url, 'updatedAt', updatedAt) | ||
28 | ]) | ||
29 | } | ||
30 | |||
31 | function getAnnounceWithoutContext (server: PeerTubeServer) { | ||
32 | const json = require(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json')) | ||
33 | const result: typeof json = {} | ||
34 | |||
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}`)) | ||
38 | } else { | ||
39 | result[key] = json[key].replace(':9002', `:${server.port}`) | ||
40 | } | ||
41 | } | ||
42 | |||
43 | return result | ||
44 | } | ||
45 | |||
46 | async 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_WITH_PAYLOAD | ||
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 | |||
72 | describe('Test ActivityPub security', function () { | ||
73 | let servers: PeerTubeServer[] | ||
74 | let sqlCommands: SQLCommand[] = [] | ||
75 | |||
76 | let url: string | ||
77 | |||
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, | ||
84 | key: keys.privateKey, | ||
85 | headers: HTTP_SIGNATURE.HEADERS_TO_SIGN_WITH_PAYLOAD | ||
86 | }) | ||
87 | |||
88 | // --------------------------------------------------------------- | ||
89 | |||
90 | before(async function () { | ||
91 | this.timeout(60000) | ||
92 | |||
93 | servers = await createMultipleServers(3) | ||
94 | |||
95 | sqlCommands = servers.map(s => new SQLCommand(s)) | ||
96 | |||
97 | url = servers[0].url + '/inbox' | ||
98 | |||
99 | await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, null) | ||
100 | await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey) | ||
101 | |||
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) | ||
105 | }) | ||
106 | |||
107 | describe('When checking HTTP signature', function () { | ||
108 | |||
109 | it('Should fail with an invalid digest', async function () { | ||
110 | const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') | ||
111 | const headers = { | ||
112 | Digest: buildDigest({ hello: 'coucou' }) | ||
113 | } | ||
114 | |||
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 | } | ||
121 | }) | ||
122 | |||
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' | ||
127 | |||
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 | } | ||
134 | }) | ||
135 | |||
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) | ||
139 | |||
140 | const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') | ||
141 | const headers = buildGlobalHeaders(body) | ||
142 | |||
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 | } | ||
149 | }) | ||
150 | |||
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) | ||
154 | |||
155 | const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') | ||
156 | const headers = buildGlobalHeaders(body) | ||
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 | |||
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 | } | ||
174 | } | ||
175 | }) | ||
176 | |||
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) | ||
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 | |||
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) | ||
191 | |||
192 | const { statusCode } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers) | ||
193 | expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204) | ||
194 | }) | ||
195 | |||
196 | it('Should refresh the actor keys', async function () { | ||
197 | this.timeout(20000) | ||
198 | |||
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') | ||
203 | |||
204 | // Invalid peertube actor cache | ||
205 | await killallServers([ servers[1] ]) | ||
206 | await servers[1].run() | ||
207 | |||
208 | const body = await activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce') | ||
209 | const headers = buildGlobalHeaders(body) | ||
210 | |||
211 | try { | ||
212 | await makePOSTAPRequest(url, body, baseHttpSignature(), headers) | ||
213 | expect(true, 'Did not throw').to.be.false | ||
214 | } catch (err) { | ||
215 | console.error(err) | ||
216 | expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
217 | } | ||
218 | }) | ||
219 | }) | ||
220 | |||
221 | describe('When checking Linked Data Signature', function () { | ||
222 | before(async function () { | ||
223 | await setKeysOfServer(sqlCommands[0], servers[1].url, keys.publicKey, keys.privateKey) | ||
224 | await setKeysOfServer(sqlCommands[1], servers[1].url, keys.publicKey, keys.privateKey) | ||
225 | await setKeysOfServer(sqlCommands[2], servers[2].url, keys.publicKey, keys.privateKey) | ||
226 | |||
227 | const to = { url: servers[0].url + '/accounts/peertube' } | ||
228 | const by = { url: servers[2].url + '/accounts/peertube', privateKey: keys.privateKey } | ||
229 | await makeFollowRequest(to, by) | ||
230 | }) | ||
231 | |||
232 | it('Should fail with bad keys', async function () { | ||
233 | await setKeysOfServer(sqlCommands[0], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey) | ||
234 | await setKeysOfServer(sqlCommands[2], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey) | ||
235 | |||
236 | const body = getAnnounceWithoutContext(servers[1]) | ||
237 | body.actor = servers[2].url + '/accounts/peertube' | ||
238 | |||
239 | const signer: any = { privateKey: invalidKeys.privateKey, url: servers[2].url + '/accounts/peertube' } | ||
240 | const signedBody = await signAndContextify(signer, body, 'Announce') | ||
241 | |||
242 | const headers = buildGlobalHeaders(signedBody) | ||
243 | |||
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 | } | ||
250 | }) | ||
251 | |||
252 | it('Should fail with an altered body', async function () { | ||
253 | await setKeysOfServer(sqlCommands[0], servers[2].url, keys.publicKey, keys.privateKey) | ||
254 | await setKeysOfServer(sqlCommands[0], servers[2].url, keys.publicKey, keys.privateKey) | ||
255 | |||
256 | const body = getAnnounceWithoutContext(servers[1]) | ||
257 | body.actor = servers[2].url + '/accounts/peertube' | ||
258 | |||
259 | const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' } | ||
260 | const signedBody = await signAndContextify(signer, body, 'Announce') | ||
261 | |||
262 | signedBody.actor = servers[2].url + '/account/peertube' | ||
263 | |||
264 | const headers = buildGlobalHeaders(signedBody) | ||
265 | |||
266 | try { | ||
267 | await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) | ||
268 | expect(true, 'Did not throw').to.be.false | ||
269 | } catch (err) { | ||
270 | expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
271 | } | ||
272 | }) | ||
273 | |||
274 | it('Should succeed with a valid signature', async function () { | ||
275 | const body = getAnnounceWithoutContext(servers[1]) | ||
276 | body.actor = servers[2].url + '/accounts/peertube' | ||
277 | |||
278 | const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' } | ||
279 | const signedBody = await signAndContextify(signer, body, 'Announce') | ||
280 | |||
281 | const headers = buildGlobalHeaders(signedBody) | ||
282 | |||
283 | const { statusCode } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) | ||
284 | expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204) | ||
285 | }) | ||
286 | |||
287 | it('Should refresh the actor keys', async function () { | ||
288 | this.timeout(20000) | ||
289 | |||
290 | // Wait refresh invalidation | ||
291 | await wait(10000) | ||
292 | |||
293 | // Update keys of server 3 to invalid keys | ||
294 | // Server 1 should refresh the actor and fail | ||
295 | await setKeysOfServer(sqlCommands[2], servers[2].url, invalidKeys.publicKey, invalidKeys.privateKey) | ||
296 | |||
297 | const body = getAnnounceWithoutContext(servers[1]) | ||
298 | body.actor = servers[2].url + '/accounts/peertube' | ||
299 | |||
300 | const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' } | ||
301 | const signedBody = await signAndContextify(signer, body, 'Announce') | ||
302 | |||
303 | const headers = buildGlobalHeaders(signedBody) | ||
304 | |||
305 | try { | ||
306 | await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers) | ||
307 | expect(true, 'Did not throw').to.be.false | ||
308 | } catch (err) { | ||
309 | expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403) | ||
310 | } | ||
311 | }) | ||
312 | }) | ||
313 | |||
314 | after(async function () { | ||
315 | for (const sql of sqlCommands) { | ||
316 | await sql.cleanup() | ||
317 | } | ||
318 | |||
319 | await cleanupTests(servers) | ||
320 | }) | ||
321 | }) | ||