Accept cleartext messages without hash header

The latest version of the crypto refresh (i.e., !313, !314) specifies that
the  "Hash" header is deprecated, and that an implementation that is verifying
a cleartext signed message MUST ignore this header.
However, we go against this directive, and keep the checks in place to avoid
arbitrary injection of text as part of the "Hash" header payload.
We also mandate that if the hash header is present, the declared
algorithm matches the signature algorithm. This is again to avoid
a spoofing attack where e.g. a SHA1 signature is presented as
using SHA512.
Related CVEs: CVE-2019-11841, CVE-2023-41037.

This commit does not change the writing part of cleartext messages.

# Conflicts:
#	src/cleartext.js
This commit is contained in:
Lukas Burkhalter 2023-06-01 16:24:38 +02:00 committed by larabr
parent af96628855
commit 1ddf4e151c
3 changed files with 57 additions and 42 deletions

View File

@ -171,30 +171,27 @@ function verifyHeaders(headers, packetlist) {
return true;
};
let oneHeader = null;
let hashAlgos = [];
headers.forEach(function(header) {
oneHeader = header.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);
const hashAlgos = [];
headers.forEach(header => {
const hashHeader = header.match(/^Hash: (.+)$/); // get header value
if (hashHeader) {
const parsedHashIDs = hashHeader[1]
.replace(/\s/g, '') // remove whitespace
.split(',')
.map(hashName => {
try {
return enums.write(enums.hash, hashName.toLowerCase());
} catch (e) {
throw new Error('Unknown hash algorithm in armor header: ' + hashName.toLowerCase());
}
});
hashAlgos.push(...parsedHashIDs);
} 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 (hashAlgos.length && !checkHashAlgos(hashAlgos)) {
if (hashAlgos.length && !checkHashAlgos(hashAlgos)) {
throw new Error('Hash algorithm mismatch in armor header and signature');
}
}

View File

@ -36,12 +36,6 @@ export default () => describe('ASCII armor', function() {
await expect(msg).to.be.rejectedWith(Error, /Hash algorithm mismatch in armor header and signature/);
});
it('Exception if no header and non-MD5 signature', async function () {
let msg = getArmor(null);
msg = openpgp.readCleartextMessage({ cleartextMessage: msg });
await expect(msg).to.be.rejectedWith(Error, /If no "Hash" header in cleartext signed message, then only MD5 signatures allowed/);
});
it('Exception if unknown hash algorithm', async function () {
let msg = getArmor(['Hash: LAV750']);
msg = openpgp.readCleartextMessage({ cleartextMessage: msg });
@ -66,18 +60,6 @@ export default () => describe('ASCII armor', function() {
await expect(msg).to.be.rejectedWith(Error, /Only "Hash" header allowed in cleartext signed message/);
});
it('Multiple wrong hash values', async function () {
let msg = getArmor(['Hash: SHA512, SHA256']);
msg = openpgp.readCleartextMessage({ cleartextMessage: msg });
await expect(msg).to.be.rejectedWith(Error, /Hash algorithm mismatch in armor header and signature/);
});
it('Multiple wrong hash values', async function () {
let msg = getArmor(['Hash: SHA512, SHA256']);
msg = openpgp.readCleartextMessage({ cleartextMessage: msg });
await expect(msg).to.be.rejectedWith(Error, /Hash algorithm mismatch in armor header and signature/);
});
it('Filter whitespace in blank line', async function () {
let msg = [
'-----BEGIN PGP SIGNED MESSAGE-----',
@ -99,11 +81,6 @@ export default () => describe('ASCII armor', function() {
expect(msg).to.be.an.instanceof(openpgp.CleartextMessage);
});
it('Exception if header is not Hash in cleartext signed message', async function () {
const msg = openpgp.readCleartextMessage({ cleartextMessage: getArmor(['Ha sh: SHA256']) });
await expect(msg).to.be.rejectedWith(Error, /Only "Hash" header allowed in cleartext signed message/);
});
it('Ignore improperly formatted armor header', async function () {
await Promise.all(['Space : trailing', 'Space :switched', ': empty', 'none', 'Space:missing'].map(async function (invalidHeader) {
expect(await openpgp.readCleartextMessage({ cleartextMessage: getArmor(['Hash: SHA1'], [invalidHeader]) })).to.be.an.instanceof(openpgp.CleartextMessage);

View File

@ -1365,6 +1365,47 @@ DAAKCRDyMVUMT0fjjlnQAQDFHUs6TIcxrNTtEZFjUFm1M0PJ1Dng/cDW4xN80fsn
expect(notations[1].critical).to.be.false;
});
it('Verify v6 cleartext signed message with openpgp.verify', async function() {
// test vector from https://www.ietf.org/archive/id/draft-ietf-openpgp-crypto-refresh-08.html#name-sample-v6-certificate-trans
const cleartextMessage = `-----BEGIN PGP SIGNED MESSAGE-----
What we need from the grocery store:
- - tofu
- - vegetables
- - noodles
-----BEGIN PGP SIGNATURE-----
wpgGARsKAAAAKQWCY5ijYyIhBssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce6
2azJAAAAAGk2IHZJX1AhiJD39eLuPBgiUU9wUA9VHYblySHkBONKU/usJ9BvuAqo
/FvLFuGWMbKAdA+epq7V4HOtAPlBWmU8QOd6aud+aSunHQaaEJ+iTFjP2OMW0KBr
NK2ay45cX1IVAQ==
-----END PGP SIGNATURE-----`;
const publicKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PUBLIC KEY BLOCK-----
xioGY4d/4xsAAAAg+U2nu0jWCmHlZ3BqZYfQMxmZu52JGggkLq2EVD34laPCsQYf
GwoAAABCBYJjh3/jAwsJBwUVCg4IDAIWAAKbAwIeCSIhBssYbE8GCaaX5NUt+mxy
KwwfHifBilZwj2Ul7Ce62azJBScJAgcCAAAAAK0oIBA+LX0ifsDm185Ecds2v8lw
gyU2kCcUmKfvBXbAf6rhRYWzuQOwEn7E/aLwIwRaLsdry0+VcallHhSu4RN6HWaE
QsiPlR4zxP/TP7mhfVEe7XWPxtnMUMtf15OyA51YBM4qBmOHf+MZAAAAIIaTJINn
+eUBXbki+PSAld2nhJh/LVmFsS+60WyvXkQ1wpsGGBsKAAAALAWCY4d/4wKbDCIh
BssYbE8GCaaX5NUt+mxyKwwfHifBilZwj2Ul7Ce62azJAAAAAAQBIKbpGG2dWTX8
j+VjFM21J0hqWlEg+bdiojWnKfA5AQpWUWtnNwDEM0g12vYxoWM8Y81W+bHBw805
I8kWVkXU6vFOi+HWvv/ira7ofJu16NnoUkhclkUrk0mXubZvyl4GBg==
-----END PGP PUBLIC KEY BLOCK-----` });
const plaintext = 'What we need from the grocery store:\n\n- tofu\n- vegetables\n- noodles\n';
const message = await openpgp.readCleartextMessage({ cleartextMessage });
const { signatures, data } = await openpgp.verify({ message, verificationKeys: publicKey });
expect(data).to.equal(plaintext);
expect(signatures).to.have.length(1);
expect(await signatures[0].verified).to.be.true;
expect((await signatures[0].signature).packets.length).to.equal(1);
});
it('Verify cleartext signed message with two signatures with openpgp.verify', async function() {
const cleartextMessage =
['-----BEGIN PGP SIGNED MESSAGE-----',