summaryrefslogtreecommitdiff
path: root/src/remote/activitypub/misc/ld-signature.ts
diff options
context:
space:
mode:
Diffstat (limited to 'src/remote/activitypub/misc/ld-signature.ts')
-rw-r--r--src/remote/activitypub/misc/ld-signature.ts133
1 files changed, 133 insertions, 0 deletions
diff --git a/src/remote/activitypub/misc/ld-signature.ts b/src/remote/activitypub/misc/ld-signature.ts
new file mode 100644
index 0000000000..d61b430f7a
--- /dev/null
+++ b/src/remote/activitypub/misc/ld-signature.ts
@@ -0,0 +1,133 @@
+import * as crypto from 'crypto';
+import * as jsonld from 'jsonld';
+import { CONTEXTS } from './contexts';
+import fetch from 'node-fetch';
+import { httpAgent, httpsAgent } from '../../../misc/fetch';
+
+// RsaSignature2017 based from https://github.com/transmute-industries/RsaSignature2017
+
+export class LdSignature {
+ public debug = false;
+ public preLoad = true;
+ public loderTimeout = 10 * 1000;
+
+ constructor() {
+ }
+
+ public async signRsaSignature2017(data: any, privateKey: string, creator: string, domain?: string, created?: Date): Promise<any> {
+ const options = {
+ type: 'RsaSignature2017',
+ creator,
+ domain,
+ nonce: crypto.randomBytes(16).toString('hex'),
+ created: (created || new Date()).toISOString()
+ } as {
+ type: string;
+ creator: string;
+ domain: string;
+ nonce: string;
+ created: string;
+ };
+
+ if (!domain) {
+ delete options.domain;
+ }
+
+ const toBeSigned = await this.createVerifyData(data, options);
+
+ const signer = crypto.createSign('sha256');
+ signer.update(toBeSigned);
+ signer.end();
+
+ const signature = signer.sign(privateKey);
+
+ return {
+ ...data,
+ signature: {
+ ...options,
+ signatureValue: signature.toString('base64')
+ }
+ };
+ }
+
+ public async verifyRsaSignature2017(data: any, publicKey: string): Promise<boolean> {
+ const toBeSigned = await this.createVerifyData(data, data.signature);
+ const verifier = crypto.createVerify('sha256');
+ verifier.update(toBeSigned);
+ return verifier.verify(publicKey, data.signature.signatureValue, 'base64');
+ }
+
+ public async createVerifyData(data: any, options: any) {
+ const transformedOptions = {
+ ...options,
+ '@context': 'https://w3id.org/identity/v1'
+ };
+ delete transformedOptions['type'];
+ delete transformedOptions['id'];
+ delete transformedOptions['signatureValue'];
+ const canonizedOptions = await this.normalize(transformedOptions);
+ const optionsHash = this.sha256(canonizedOptions);
+ const transformedData = { ...data };
+ delete transformedData['signature'];
+ const cannonidedData = await this.normalize(transformedData);
+ const documentHash = this.sha256(cannonidedData);
+ const verifyData = `${optionsHash}${documentHash}`;
+ return verifyData;
+ }
+
+ public async normalize(data: any) {
+ const customLoader = this.getLoader();
+ return await jsonld.normalize(data, {
+ documentLoader: customLoader
+ });
+ }
+
+ private getLoader() {
+ return async (url: string): Promise<any> => {
+ if (!url.match('^https?\:\/\/')) throw `Invalid URL ${url}`;
+
+ if (this.preLoad) {
+ if (url in CONTEXTS) {
+ if (this.debug) console.debug(`HIT: ${url}`);
+ return {
+ contextUrl: null,
+ document: CONTEXTS[url],
+ documentUrl: url
+ };
+ }
+ }
+
+ if (this.debug) console.debug(`MISS: ${url}`);
+ const document = await this.fetchDocument(url);
+ return {
+ contextUrl: null,
+ document: document,
+ documentUrl: url
+ };
+ };
+ }
+
+ private async fetchDocument(url: string) {
+ const json = await fetch(url, {
+ headers: {
+ Accept: 'application/ld+json, application/json',
+ },
+ timeout: this.loderTimeout,
+ agent: u => u.protocol == 'http:' ? httpAgent : httpsAgent,
+ }).then(res => {
+ if (!res.ok) {
+ throw `${res.status} ${res.statusText}`;
+ } else {
+ return res.json();
+ }
+ });
+
+ return json;
+ }
+
+ public sha256(data: string): string {
+ const hash = crypto.createHash('sha256');
+ hash.update(data);
+ return hash.digest('hex');
+ }
+}