diff --git a/server/core/helpers/activity-pub-utils.ts b/server/core/helpers/activity-pub-utils.ts index 5bee8ca8e..6e28c90e2 100644 --- a/server/core/helpers/activity-pub-utils.ts +++ b/server/core/helpers/activity-pub-utils.ts @@ -1,6 +1,7 @@ import { ContextType } from '@peertube/peertube-models' import { ACTIVITY_PUB } from '@server/initializers/constants.js' -import { buildDigest, signJsonLDObject } from './peertube-crypto.js' +import { buildDigest } from './peertube-crypto.js' +import type { signJsonLDObject } from './peertube-jsonld.js' export type ContextFilter = (arg: T) => Promise diff --git a/server/core/helpers/peertube-crypto.ts b/server/core/helpers/peertube-crypto.ts index fca635a63..b8c0ae7f6 100644 --- a/server/core/helpers/peertube-crypto.ts +++ b/server/core/helpers/peertube-crypto.ts @@ -1,13 +1,11 @@ import httpSignature from '@peertube/http-signature' import { sha256 } from '@peertube/peertube-node-utils' -import { createCipheriv, createDecipheriv, createSign, createVerify } from 'crypto' +import { createCipheriv, createDecipheriv } from 'crypto' import { Request } from 'express' -import cloneDeep from 'lodash-es/cloneDeep.js' import { BCRYPT_SALT_SIZE, ENCRYPTION, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers/constants.js' import { MActor } from '../types/models/index.js' import { generateRSAKeyPairPromise, randomBytesPromise, scryptPromise } from './core-utils.js' import { logger } from './logger.js' -import { assertIsInWorkerThread } from './threads.js' function createPrivateAndPublicKeys () { logger.info('Generating a RSA key...') @@ -66,66 +64,6 @@ function parseHTTPSignature (req: Request, clockSkew?: number) { return parsed } -// --------------------------------------------------------------------------- -// JSONLD -// --------------------------------------------------------------------------- - -function isJsonLDSignatureVerified (fromActor: MActor, signedDocument: any): Promise { - if (signedDocument.signature.type === 'RsaSignature2017') { - return isJsonLDRSA2017Verified(fromActor, signedDocument) - } - - logger.warn('Unknown JSON LD signature %s.', signedDocument.signature.type, signedDocument) - - return Promise.resolve(false) -} - -// Backward compatibility with "other" implementations -async function isJsonLDRSA2017Verified (fromActor: MActor, signedDocument: any) { - const [ documentHash, optionsHash ] = await Promise.all([ - createDocWithoutSignatureHash(signedDocument), - createSignatureHash(signedDocument.signature) - ]) - - const toVerify = optionsHash + documentHash - - const verify = createVerify('RSA-SHA256') - verify.update(toVerify, 'utf8') - - return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64') -} - -async function signJsonLDObject (options: { - byActor: { url: string, privateKey: string } - data: T - disableWorkerThreadAssertion?: boolean -}) { - const { byActor, data, disableWorkerThreadAssertion = false } = options - - if (!disableWorkerThreadAssertion) assertIsInWorkerThread() - - const signature = { - type: 'RsaSignature2017', - creator: byActor.url, - created: new Date().toISOString() - } - - const [ documentHash, optionsHash ] = await Promise.all([ - createDocWithoutSignatureHash(data), - createSignatureHash(signature) - ]) - - const toSign = optionsHash + documentHash - - const sign = createSign('RSA-SHA256') - sign.update(toSign, 'utf8') - - const signatureValue = sign.sign(byActor.privateKey, 'base64') - Object.assign(signature, { signatureValue }) - - return Object.assign(data, { signature }) -} - // --------------------------------------------------------------------------- function buildDigest (body: any) { @@ -169,49 +107,10 @@ export { parseHTTPSignature, isHTTPSignatureVerified, buildDigest, - isJsonLDSignatureVerified, comparePassword, createPrivateAndPublicKeys, cryptPassword, - signJsonLDObject, encrypt, decrypt } - -// --------------------------------------------------------------------------- - -async function hashObject (obj: any): Promise { - const { jsonld } = await import('./custom-jsonld-signature.js') - - const res = await (jsonld as any).promises.normalize(obj, { - safe: false, - algorithm: 'URDNA2015', - format: 'application/n-quads' - }) - - return sha256(res) -} - -function createSignatureHash (signature: any) { - const signatureCopy = cloneDeep(signature) - Object.assign(signatureCopy, { - '@context': [ - 'https://w3id.org/security/v1', - { RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' } - ] - }) - - delete signatureCopy.type - delete signatureCopy.id - delete signatureCopy.signatureValue - - return hashObject(signatureCopy) -} - -function createDocWithoutSignatureHash (doc: any) { - const docWithoutSignature = cloneDeep(doc) - delete docWithoutSignature.signature - - return hashObject(docWithoutSignature) -} diff --git a/server/core/helpers/peertube-jsonld.ts b/server/core/helpers/peertube-jsonld.ts new file mode 100644 index 000000000..320351e67 --- /dev/null +++ b/server/core/helpers/peertube-jsonld.ts @@ -0,0 +1,100 @@ +import { sha256 } from '@peertube/peertube-node-utils' +import { createSign, createVerify } from 'crypto' +import cloneDeep from 'lodash-es/cloneDeep.js' +import { MActor } from '../types/models/index.js' +import { logger } from './logger.js' +import { assertIsInWorkerThread } from './threads.js' +import { jsonld } from './custom-jsonld-signature.js' + +export function isJsonLDSignatureVerified (fromActor: MActor, signedDocument: any): Promise { + if (signedDocument.signature.type === 'RsaSignature2017') { + return isJsonLDRSA2017Verified(fromActor, signedDocument) + } + + logger.warn('Unknown JSON LD signature %s.', signedDocument.signature.type, signedDocument) + + return Promise.resolve(false) +} + +// Backward compatibility with "other" implementations +export async function isJsonLDRSA2017Verified (fromActor: MActor, signedDocument: any) { + const [ documentHash, optionsHash ] = await Promise.all([ + createDocWithoutSignatureHash(signedDocument), + createSignatureHash(signedDocument.signature) + ]) + + const toVerify = optionsHash + documentHash + + const verify = createVerify('RSA-SHA256') + verify.update(toVerify, 'utf8') + + return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64') +} + +export async function signJsonLDObject (options: { + byActor: { url: string, privateKey: string } + data: T + disableWorkerThreadAssertion?: boolean +}) { + const { byActor, data, disableWorkerThreadAssertion = false } = options + + if (!disableWorkerThreadAssertion) assertIsInWorkerThread() + + const signature = { + type: 'RsaSignature2017', + creator: byActor.url, + created: new Date().toISOString() + } + + const [ documentHash, optionsHash ] = await Promise.all([ + createDocWithoutSignatureHash(data), + createSignatureHash(signature) + ]) + + const toSign = optionsHash + documentHash + + const sign = createSign('RSA-SHA256') + sign.update(toSign, 'utf8') + + const signatureValue = sign.sign(byActor.privateKey, 'base64') + Object.assign(signature, { signatureValue }) + + return Object.assign(data, { signature }) +} + +// --------------------------------------------------------------------------- +// Private +// --------------------------------------------------------------------------- + +async function hashObject (obj: any): Promise { + const res = await (jsonld as any).promises.normalize(obj, { + safe: false, + algorithm: 'URDNA2015', + format: 'application/n-quads' + }) + + return sha256(res) +} + +function createSignatureHash (signature: any) { + const signatureCopy = cloneDeep(signature) + Object.assign(signatureCopy, { + '@context': [ + 'https://w3id.org/security/v1', + { RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' } + ] + }) + + delete signatureCopy.type + delete signatureCopy.id + delete signatureCopy.signatureValue + + return hashObject(signatureCopy) +} + +function createDocWithoutSignatureHash (doc: any) { + const docWithoutSignature = cloneDeep(doc) + delete docWithoutSignature.signature + + return hashObject(docWithoutSignature) +} diff --git a/server/core/lib/worker/workers/sign-json-ld-object.ts b/server/core/lib/worker/workers/sign-json-ld-object.ts index 9baf7c7b0..e0ac368ca 100644 --- a/server/core/lib/worker/workers/sign-json-ld-object.ts +++ b/server/core/lib/worker/workers/sign-json-ld-object.ts @@ -1,3 +1,3 @@ -import { signJsonLDObject } from '@server/helpers/peertube-crypto.js' +import { signJsonLDObject } from '@server/helpers/peertube-jsonld.js' export default signJsonLDObject diff --git a/server/core/middlewares/activitypub.ts b/server/core/middlewares/activitypub.ts index 0d333061e..e955f251d 100644 --- a/server/core/middlewares/activitypub.ts +++ b/server/core/middlewares/activitypub.ts @@ -4,7 +4,7 @@ import { getAPId } from '@server/lib/activitypub/activity.js' import { wrapWithSpanAndContext } from '@server/lib/opentelemetry/tracing.js' import { ActivityDelete, ActivityPubSignature, HttpStatusCode } from '@peertube/peertube-models' import { logger } from '../helpers/logger.js' -import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto.js' +import { isHTTPSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto.js' import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers/constants.js' import { getOrCreateAPActor, loadActorUrlOrGetFromWebfinger } from '../lib/activitypub/actors/index.js' @@ -122,6 +122,9 @@ async function checkHttpSignature (req: Request, res: Response) { } async function checkJsonLDSignature (req: Request, res: Response) { + // Lazy load the module as it's quite big with json.ld dependency + const { isJsonLDSignatureVerified } = await import('../helpers/peertube-jsonld.js') + return wrapWithSpanAndContext('peertube.activitypub.JSONLDSignature', async () => { const signatureObject: ActivityPubSignature = req.body.signature