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
This commit is contained in:
parent
333210d862
commit
f7509cbec8
|
@ -108,6 +108,7 @@
|
||||||
"fluent-ffmpeg": "^2.1.0",
|
"fluent-ffmpeg": "^2.1.0",
|
||||||
"fs-extra": "^7.0.0",
|
"fs-extra": "^7.0.0",
|
||||||
"helmet": "^3.12.1",
|
"helmet": "^3.12.1",
|
||||||
|
"http-signature": "^1.2.0",
|
||||||
"ip-anonymize": "^0.0.6",
|
"ip-anonymize": "^0.0.6",
|
||||||
"ipaddr.js": "1.8.1",
|
"ipaddr.js": "1.8.1",
|
||||||
"is-cidr": "^2.0.5",
|
"is-cidr": "^2.0.5",
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { ResultList } from '../../shared/models'
|
||||||
import { Activity, ActivityPubActor } from '../../shared/models/activitypub'
|
import { Activity, ActivityPubActor } from '../../shared/models/activitypub'
|
||||||
import { ACTIVITY_PUB } from '../initializers'
|
import { ACTIVITY_PUB } from '../initializers'
|
||||||
import { ActorModel } from '../models/activitypub/actor'
|
import { ActorModel } from '../models/activitypub/actor'
|
||||||
import { signObject } from './peertube-crypto'
|
import { signJsonLDObject } from './peertube-crypto'
|
||||||
import { pageToStartAndCount } from './core-utils'
|
import { pageToStartAndCount } from './core-utils'
|
||||||
|
|
||||||
function activityPubContextify <T> (data: T) {
|
function activityPubContextify <T> (data: T) {
|
||||||
|
@ -15,22 +15,22 @@ function activityPubContextify <T> (data: T) {
|
||||||
{
|
{
|
||||||
RsaSignature2017: 'https://w3id.org/security#RsaSignature2017',
|
RsaSignature2017: 'https://w3id.org/security#RsaSignature2017',
|
||||||
pt: 'https://joinpeertube.org/ns',
|
pt: 'https://joinpeertube.org/ns',
|
||||||
schema: 'http://schema.org#',
|
sc: 'http://schema.org#',
|
||||||
Hashtag: 'as:Hashtag',
|
Hashtag: 'as:Hashtag',
|
||||||
uuid: 'schema:identifier',
|
uuid: 'sc:identifier',
|
||||||
category: 'schema:category',
|
category: 'sc:category',
|
||||||
licence: 'schema:license',
|
licence: 'sc:license',
|
||||||
subtitleLanguage: 'schema:subtitleLanguage',
|
subtitleLanguage: 'sc:subtitleLanguage',
|
||||||
sensitive: 'as:sensitive',
|
sensitive: 'as:sensitive',
|
||||||
language: 'schema:inLanguage',
|
language: 'sc:inLanguage',
|
||||||
views: 'schema:Number',
|
views: 'sc:Number',
|
||||||
stats: 'schema:Number',
|
stats: 'sc:Number',
|
||||||
size: 'schema:Number',
|
size: 'sc:Number',
|
||||||
fps: 'schema:Number',
|
fps: 'sc:Number',
|
||||||
commentsEnabled: 'schema:Boolean',
|
commentsEnabled: 'sc:Boolean',
|
||||||
waitTranscoding: 'schema:Boolean',
|
waitTranscoding: 'sc:Boolean',
|
||||||
expires: 'schema:expires',
|
expires: 'sc:expires',
|
||||||
support: 'schema:Text',
|
support: 'sc:Text',
|
||||||
CacheFile: 'pt:CacheFile'
|
CacheFile: 'pt:CacheFile'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -102,7 +102,7 @@ async function activityPubCollectionPagination (url: string, handler: ActivityPu
|
||||||
function buildSignedActivity (byActor: ActorModel, data: Object) {
|
function buildSignedActivity (byActor: ActorModel, data: Object) {
|
||||||
const activity = activityPubContextify(data)
|
const activity = activityPubContextify(data)
|
||||||
|
|
||||||
return signObject(byActor, activity) as Promise<Activity>
|
return signJsonLDObject(byActor, activity) as Promise<Activity>
|
||||||
}
|
}
|
||||||
|
|
||||||
function getActorUrl (activityActor: string | ActivityPubActor) {
|
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 { ActorModel } from '../models/activitypub/actor'
|
||||||
import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey } from './core-utils'
|
import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey } from './core-utils'
|
||||||
import { jsig } from './custom-jsonld-signature'
|
import { jsig } from './custom-jsonld-signature'
|
||||||
import { logger } from './logger'
|
import { logger } from './logger'
|
||||||
|
|
||||||
|
const httpSignature = require('http-signature')
|
||||||
|
|
||||||
async function createPrivateAndPublicKeys () {
|
async function createPrivateAndPublicKeys () {
|
||||||
logger.info('Generating a RSA key...')
|
logger.info('Generating a RSA key...')
|
||||||
|
|
||||||
|
@ -13,42 +16,7 @@ async function createPrivateAndPublicKeys () {
|
||||||
return { privateKey: key, publicKey }
|
return { privateKey: key, publicKey }
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSignatureVerified (fromActor: ActorModel, signedDocument: object) {
|
// User password checks
|
||||||
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)
|
|
||||||
}
|
|
||||||
|
|
||||||
function comparePassword (plainPassword: string, hashPassword: string) {
|
function comparePassword (plainPassword: string, hashPassword: string) {
|
||||||
return bcryptComparePromise(plainPassword, hashPassword)
|
return bcryptComparePromise(plainPassword, hashPassword)
|
||||||
|
@ -60,12 +28,68 @@ async function cryptPassword (password: string) {
|
||||||
return bcryptHashPromise(password, salt)
|
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 {
|
export {
|
||||||
isSignatureVerified,
|
parseHTTPSignature,
|
||||||
|
isHTTPSignatureVerified,
|
||||||
|
isJsonLDSignatureVerified,
|
||||||
comparePassword,
|
comparePassword,
|
||||||
createPrivateAndPublicKeys,
|
createPrivateAndPublicKeys,
|
||||||
cryptPassword,
|
cryptPassword,
|
||||||
signObject
|
signJsonLDObject
|
||||||
}
|
}
|
||||||
|
|
|
@ -529,6 +529,12 @@ const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
|
||||||
APPLICATION: 'Application'
|
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
|
const PRIVATE_RSA_KEY_SIZE = 2048
|
||||||
|
@ -728,6 +734,7 @@ export {
|
||||||
VIDEO_EXT_MIMETYPE,
|
VIDEO_EXT_MIMETYPE,
|
||||||
CRAWL_REQUEST_CONCURRENCY,
|
CRAWL_REQUEST_CONCURRENCY,
|
||||||
JOB_COMPLETED_LIFETIME,
|
JOB_COMPLETED_LIFETIME,
|
||||||
|
HTTP_SIGNATURE,
|
||||||
VIDEO_IMPORT_STATES,
|
VIDEO_IMPORT_STATES,
|
||||||
VIDEO_VIEW_LIFETIME,
|
VIDEO_VIEW_LIFETIME,
|
||||||
buildLanguages
|
buildLanguages
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { buildSignedActivity } from '../../../../helpers/activitypub'
|
||||||
import { getServerActor } from '../../../../helpers/utils'
|
import { getServerActor } from '../../../../helpers/utils'
|
||||||
import { ActorModel } from '../../../../models/activitypub/actor'
|
import { ActorModel } from '../../../../models/activitypub/actor'
|
||||||
import { sha256 } from '../../../../helpers/core-utils'
|
import { sha256 } from '../../../../helpers/core-utils'
|
||||||
|
import { HTTP_SIGNATURE } from '../../../../initializers'
|
||||||
|
|
||||||
type Payload = { body: any, signatureActorId?: number }
|
type Payload = { body: any, signatureActorId?: number }
|
||||||
|
|
||||||
|
@ -29,11 +30,11 @@ async function buildSignedRequestOptions (payload: Payload) {
|
||||||
|
|
||||||
const keyId = actor.getWebfingerUrl()
|
const keyId = actor.getWebfingerUrl()
|
||||||
return {
|
return {
|
||||||
algorithm: 'rsa-sha256',
|
algorithm: HTTP_SIGNATURE.ALGORITHM,
|
||||||
authorizationHeaderName: 'Signature',
|
authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
|
||||||
keyId,
|
keyId,
|
||||||
key: actor.privateKey,
|
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 { NextFunction, Request, RequestHandler, Response } from 'express'
|
||||||
import { ActivityPubSignature } from '../../shared'
|
import { ActivityPubSignature } from '../../shared'
|
||||||
import { logger } from '../helpers/logger'
|
import { logger } from '../helpers/logger'
|
||||||
import { isSignatureVerified } from '../helpers/peertube-crypto'
|
import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto'
|
||||||
import { ACCEPT_HEADERS, ACTIVITY_PUB } from '../initializers'
|
import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers'
|
||||||
import { getOrCreateActorAndServerAndModel } from '../lib/activitypub'
|
import { getOrCreateActorAndServerAndModel } from '../lib/activitypub'
|
||||||
import { ActorModel } from '../models/activitypub/actor'
|
import { ActorModel } from '../models/activitypub/actor'
|
||||||
|
import { loadActorUrlOrGetFromWebfinger } from '../helpers/webfinger'
|
||||||
|
|
||||||
async function checkSignature (req: Request, res: Response, next: NextFunction) {
|
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 {
|
try {
|
||||||
actor = await getOrCreateActorAndServerAndModel(creator)
|
const httpSignatureChecked = await checkHttpSignature(req, res)
|
||||||
} catch (err) {
|
if (httpSignatureChecked !== true) return
|
||||||
logger.warn('Cannot create remote actor %s and check signature.', creator, { err })
|
|
||||||
return res.sendStatus(403)
|
|
||||||
}
|
|
||||||
|
|
||||||
const verified = await isSignatureVerified(actor, req.body)
|
const actor: ActorModel = res.locals.signature.actor
|
||||||
if (verified === false) return res.sendStatus(403)
|
|
||||||
|
|
||||||
res.locals.signature = {
|
// Forwarded activity
|
||||||
actor
|
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()
|
return next()
|
||||||
|
} catch (err) {
|
||||||
|
logger.error('Error in ActivityPub signature checker.', err)
|
||||||
|
return res.sendStatus(403)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) {
|
function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) {
|
||||||
|
@ -57,3 +55,63 @@ export {
|
||||||
checkSignature,
|
checkSignature,
|
||||||
executeIfActivityPub
|
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'
|
import { areValidationErrors } from '../utils'
|
||||||
|
|
||||||
const signatureValidator = [
|
const signatureValidator = [
|
||||||
body('signature.type').custom(isSignatureTypeValid).withMessage('Should have a valid signature type'),
|
body('signature.type')
|
||||||
body('signature.created').custom(isDateValid).withMessage('Should have a valid signature created date'),
|
.optional()
|
||||||
body('signature.creator').custom(isSignatureCreatorValid).withMessage('Should have a valid signature creator'),
|
.custom(isSignatureTypeValid).withMessage('Should have a valid signature type'),
|
||||||
body('signature.signatureValue').custom(isSignatureValueValid).withMessage('Should have a valid signature value'),
|
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) => {
|
(req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||||
logger.debug('Checking activitypub signature parameter', { parameters: { signature: req.body.signature } })
|
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"
|
version "1.1.0"
|
||||||
resolved "https://registry.yarnpkg.com/http-response-object/-/http-response-object-1.1.0.tgz#a7c4e75aae82f3bb4904e4f43f615673b4d518c3"
|
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"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
|
resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|
Loading…
Reference in New Issue