sign.js 3.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117
  1. 'use strict';
  2. const punycode = require('punycode');
  3. const mimeFuncs = require('../mime-funcs');
  4. const crypto = require('crypto');
  5. /**
  6. * Returns DKIM signature header line
  7. *
  8. * @param {Object} headers Parsed headers object from MessageParser
  9. * @param {String} bodyHash Base64 encoded hash of the message
  10. * @param {Object} options DKIM options
  11. * @param {String} options.domainName Domain name to be signed for
  12. * @param {String} options.keySelector DKIM key selector to use
  13. * @param {String} options.privateKey DKIM private key to use
  14. * @return {String} Complete header line
  15. */
  16. module.exports = (headers, hashAlgo, bodyHash, options) => {
  17. options = options || {};
  18. // all listed fields from RFC4871 #5.5
  19. let defaultFieldNames =
  20. 'From:Sender:Reply-To:Subject:Date:Message-ID:To:' +
  21. 'Cc:MIME-Version:Content-Type:Content-Transfer-Encoding:Content-ID:' +
  22. 'Content-Description:Resent-Date:Resent-From:Resent-Sender:' +
  23. 'Resent-To:Resent-Cc:Resent-Message-ID:In-Reply-To:References:' +
  24. 'List-Id:List-Help:List-Unsubscribe:List-Subscribe:List-Post:' +
  25. 'List-Owner:List-Archive';
  26. let fieldNames = options.headerFieldNames || defaultFieldNames;
  27. let canonicalizedHeaderData = relaxedHeaders(headers, fieldNames, options.skipFields);
  28. let dkimHeader = generateDKIMHeader(options.domainName, options.keySelector, canonicalizedHeaderData.fieldNames, hashAlgo, bodyHash);
  29. let signer, signature;
  30. canonicalizedHeaderData.headers += 'dkim-signature:' + relaxedHeaderLine(dkimHeader);
  31. signer = crypto.createSign(('rsa-' + hashAlgo).toUpperCase());
  32. signer.update(canonicalizedHeaderData.headers);
  33. try {
  34. signature = signer.sign(options.privateKey, 'base64');
  35. } catch (E) {
  36. return false;
  37. }
  38. return dkimHeader + signature.replace(/(^.{73}|.{75}(?!\r?\n|\r))/g, '$&\r\n ').trim();
  39. };
  40. module.exports.relaxedHeaders = relaxedHeaders;
  41. function generateDKIMHeader(domainName, keySelector, fieldNames, hashAlgo, bodyHash) {
  42. let dkim = [
  43. 'v=1',
  44. 'a=rsa-' + hashAlgo,
  45. 'c=relaxed/relaxed',
  46. 'd=' + punycode.toASCII(domainName),
  47. 'q=dns/txt',
  48. 's=' + keySelector,
  49. 'bh=' + bodyHash,
  50. 'h=' + fieldNames
  51. ].join('; ');
  52. return mimeFuncs.foldLines('DKIM-Signature: ' + dkim, 76) + ';\r\n b=';
  53. }
  54. function relaxedHeaders(headers, fieldNames, skipFields) {
  55. let includedFields = new Set();
  56. let skip = new Set();
  57. let headerFields = new Map();
  58. (skipFields || '')
  59. .toLowerCase()
  60. .split(':')
  61. .forEach(field => {
  62. skip.add(field.trim());
  63. });
  64. (fieldNames || '')
  65. .toLowerCase()
  66. .split(':')
  67. .filter(field => !skip.has(field.trim()))
  68. .forEach(field => {
  69. includedFields.add(field.trim());
  70. });
  71. for (let i = headers.length - 1; i >= 0; i--) {
  72. let line = headers[i];
  73. // only include the first value from bottom to top
  74. if (includedFields.has(line.key) && !headerFields.has(line.key)) {
  75. headerFields.set(line.key, relaxedHeaderLine(line.line));
  76. }
  77. }
  78. let headersList = [];
  79. let fields = [];
  80. includedFields.forEach(field => {
  81. if (headerFields.has(field)) {
  82. fields.push(field);
  83. headersList.push(field + ':' + headerFields.get(field));
  84. }
  85. });
  86. return {
  87. headers: headersList.join('\r\n') + '\r\n',
  88. fieldNames: fields.join(':')
  89. };
  90. }
  91. function relaxedHeaderLine(line) {
  92. return line
  93. .substr(line.indexOf(':') + 1)
  94. .replace(/\r?\n/g, '')
  95. .replace(/\s+/g, ' ')
  96. .trim();
  97. }