mirror of
https://github.com/openpgpjs/openpgpjs.git
synced 2025-03-30 15:08:32 +00:00
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:
parent
af96628855
commit
1ddf4e151c
@ -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');
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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-----',
|
||||
|
Loading…
x
Reference in New Issue
Block a user