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.
This commit is contained in:
Daniel Huigens 2025-08-21 12:41:14 +02:00
parent 0a92baf8ba
commit 364eb1ee18
No known key found for this signature in database
GPG Key ID: CB064A128FA90686
3 changed files with 40 additions and 10 deletions

View File

@ -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 <CR><LF>
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 <CR><LF>
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;
}
/**

View File

@ -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 = [];

View File

@ -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;
});
});