import * as crypto from "node:crypto"; import jsonld from "jsonld"; import { CONTEXTS } from "./contexts.js"; import fetch from "node-fetch"; import { httpAgent, httpsAgent } from "@/misc/fetch.js"; // 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 { 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) { options.domain = undefined; } 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 { 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", }; transformedOptions["type"] = undefined; transformedOptions["id"] = undefined; transformedOptions["signatureValue"] = undefined; const canonizedOptions = await this.normalize(transformedOptions); const optionsHash = this.sha256(canonizedOptions); const transformedData = { ...data }; transformedData["signature"] = undefined; const cannonidedData = await this.normalize(transformedData); if (this.debug) console.debug(`cannonidedData: ${cannonidedData}`); 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 => { if (!url.match("^https?://")) throw new Error(`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", }, // TODO //timeout: this.loderTimeout, agent: (u) => (u.protocol === "http:" ? httpAgent : httpsAgent), }).then((res) => { if (!res.ok) { throw new Error(`${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"); } }