Skip to content

Instantly share code, notes, and snippets.

@DinoChiesa
Last active March 30, 2023 21:08
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save DinoChiesa/36ff5491a3d9ceda55665bead3819918 to your computer and use it in GitHub Desktop.
Save DinoChiesa/36ff5491a3d9ceda55665bead3819918 to your computer and use it in GitHub Desktop.
// createKeyPair.js
// ------------------------------------------------------------------
//
// created: Thu Feb 28 15:49:15 2019
// last saved: <2023-March-29 14:19:28>
/* jshint esversion:9, node: true */
/* global process, console, Buffer, require */
const crypto = require('crypto');
crypto.generateKeyPair('rsa', { // requires node@10
modulusLength: 2048, //4096
publicKeyEncoding: {
type: 'spki',
format: 'pem'
},
privateKeyEncoding: {
type: 'pkcs8',
format: 'pem'
//cipher: 'aes-256-cbc',
//passphrase: 'top secret'
}
}, (e, publicKey, privateKey) => {
// Handle errors and use the generated key pair.
console.log('\n\npublic:\n' + publicKey);
console.log('\n\nprivate:\n' + privateKey);
let fs = require('fs');
fs.writeFileSync('public-rsa.pem', publicKey, {encoding:'utf8'});
fs.writeFileSync('private-rsa.pem', privateKey, {encoding:'utf8'});
});
// sign.js
// ------------------------------------------------------------------
//
// It seems that https://www.npmjs.com/package/http-signature is out of date with the current
// HTTP Signature specification (https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures).
//
// This example shows how to compute an RSA-signed signature for hs2019, wuth created/expires and digest.
//
// Run the accompanying createKeyPair.js script to get a keypair, if you need one.
//
// created: Wed Mar 29 13:14:19 2023
// last saved: <2023-March-29 14:15:26>
/* jshint esversion:9, node:true, strict:implied */
/* global process, console, Buffer, URL, require */
const crypto = require('crypto');
const SIG_LIFETIME = 90; // in seconds
function toBase64Url(s) {
return s.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
function getRsaPrivateKey() {
return getRsaKey('./private-rsa.pem');
}
function getRsaPublicKey() {
return getRsaKey('./public-rsa.pem');
}
function getRsaKey(filepath) {
let fs = require('fs');
let privatePem = fs.readFileSync(filepath);
let key = privatePem.toString('utf-8');
return key;
}
/*
* signs according to https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures
*
* method: get, post, etc
* url: a string specifying the target URL.
* inboundHeaders: a hash of all headers to send.
* namesOfHeadersToSign: an array of header names to include in signature. This can include the
* so-called "special" header name of "(request-target)", as well as "created" and "expires".
* An example of a value for this parameter might be:
* [ "(request-target)", "host", "date", "digest", "created", "expires" ]
* keyInfo: hash of { key, keyId}
**/
function sign(method, url, inboundHeaders, namesOfHeadersToSign, keyInfo) {
let now = Math.floor((new Date()).valueOf() / 1000); // secconds since epoch
let signatureBase =
namesOfHeadersToSign.map(name => name .toLowerCase())
.map( name => {
if (name == '(request-target)') {
let parsedUrl = new URL(url);
return "(request-target): " + method.toLowerCase() + ' ' + parsedUrl.path + parsedUrl.search;
}
if (name == 'created') { return 'created: ' + now; }
if (name == 'expires') { return 'expires: ' + (now + SIG_LIFETIME); }
return `${name}: ${inboundHeaders[name]}`;
})
.join('\n');
// example signatureBase:
// (request-target): get /foo
// host: example.org
// date: Tue, 07 Jun 2014 20:51:35 GMT
let signer = crypto.createSign('sha256');
signer.update(signatureBase);
let computedSignature = toBase64Url(signer.sign(keyInfo.key, 'base64'));
let sigHeaderItems = [
`keyId="${keyInfo.keyId}"`,
`headers="${namesOfHeadersToSign.join(' ')}"`
];
if (namesOfHeadersToSign.indexOf('created') >= 0) {
sigHeaderItems.push(`created=${now}`);
}
if (namesOfHeadersToSign.indexOf('expires') >= 0) {
sigHeaderItems.push(`expires=${(now+SIG_LIFETIME)}`);
}
sigHeaderItems.push(`signature="${computedSignature}"`);
let updatedHeaders = {...inboundHeaders, signature: sigHeaderItems.join(', ')};
return updatedHeaders;
}
// ====================================================================
console.log('\nGET request, signing date and host.');
let method1 = 'GET';
let url = "https://mastodon.example.net/users/username/inbox";
let preparedHeaders = {
date: (new Date()).toUTCString(),
host: (new URL(url)).host };
let keyInfo = {
key: getRsaPrivateKey(),
keyId: 'https://my-key-site.com/abcdefg'
};
let resultHeaders =
sign(method1, url, preparedHeaders,
['(request-target)', 'date', 'host', 'digest'],
keyInfo
);
console.log(JSON.stringify(resultHeaders, null, 2));
// ====================================================================
console.log('\nPOST with a payload, signing date, host, and digest. as well as created/expires.');
let method2 = 'POST';
let payloadJson = {
request: "hello"
};
let payloadTxt = JSON.stringify(payloadJson, null, 2);
let hash = crypto.createHash('sha256');
hash.update(payloadTxt);
preparedHeaders.digest = 'SHA-256=' + hash.digest('base64');
resultHeaders =
sign(method2, url, preparedHeaders,
['(request-target)', 'date', 'host', 'digest', 'created', 'expires'],
keyInfo
);
console.log(JSON.stringify(resultHeaders, null, 2));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment