aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorChocobozzz <me@florianbigard.com>2018-10-19 11:41:19 +0200
committerChocobozzz <me@florianbigard.com>2018-10-19 12:26:37 +0200
commit41f2ebae4f970932fb62d2d8923b1f776f0b1494 (patch)
tree9e3f89e6363fc63e77d352f07956d1b6ff0cacda
parentd23e6a1c97a6ae3ca8d340a8c9adad268a5be57e (diff)
downloadPeerTube-41f2ebae4f970932fb62d2d8923b1f776f0b1494.tar.gz
PeerTube-41f2ebae4f970932fb62d2d8923b1f776f0b1494.tar.zst
PeerTube-41f2ebae4f970932fb62d2d8923b1f776f0b1494.zip
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
-rw-r--r--package.json1
-rw-r--r--server/helpers/activitypub.ts32
-rw-r--r--server/helpers/peertube-crypto.ts70
-rw-r--r--server/initializers/constants.ts7
-rw-r--r--server/lib/job-queue/handlers/utils/activitypub-http-utils.ts7
-rw-r--r--server/middlewares/activitypub.ts94
-rw-r--r--server/middlewares/validators/activitypub/signature.ts16
-rw-r--r--yarn.lock2
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 @@
109 "fluent-ffmpeg": "^2.1.0", 109 "fluent-ffmpeg": "^2.1.0",
110 "fs-extra": "^7.0.0", 110 "fs-extra": "^7.0.0",
111 "helmet": "^3.12.1", 111 "helmet": "^3.12.1",
112 "http-signature": "^1.2.0",
112 "ip-anonymize": "^0.0.6", 113 "ip-anonymize": "^0.0.6",
113 "ipaddr.js": "1.8.1", 114 "ipaddr.js": "1.8.1",
114 "is-cidr": "^2.0.5", 115 "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'
4import { Activity, ActivityPubActor } from '../../shared/models/activitypub' 4import { Activity, ActivityPubActor } from '../../shared/models/activitypub'
5import { ACTIVITY_PUB } from '../initializers' 5import { ACTIVITY_PUB } from '../initializers'
6import { ActorModel } from '../models/activitypub/actor' 6import { ActorModel } from '../models/activitypub/actor'
7import { signObject } from './peertube-crypto' 7import { signJsonLDObject } from './peertube-crypto'
8import { pageToStartAndCount } from './core-utils' 8import { pageToStartAndCount } from './core-utils'
9 9
10function activityPubContextify <T> (data: T) { 10function activityPubContextify <T> (data: T) {
@@ -15,22 +15,22 @@ function activityPubContextify <T> (data: T) {
15 { 15 {
16 RsaSignature2017: 'https://w3id.org/security#RsaSignature2017', 16 RsaSignature2017: 'https://w3id.org/security#RsaSignature2017',
17 pt: 'https://joinpeertube.org/ns', 17 pt: 'https://joinpeertube.org/ns',
18 schema: 'http://schema.org#', 18 sc: 'http://schema.org#',
19 Hashtag: 'as:Hashtag', 19 Hashtag: 'as:Hashtag',
20 uuid: 'schema:identifier', 20 uuid: 'sc:identifier',
21 category: 'schema:category', 21 category: 'sc:category',
22 licence: 'schema:license', 22 licence: 'sc:license',
23 subtitleLanguage: 'schema:subtitleLanguage', 23 subtitleLanguage: 'sc:subtitleLanguage',
24 sensitive: 'as:sensitive', 24 sensitive: 'as:sensitive',
25 language: 'schema:inLanguage', 25 language: 'sc:inLanguage',
26 views: 'schema:Number', 26 views: 'sc:Number',
27 stats: 'schema:Number', 27 stats: 'sc:Number',
28 size: 'schema:Number', 28 size: 'sc:Number',
29 fps: 'schema:Number', 29 fps: 'sc:Number',
30 commentsEnabled: 'schema:Boolean', 30 commentsEnabled: 'sc:Boolean',
31 waitTranscoding: 'schema:Boolean', 31 waitTranscoding: 'sc:Boolean',
32 expires: 'schema:expires', 32 expires: 'sc:expires',
33 support: 'schema:Text', 33 support: 'sc:Text',
34 CacheFile: 'pt:CacheFile' 34 CacheFile: 'pt:CacheFile'
35 }, 35 },
36 { 36 {
@@ -102,7 +102,7 @@ async function activityPubCollectionPagination (url: string, handler: ActivityPu
102function buildSignedActivity (byActor: ActorModel, data: Object) { 102function buildSignedActivity (byActor: ActorModel, data: Object) {
103 const activity = activityPubContextify(data) 103 const activity = activityPubContextify(data)
104 104
105 return signObject(byActor, activity) as Promise<Activity> 105 return signJsonLDObject(byActor, activity) as Promise<Activity>
106} 106}
107 107
108function getActorUrl (activityActor: string | ActivityPubActor) { 108function 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 @@
1import { BCRYPT_SALT_SIZE, PRIVATE_RSA_KEY_SIZE } from '../initializers' 1import { Request } from 'express'
2import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers'
2import { ActorModel } from '../models/activitypub/actor' 3import { ActorModel } from '../models/activitypub/actor'
3import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey } from './core-utils' 4import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey } from './core-utils'
4import { jsig } from './custom-jsonld-signature' 5import { jsig } from './custom-jsonld-signature'
5import { logger } from './logger' 6import { logger } from './logger'
6 7
8const httpSignature = require('http-signature')
9
7async function createPrivateAndPublicKeys () { 10async function createPrivateAndPublicKeys () {
8 logger.info('Generating a RSA key...') 11 logger.info('Generating a RSA key...')
9 12
@@ -13,18 +16,42 @@ async function createPrivateAndPublicKeys () {
13 return { privateKey: key, publicKey } 16 return { privateKey: key, publicKey }
14} 17}
15 18
16function isSignatureVerified (fromActor: ActorModel, signedDocument: object) { 19// User password checks
20
21function comparePassword (plainPassword: string, hashPassword: string) {
22 return bcryptComparePromise(plainPassword, hashPassword)
23}
24
25async function cryptPassword (password: string) {
26 const salt = await bcryptGenSaltPromise(BCRYPT_SALT_SIZE)
27
28 return bcryptHashPromise(password, salt)
29}
30
31// HTTP Signature
32
33function isHTTPSignatureVerified (httpSignatureParsed: any, actor: ActorModel) {
34 return httpSignature.verifySignature(httpSignatureParsed, actor.publicKey) === true
35}
36
37function parseHTTPSignature (req: Request) {
38 return httpSignature.parse(req, { authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME })
39}
40
41// JSONLD
42
43function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any) {
17 const publicKeyObject = { 44 const publicKeyObject = {
18 '@context': jsig.SECURITY_CONTEXT_URL, 45 '@context': jsig.SECURITY_CONTEXT_URL,
19 '@id': fromActor.url, 46 id: fromActor.url,
20 '@type': 'CryptographicKey', 47 type: 'CryptographicKey',
21 owner: fromActor.url, 48 owner: fromActor.url,
22 publicKeyPem: fromActor.publicKey 49 publicKeyPem: fromActor.publicKey
23 } 50 }
24 51
25 const publicKeyOwnerObject = { 52 const publicKeyOwnerObject = {
26 '@context': jsig.SECURITY_CONTEXT_URL, 53 '@context': jsig.SECURITY_CONTEXT_URL,
27 '@id': fromActor.url, 54 id: fromActor.url,
28 publicKey: [ publicKeyObject ] 55 publicKey: [ publicKeyObject ]
29 } 56 }
30 57
@@ -33,14 +60,19 @@ function isSignatureVerified (fromActor: ActorModel, signedDocument: object) {
33 publicKeyOwner: publicKeyOwnerObject 60 publicKeyOwner: publicKeyOwnerObject
34 } 61 }
35 62
36 return jsig.promises.verify(signedDocument, options) 63 return jsig.promises
37 .catch(err => { 64 .verify(signedDocument, options)
38 logger.error('Cannot check signature.', { err }) 65 .then((result: { verified: boolean }) => {
39 return false 66 logger.info('coucou', result)
40 }) 67 return result.verified
68 })
69 .catch(err => {
70 logger.error('Cannot check signature.', { err })
71 return false
72 })
41} 73}
42 74
43function signObject (byActor: ActorModel, data: any) { 75function signJsonLDObject (byActor: ActorModel, data: any) {
44 const options = { 76 const options = {
45 privateKeyPem: byActor.privateKey, 77 privateKeyPem: byActor.privateKey,
46 creator: byActor.url, 78 creator: byActor.url,
@@ -50,22 +82,14 @@ function signObject (byActor: ActorModel, data: any) {
50 return jsig.promises.sign(data, options) 82 return jsig.promises.sign(data, options)
51} 83}
52 84
53function comparePassword (plainPassword: string, hashPassword: string) {
54 return bcryptComparePromise(plainPassword, hashPassword)
55}
56
57async function cryptPassword (password: string) {
58 const salt = await bcryptGenSaltPromise(BCRYPT_SALT_SIZE)
59
60 return bcryptHashPromise(password, salt)
61}
62
63// --------------------------------------------------------------------------- 85// ---------------------------------------------------------------------------
64 86
65export { 87export {
66 isSignatureVerified, 88 parseHTTPSignature,
89 isHTTPSignatureVerified,
90 isJsonLDSignatureVerified,
67 comparePassword, 91 comparePassword,
68 createPrivateAndPublicKeys, 92 createPrivateAndPublicKeys,
69 cryptPassword, 93 cryptPassword,
70 signObject 94 signJsonLDObject
71} 95}
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 } = {
532 APPLICATION: 'Application' 532 APPLICATION: 'Application'
533} 533}
534 534
535const HTTP_SIGNATURE = {
536 HEADER_NAME: 'signature',
537 ALGORITHM: 'rsa-sha256',
538 HEADERS_TO_SIGN: [ 'date', 'host', 'digest', '(request-target)' ]
539}
540
535// --------------------------------------------------------------------------- 541// ---------------------------------------------------------------------------
536 542
537const PRIVATE_RSA_KEY_SIZE = 2048 543const PRIVATE_RSA_KEY_SIZE = 2048
@@ -731,6 +737,7 @@ export {
731 VIDEO_EXT_MIMETYPE, 737 VIDEO_EXT_MIMETYPE,
732 CRAWL_REQUEST_CONCURRENCY, 738 CRAWL_REQUEST_CONCURRENCY,
733 JOB_COMPLETED_LIFETIME, 739 JOB_COMPLETED_LIFETIME,
740 HTTP_SIGNATURE,
734 VIDEO_IMPORT_STATES, 741 VIDEO_IMPORT_STATES,
735 VIDEO_VIEW_LIFETIME, 742 VIDEO_VIEW_LIFETIME,
736 buildLanguages 743 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'
2import { getServerActor } from '../../../../helpers/utils' 2import { getServerActor } from '../../../../helpers/utils'
3import { ActorModel } from '../../../../models/activitypub/actor' 3import { ActorModel } from '../../../../models/activitypub/actor'
4import { sha256 } from '../../../../helpers/core-utils' 4import { sha256 } from '../../../../helpers/core-utils'
5import { HTTP_SIGNATURE } from '../../../../initializers'
5 6
6type Payload = { body: any, signatureActorId?: number } 7type Payload = { body: any, signatureActorId?: number }
7 8
@@ -29,11 +30,11 @@ async function buildSignedRequestOptions (payload: Payload) {
29 30
30 const keyId = actor.getWebfingerUrl() 31 const keyId = actor.getWebfingerUrl()
31 return { 32 return {
32 algorithm: 'rsa-sha256', 33 algorithm: HTTP_SIGNATURE.ALGORITHM,
33 authorizationHeaderName: 'Signature', 34 authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
34 keyId, 35 keyId,
35 key: actor.privateKey, 36 key: actor.privateKey,
36 headers: [ 'date', 'host', 'digest', '(request-target)' ] 37 headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
37 } 38 }
38} 39}
39 40
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'
2import { NextFunction, Request, RequestHandler, Response } from 'express' 2import { NextFunction, Request, RequestHandler, Response } from 'express'
3import { ActivityPubSignature } from '../../shared' 3import { ActivityPubSignature } from '../../shared'
4import { logger } from '../helpers/logger' 4import { logger } from '../helpers/logger'
5import { isSignatureVerified } from '../helpers/peertube-crypto' 5import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../helpers/peertube-crypto'
6import { ACCEPT_HEADERS, ACTIVITY_PUB } from '../initializers' 6import { ACCEPT_HEADERS, ACTIVITY_PUB, HTTP_SIGNATURE } from '../initializers'
7import { getOrCreateActorAndServerAndModel } from '../lib/activitypub' 7import { getOrCreateActorAndServerAndModel } from '../lib/activitypub'
8import { ActorModel } from '../models/activitypub/actor' 8import { ActorModel } from '../models/activitypub/actor'
9import { loadActorUrlOrGetFromWebfinger } from '../helpers/webfinger'
9 10
10async function checkSignature (req: Request, res: Response, next: NextFunction) { 11async function checkSignature (req: Request, res: Response, next: NextFunction) {
11 const signatureObject: ActivityPubSignature = req.body.signature 12 try {
13 const httpSignatureChecked = await checkHttpSignature(req, res)
14 if (httpSignatureChecked !== true) return
12 15
13 const [ creator ] = signatureObject.creator.split('#') 16 const actor: ActorModel = res.locals.signature.actor
14 17
15 logger.debug('Checking signature of actor %s...', creator) 18 // Forwarded activity
19 const bodyActor = req.body.actor
20 const bodyActorId = bodyActor && bodyActor.id ? bodyActor.id : bodyActor
21 if (bodyActorId && bodyActorId !== actor.url) {
22 const jsonLDSignatureChecked = await checkJsonLDSignature(req, res)
23 if (jsonLDSignatureChecked !== true) return
24 }
16 25
17 let actor: ActorModel 26 return next()
18 try {
19 actor = await getOrCreateActorAndServerAndModel(creator)
20 } catch (err) { 27 } catch (err) {
21 logger.warn('Cannot create remote actor %s and check signature.', creator, { err }) 28 logger.error('Error in ActivityPub signature checker.', err)
22 return res.sendStatus(403) 29 return res.sendStatus(403)
23 } 30 }
24
25 const verified = await isSignatureVerified(actor, req.body)
26 if (verified === false) return res.sendStatus(403)
27
28 res.locals.signature = {
29 actor
30 }
31
32 return next()
33} 31}
34 32
35function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) { 33function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) {
@@ -57,3 +55,63 @@ export {
57 checkSignature, 55 checkSignature,
58 executeIfActivityPub 56 executeIfActivityPub
59} 57}
58
59// ---------------------------------------------------------------------------
60
61async function checkHttpSignature (req: Request, res: Response) {
62 // FIXME: mastodon does not include the Signature scheme
63 const sig = req.headers[HTTP_SIGNATURE.HEADER_NAME] as string
64 if (sig && sig.startsWith('Signature ') === false) req.headers[HTTP_SIGNATURE.HEADER_NAME] = 'Signature ' + sig
65
66 const parsed = parseHTTPSignature(req)
67
68 const keyId = parsed.keyId
69 if (!keyId) {
70 res.sendStatus(403)
71 return false
72 }
73
74 logger.debug('Checking HTTP signature of actor %s...', keyId)
75
76 let [ actorUrl ] = keyId.split('#')
77 if (actorUrl.startsWith('acct:')) {
78 actorUrl = await loadActorUrlOrGetFromWebfinger(actorUrl.replace(/^acct:/, ''))
79 }
80
81 const actor = await getOrCreateActorAndServerAndModel(actorUrl)
82
83 const verified = isHTTPSignatureVerified(parsed, actor)
84 if (verified !== true) {
85 res.sendStatus(403)
86 return false
87 }
88
89 res.locals.signature = { actor }
90
91 return true
92}
93
94async function checkJsonLDSignature (req: Request, res: Response) {
95 const signatureObject: ActivityPubSignature = req.body.signature
96
97 if (!signatureObject.creator) {
98 res.sendStatus(403)
99 return false
100 }
101
102 const [ creator ] = signatureObject.creator.split('#')
103
104 logger.debug('Checking JsonLD signature of actor %s...', creator)
105
106 const actor = await getOrCreateActorAndServerAndModel(creator)
107 const verified = await isJsonLDSignatureVerified(actor, req.body)
108
109 if (verified !== true) {
110 res.sendStatus(403)
111 return false
112 }
113
114 res.locals.signature = { actor }
115
116 return true
117}
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'
9import { areValidationErrors } from '../utils' 9import { areValidationErrors } from '../utils'
10 10
11const signatureValidator = [ 11const signatureValidator = [
12 body('signature.type').custom(isSignatureTypeValid).withMessage('Should have a valid signature type'), 12 body('signature.type')
13 body('signature.created').custom(isDateValid).withMessage('Should have a valid signature created date'), 13 .optional()
14 body('signature.creator').custom(isSignatureCreatorValid).withMessage('Should have a valid signature creator'), 14 .custom(isSignatureTypeValid).withMessage('Should have a valid signature type'),
15 body('signature.signatureValue').custom(isSignatureValueValid).withMessage('Should have a valid signature value'), 15 body('signature.created')
16 .optional()
17 .custom(isDateValid).withMessage('Should have a valid signature created date'),
18 body('signature.creator')
19 .optional()
20 .custom(isSignatureCreatorValid).withMessage('Should have a valid signature creator'),
21 body('signature.signatureValue')
22 .optional()
23 .custom(isSignatureValueValid).withMessage('Should have a valid signature value'),
16 24
17 (req: express.Request, res: express.Response, next: express.NextFunction) => { 25 (req: express.Request, res: express.Response, next: express.NextFunction) => {
18 logger.debug('Checking activitypub signature parameter', { parameters: { signature: req.body.signature } }) 26 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:
4270 resolved "https://registry.yarnpkg.com/http-response-object/-/http-response-object-1.1.0.tgz#a7c4e75aae82f3bb4904e4f43f615673b4d518c3" 4270 resolved "https://registry.yarnpkg.com/http-response-object/-/http-response-object-1.1.0.tgz#a7c4e75aae82f3bb4904e4f43f615673b4d518c3"
4271 integrity sha1-p8TnWq6C87tJBOT0P2FWc7TVGMM= 4271 integrity sha1-p8TnWq6C87tJBOT0P2FWc7TVGMM=
4272 4272
4273http-signature@~1.2.0: 4273http-signature@^1.2.0, http-signature@~1.2.0:
4274 version "1.2.0" 4274 version "1.2.0"
4275 resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1" 4275 resolved "https://registry.yarnpkg.com/http-signature/-/http-signature-1.2.0.tgz#9aecd925114772f3d95b65a60abb8f7c18fbace1"
4276 integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE= 4276 integrity sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=