Add compatibility with other Linked Signature algorithms
authorChocobozzz <me@florianbigard.com>
Tue, 23 Oct 2018 09:38:48 +0000 (11:38 +0200)
committerChocobozzz <me@florianbigard.com>
Wed, 14 Nov 2018 15:32:27 +0000 (16:32 +0100)
29 files changed:
scripts/travis.sh
server.ts
server/helpers/custom-jsonld-signature.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/tests/activitypub.ts [deleted file]
server/tests/api/activitypub/client.ts [new file with mode: 0644]
server/tests/api/activitypub/helpers.ts [new file with mode: 0644]
server/tests/api/activitypub/index.ts [new file with mode: 0644]
server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json [new file with mode: 0644]
server/tests/api/activitypub/json/mastodon/bad-http-signature.json [new file with mode: 0644]
server/tests/api/activitypub/json/mastodon/bad-public-key.json [new file with mode: 0644]
server/tests/api/activitypub/json/mastodon/create-bad-signature.json [new file with mode: 0644]
server/tests/api/activitypub/json/mastodon/create.json [new file with mode: 0644]
server/tests/api/activitypub/json/mastodon/http-signature.json [new file with mode: 0644]
server/tests/api/activitypub/json/mastodon/public-key.json [new file with mode: 0644]
server/tests/api/activitypub/json/peertube/announce-without-context.json [new file with mode: 0644]
server/tests/api/activitypub/json/peertube/invalid-keys.json [new file with mode: 0644]
server/tests/api/activitypub/json/peertube/keys.json [new file with mode: 0644]
server/tests/api/activitypub/security.ts [new file with mode: 0644]
server/tests/api/index-4.ts
server/tests/index.ts
server/tests/utils/index.ts
server/tests/utils/miscs/sql.ts [new file with mode: 0644]
server/tests/utils/miscs/stubs.ts [new file with mode: 0644]
server/tests/utils/requests/activitypub.ts [new file with mode: 0644]
yarn.lock

index ae4a9f9260e0bbf4e7f11565d8bf2764d9107ceb..8f0c4a40d0de892b5532d16bf91e1522f073e07a 100755 (executable)
@@ -12,7 +12,6 @@ killall -q peertube || true
 if [ "$1" = "misc" ]; then
     npm run build -- --light-fr
     mocha --timeout 5000 --exit --require ts-node/register/type-check --bail server/tests/client.ts \
-        server/tests/activitypub.ts \
         server/tests/feeds/index.ts \
         server/tests/misc-endpoints.ts \
         server/tests/helpers/index.ts
index 51aa6763896a74086e64713a0b82261ac4e128e7..f3514cf9c1d8d9b487e1bcec00a4deb92be42723 100644 (file)
--- a/server.ts
+++ b/server.ts
@@ -28,7 +28,7 @@ import { checkMissedConfig, checkFFmpeg } from './server/initializers/checker-be
 
 // Do not use barrels because we don't want to load all modules here (we need to initialize database first)
 import { logger } from './server/helpers/logger'
-import { API_VERSION, CONFIG, CACHE } from './server/initializers/constants'
+import { API_VERSION, CONFIG, CACHE, HTTP_SIGNATURE } from './server/initializers/constants'
 
 const missed = checkMissedConfig()
 if (missed.length !== 0) {
@@ -96,6 +96,7 @@ import { RemoveOldJobsScheduler } from './server/lib/schedulers/remove-old-jobs-
 import { UpdateVideosScheduler } from './server/lib/schedulers/update-videos-scheduler'
 import { YoutubeDlUpdateScheduler } from './server/lib/schedulers/youtube-dl-update-scheduler'
 import { VideosRedundancyScheduler } from './server/lib/schedulers/videos-redundancy-scheduler'
+import { isHTTPSignatureDigestValid } from './server/helpers/peertube-crypto'
 
 // ----------- Command line -----------
 
@@ -131,7 +132,11 @@ app.use(morgan('combined', {
 app.use(bodyParser.urlencoded({ extended: false }))
 app.use(bodyParser.json({
   type: [ 'application/json', 'application/*+json' ],
-  limit: '500kb'
+  limit: '500kb',
+  verify: (req: express.Request, _, buf: Buffer, encoding: string) => {
+    const valid = isHTTPSignatureDigestValid(buf, req)
+    if (valid !== true) throw new Error('Invalid digest')
+  }
 }))
 // Cookies
 app.use(cookieParser())
index e4f28018e0767806f452ab72cb5b2471fd7d6fbf..27a187db1d35e3da55eacf864d61092f16e04799 100644 (file)
@@ -1,5 +1,5 @@
 import * as AsyncLRU from 'async-lru'
-import * as jsonld from 'jsonld/'
+import * as jsonld from 'jsonld'
 import * as jsig from 'jsonld-signatures'
 
 const nodeDocumentLoader = jsonld.documentLoaders.node()
@@ -17,4 +17,4 @@ jsonld.documentLoader = (url, cb) => {
 
 jsig.use('jsonld', jsonld)
 
-export { jsig }
+export { jsig, jsonld }
index 8ef7b1359422d458e44add61cae5028a22d38e0d..ab9ec077ee3957806d6009207d5349fedb0209f6 100644 (file)
@@ -1,9 +1,12 @@
 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 { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey, sha256 } from './core-utils'
+import { jsig, jsonld } from './custom-jsonld-signature'
 import { logger } from './logger'
+import { cloneDeep } from 'lodash'
+import { createVerify } from 'crypto'
+import { buildDigest } from '../lib/job-queue/handlers/utils/activitypub-http-utils'
 
 const httpSignature = require('http-signature')
 
@@ -30,21 +33,36 @@ async function cryptPassword (password: string) {
 
 // HTTP Signature
 
-function isHTTPSignatureVerified (httpSignatureParsed: any, actor: ActorModel) {
+function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean {
+  if (req.headers[HTTP_SIGNATURE.HEADER_NAME] && req.headers['digest']) {
+    return buildDigest(rawBody.toString()) === req.headers['digest']
+  }
+
+  return true
+}
+
+function isHTTPSignatureVerified (httpSignatureParsed: any, actor: ActorModel): boolean {
   return httpSignature.verifySignature(httpSignatureParsed, actor.publicKey) === true
 }
 
-function parseHTTPSignature (req: Request) {
-  return httpSignature.parse(req, { authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME })
+function parseHTTPSignature (req: Request, clockSkew?: number) {
+  return httpSignature.parse(req, { authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME, clockSkew })
 }
 
 // JSONLD
 
-function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any) {
+async function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any): Promise<boolean> {
+  if (signedDocument.signature.type === 'RsaSignature2017') {
+    // Mastodon algorithm
+    const res = await isJsonLDRSA2017Verified(fromActor, signedDocument)
+    // Success? If no, try with our library
+    if (res === true) return true
+  }
+
   const publicKeyObject = {
     '@context': jsig.SECURITY_CONTEXT_URL,
     id: fromActor.url,
-    type:  'CryptographicKey',
+    type: 'CryptographicKey',
     owner: fromActor.url,
     publicKeyPem: fromActor.publicKey
   }
@@ -69,6 +87,44 @@ function isJsonLDSignatureVerified (fromActor: ActorModel, signedDocument: any)
              })
 }
 
+// Backward compatibility with "other" implementations
+async function isJsonLDRSA2017Verified (fromActor: ActorModel, signedDocument: any) {
+  function hash (obj: any): Promise<any> {
+    return jsonld.promises
+                 .normalize(obj, {
+                   algorithm: 'URDNA2015',
+                   format: 'application/n-quads'
+                 })
+                 .then(res => sha256(res))
+  }
+
+  const signatureCopy = cloneDeep(signedDocument.signature)
+  Object.assign(signatureCopy, {
+    '@context': [
+      'https://w3id.org/security/v1',
+      { RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' }
+    ]
+  })
+  delete signatureCopy.type
+  delete signatureCopy.id
+  delete signatureCopy.signatureValue
+
+  const docWithoutSignature = cloneDeep(signedDocument)
+  delete docWithoutSignature.signature
+
+  const [ documentHash, optionsHash ] = await Promise.all([
+    hash(docWithoutSignature),
+    hash(signatureCopy)
+  ])
+
+  const toVerify = optionsHash + documentHash
+
+  const verify = createVerify('RSA-SHA256')
+  verify.update(toVerify, 'utf8')
+
+  return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64')
+}
+
 function signJsonLDObject (byActor: ActorModel, data: any) {
   const options = {
     privateKeyPem: byActor.privateKey,
@@ -82,6 +138,7 @@ function signJsonLDObject (byActor: ActorModel, data: any) {
 // ---------------------------------------------------------------------------
 
 export {
+  isHTTPSignatureDigestValid,
   parseHTTPSignature,
   isHTTPSignatureVerified,
   isJsonLDSignatureVerified,
index 28d51068b03222263b1a16b71e51e9abdf953b8a..9aadbe824ea6670b3fcfe25edf61269d84ede6da 100644 (file)
@@ -535,7 +535,7 @@ const ACTIVITY_PUB_ACTOR_TYPES: { [ id: string ]: ActivityPubActorType } = {
 const HTTP_SIGNATURE = {
   HEADER_NAME: 'signature',
   ALGORITHM: 'rsa-sha256',
-  HEADERS_TO_SIGN: [ 'date', 'host', 'digest', '(request-target)' ]
+  HEADERS_TO_SIGN: [ '(request-target)', 'host', 'date', 'digest' ]
 }
 
 // ---------------------------------------------------------------------------
index fd9c743413daa42fa8a7af747ea715355e5d4bb4..4961d4502e2a5b43ceb2ed7f6c6c2f49854ebf2e 100644 (file)
@@ -38,15 +38,20 @@ async function buildSignedRequestOptions (payload: Payload) {
   }
 }
 
-function buildGlobalHeaders (body: object) {
-  const digest = 'SHA-256=' + sha256(JSON.stringify(body), 'base64')
-
+function buildGlobalHeaders (body: any) {
   return {
-    'Digest': digest
+    'Digest': buildDigest(body)
   }
 }
 
+function buildDigest (body: any) {
+  const rawBody = typeof body === 'string' ? body : JSON.stringify(body)
+
+  return 'SHA-256=' + sha256(rawBody, 'base64')
+}
+
 export {
+  buildDigest,
   buildGlobalHeaders,
   computeBody,
   buildSignedRequestOptions
index 1ec8884772d6ea8186c92a3cdfae8f416caf0c6c..01e5dd24e6423644bf52cbf53850d7ca69232609 100644 (file)
@@ -53,7 +53,8 @@ function executeIfActivityPub (fun: RequestHandler | RequestHandler[]) {
 
 export {
   checkSignature,
-  executeIfActivityPub
+  executeIfActivityPub,
+  checkHttpSignature
 }
 
 // ---------------------------------------------------------------------------
@@ -94,7 +95,7 @@ async function checkHttpSignature (req: Request, res: Response) {
 async function checkJsonLDSignature (req: Request, res: Response) {
   const signatureObject: ActivityPubSignature = req.body.signature
 
-  if (!signatureObject.creator) {
+  if (!signatureObject || !signatureObject.creator) {
     res.sendStatus(403)
     return false
   }
diff --git a/server/tests/activitypub.ts b/server/tests/activitypub.ts
deleted file mode 100644 (file)
index 53a04d3..0000000
+++ /dev/null
@@ -1,35 +0,0 @@
-/* tslint:disable:no-unused-expression */
-
-import * as chai from 'chai'
-import 'mocha'
-import { flushTests, killallServers, makeActivityPubGetRequest, runServer, ServerInfo, setAccessTokensToServers } from './utils'
-
-const expect = chai.expect
-
-describe('Test activitypub', function () {
-  let server: ServerInfo = null
-
-  before(async function () {
-    this.timeout(30000)
-
-    await flushTests()
-
-    server = await runServer(1)
-
-    await setAccessTokensToServers([ server ])
-  })
-
-  it('Should return the account object', async function () {
-    const res = await makeActivityPubGetRequest(server.url, '/accounts/root')
-    const object = res.body
-
-    expect(object.type).to.equal('Person')
-    expect(object.id).to.equal('http://localhost:9001/accounts/root')
-    expect(object.name).to.equal('root')
-    expect(object.preferredUsername).to.equal('root')
-  })
-
-  after(async function () {
-    killallServers([ server ])
-  })
-})
diff --git a/server/tests/api/activitypub/client.ts b/server/tests/api/activitypub/client.ts
new file mode 100644 (file)
index 0000000..5ca8bdf
--- /dev/null
@@ -0,0 +1,35 @@
+/* tslint:disable:no-unused-expression */
+
+import * as chai from 'chai'
+import 'mocha'
+import { flushTests, killallServers, makeActivityPubGetRequest, runServer, ServerInfo, setAccessTokensToServers } from '../../utils'
+
+const expect = chai.expect
+
+describe('Test activitypub', function () {
+  let server: ServerInfo = null
+
+  before(async function () {
+    this.timeout(30000)
+
+    await flushTests()
+
+    server = await runServer(1)
+
+    await setAccessTokensToServers([ server ])
+  })
+
+  it('Should return the account object', async function () {
+    const res = await makeActivityPubGetRequest(server.url, '/accounts/root')
+    const object = res.body
+
+    expect(object.type).to.equal('Person')
+    expect(object.id).to.equal('http://localhost:9001/accounts/root')
+    expect(object.name).to.equal('root')
+    expect(object.preferredUsername).to.equal('root')
+  })
+
+  after(async function () {
+    killallServers([ server ])
+  })
+})
diff --git a/server/tests/api/activitypub/helpers.ts b/server/tests/api/activitypub/helpers.ts
new file mode 100644 (file)
index 0000000..6108462
--- /dev/null
@@ -0,0 +1,182 @@
+/* tslint:disable:no-unused-expression */
+
+import 'mocha'
+import { expect } from 'chai'
+import { buildRequestStub } from '../../utils'
+import { isHTTPSignatureVerified, isJsonLDSignatureVerified, parseHTTPSignature } from '../../../helpers/peertube-crypto'
+import { cloneDeep } from 'lodash'
+import { buildSignedActivity } from '../../../helpers/activitypub'
+
+describe('Test activity pub helpers', function () {
+  describe('When checking the Linked Signature', function () {
+
+    it('Should fail with an invalid Mastodon signature', async function () {
+      const body = require('./json/mastodon/create-bad-signature.json')
+      const publicKey = require('./json/mastodon/public-key.json').publicKey
+      const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
+
+      const result = await isJsonLDSignatureVerified(fromActor as any, body)
+
+      expect(result).to.be.false
+    })
+
+    it('Should fail with an invalid public key', async function () {
+      const body = require('./json/mastodon/create.json')
+      const publicKey = require('./json/mastodon/bad-public-key.json').publicKey
+      const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
+
+      const result = await isJsonLDSignatureVerified(fromActor as any, body)
+
+      expect(result).to.be.false
+    })
+
+    it('Should succeed with a valid Mastodon signature', async function () {
+      const body = require('./json/mastodon/create.json')
+      const publicKey = require('./json/mastodon/public-key.json').publicKey
+      const fromActor = { publicKey, url: 'http://localhost:9002/accounts/peertube' }
+
+      const result = await isJsonLDSignatureVerified(fromActor as any, body)
+
+      expect(result).to.be.true
+    })
+
+    it('Should fail with an invalid PeerTube signature', async function () {
+      const keys = require('./json/peertube/invalid-keys.json')
+      const body = require('./json/peertube/announce-without-context.json')
+
+      const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
+      const signedBody = await buildSignedActivity(actorSignature as any, body)
+
+      const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' }
+      const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
+
+      expect(result).to.be.false
+    })
+
+    it('Should fail with an invalid PeerTube URL', async function () {
+      const keys = require('./json/peertube/keys.json')
+      const body = require('./json/peertube/announce-without-context.json')
+
+      const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
+      const signedBody = await buildSignedActivity(actorSignature as any, body)
+
+      const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9003/accounts/peertube' }
+      const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
+
+      expect(result).to.be.false
+    })
+
+    it('Should succeed with a valid PeerTube signature', async function () {
+      const keys = require('./json/peertube/keys.json')
+      const body = require('./json/peertube/announce-without-context.json')
+
+      const actorSignature = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
+      const signedBody = await buildSignedActivity(actorSignature as any, body)
+
+      const fromActor = { publicKey: keys.publicKey, url: 'http://localhost:9002/accounts/peertube' }
+      const result = await isJsonLDSignatureVerified(fromActor as any, signedBody)
+
+      expect(result).to.be.true
+    })
+  })
+
+  describe('When checking HTTP signature', function () {
+    it('Should fail with an invalid http signature', async function () {
+      const req = buildRequestStub()
+      req.method = 'POST'
+      req.url = '/accounts/ronan/inbox'
+
+      const mastodonObject = cloneDeep(require('./json/mastodon/bad-http-signature.json'))
+      req.body = mastodonObject.body
+      req.headers = mastodonObject.headers
+      req.headers.signature = 'Signature ' + req.headers.signature
+
+      const parsed = parseHTTPSignature(req, 3600 * 365 * 3)
+      const publicKey = require('./json/mastodon/public-key.json').publicKey
+
+      const actor = { publicKey }
+      const verified = isHTTPSignatureVerified(parsed, actor as any)
+
+      expect(verified).to.be.false
+    })
+
+    it('Should fail with an invalid public key', async function () {
+      const req = buildRequestStub()
+      req.method = 'POST'
+      req.url = '/accounts/ronan/inbox'
+
+      const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json'))
+      req.body = mastodonObject.body
+      req.headers = mastodonObject.headers
+      req.headers.signature = 'Signature ' + req.headers.signature
+
+      const parsed = parseHTTPSignature(req, 3600 * 365 * 3)
+      const publicKey = require('./json/mastodon/bad-public-key.json').publicKey
+
+      const actor = { publicKey }
+      const verified = isHTTPSignatureVerified(parsed, actor as any)
+
+      expect(verified).to.be.false
+    })
+
+    it('Should fail because of clock skew', async function () {
+      const req = buildRequestStub()
+      req.method = 'POST'
+      req.url = '/accounts/ronan/inbox'
+
+      const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json'))
+      req.body = mastodonObject.body
+      req.headers = mastodonObject.headers
+      req.headers.signature = 'Signature ' + req.headers.signature
+
+      let errored = false
+      try {
+        parseHTTPSignature(req)
+      } catch {
+        errored = true
+      }
+
+      expect(errored).to.be.true
+    })
+
+    it('Should fail without scheme', async function () {
+      const req = buildRequestStub()
+      req.method = 'POST'
+      req.url = '/accounts/ronan/inbox'
+
+      const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json'))
+      req.body = mastodonObject.body
+      req.headers = mastodonObject.headers
+
+      let errored = false
+      try {
+        parseHTTPSignature(req, 3600 * 365 * 3)
+      } catch {
+        errored = true
+      }
+
+      expect(errored).to.be.true
+    })
+
+    it('Should succeed with a valid signature', async function () {
+      const req = buildRequestStub()
+      req.method = 'POST'
+      req.url = '/accounts/ronan/inbox'
+
+      const mastodonObject = cloneDeep(require('./json/mastodon/http-signature.json'))
+      req.body = mastodonObject.body
+      req.headers = mastodonObject.headers
+      req.headers.signature = 'Signature ' + req.headers.signature
+
+      const parsed = parseHTTPSignature(req, 3600 * 365 * 3)
+      const publicKey = require('./json/mastodon/public-key.json').publicKey
+
+      const actor = { publicKey }
+      const verified = isHTTPSignatureVerified(parsed, actor as any)
+
+      expect(verified).to.be.true
+    })
+
+  })
+
+})
diff --git a/server/tests/api/activitypub/index.ts b/server/tests/api/activitypub/index.ts
new file mode 100644 (file)
index 0000000..de8a599
--- /dev/null
@@ -0,0 +1,3 @@
+import './client'
+import './helpers'
+import './security'
diff --git a/server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json b/server/tests/api/activitypub/json/mastodon/bad-body-http-signature.json
new file mode 100644 (file)
index 0000000..4e7bc3a
--- /dev/null
@@ -0,0 +1,93 @@
+{
+  "headers": {
+    "user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)",
+    "host": "localhost",
+    "date": "Mon, 22 Oct 2018 13:34:22 GMT",
+    "accept-encoding": "gzip",
+    "digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=",
+    "content-type": "application/activity+json",
+    "signature": "keyId=\"http://localhost:3000/users/ronan2#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"oLKbgxdFXdXsHJ3x/UsG9Svu7oa8Dyqiy6Jif4wqNuhAqRVMRaG18f+dd2OcfFX3XRGF8p8flZkU6vvoEQBauTwGRGcgXAJuKC1zYIWGk+PeiW8lNUnE4qGapWcTiFnIo7FKauNdsgqg/tvgs1pQIdHkDDjZMI64twP7sTN/4vG1PCq+kyqi/DM+ORLi/W7vFuLVHt2Iz7ikfw/R3/mMtS4FwLops+tVYBQ2iQ9DVRhTwLKVbeL/LLVB/tdGzNZ4F4nImBAQQ9I7WpPM6J/k+cBmoEbrUKs8ptx9gbX3OSsl5wlvPVMNzU9F9yb2MrB/Y/J4qssKz+LbiaktKGj7OQ==\"",
+    "content-length": "2815"
+  },
+  "body": {
+    "@context": [
+      "https://www.w3.org/ns/activitystreams",
+      "https://w3id.org/security/v1",
+      {
+        "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+        "sensitive": "as:sensitive",
+        "movedTo": {
+          "@id": "as:movedTo",
+          "@type": "@id"
+        },
+        "Hashtag": "as:Hashtag",
+        "ostatus": "http://ostatus.org#",
+        "atomUri": "ostatus:atomUri",
+        "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+        "conversation": "ostatus:conversation",
+        "toot": "http://joinmastodon.org/ns#",
+        "Emoji": "toot:Emoji",
+        "focalPoint": {
+          "@container": "@list",
+          "@id": "toot:focalPoint"
+        },
+        "featured": {
+          "@id": "toot:featured",
+          "@type": "@id"
+        },
+        "schema": "http://schema.org#",
+        "PropertyValue": "schema:PropertyValue",
+        "value": "schema:value"
+      }
+    ],
+    "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity",
+    "type": "Create",
+    "actor": "http://localhost:3000/users/ronan2",
+    "published": "2018-10-22T13:34:18Z",
+    "to": [
+      "https://www.w3.org/ns/activitystreams#Public"
+    ],
+    "cc": [
+      "http://localhost:3000/users/ronan2/followers",
+      "http://localhost:9000/accounts/ronan"
+    ],
+    "object": {
+      "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
+      "type": "Note",
+      "summary": null,
+      "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
+      "published": "2018-10-22T13:34:18Z",
+      "url": "http://localhost:3000/@ronan2/100939547203370948",
+      "attributedTo": "http://localhost:3000/users/ronan2",
+      "to": [
+        "https://www.w3.org/ns/activitystreams#Public"
+      ],
+      "cc": [
+        "http://localhost:3000/users/ronan2/followers",
+        "http://localhost:9000/accounts/ronan"
+      ],
+      "sensitive": false,
+      "atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
+      "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
+      "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
+      "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>",
+      "contentMap": {
+        "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>"
+      },
+      "attachment": [],
+      "tag": [
+        {
+          "type": "Mention",
+          "href": "http://localhost:9000/accounts/ronan",
+          "name": "@ronan@localhost:9000"
+        }
+      ]
+    },
+    "signature": {
+      "type": "RsaSignature2017",
+      "creator": "http://localhost:3000/users/ronan2#main-key",
+      "created": "2018-10-22T13:34:19Z",
+      "signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q=="
+    }
+  }
+}
diff --git a/server/tests/api/activitypub/json/mastodon/bad-http-signature.json b/server/tests/api/activitypub/json/mastodon/bad-http-signature.json
new file mode 100644 (file)
index 0000000..098597d
--- /dev/null
@@ -0,0 +1,93 @@
+{
+  "headers": {
+    "user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)",
+    "host": "localhost",
+    "date": "Mon, 22 Oct 2018 13:34:22 GMT",
+    "accept-encoding": "gzip",
+    "digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=",
+    "content-type": "application/activity+json",
+    "signature": "keyId=\"http://localhost:3000/users/ronan2#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"oLKbgxdFXdXsHJ3x/UsG9Svu7oa8Dyqiy6Jif4wqNuhAqRVMRaG18f+dd2OcfFX3XRGF8p8flZkU6vvoEQBauTwGRGcgXAJuKC1zYIWGk+PeiW8lNUnE4qGapWcTiFnIo7FKauNdsgqg/tvgs1pQIdHkDDjZMI64twP7sTN/4vG1PCq+kyqi/DM+ORLi/W7vFuLVHt2Iz7ikfw/R3/mMtS4FwLops+tVYBQ2iQ9DVRhTwLKVbeL/LLVB/tdGzNZ4F4nImBAQQ9I7WpPM6J/k+cBmoEbrUKs8ptx9gbX3OSsl4wlvPVMNzU9F9yb2MrB/Y/J4qssKz+LbiaktKGj7OQ==\"",
+    "content-length": "2815"
+  },
+  "body": {
+    "@context": [
+      "https://www.w3.org/ns/activitystreams",
+      "https://w3id.org/security/v1",
+      {
+        "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+        "sensitive": "as:sensitive",
+        "movedTo": {
+          "@id": "as:movedTo",
+          "@type": "@id"
+        },
+        "Hashtag": "as:Hashtag",
+        "ostatus": "http://ostatus.org#",
+        "atomUri": "ostatus:atomUri",
+        "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+        "conversation": "ostatus:conversation",
+        "toot": "http://joinmastodon.org/ns#",
+        "Emoji": "toot:Emoji",
+        "focalPoint": {
+          "@container": "@list",
+          "@id": "toot:focalPoint"
+        },
+        "featured": {
+          "@id": "toot:featured",
+          "@type": "@id"
+        },
+        "schema": "http://schema.org#",
+        "PropertyValue": "schema:PropertyValue",
+        "value": "schema:value"
+      }
+    ],
+    "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity",
+    "type": "Create",
+    "actor": "http://localhost:3000/users/ronan2",
+    "published": "2018-10-22T13:34:18Z",
+    "to": [
+      "https://www.w3.org/ns/activitystreams#Public"
+    ],
+    "cc": [
+      "http://localhost:3000/users/ronan2/followers",
+      "http://localhost:9000/accounts/ronan"
+    ],
+    "object": {
+      "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
+      "type": "Note",
+      "summary": null,
+      "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
+      "published": "2018-10-22T13:34:18Z",
+      "url": "http://localhost:3000/@ronan2/100939547203370948",
+      "attributedTo": "http://localhost:3000/users/ronan2",
+      "to": [
+        "https://www.w3.org/ns/activitystreams#Public"
+      ],
+      "cc": [
+        "http://localhost:3000/users/ronan2/followers",
+        "http://localhost:9000/accounts/ronan"
+      ],
+      "sensitive": false,
+      "atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
+      "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
+      "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
+      "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>",
+      "contentMap": {
+        "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>"
+      },
+      "attachment": [],
+      "tag": [
+        {
+          "type": "Mention",
+          "href": "http://localhost:9000/accounts/ronan",
+          "name": "@ronan@localhost:9000"
+        }
+      ]
+    },
+    "signature": {
+      "type": "RsaSignature2017",
+      "creator": "http://localhost:3000/users/ronan2#main-key",
+      "created": "2018-10-22T13:34:19Z",
+      "signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q=="
+    }
+  }
+}
diff --git a/server/tests/api/activitypub/json/mastodon/bad-public-key.json b/server/tests/api/activitypub/json/mastodon/bad-public-key.json
new file mode 100644 (file)
index 0000000..73d18b3
--- /dev/null
@@ -0,0 +1,3 @@
+{
+  "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0YyuthHtWWgDe0Fdgdp2\ndC5dTJsRqW6pFw5omIYYYjoES/WRewhVxEA54BhmxD3L1zChfx131N1TS8jVowhW\nm999jpUffKCCvLgYKIXETJDHiDeMONVx8wp7v9fS1HiFXo/E5und39gUMs14CMFZ\n6PE5jRV3r4XIKQJHQl7/X5n5FOb2934K+1TKUeBkbft/AushlKatYQakt3qHxpwx\nFvE+JjGo7QTnzdjaOx/e5QvojdGi2Kx4+jl77j2WVcSo5lOBz04OAVJtChtn82vS\nulPdDh3hZcDn+WK67yAhGP6AnzvOybZZS4zowlKiQ3kqjVVXKdl8gAsL4Y7MZ40R\nJQIDAQAB\n-----END PUBLIC KEY-----\n"
+}
diff --git a/server/tests/api/activitypub/json/mastodon/create-bad-signature.json b/server/tests/api/activitypub/json/mastodon/create-bad-signature.json
new file mode 100644 (file)
index 0000000..2cd0372
--- /dev/null
@@ -0,0 +1,81 @@
+{
+  "@context": [
+    "https://www.w3.org/ns/activitystreams",
+    "https://w3id.org/security/v1",
+    {
+      "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+      "sensitive": "as:sensitive",
+      "movedTo": {
+        "@id": "as:movedTo",
+        "@type": "@id"
+      },
+      "Hashtag": "as:Hashtag",
+      "ostatus": "http://ostatus.org#",
+      "atomUri": "ostatus:atomUri",
+      "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+      "conversation": "ostatus:conversation",
+      "toot": "http://joinmastodon.org/ns#",
+      "Emoji": "toot:Emoji",
+      "focalPoint": {
+        "@container": "@list",
+        "@id": "toot:focalPoint"
+      },
+      "featured": {
+        "@id": "toot:featured",
+        "@type": "@id"
+      },
+      "schema": "http://schema.org#",
+      "PropertyValue": "schema:PropertyValue",
+      "value": "schema:value"
+    }
+  ],
+  "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698/activity",
+  "type": "Create",
+  "actor": "http://localhost:3000/users/ronan2",
+  "published": "2018-10-22T12:43:07Z",
+  "to": [
+    "https://www.w3.org/ns/activitystreams#Public"
+  ],
+  "cc": [
+    "http://localhost:3000/users/ronan2/followers",
+    "http://localhost:9000/accounts/ronan"
+  ],
+  "object": {
+    "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698",
+    "type": "Note",
+    "summary": null,
+    "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
+    "published": "2018-10-22T12:43:07Z",
+    "url": "http://localhost:3000/@ronan2/100939345950887698",
+    "attributedTo": "http://localhost:3000/users/ronan2",
+    "to": [
+      "https://www.w3.org/ns/activitystreams#Public"
+    ],
+    "cc": [
+      "http://localhost:3000/users/ronan2/followers",
+      "http://localhost:9000/accounts/ronan"
+    ],
+    "sensitive": false,
+    "atomUri": "http://localhost:3000/users/ronan2/statuses/100939345950887698",
+    "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
+    "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
+    "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>",
+    "contentMap": {
+      "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>"
+    },
+    "attachment": [],
+    "tag": [
+      {
+        "type": "Mention",
+        "href": "http://localhost:9000/accounts/ronan",
+        "name": "@ronan@localhost:9000"
+      }
+    ]
+  },
+  "signature": {
+    "type": "RsaSignature2017",
+    "creator": "http://localhost:3000/users/ronan2#main-key",
+    "created": "2018-10-22T12:43:08Z",
+    "signatureValue": "Vgr8nA0agPr9TcA4BlX+MWhmuE+rBcoIJLpnPbm3E5SnOCXbgjEfEaTLqfuzzkKNsR3PBbkvi3YWK4/DxJ0zmpzSB7yy4NRzluQMVQHqJiFKXAX3Sr3fIrK24xkWW9/F207c1NpFajSGbgnFKBdtFE0e5VqwSrSoOJkZukZW/2ATSnsyzblieuUmvTWpD0PqpUOsynPjw+RqZnqPn0cjw1z2Dm7ZRt3trnyMTXFYZw5U/YuqMY2kpadD6vq780md8kXlJIylxG6ZrlO2jz9fJdnfuVq43d4QFNsBm1K1r2WtNqX+i+wiqh+u3PjF4pzXtl/a3hJOH18IfZnK7I21mQ=="
+  }
+}
diff --git a/server/tests/api/activitypub/json/mastodon/create.json b/server/tests/api/activitypub/json/mastodon/create.json
new file mode 100644 (file)
index 0000000..0be271b
--- /dev/null
@@ -0,0 +1,81 @@
+{
+  "@context": [
+    "https://www.w3.org/ns/activitystreams",
+    "https://w3id.org/security/v1",
+    {
+      "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+      "sensitive": "as:sensitive",
+      "movedTo": {
+        "@id": "as:movedTo",
+        "@type": "@id"
+      },
+      "Hashtag": "as:Hashtag",
+      "ostatus": "http://ostatus.org#",
+      "atomUri": "ostatus:atomUri",
+      "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+      "conversation": "ostatus:conversation",
+      "toot": "http://joinmastodon.org/ns#",
+      "Emoji": "toot:Emoji",
+      "focalPoint": {
+        "@container": "@list",
+        "@id": "toot:focalPoint"
+      },
+      "featured": {
+        "@id": "toot:featured",
+        "@type": "@id"
+      },
+      "schema": "http://schema.org#",
+      "PropertyValue": "schema:PropertyValue",
+      "value": "schema:value"
+    }
+  ],
+  "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698/activity",
+  "type": "Create",
+  "actor": "http://localhost:3000/users/ronan2",
+  "published": "2018-10-22T12:43:07Z",
+  "to": [
+    "https://www.w3.org/ns/activitystreams#Public"
+  ],
+  "cc": [
+    "http://localhost:3000/users/ronan2/followers",
+    "http://localhost:9000/accounts/ronan"
+  ],
+  "object": {
+    "id": "http://localhost:3000/users/ronan2/statuses/100939345950887698",
+    "type": "Note",
+    "summary": null,
+    "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
+    "published": "2018-10-22T12:43:07Z",
+    "url": "http://localhost:3000/@ronan2/100939345950887698",
+    "attributedTo": "http://localhost:3000/users/ronan2",
+    "to": [
+      "https://www.w3.org/ns/activitystreams#Public"
+    ],
+    "cc": [
+      "http://localhost:3000/users/ronan2/followers",
+      "http://localhost:9000/accounts/ronan"
+    ],
+    "sensitive": false,
+    "atomUri": "http://localhost:3000/users/ronan2/statuses/100939345950887698",
+    "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
+    "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
+    "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>",
+    "contentMap": {
+      "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zerg</p>"
+    },
+    "attachment": [],
+    "tag": [
+      {
+        "type": "Mention",
+        "href": "http://localhost:9000/accounts/ronan",
+        "name": "@ronan@localhost:9000"
+      }
+    ]
+  },
+  "signature": {
+    "type": "RsaSignature2017",
+    "creator": "http://localhost:3000/users/ronan2#main-key",
+    "created": "2018-10-22T12:43:08Z",
+    "signatureValue": "VgR8nA0agPr9TcA4BlX+MWhmuE+rBcoIJLpnPbm3E5SnOCXbgjEfEaTLqfuzzkKNsR3PBbkvi3YWK4/DxJ0zmpzSB7yy4NRzluQMVQHqJiFKXAX3Sr3fIrK24xkWW9/F207c1NpFajSGbgnFKBdtFE0e5VqwSrSoOJkZukZW/2ATSnsyzblieuUmvTWpD0PqpUOsynPjw+RqZnqPn0cjw1z2Dm7ZRt3trnyMTXFYZw5U/YuqMY2kpadD6vq780md8kXlJIylxG6ZrlO2jz9fJdnfuVq43d4QFNsBm1K1r2WtNqX+i+wiqh+u3PjF4pzXtl/a3hJOH18IfZnK7I21mQ=="
+  }
+}
diff --git a/server/tests/api/activitypub/json/mastodon/http-signature.json b/server/tests/api/activitypub/json/mastodon/http-signature.json
new file mode 100644 (file)
index 0000000..4e7bc3a
--- /dev/null
@@ -0,0 +1,93 @@
+{
+  "headers": {
+    "user-agent": "http.rb/3.3.0 (Mastodon/2.5.0; +http://localhost:3000/)",
+    "host": "localhost",
+    "date": "Mon, 22 Oct 2018 13:34:22 GMT",
+    "accept-encoding": "gzip",
+    "digest": "SHA-256=FEr5j2WSSfdEMcG3NTOXuGU0lUchfTJx4+BtUlWOwDk=",
+    "content-type": "application/activity+json",
+    "signature": "keyId=\"http://localhost:3000/users/ronan2#main-key\",algorithm=\"rsa-sha256\",headers=\"(request-target) host date digest content-type\",signature=\"oLKbgxdFXdXsHJ3x/UsG9Svu7oa8Dyqiy6Jif4wqNuhAqRVMRaG18f+dd2OcfFX3XRGF8p8flZkU6vvoEQBauTwGRGcgXAJuKC1zYIWGk+PeiW8lNUnE4qGapWcTiFnIo7FKauNdsgqg/tvgs1pQIdHkDDjZMI64twP7sTN/4vG1PCq+kyqi/DM+ORLi/W7vFuLVHt2Iz7ikfw/R3/mMtS4FwLops+tVYBQ2iQ9DVRhTwLKVbeL/LLVB/tdGzNZ4F4nImBAQQ9I7WpPM6J/k+cBmoEbrUKs8ptx9gbX3OSsl5wlvPVMNzU9F9yb2MrB/Y/J4qssKz+LbiaktKGj7OQ==\"",
+    "content-length": "2815"
+  },
+  "body": {
+    "@context": [
+      "https://www.w3.org/ns/activitystreams",
+      "https://w3id.org/security/v1",
+      {
+        "manuallyApprovesFollowers": "as:manuallyApprovesFollowers",
+        "sensitive": "as:sensitive",
+        "movedTo": {
+          "@id": "as:movedTo",
+          "@type": "@id"
+        },
+        "Hashtag": "as:Hashtag",
+        "ostatus": "http://ostatus.org#",
+        "atomUri": "ostatus:atomUri",
+        "inReplyToAtomUri": "ostatus:inReplyToAtomUri",
+        "conversation": "ostatus:conversation",
+        "toot": "http://joinmastodon.org/ns#",
+        "Emoji": "toot:Emoji",
+        "focalPoint": {
+          "@container": "@list",
+          "@id": "toot:focalPoint"
+        },
+        "featured": {
+          "@id": "toot:featured",
+          "@type": "@id"
+        },
+        "schema": "http://schema.org#",
+        "PropertyValue": "schema:PropertyValue",
+        "value": "schema:value"
+      }
+    ],
+    "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948/activity",
+    "type": "Create",
+    "actor": "http://localhost:3000/users/ronan2",
+    "published": "2018-10-22T13:34:18Z",
+    "to": [
+      "https://www.w3.org/ns/activitystreams#Public"
+    ],
+    "cc": [
+      "http://localhost:3000/users/ronan2/followers",
+      "http://localhost:9000/accounts/ronan"
+    ],
+    "object": {
+      "id": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
+      "type": "Note",
+      "summary": null,
+      "inReplyTo": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
+      "published": "2018-10-22T13:34:18Z",
+      "url": "http://localhost:3000/@ronan2/100939547203370948",
+      "attributedTo": "http://localhost:3000/users/ronan2",
+      "to": [
+        "https://www.w3.org/ns/activitystreams#Public"
+      ],
+      "cc": [
+        "http://localhost:3000/users/ronan2/followers",
+        "http://localhost:9000/accounts/ronan"
+      ],
+      "sensitive": false,
+      "atomUri": "http://localhost:3000/users/ronan2/statuses/100939547203370948",
+      "inReplyToAtomUri": "http://localhost:9000/videos/watch/90e6f8ed-b369-423c-b0c8-f44e5350c752",
+      "conversation": "tag:localhost:3000,2018-10-19:objectId=72:objectType=Conversation",
+      "content": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>",
+      "contentMap": {
+        "en": "<p><span class=\"h-card\"><a href=\"http://localhost:9000/accounts/ronan\" class=\"u-url mention\">@<span>ronan</span></a></span> zergzerg</p>"
+      },
+      "attachment": [],
+      "tag": [
+        {
+          "type": "Mention",
+          "href": "http://localhost:9000/accounts/ronan",
+          "name": "@ronan@localhost:9000"
+        }
+      ]
+    },
+    "signature": {
+      "type": "RsaSignature2017",
+      "creator": "http://localhost:3000/users/ronan2#main-key",
+      "created": "2018-10-22T13:34:19Z",
+      "signatureValue": "x+xL4l8ERziYVhwEafHJyBQOInvNZ0gV4ccYd9AtFYeGJagc8fY6jjjhbDRCD7yMhgTjBX69z20MXnDuwpmM6wej3dt1wLKdIyXVViO84nAlqFz7KmNxtk5lDnAVX/vttscT5YUFvw4dbPT2mQiEd1lKbaLftRiIPEomZpQ37+fUkQdcPrnhruPAISO/Sof1n1LFW4mYIffozteQSZBH6HaCVp+MRMIhdMi5e8w7PD48/cZz8D/EU8Vqi91FM76/3tMqg6nLqQ+8bq74Jvt2kzwZlIufe+I55QMpZOmF6hGIJEt+R0JXdjQbtgcELONmNj2dr8sAlzu7zKlAGuJ24Q=="
+    }
+  }
+}
diff --git a/server/tests/api/activitypub/json/mastodon/public-key.json b/server/tests/api/activitypub/json/mastodon/public-key.json
new file mode 100644 (file)
index 0000000..b7b9b83
--- /dev/null
@@ -0,0 +1,3 @@
+{
+  "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0YyuthHtWWgDe0Fdgdp2\ndC5dTJsRqW6pFw5omIYYYjoES/WRewhVxEA54BhmxD3L1zChfx131N1TS8jVowhW\nm999jpUffKCCvLgYKIXETJDHiDeMONVx8wp7v9fS1HiFXo/E5und39gUMs14CMFZ\n6PE5jRV3r4XIKQJHQl7/X5n5FOb2934K+1TKUeBkbft/AushlKatYQakt3qHxpwx\nFvE+JjGo7QTnzdjaOx/e5QvojdGi2Kx4+jl87j2WVcSo5lOBz04OAVJtChtn82vS\nulPdDh3hZcDn+WK67yAhGP6AnzvOybZZS4zowlKiQ3kqjVVXKdl8gAsL4Y7MZ40R\nJQIDAQAB\n-----END PUBLIC KEY-----\n"
+}
diff --git a/server/tests/api/activitypub/json/peertube/announce-without-context.json b/server/tests/api/activitypub/json/peertube/announce-without-context.json
new file mode 100644 (file)
index 0000000..5f2af0c
--- /dev/null
@@ -0,0 +1,13 @@
+{
+  "type": "Announce",
+  "id": "http://localhost:9002/videos/watch/997111d4-e8d8-4f45-99d3-857905785d05/announces/1",
+  "actor": "http://localhost:9002/accounts/peertube",
+  "object": "http://localhost:9002/videos/watch/997111d4-e8d8-4f45-99d3-857905785d05",
+  "to": [
+    "https://www.w3.org/ns/activitystreams#Public",
+    "http://localhost:9002/accounts/peertube/followers",
+    "http://localhost:9002/video-channels/root_channel/followers",
+    "http://localhost:9002/accounts/root/followers"
+  ],
+  "cc": []
+}
diff --git a/server/tests/api/activitypub/json/peertube/invalid-keys.json b/server/tests/api/activitypub/json/peertube/invalid-keys.json
new file mode 100644 (file)
index 0000000..0544e96
--- /dev/null
@@ -0,0 +1,6 @@
+{
+  "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqjQGdH6D3naKmSbbr/Df\nEh1H42F3WlHYXuxKLkm5Bemjdde+GwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYO\nwAyc3Zoy7afPNa4bZXqhJ1Im41rMGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55s\nIkczDkseJuadTvG+A1e4uNY2lnRmVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/F\npP5S75TS5l1DfJQIq2lp8RwrH6FvGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM\n7mS7eP8zF8lKXYUu8cjIscKm+XqGmyRoPyw2Pp53tew29idRUocVQHGBnlNbpKdd\naQIDAQAB\n-----END PUBLIC KEY-----\n",
+  "privateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAqjQGdH6D3naKmSbbr/DfEh1H42F3WlHYXuxKLkm5Bemjdde+\nGwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYOwAyc3Zoy7afPNa4bZXqhJ1Im41rM\nGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55sIkczDkseJuadTvG+A1e4uNY2lnRm\nVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/FpP5S75TS5l1DfJQIq2lp8RwrH6Fv\nGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM7mS7eP8zF8lKXYUu8cjIscKm+XqG\nmyRoPyw3Pp53tew29idRUocVQHGBnlNbpKddaQIDAQABAoIBAQCnBZawCtbtH/ay\ng+dhqEW/SOyavbKZ92cU/1tsQPxISRYXNjdf2VfK7HmVqC2S7NqBanz+AVZPHmda\n7OfamkSvQbFN5VvEy8ATNV+9HbG3HG78/MT9hZcGigmyJkcZuy4wILgoXCxfpxlD\netla60PB/4yioiRcmEIWjjOgpByphDJ7RuuuptyEvgjUjpPtvHK47O/loaD2HFJk\nbIYbRirbjUjITRjQxGVIvanqiwPG9pB26YDLxDOoXEumcnzRcEFWNdvoleaLgquS\nn/zVsXWEq4+1i7t44DDstWUt/2Bw5ksIkSdayQ6oy3vzre3YFHwvbVZ7qtQQgpru\nx+NIolZhAoGBAN1RgNj8zy9Py3SJdsoXtnuCItfD7eo7LWXUa06cM/NS695Q+/to\naa5i3cJnRlv+b+b3VvnhkhIBLfFQW+hWwPnnxJEehcm09ddN9zbWrZ4Yv9yYu+8d\nTLGyWL8kPFF1dz+29DcrSv3tXEOwxByX/O4U/X/i3wl2WhkybxVFnCuvAoGBAMTf\n91BgLzvcYKOxH+vRPOJY7g2HKGFe35R91M4E+9Eq1rq4LUQHBb3fhRh4+scNu0yb\nNfN1Zdx2nbgCXdTKomF1Ahxp58/A2iU65vVzL6hYfWXEGSmoBqsGCIpIxQ9jgB9k\nCl7t/Ban8Z/ORHTjI9fpHlSZyCWJ3ajepiM2a1ZnAoGAPpDO6wi1DXvyWVSPF1yS\nwuGsNfD2rjPihpoBZ+yypwP3GBcu1QjUb28Vn+KQOmt4eQPNO8DwCVT6BvEfulPk\nJAHISPom+jnFEgPBcmhIFpyKiLNI1bUjvExd2FNHFgQuHP38ligQAC782Un8dtTk\ntO2MKH4bbVJe8CaYzpuqJZMCgYABZyMpBHZxs8FQiUuT75rCdiXEHOlxwC5RrY/d\no/VzaR28mOFhsbcdwkD9iqcm0fc6tYRt5rFCH+pBzGqEwKjljuLj9vE67sHfMAtD\nRn3Zcj/6gKo5PMRHZbSb36bf1DKuhpT4VjPMqYe0PtEIEDJKMJQRwELH2bKlqGiA\nqbucEwKBgQCkS85JnpHEV/tSylsEEn2W3CQCx58zl7iZNV7h/tWMR4AyrcI0HqP6\nllJ7V/Cfw66MgelPnosKgagwLVI6gsqDtjnzYo3XuMRVlYIySJ/jV3eiUNkV2Ky2\nfp/gA9sVgp38QSr+xB9E0LNStcbqDzoCCcDRws/SK7PbkQH9KV47tQ==\n-----END RSA PRIVATE KEY-----"
+}
+
+
diff --git a/server/tests/api/activitypub/json/peertube/keys.json b/server/tests/api/activitypub/json/peertube/keys.json
new file mode 100644 (file)
index 0000000..1a77008
--- /dev/null
@@ -0,0 +1,4 @@
+{
+  "publicKey": "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAqjQGdH6D3naKmSbbr/Df\nEh1H42F3WlHYXuxKLkm5Bemjdde+GwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYO\nwAyc3Zoy7afPNa4bZXqhJ1Im41rMGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55s\nIkczDkseJuadTvG+A1e4uNY2lnRmVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/F\npP5S75TS5l1DfJQIq2lp8RwrH6FvGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM\n7mS7eP8zF8lKXYUu8cjIscKm+XqGmyRoPyw3Pp53tew29idRUocVQHGBnlNbpKdd\naQIDAQAB\n-----END PUBLIC KEY-----\n",
+  "privateKey": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAqjQGdH6D3naKmSbbr/DfEh1H42F3WlHYXuxKLkm5Bemjdde+\nGwHYdz5m3fcIWw3HTzfA+y9Of8epGdfSrtYOwAyc3Zoy7afPNa4bZXqhJ1Im41rM\nGieiCuUn4uTPPucIjC0gCkVwvuQr3Elbk55sIkczDkseJuadTvG+A1e4uNY2lnRm\nVhf4g5B90u6CLe2KdbPpifRoKlw9zaUBj4/FpP5S75TS5l1DfJQIq2lp8RwrH6Fv\nGKLnWlbGeNYX96DDvlA5Sxoxz6a+bTV9OopM7mS7eP8zF8lKXYUu8cjIscKm+XqG\nmyRoPyw3Pp53tew29idRUocVQHGBnlNbpKddaQIDAQABAoIBAQCnBZawCtbtH/ay\ng+dhqEW/SOyavbKZ92cU/1tsQPxISRYXNjdf2VfK7HmVqC2S7NqBanz+AVZPHmda\n7OfamkSvQbFN5VvEy8ATNV+9HbG3HG78/MT9hZcGigmyJkcZuy4wILgoXCxfpxlD\netla60PB/4yioiRcmEIWjjOgpByphDJ7RuuuptyEvgjUjpPtvHK47O/loaD2HFJk\nbIYbRirbjUjITRjQxGVIvanqiwPG9pB26YDLxDOoXEumcnzRcEFWNdvoleaLgquS\nn/zVsXWEq4+1i7t44DDstWUt/2Bw5ksIkSdayQ6oy3vzre3YFHwvbVZ7qtQQgpru\nx+NIolZhAoGBAN1RgNj8zy9Py3SJdsoXtnuCItfD7eo7LWXUa06cM/NS695Q+/to\naa5i3cJnRlv+b+b3VvnhkhIBLfFQW+hWwPnnxJEehcm09ddN9zbWrZ4Yv9yYu+8d\nTLGyWL8kPFF1dz+29DcrSv3tXEOwxByX/O4U/X/i3wl2WhkybxVFnCuvAoGBAMTf\n91BgLzvcYKOxH+vRPOJY7g2HKGFe35R91M4E+9Eq1rq4LUQHBb3fhRh4+scNu0yb\nNfN1Zdx2nbgCXdTKomF1Ahxp58/A2iU65vVzL6hYfWXEGSmoBqsGCIpIxQ9jgB9k\nCl7t/Ban8Z/ORHTjI9fpHlSZyCWJ3ajepiM2a1ZnAoGAPpDO6wi1DXvyWVSPF1yS\nwuGsNfD2rjPihpoBZ+yypwP3GBcu1QjUb28Vn+KQOmt4eQPNO8DwCVT6BvEfulPk\nJAHISPom+jnFEgPBcmhIFpyKiLNI1bUjvExd2FNHFgQuHP38ligQAC782Un8dtTk\ntO2MKH4bbVJe8CaYzpuqJZMCgYABZyMpBHZxs8FQiUuT75rCdiXEHOlxwC5RrY/d\no/VzaR28mOFhsbcdwkD9iqcm0fc6tYRt5rFCH+pBzGqEwKjljuLj9vE67sHfMAtD\nRn3Zcj/6gKo5PMRHZbSb36bf1DKuhpT4VjPMqYe0PtEIEDJKMJQRwELH2bKlqGiA\nqbucEwKBgQCkS85JnpHEV/tSylsEEn2W3CQCx58zl7iZNV7h/tWMR4AyrcI0HqP6\nllJ7V/Cfw66MgelPnosKgagwLVI6gsqDtjnzYo3XuMRVlYIySJ/jV3eiUNkV2Ky2\nfp/gA9sVgp38QSr+xB9E0LNStcbqDzoCCcDRws/SK7PbkQH9KV47tQ==\n-----END RSA PRIVATE KEY-----"
+}
diff --git a/server/tests/api/activitypub/security.ts b/server/tests/api/activitypub/security.ts
new file mode 100644 (file)
index 0000000..c5428ab
--- /dev/null
@@ -0,0 +1,180 @@
+/* tslint:disable:no-unused-expression */
+
+import 'mocha'
+
+import { flushAndRunMultipleServers, flushTests, killallServers, makeAPRequest, makeFollowRequest, ServerInfo } from '../../utils'
+import { HTTP_SIGNATURE } from '../../../initializers'
+import { buildDigest, buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
+import * as chai from 'chai'
+import { setActorField } from '../../utils/miscs/sql'
+import { activityPubContextify, buildSignedActivity } from '../../../helpers/activitypub'
+
+const expect = chai.expect
+
+function setKeysOfServer2 (serverNumber: number, publicKey: string, privateKey: string) {
+  return Promise.all([
+    setActorField(serverNumber, 'http://localhost:9002/accounts/peertube', 'publicKey', publicKey),
+    setActorField(serverNumber, 'http://localhost:9002/accounts/peertube', 'privateKey', privateKey)
+  ])
+}
+
+function setKeysOfServer3 (serverNumber: number, publicKey: string, privateKey: string) {
+  return Promise.all([
+    setActorField(serverNumber, 'http://localhost:9003/accounts/peertube', 'publicKey', publicKey),
+    setActorField(serverNumber, 'http://localhost:9003/accounts/peertube', 'privateKey', privateKey)
+  ])
+}
+
+describe('Test ActivityPub security', function () {
+  let servers: ServerInfo[]
+  let url: string
+
+  const keys = require('./json/peertube/keys.json')
+  const invalidKeys = require('./json/peertube/invalid-keys.json')
+  const baseHttpSignature = {
+    algorithm: HTTP_SIGNATURE.ALGORITHM,
+    authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
+    keyId: 'acct:peertube@localhost:9002',
+    key: keys.privateKey,
+    headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
+  }
+
+  // ---------------------------------------------------------------
+
+  before(async function () {
+    this.timeout(60000)
+
+    servers = await flushAndRunMultipleServers(3)
+
+    url = servers[0].url + '/inbox'
+
+    await setKeysOfServer2(1, keys.publicKey, keys.privateKey)
+
+    const to = { url: 'http://localhost:9001/accounts/peertube' }
+    const by = { url: 'http://localhost:9002/accounts/peertube', privateKey: keys.privateKey }
+    await makeFollowRequest(to, by)
+  })
+
+  describe('When checking HTTP signature', function () {
+
+    it('Should fail with an invalid digest', async function () {
+      const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
+      const headers = {
+        Digest: buildDigest({ hello: 'coucou' })
+      }
+
+      const { response } = await makeAPRequest(url, body, baseHttpSignature, headers)
+
+      expect(response.statusCode).to.equal(403)
+    })
+
+    it('Should fail with an invalid date', async function () {
+      const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
+      const headers = buildGlobalHeaders(body)
+      headers['date'] = 'Wed, 21 Oct 2015 07:28:00 GMT'
+
+      const { response } = await makeAPRequest(url, body, baseHttpSignature, headers)
+
+      expect(response.statusCode).to.equal(403)
+    })
+
+    it('Should fail with bad keys', async function () {
+      await setKeysOfServer2(1, invalidKeys.publicKey, invalidKeys.privateKey)
+      await setKeysOfServer2(2, invalidKeys.publicKey, invalidKeys.privateKey)
+
+      const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
+      const headers = buildGlobalHeaders(body)
+
+      const { response } = await makeAPRequest(url, body, baseHttpSignature, headers)
+
+      expect(response.statusCode).to.equal(403)
+    })
+
+    it('Should succeed with a valid HTTP signature', async function () {
+      await setKeysOfServer2(1, keys.publicKey, keys.privateKey)
+      await setKeysOfServer2(2, keys.publicKey, keys.privateKey)
+
+      const body = activityPubContextify(require('./json/peertube/announce-without-context.json'))
+      const headers = buildGlobalHeaders(body)
+
+      const { response } = await makeAPRequest(url, body, baseHttpSignature, headers)
+
+      expect(response.statusCode).to.equal(204)
+    })
+  })
+
+  describe('When checking Linked Data Signature', function () {
+    before(async () => {
+      await setKeysOfServer3(3, keys.publicKey, keys.privateKey)
+
+      const to = { url: 'http://localhost:9001/accounts/peertube' }
+      const by = { url: 'http://localhost:9003/accounts/peertube', privateKey: keys.privateKey }
+      await makeFollowRequest(to, by)
+    })
+
+    it('Should fail with bad keys', async function () {
+      this.timeout(10000)
+
+      await setKeysOfServer3(1, invalidKeys.publicKey, invalidKeys.privateKey)
+      await setKeysOfServer3(3, invalidKeys.publicKey, invalidKeys.privateKey)
+
+      const body = require('./json/peertube/announce-without-context.json')
+      body.actor = 'http://localhost:9003/accounts/peertube'
+
+      const signer: any = { privateKey: invalidKeys.privateKey, url: 'http://localhost:9003/accounts/peertube' }
+      const signedBody = await buildSignedActivity(signer, body)
+
+      const headers = buildGlobalHeaders(signedBody)
+
+      const { response } = await makeAPRequest(url, signedBody, baseHttpSignature, headers)
+
+      expect(response.statusCode).to.equal(403)
+    })
+
+    it('Should fail with an altered body', async function () {
+      this.timeout(10000)
+
+      await setKeysOfServer3(1, keys.publicKey, keys.privateKey)
+      await setKeysOfServer3(3, keys.publicKey, keys.privateKey)
+
+      const body = require('./json/peertube/announce-without-context.json')
+      body.actor = 'http://localhost:9003/accounts/peertube'
+
+      const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:9003/accounts/peertube' }
+      const signedBody = await buildSignedActivity(signer, body)
+
+      signedBody.actor = 'http://localhost:9003/account/peertube'
+
+      const headers = buildGlobalHeaders(signedBody)
+
+      const { response } = await makeAPRequest(url, signedBody, baseHttpSignature, headers)
+
+      expect(response.statusCode).to.equal(403)
+    })
+
+    it('Should succeed with a valid signature', async function () {
+      this.timeout(10000)
+
+      const body = require('./json/peertube/announce-without-context.json')
+      body.actor = 'http://localhost:9003/accounts/peertube'
+
+      const signer: any = { privateKey: keys.privateKey, url: 'http://localhost:9003/accounts/peertube' }
+      const signedBody = await buildSignedActivity(signer, body)
+
+      const headers = buildGlobalHeaders(signedBody)
+
+      const { response } = await makeAPRequest(url, signedBody, baseHttpSignature, headers)
+
+      expect(response.statusCode).to.equal(204)
+    })
+  })
+
+  after(async function () {
+    killallServers(servers)
+
+    // Keep the logs if the test failed
+    if (this['ok']) {
+      await flushTests()
+    }
+  })
+})
index 8e69b95a6ca9ab2ed443d67e34fa43e56bf51c86..7d8be2b3d180c6f2c882ffee9f2c6568c475973d 100644 (file)
@@ -1 +1,2 @@
 import './redundancy'
+import './activitypub'
index e659fd3df9bb4998241257771d1e4f52d108f4db..ed16d65dd298c76a8b805c5a2492fc8c41c42c7d 100644 (file)
@@ -1,6 +1,5 @@
 // Order of the tests we want to execute
 import './client'
-import './activitypub'
 import './feeds/'
 import './cli/'
 import './api/'
index 897389824d165d84cedf35fe11e3b06d7f477f93..905d938238ac6588aeb185a9f1fa056747ffb761 100644 (file)
@@ -4,8 +4,10 @@ export * from './server/clients'
 export * from './server/config'
 export * from './users/login'
 export * from './miscs/miscs'
+export * from './miscs/stubs'
 export * from './server/follows'
 export * from './requests/requests'
+export * from './requests/activitypub'
 export * from './server/servers'
 export * from './videos/services'
 export * from './users/users'
diff --git a/server/tests/utils/miscs/sql.ts b/server/tests/utils/miscs/sql.ts
new file mode 100644 (file)
index 0000000..204ff51
--- /dev/null
@@ -0,0 +1,29 @@
+import * as Sequelize from 'sequelize'
+
+function getSequelize (serverNumber: number) {
+  const dbname = 'peertube_test' + serverNumber
+  const username = 'peertube'
+  const password = 'peertube'
+  const host = 'localhost'
+  const port = 5432
+
+  return new Sequelize(dbname, username, password, {
+    dialect: 'postgres',
+    host,
+    port,
+    operatorsAliases: false,
+    logging: false
+  })
+}
+
+function setActorField (serverNumber: number, to: string, field: string, value: string) {
+  const seq = getSequelize(serverNumber)
+
+  const options = { type: Sequelize.QueryTypes.UPDATE }
+
+  return seq.query(`UPDATE actor SET "${field}" = '${value}' WHERE url = '${to}'`, options)
+}
+
+export {
+  setActorField
+}
diff --git a/server/tests/utils/miscs/stubs.ts b/server/tests/utils/miscs/stubs.ts
new file mode 100644 (file)
index 0000000..d1eb0e3
--- /dev/null
@@ -0,0 +1,14 @@
+function buildRequestStub (): any {
+  return { }
+}
+
+function buildResponseStub (): any {
+  return {
+    locals: {}
+  }
+}
+
+export {
+  buildResponseStub,
+  buildRequestStub
+}
diff --git a/server/tests/utils/requests/activitypub.ts b/server/tests/utils/requests/activitypub.ts
new file mode 100644 (file)
index 0000000..e3e08ce
--- /dev/null
@@ -0,0 +1,43 @@
+import { doRequest } from '../../../helpers/requests'
+import { HTTP_SIGNATURE } from '../../../initializers'
+import { buildGlobalHeaders } from '../../../lib/job-queue/handlers/utils/activitypub-http-utils'
+import { activityPubContextify } from '../../../helpers/activitypub'
+
+function makeAPRequest (url: string, body: any, httpSignature: any, headers: any) {
+  const options = {
+    method: 'POST',
+    uri: url,
+    json: body,
+    httpSignature,
+    headers
+  }
+
+  return doRequest(options)
+}
+
+async function makeFollowRequest (to: { url: string }, by: { url: string, privateKey }) {
+  const follow = {
+    type: 'Follow',
+    id: by.url + '/toto',
+    actor: by.url,
+    object: to.url
+  }
+
+  const body = activityPubContextify(follow)
+
+  const httpSignature = {
+    algorithm: HTTP_SIGNATURE.ALGORITHM,
+    authorizationHeaderName: HTTP_SIGNATURE.HEADER_NAME,
+    keyId: by.url,
+    key: by.privateKey,
+    headers: HTTP_SIGNATURE.HEADERS_TO_SIGN
+  }
+  const headers = buildGlobalHeaders(body)
+
+  return makeAPRequest(to.url, body, httpSignature, headers)
+}
+
+export {
+  makeAPRequest,
+  makeFollowRequest
+}
index 2478a0664efe090c0a4f20703b0d267cb7827975..6aeb87e3f7c5d1fdecd1ddf42ddb789b066551f5 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -5054,7 +5054,7 @@ jsonify@~0.0.0:
 
 jsonld@^0.5.12:
   version "0.5.21"
-  resolved "http://registry.npmjs.org/jsonld/-/jsonld-0.5.21.tgz#4d5b78d717eb92bcd1ac9d88e34efad95370c0bf"
+  resolved "https://registry.yarnpkg.com/jsonld/-/jsonld-0.5.21.tgz#4d5b78d717eb92bcd1ac9d88e34efad95370c0bf"
   integrity sha512-1dQhaw1Eb3p7Cz5ECE2DNPwLvTmK+f6D45hACBdonJaFKP1bN9zlKLZWbPZQeZtduAc/LNv10J4ML0IiTBVahw==
   dependencies:
     rdf-canonize "^0.2.1"