activitypub.ts 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156
  1. import { NextFunction, Request, Response } from 'express'
  2. import { isActorDeleteActivityValid } from '@server/helpers/custom-validators/activitypub/actor'
  3. import { getAPId } from '@server/lib/activitypub/activity'
  4. import { wrapWithSpanAndContext } from '@server/lib/opentelemetry/tracing'
  5. import { ActivityDelete, ActivityPubSignature, HttpStatusCode } from '@shared/models'
  6. import { logger } from '../helpers/logger'
  7. import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto'
  8. import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers/constants'
  9. import { getOrCreateAPActor, loadActorUrlOrGetFromWebfinger } from '../lib/activitypub/actors'
  10. async function checkSignature (req: Request, res: Response, next: NextFunction) {
  11. try {
  12. const httpSignatureChecked = await checkHttpSignature(req, res)
  13. if (httpSignatureChecked !== true) return
  14. const actor = res.locals.signature.actor
  15. // Forwarded activity
  16. const bodyActor = req.body.actor
  17. const bodyActorId = getAPId(bodyActor)
  18. if (bodyActorId && bodyActorId !== actor.url) {
  19. const jsonLDSignatureChecked = await checkJsonLDSignature(req, res)
  20. if (jsonLDSignatureChecked !== true) return
  21. }
  22. return next()
  23. } catch (err) {
  24. const activity: ActivityDelete = req.body
  25. if (isActorDeleteActivityValid(activity) && activity.object === activity.actor) {
  26. logger.debug('Handling signature error on actor delete activity', { err })
  27. return res.status(HttpStatusCode.NO_CONTENT_204).end()
  28. }
  29. logger.warn('Error in ActivityPub signature checker.', { err })
  30. return res.fail({
  31. status: HttpStatusCode.FORBIDDEN_403,
  32. message: 'ActivityPub signature could not be checked'
  33. })
  34. }
  35. }
  36. function executeIfActivityPub (req: Request, res: Response, next: NextFunction) {
  37. const accepted = req.accepts(ACCEPT_HEADERS)
  38. if (accepted === false || ACTIVITY_PUB.POTENTIAL_ACCEPT_HEADERS.includes(accepted) === false) {
  39. // Bypass this route
  40. return next('route')
  41. }
  42. logger.debug('ActivityPub request for %s.', req.url)
  43. return next()
  44. }
  45. // ---------------------------------------------------------------------------
  46. export {
  47. checkSignature,
  48. executeIfActivityPub,
  49. checkHttpSignature
  50. }
  51. // ---------------------------------------------------------------------------
  52. async function checkHttpSignature (req: Request, res: Response) {
  53. return wrapWithSpanAndContext('peertube.activitypub.checkHTTPSignature', async () => {
  54. // FIXME: compatibility with http-signature < v1.3
  55. const sig = req.headers[HTTP_SIGNATURE.HEADER_NAME] as string
  56. if (sig && sig.startsWith('Signature ') === true) req.headers[HTTP_SIGNATURE.HEADER_NAME] = sig.replace(/^Signature /, '')
  57. let parsed: any
  58. try {
  59. parsed = parseHTTPSignature(req, HTTP_SIGNATURE.CLOCK_SKEW_SECONDS)
  60. } catch (err) {
  61. logger.warn('Invalid signature because of exception in signature parser', { reqBody: req.body, err })
  62. res.fail({
  63. status: HttpStatusCode.FORBIDDEN_403,
  64. message: err.message
  65. })
  66. return false
  67. }
  68. const keyId = parsed.keyId
  69. if (!keyId) {
  70. res.fail({
  71. status: HttpStatusCode.FORBIDDEN_403,
  72. message: 'Invalid key ID',
  73. data: {
  74. keyId
  75. }
  76. })
  77. return false
  78. }
  79. logger.debug('Checking HTTP signature of actor %s...', keyId)
  80. let [ actorUrl ] = keyId.split('#')
  81. if (actorUrl.startsWith('acct:')) {
  82. actorUrl = await loadActorUrlOrGetFromWebfinger(actorUrl.replace(/^acct:/, ''))
  83. }
  84. const actor = await getOrCreateAPActor(actorUrl)
  85. const verified = isHTTPSignatureVerified(parsed, actor)
  86. if (verified !== true) {
  87. logger.warn('Signature from %s is invalid', actorUrl, { parsed })
  88. res.fail({
  89. status: HttpStatusCode.FORBIDDEN_403,
  90. message: 'Invalid signature',
  91. data: {
  92. actorUrl
  93. }
  94. })
  95. return false
  96. }
  97. res.locals.signature = { actor }
  98. return true
  99. })
  100. }
  101. async function checkJsonLDSignature (req: Request, res: Response) {
  102. return wrapWithSpanAndContext('peertube.activitypub.JSONLDSignature', async () => {
  103. const signatureObject: ActivityPubSignature = req.body.signature
  104. if (!signatureObject?.creator) {
  105. res.fail({
  106. status: HttpStatusCode.FORBIDDEN_403,
  107. message: 'Object and creator signature do not match'
  108. })
  109. return false
  110. }
  111. const [ creator ] = signatureObject.creator.split('#')
  112. logger.debug('Checking JsonLD signature of actor %s...', creator)
  113. const actor = await getOrCreateAPActor(creator)
  114. const verified = await isJsonLDSignatureVerified(actor, req.body)
  115. if (verified !== true) {
  116. logger.warn('Signature not verified.', req.body)
  117. res.fail({
  118. status: HttpStatusCode.FORBIDDEN_403,
  119. message: 'Signature could not be verified'
  120. })
  121. return false
  122. }
  123. res.locals.signature = { actor }
  124. return true
  125. })
  126. }