From 55f52787305f82cdc8a2a0b9f82099e52d252c02 Mon Sep 17 00:00:00 2001 From: Deva Midhun Date: Sat, 8 Feb 2025 19:38:25 +0000 Subject: [PATCH] update --- .../utils/fetchActorInformation.ts | 21 ++ lib/activitypub/utils/parseSignature.ts | 7 + lib/activitypub/utils/sendSignedRequest.ts | 45 ++++ lib/activitypub/utils/verifySignature.js | 14 ++ lib/http-signature/index.js | 215 ++++++++++++++++++ 5 files changed, 302 insertions(+) create mode 100644 lib/activitypub/utils/fetchActorInformation.ts create mode 100644 lib/activitypub/utils/parseSignature.ts create mode 100644 lib/activitypub/utils/sendSignedRequest.ts create mode 100644 lib/activitypub/utils/verifySignature.js create mode 100644 lib/http-signature/index.js diff --git a/lib/activitypub/utils/fetchActorInformation.ts b/lib/activitypub/utils/fetchActorInformation.ts new file mode 100644 index 0000000..9206ab1 --- /dev/null +++ b/lib/activitypub/utils/fetchActorInformation.ts @@ -0,0 +1,21 @@ +import { AP } from "activitypub-core-types"; + +export async function fetchActorInformation(actorUrl: string): Promise { + try { + const response = await fetch( + actorUrl, + { + headers: { + "Content-type": 'application/activity+json', + "Accept": 'application/activity+json' + }, + signal: AbortSignal.timeout(5000) // kill after 5 seconds + } + ); + + return await response.json(); + } catch (error) { + console.log("Unable to fetch action information", actorUrl); + } + return null; +} diff --git a/lib/activitypub/utils/parseSignature.ts b/lib/activitypub/utils/parseSignature.ts new file mode 100644 index 0000000..11b1236 --- /dev/null +++ b/lib/activitypub/utils/parseSignature.ts @@ -0,0 +1,7 @@ +import { VercelRequest } from '@vercel/node'; +import parser from '../../http-signature/index.js'; + +export function parseSignature(request: VercelRequest) { + const { url, method, headers } = request; + return parser.parse({ url, method, headers }); +} diff --git a/lib/activitypub/utils/sendSignedRequest.ts b/lib/activitypub/utils/sendSignedRequest.ts new file mode 100644 index 0000000..53dee7f --- /dev/null +++ b/lib/activitypub/utils/sendSignedRequest.ts @@ -0,0 +1,45 @@ +import { AP } from 'activitypub-core-types'; +import { Sha256Signer } from '../../http-signature/index.js'; +import { createHash } from 'crypto'; + +export async function sendSignedRequest(endpoint: URL, object: AP.Activity): Promise { + const publicKeyId = "https://coderrrrr.site/coder#main-key"; + const privateKey = process.env.ACTIVITYPUB_PRIVATE_KEY; + + const signer = new Sha256Signer({ publicKeyId, privateKey, headerNames: ["host", "date", "digest"] }); + + const requestHeaders = { + host: endpoint.hostname, + date: new Date().toUTCString(), + digest: `SHA-256=${createHash('sha256').update(JSON.stringify(object)).digest('base64')}` + }; + + // Generate the signature header + const signature = signer.sign({ + url: endpoint, + method: "POST", + headers: requestHeaders + }); + + console.log("object", object); + console.log("endpoint", endpoint); + console.log("requestHeaders", requestHeaders); + console.log("signature", signature); + + const response = await fetch( + endpoint, + { + method: 'POST', + body: JSON.stringify(object), + headers: { + 'content-type': "application/activity+json", + accept: "application/activity+json", + ...requestHeaders, + signature: signature + }, + signal: AbortSignal.timeout(5000) // kill after 5 seconds + } + ); + + return response; +} diff --git a/lib/activitypub/utils/verifySignature.js b/lib/activitypub/utils/verifySignature.js new file mode 100644 index 0000000..69e525f --- /dev/null +++ b/lib/activitypub/utils/verifySignature.js @@ -0,0 +1,14 @@ +export function verifySignature(signature:any, publicKeyJson:any) { + let signatureValid; + + try { + // Verify the signature + signatureValid = signature.verify( + publicKeyJson.publicKeyPem + ); + } catch (error) { + console.log("Signature Verification error", error); + } + + return signatureValid; +} diff --git a/lib/http-signature/index.js b/lib/http-signature/index.js new file mode 100644 index 0000000..0181ac1 --- /dev/null +++ b/lib/http-signature/index.js @@ -0,0 +1,215 @@ +/** PK Fixed version. + * Activitypub HTTP Signatures + * Based on [HTTP Signatures draft 08](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-08) + * @module activitypub-http-signatures + */ + +import crypto from 'crypto'; + + +// token definitions from definitions in rfc7230 and rfc7235 +const token = String.raw`[!#$%&'\*+\-\.\^_\`\|~0-9a-z]+`; // Key or value +const qdtext = String.raw`[^"\\\x7F]`; // Characters that don't need escaping +const quotedPair = String.raw`\\[\t \x21-\x7E\x80-\xFF]`; // Escaped characters +const quotedString = `(?:${qdtext}|${quotedPair})*`; +const fieldMatch = new RegExp(String.raw`(?<=^|,\s*)(${token})\s*=\s*(?:(${token})|"(${quotedString})")(?=,|$)`, 'ig'); +const parseSigFields = str => Object.fromEntries( + Array.from( + str.matchAll(fieldMatch) + ).map( + // capture groups: 1=fieldname, 2=field value if unquoted, 3=field value if quoted + v=>[ + v[1], + v[2] ?? v[3].replace( + /\\./g, + c=>c[1] + + ) + ] + ) +); + +const defaultHeaderNames = ['(request-target)', 'host', 'date']; + +/** + * @private + * Generate the string to be signed for the signature header + * @param {Object} options Options + * @param {string} options.target The pathname of the request URL (including query and hash strings) + * @param {string} options.method The HTTP request method + * @param {object} options.headers Object whose keys are http header names and whose values are those headers' values + * @param {string[]} headerNames Names of the headers to use in the signature + * @returns {string} + */ +function getSignString({ target, method, headers }, headerNames) { + const requestTarget = `${method.toLowerCase()} ${target}`; + headers = { + ...headers, + '(request-target)': requestTarget + }; + return headerNames.map(header => `${header.toLowerCase()}: ${headers[header]}`).join('\n'); +} + +export class Sha256Signer { + #publicKeyId; + #privateKey; + #headerNames; + + /** + * Class for signing a request and returning the signature header + * @param {object} options Config options + * @param {string} options.publicKeyId URI for public key that must be used for verification + * @param {string} options.privateKey Private key to use for signing + * @param {string[]} options.headerNames Names of headers to use in generating signature + */ + constructor({ publicKeyId, privateKey, headerNames }) { + this.#publicKeyId = publicKeyId; + this.#privateKey = privateKey; + this.#headerNames = headerNames ?? defaultHeaderNames; + } + + /** + * Generate the signature header for an outgoing message + * @param {object} reqOptions Request options + * @param {string} reqOptions.url The full URL of the request to sign + * @param {string} reqOptions.method Method of the request + * @param {object} reqOptions.headers Dict of headers used in the request + * @returns {string} Value for the signature header + */ + sign({ url, method, headers }) { + const { host, pathname, search } = new URL(url); + const target = `${pathname}${search}`; + headers.date = headers.date || new Date().toUTCString(); + headers.host = headers.host || host; + + const headerNames = this.#headerNames; + + const stringToSign = getSignString({ target, method, headers }, headerNames); + + const signature = this.#signSha256(this.#privateKey, stringToSign).toString('base64'); + + return `keyId="${this.#publicKeyId}",headers="${headerNames.join(' ')}",signature="${signature.replace(/"/g, '\\"')}",algorithm="rsa-sha256"`; + } + + /** + * @private + * Sign a string with a private key using sha256 alg + * @param {string} privateKey Private key + * @param {string} stringToSign String to sign + * @returns {Buffer} Signature buffer + */ + #signSha256(privateKey, stringToSign) { + const signer = crypto.createSign('sha256'); + signer.update(stringToSign); + const signature = signer.sign(privateKey); + signer.end(); + return signature; + } +} + +/** + * Incoming request parser and Signature factory. + * If you wish to support more signature types you can extend this class + * and overide getSignatureClass. + */ +export class Parser { + /** + * Parse an incomming request's http signature header + * @param {object} reqOptions Request options + * @param {string} reqOptions.url The pathname (and query string) of the request URL + * @param {string} reqOptions.method Method of the request + * @param {object} reqOptions.headers Dict of headers used in the request + * @returns {Signature} Object representing the signature + * @throws {UnkownAlgorithmError} If the algorithm used isn't one we know how to verify + */ + parse({ headers, method, url }){ + const fields = parseSigFields(headers.signature); + const headerNames = (fields.headers ?? 'date').split(/\s+/); + const signature = Buffer.from(fields.signature, 'base64'); + const signString = getSignString({ target: url, method, headers }, headerNames); + const keyId = fields.keyId; + const algorithm = fields.algorithm ?? 'rsa-sha256'; + + return this.getSignatureClass(algorithm, { signature, string: signString, keyId }); + } + + /** + * Construct the signature class for a given algorithm. + * Override this method if you want to support additional + * algorithms. + * @param {string} algorithm The algorithm used by the signed request + * @param {object} options + * @param {Buffer} options.signature The signature as a buffer + * @param {string} options.string The string that was signed + * @param {string} options.keyId The ID of the public key to be used for verification + * @returns {Signature} + * @throws {UnkownAlgorithmError} If an unknown algorithm was used + */ + getSignatureClass(algorithm, { signature, string, keyId }) { + if(algorithm === 'rsa-sha256') { + return new Sha256Signature({ signature, string, keyId }); + } else { + throw new UnkownAlgorithmError(`Don't know how to verify ${algorithm} signatures.`); + } + } +} + +export class UnkownAlgorithmError extends Error {} + +export class Signature { + #keyId; + + constructor(keyId) { + this.#keyId = keyId; + } + + get keyId(){ + return this.#keyId; + } + + verify(key){ + return false; + } +} + +export class Sha256Signature extends Signature { + #signature; + #string; + + /** + * Class representing the HTTP signature + * @param {object} options + * @param {Buffer} options.signature The signature as a buffer + * @param {string} options.string The string that was signed + * @param {string} options.keyId The ID of the public key to be used for verification + */ + constructor({ signature, string, keyId }) { + super(keyId); + this.#signature = signature; + this.#string = string; + } + + /** + * @property {string} keyId The ID of the public key that can verify the signature + */ + + /** + * Verify the signature using a public key + * @param {string} key The public key matching the signature's keyId + * @returns {boolean} + */ + verify(key) { + const signature = this.#signature; + const signedString = this.#string; + const verifier = crypto.createVerify('sha256'); + verifier.write(signedString); + verifier.end(); + + return verifier.verify(key, signature); + } +} + +/** + * Default export: new instance of Parser class + */ +export default new Parser;