Skip to content

Commit

Permalink
Move proxy service to signature package, split keypair service
Browse files Browse the repository at this point in the history
  • Loading branch information
srosset81 committed Dec 16, 2022
1 parent 285590b commit ab62eeb
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 190 deletions.
1 change: 0 additions & 1 deletion src/middleware/packages/activitypub/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ module.exports = {
InboxService: require('./services/inbox'),
ObjectService: require('./services/object'),
OutboxService: require('./services/outbox'),
ProxyService: require('./services/proxy'),
RegistryService: require('./services/registry'),
RelayService: require('./services/relay'),
// Other services
Expand Down
2 changes: 1 addition & 1 deletion src/middleware/packages/activitypub/service.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ const ActivityPubService = {
}
},
dependencies: ['api'],
async created() {
created() {
let { baseUri, jsonContext, podProvider, selectActorData, dispatch, reply, like, follow, relay } = this.settings;

this.broker.createService(CollectionService, {
Expand Down
30 changes: 4 additions & 26 deletions src/middleware/packages/activitypub/services/actor.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const fetch = require('node-fetch');
const { namedNode, blankNode, literal, triple, variable } = require('@rdfjs/data-model');
const { namedNode, literal, triple, variable } = require('@rdfjs/data-model');
const { MIME_TYPES } = require('@semapps/mime-types');
const { ACTOR_TYPES, AS_PREFIX } = require('../constants');
const { delay, defaultToArray, getSlugFromUri } = require('../utils');
Expand Down Expand Up @@ -134,28 +134,6 @@ const ActorService = {
dataset
});
},
async generateKeyPair(ctx) {
const { actorUri } = ctx.params;
const actor = await this.actions.get({ actorUri, webId: 'system' }, { parentCtx: ctx });

if (!actor.publicKey) {
const publicKey = await ctx.call('signature.generateActorKeyPair', { actorUri });

await ctx.call('ldp.resource.patch', {
resourceUri: actorUri,
triplesToAdd: [
triple(namedNode(actorUri), namedNode('https://w3id.org/security#publicKey'), blankNode('b_0')),
triple(blankNode('b_0'), namedNode('https://w3id.org/security#owner'), namedNode(actorUri)),
triple(blankNode('b_0'), namedNode('https://w3id.org/security#publicKeyPem'), literal(publicKey)),
],
webId: 'system'
});
}
},
async deleteKeyPair(ctx) {
const { actorUri } = ctx.params;
await ctx.call('signature.deleteActorKeyPair', { actorUri });
},
async awaitCreateComplete(ctx) {
let { actorUri, additionalKeys = [] } = ctx.params;
const keysToCheck = ['publicKey', 'outbox', 'inbox', 'followers', 'following', ...additionalKeys];
Expand Down Expand Up @@ -214,19 +192,19 @@ const ActorService = {
const { resourceUri, newData } = ctx.params;
if (this.isActor(newData)) {
await this.actions.appendActorData({ actorUri: resourceUri }, { parentCtx: ctx });
await this.actions.generateKeyPair({ actorUri: resourceUri }, { parentCtx: ctx });
await ctx.call('signature.generateKeyPair', { actorUri: resourceUri });
await ctx.call('signature.attachPublicKey', { actorUri: resourceUri });
}
},
async 'ldp.resource.deleted'(ctx) {
const { resourceUri, oldData } = ctx.params;
if (this.isActor(oldData)) {
await this.actions.deleteKeyPair({ actorUri: resourceUri }, { parentCtx: ctx });
await ctx.call('signature.deleteActorKeyPair', { actorUri: resourceUri });
}
},
async 'auth.registered'(ctx) {
const { webId } = ctx.params;
await this.actions.appendActorData({ actorUri: webId }, { parentCtx: ctx });
await this.actions.generateKeyPair({ actorUri: webId }, { parentCtx: ctx });
}
}
};
Expand Down
11 changes: 7 additions & 4 deletions src/middleware/packages/activitypub/services/object.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,13 @@ const ObjectService = {
// TODO only do this for distant objects
// If the object was not found in cache, try to query it distantly
if (actorUri && actorUri !== 'system' && actorUri !== 'anon') {
return await ctx.call('activitypub.proxy.query', {
resourceUri: objectUri,
actorUri
});
const services = await ctx.call('$node.services');
if (services.filter(s => s.name === 'signature.proxy')) {
return await ctx.call('signature.proxy.query', {
resourceUri: objectUri,
actorUri
});
}
// TODO put results in cache ?
} else {
const response = await fetch(objectUri, {
Expand Down
70 changes: 35 additions & 35 deletions src/middleware/packages/ldp/services/resource/methods.js
Original file line number Diff line number Diff line change
@@ -1,54 +1,54 @@
const fs = require('fs');
const rdfParser = require('rdf-parse').default;
const streamifyString = require('streamify-string');
const { variable } = require('@rdfjs/data-model');
const { MIME_TYPES } = require('@semapps/mime-types');
const { MoleculerError } = require('moleculer').Errors;
const fs = require('fs');

const { defaultToArray } = require('../../utils');

// TODO put each method in a different file (problems with "this" not working)
module.exports = {
async updateSingleMirroredResources() {
this.logger.info('updateSingleMirroredResources');
let singles = await this.broker.call('triplestore.query', {
query: `SELECT DISTINCT ?s WHERE { GRAPH <${this.settings.mirrorGraphName}> { ?s <http://semapps.org/ns/core#singleMirroredResource> ?o } }`
});
for (const single of singles) {
try {
const resourceUri = single.s.value;
if (!this.settings.podProvider) {
let singles = await this.broker.call('triplestore.query', {
query: `SELECT DISTINCT ?s WHERE { GRAPH <${this.settings.mirrorGraphName}> { ?s <http://semapps.org/ns/core#singleMirroredResource> ?o } }`
});
for (const single of singles) {
try {
const resourceUri = single.s.value;

const response = await fetch(resourceUri, { headers: { Accept: MIME_TYPES.JSON } });
if (response.status == 403 || response.status == 404 || response.status == 401) {
// remove the resource from cache and from its containers
await this.broker.call(
'ldp.resource.delete',
{ resourceUri, webId: 'system' },
{ meta: { forceMirror: true } }
);
// get the list of all the containers that contain this mirrored resource we are removing
const containers = await this.broker.call('triplestore.query', {
query: `SELECT ?container WHERE { ?container <http://www.w3.org/ns/ldp#contains> <${resourceUri}> }`
});
for (let containerUri of containers.map(c => c.container.value)) {
const response = await fetch(resourceUri, {headers: {Accept: MIME_TYPES.JSON}});
if (response.status === 403 || response.status === 404 || response.status === 401) {
// remove the resource from cache and from its containers
await this.broker.call(
'ldp.container.detach',
{ containerUri, resourceUri, webId: 'system' },
'ldp.resource.delete',
{ resourceUri, webId: 'system' },
{ meta: { forceMirror: true } }
);
}
} else {
// update the local cache
let resource = await response.json();
resource['http://semapps.org/ns/core#singleMirroredResource'] = new URL(resourceUri).origin;
// get the list of all the containers that contain this mirrored resource we are removing
const containers = await this.broker.call('triplestore.query', {
query: `SELECT ?container WHERE { ?container <http://www.w3.org/ns/ldp#contains> <${resourceUri}> }`
});
for (let containerUri of containers.map(c => c.container.value)) {
await this.broker.call(
'ldp.container.detach',
{ containerUri, resourceUri, webId: 'system' },
{ meta: { forceMirror: true } }
);
}
} else {
// update the local cache
let resource = await response.json();
resource['http://semapps.org/ns/core#singleMirroredResource'] = new URL(resourceUri).origin;

await this.broker.call('ldp.resource.put', { resource, contentType: MIME_TYPES.JSON });
await this.broker.call('ldp.resource.put', { resource, contentType: MIME_TYPES.JSON });
}
} catch (e) {
// connection errors are not counted as errors that indicate the resource is gone.
// those error just indicate that the remote server is not responding. can be temporary.
this.logger.warn('Failed to update single mirrored resource: ' + single.s.value);
console.error(e);
}
} catch (e) {
// connection errors are not counted as errors that indicate the resource is gone.
// those error just indicate that the remote server is not responding. can be temporary.
this.logger.warn('Failed to update single mirrored resource' + single.s.value);
console.error(e);
}
}
},
Expand Down
3 changes: 2 additions & 1 deletion src/middleware/packages/signature/index.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
module.exports = {
SignatureService: require('./service')
SignatureService: require('./service'),
ProxyService: require('./services/proxy')
};
118 changes: 11 additions & 107 deletions src/middleware/packages/signature/service.js
Original file line number Diff line number Diff line change
@@ -1,91 +1,27 @@
const fs = require('fs');
const fsPromises = require('fs').promises;
const path = require('path');
const { generateKeyPair, createSign, createHash } = require('crypto');
const { createSign, createHash } = require('crypto');
const { parseRequest, verifySignature } = require('http-signature');
const { createAuthzHeader, createSignatureString } = require('http-signature-header');
const { Errors: E } = require('moleculer-web');
const { MIME_TYPES } = require('@semapps/mime-types');
const { getSlugFromUri } = require('@semapps/ldp');
const KeypairService = require('./services/keypair');

const SignatureService = {
name: 'signature',
settings: {
actorsKeyPairsDir: null
},
started() {
this.remoteActorPublicKeyCache = {};
},
created() {
if (!this.settings.actorsKeyPairsDir) {
throw new Error('You must set the actorsKeyPairsDir setting in the signature service');
} else if (!fs.existsSync(this.settings.actorsKeyPairsDir)) {
throw new Error(`The actorsKeyPairsDir (${this.settings.actorsKeyPairsDir}) does not exist! Please create it.`);
}
},
actions: {
async getActorPublicKey(ctx) {
const { actorUri } = ctx.params;
const { publicKeyPath } = await this.getKeyPaths(ctx, actorUri);

try {
return await fs.promises.readFile(publicKeyPath, { encoding: 'utf8' });
} catch (e) {
return null;
}
},
async generateActorKeyPair(ctx) {
const { actorUri } = ctx.params;

const publicKey = await this.actions.getActorPublicKey({ actorUri }, { parentCtx: ctx });
if (publicKey) {
this.logger.info(`Key for ${actorUri} already exists, skipping...`);
return publicKey;
}

const { privateKeyPath, publicKeyPath } = await this.getKeyPaths(ctx, actorUri);

return new Promise((resolve, reject) => {
generateKeyPair(
'rsa',
{
modulusLength: 4096,
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
}
},
(err, publicKey, privateKey) => {
if (!err) {
fs.writeFile(privateKeyPath, privateKey, err => reject(err));
fs.writeFile(publicKeyPath, publicKey, err => reject(err));
resolve(publicKey);
} else {
reject(err);
}
}
);
});
},
async deleteActorKeyPair(ctx) {
const { actorUri } = ctx.params;
const { privateKeyPath, publicKeyPath } = await this.getKeyPaths(ctx, actorUri);
const { actorsKeyPairsDir } = this.settings;

try {
await fs.promises.unlink(privateKeyPath);
await fs.promises.unlink(publicKeyPath);
} catch (e) {
console.log(`Could not delete key pair for actor ${actorUri}`);
this.broker.createService(KeypairService, {
settings: {
actorsKeyPairsDir
}
},
});
},
actions: {
async generateSignatureHeaders(ctx) {
const { url, method, body, actorUri } = ctx.params;
const { privateKeyPath } = await this.getKeyPaths(ctx, actorUri);
const privateKey = await fsPromises.readFile(privateKeyPath);
const { privateKey } = await ctx.call('signature.keypair.get', { actorUri });

const headers = { Date: new Date().toUTCString() };
const includeHeaders = ['(request-target)', 'host', 'date'];
Expand Down Expand Up @@ -135,7 +71,7 @@ const SignatureService = {
if (!keyId) return false;
const [actorUri] = keyId.split('#');

const publicKey = await this.getRemoteActorPublicKey(actorUri);
const publicKey = await ctx.call('signature.keypair.getRemotePublicKey', { actorUri });
if (!publicKey) return false;

const isValid = verifySignature(parsedSignature, publicKey);
Expand Down Expand Up @@ -190,38 +126,6 @@ const SignatureService = {
.update(body)
.digest('base64')
);
},
// TODO use cache mechanisms
async getRemoteActorPublicKey(actorUri) {
if (this.remoteActorPublicKeyCache[actorUri]) {
return this.remoteActorPublicKeyCache[actorUri];
}

// TODO use activitypub.actor.get
const response = await fetch(actorUri, { headers: { Accept: 'application/json' } });
if (!response) return false;

const actor = await response.json();
if (!actor || !actor.publicKey) return false;

this.remoteActorPublicKeyCache[actorUri] = actor.publicKey.publicKeyPem;
return actor.publicKey.publicKeyPem;
},
async getKeyPaths(ctx, actorUri) {
const actorData = await ctx.call('ldp.resource.get', {
resourceUri: actorUri,
accept: MIME_TYPES.JSON,
webId: 'system'
});

if (actorData) {
const username = actorData.preferredUsername || getSlugFromUri(actorUri);
const privateKeyPath = path.join(this.settings.actorsKeyPairsDir, username + '.key');
const publicKeyPath = path.join(this.settings.actorsKeyPairsDir, username + '.key.pub');
return { privateKeyPath, publicKeyPath };
} else {
throw new Error('No valid actor found with URI ' + actorUri);
}
}
}
};
Expand Down
Loading

0 comments on commit ab62eeb

Please sign in to comment.