You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

401 lines
13 KiB

  1. // Copyright 2012 Joyent, Inc. All rights reserved.
  2. var assert = require('assert-plus');
  3. var crypto = require('crypto');
  4. var http = require('http');
  5. var util = require('util');
  6. var sshpk = require('sshpk');
  7. var jsprim = require('jsprim');
  8. var utils = require('./utils');
  9. var sprintf = require('util').format;
  10. var HASH_ALGOS = utils.HASH_ALGOS;
  11. var PK_ALGOS = utils.PK_ALGOS;
  12. var InvalidAlgorithmError = utils.InvalidAlgorithmError;
  13. var HttpSignatureError = utils.HttpSignatureError;
  14. var validateAlgorithm = utils.validateAlgorithm;
  15. ///--- Globals
  16. var AUTHZ_FMT =
  17. 'Signature keyId="%s",algorithm="%s",headers="%s",signature="%s"';
  18. ///--- Specific Errors
  19. function MissingHeaderError(message) {
  20. HttpSignatureError.call(this, message, MissingHeaderError);
  21. }
  22. util.inherits(MissingHeaderError, HttpSignatureError);
  23. function StrictParsingError(message) {
  24. HttpSignatureError.call(this, message, StrictParsingError);
  25. }
  26. util.inherits(StrictParsingError, HttpSignatureError);
  27. /* See createSigner() */
  28. function RequestSigner(options) {
  29. assert.object(options, 'options');
  30. var alg = [];
  31. if (options.algorithm !== undefined) {
  32. assert.string(options.algorithm, 'options.algorithm');
  33. alg = validateAlgorithm(options.algorithm);
  34. }
  35. this.rs_alg = alg;
  36. /*
  37. * RequestSigners come in two varieties: ones with an rs_signFunc, and ones
  38. * with an rs_signer.
  39. *
  40. * rs_signFunc-based RequestSigners have to build up their entire signing
  41. * string within the rs_lines array and give it to rs_signFunc as a single
  42. * concat'd blob. rs_signer-based RequestSigners can add a line at a time to
  43. * their signing state by using rs_signer.update(), thus only needing to
  44. * buffer the hash function state and one line at a time.
  45. */
  46. if (options.sign !== undefined) {
  47. assert.func(options.sign, 'options.sign');
  48. this.rs_signFunc = options.sign;
  49. } else if (alg[0] === 'hmac' && options.key !== undefined) {
  50. assert.string(options.keyId, 'options.keyId');
  51. this.rs_keyId = options.keyId;
  52. if (typeof (options.key) !== 'string' && !Buffer.isBuffer(options.key))
  53. throw (new TypeError('options.key for HMAC must be a string or Buffer'));
  54. /*
  55. * Make an rs_signer for HMACs, not a rs_signFunc -- HMACs digest their
  56. * data in chunks rather than requiring it all to be given in one go
  57. * at the end, so they are more similar to signers than signFuncs.
  58. */
  59. this.rs_signer = crypto.createHmac(alg[1].toUpperCase(), options.key);
  60. this.rs_signer.sign = function () {
  61. var digest = this.digest('base64');
  62. return ({
  63. hashAlgorithm: alg[1],
  64. toString: function () { return (digest); }
  65. });
  66. };
  67. } else if (options.key !== undefined) {
  68. var key = options.key;
  69. if (typeof (key) === 'string' || Buffer.isBuffer(key))
  70. key = sshpk.parsePrivateKey(key);
  71. assert.ok(sshpk.PrivateKey.isPrivateKey(key, [1, 2]),
  72. 'options.key must be a sshpk.PrivateKey');
  73. this.rs_key = key;
  74. assert.string(options.keyId, 'options.keyId');
  75. this.rs_keyId = options.keyId;
  76. if (!PK_ALGOS[key.type]) {
  77. throw (new InvalidAlgorithmError(key.type.toUpperCase() + ' type ' +
  78. 'keys are not supported'));
  79. }
  80. if (alg[0] !== undefined && key.type !== alg[0]) {
  81. throw (new InvalidAlgorithmError('options.key must be a ' +
  82. alg[0].toUpperCase() + ' key, was given a ' +
  83. key.type.toUpperCase() + ' key instead'));
  84. }
  85. this.rs_signer = key.createSign(alg[1]);
  86. } else {
  87. throw (new TypeError('options.sign (func) or options.key is required'));
  88. }
  89. this.rs_headers = [];
  90. this.rs_lines = [];
  91. }
  92. /**
  93. * Adds a header to be signed, with its value, into this signer.
  94. *
  95. * @param {String} header
  96. * @param {String} value
  97. * @return {String} value written
  98. */
  99. RequestSigner.prototype.writeHeader = function (header, value) {
  100. assert.string(header, 'header');
  101. header = header.toLowerCase();
  102. assert.string(value, 'value');
  103. this.rs_headers.push(header);
  104. if (this.rs_signFunc) {
  105. this.rs_lines.push(header + ': ' + value);
  106. } else {
  107. var line = header + ': ' + value;
  108. if (this.rs_headers.length > 0)
  109. line = '\n' + line;
  110. this.rs_signer.update(line);
  111. }
  112. return (value);
  113. };
  114. /**
  115. * Adds a default Date header, returning its value.
  116. *
  117. * @return {String}
  118. */
  119. RequestSigner.prototype.writeDateHeader = function () {
  120. return (this.writeHeader('date', jsprim.rfc1123(new Date())));
  121. };
  122. /**
  123. * Adds the request target line to be signed.
  124. *
  125. * @param {String} method, HTTP method (e.g. 'get', 'post', 'put')
  126. * @param {String} path
  127. */
  128. RequestSigner.prototype.writeTarget = function (method, path) {
  129. assert.string(method, 'method');
  130. assert.string(path, 'path');
  131. method = method.toLowerCase();
  132. this.writeHeader('(request-target)', method + ' ' + path);
  133. };
  134. /**
  135. * Calculate the value for the Authorization header on this request
  136. * asynchronously.
  137. *
  138. * @param {Func} callback (err, authz)
  139. */
  140. RequestSigner.prototype.sign = function (cb) {
  141. assert.func(cb, 'callback');
  142. if (this.rs_headers.length < 1)
  143. throw (new Error('At least one header must be signed'));
  144. var alg, authz;
  145. if (this.rs_signFunc) {
  146. var data = this.rs_lines.join('\n');
  147. var self = this;
  148. this.rs_signFunc(data, function (err, sig) {
  149. if (err) {
  150. cb(err);
  151. return;
  152. }
  153. try {
  154. assert.object(sig, 'signature');
  155. assert.string(sig.keyId, 'signature.keyId');
  156. assert.string(sig.algorithm, 'signature.algorithm');
  157. assert.string(sig.signature, 'signature.signature');
  158. alg = validateAlgorithm(sig.algorithm);
  159. authz = sprintf(AUTHZ_FMT,
  160. sig.keyId,
  161. sig.algorithm,
  162. self.rs_headers.join(' '),
  163. sig.signature);
  164. } catch (e) {
  165. cb(e);
  166. return;
  167. }
  168. cb(null, authz);
  169. });
  170. } else {
  171. try {
  172. var sigObj = this.rs_signer.sign();
  173. } catch (e) {
  174. cb(e);
  175. return;
  176. }
  177. alg = (this.rs_alg[0] || this.rs_key.type) + '-' + sigObj.hashAlgorithm;
  178. var signature = sigObj.toString();
  179. authz = sprintf(AUTHZ_FMT,
  180. this.rs_keyId,
  181. alg,
  182. this.rs_headers.join(' '),
  183. signature);
  184. cb(null, authz);
  185. }
  186. };
  187. ///--- Exported API
  188. module.exports = {
  189. /**
  190. * Identifies whether a given object is a request signer or not.
  191. *
  192. * @param {Object} object, the object to identify
  193. * @returns {Boolean}
  194. */
  195. isSigner: function (obj) {
  196. if (typeof (obj) === 'object' && obj instanceof RequestSigner)
  197. return (true);
  198. return (false);
  199. },
  200. /**
  201. * Creates a request signer, used to asynchronously build a signature
  202. * for a request (does not have to be an http.ClientRequest).
  203. *
  204. * @param {Object} options, either:
  205. * - {String} keyId
  206. * - {String|Buffer} key
  207. * - {String} algorithm (optional, required for HMAC)
  208. * or:
  209. * - {Func} sign (data, cb)
  210. * @return {RequestSigner}
  211. */
  212. createSigner: function createSigner(options) {
  213. return (new RequestSigner(options));
  214. },
  215. /**
  216. * Adds an 'Authorization' header to an http.ClientRequest object.
  217. *
  218. * Note that this API will add a Date header if it's not already set. Any
  219. * other headers in the options.headers array MUST be present, or this
  220. * will throw.
  221. *
  222. * You shouldn't need to check the return type; it's just there if you want
  223. * to be pedantic.
  224. *
  225. * The optional flag indicates whether parsing should use strict enforcement
  226. * of the version draft-cavage-http-signatures-04 of the spec or beyond.
  227. * The default is to be loose and support
  228. * older versions for compatibility.
  229. *
  230. * @param {Object} request an instance of http.ClientRequest.
  231. * @param {Object} options signing parameters object:
  232. * - {String} keyId required.
  233. * - {String} key required (either a PEM or HMAC key).
  234. * - {Array} headers optional; defaults to ['date'].
  235. * - {String} algorithm optional (unless key is HMAC);
  236. * default is the same as the sshpk default
  237. * signing algorithm for the type of key given
  238. * - {String} httpVersion optional; defaults to '1.1'.
  239. * - {Boolean} strict optional; defaults to 'false'.
  240. * @return {Boolean} true if Authorization (and optionally Date) were added.
  241. * @throws {TypeError} on bad parameter types (input).
  242. * @throws {InvalidAlgorithmError} if algorithm was bad or incompatible with
  243. * the given key.
  244. * @throws {sshpk.KeyParseError} if key was bad.
  245. * @throws {MissingHeaderError} if a header to be signed was specified but
  246. * was not present.
  247. */
  248. signRequest: function signRequest(request, options) {
  249. assert.object(request, 'request');
  250. assert.object(options, 'options');
  251. assert.optionalString(options.algorithm, 'options.algorithm');
  252. assert.string(options.keyId, 'options.keyId');
  253. assert.optionalArrayOfString(options.headers, 'options.headers');
  254. assert.optionalString(options.httpVersion, 'options.httpVersion');
  255. if (!request.getHeader('Date'))
  256. request.setHeader('Date', jsprim.rfc1123(new Date()));
  257. if (!options.headers)
  258. options.headers = ['date'];
  259. if (!options.httpVersion)
  260. options.httpVersion = '1.1';
  261. var alg = [];
  262. if (options.algorithm) {
  263. options.algorithm = options.algorithm.toLowerCase();
  264. alg = validateAlgorithm(options.algorithm);
  265. }
  266. var i;
  267. var stringToSign = '';
  268. for (i = 0; i < options.headers.length; i++) {
  269. if (typeof (options.headers[i]) !== 'string')
  270. throw new TypeError('options.headers must be an array of Strings');
  271. var h = options.headers[i].toLowerCase();
  272. if (h === 'request-line') {
  273. if (!options.strict) {
  274. /**
  275. * We allow headers from the older spec drafts if strict parsing isn't
  276. * specified in options.
  277. */
  278. stringToSign +=
  279. request.method + ' ' + request.path + ' HTTP/' +
  280. options.httpVersion;
  281. } else {
  282. /* Strict parsing doesn't allow older draft headers. */
  283. throw (new StrictParsingError('request-line is not a valid header ' +
  284. 'with strict parsing enabled.'));
  285. }
  286. } else if (h === '(request-target)') {
  287. stringToSign +=
  288. '(request-target): ' + request.method.toLowerCase() + ' ' +
  289. request.path;
  290. } else {
  291. var value = request.getHeader(h);
  292. if (value === undefined || value === '') {
  293. throw new MissingHeaderError(h + ' was not in the request');
  294. }
  295. stringToSign += h + ': ' + value;
  296. }
  297. if ((i + 1) < options.headers.length)
  298. stringToSign += '\n';
  299. }
  300. /* This is just for unit tests. */
  301. if (request.hasOwnProperty('_stringToSign')) {
  302. request._stringToSign = stringToSign;
  303. }
  304. var signature;
  305. if (alg[0] === 'hmac') {
  306. if (typeof (options.key) !== 'string' && !Buffer.isBuffer(options.key))
  307. throw (new TypeError('options.key must be a string or Buffer'));
  308. var hmac = crypto.createHmac(alg[1].toUpperCase(), options.key);
  309. hmac.update(stringToSign);
  310. signature = hmac.digest('base64');
  311. } else {
  312. var key = options.key;
  313. if (typeof (key) === 'string' || Buffer.isBuffer(key))
  314. key = sshpk.parsePrivateKey(options.key);
  315. assert.ok(sshpk.PrivateKey.isPrivateKey(key, [1, 2]),
  316. 'options.key must be a sshpk.PrivateKey');
  317. if (!PK_ALGOS[key.type]) {
  318. throw (new InvalidAlgorithmError(key.type.toUpperCase() + ' type ' +
  319. 'keys are not supported'));
  320. }
  321. if (alg[0] !== undefined && key.type !== alg[0]) {
  322. throw (new InvalidAlgorithmError('options.key must be a ' +
  323. alg[0].toUpperCase() + ' key, was given a ' +
  324. key.type.toUpperCase() + ' key instead'));
  325. }
  326. var signer = key.createSign(alg[1]);
  327. signer.update(stringToSign);
  328. var sigObj = signer.sign();
  329. if (!HASH_ALGOS[sigObj.hashAlgorithm]) {
  330. throw (new InvalidAlgorithmError(sigObj.hashAlgorithm.toUpperCase() +
  331. ' is not a supported hash algorithm'));
  332. }
  333. options.algorithm = key.type + '-' + sigObj.hashAlgorithm;
  334. signature = sigObj.toString();
  335. assert.notStrictEqual(signature, '', 'empty signature produced');
  336. }
  337. var authzHeaderName = options.authorizationHeaderName || 'Authorization';
  338. request.setHeader(authzHeaderName, sprintf(AUTHZ_FMT,
  339. options.keyId,
  340. options.algorithm,
  341. options.headers.join(' '),
  342. signature));
  343. return true;
  344. }
  345. };