From 364eb1ee18ffe4e5c4db9ad8e32e3683c85d91d2 Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Thu, 21 Aug 2025 12:41:14 +0200 Subject: [PATCH 1/2] Generate cleartext signed messages with consistent LF newlines Instead of generating CRLF newlines in the signed data and LF newlines in the surrounding armor framing, consistently generate LF newlines. --- src/cleartext.js | 14 +++++++------- src/encoding/armor.js | 2 +- test/general/armor.js | 34 ++++++++++++++++++++++++++++++++-- 3 files changed, 40 insertions(+), 10 deletions(-) diff --git a/src/cleartext.js b/src/cleartext.js index 61136c28..834f92d4 100644 --- a/src/cleartext.js +++ b/src/cleartext.js @@ -36,8 +36,8 @@ export class CleartextMessage { * @param {Signature} signature - The detached signature or an empty signature for unsigned messages */ constructor(text, signature) { - // remove trailing whitespace and normalize EOL to canonical form - this.text = util.removeTrailingSpaces(text).replace(/\r?\n/g, '\r\n'); + // remove trailing whitespace from each line + this.text = util.removeTrailingSpaces(text); if (signature && !(signature instanceof Signature)) { throw new Error('Invalid signature input'); } @@ -73,7 +73,8 @@ export class CleartextMessage { */ async sign(signingKeys, recipientKeys = [], signature = null, signingKeyIDs = [], date = new Date(), signingUserIDs = [], recipientUserIDs = [], notations = [], config = defaultConfig) { const literalDataPacket = new LiteralDataPacket(); - literalDataPacket.setText(this.text); + // normalize EOL to canonical form + literalDataPacket.setText(this.text.replace(/\n/g, '\r\n')); const newSignature = new Signature(await createSignaturePackets(literalDataPacket, signingKeys, recipientKeys, signature, signingKeyIDs, date, signingUserIDs, recipientUserIDs, notations, true, config)); return new CleartextMessage(this.text, newSignature); } @@ -93,8 +94,8 @@ export class CleartextMessage { verify(keys, date = new Date(), config = defaultConfig) { const signatureList = this.signature.packets.filterByTag(enums.packet.signature); // drop UnparsablePackets const literalDataPacket = new LiteralDataPacket(); - // we assume that cleartext signature is generated based on UTF8 cleartext - literalDataPacket.setText(this.text); + // cleartext signatures should be generated over UTF8 cleartext with canonical newlines + literalDataPacket.setText(this.text.replace(/\n/g, '\r\n')); return createVerificationObjects(signatureList, [literalDataPacket], keys, date, true, config); } @@ -103,8 +104,7 @@ export class CleartextMessage { * @returns {String} Cleartext of message. */ getText() { - // normalize end of line to \n - return this.text.replace(/\r\n/g, '\n'); + return this.text; } /** diff --git a/src/encoding/armor.js b/src/encoding/armor.js index fd5efdab..e9cf9cc6 100644 --- a/src/encoding/armor.js +++ b/src/encoding/armor.js @@ -272,7 +272,7 @@ export function unarmor(input) { // Reverse dash-escaping for msg text.push(line.replace(/^- /, '')); } else { - text = text.join('\r\n'); + text = text.join('\n'); textDone = true; verifyHeaders(lastHeaders); lastHeaders = []; diff --git a/test/general/armor.js b/test/general/armor.js index 01df7c05..d8b2dc18 100644 --- a/test/general/armor.js +++ b/test/general/armor.js @@ -252,7 +252,7 @@ export default () => describe('ASCII armor', function() { it('Do not filter blank lines after header', async function () { let msg = getArmor(['Hash: SHA1', '']); msg = await openpgp.readCleartextMessage({ cleartextMessage: msg }); - expect(msg.text).to.equal('\r\nsign this'); + expect(msg.getText()).to.equal('\nsign this'); }); it('Selectively output CRC checksum', async function () { @@ -353,9 +353,39 @@ NJCB6+LWtabSoVIjNVgKwyKqyTLaESNwC2ogZwkdE8qPGiDFEHo4Gg9zuRof ).to.equal( pubKey .replace('\n-', '-') - .replace(/\n\r/g, '\n') ); }); + it('Armored messages use LF newlines', async function () { + const usesLF = armoredData => { + return !armoredData.includes('\r'); + }; + + const { privateKey: v4Key } = await openpgp.generateKey({ userIDs: { email: 'v4@armor.test' }, format: 'object' }); + expect(usesLF(v4Key.armor())).to.be.true; + const { privateKey: v6Key } = await openpgp.generateKey({ userIDs: { email: 'v6@armor.test' }, config: { v6Keys: true, aeadProtect: true }, format: 'object' }); + expect(usesLF(v6Key.armor())).to.be.true; + + const messageWithSEIPDv1 = await openpgp.encrypt({ message: await openpgp.createMessage({ text: 'test' }), encryptionKeys: v4Key }); + expect(usesLF(messageWithSEIPDv1)).to.be.true; + const messageWithSEIPDv2 = await openpgp.encrypt({ message: await openpgp.createMessage({ text: 'test' }), encryptionKeys: v6Key }); + expect(usesLF(messageWithSEIPDv2)).to.be.true; + + const signatureV4V6 = await openpgp.sign({ message: await openpgp.createMessage({ text: 'test' }), signingKeys: [v4Key, v6Key] }); + expect(usesLF(signatureV4V6)).to.be.true; + const signatureV6 = await openpgp.sign({ message: await openpgp.createMessage({ text: 'test' }), signingKeys: v6Key }); + expect(usesLF(signatureV6)).to.be.true; + + const detachedSignatureV4V6 = await openpgp.sign({ message: await openpgp.createMessage({ text: 'test' }), signingKeys: [v4Key, v6Key], detached: true }); + expect(usesLF(detachedSignatureV4V6)).to.be.true; + const detachedSignatureV6 = await openpgp.sign({ message: await openpgp.createMessage({ text: 'test' }), signingKeys: v6Key, detached: true }); + expect(usesLF(detachedSignatureV6)).to.be.true; + + const cleartextSignatureV4V6 = await openpgp.sign({ message: await openpgp.createCleartextMessage({ text: 'test' }), signingKeys: [v4Key, v6Key] }); + expect(usesLF(cleartextSignatureV4V6)).to.be.true; + const cleartextSignatureV6 = await openpgp.sign({ message: await openpgp.createCleartextMessage({ text: 'test' }), signingKeys: v6Key }); + expect(usesLF(cleartextSignatureV6)).to.be.true; + }); + }); From a3a934d06b7ff4540b3afb3371a70c51e043e65c Mon Sep 17 00:00:00 2001 From: Daniel Huigens Date: Thu, 21 Aug 2025 18:45:07 +0200 Subject: [PATCH 2/2] Update src/cleartext.js Co-authored-by: larabr --- src/cleartext.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cleartext.js b/src/cleartext.js index 834f92d4..23ac6e3b 100644 --- a/src/cleartext.js +++ b/src/cleartext.js @@ -36,7 +36,7 @@ export class CleartextMessage { * @param {Signature} signature - The detached signature or an empty signature for unsigned messages */ constructor(text, signature) { - // remove trailing whitespace from each line + // remove trailing whitespace from each line and normalize line endings to LF this.text = util.removeTrailingSpaces(text); if (signature && !(signature instanceof Signature)) { throw new Error('Invalid signature input');