Skip to content

Commit

Permalink
Update algorithm to sign header names.
Browse files Browse the repository at this point in the history
- See issue #10.
- Header names must be included in the signed string to avoid an issue
  where an attacker could switch header list order and header value
  order and end up with the same signature for different requests.
- Update parser, signer, and tests.
- Slight change to signer to always treat 'request-line' as the request
  line and never check for a header named 'Request-Line'.
- Add test from spec into a test file.
- Update spec text and example values.
  • Loading branch information
davidlehn committed Apr 20, 2013
1 parent 0a12d29 commit 658961f
Show file tree
Hide file tree
Showing 5 changed files with 123 additions and 36 deletions.
57 changes: 36 additions & 21 deletions http_signing.md
Expand Up @@ -92,20 +92,26 @@ OPTIONAL. The `extensions` parameter is used to include additional information
which is covered by the request. The content and format of the string is out of
scope for this document, and expected to be specified by implementors.

### Digital Signature
#### sig

The `digitalSignature` portion of the credentials is a REQUIRED field, and is
REQUIRED. The `sig` parameter is a `Base64` encoded digital signature
generated by the client. The client uses the `algorithm` and `headers` request
parameters to form a canonicalized `signing string`. This `signing string` is
then signed with the key associated with `keyId` and the algorithm corresponding
to `algorithm`. The result is then `Base64` encoded.
then signed with the key associated with `keyId` and the algorithm
corresponding to `algorithm`. The `sig` parameter is then set to the `Base64`
encoding of the signature.

### Signing String Composition

In order to generate the string that is signed with a key, the client MUST
take the values of each HTTP header specified by `headers`, in the order they
appear, and separate with an ASCII newline `\n`. The last header in the list
MUST NOT include a trailing ASCII newline.
In order to generate the string that is signed with a key, the client MUST take
the values of each HTTP header specified by `headers` in the order they appear.

1. If the header name is not `request-line` then append the lowercased header
name followed with an ASCII colon `:` and an ASCII space ` `.
2. If the header name is `request-line` then appened the HTTP request line,
otherwise append the header value.
3. If value is not the last value then append an ASCII newline `\n`. The string
MUST NOT include a trailing ASCII newline.

# Example Requests

Expand All @@ -129,9 +135,19 @@ All requests refer to the following request (body ommitted):
The client would compose the signing string as (`+ "\n"` inserted for
readability):

application/json + "\n"
Tue, 07 Jun 2011 20:51:35 GMT + "\n"
h0auK8hnYJKmHTLhKtMTkQ==
content-type: application/json + "\n"
date: Tue, 07 Jun 2011 20:51:35 GMT + "\n"
content-md5: h0auK8hnYJKmHTLhKtMTkQ==

## Header List with `request-line`

Authorization: Signature keyId="123",headers="date request-line" Base64(RSA-SHA256(Tue, 07 Jun 2011 20:51:35 GMT))

The client would compose the signing string as (`+ "\n"` inserted for
readability):

date: Tue, 07 Jun 2011 20:51:35 GMT + "\n"
POST /foo HTTP/1.1

## Algorithm

Expand Down Expand Up @@ -246,28 +262,27 @@ And all examples use this request:
The string to sign would be:

```
Thu, 05 Jan 2012 21:31:40 GMT
date: Thu, 05 Jan 2012 21:31:40 GMT
```

The Authorization header would be:

Authorization: Signature keyId="Test",algorithm="rsa-sha256" MDyO5tSvin5FBVdq3gMBTwtVgE8U/JpzSwFvY7gu7Q2tiZ5TvfHzf/RzmRoYwO8PoV1UGaw6IMwWzxDQkcoYOwvG/w4ljQBBoNusO/mYSvKrbqxUmZi8rNtrMcb82MS33bai5IeLnOGl31W1UbL4qE/wL8U9wCPGRJlCFLsTgD8=
Authorization: Signature keyId="Test",algorithm="rsa-sha256" JldXnt8W9t643M2Sce10gqCh/+E7QIYLiI+bSjnFBGCti7s+mPPvOjVb72sbd1FjeOUwPTDpKbrQQORrm+xBYfAwCxF3LBSSzORvyJ5nRFCFxfJ3nlQD6Kdxhw8wrVZX5nSem4A/W3C8qH5uhFTRwF4ruRjh+ENHWuovPgO/HGQ=

### All Headers

Parameterized to include all headers, the string to sign would be:

```
/foo?param=value&pet=dog HTTP/1.1
example.com
Thu, 05 Jan 2012 21:31:40 GMT
application/json
Sd/dVLAcvNLSq16eXua5uQ==
18
POST /foo?param=value&pet=dog HTTP/1.1
host: example.com
date: Thu, 05 Jan 2012 21:31:40 GMT
content-type: application/json
content-md5: Sd/dVLAcvNLSq16eXua5uQ==
content-length: 18
```

The Authorization header would be:

Authorization: Signature
keyId="Test",algorithm="rsa-sha256",headers="request-line host date content-type content-md5 content-length" gVrKP7wVh1+FmWbNlhj0pNXIe9XmeOA6EcnoOKAvUILnwaMFzaKaam9UmeDPwjC9TdT+jSRqjtyZE49kZcSpYAHxGlPQ4ziXFRfPprlN/3Xwg3sUOGqbBiS3WFuY3QOOWv4tzc5p70g74U/QvHNNiYMcjoz89vRJhefbFSNwCDs=
Authorization: Signature keyId="Test",algorithm="rsa-sha256",headers="request-line host date content-type content-md5 content-length" Gm7W/r+e90REDpWytALMrft4MqZxCmslOTOvwJX17ViEBA5E65QqvWI0vIH3l/vSsGiaMVmuUgzYsJLYMLcm5dGrv1+a+0fCoUdVKPZWHyImQEqpLkopVwqEH67LVECFBqFTAKlQgBn676zrfXQbb+b/VebAsNUtvQMe6cTjnDY=

7 changes: 3 additions & 4 deletions lib/parser.js
Expand Up @@ -237,17 +237,16 @@ module.exports = {
var h = parsed.params.headers[i].toLowerCase();
parsed.params.headers[i] = h;

var value;
if (h !== 'request-line') {
value = request.headers[h];
var value = request.headers[h];
if (!value)
throw new MissingHeaderError(h + ' was not in the request');
parsed.signingString += h + ': ' + value;
} else {
value =
parsed.signingString +=
request.method + ' ' + request.url + ' HTTP/' + request.httpVersion;
}

parsed.signingString += value;
if ((i + 1) < parsed.params.headers.length)
parsed.signingString += '\n';
}
Expand Down
14 changes: 7 additions & 7 deletions lib/signer.js
Expand Up @@ -133,18 +133,18 @@ module.exports = {
throw new TypeError('options.headers must be an array of Strings');

var h = options.headers[i].toLowerCase();
request.getHeader(h);

var value = request.getHeader(h);
if (!value) {
if (h === 'request-line') {
value = request.method + ' ' + request.path + ' HTTP/1.1';
} else {
if (h !== 'request-line') {
var value = request.getHeader(h);
if (!value) {
throw new MissingHeaderError(h + ' was not in the request');
}
stringToSign += h + ': ' + value;
} else {
value =
stringToSign += request.method + ' ' + request.path + ' HTTP/1.1';
}

stringToSign += value;
if ((i + 1) < options.headers.length)
stringToSign += '\n';
}
Expand Down
4 changes: 2 additions & 2 deletions test/parser.test.js
Expand Up @@ -373,8 +373,8 @@ test('valid explicit headers', function(t) {
t.equal(parsed.signature, 'digitalSignature');
t.ok(parsed.signingString);
t.equal(parsed.signingString,
(options.headers.Date + '\n' +
options.headers['content-md5'] + '\n' +
('date: ' + options.headers.Date + '\n' +
'content-md5: ' + options.headers['content-md5'] + '\n' +
'GET / HTTP/1.1'));
t.equal(parsed.params.keyId, parsed.keyId);
t.equal(parsed.params.algorithm.toUpperCase(),
Expand Down
77 changes: 75 additions & 2 deletions test/verify.test.js
Expand Up @@ -120,7 +120,7 @@ test('valid hmac', function(t) {

options.headers.Date = _rfc1123();
var hmac = crypto.createHmac('sha1', hmacKey);
hmac.update(options.headers.Date);
hmac.update('date: ' + options.headers.Date);
options.headers.Authorization =
'Signature keyId="foo",algorithm="hmac-sha1" ' +
hmac.digest('base64');
Expand Down Expand Up @@ -166,7 +166,7 @@ test('valid rsa', function(t) {

options.headers.Date = _rfc1123();
var signer = crypto.createSign('RSA-SHA256');
signer.update(options.headers.Date);
signer.update('date: ' + options.headers.Date);
options.headers.Authorization =
'Signature keyId="foo",algorithm="rsa-sha256" ' +
signer.sign(rsaPrivate, 'base64');
Expand All @@ -178,6 +178,79 @@ test('valid rsa', function(t) {
});


// test values from spec for defaults
test('valid rsa from spec default', function(t) {
server.tester = function(req, res) {
console.log('> [DEFAULT]', req.headers.authorization);
var parsed = httpSignature.parseRequest(req);
t.ok(httpSignature.verify(parsed, rsaPublic));

res.writeHead(200);
res.write(JSON.stringify(parsed, null, 2));
res.end();
};

options.method = 'POST';
options.path = '/foo?param=value&pet=dog';
options.headers.host = 'example.com';
options.headers.Date = _rfc1123();
options.headers['content-type'] = 'application/json';
options.headers['content-md5'] = 'Sd/dVLAcvNLSq16eXua5uQ==';
options.headers['content-length'] = '18';
var signer = crypto.createSign('RSA-SHA256');
signer.update('date: ' + options.headers.Date);
options.headers.Authorization =
'Signature keyId="Test",algorithm="rsa-sha256"' +
' ' + signer.sign(rsaPrivate, 'base64');

var req = http.request(options, function(res) {
t.equal(res.statusCode, 200);
t.end();
});
req.write('{"hello": "world"}');
req.end();
});

// test values from spec for all headers
test('valid rsa from spec all headers', function(t) {
server.tester = function(req, res) {
console.log('> [ALL]', req.headers.authorization);
var parsed = httpSignature.parseRequest(req);
t.ok(httpSignature.verify(parsed, rsaPublic));

res.writeHead(200);
res.write(JSON.stringify(parsed, null, 2));
res.end();
};

options.method = 'POST';
options.path = '/foo?param=value&pet=dog';
options.headers.host = 'example.com';
options.headers.Date = _rfc1123();
options.headers['content-type'] = 'application/json';
options.headers['content-md5'] = 'Sd/dVLAcvNLSq16eXua5uQ==';
options.headers['content-length'] = '18';
var signer = crypto.createSign('RSA-SHA256');
signer.update(options.method + ' ' + options.path + ' HTTP/1.1\n');
signer.update('host: ' + options.headers.host + '\n');
signer.update('date: ' + options.headers.Date + '\n');
signer.update('content-type: ' + options.headers['content-type'] + '\n');
signer.update('content-md5: ' + options.headers['content-md5'] + '\n');
signer.update('content-length: ' + options.headers['content-length']);
options.headers.Authorization =
'Signature keyId="Test",algorithm="rsa-sha256",headers=' +
'"request-line host date content-type content-md5 content-length"' +
' ' + signer.sign(rsaPrivate, 'base64');

var req = http.request(options, function(res) {
t.equal(res.statusCode, 200);
t.end();
});
req.write('{"hello": "world"}');
req.end();
});


test('tear down', function(t) {
server.on('close', function() {
t.end();
Expand Down

0 comments on commit 658961f

Please sign in to comment.