Improved HTTPSignature library

ref https://linear.app/tryghost/issue/MOM-73

We've made it easier to use by adding defaults for required header, as well as
adding support for signing POST requests.
This commit is contained in:
Fabien O'Carroll 2024-05-14 11:21:29 +07:00 committed by Fabien 'egg' O'Carroll
parent e98f505ae3
commit f31330a228
2 changed files with 68 additions and 5 deletions

View File

@ -1,4 +1,5 @@
import assert from 'assert';
import crypto from 'crypto';
import {HTTPSignature} from './http-signature.service';
describe('HTTPSignature', function () {
@ -50,4 +51,50 @@ describe('HTTPSignature', function () {
assert.equal(actual, expected, 'The signature should have been validated');
});
});
describe('#sign', function () {
it('Can sign a request that does not have explicit Date or Host headers', async function () {
const keypair = crypto.generateKeyPairSync('rsa', {
modulusLength: 512
});
const request = new Request('https://example.com:2368/blah');
const signed = await HTTPSignature.sign(request, new URL('https://keyid.com'), keypair.privateKey);
assert.ok(signed);
});
it('Can sign a post request which is valid', async function () {
const keypair = crypto.generateKeyPairSync('rsa', {
modulusLength: 512
});
class MockHTTPSignature extends HTTPSignature {
protected static async getPublicKey() {
return keypair.publicKey;
}
}
const request = new Request('https://example.com:2368/blah', {
method: 'POST',
headers: {
'Content-Type': 'application/ld+json'
},
body: JSON.stringify({
id: 'https://mastodon.social/users/testingshtuff',
type: 'Accept',
actor: 'https://mastodon.social/users/testingshtuff',
object: 'https://mastodon.social/79f89120-fd13-43e8-aa6d-3bd03652cfad'
})
});
const signed = await MockHTTPSignature.sign(request, new URL('https://keyid.com'), keypair.privateKey);
assert.ok(signed);
const url = new URL(signed.url);
const body = Buffer.from(await signed.text());
const result = await MockHTTPSignature.validate(signed.method, url.pathname, signed.headers, body);
assert.equal(result, true);
});
});
});

View File

@ -106,7 +106,9 @@ export class HTTPSignature {
.update(requestBody)
.digest('base64');
const remoteDigest = requestHeaders.get('digest')?.split('SHA-256=')[1];
const parts = requestHeaders.get('digest')?.split('=');
parts?.shift();
const remoteDigest = parts?.join('=');
return digest === remoteDigest;
}
@ -168,9 +170,24 @@ export class HTTPSignature {
algorithm: 'rsa-sha256'
};
const url = new URL(request.url);
const requestHeaders = new Headers(request.headers);
if (!requestHeaders.has('host')) {
requestHeaders.set('host', url.host);
}
if (!requestHeaders.has('date')) {
requestHeaders.set('date', (new Date()).toUTCString());
}
if (request.method.toLowerCase() === 'post') {
const digest = crypto
.createHash(signatureData.algorithm)
.update(Buffer.from(await request.clone().text(), 'utf8'))
.digest('base64');
requestHeaders.set('digest', `${signatureData.algorithm}=${digest}`);
}
const signatureString = this.generateSignatureString(
signatureData,
request.headers,
requestHeaders,
request.method,
url.pathname
);
@ -180,14 +197,13 @@ export class HTTPSignature {
.sign(privateKey)
.toString('base64');
const newHeaders = new Headers(request.headers);
newHeaders.set(
requestHeaders.set(
'Signature',
`keyId="${keyId}",headers="${headers.join(' ')}",signature="${signature}",algorithm="${signatureData.algorithm}"`
);
return new Request(request, {
headers: newHeaders
headers: requestHeaders
});
}
}