Skip to content

Commit

Permalink
Add UnparseablePacket to properly deal with key blocks that include…
Browse files Browse the repository at this point in the history
… malformed/unsupported packets (#1522)

When parsing errors are being ignored, packets that fail to parse are now
included in the resulting packet list as `UnparseablePacket`s . This way, when
parsing keys that contain unparsable (sub)key, we avoid associating the
following non-key packets to the wrong key entity.

On serialization, `UnparseablePacket`s are also included by writing their raw
packet body as it was read.
  • Loading branch information
larabr committed May 24, 2022
1 parent cb8901c commit 775dade
Show file tree
Hide file tree
Showing 7 changed files with 124 additions and 10 deletions.
11 changes: 8 additions & 3 deletions openpgp.d.ts
Expand Up @@ -74,11 +74,11 @@ export abstract class Key {

type AllowedKeyPackets = PublicKeyPacket | PublicSubkeyPacket | SecretKeyPacket | SecretSubkeyPacket | UserIDPacket | UserAttributePacket | SignaturePacket;
export class PublicKey extends Key {
constructor(packetlist: PacketList<AnyKeyPacket>);
constructor(packetlist: PacketList<AnyPacket>);
}

export class PrivateKey extends PublicKey {
constructor(packetlist: PacketList<AnyKeyPacket>);
constructor(packetlist: PacketList<AnyPacket>);
public revoke(reason?: ReasonForRevocation, date?: Date, config?: Config): Promise<PrivateKey>;
public isDecrypted(): boolean;
public addSubkey(options: SubkeyOptions): Promise<PrivateKey>;
Expand Down Expand Up @@ -527,7 +527,12 @@ export class TrustPacket extends BasePacket {
static readonly tag: enums.packet.trust;
}

export type AnyPacket = BasePacket;
export class UnparseablePacket {
tag: enums.packet;
write: () => Uint8Array;
}

export type AnyPacket = BasePacket | UnparseablePacket;
export type AnySecretKeyPacket = SecretKeyPacket | SecretSubkeyPacket;
export type AnyKeyPacket = BasePublicKeyPacket;

Expand Down
27 changes: 27 additions & 0 deletions src/key/key.js
Expand Up @@ -28,9 +28,15 @@ import Subkey from './subkey';
import * as helper from './helper';
import PrivateKey from './private_key';
import PublicKey from './public_key';
import { UnparseablePacket } from '../packet/packet';

// A key revocation certificate can contain the following packets
const allowedRevocationPackets = /*#__PURE__*/ util.constructAllowedPackets([SignaturePacket]);
const mainKeyPacketTags = new Set([enums.packet.publicKey, enums.packet.privateKey]);
const keyPacketTags = new Set([
enums.packet.publicKey, enums.packet.privateKey,
enums.packet.publicSubkey, enums.packet.privateSubkey
]);

/**
* Abstract class that represents an OpenPGP key. Must contain a primary key.
Expand All @@ -51,8 +57,29 @@ class Key {
let user;
let primaryKeyID;
let subkey;
let ignoreUntil;

for (const packet of packetlist) {

if (packet instanceof UnparseablePacket) {
const isUnparseableKeyPacket = keyPacketTags.has(packet.tag);
if (isUnparseableKeyPacket && !ignoreUntil){
// Since non-key packets apply to the preceding key packet, if a (sub)key is Unparseable we must
// discard all non-key packets that follow, until another (sub)key packet is found.
if (mainKeyPacketTags.has(packet.tag)) {
ignoreUntil = mainKeyPacketTags;
} else {
ignoreUntil = keyPacketTags;
}
}
continue;
}

const tag = packet.constructor.tag;
if (ignoreUntil) {
if (!ignoreUntil.has(tag)) continue;
ignoreUntil = null;
}
if (disallowedPackets.has(tag)) {
throw new Error(`Unexpected packet type: ${tag}`);
}
Expand Down
1 change: 1 addition & 0 deletions src/packet/index.js
@@ -1,2 +1,3 @@
export * from './all_packets';
export { default as PacketList } from './packetlist';
export { UnparseablePacket } from './packet';
10 changes: 10 additions & 0 deletions src/packet/packet.js
Expand Up @@ -309,3 +309,13 @@ export class UnsupportedError extends Error {
}
}

export class UnparseablePacket {
constructor(tag, rawContent) {
this.tag = tag;
this.rawContent = rawContent;
}

write() {
return this.rawContent;
}
}
11 changes: 8 additions & 3 deletions src/packet/packetlist.js
Expand Up @@ -3,6 +3,7 @@ import {
readPackets, supportsStreaming,
writeTag, writeHeader,
writePartialLength, writeSimpleLength,
UnparseablePacket,
UnsupportedError
} from './packet';
import util from '../util';
Expand Down Expand Up @@ -89,6 +90,9 @@ class PacketList extends Array {
// Those are also the ones we want to be more strict about and throw on parse errors
// (since we likely cannot process the message without these packets anyway).
await writer.abort(e);
} else {
const unparsedPacket = new UnparseablePacket(parsed.tag, parsed.packet);
await writer.write(unparsedPacket);
}
util.printDebugError(e);
}
Expand Down Expand Up @@ -129,12 +133,13 @@ class PacketList extends Array {
const arr = [];

for (let i = 0; i < this.length; i++) {
const tag = this[i] instanceof UnparseablePacket ? this[i].tag : this[i].constructor.tag;
const packetbytes = this[i].write();
if (util.isStream(packetbytes) && supportsStreaming(this[i].constructor.tag)) {
let buffer = [];
let bufferLength = 0;
const minLength = 512;
arr.push(writeTag(this[i].constructor.tag));
arr.push(writeTag(tag));
arr.push(stream.transform(packetbytes, value => {
buffer.push(value);
bufferLength += value.length;
Expand All @@ -152,9 +157,9 @@ class PacketList extends Array {
let length = 0;
arr.push(stream.transform(stream.clone(packetbytes), value => {
length += value.length;
}, () => writeHeader(this[i].constructor.tag, length)));
}, () => writeHeader(tag, length)));
} else {
arr.push(writeHeader(this[i].constructor.tag, packetbytes.length));
arr.push(writeHeader(tag, packetbytes.length));
}
arr.push(packetbytes);
}
Expand Down
68 changes: 66 additions & 2 deletions test/general/config.js
Expand Up @@ -10,12 +10,17 @@ module.exports = () => describe('Custom configuration', function() {

const config = { ignoreUnsupportedPackets: true };
const parsedMessage = await openpgp.readMessage({ armoredMessage: message.armor(), config });
expect(parsedMessage.packets.length).to.equal(1);
expect(parsedMessage.packets.length).to.equal(2);
expect(parsedMessage.packets[0].tag).to.equal(openpgp.enums.packet.symEncryptedSessionKey);

config.ignoreUnsupportedPackets = false;
await expect(
openpgp.readMessage({ armoredMessage: message.armor(), config })
).to.be.rejectedWith(/Version 1 of the SKESK packet is unsupported/);
// writing of partially parsed message should succeed
await expect(
openpgp.readMessage({ armoredMessage: parsedMessage.armor(), config })
).to.be.rejectedWith(/Version 1 of the SKESK packet is unsupported/);
});

it('openpgp.readSignature', async function() {
Expand All @@ -32,12 +37,17 @@ vAFM3jjrAQDgJPXsv8PqCrLGDuMa/2r6SgzYd03aw/xt1WM6hgUvhQD+J54Z

const config = { ignoreUnsupportedPackets: true };
const parsedSignature = await openpgp.readSignature({ armoredSignature: signature.armor(), config });
expect(parsedSignature.packets.length).to.equal(0);
expect(parsedSignature.packets.length).to.equal(1);
expect(parsedSignature.packets[0].tag).to.equal(openpgp.enums.packet.signature);

config.ignoreUnsupportedPackets = false;
await expect(
openpgp.readSignature({ armoredSignature: signature.armor(), config })
).to.be.rejectedWith(/Version 1 of the signature packet is unsupported/);
// writing of partially parsed signature should succeed
await expect(
openpgp.readSignature({ armoredSignature: parsedSignature.armor(), config })
).to.be.rejectedWith(/Version 1 of the signature packet is unsupported/);
});

it('openpgp.readKey', async function() {
Expand All @@ -50,6 +60,60 @@ vAFM3jjrAQDgJPXsv8PqCrLGDuMa/2r6SgzYd03aw/xt1WM6hgUvhQD+J54Z
).to.be.rejectedWith(/User ID string is too long/);
});

it('openpgp.readKeys', async function() {
// Valid v4 key followed by modified key declared as v3 (unsupported) and another valid v4 key.
// When ignoring malfored/unsupported packets, we do not want the userID and subkey of the trailing key
// to be associated with the leading one
const partiallyUnsupportedKeyBlock = `-----BEGIN PGP PUBLIC KEY BLOCK-----
xjMEYotwYxYJKwYBBAHaRw8BAQdAQrm/H1rTYvBLV2mP0+6u+jVa5iOgPIgA
VkH1H7KipDrNDzx0ZXN0QHRlc3QuY29tPsKMBBAWCgAdBQJii3BjBAsJBwgD
FQgKBBYAAgECGQECGwMCHgEAIQkQLavVE0KkGtwWIQQv90VxLmdeWJRzEWUt
q9UTQqQa3L/3APwM4ypA9q/qml+ezCdVFilv9huZVSbPlQ06AN5E0ZclgwD9
FeCHPwKqDkcKvqSQGdTv3QSefwjrt9oO8DI71vKjWQjOOARii3BjEgorBgEE
AZdVAQUBAQdALl5wAhaoMgtlk7aV6v1DC3T+7kuNQVDZZPPPbxhaYwMDAQgH
wngEGBYIAAkFAmKLcGMCGwwAIQkQLavVE0KkGtwWIQQv90VxLmdeWJRzEWUt
q9UTQqQa3N16APwLtHt26M1o1yUtBfQ2yddFQb/Xi4Kq3PBG5ltUBj38EAD/
aNfrR+NWb3LWRTe+LDuU7M+8ucdZ00TeAAOHGF11UAXGMwNii3B7FgkrBgEE
AdpHDwEBB0CF7hJ4IhKdtYMa2hkA1ckjgBcZL5TaK/+A+laliBVh2s0WPGFu
b3RoZXJ0ZXN0QHRlc3QuY29tPsKMBBAWCgAdBQJii3B7BAsJBwgDFQgKBBYA
AgECGQECGwMCHgEAIQkQxKiJcMvjhmEWIQQgDYaTtkFIWF89hvXEqIlwy+OG
YWnWAQDVjVaF4FpjV9rwhqqQ+pLQYWSjFGEQV9u05YPzOZWs0AEA4stxQp1H
OtXx2S/tfY74d+I/QPTVHgB6TVcADtdKnQjOOARii3B7EgorBgEEAZdVAQUB
AQdAsAnhg90WUEy1raZ/DrJ1MI9g8f2SBxUtvNfCikBwpWMDAQgHwngEGBYI
AAkFAmKLcHsCGwwAIQkQxKiJcMvjhmEWIQQgDYaTtkFIWF89hvXEqIlwy+OG
Ya2ZAQC5fDrNXuyqvjaJiVomAl7YnuFwR4tLlgJTVDDNkTOfvAD+IJo8ptfg
/lzgTPMPLP8RgpGs8jU5cWhLlH6866UkAwXGMwRii3B/FgkrBgEEAdpHDwEB
B0AU3y3+X4mAYxFDz54RroBsES1YTufnIndjbljQ4UCpcs0dPGFub3RoZXJh
bm90aGVydGVzdEB0ZXN0LmNvbT7CjAQQFgoAHQUCYotwfwQLCQcIAxUICgQW
AAIBAhkBAhsDAh4BACEJEDc5RdIx+aTBFiEE6N7yK4zw3IhhDLIwNzlF0jH5
pMFQWwEAwUBNM2wHH3PexhLv4QpmteIg8I2wlYmuYk0w0GfAPywBAOuyKqxE
g4vye4Mfs2Ns3FEUQP0y+YbAkZhxhjVX3gYJzjgEYotwfxIKKwYBBAGXVQEF
AQEHQK1UDFW1ue61hhm1O57eSv29+A2gId5Zi9TEqP1mopgkAwEIB8J4BBgW
CAAJBQJii3B/AhsMACEJEDc5RdIx+aTBFiEE6N7yK4zw3IhhDLIwNzlF0jH5
pMH3oQEA/gjeM/XpBP/DIhqzQxAVtrDFlkKairQMRMVQfoU4vVcBAITA9cqc
n9/quqtmyOtYOA6gXNCw0Fal3iANKBmsPmYI
=O3ZV
-----END PGP PUBLIC KEY BLOCK-----
`;
await expect(
openpgp.readKeys({ armoredKeys: partiallyUnsupportedKeyBlock, config: { ignoreUnsupportedPackets: false } })
).to.be.rejectedWith(/key packet is unsupported/);

const parsedKeys = await openpgp.readKeys({ armoredKeys: partiallyUnsupportedKeyBlock, config: { ignoreUnsupportedPackets: true } });
expect(parsedKeys.length).to.equal(2);
expect(parsedKeys[0].subkeys.length).to.equal(1);
expect(parsedKeys[0].subkeys[0].getKeyID().toHex()).to.equal('0861c76681a34407');
expect(parsedKeys[0].users.length).to.equal(1);
expect(parsedKeys[0].users[0].userID.email).to.equal('test@test.com');
expect(await parsedKeys[0].getEncryptionKey().then(key => key.getKeyID().toHex())).to.equal('0861c76681a34407');

expect(parsedKeys[1].subkeys.length).to.equal(1);
expect(parsedKeys[1].subkeys[0].getKeyID().toHex()).to.equal('48050814f28f2263');
expect(parsedKeys[1].users.length).to.equal(1);
expect(parsedKeys[1].users[0].userID.email).to.equal('anotheranothertest@test.com');
expect(await parsedKeys[1].getEncryptionKey().then(key => key.getKeyID().toHex())).to.equal('48050814f28f2263');
});

it('openpgp.generateKey', async function() {
const v5KeysVal = openpgp.config.v5Keys;
Expand Down
6 changes: 4 additions & 2 deletions test/general/packet.js
Expand Up @@ -969,7 +969,8 @@ kePFjAnu9cpynKXu3usf8+FuBw2zLsg1Id1n7ttxoAte416KjBN9lFBt8mcu
packets.push(signaturePacket);
const bytes = packets.write();
const parsed = await openpgp.PacketList.fromBinary(bytes, allAllowedPackets, { ...openpgp.config, ignoreUnsupportedPackets: true });
expect(parsed.length).to.equal(0);
expect(parsed.length).to.equal(1);
expect(parsed[0].tag).to.equal(openpgp.enums.packet.signature);
});

it('Throws on unknown packet version with `config.ignoreUnsupportedPackets` disabled', async function() {
Expand Down Expand Up @@ -1011,7 +1012,8 @@ kePFjAnu9cpynKXu3usf8+FuBw2zLsg1Id1n7ttxoAte416KjBN9lFBt8mcu
openpgp.PacketList.fromBinary(bytes, allAllowedPackets, { ...openpgp.config, maxUserIDLength: 2, ignoreMalformedPackets: false })
).to.be.rejectedWith(/User ID string is too long/);
const parsed = await openpgp.PacketList.fromBinary(bytes, allAllowedPackets, { ...openpgp.config, maxUserIDLength: 2, ignoreMalformedPackets: true });
expect(parsed.length).to.equal(0);
expect(parsed.length).to.equal(1);
expect(parsed[0].tag).to.equal(openpgp.enums.packet.userID);
});
});
});

0 comments on commit 775dade

Please sign in to comment.