Skip to content

Commit ebe32d4

Browse files
authored
Merge pull request #157 from dhensby/pulls/docs-and-config
docs: update documented examples
2 parents 7f61666 + 6f5983c commit ebe32d4

File tree

2 files changed

+220
-36
lines changed

2 files changed

+220
-36
lines changed

.releaserc

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
branches:
2-
- master
31
preset: conventionalcommits
42
plugins:
53
- '@semantic-release/commit-analyzer'

README.md

Lines changed: 220 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,32 @@
22

33
[![Node.js CI](https://github.com/dhensby/node-http-message-signatures/actions/workflows/nodejs.yml/badge.svg)](https://github.com/dhensby/node-http-message-signatures/actions/workflows/nodejs.yml)
44

5-
Based on the draft specifications for HTTP Message Signatures, this library facilitates the signing
6-
of HTTP messages before being sent.
5+
This library provides a way to perform HTTP message signing as per the HTTP Working Group draft specification for
6+
[HTTP Message Signatures](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures).
7+
8+
HTTP Message Signatures are designed to provide a way to verify the authenticity and integrity of *parts* of an HTTP
9+
message by performing a deterministic serialisation of components of an HTTP Message. More details can be found in the
10+
specifications.
711

812
## Specifications
913

1014
Two specifications are supported by this library:
1115

12-
1. [HTTPbis](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures)
13-
2. [Cavage](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures) and subsequent [RichAnna](https://datatracker.ietf.org/doc/html/draft-richanna-http-message-signatures)
16+
1. [HTTP Working Group spec](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures)
17+
2. [Network Working Group spec](https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures)
1418

1519
## Approach
1620

17-
As the Cavage/RichAnna specification is now expired and superseded by the HTTPbis one, this library takes a
18-
"HTTPbis-first" approach. This means that most support and maintenance will go into the HTTPbis
19-
implementation and syntax. The syntax is then back-ported to the as much as possible.
21+
As the Network WG specification is now expired and superseded by the HTTP WG one. This library takes a
22+
"HTTP WG" approach. This means that most support and maintenance will go into the HTTP WG
23+
implementation and syntax. The syntax is then back-ported to the legacy specification as much as possible.
2024

2125
## Caveats
2226

23-
The Cavage/RichAnna specifications have changed over time, introducing new features. The aim is to support
24-
the [latest version of the specification](https://datatracker.ietf.org/doc/html/draft-richanna-http-message-signatures)
25-
and not to try to support each version in isolation.
27+
The specifications are in draft and are liable to change over time, introducing new features and removing existing ones.
28+
The aim is to support the [latest version of the specification](https://datatracker.ietf.org/doc/html/draft-richanna-http-message-signatures)
29+
and not to try to support each version in isolation. However, this library was last updated against
30+
[revision 13](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures-13) of the HTTP WG specification.
2631

2732
## Limitations in compliance with the specification
2833

@@ -43,7 +48,7 @@ application and must be derived from a URL.
4348

4449
The [`@request-target`](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures#section-2.2.5)
4550
component is intended to be the equivalent to the "request target portion of the request line".
46-
See the specification for examples of what this means. In NodeJS, this line in requests is automatically
51+
See the specification for examples of what this means. In Node.js, this line in requests is automatically
4752
constructed for consumers, so it's not possible to know for certainty what this will be. For incoming
4853
requests, it is possible to extract, but for simplicity’s sake this library does not process the raw
4954
headers for the incoming request and, as such, cannot calculate this value with certainty. It is
@@ -61,51 +66,232 @@ message context for signatures that need to be reinterpreted based on other sign
6166
### Padding attacks
6267

6368
As described in [section 7.5.7](https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-message-signatures-13#section-7.5.7)
64-
it is expected that the NodeJS application has taken steps to ensure that headers are valid and not
69+
it is expected that the Node.js application has taken steps to ensure that headers are valid and not
6570
"garbage". For this library to take on that obligation would be to widen the scope of the library to
6671
a complete HTTP Message validator.
6772

6873
## Examples
6974

70-
### Signing a request
75+
> NB: These examples show the "minimal" signature implementation. That is, they provider a proof of possession of the
76+
key by the sender, but don't provide any integrity over the message. To do that, you must add HTTP fields / components
77+
to the signing object. Please see the tests for further examples, or the type definitions.
78+
79+
### Signing a request (Node.js)
80+
81+
This library has built-in signers/verifiers for Node.js using the native `cryto` package to perform all the required
82+
cryptographic operations. However, this is designed to be easily replaced with any other crypto library/runtime
83+
including `SubtleCrypto` or even a hosted KMS (Key Management Service).
7184

7285
```js
73-
const { sign, createSigner } = require('http-message-signing');
86+
const { httpbis: { signMessage }, createSigner } = require('http-message-signatures');
7487

7588
(async () => {
76-
const signedRequest = await sign({
89+
// create a signing key using Node's built in crypto engine.
90+
// you can supply RSA kets, ECDSA, or ED25519 keys.
91+
const key = createSigner('sharedsecret', 'hmac-sha256', 'my-key-id');
92+
// minimal signing of a request - more aspects of the request can be signed by providing additional
93+
// parameters to the first argument of signMessage.
94+
const signedRequest = await signMessage({
95+
key,
96+
}, {
7797
method: 'POST',
7898
url: 'https://example.com',
7999
headers: {
80-
'content-type': 'text/plain',
100+
'content-type': 'application/json',
101+
'content-digest': 'sha-512=:YMAam51Jz/jOATT6/zvHrLVgOYTGFy1d6GJiOHTohq4yP+pgk4vf2aCsyRZOtw8MjkM7iw7yZ/WkppmM44T3qg==:',
102+
'content-length': '19',
81103
},
82-
body: 'test',
83-
}, {
84-
components: [
85-
'@method',
86-
'@authority',
87-
'content-type',
88-
],
89-
parameters: {
90-
created: Math.floor(Date.now() / 1000),
91-
},
92-
keyId: 'my-hmac-secret',
93-
signer: createSigner('hmac-sha256'),
104+
body: '{"hello": "world"}\n',
94105
});
95106
// signedRequest now has the `Signature` and `Signature-Input` headers
107+
console.log(signedRequest);
96108
})().catch(console.error);
97109
```
98110

111+
This will output the following object (note the new `Signature` and `Signature-Input` headers):
112+
113+
```js
114+
{
115+
method: 'POST',
116+
url: 'https://example.com',
117+
headers: {
118+
'content-type': 'application/json',
119+
'content-digest': 'sha-512=:YMAam51Jz/jOATT6/zvHrLVgOYTGFy1d6GJiOHTohq4yP+pgk4vf2aCsyRZOtw8MjkM7iw7yZ/WkppmM44T3qg==:',
120+
'content-length': '19',
121+
'Signature': 'sig=:RkplfaUzQ4xIkSVP9hT+Y55yAYX9VwSeHmjS5X7d0fE=:',
122+
'Signature-Input': 'sig=();keyid="my-key-id";alg="hmac-sha256";created=1700669009;expires=1700669309'
123+
},
124+
body: '{"hello": "world"}\n'
125+
}
126+
```
127+
99128
### Signing with your own signer
100129

101130
It's possible to provide your own signer (this is useful if you're using a secure enclave or key
102-
management service). To do so, you must implement a callable that has the `alg` prop set to a valid
103-
algorithm value. It's possible to use proprietary algorithm values if you have some internal signing
104-
logic you need to support.
131+
management service). To do so, you must create an object that conforms to the `SigningKey` interface.
132+
133+
For example, using SubtleCrypto:
105134

106135
```js
107-
const mySigner = async (data) => {
108-
return Buffer.from('my sig');
136+
const { webcrypto: crypto } = require('node:crypto');
137+
138+
function createMySigner() {
139+
return {
140+
id: 'my-key-id',
141+
alg: 'hmac-sha256',
142+
async sign(data) {
143+
const key = await crypto.subtle.importKey('raw', Buffer.from('sharedsecret'), {
144+
name: 'HMAC',
145+
hash: 'SHA-256',
146+
}, true, ['sign', 'verify']);
147+
return Buffer.from(await crypto.subtle.sign('HMAC', key, data));
148+
},
149+
};
109150
}
110-
mySigner.alg = 'custom-123';
151+
```
152+
153+
### Verifying a request
154+
155+
Verifying a message requires that there is a key-store that can be used to look-up keys based on the signature parameters,
156+
for example via the signatures `keyid`.
157+
158+
```js
159+
const { httpbis: { verifyMessage }, createVerifier } = require('http-message-signatures');
160+
161+
(async () => {
162+
// an example keystore for looking up keys by ID
163+
const keys = new Map();
164+
keys.set('my-key-id', {
165+
id: 'my-key-id',
166+
algs: ['hmac-sha256'],
167+
// as with signing, you can provide your own verifier here instead of using the built-in helpers
168+
verify: createVerifier('sharedsecret', 'hmac-sha256'),
169+
});
170+
// minimal verification
171+
const verified = await verifyMessage({
172+
// logic for finding a key based on the signature parameters
173+
async keyLookup(params) {
174+
const keyId = params.keyid;
175+
// lookup and return key - note, we could also lookup using the alg too (`params.alg`)
176+
// if there is no key, `verifyMessage()` will throw an error
177+
return keys.get(keyId);
178+
},
179+
}, {
180+
method: 'POST',
181+
url: 'https://example.com',
182+
headers: {
183+
'content-type': 'application/json',
184+
'content-digest': 'sha-512=:YMAam51Jz/jOATT6/zvHrLVgOYTGFy1d6GJiOHTohq4yP+pgk4vf2aCsyRZOtw8MjkM7iw7yZ/WkppmM44T3qg==:',
185+
'content-length': '19',
186+
'signature': 'sig=:RkplfaUzQ4xIkSVP9hT+Y55yAYX9VwSeHmjS5X7d0fE=:',
187+
'signature-input': 'sig=();keyid="my-key-id";alg="hmac-sha256";created=1700669009;expires=1700669309',
188+
},
189+
});
190+
console.log(verified);
191+
})().catch(console.error);
192+
```
193+
194+
### Verifying a response with request components
195+
196+
The HTTP Message Signatures specification allows for responses to reference parts of the request and incorporate them
197+
within the signature, tightly binding the response to the request. If you expect that request bound signatures will be
198+
used, you can provide the request as an optional parameter to the `verifyMessage()` method:
199+
200+
```js
201+
const { httpbis: { verifyMessage }, createVerifier } = require('http-message-signatures');
202+
203+
(async () => {
204+
// an example keystore for looking up keys by ID
205+
const keys = new Map();
206+
keys.set('my-key-id', {
207+
id: 'my-key-id',
208+
alg: 'hmac-sha256',
209+
// as with signing, you can provide your own verifier here instead of using the built-in helpers
210+
verify: createVerifier('sharedsecret', 'hmac-sha256'),
211+
});
212+
// minimal verification
213+
const verified = await verifyMessage({
214+
// logic for finding a key based on the signature parameters
215+
async keyLookup(params) {
216+
const keyId = params.keyid;
217+
// lookup and return key - note, we could also lookup using the alg too (`params.alg`)
218+
// if there is no key, `verifyMessage()` will throw an error
219+
return keys.get(keyId);
220+
},
221+
}, {
222+
// the response
223+
status: 200,
224+
headers: {
225+
'content-type': 'application/json',
226+
'content-digest': 'sha-512=:YMAam51Jz/jOATT6/zvHrLVgOYTGFy1d6GJiOHTohq4yP+pgk4vf2aCsyRZOtw8MjkM7iw7yZ/WkppmM44T3qg==:',
227+
'content-length': '19',
228+
'signature': 'sig=:RkplfaUzQ4xIkSVP9hT+Y55yAYX9VwSeHmjS5X7d0fE=:',
229+
'signature-input': 'sig=();keyid="my-key-id";alg="hmac-sha256";created=1700669009;expires=1700669309',
230+
},
231+
}, {
232+
// the request
233+
method: 'POST',
234+
url: 'https://example.com',
235+
headers: {
236+
'content-type': 'application/json',
237+
'content-digest': 'sha-512=:YMAam51Jz/jOATT6/zvHrLVgOYTGFy1d6GJiOHTohq4yP+pgk4vf2aCsyRZOtw8MjkM7iw7yZ/WkppmM44T3qg==:',
238+
'content-length': '19',
239+
'signature': 'sig=:RkplfaUzQ4xIkSVP9hT+Y55yAYX9VwSeHmjS5X7d0fE=:',
240+
'signature-input': 'sig=();keyid="my-key-id";alg="hmac-sha256";created=1700669009;expires=1700669309',
241+
},
242+
});
243+
console.log(verified);
244+
})().catch(console.error);
245+
```
246+
247+
### Verifying with your own verifier
248+
249+
As with signing, it's possible to provide your own verifier (this is useful if you're running in an environment that
250+
may not have access to Node.js' native `crypto` package). To do so, you must create an object that conforms to the
251+
`VerifyingKey` interface.
252+
253+
For example, using SubtleCrypto:
254+
255+
```js
256+
const { webcrypto: crypto } = require('node:crypto');
257+
const { httpbis: { verifyMessage } } = require('http-message-signatures');
258+
259+
(async () => {
260+
// an example keystore for looking up keys by ID
261+
const keys = new Map();
262+
keys.set('my-key-id', {
263+
id: 'my-key-id',
264+
alg: 'hmac-sha256',
265+
// provide a custom verify function
266+
async verify(data, signature, parameters) {
267+
const key = await crypto.subtle.importKey('raw', Buffer.from('sharedsecret'), {
268+
name: 'HMAC',
269+
hash: 'SHA-256',
270+
}, true, ['sign', 'verify']);
271+
return crypto.subtle.verify('HMAC', key, signature, data);
272+
},
273+
});
274+
// minimal verification
275+
const verified = await verifyMessage({
276+
// logic for finding a key based on the signature parameters
277+
async keyLookup(params) {
278+
const keyId = params.keyid;
279+
// lookup and return key - note, we could also lookup using the alg too (`params.alg`)
280+
// if there is no key, `verifyMessage()` will throw an error
281+
return keys.get(keyId);
282+
},
283+
}, {
284+
// the request
285+
method: 'POST',
286+
url: 'https://example.com',
287+
headers: {
288+
'content-type': 'application/json',
289+
'content-digest': 'sha-512=:YMAam51Jz/jOATT6/zvHrLVgOYTGFy1d6GJiOHTohq4yP+pgk4vf2aCsyRZOtw8MjkM7iw7yZ/WkppmM44T3qg==:',
290+
'content-length': '19',
291+
'signature': 'sig=:RkplfaUzQ4xIkSVP9hT+Y55yAYX9VwSeHmjS5X7d0fE=:',
292+
'signature-input': 'sig=();keyid="my-key-id";alg="hmac-sha256";created=1700669009;expires=1700669309',
293+
},
294+
});
295+
console.log(verified);
296+
})().catch(console.error);
111297
```

0 commit comments

Comments
 (0)