Fix wrong serialization of PKESK v6 for x25519/x448 (#1734)

The cleartext session key symmetric algorithm was accidentally included in the packet.
As a result, the generated messages may fail to parse and/or decrypt in other implementations.
The messages would still decrypt successfully in OpenPGP.js, due to an overly permissive parsing procedure,
which simply discarded the unused additional byte.

We know also throw on unexpected cleartext symmetric algo in PKESK v6.
This commit is contained in:
larabr 2024-03-22 17:10:27 +01:00 committed by GitHub
parent b41298a3f6
commit 2574795d37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 68 additions and 27 deletions

View File

@ -39,7 +39,7 @@ import ECDHXSymmetricKey from '../type/ecdh_x_symkey';
* Encrypts data using specified algorithm and public key parameters. * Encrypts data using specified algorithm and public key parameters.
* See {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC 4880 9.1} for public key algorithms. * See {@link https://tools.ietf.org/html/rfc4880#section-9.1|RFC 4880 9.1} for public key algorithms.
* @param {module:enums.publicKey} keyAlgo - Public key algorithm * @param {module:enums.publicKey} keyAlgo - Public key algorithm
* @param {module:enums.symmetric} symmetricAlgo - Cipher algorithm * @param {module:enums.symmetric|null} symmetricAlgo - Cipher algorithm (v3 only)
* @param {Object} publicParams - Algorithm-specific public key parameters * @param {Object} publicParams - Algorithm-specific public key parameters
* @param {Uint8Array} data - Session key data to be encrypted * @param {Uint8Array} data - Session key data to be encrypted
* @param {Uint8Array} fingerprint - Recipient fingerprint * @param {Uint8Array} fingerprint - Recipient fingerprint
@ -66,7 +66,7 @@ export async function publicKeyEncrypt(keyAlgo, symmetricAlgo, publicParams, dat
} }
case enums.publicKey.x25519: case enums.publicKey.x25519:
case enums.publicKey.x448: { case enums.publicKey.x448: {
if (!util.isAES(symmetricAlgo)) { if (symmetricAlgo && !util.isAES(symmetricAlgo)) {
// see https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/276 // see https://gitlab.com/openpgp-wg/rfc4880bis/-/merge_requests/276
throw new Error('X25519 and X448 keys can only encrypt AES session keys'); throw new Error('X25519 and X448 keys can only encrypt AES session keys');
} }

View File

@ -128,9 +128,12 @@ class PublicKeyEncryptedSessionKeyPacket {
} }
this.publicKeyAlgorithm = bytes[offset++]; this.publicKeyAlgorithm = bytes[offset++];
this.encrypted = crypto.parseEncSessionKeyParams(this.publicKeyAlgorithm, bytes.subarray(offset)); this.encrypted = crypto.parseEncSessionKeyParams(this.publicKeyAlgorithm, bytes.subarray(offset));
if (this.version === 3 && ( if (this.publicKeyAlgorithm === enums.publicKey.x25519 || this.publicKeyAlgorithm === enums.publicKey.x448) {
this.publicKeyAlgorithm === enums.publicKey.x25519 || this.publicKeyAlgorithm === enums.publicKey.x448)) { if (this.version === 3) {
this.sessionKeyAlgorithm = enums.write(enums.symmetric, this.encrypted.C.algorithm); this.sessionKeyAlgorithm = enums.write(enums.symmetric, this.encrypted.C.algorithm);
} else if (this.encrypted.C.algorithm !== null) {
throw new Error('Unexpected cleartext symmetric algorithm');
}
} }
} }
@ -174,9 +177,12 @@ class PublicKeyEncryptedSessionKeyPacket {
*/ */
async encrypt(key) { async encrypt(key) {
const algo = enums.write(enums.publicKey, this.publicKeyAlgorithm); const algo = enums.write(enums.publicKey, this.publicKeyAlgorithm);
const encoded = encodeSessionKey(this.version, algo, this.sessionKeyAlgorithm, this.sessionKey); // No symmetric encryption algorithm identifier is passed to the public-key algorithm for a
// v6 PKESK packet, as it is included in the v2 SEIPD packet.
const sessionKeyAlgorithm = this.version === 3 ? this.sessionKeyAlgorithm : null;
const encoded = encodeSessionKey(this.version, algo, sessionKeyAlgorithm, this.sessionKey);
this.encrypted = await crypto.publicKeyEncrypt( this.encrypted = await crypto.publicKeyEncrypt(
algo, this.sessionKeyAlgorithm, key.publicParams, encoded, key.getFingerprintBytes()); algo, sessionKeyAlgorithm, key.publicParams, encoded, key.getFingerprintBytes());
} }
/** /**
@ -275,6 +281,7 @@ function decodeSessionKey(version, keyAlgo, decryptedData, randomSessionKey) {
case enums.publicKey.x25519: case enums.publicKey.x25519:
case enums.publicKey.x448: case enums.publicKey.x448:
return { return {
sessionKeyAlgorithm: null,
sessionKey: decryptedData sessionKey: decryptedData
}; };
default: default:

View File

@ -8,6 +8,8 @@ chaiUse(chaiAsPromised);
import openpgp from '../initOpenpgp.js'; import openpgp from '../initOpenpgp.js';
import crypto from '../../src/crypto'; import crypto from '../../src/crypto';
import util from '../../src/util.js'; import util from '../../src/util.js';
import * as packet from '../../src/packet';
import * as input from './testInputs.js'; import * as input from './testInputs.js';
@ -469,32 +471,64 @@ export default () => describe('Packet', function() {
}); });
}); });
it('Public key encrypted symmetric key packet', function() { describe('Public key encrypted symmetric key packet - roundtrip', () => {
const rsa = openpgp.enums.publicKey.rsaEncryptSign; const testData = [{
const keySize = 1024; algoLabel: 'RSA',
publicKeyAlgorithm: openpgp.enums.publicKey.rsaEncryptSign,
paramsPromise: crypto.generateParams(openpgp.enums.publicKey.rsaEncryptSign, 1024, 65537)
},
{
algoLabel: 'ECDH NIST P-256',
publicKeyAlgorithm: openpgp.enums.publicKey.ecdh,
paramsPromise: crypto.generateParams(openpgp.enums.publicKey.ecdh, null, openpgp.enums.curve.nistP256)
},
{
algoLabel: 'ECDH x25519Legacy',
publicKeyAlgorithm: openpgp.enums.publicKey.ecdh,
paramsPromise: crypto.generateParams(openpgp.enums.publicKey.ecdh, null, openpgp.enums.curve.curve25519Legacy)
},
{
algoLabel: 'x25519',
publicKeyAlgorithm: openpgp.enums.publicKey.x25519,
paramsPromise: crypto.generateParams(openpgp.enums.publicKey.x25519)
}];
return crypto.generateParams(rsa, keySize, 65537).then(function({ publicParams, privateParams }) { function testRoundtrip({ v6 }) {
const enc = new openpgp.PublicKeyEncryptedSessionKeyPacket(); testData.forEach(({ algoLabel, publicKeyAlgorithm, paramsPromise }) => {
enc.version = 3; it(`${algoLabel} (PKESK ${v6 ? 'v6' : 'v3'})`, async () => {
const msg = new openpgp.PacketList(); const { publicParams, privateParams } = await paramsPromise;
const msg2 = new openpgp.PacketList(); // cannot use the `openpgp` exported values, since the different context gives issues when internally
// evaluating the `OID` instanceof of `publicParams.oid`, as part of `pkesk.encrypt` and `decrypt`
const pkesk = new packet.PublicKeyEncryptedSessionKeyPacket();
pkesk.version = v6 ? 6 : 3;
const msg = new packet.PacketList();
const msg2 = new packet.PacketList();
enc.sessionKey = new Uint8Array([1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2]); const privateKey = {
enc.publicKeyAlgorithm = openpgp.enums.publicKey.rsaEncryptSign; algorithm: publicKeyAlgorithm,
enc.sessionKeyAlgorithm = openpgp.enums.symmetric.aes256; publicParams,
enc.publicKeyID.bytes = '12345678'; privateParams,
return enc.encrypt({ publicParams, getFingerprintBytes() {} }).then(async () => { getFingerprintBytes: () => new Uint8Array(64)
};
pkesk.sessionKey = new Uint8Array([1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2]);
pkesk.publicKeyAlgorithm = publicKeyAlgorithm;
pkesk.sessionKeyAlgorithm = openpgp.enums.symmetric.aes256;
pkesk.publicKeyID.bytes = '12345678';
await pkesk.encrypt({ publicParams: privateKey.publicParams, getFingerprintBytes: privateKey.getFingerprintBytes });
msg.push(enc); msg.push(pkesk);
await msg2.read(msg.write(), allAllowedPackets); const allAllowedPackets = util.constructAllowedPackets([...Object.values(packet).filter(packetClass => !!packetClass.tag)]);
await msg2.read(msg.write(), allAllowedPackets);
const privateKey = { algorithm: openpgp.enums.publicKey.rsaEncryptSign, publicParams, privateParams, getFingerprintBytes() {} }; await msg2[0].decrypt(privateKey);
return msg2[0].decrypt(privateKey).then(() => { expect(msg2[0].sessionKey).to.deep.equal(pkesk.sessionKey);
expect(stringify(msg2[0].sessionKey)).to.equal(stringify(enc.sessionKey)); expect(msg2[0].sessionKeyAlgorithm).to.equal(v6 ? null : pkesk.sessionKeyAlgorithm);
expect(msg2[0].sessionKeyAlgorithm).to.equal(enc.sessionKeyAlgorithm);
}); });
}); });
}); }
testRoundtrip({ v6: false });
testRoundtrip({ v6: true });
}); });
it('Secret key packet (reading, unencrypted)', async function() { it('Secret key packet (reading, unencrypted)', async function() {