Add HTTP signature check before linked signature
authorChocobozzz <me@florianbigard.com>
Fri, 19 Oct 2018 09:41:19 +0000 (11:41 +0200)
committerChocobozzz <me@florianbigard.com>
Fri, 19 Oct 2018 10:26:37 +0000 (12:26 +0200)
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
server/helpers/activitypub.ts
server/helpers/peertube-crypto.ts
server/initializers/constants.ts
server/lib/job-queue/handlers/utils/activitypub-http-utils.ts
server/middlewares/activitypub.ts
server/middlewares/validators/activitypub/signature.ts
yarn.lock

index 46c6d5dce072f29bbb4e67d367e7b38e45beda6c..295b4e74bba81b6a9b8869d06863a60a70382c24 100644 (file)
     "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",
index 1304c7559d0c4b302bebebfc9731696826d18161..278010e782232cbd567619fd48487028fe0c0e5e 100644 (file)
@@ -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) {
index 5c182961d4a8a41e1413ea1d33bc99df1a191178..cb5f272401dcd23828c1a2a2cd414108ebb62b20 100644 (file)
@@ -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
 }
index e8843a3ab1a8e952938c68da6e67133b76be97c6..28d51068b03222263b1a16b71e51e9abdf953b8a 100644 (file)
@@ -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
index d71c91a2408a6152b25ebd63b5bf5b61530ae711..fd9c743413daa42fa8a7af747ea715355e5d4bb4 100644 (file)
@@ -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
   }
 }
 
index d7f59be8c5a2553ebd8fb76c1f4835d20facaf50..1ec8884772d6ea8186c92a3cdfae8f416caf0c6c 100644 (file)
@@ -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
+}
index 4efe9aafa63cacd5a50ceece0e2ebe2927615eae..be14e92eac3bbd921785e4edb55625093310031b 100644 (file)
@@ -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 } })
index 0ec5427be6f07cffd2a67436c4c6cb692bb0886d..a0fec9b5f8a73bceb0164d5c00c9710d6bf556b3 100644 (file)
--- 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=