Skip to content

Commit

Permalink
Merge pull request #161 from dhensby/pulls/cavage-spec-examples
Browse files Browse the repository at this point in the history
fix: never sign empty set of fields
  • Loading branch information
dhensby authored Jan 16, 2024
2 parents 0900b35 + fc9ff1f commit 78dfdad
Show file tree
Hide file tree
Showing 2 changed files with 97 additions and 2 deletions.
9 changes: 7 additions & 2 deletions src/cavage/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,16 @@ export function createSignatureBase(fields: string[], message: Request | Respons

export async function signMessage<T extends Request | Response = Request | Response>(config: SignConfig, message: T): Promise<T> {
const signingParameters = createSigningParameters(config);
const signatureBase = createSignatureBase(config.fields ?? [], message, signingParameters);
// NB: In spec versions 11 & 12 (the last 2), if no set of fields to sign has been provided, the default should be (created)
// other versions relied on the Date header - perhaps this should be configurable
const signatureBase = createSignatureBase(config.fields ?? ['@created'], message, signingParameters);
const base = formatSignatureBase(signatureBase);
// call sign
const signature = await config.key.sign(Buffer.from(base));
const headerNames = signatureBase.map(([key]) => key);
// there is a somewhat deliberate and intentional deviation from spec here:
// If no headers (config.fields) are specified, the spec allows for it to be *inferred*
// that the (created) value is used, I don't like that and would rather be explicit
const header = [
...Array.from(signingParameters.entries()).map(([name, value]) => {
if (name === 'alg') {
Expand Down Expand Up @@ -224,7 +229,7 @@ export async function verifyMessage(config: VerifyConfig, message: Request | Res
if (!parsedHeader.has('signature')) {
throw new Error('Missing signature from header');
}
const baseParts = new Map(createSignatureBase((parsedHeader.get('headers') ?? '').split(' ').map((component: string) => {
const baseParts = new Map(createSignatureBase((parsedHeader.get('headers') ?? '(created)').split(' ').map((component: string) => {
return component.toLowerCase().replace(/^\((.*)\)$/, '@$1');
}), message, parsedHeader));
const base = formatSignatureBase(Array.from(baseParts.entries()));
Expand Down
90 changes: 90 additions & 0 deletions test/cavage/examples.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import { createPrivateKey, KeyObject } from 'crypto';
import { cavage, createSigner } from '../../src';
import { expect } from 'chai';

/**
* These test have been taken from the specification, but they are only accurate as of
* version 10 of the specification (https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-10)
* and not version 12 (the last one). As such, some of the tests have been modified to pass with the
* latest implementation.
*/
describe('cavage', () => {
describe('specification', () => {
const request = {
method: 'POST',
url: 'https://example.com/foo?param=value&pet=dog',
headers: {
'Host': 'example.com',
'Date': 'Sun, 05 Jan 2014 21:31:40 GMT',
'Content-Type': 'application/json',
'Digest': 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=',
'Content-Length': '18',
},
body: '{"hello": "world"}',
}
let key: KeyObject;
before('load rsa key', () => {
key = createPrivateKey('-----BEGIN RSA PRIVATE KEY-----\n' +
'MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF\n' +
'NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F\n' +
'UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB\n' +
'AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA\n' +
'QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK\n' +
'kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg\n' +
'f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u\n' +
'412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc\n' +
'mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7\n' +
'kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA\n' +
'gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW\n' +
'G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI\n' +
'7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA==\n' +
'-----END RSA PRIVATE KEY-----')
});
it('Default Test (C.1)', async () => {
const signed = await cavage.signMessage({
key: createSigner(key, 'rsa-v1_5-sha256', 'Test'),
fields: ['Date'],
params: ['keyid', 'alg'],
}, request);
expect(signed.headers).to.have.property('Signature', 'keyId="Test",algorithm="rsa-sha256",' +
'headers="date",' + // NB: Not present in specificaiton example
'signature="SjWJWbWN7i0wzBvtPl8rbASWz5xQW6mcJmn+ibttBqtifLN7Sazz' +
'6m79cNfwwb8DMJ5cou1s7uEGKKCs+FLEEaDV5lp7q25WqS+lavg7T8hc0GppauB' +
'6hbgEKTwblDHYGEtbGmtdHgVCk9SuS13F0hZ8FD0k/5OxEPXe5WozsbM="');
});
it('Basic Test (C.2)', async () => {
const signed = await cavage.signMessage({
key: createSigner(key, 'rsa-v1_5-sha256', 'Test'),
params: ['keyid', 'alg'],
fields: ['@request-target', 'host', 'date'],
}, request);
expect(signed.headers).to.have.property('Signature', 'keyId="Test",algorithm="rsa-sha256",' +
'headers="(request-target) host date",' +
'signature="qdx+H7PHHDZgy4y/Ahn9Tny9V3GP6YgBPyUXMmoxWtLbHpUnXS' +
'2mg2+SbrQDMCJypxBLSPQR2aAjn7ndmw2iicw3HMbe8VfEdKFYRqzic+efkb3' +
'nndiv/x1xSHDJWeSWkx3ButlYSuBskLu6kd9Fswtemr3lgdDEmn04swr2Os0="');
});
it('All Headers Test (C.3)', async () => {
const signed = await cavage.signMessage({
key: createSigner(key, 'rsa-v1_5-sha256', 'Test'),
params: ['keyid', 'alg', 'created', 'expires'],
paramValues: {
created: new Date(1402170695000),
expires: new Date(1402170699000),
},
fields: ['@request-target', 'host', 'date', 'content-type', 'digest', 'content-length'],
}, request);
// NB: As noted in the spec, some of the test "vectors" are wrong. For this test, the signature has been
// calculated without the (created) and (expires) params being included in the signature despite the example
// showing they are in the signature header
expect(signed.headers).to.have.property('Signature', 'keyId="Test",algorithm="rsa-sha256",' +
'created=1402170695,expires=1402170699,' +
// 'headers="(request-target) (created) (expires) ' +
'headers="(request-target) ' + // NB: the example signature has only been computed over request-target
'host date content-type digest content-length",' +
'signature="vSdrb+dS3EceC9bcwHSo4MlyKS59iFIrhgYkz8+oVLEEzmYZZvRs' +
'8rgOp+63LEM3v+MFHB32NfpB2bEKBIvB1q52LaEUHFv120V01IL+TAD48XaERZF' +
'ukWgHoBTLMhYS2Gb51gWxpeIq8knRmPnYePbF5MOkR0Zkly4zKH7s1dE="');
});
});
});

0 comments on commit 78dfdad

Please sign in to comment.