import * as crypto from "node:crypto"; import jsonld from "jsonld"; import { CONTEXTS, WellKnownContext } 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 { debug = false; preLoad = true; loaderTimeout = 10 * 1000; async signRsaSignature2017(data, privateKey, creator, domain, created) { const options = { type: "RsaSignature2017", creator, domain, nonce: crypto.randomBytes(16).toString("hex"), created: (created || new Date()).toISOString() }; 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") } }; } async verifyRsaSignature2017(data, signature, publicKey) { const toBeSigned = await this.createVerifyData(data, signature); const verifier = crypto.createVerify("sha256"); verifier.update(toBeSigned); return verifier.verify(publicKey, signature.signatureValue, "base64"); } async createVerifyData(data, options) { 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 }; const cannonidedData = await this.normalize(transformedData); if (this.debug) console.debug(`cannonidedData: ${cannonidedData}`); const documentHash = this.sha256(cannonidedData); const verifyData = `${optionsHash}${documentHash}`; return verifyData; } async normalize(data) { const customLoader = this.getLoader(); return await jsonld.normalize(data, { documentLoader: customLoader }); } async compactToWellKnown(data) { const options = { documentLoader: this.getLoader() }; const context = WellKnownContext; return await jsonld.compact(data, context, options); } getLoader() { return async (url)=>{ 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 }; }; } async fetchDocument(url) { const json = await fetch(url, { headers: { Accept: "application/ld+json, application/json" }, size: 1024 * 1024, // TODO //timeout: this.loaderTimeout, 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; } sha256(data) { const hash = crypto.createHash("sha256"); hash.update(data); return hash.digest("hex"); } containsForbiddenDirectives(doc) { if (typeof doc === "object" && doc !== null) { if (Array.isArray(doc)) { for (const item of doc){ if (this.containsForbiddenDirectives(item)) { return true; } } } else { for (const [key, value] of Object.entries(doc)){ if ([ "@included", "@graph", "@reverse" ].includes(key)) { return true; } if (typeof value === "object" && value !== null) { if (this.containsForbiddenDirectives(value)) { return true; } } } } } return false; } }