Files
FrozenFriendsYume/packages/backend/built/remote/activitypub/misc/ld-signature.js
T
2026-07-26 18:25:37 +09:00

146 lines
5.0 KiB
JavaScript

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;
}
}