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
// 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) {
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 -----------
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())
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()
jsig.use('jsonld', jsonld)
-export { jsig }
+export { jsig, jsonld }
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')
// 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
}
})
}
+// 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,
// ---------------------------------------------------------------------------
export {
+ isHTTPSignatureDigestValid,
parseHTTPSignature,
isHTTPSignatureVerified,
isJsonLDSignatureVerified,
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' ]
}
// ---------------------------------------------------------------------------
}
}
-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
export {
checkSignature,
- executeIfActivityPub
+ executeIfActivityPub,
+ checkHttpSignature
}
// ---------------------------------------------------------------------------
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
}
+++ /dev/null
-/* 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 ])
- })
-})
--- /dev/null
+/* 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 ])
+ })
+})
--- /dev/null
+/* 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
+ })
+
+ })
+
+})
--- /dev/null
+import './client'
+import './helpers'
+import './security'
--- /dev/null
+{
+ "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=="
+ }
+ }
+}
--- /dev/null
+{
+ "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=="
+ }
+ }
+}
--- /dev/null
+{
+ "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"
+}
--- /dev/null
+{
+ "@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=="
+ }
+}
--- /dev/null
+{
+ "@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=="
+ }
+}
--- /dev/null
+{
+ "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=="
+ }
+ }
+}
--- /dev/null
+{
+ "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"
+}
--- /dev/null
+{
+ "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": []
+}
--- /dev/null
+{
+ "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-----"
+}
+
+
--- /dev/null
+{
+ "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-----"
+}
--- /dev/null
+/* 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()
+ }
+ })
+})
import './redundancy'
+import './activitypub'
// Order of the tests we want to execute
import './client'
-import './activitypub'
import './feeds/'
import './cli/'
import './api/'
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'
--- /dev/null
+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
+}
--- /dev/null
+function buildRequestStub (): any {
+ return { }
+}
+
+function buildResponseStub (): any {
+ return {
+ locals: {}
+ }
+}
+
+export {
+ buildResponseStub,
+ buildRequestStub
+}
--- /dev/null
+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
+}
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"