Skip to content

Commit

Permalink
Add support for an opaque param in the Authorization header (#101)
Browse files Browse the repository at this point in the history
Contributed by: David Gwynne <dlg@uq.edu.au>
  • Loading branch information
dgwynne committed Mar 17, 2020
1 parent 202ebc3 commit dafcdaa
Show file tree
Hide file tree
Showing 4 changed files with 100 additions and 38 deletions.
2 changes: 1 addition & 1 deletion http_signing.md
Expand Up @@ -287,7 +287,7 @@ The Authorization header would be:

<!-- authz -->

Authorization: Signature keyId="Test",algorithm="rsa-sha256",headers="date",signature="jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w="
Authorization: Signature keyId="Test",algorithm="rsa-sha256",signature="jKyvPcxB4JbmYY4mByyBY7cZfNl4OW9HpFQlG7N4YcJPteKTu4MWCLyk+gIr0wDgqtLWf9NLpMAMimdfsH7FSWGfbMFSrsVTHNTk0rK3usrfFnti1dxsM4jl0kYJCKTGI/UWkqiaxwNiKqGcdlEDrTcUhhsFsOIo8VhddmZTZ8w="

<!-- /authz -->

Expand Down
30 changes: 20 additions & 10 deletions lib/parser.js
Expand Up @@ -110,17 +110,18 @@ module.exports = {
if (options === undefined) {
options = {};
}
if (options.headers === undefined) {
options.headers = [request.headers['x-date'] ? 'x-date' : 'date'];
}
assert.object(options, 'options');
assert.arrayOfString(options.headers, 'options.headers');
assert.optionalFinite(options.clockSkew, 'options.clockSkew');

var headers = request.headers;
var headers = [request.headers['x-date'] ? 'x-date' : 'date'];
if (options.headers !== undefined) {
assert.arrayOfString(headers, 'options.headers');
headers = options.headers;
}

var authzHeaderName = options.authorizationHeaderName;
var authz = headers[authzHeaderName] || headers[utils.HEADER.AUTH] ||
headers[utils.HEADER.SIG];
var authz = request.headers[authzHeaderName] ||
request.headers[utils.HEADER.AUTH] || request.headers[utils.HEADER.SIG];

if (!authz) {
var errHeader = authzHeaderName ? authzHeaderName :
Expand All @@ -134,13 +135,14 @@ module.exports = {


var i = 0;
var state = authz === headers[utils.HEADER.SIG] ? State.Params : State.New;
var state = authz === request.headers[utils.HEADER.SIG] ?
State.Params : State.New;
var substate = ParamsState.Name;
var tmpName = '';
var tmpValue = '';

var parsed = {
scheme: authz === headers[utils.HEADER.SIG] ? 'Signature' : '',
scheme: authz === request.headers[utils.HEADER.SIG] ? 'Signature' : '',
params: {},
signingString: ''
};
Expand Down Expand Up @@ -271,6 +273,13 @@ module.exports = {
parsed.signingString += '(keyid): ' + parsed.params.keyId;
} else if (h === '(algorithm)') {
parsed.signingString += '(algorithm): ' + parsed.params.algorithm;
} else if (h === '(opaque)') {
var opaque = parsed.params.opaque;
if (opaque === undefined) {
throw new MissingHeaderError('opaque param was not in the ' +
authzHeaderName + ' header');
}
parsed.signingString += '(opaque): ' + opaque;
} else {
var value = request.headers[h];
if (value === undefined)
Expand Down Expand Up @@ -301,7 +310,7 @@ module.exports = {
}
}

options.headers.forEach(function (hdr) {
headers.forEach(function (hdr) {
// Remember that we already checked any headers in the params
// were in the request, so if this passes we're good.
if (parsed.params.headers.indexOf(hdr.toLowerCase()) < 0)
Expand All @@ -317,6 +326,7 @@ module.exports = {

parsed.algorithm = parsed.params.algorithm.toUpperCase();
parsed.keyId = parsed.params.keyId;
parsed.opaque = parsed.params.opaque;
return parsed;
}

Expand Down
85 changes: 58 additions & 27 deletions lib/signer.js
Expand Up @@ -17,10 +17,7 @@ var validateAlgorithm = utils.validateAlgorithm;

///--- Globals

var AUTHZ_FMT =
'Signature keyId="%s",algorithm="%s",headers="%s",signature="%s"';

var SIGNATURE_FMT = 'keyId="%s",algorithm="%s",headers="%s",signature="%s"';
var AUTHZ_PARAMS = [ 'keyId', 'algorithm', 'opaque', 'headers', 'signature' ];

///--- Specific Errors

Expand All @@ -34,6 +31,25 @@ function StrictParsingError(message) {
}
util.inherits(StrictParsingError, HttpSignatureError);

function FormatAuthz(prefix, params) {
assert.string(prefix, 'prefix');
assert.object(params, 'params');

var authz = '';
for (var i = 0; i < AUTHZ_PARAMS.length; i++) {
var param = AUTHZ_PARAMS[i];
var value = params[param];
if (value === undefined)
continue;
assert.string(value, 'params.' + param);

authz += prefix + sprintf('%s="%s"', param, value);
prefix = ',';
}

return (authz);
}

/* See createSigner() */
function RequestSigner(options) {
assert.object(options, 'options');
Expand Down Expand Up @@ -190,11 +206,12 @@ RequestSigner.prototype.sign = function (cb) {
assert.string(sig.signature, 'signature.signature');
alg = validateAlgorithm(sig.algorithm);

authz = sprintf(AUTHZ_FMT,
sig.keyId,
sig.algorithm,
self.rs_headers.join(' '),
sig.signature);
authz = FormatAuthz('Signature ', {
keyId: sig.keyId,
algorithm: sig.keyId,
headers: self.rs_headers.join(' '),
signature: sig.signature
});
} catch (e) {
cb(e);
return;
Expand All @@ -211,11 +228,12 @@ RequestSigner.prototype.sign = function (cb) {
}
alg = (this.rs_alg[0] || this.rs_key.type) + '-' + sigObj.hashAlgorithm;
var signature = sigObj.toString();
authz = sprintf(AUTHZ_FMT,
this.rs_keyId,
alg,
this.rs_headers.join(' '),
signature);
authz = FormatAuthz('Signature ', {
keyId: this.rs_keyId,
algorithm: alg,
headers: this.rs_headers.join(' '),
signature: signature
});
cb(null, authz);
}
};
Expand Down Expand Up @@ -289,13 +307,15 @@ module.exports = {
assert.object(options, 'options');
assert.optionalString(options.algorithm, 'options.algorithm');
assert.string(options.keyId, 'options.keyId');
assert.optionalString(options.opaque, 'options.opaque');
assert.optionalArrayOfString(options.headers, 'options.headers');
assert.optionalString(options.httpVersion, 'options.httpVersion');

if (!request.getHeader('Date'))
request.setHeader('Date', jsprim.rfc1123(new Date()));
if (!options.headers)
options.headers = ['date'];
var headers = ['date'];
if (options.headers)
headers = options.headers;
if (!options.httpVersion)
options.httpVersion = '1.1';

Expand Down Expand Up @@ -337,11 +357,11 @@ module.exports = {

var i;
var stringToSign = '';
for (i = 0; i < options.headers.length; i++) {
if (typeof (options.headers[i]) !== 'string')
for (i = 0; i < headers.length; i++) {
if (typeof (headers[i]) !== 'string')
throw new TypeError('options.headers must be an array of Strings');

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

if (h === 'request-line') {
if (!options.strict) {
Expand All @@ -365,6 +385,12 @@ module.exports = {
stringToSign += '(keyid): ' + options.keyId;
} else if (h === '(algorithm)') {
stringToSign += '(algorithm): ' + options.algorithm;
} else if (h === '(opaque)') {
var opaque = options.opaque;
if (opaque == undefined || opaque === '') {
throw new MissingHeaderError('options.opaque was not in the request');
}
stringToSign += '(opaque): ' + opaque;
} else {
var value = request.getHeader(h);
if (value === undefined || value === '') {
Expand All @@ -373,7 +399,7 @@ module.exports = {
stringToSign += h + ': ' + value;
}

if ((i + 1) < options.headers.length)
if ((i + 1) < headers.length)
stringToSign += '\n';
}

Expand Down Expand Up @@ -402,15 +428,20 @@ module.exports = {
}

var authzHeaderName = options.authorizationHeaderName || 'Authorization';
var prefix = authzHeaderName.toLowerCase() === utils.HEADER.SIG ?
'' : 'Signature ';

var FMT = authzHeaderName.toLowerCase() === utils.HEADER.SIG ?
SIGNATURE_FMT : AUTHZ_FMT;
var params = {
'keyId': options.keyId,
'algorithm': options.algorithm,
'signature': signature
};
if (options.opaque)
params.opaque = options.opaque;
if (options.headers)
params.headers = options.headers.join(' ');

request.setHeader(authzHeaderName, sprintf(FMT,
options.keyId,
options.algorithm,
options.headers.join(' '),
signature));
request.setHeader(authzHeaderName, FormatAuthz(prefix, params));

return true;
}
Expand Down
21 changes: 21 additions & 0 deletions test/signer.test.js
Expand Up @@ -241,6 +241,27 @@ test('signing with unspecified algorithm', function(t) {
req.end();
});

test('signing opaque param', function(t) {
var req = http.request(httpOptions, function(res) {
t.end();
});
var opts = {
keyId: 'unit',
key: rsaPrivate,
opaque: 'opaque',
headers: ['date', '(opaque)']
};

req._stringToSign = null;
t.ok(httpSignature.sign(req, opts));
t.ok(req.getHeader('Authorization'));
t.strictEqual(typeof (opts.algorithm), 'string');
t.strictEqual(typeof (req._stringToSign), 'string');
t.ok(req._stringToSign.match(/^date: [^\n]*\n\(opaque\): opaque$/));
console.log('> ' + req.getHeader('Authorization'));
req.end();
});

test('request-target with dsa key', function(t) {
var req = http.request(httpOptions, function(res) {
t.end();
Expand Down

0 comments on commit dafcdaa

Please sign in to comment.