Skip to content

Commit 78dfdad

Browse files
authored
Merge pull request #161 from dhensby/pulls/cavage-spec-examples
fix: never sign empty set of fields
2 parents 0900b35 + fc9ff1f commit 78dfdad

File tree

2 files changed

+97
-2
lines changed

2 files changed

+97
-2
lines changed

src/cavage/index.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,11 +170,16 @@ export function createSignatureBase(fields: string[], message: Request | Respons
170170

171171
export async function signMessage<T extends Request | Response = Request | Response>(config: SignConfig, message: T): Promise<T> {
172172
const signingParameters = createSigningParameters(config);
173-
const signatureBase = createSignatureBase(config.fields ?? [], message, signingParameters);
173+
// NB: In spec versions 11 & 12 (the last 2), if no set of fields to sign has been provided, the default should be (created)
174+
// other versions relied on the Date header - perhaps this should be configurable
175+
const signatureBase = createSignatureBase(config.fields ?? ['@created'], message, signingParameters);
174176
const base = formatSignatureBase(signatureBase);
175177
// call sign
176178
const signature = await config.key.sign(Buffer.from(base));
177179
const headerNames = signatureBase.map(([key]) => key);
180+
// there is a somewhat deliberate and intentional deviation from spec here:
181+
// If no headers (config.fields) are specified, the spec allows for it to be *inferred*
182+
// that the (created) value is used, I don't like that and would rather be explicit
178183
const header = [
179184
...Array.from(signingParameters.entries()).map(([name, value]) => {
180185
if (name === 'alg') {
@@ -224,7 +229,7 @@ export async function verifyMessage(config: VerifyConfig, message: Request | Res
224229
if (!parsedHeader.has('signature')) {
225230
throw new Error('Missing signature from header');
226231
}
227-
const baseParts = new Map(createSignatureBase((parsedHeader.get('headers') ?? '').split(' ').map((component: string) => {
232+
const baseParts = new Map(createSignatureBase((parsedHeader.get('headers') ?? '(created)').split(' ').map((component: string) => {
228233
return component.toLowerCase().replace(/^\((.*)\)$/, '@$1');
229234
}), message, parsedHeader));
230235
const base = formatSignatureBase(Array.from(baseParts.entries()));

test/cavage/examples.spec.ts

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { createPrivateKey, KeyObject } from 'crypto';
2+
import { cavage, createSigner } from '../../src';
3+
import { expect } from 'chai';
4+
5+
/**
6+
* These test have been taken from the specification, but they are only accurate as of
7+
* version 10 of the specification (https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-10)
8+
* and not version 12 (the last one). As such, some of the tests have been modified to pass with the
9+
* latest implementation.
10+
*/
11+
describe('cavage', () => {
12+
describe('specification', () => {
13+
const request = {
14+
method: 'POST',
15+
url: 'https://example.com/foo?param=value&pet=dog',
16+
headers: {
17+
'Host': 'example.com',
18+
'Date': 'Sun, 05 Jan 2014 21:31:40 GMT',
19+
'Content-Type': 'application/json',
20+
'Digest': 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=',
21+
'Content-Length': '18',
22+
},
23+
body: '{"hello": "world"}',
24+
}
25+
let key: KeyObject;
26+
before('load rsa key', () => {
27+
key = createPrivateKey('-----BEGIN RSA PRIVATE KEY-----\n' +
28+
'MIICXgIBAAKBgQDCFENGw33yGihy92pDjZQhl0C36rPJj+CvfSC8+q28hxA161QF\n' +
29+
'NUd13wuCTUcq0Qd2qsBe/2hFyc2DCJJg0h1L78+6Z4UMR7EOcpfdUE9Hf3m/hs+F\n' +
30+
'UR45uBJeDK1HSFHD8bHKD6kv8FPGfJTotc+2xjJwoYi+1hqp1fIekaxsyQIDAQAB\n' +
31+
'AoGBAJR8ZkCUvx5kzv+utdl7T5MnordT1TvoXXJGXK7ZZ+UuvMNUCdN2QPc4sBiA\n' +
32+
'QWvLw1cSKt5DsKZ8UETpYPy8pPYnnDEz2dDYiaew9+xEpubyeW2oH4Zx71wqBtOK\n' +
33+
'kqwrXa/pzdpiucRRjk6vE6YY7EBBs/g7uanVpGibOVAEsqH1AkEA7DkjVH28WDUg\n' +
34+
'f1nqvfn2Kj6CT7nIcE3jGJsZZ7zlZmBmHFDONMLUrXR/Zm3pR5m0tCmBqa5RK95u\n' +
35+
'412jt1dPIwJBANJT3v8pnkth48bQo/fKel6uEYyboRtA5/uHuHkZ6FQF7OUkGogc\n' +
36+
'mSJluOdc5t6hI1VsLn0QZEjQZMEOWr+wKSMCQQCC4kXJEsHAve77oP6HtG/IiEn7\n' +
37+
'kpyUXRNvFsDE0czpJJBvL/aRFUJxuRK91jhjC68sA7NsKMGg5OXb5I5Jj36xAkEA\n' +
38+
'gIT7aFOYBFwGgQAQkWNKLvySgKbAZRTeLBacpHMuQdl1DfdntvAyqpAZ0lY0RKmW\n' +
39+
'G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI\n' +
40+
'7U1yQXnTAEFYM560yJlzUpOb1V4cScGd365tiSMvxLOvTA==\n' +
41+
'-----END RSA PRIVATE KEY-----')
42+
});
43+
it('Default Test (C.1)', async () => {
44+
const signed = await cavage.signMessage({
45+
key: createSigner(key, 'rsa-v1_5-sha256', 'Test'),
46+
fields: ['Date'],
47+
params: ['keyid', 'alg'],
48+
}, request);
49+
expect(signed.headers).to.have.property('Signature', 'keyId="Test",algorithm="rsa-sha256",' +
50+
'headers="date",' + // NB: Not present in specificaiton example
51+
'signature="SjWJWbWN7i0wzBvtPl8rbASWz5xQW6mcJmn+ibttBqtifLN7Sazz' +
52+
'6m79cNfwwb8DMJ5cou1s7uEGKKCs+FLEEaDV5lp7q25WqS+lavg7T8hc0GppauB' +
53+
'6hbgEKTwblDHYGEtbGmtdHgVCk9SuS13F0hZ8FD0k/5OxEPXe5WozsbM="');
54+
});
55+
it('Basic Test (C.2)', async () => {
56+
const signed = await cavage.signMessage({
57+
key: createSigner(key, 'rsa-v1_5-sha256', 'Test'),
58+
params: ['keyid', 'alg'],
59+
fields: ['@request-target', 'host', 'date'],
60+
}, request);
61+
expect(signed.headers).to.have.property('Signature', 'keyId="Test",algorithm="rsa-sha256",' +
62+
'headers="(request-target) host date",' +
63+
'signature="qdx+H7PHHDZgy4y/Ahn9Tny9V3GP6YgBPyUXMmoxWtLbHpUnXS' +
64+
'2mg2+SbrQDMCJypxBLSPQR2aAjn7ndmw2iicw3HMbe8VfEdKFYRqzic+efkb3' +
65+
'nndiv/x1xSHDJWeSWkx3ButlYSuBskLu6kd9Fswtemr3lgdDEmn04swr2Os0="');
66+
});
67+
it('All Headers Test (C.3)', async () => {
68+
const signed = await cavage.signMessage({
69+
key: createSigner(key, 'rsa-v1_5-sha256', 'Test'),
70+
params: ['keyid', 'alg', 'created', 'expires'],
71+
paramValues: {
72+
created: new Date(1402170695000),
73+
expires: new Date(1402170699000),
74+
},
75+
fields: ['@request-target', 'host', 'date', 'content-type', 'digest', 'content-length'],
76+
}, request);
77+
// NB: As noted in the spec, some of the test "vectors" are wrong. For this test, the signature has been
78+
// calculated without the (created) and (expires) params being included in the signature despite the example
79+
// showing they are in the signature header
80+
expect(signed.headers).to.have.property('Signature', 'keyId="Test",algorithm="rsa-sha256",' +
81+
'created=1402170695,expires=1402170699,' +
82+
// 'headers="(request-target) (created) (expires) ' +
83+
'headers="(request-target) ' + // NB: the example signature has only been computed over request-target
84+
'host date content-type digest content-length",' +
85+
'signature="vSdrb+dS3EceC9bcwHSo4MlyKS59iFIrhgYkz8+oVLEEzmYZZvRs' +
86+
'8rgOp+63LEM3v+MFHB32NfpB2bEKBIvB1q52LaEUHFv120V01IL+TAD48XaERZF' +
87+
'ukWgHoBTLMhYS2Gb51gWxpeIq8knRmPnYePbF5MOkR0Zkly4zKH7s1dE="');
88+
});
89+
});
90+
});

0 commit comments

Comments
 (0)