ニジカ投稿局 https://tv.nizika.tv
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

peertube-jsonld.ts 4.8 KiB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. import { omit } from '@peertube/peertube-core-utils'
  2. import { sha256 } from '@peertube/peertube-node-utils'
  3. import { createSign, createVerify } from 'crypto'
  4. import cloneDeep from 'lodash-es/cloneDeep.js'
  5. import { MActor } from '../types/models/index.js'
  6. import { getAllContext } from './activity-pub-utils.js'
  7. import { jsonld } from './custom-jsonld-signature.js'
  8. import { isArray } from './custom-validators/misc.js'
  9. import { logger } from './logger.js'
  10. import { assertIsInWorkerThread } from './threads.js'
  11. type ExpressRequest = { body: any }
  12. export function compactJSONLDAndCheckSignature (fromActor: MActor, req: ExpressRequest): Promise<boolean> {
  13. if (req.body.signature.type === 'RsaSignature2017') {
  14. return compactJSONLDAndCheckRSA2017Signature(fromActor, req)
  15. }
  16. logger.warn('Unknown JSON LD signature %s.', req.body.signature.type, req.body)
  17. return Promise.resolve(false)
  18. }
  19. // Backward compatibility with "other" implementations
  20. export async function compactJSONLDAndCheckRSA2017Signature (fromActor: MActor, req: ExpressRequest) {
  21. const compacted = await jsonldCompact(omit(req.body, [ 'signature' ]))
  22. fixCompacted(req.body, compacted)
  23. req.body = { ...compacted, signature: req.body.signature }
  24. if (compacted['@include']) {
  25. logger.warn('JSON-LD @include is not supported')
  26. return false
  27. }
  28. // TODO: compat with < 6.1, remove in 7.0
  29. let safe = true
  30. if (
  31. (compacted.type === 'Create' && (compacted?.object?.type === 'WatchAction' || compacted?.object?.type === 'CacheFile')) ||
  32. (compacted.type === 'Undo' && compacted?.object?.type === 'Create' && compacted?.object?.object.type === 'CacheFile')
  33. ) {
  34. safe = false
  35. }
  36. const [ documentHash, optionsHash ] = await Promise.all([
  37. hashObject(compacted, safe),
  38. createSignatureHash(req.body.signature, safe)
  39. ])
  40. const toVerify = optionsHash + documentHash
  41. const verify = createVerify('RSA-SHA256')
  42. verify.update(toVerify, 'utf8')
  43. return verify.verify(fromActor.publicKey, req.body.signature.signatureValue, 'base64')
  44. }
  45. function fixCompacted (original: any, compacted: any) {
  46. if (!original || !compacted) return
  47. for (const [ k, v ] of Object.entries(original)) {
  48. if (k === '@context' || k === 'signature') continue
  49. if (v === undefined || v === null) continue
  50. const cv = compacted[k]
  51. if (cv === undefined || cv === null) continue
  52. if (typeof v === 'string') {
  53. if (v === 'https://www.w3.org/ns/activitystreams#Public' && cv === 'as:Public') {
  54. compacted[k] = v
  55. }
  56. }
  57. if (isArray(v) && !isArray(cv)) {
  58. compacted[k] = [ cv ]
  59. for (let i = 0; i < v.length; i++) {
  60. if (v[i] === 'https://www.w3.org/ns/activitystreams#Public' && cv[i] === 'as:Public') {
  61. compacted[k][i] = v[i]
  62. }
  63. }
  64. }
  65. if (typeof v === 'object') {
  66. fixCompacted(original[k], compacted[k])
  67. }
  68. }
  69. }
  70. export async function signJsonLDObject <T> (options: {
  71. byActor: { url: string, privateKey: string }
  72. data: T
  73. disableWorkerThreadAssertion?: boolean
  74. }) {
  75. const { byActor, data, disableWorkerThreadAssertion = false } = options
  76. if (!disableWorkerThreadAssertion) assertIsInWorkerThread()
  77. const signature = {
  78. type: 'RsaSignature2017',
  79. creator: byActor.url,
  80. created: new Date().toISOString()
  81. }
  82. const [ documentHash, optionsHash ] = await Promise.all([
  83. createDocWithoutSignatureHash(data),
  84. createSignatureHash(signature)
  85. ])
  86. const toSign = optionsHash + documentHash
  87. const sign = createSign('RSA-SHA256')
  88. sign.update(toSign, 'utf8')
  89. const signatureValue = sign.sign(byActor.privateKey, 'base64')
  90. Object.assign(signature, { signatureValue })
  91. return Object.assign(data, { signature })
  92. }
  93. // ---------------------------------------------------------------------------
  94. // Private
  95. // ---------------------------------------------------------------------------
  96. async function hashObject (obj: any, safe: boolean): Promise<any> {
  97. const res = await jsonldNormalize(obj, safe)
  98. return sha256(res)
  99. }
  100. function jsonldCompact (obj: any) {
  101. return (jsonld as any).promises.compact(obj, getAllContext())
  102. }
  103. function jsonldNormalize (obj: any, safe: boolean) {
  104. return (jsonld as any).promises.normalize(obj, {
  105. safe,
  106. algorithm: 'URDNA2015',
  107. format: 'application/n-quads'
  108. })
  109. }
  110. // ---------------------------------------------------------------------------
  111. function createSignatureHash (signature: any, safe = true) {
  112. return hashObject({
  113. '@context': [
  114. 'https://w3id.org/security/v1',
  115. { RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' }
  116. ],
  117. ...omit(signature, [ 'type', 'id', 'signatureValue' ])
  118. }, safe)
  119. }
  120. function createDocWithoutSignatureHash (doc: any) {
  121. const docWithoutSignature = cloneDeep(doc)
  122. delete docWithoutSignature.signature
  123. return hashObject(docWithoutSignature, true)
  124. }