mirror of https://github.com/Chocobozzz/PeerTube
				
				
				
			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 federationpull/1386/head
							parent
							
								
									333210d862
								
							
						
					
					
						commit
						f7509cbec8
					
				|  | @ -108,6 +108,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", | ||||
|  |  | |||
|  | @ -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 <T> (data: T) { | ||||
|  | @ -15,22 +15,22 @@ function activityPubContextify <T> (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<Activity> | ||||
|   return signJsonLDObject(byActor, activity) as Promise<Activity> | ||||
| } | ||||
| 
 | ||||
| function getActorUrl (activityActor: string | ActivityPubActor) { | ||||
|  |  | |||
|  | @ -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,42 +16,7 @@ async function createPrivateAndPublicKeys () { | |||
|   return { privateKey: key, publicKey } | ||||
| } | ||||
| 
 | ||||
| function isSignatureVerified (fromActor: ActorModel, signedDocument: object) { | ||||
|   const publicKeyObject = { | ||||
|     '@context': jsig.SECURITY_CONTEXT_URL, | ||||
|     '@id': fromActor.url, | ||||
|     '@type':  'CryptographicKey', | ||||
|     owner: fromActor.url, | ||||
|     publicKeyPem: fromActor.publicKey | ||||
|   } | ||||
| 
 | ||||
|   const publicKeyOwnerObject = { | ||||
|     '@context': jsig.SECURITY_CONTEXT_URL, | ||||
|     '@id': fromActor.url, | ||||
|     publicKey: [ publicKeyObject ] | ||||
|   } | ||||
| 
 | ||||
|   const options = { | ||||
|     publicKey: publicKeyObject, | ||||
|     publicKeyOwner: publicKeyOwnerObject | ||||
|   } | ||||
| 
 | ||||
|   return jsig.promises.verify(signedDocument, options) | ||||
|     .catch(err => { | ||||
|       logger.error('Cannot check signature.', { err }) | ||||
|       return false | ||||
|     }) | ||||
| } | ||||
| 
 | ||||
| function signObject (byActor: ActorModel, data: any) { | ||||
|   const options = { | ||||
|     privateKeyPem: byActor.privateKey, | ||||
|     creator: byActor.url, | ||||
|     algorithm: 'RsaSignature2017' | ||||
|   } | ||||
| 
 | ||||
|   return jsig.promises.sign(data, options) | ||||
| } | ||||
| // User password checks
 | ||||
| 
 | ||||
| function comparePassword (plainPassword: string, hashPassword: string) { | ||||
|   return bcryptComparePromise(plainPassword, hashPassword) | ||||
|  | @ -60,12 +28,68 @@ async function cryptPassword (password: string) { | |||
|   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', | ||||
|     owner: fromActor.url, | ||||
|     publicKeyPem: fromActor.publicKey | ||||
|   } | ||||
| 
 | ||||
|   const publicKeyOwnerObject = { | ||||
|     '@context': jsig.SECURITY_CONTEXT_URL, | ||||
|     id: fromActor.url, | ||||
|     publicKey: [ publicKeyObject ] | ||||
|   } | ||||
| 
 | ||||
|   const options = { | ||||
|     publicKey: publicKeyObject, | ||||
|     publicKeyOwner: publicKeyOwnerObject | ||||
|   } | ||||
| 
 | ||||
|   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 signJsonLDObject (byActor: ActorModel, data: any) { | ||||
|   const options = { | ||||
|     privateKeyPem: byActor.privateKey, | ||||
|     creator: byActor.url, | ||||
|     algorithm: 'RsaSignature2017' | ||||
|   } | ||||
| 
 | ||||
|   return jsig.promises.sign(data, options) | ||||
| } | ||||
| 
 | ||||
| // ---------------------------------------------------------------------------
 | ||||
| 
 | ||||
| export { | ||||
|   isSignatureVerified, | ||||
|   parseHTTPSignature, | ||||
|   isHTTPSignatureVerified, | ||||
|   isJsonLDSignatureVerified, | ||||
|   comparePassword, | ||||
|   createPrivateAndPublicKeys, | ||||
|   cryptPassword, | ||||
|   signObject | ||||
|   signJsonLDObject | ||||
| } | ||||
|  |  | |||
|  | @ -529,6 +529,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 | ||||
|  | @ -728,6 +734,7 @@ export { | |||
|   VIDEO_EXT_MIMETYPE, | ||||
|   CRAWL_REQUEST_CONCURRENCY, | ||||
|   JOB_COMPLETED_LIFETIME, | ||||
|   HTTP_SIGNATURE, | ||||
|   VIDEO_IMPORT_STATES, | ||||
|   VIDEO_VIEW_LIFETIME, | ||||
|   buildLanguages | ||||
|  |  | |||
|  | @ -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 | ||||
|   } | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -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 | ||||
| 
 | ||||
|   const [ creator ] = signatureObject.creator.split('#') | ||||
| 
 | ||||
|   logger.debug('Checking signature of actor %s...', creator) | ||||
| 
 | ||||
|   let actor: ActorModel | ||||
|   try { | ||||
|     actor = await getOrCreateActorAndServerAndModel(creator) | ||||
|     const httpSignatureChecked = await checkHttpSignature(req, res) | ||||
|     if (httpSignatureChecked !== true) return | ||||
| 
 | ||||
|     const actor: ActorModel = res.locals.signature.actor | ||||
| 
 | ||||
|     // 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 | ||||
|     } | ||||
| 
 | ||||
|     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 | ||||
| } | ||||
|  |  | |||
|  | @ -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 } }) | ||||
|  |  | |||
|  | @ -3671,7 +3671,7 @@ http-response-object@^1.0.0, http-response-object@^1.1.0: | |||
|   version "1.1.0" | ||||
|   resolved "https://registry.yarnpkg.com/http-response-object/-/http-response-object-1.1.0.tgz#a7c4e75aae82f3bb4904e4f43f615673b4d518c3" | ||||
| 
 | ||||
| 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" | ||||
|   dependencies: | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue
	
	 Chocobozzz
						Chocobozzz