diff --git a/src/cleartext.js b/src/cleartext.js index 4c1604ae..a5a96b28 100644 --- a/src/cleartext.js +++ b/src/cleartext.js @@ -39,7 +39,7 @@ var config = require('./config'), function CleartextMessage(text, packetlist) { if (!(this instanceof CleartextMessage)) { - return new CleartextMessage(packetlist); + return new CleartextMessage(text, packetlist); } // normalize EOL to canonical form this.text = text.replace(/\r/g, '').replace(/[\t ]+\n/g, "\n").replace(/\n/g,"\r\n"); @@ -142,9 +142,55 @@ function readArmored(armoredText) { } var packetlist = new packet.List(); packetlist.read(input.data); + verifyHeaders(input.headers, packetlist); var newMessage = new CleartextMessage(input.text, packetlist); return newMessage; } +/** + * Compare hash algorithm specified in the armor header with signatures + * @private + * @param {Array} headers Armor headers + * @param {module:packet/packetlist} packetlist The packetlist with signature packets + */ +function verifyHeaders(headers, packetlist) { + var checkHashAlgos = function(hashAlgos) { + for (var i = 0; i < packetlist.length; i++) { + if (packetlist[i].tag === enums.packet.signature && + !hashAlgos.some(function(algo) { + return packetlist[i].hashAlgorithm === algo; + })) { + return false; + } + } + return true; + } + var oneHeader = null; + var hashAlgos = []; + for (var i = 0; i < headers.length; i++) { + oneHeader = headers[i].match(/Hash: (.+)/); // get header value + if (oneHeader) { + oneHeader = oneHeader[1].replace(/\s/g, ''); // remove whitespace + oneHeader = oneHeader.split(','); + oneHeader = oneHeader.map(function(hash) { + hash = hash.toLowerCase(); + try { + return enums.write(enums.hash, hash); + } catch (e) { + throw new Error('Unknown hash algorithm in armor header: ' + hash); + } + }); + hashAlgos = hashAlgos.concat(oneHeader); + } else { + throw new Error('Only "Hash" header allowed in cleartext signed message'); + } + } + if (!hashAlgos.length && !checkHashAlgos([enums.hash.md5])) { + throw new Error('If no "Hash" header in cleartext signed message, then only MD5 signatures allowed'); + } else if (!checkHashAlgos(hashAlgos)) { + throw new Error('Hash algorithm mismatch in armor header and signature'); + } +} + exports.CleartextMessage = CleartextMessage; exports.readArmored = readArmored; diff --git a/src/encoding/armor.js b/src/encoding/armor.js index 3164d952..5de8771d 100644 --- a/src/encoding/armor.js +++ b/src/encoding/armor.js @@ -207,8 +207,8 @@ function createcrc24(input) { * and an attribute "body" containing the body. */ function splitHeaders(text) { - var reEmptyLine = /^[\t ]*\n/m; - var headers = ""; + var reEmptyLine = /^\s*\n/m; + var headers = ''; var body = text; var matchResult = reEmptyLine.exec(text); @@ -216,11 +216,31 @@ function splitHeaders(text) { if (matchResult !== null) { headers = text.slice(0, matchResult.index); body = text.slice(matchResult.index + matchResult[0].length); + } else { + throw new Error('Mandatory blank line missing between armor headers and armor data'); } + headers = headers.split('\n'); + // remove empty entry + headers.pop(); + return { headers: headers, body: body }; } +/** + * Verify armored headers. RFC4880, section 6.3: "OpenPGP should consider improperly formatted + * Armor Headers to be corruption of the ASCII Armor." + * @private + * @param {Array} headers Armor headers + */ +function verifyHeaders(headers) { + for (var i = 0; i < headers.length; i++) { + if (!headers[i].match(/^(Version|Comment|MessageID|Hash|Charset): .+$/)) { + throw new Error('Improperly formatted armor header: ' + headers[i]);; + } + } +} + /** * Splits a message into two parts, the body and the checksum. This is an internal function * @param {String} text OpenPGP armored message part @@ -280,19 +300,22 @@ function dearmor(text) { result = { data: base64.decode(msg_sum.body), + headers: msg.headers, type: type }; checksum = msg_sum.checksum; } else { - // Reverse dash-escaping for msg and remove trailing whitespace at end of line + // Reverse dash-escaping for msg and remove trailing whitespace (0x20) and tabs (0x09) at end of line msg = splitHeaders(splittext[indexBase].replace(/^- /mg, '').replace(/[\t ]+\n/g, "\n")); var sig = splitHeaders(splittext[indexBase + 1].replace(/^- /mg, '')); + verifyHeaders(sig.headers); var sig_sum = splitChecksum(sig.body); result = { text: msg.body.replace(/\n$/, '').replace(/\n/g, "\r\n"), data: base64.decode(sig_sum.body), + headers: msg.headers, type: type }; @@ -304,9 +327,11 @@ function dearmor(text) { checksum + "' should be '" + getCheckSum(result) + "'"); - } else { - return result; } + + verifyHeaders(result.headers); + + return result; } diff --git a/test/general/armor.js b/test/general/armor.js new file mode 100644 index 00000000..7bdfb695 --- /dev/null +++ b/test/general/armor.js @@ -0,0 +1,117 @@ +'use strict'; + +var openpgp = typeof window != 'undefined' && window.openpgp ? window.openpgp : require('../../src/index'); + +var chai = require('chai'), + expect = chai.expect; + + +describe("ASCII armor", function() { + + function getArmor(headers) { + return ['-----BEGIN PGP SIGNED MESSAGE-----'] + .concat(headers) + .concat( + ['', + 'sign this', + '-----BEGIN PGP SIGNATURE-----', + 'Version: GnuPG v2.0.22 (GNU/Linux)', + '', + 'iJwEAQECAAYFAlMrPj0ACgkQ4IT3RGwgLJfYkQQAgHMQieazCVdfGAfzQM69Egm5', + 'HhcQszODD898wpoGCHgiNdNo1+5nujQAtXnkcxM+Vf7onfbTvUqut/siyO3fzqhK', + 'LQ9DiQUwJMBE8nOwVR7Mpc4kLNngMTNaHAjZaVaDpTCrklPY+TPHIZnu0B6Ur+6t', + 'skTzzVXIxMYw8ihbHfk=', + '=e/eA', + '-----END PGP SIGNATURE-----'] + ).join('\n'); + } + + it('Parse cleartext signed message', function () { + var msg = getArmor(['Hash: SHA1']); + msg = openpgp.cleartext.readArmored(msg); + expect(msg).to.be.an.instanceof(openpgp.cleartext.CleartextMessage); + }); + + it('Exception if mismatch in armor header and signature', function () { + var msg = getArmor(['Hash: SHA256']); + msg = openpgp.cleartext.readArmored.bind(null, msg); + expect(msg).to.throw(Error, /Hash algorithm mismatch in armor header and signature/); + }); + + it('Exception if no header and non-MD5 signature', function () { + var msg = getArmor(null); + msg = openpgp.cleartext.readArmored.bind(null, msg); + expect(msg).to.throw(Error, /If no "Hash" header in cleartext signed message, then only MD5 signatures allowed/); + }); + + it('Exception if unknown hash algorithm', function () { + var msg = getArmor(['Hash: LAV750']); + msg = openpgp.cleartext.readArmored.bind(null, msg); + expect(msg).to.throw(Error, /Unknown hash algorithm in armor header/); + }); + + it('Multiple hash values', function () { + var msg = getArmor(['Hash: SHA1, SHA256']); + msg = openpgp.cleartext.readArmored(msg); + expect(msg).to.be.an.instanceof(openpgp.cleartext.CleartextMessage); + }); + + it('Multiple hash header lines', function () { + var msg = getArmor(['Hash: SHA1', 'Hash: SHA256']); + msg = openpgp.cleartext.readArmored(msg); + expect(msg).to.be.an.instanceof(openpgp.cleartext.CleartextMessage); + }); + + it('Non-hash header line throws exception', function () { + var msg = getArmor(['Hash: SHA1', 'Comment: could be anything']); + msg = openpgp.cleartext.readArmored.bind(null, msg); + expect(msg).to.throw(Error, /Only "Hash" header allowed in cleartext signed message/); + }); + + it('Multiple wrong hash values', function () { + var msg = getArmor(['Hash: SHA512, SHA256']); + msg = openpgp.cleartext.readArmored.bind(null, msg); + expect(msg).to.throw(Error, /Hash algorithm mismatch in armor header and signature/); + }); + + it('Multiple wrong hash values', function () { + var msg = getArmor(['Hash: SHA512, SHA256']); + msg = openpgp.cleartext.readArmored.bind(null, msg); + expect(msg).to.throw(Error, /Hash algorithm mismatch in armor header and signature/); + }); + + it('Filter whitespace in blank line', function () { + var msg = + ['-----BEGIN PGP SIGNED MESSAGE-----', + 'Hash: SHA1', + '\u000b\u00a0', + 'sign this', + '-----BEGIN PGP SIGNATURE-----', + 'Version: GnuPG v2.0.22 (GNU/Linux)', + '', + 'iJwEAQECAAYFAlMrPj0ACgkQ4IT3RGwgLJfYkQQAgHMQieazCVdfGAfzQM69Egm5', + 'HhcQszODD898wpoGCHgiNdNo1+5nujQAtXnkcxM+Vf7onfbTvUqut/siyO3fzqhK', + 'LQ9DiQUwJMBE8nOwVR7Mpc4kLNngMTNaHAjZaVaDpTCrklPY+TPHIZnu0B6Ur+6t', + 'skTzzVXIxMYw8ihbHfk=', + '=e/eA', + '-----END PGP SIGNATURE-----'].join('\n'); + + msg = openpgp.cleartext.readArmored(msg); + expect(msg).to.be.an.instanceof(openpgp.cleartext.CleartextMessage); + }); + + it('Exception if improperly formatted armor header', function () { + var msg = getArmor(['Hash:SHA256']); + msg = openpgp.cleartext.readArmored.bind(null, msg); + expect(msg).to.throw(Error, /Improperly formatted armor header/); + msg = getArmor(['