Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

Add dkim_sign plugin

  • Loading branch information...
commit 3f538d7fa2796263cc6b057f8b9656d77df87a04 1 parent 2443b6e
@smfreegard smfreegard authored
View
4 config/dkim_sign.ini
@@ -0,0 +1,4 @@
+disabled = true
+selector = mail
+domain = example.com
+headers_to_sign = From, Sender, Reply-To, Subject, Date, Message-ID, To, Cc, MIME-Version
View
72 docs/plugins/dkim_sign.md
@@ -0,0 +1,72 @@
+dkim_sign
+=========
+
+This plugin implements the DKIM Core specification found at dkimcore.org
+
+DKIM Core is a simplified subset of DKIM which is easier to understand
+and deploy, yet provides all the same delivery advantages as DKIM.
+
+This plugin can only *sign* outbound messages. It does not validate
+DKIM signatures.
+
+Getting Started
+---------------
+
+First, generate an RSA key pair in your Haraka config directory by
+running the following commands:
+
+ cd /path/to/haraka/config
+ openssl genrsa -out dkim.private.key 1024
+ openssl rsa -in dkim.private.key -pubout > dkim.public.key
+
+A selector is used to identify the keys used to attach a token to a
+piece of email. It does appear in the header of the email sent, but
+isn’t otherwise visible or meaningful to the final recipient. Any time
+you generate a new key pair you need to choose a new selector.
+
+A selector is a string of no more than 63 lower-case alphanumeric
+characters (a-z or 0-9) followed by a period “.”, followed by another
+string of no more than 63 lower-case alphanumeric characters.
+
+Next you have to publish the public key as a DNS TXT record for your
+domain by concatenating the selector, the literal string ._domainkey.
+and your domain name. e.g. mail._domainkey.example.com
+
+The content of the TXT record can be created by concatenating the
+literal string “v=DKIM1;t=s;n=core;p=” and the public key excluding
+the ---BEGIN and ---END lines and wrapping the key into a single line.
+
+See the key wizard at http://dkimtools.org/tools
+
+Configuation
+------------
+
+This plugin uses the configuration dkim_sign.ini in INI format.
+All configuration should appear within the 'main' block and is
+checked for updates on every run.
+
+- disabled = [ 1 | true | yes ] (OPTIONAL)
+
+ Set this to disable DKIM signing
+
+- selector = <name> (REQUIRED)
+
+ Set this to the selector name published in DNS under the
+ _domainkey sub-domain of the domain referenced below.
+
+- domain = <name> (REQUIRED)
+
+ Set this to the domain name that will be used to sign the
+ message. The DNS TXT entry for:
+
+ <selector>._domainkey.<domain>
+
+ MUST be present, otherwise remote systems will not be able
+ to validate the signature applied to the message.
+
+- headers_to_sign = <list of headers> (REQUIRED)
+
+ Set this to the list of headers that should be signed.
+ This is to prevent any tampering of the specified headers.
+ The 'From' header is required to be present by the RFC and
+ will be added if it is missing.
View
103 plugins/dkim_sign.js
@@ -0,0 +1,103 @@
+// dkim_signer
+// Implements DKIM core as per www.dkimcore.org
+
+var crypto = require('crypto');
+
+exports.hook_queue_outbound = function (next, connection) {
+ var transaction = connection.transaction;
+ var config = this.config.get('dkim_sign.ini');
+ var private_key = this.config.get('dkim.private.key','data').join("\n");
+ var headers_to_sign = [];
+
+ // Make sure we have all the relevant configuration
+ if (!private_key) {
+ connection.logerror(this, 'skipped: missing dkim.private.key');
+ return next();
+ }
+ if (config.main.disabled && /(?:1|true|y[es])/i.test(config.main.disabled)) {
+ connection.logerror(this, 'skipped: disabled');
+ return next();
+ }
+ if (!config.main.selector) {
+ connection.logerror(this, 'skipped: missing selector');
+ return next();
+ }
+ if (!config.main.domain) {
+ connection.logerror(this, 'skipped: missing domain');
+ return next();
+ }
+ if (config.main.headers_to_sign) {
+ headers_to_sign = config.main.headers_to_sign
+ .toLowerCase()
+ .replace(/\s+/g,'')
+ .split(/[,;:]/);
+ }
+ // From MUST be present
+ if (headers_to_sign.indexOf('from') === -1) {
+ headers_to_sign.push('from');
+ }
+
+ /*
+ ** BODY (simple canonicalization)
+ */
+ var data_marker = 0;
+ var found_body = false;
+ var buffer = "";
+ var hash = crypto.createHash('SHA256');
+ while (data_marker < transaction.data_lines.length) {
+ var line = transaction.data_lines[data_marker];
+ line = line.replace(/\r?\n/g, "\r\n");
+ // Skip until we find the end-of-headers
+ if (!found_body) {
+ if (line === "\r\n") {
+ found_body = true;
+ }
+ data_marker++;
+ continue;
+ }
+ if (line === "\r\n") {
+ // Buffer any empty lines so we can discard
+ // and trailing CRLFs at the end of the message.
+ buffer += line;
+ }
+ else {
+ if (buffer) {
+ hash.update(buffer);
+ buffer = "";
+ }
+ hash.update(line, 'ascii');
+ }
+ data_marker++;
+ }
+ // Add trailing CRLF if it was missing from the last line
+ if (line.slice(-2) !== "\r\n") {
+ hash.update("\r\n", 'ascii');
+ }
+ var bodyhash = hash.digest('base64');
+
+ /*
+ ** HEADERS (relaxed canonicaliztion)
+ */
+ var headers = [];
+ var signer = crypto.createSign('RSA-SHA256');
+ for (var i=0; i < headers_to_sign.length; i++ ) {
+ var head = transaction.header.get(headers_to_sign[i]);
+ if (head) {
+ head = head.replace(/\r?\n/gm, '');
+ head = head.replace(/\s+/gm, ' ');
+ head = head.replace(/\s+$/gm, '');
+ signer.update(headers_to_sign[i] + ':' + head + "\r\n");
+ headers.push(headers_to_sign[i]);
+ }
+ };
+ var dkim_header = 'v=1;a=rsa-sha256;bh=' + bodyhash +
+ ';c=relaxed/simple;d=fsl.com;h=' + headers.join(':') +
+ ';s=mail;b=';
+ signer.update('dkim-signature:' + dkim_header);
+ var signature = signer.sign(private_key, 'base64');
+ dkim_header += signature;
+ transaction.add_header('DKIM-Signature', dkim_header);
+ connection.loginfo(this, 'added DKIM signature');
+
+ return next();
+}
Please sign in to comment.
Something went wrong with that request. Please try again.