This commit is contained in:
Deva Midhun 2025-02-08 19:38:25 +00:00
parent ecf9fe1dc2
commit 55f5278730
5 changed files with 302 additions and 0 deletions

View File

@ -0,0 +1,21 @@
import { AP } from "activitypub-core-types";
export async function fetchActorInformation(actorUrl: string): Promise<AP.Actor | null> {
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;
}

View File

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

View File

@ -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<Response> {
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;
}

View File

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

215
lib/http-signature/index.js Normal file
View File

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