From 41f2ebae4f970932fb62d2d8923b1f776f0b1494 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 19 Oct 2018 11:41:19 +0200 Subject: [PATCH] Add HTTP signature check before linked signature It's faster, and will allow us to use RSA signature 2018 (with upstream jsonld-signature module) without too much incompatibilities in the peertube federation --- package.json | 1 + server/helpers/activitypub.ts | 32 +++---- server/helpers/peertube-crypto.ts | 70 +++++++++----- server/initializers/constants.ts | 7 ++ .../handlers/utils/activitypub-http-utils.ts | 7 +- server/middlewares/activitypub.ts | 94 +++++++++++++++---- .../validators/activitypub/signature.ts | 16 +++- yarn.lock | 2 +- 8 files changed, 164 insertions(+), 65 deletions(-) diff --git a/package.json b/package.json index 46c6d5dce..295b4e74b 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "fluent-ffmpeg": "^2.1.0", "fs-extra": "^7.0.0", "helmet": "^3.12.1", + "http-signature": "^1.2.0", "ip-anonymize": "^0.0.6", "ipaddr.js": "1.8.1", "is-cidr": "^2.0.5", diff --git a/server/helpers/activitypub.ts b/server/helpers/activitypub.ts index 1304c7559..278010e78 100644 --- a/server/helpers/activitypub.ts +++ b/server/helpers/activitypub.ts @@ -4,7 +4,7 @@ import { ResultList } from '../../shared/models' import { Activity, ActivityPubActor } from '../../shared/models/activitypub' import { ACTIVITY_PUB } from '../initializers' import { ActorModel } from '../models/activitypub/actor' -import { signObject } from './peertube-crypto' +import { signJsonLDObject } from './peertube-crypto' import { pageToStartAndCount } from './core-utils' function activityPubContextify (data: T) { @@ -15,22 +15,22 @@ function activityPubContextify (data: T) { { RsaSignature2017: 'https://w3id.org/security#RsaSignature2017', pt: 'https://joinpeertube.org/ns', - schema: 'http://schema.org#', + sc: 'http://schema.org#', Hashtag: 'as:Hashtag', - uuid: 'schema:identifier', - category: 'schema:category', - licence: 'schema:license', - subtitleLanguage: 'schema:subtitleLanguage', + uuid: 'sc:identifier', + category: 'sc:category', + licence: 'sc:license', + subtitleLanguage: 'sc:subtitleLanguage', sensitive: 'as:sensitive', - language: 'schema:inLanguage', - views: 'schema:Number', - stats: 'schema:Number', - size: 'schema:Number', - fps: 'schema:Number', - commentsEnabled: 'schema:Boolean', - waitTranscoding: 'schema:Boolean', - expires: 'schema:expires', - support: 'schema:Text', + language: 'sc:inLanguage', + views: 'sc:Number', + stats: 'sc:Number', + size: 'sc:Number', + fps: 'sc:Number', + commentsEnabled: 'sc:Boolean', + waitTranscoding: 'sc:Boolean', + expires: 'sc:expires', + support: 'sc:Text', CacheFile: 'pt:CacheFile' }, { @@ -102,7 +102,7 @@ async function activityPubCollectionPagination (url: string, handler: ActivityPu function buildSignedActivity (byActor: ActorModel, data: Object) { const activity = activityPubContextify(data) - return signObject(byActor, activity) as Promise + return signJsonLDObject(byActor, activity) as Promise } function getActorUrl (activityActor: string | ActivityPubActor) { diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts index 5c182961d..cb5f27240 100644 --- a/server/helpers/peertube-crypto.ts +++ b/server/helpers/peertube-crypto.ts @@ -1,9 +1,12 @@ -import { BCRYPT_SALT_SIZE, PRIVATE_RSA_KEY_SIZE } from '../initializers' +import { Request } from 'express' +import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers' import { ActorModel } from '../models/activitypub/actor' import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey } from './core-utils' import { jsig } from './custom-jsonld-signature' import { logger } from './logger' +const httpSignature = require('http-signature') + async function createPrivateAndPublicKeys () { logger.info('Generating a RSA key...') @@ -13,18 +16,42 @@ async function createPrivateAndPublicKeys () { return { privateKey: key, publicKey } } -function isSignatureVerified (fromActor: ActorModel, signedDocument: object) { +// User password checks + +function comparePassword (plainPassword: string, hashPassword: string) { + return bcryptComparePromise(plainPassword, hashPassword) +} + +async function cryptPassword (password: string) { + const salt = await bcryptGenSaltPromise(BCRYPT_SALT_SIZE) + + return bcryptHashPromise(password, salt) +} + +// HTTP Signature + +function isHTTPSignatureVerified (httpSignatureParsed: any, actor: ActorModel) { + return httpSignature.verifySignature(httpSignatureParsed, actor.publicKey) === true +} + +function parseHTTPSignature (req: Request) { + return httpSignature.parse(req, { authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME }) +} + +// JSONLD + +function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any) { const publicKeyObject = { '@context': jsig.SECURITY_CONTEXT_URL, - '@id': fromActor.url, - '@type': 'CryptographicKey', + id: fromActor.url, + type: 'CryptographicKey', owner: fromActor.url, publicKeyPem: fromActor.publicKey } const publicKeyOwnerObject = { '@context': jsig.SECURITY_CONTEXT_URL, - '@id': fromActor.url, + id: fromActor.url, publicKey: [ publicKeyObject ] } @@ -33,14 +60,19 @@ function isSignatureVerified (fromActor: ActorModel, signedDocument: object) { publicKeyOwner: publicKeyOwnerObject } - return jsig.promises.verify(signedDocument, options) - .catch(err => { - logger.error('Cannot check signature.', { err }) - return false - }) + return jsig.promises + .verify(signedDocument, options) + .then((result: { verified: boolean }) => { + logger.info('coucou', result) + return result.verified + }) + .catch(err => { + logger.error('Cannot check signature.', { err }) + return false + }) } -function signObject (byActor: ActorModel, data: any) { +function signJsonLDObject (byActor: ActorModel, data: any) { const options = { privateKeyPem: byActor.privateKey, creator: byActor.url, @@ -50,22 +82,14 @@ function signObject (byActor: ActorModel, data: any) { return jsig.promises.sign(data, options) } -function comparePassword (plainPassword: string, hashPassword: string) { - return bcryptComparePromise(plainPassword, hashPassword) -} - -async function cryptPassword (password: string) { - const salt = await bcryptGenSaltPromise(BCRYPT_SALT_SIZE) - - return bcryptHashPromise(password, salt) -} - // --------------------------------------------------------------------------- export { - isSignatureVerified, + parseHTTPSignature, + isHTTPSignatureVerified, + isJsonLDSignatureVerified, comparePassword, createPrivateAndPublicKeys, cryptPassword, - signObject + signJsonLDObject } diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts index e8843a3ab..28d51068b 100644 --- a/server/initializers/constants.ts +++ b/server/initializers/constants.ts @@ -532,6 +532,12 @@ const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = { APPLICATION: 'Application' } +const HTTP_SIGNATURE = { + HEADER_NAME: 'signature', + ALGORITHM: 'rsa-sha256', + HEADERS_TO_SIGN: [ 'date', 'host', 'digest', '(request-target)' ] +} + // --------------------------------------------------------------------------- const PRIVATE_RSA_KEY_SIZE = 2048 @@ -731,6 +737,7 @@ export { VIDEO_EXT_MIMETYPE, CRAWL_REQUEST_CONCURRENCY, JOB_COMPLETED_LIFETIME, + HTTP_SIGNATURE, VIDEO_IMPORT_STATES, VIDEO_VIEW_LIFETIME, buildLanguages diff --git a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts index d71c91a24..fd9c74341 100644 --- a/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts +++ b/server/lib/job-queue/handlers/utils/activitypub-http-utils.ts @@ -2,6 +2,7 @@ import { buildSignedActivity } from '../../../../helpers/activitypub' import { getServerActor } from '../../../../helpers/utils' import { ActorModel } from '../../../../models/activitypub/actor' import { sha256 } from '../../../../helpers/core-utils' +import { HTTP_SIGNATURE } from '../../../../initializers' type Payload = { body: any, signatureActorId?: number } @@ -29,11 +30,11 @@ async function buildSignedRequestOptions (payload: Payload) { const keyId = actor.getWebfingerUrl() return { - algorithm: 'rsa-sha256', - authorizationHeaderName: 'Signature', + algorithm: HTTP_SIGNATURE.ALGORITHM, + authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, keyId, key: actor.privateKey, - headers: [ 'date', 'host', 'digest', '(request-target)' ] + headers: HTTP_SIGNATURE.HEADERS_TO_SIGN } } diff --git a/server/middlewares/activitypub.ts b/server/middlewares/activitypub.ts index d7f59be8c..1ec888477 100644 --- a/server/middlewares/activitypub.ts +++ b/server/middlewares/activitypub.ts @@ -2,34 +2,32 @@ import { eachSeries } from 'async' import { NextFunction, Request, RequestHandler, Response } from 'express' import { ActivityPubSignature } from '../../shared' import { logger } from '../helpers/logger' -import { isSignatureVerified } from '../helpers/peertube-crypto' -import { ACCEPT_HEADERS, ACTIVITY_PUB } from '../initializers' +import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto' +import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers' import { getOrCreateActorAndServerAndModel } from '../lib/activitypub' import { ActorModel } from '../models/activitypub/actor' +import { loadActorUrlOrGetFromWebfinger } from '../helpers/webfinger' async function checkSignature (req: Request, res: Response, next: NextFunction) { - const signatureObject: ActivityPubSignature = req.body.signature + try { + const httpSignatureChecked = await checkHttpSignature(req, res) + if (httpSignatureChecked !== true) return - const [ creator ] = signatureObject.creator.split('#') + const actor: ActorModel = res.locals.signature.actor - logger.debug('Checking signature of actor %s...', creator) + // Forwarded activity + const bodyActor = req.body.actor + const bodyActorId = bodyActor && bodyActor.id ? bodyActor.id : bodyActor + if (bodyActorId && bodyActorId !== actor.url) { + const jsonLDSignatureChecked = await checkJsonLDSignature(req, res) + if (jsonLDSignatureChecked !== true) return + } - let actor: ActorModel - try { - actor = await getOrCreateActorAndServerAndModel(creator) + return next() } catch (err) { - logger.warn('Cannot create remote actor %s and check signature.', creator, { err }) + logger.error('Error in ActivityPub signature checker.', err) return res.sendStatus(403) } - - const verified = await isSignatureVerified(actor, req.body) - if (verified === false) return res.sendStatus(403) - - res.locals.signature = { - actor - } - - return next() } function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) { @@ -57,3 +55,63 @@ export { checkSignature, executeIfActivityPub } + +// --------------------------------------------------------------------------- + +async function checkHttpSignature (req: Request, res: Response) { + // FIXME: mastodon does not include the Signature scheme + const sig = req.headers[HTTP_SIGNATURE.HEADER_NAME] as string + if (sig && sig.startsWith('Signature ') === false) req.headers[HTTP_SIGNATURE.HEADER_NAME] = 'Signature ' + sig + + const parsed = parseHTTPSignature(req) + + const keyId = parsed.keyId + if (!keyId) { + res.sendStatus(403) + return false + } + + logger.debug('Checking HTTP signature of actor %s...', keyId) + + let [ actorUrl ] = keyId.split('#') + if (actorUrl.startsWith('acct:')) { + actorUrl = await loadActorUrlOrGetFromWebfinger(actorUrl.replace(/^acct:/, '')) + } + + const actor = await getOrCreateActorAndServerAndModel(actorUrl) + + const verified = isHTTPSignatureVerified(parsed, actor) + if (verified !== true) { + res.sendStatus(403) + return false + } + + res.locals.signature = { actor } + + return true +} + +async function checkJsonLDSignature (req: Request, res: Response) { + const signatureObject: ActivityPubSignature = req.body.signature + + if (!signatureObject.creator) { + res.sendStatus(403) + return false + } + + const [ creator ] = signatureObject.creator.split('#') + + logger.debug('Checking JsonLD signature of actor %s...', creator) + + const actor = await getOrCreateActorAndServerAndModel(creator) + const verified = await isJsonLDSignatureVerified(actor, req.body) + + if (verified !== true) { + res.sendStatus(403) + return false + } + + res.locals.signature = { actor } + + return true +} diff --git a/server/middlewares/validators/activitypub/signature.ts b/server/middlewares/validators/activitypub/signature.ts index 4efe9aafa..be14e92ea 100644 --- a/server/middlewares/validators/activitypub/signature.ts +++ b/server/middlewares/validators/activitypub/signature.ts @@ -9,10 +9,18 @@ import { logger } from '../../../helpers/logger' import { areValidationErrors } from '../utils' const signatureValidator = [ - body('signature.type').custom(isSignatureTypeValid).withMessage('Should have a valid signature type'), - body('signature.created').custom(isDateValid).withMessage('Should have a valid signature created date'), - body('signature.creator').custom(isSignatureCreatorValid).withMessage('Should have a valid signature creator'), - body('signature.signatureValue').custom(isSignatureValueValid).withMessage('Should have a valid signature value'), + body('signature.type') + .optional() + .custom(isSignatureTypeValid).withMessage('Should have a valid signature type'), + body('signature.created') + .optional() + .custom(isDateValid).withMessage('Should have a valid signature created date'), + body('signature.creator') + .optional() + .custom(isSignatureCreatorValid).withMessage('Should have a valid signature creator'), + body('signature.signatureValue') + .optional() + .custom(isSignatureValueValid).withMessage('Should have a valid signature value'), (req: express.Request, res: express.Response, next: express.NextFunction) => { logger.debug('Checking activitypub signature parameter', { parameters: { signature: req.body.signature } }) diff --git a/yarn.lock b/yarn.lock index 0ec5427be..a0fec9b5f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4270,7 +4270,7 @@ http-response-object@^1.0.0, http-response-object@^1.1.0: resolved "https://registry.yarnpkg.com/http-response-object/-/http-response-object-1.1.0.tgz#a7c4e75aae82f3bb4904e4f43f615673b4d518c3" integrity sha1-p8TnWq6C87tJBOT0P2FWc7TVGMM= -http-signature@~1.2.0: +http-signature@^1.2.0, http-signature@~1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= -- 2.25.1