Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add UnparseablePacket to properly deal with key blocks that include malformed/unsupported packets #1522

Merged
merged 4 commits into from May 24, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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);
});
});
});