Repository: haraka/Haraka (JavaScript)

Issue:
Invalid DKIM signature with empty email
### system info
Haraka | Haraka.js — Version: 2.8.18
 --- | :--- 
Node | v8.11.1
OS | Linux e3bb5005bb6e 4.10.0-041000-generic #201702191831 SMP Sun Feb 19 23:33:19 UTC 2017 x86_64 GNU/Linux
openssl | OpenSSL 1.1.0f  25 May 2017

### Steps to reproduce
Send email with empty body AND empty subject to something like mail-tester.com



Discussion:
What do you mean by invalid signature? It seems like there is no signature generated in that case.
Nope, tested right now. Invalid signature is generated. I guess thats something like "null" js stuff
How do you test this? My gmail does not complain.
> Steps to reproduce
Send email with empty body AND empty subject to something like mail-tester.com



Patch:
diff --git a/Changes.md b/Changes.md
index 5790e33b5..b0acbec3e 100644
--- a/Changes.md
+++ b/Changes.md
@@ -15,6 +15,7 @@
 ### Fixes
 
 * mf.resolvable: reduce timeout by one second (so < plugin.timeout) #2544
+* invalid DKIM when empty body #2410
 
 ## 2.8.23 - Nov 18, 2018
 
diff --git a/plugins/dkim_sign.js b/plugins/dkim_sign.js
index 8864c6e4d..cdd06d34f 100644
--- a/plugins/dkim_sign.js
+++ b/plugins/dkim_sign.js
@@ -25,6 +25,7 @@ class DKIMSignStream extends Stream {
         this.hash = crypto.createHash('SHA256');
         this.line_buffer = { ar: [], len: 0 };
         this.signer = crypto.createSign('RSA-SHA256');
+        this.body_found = false;
     }
 
     write (buf) {
@@ -69,6 +70,7 @@ class DKIMSignStream extends Stream {
                     this.hash.update(lb);
                 }
                 this.hash.update(line);
+                this.body_found = true;
             }
         }
         if (buf.length) {
@@ -90,6 +92,10 @@ class DKIMSignStream extends Stream {
             this.buffer = { ar: [], len: 0 };
         }
 
+        if (!this.body_found) {
+            this.hash.update(Buffer.from("\r\n"));
+        }
+
         const bodyhash = this.hash.digest('base64');
 
         /*


Test patch:
diff --git a/tests/plugins/dkim_signer.js b/tests/plugins/dkim_signer.js
new file mode 100644
index 000000000..929b2364d
--- /dev/null
+++ b/tests/plugins/dkim_signer.js
@@ -0,0 +1,107 @@
+'use strict';
+
+const DKIMSignStream = require('../../plugins/dkim_sign').DKIMSignStream;
+const Header = require('../../mailheader').Header;
+
+const privateKey = `-----BEGIN RSA PRIVATE KEY-----
+MIICXwIBAAKBgQDwIRP/UC3SBsEmGqZ9ZJW3/DkMoGeLnQg1fWn7/zYtIxN2SnFC
+jxOCKG9v3b4jYfcTNh5ijSsq631uBItLa7od+v/RtdC2UzJ1lWT947qR+Rcac2gb
+to/NMqJ0fzfVjH4OuKhitdY9tf6mcwGjaNBcWToIMmPSPDdQPNUYckcQ2QIDAQAB
+AoGBALmn+XwWk7akvkUlqb+dOxyLB9i5VBVfje89Teolwc9YJT36BGN/l4e0l6QX
+/1//6DWUTB3KI6wFcm7TWJcxbS0tcKZX7FsJvUz1SbQnkS54DJck1EZO/BLa5ckJ
+gAYIaqlA9C0ZwM6i58lLlPadX/rtHb7pWzeNcZHjKrjM461ZAkEA+itss2nRlmyO
+n1/5yDyCluST4dQfO8kAB3toSEVc7DeFeDhnC1mZdjASZNvdHS4gbLIA1hUGEF9m
+3hKsGUMMPwJBAPW5v/U+AWTADFCS22t72NUurgzeAbzb1HWMqO4y4+9Hpjk5wvL/
+eVYizyuce3/fGke7aRYw/ADKygMJdW8H/OcCQQDz5OQb4j2QDpPZc0Nc4QlbvMsj
+7p7otWRO5xRa6SzXqqV3+F0VpqvDmshEBkoCydaYwc2o6WQ5EBmExeV8124XAkEA
+qZzGsIxVP+sEVRWZmW6KNFSdVUpk3qzK0Tz/WjQMe5z0UunY9Ax9/4PVhp/j61bf
+eAYXunajbBSOLlx4D+TunwJBANkPI5S9iylsbLs6NkaMHV6k5ioHBBmgCak95JGX
+GMot/L2x0IYyMLAz6oLWh2hm7zwtb0CgOrPo1ke44hFYnfc=
+-----END RSA PRIVATE KEY-----`;
+
+/*
+Body hash can be simply checked by:
+
+$ echo -e -n 'Hello world!\r\n'| openssl dgst -binary -sha256 | openssl base64
+z6TUz85EdYrACGMHYgZhJGvVy5oQI0dooVMKa2ZT7c4=
+$ echo -e -n '\r\n' | openssl dgst -binary -sha256 | openssl base64
+frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=
+*/
+
+function getValueFromDKIM (dkim_header, key) {
+    const kv = dkim_header.split(';');
+    for (let i = 0, len = kv.length; i < len; i++) {
+        const arr = kv[i].match(/^([^=]+)=(.*)$/);
+        if (arr[1] === key) {
+            return arr[2];
+        }
+    }
+    throw `Key ${key} not found at ${dkim_header}`;
+}
+
+exports.sign = {
+    'body hash simple': function (test) {
+        // took from RFC
+        test.expect(1);
+
+        const email = 'Ignored: header\r\n\r\nHi.\r\n\r\nWe lost the game. Are you hungry yet?\r\n\r\nJoe.\r\n';
+
+        const header = new Header();
+        header.parse(['Ignored: header']);
+        const signer = new DKIMSignStream('selector', 'haraka.top', privateKey, [], header,
+            function (n, dkim) {
+                test.equal(getValueFromDKIM(dkim, 'bh'), '2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=');
+                test.done();
+            }
+        );
+        signer.write(Buffer.from(email));
+        signer.end();
+    },
+    'empty body hash simple': function (test) {
+        test.expect(1);
+
+        const email = 'Ignored: header\r\n\r\n';
+
+        const header = new Header();
+        header.parse(['Ignored: header']);
+        const signer = new DKIMSignStream('selector', 'haraka.top', privateKey, [], header,
+            function (n, dkim) {
+                test.equal(getValueFromDKIM(dkim, 'bh'), 'frcCV1k9oG9oKj3dpUqdJg1PxRT2RSN/XKdLCPjaYaY=');
+                test.done();
+            }
+        );
+        signer.write(Buffer.from(email));
+        signer.end();
+    },
+    'body hash simple, two writes': function (test) {
+        test.expect(1);
+
+        const header = new Header();
+        header.parse(['Ignored: header']);
+        const signer = new DKIMSignStream('selector', 'haraka.top', privateKey, [], header,
+            function (n, dkim) {
+                test.equal(getValueFromDKIM(dkim, 'bh'), '2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=');
+                test.done();
+            }
+        );
+        signer.write(Buffer.from('Ignored: header\r\n\r\nHi.\r\n\r\nWe lost the game. '));
+        signer.write(Buffer.from('Are you hungry yet?\r\n\r\nJoe.\r\n'));
+        signer.end();
+    },
+    'body hash simple, empty lines': function (test) {
+        test.expect(1);
+
+        const email = 'Ignored: header\r\n\r\nHi.\r\n\r\nWe lost the game. Are you hungry yet?\r\n\r\nJoe.\r\n\r\n\r\n';
+
+        const header = new Header();
+        header.parse(['Ignored: header']);
+        const signer = new DKIMSignStream('selector', 'haraka.top', privateKey, [], header,
+            function (n, dkim) {
+                test.equal(getValueFromDKIM(dkim, 'bh'), '2jUSOH9NhtVGCQWNr9BrIAPreKQjO6Sn7XIkfJVOzv8=');
+                test.done();
+            }
+        );
+        signer.write(Buffer.from(email));
+        signer.end();
+    },
+}