security.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297
  1. /* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
  2. import { expect } from 'chai'
  3. import { buildDigest } from '@server/helpers/peertube-crypto'
  4. import { HTTP_SIGNATURE } from '@server/initializers/constants'
  5. import { activityPubContextify } from '@server/lib/activitypub/context'
  6. import { buildGlobalHeaders, signAndContextify } from '@server/lib/activitypub/send'
  7. import { makeFollowRequest, makePOSTAPRequest } from '@server/tests/shared'
  8. import { buildAbsoluteFixturePath, wait } from '@shared/core-utils'
  9. import { HttpStatusCode } from '@shared/models'
  10. import { cleanupTests, createMultipleServers, killallServers, PeerTubeServer } from '@shared/server-commands'
  11. function setKeysOfServer (onServer: PeerTubeServer, ofServer: PeerTubeServer, publicKey: string, privateKey: string) {
  12. const url = ofServer.url + '/accounts/peertube'
  13. return Promise.all([
  14. onServer.sql.setActorField(url, 'publicKey', publicKey),
  15. onServer.sql.setActorField(url, 'privateKey', privateKey)
  16. ])
  17. }
  18. function setUpdatedAtOfServer (onServer: PeerTubeServer, ofServer: PeerTubeServer, updatedAt: string) {
  19. const url = ofServer.url + '/accounts/peertube'
  20. return Promise.all([
  21. onServer.sql.setActorField(url, 'createdAt', updatedAt),
  22. onServer.sql.setActorField(url, 'updatedAt', updatedAt)
  23. ])
  24. }
  25. function getAnnounceWithoutContext (server: PeerTubeServer) {
  26. const json = require(buildAbsoluteFixturePath('./ap-json/peertube/announce-without-context.json'))
  27. const result: typeof json = {}
  28. for (const key of Object.keys(json)) {
  29. if (Array.isArray(json[key])) {
  30. result[key] = json[key].map(v => v.replace(':9002', `:${server.port}`))
  31. } else {
  32. result[key] = json[key].replace(':9002', `:${server.port}`)
  33. }
  34. }
  35. return result
  36. }
  37. describe('Test ActivityPub security', function () {
  38. let servers: PeerTubeServer[]
  39. let url: string
  40. const keys = require(buildAbsoluteFixturePath('./ap-json/peertube/keys.json'))
  41. const invalidKeys = require(buildAbsoluteFixturePath('./ap-json/peertube/invalid-keys.json'))
  42. const baseHttpSignature = () => ({
  43. algorithm: HTTP_SIGNATURE.ALGORITHM,
  44. authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
  45. keyId: 'acct:peertube@' + servers[1].host,
  46. key: keys.privateKey,
  47. headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
  48. })
  49. // ---------------------------------------------------------------
  50. before(async function () {
  51. this.timeout(60000)
  52. servers = await createMultipleServers(3)
  53. url = servers[0].url + '/inbox'
  54. await setKeysOfServer(servers[0], servers[1], keys.publicKey, null)
  55. await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
  56. const to = { url: servers[0].url + '/accounts/peertube' }
  57. const by = { url: servers[1].url + '/accounts/peertube', privateKey: keys.privateKey }
  58. await makeFollowRequest(to, by)
  59. })
  60. describe('When checking HTTP signature', function () {
  61. it('Should fail with an invalid digest', async function () {
  62. const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
  63. const headers = {
  64. Digest: buildDigest({ hello: 'coucou' })
  65. }
  66. try {
  67. await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
  68. expect(true, 'Did not throw').to.be.false
  69. } catch (err) {
  70. expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
  71. }
  72. })
  73. it('Should fail with an invalid date', async function () {
  74. const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
  75. const headers = buildGlobalHeaders(body)
  76. headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
  77. try {
  78. await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
  79. expect(true, 'Did not throw').to.be.false
  80. } catch (err) {
  81. expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
  82. }
  83. })
  84. it('Should fail with bad keys', async function () {
  85. await setKeysOfServer(servers[0], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
  86. await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
  87. const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
  88. const headers = buildGlobalHeaders(body)
  89. try {
  90. await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
  91. expect(true, 'Did not throw').to.be.false
  92. } catch (err) {
  93. expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
  94. }
  95. })
  96. it('Should reject requests without appropriate signed headers', async function () {
  97. await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey)
  98. await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
  99. const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
  100. const headers = buildGlobalHeaders(body)
  101. const signatureOptions = baseHttpSignature()
  102. const badHeadersMatrix = [
  103. [ '(request-target)', 'date', 'digest' ],
  104. [ 'host', 'date', 'digest' ],
  105. [ '(request-target)', 'host', 'digest' ]
  106. ]
  107. for (const badHeaders of badHeadersMatrix) {
  108. signatureOptions.headers = badHeaders
  109. try {
  110. await makePOSTAPRequest(url, body, signatureOptions, headers)
  111. expect(true, 'Did not throw').to.be.false
  112. } catch (err) {
  113. expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
  114. }
  115. }
  116. })
  117. it('Should succeed with a valid HTTP signature draft 11 (without date but with (created))', async function () {
  118. const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
  119. const headers = buildGlobalHeaders(body)
  120. const signatureOptions = baseHttpSignature()
  121. signatureOptions.headers = [ '(request-target)', '(created)', 'host', 'digest' ]
  122. const { statusCode } = await makePOSTAPRequest(url, body, signatureOptions, headers)
  123. expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
  124. })
  125. it('Should succeed with a valid HTTP signature', async function () {
  126. const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
  127. const headers = buildGlobalHeaders(body)
  128. const { statusCode } = await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
  129. expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
  130. })
  131. it('Should refresh the actor keys', async function () {
  132. this.timeout(20000)
  133. // Update keys of server 2 to invalid keys
  134. // Server 1 should refresh the actor and fail
  135. await setKeysOfServer(servers[1], servers[1], invalidKeys.publicKey, invalidKeys.privateKey)
  136. await setUpdatedAtOfServer(servers[0], servers[1], '2015-07-17 22:00:00+00')
  137. // Invalid peertube actor cache
  138. await killallServers([ servers[1] ])
  139. await servers[1].run()
  140. const body = activityPubContextify(getAnnounceWithoutContext(servers[1]), 'Announce')
  141. const headers = buildGlobalHeaders(body)
  142. try {
  143. await makePOSTAPRequest(url, body, baseHttpSignature(), headers)
  144. expect(true, 'Did not throw').to.be.false
  145. } catch (err) {
  146. console.error(err)
  147. expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
  148. }
  149. })
  150. })
  151. describe('When checking Linked Data Signature', function () {
  152. before(async function () {
  153. this.timeout(10000)
  154. await setKeysOfServer(servers[0], servers[1], keys.publicKey, keys.privateKey)
  155. await setKeysOfServer(servers[1], servers[1], keys.publicKey, keys.privateKey)
  156. await setKeysOfServer(servers[2], servers[2], keys.publicKey, keys.privateKey)
  157. const to = { url: servers[0].url + '/accounts/peertube' }
  158. const by = { url: servers[2].url + '/accounts/peertube', privateKey: keys.privateKey }
  159. await makeFollowRequest(to, by)
  160. })
  161. it('Should fail with bad keys', async function () {
  162. this.timeout(10000)
  163. await setKeysOfServer(servers[0], servers[2], invalidKeys.publicKey, invalidKeys.privateKey)
  164. await setKeysOfServer(servers[2], servers[2], invalidKeys.publicKey, invalidKeys.privateKey)
  165. const body = getAnnounceWithoutContext(servers[1])
  166. body.actor = servers[2].url + '/accounts/peertube'
  167. const signer: any = { privateKey: invalidKeys.privateKey, url: servers[2].url + '/accounts/peertube' }
  168. const signedBody = await signAndContextify(signer, body, 'Announce')
  169. const headers = buildGlobalHeaders(signedBody)
  170. try {
  171. await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
  172. expect(true, 'Did not throw').to.be.false
  173. } catch (err) {
  174. expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
  175. }
  176. })
  177. it('Should fail with an altered body', async function () {
  178. this.timeout(10000)
  179. await setKeysOfServer(servers[0], servers[2], keys.publicKey, keys.privateKey)
  180. await setKeysOfServer(servers[0], servers[2], keys.publicKey, keys.privateKey)
  181. const body = getAnnounceWithoutContext(servers[1])
  182. body.actor = servers[2].url + '/accounts/peertube'
  183. const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
  184. const signedBody = await signAndContextify(signer, body, 'Announce')
  185. signedBody.actor = servers[2].url + '/account/peertube'
  186. const headers = buildGlobalHeaders(signedBody)
  187. try {
  188. await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
  189. expect(true, 'Did not throw').to.be.false
  190. } catch (err) {
  191. expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
  192. }
  193. })
  194. it('Should succeed with a valid signature', async function () {
  195. this.timeout(10000)
  196. const body = getAnnounceWithoutContext(servers[1])
  197. body.actor = servers[2].url + '/accounts/peertube'
  198. const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
  199. const signedBody = await signAndContextify(signer, body, 'Announce')
  200. const headers = buildGlobalHeaders(signedBody)
  201. const { statusCode } = await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
  202. expect(statusCode).to.equal(HttpStatusCode.NO_CONTENT_204)
  203. })
  204. it('Should refresh the actor keys', async function () {
  205. this.timeout(20000)
  206. // Wait refresh invalidation
  207. await wait(10000)
  208. // Update keys of server 3 to invalid keys
  209. // Server 1 should refresh the actor and fail
  210. await setKeysOfServer(servers[2], servers[2], invalidKeys.publicKey, invalidKeys.privateKey)
  211. const body = getAnnounceWithoutContext(servers[1])
  212. body.actor = servers[2].url + '/accounts/peertube'
  213. const signer: any = { privateKey: keys.privateKey, url: servers[2].url + '/accounts/peertube' }
  214. const signedBody = await signAndContextify(signer, body, 'Announce')
  215. const headers = buildGlobalHeaders(signedBody)
  216. try {
  217. await makePOSTAPRequest(url, signedBody, baseHttpSignature(), headers)
  218. expect(true, 'Did not throw').to.be.false
  219. } catch (err) {
  220. expect(err.statusCode).to.equal(HttpStatusCode.FORBIDDEN_403)
  221. }
  222. })
  223. })
  224. after(async function () {
  225. this.timeout(10000)
  226. await cleanupTests(servers)
  227. })
  228. })