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

Throw UnsupportedError on unknown algorithm in keys, signatures and encrypted session keys #1523

Merged
merged 9 commits into from Jun 7, 2022
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
13 changes: 7 additions & 6 deletions src/crypto/crypto.js
Expand Up @@ -34,6 +34,7 @@ import enums from '../enums';
import util from '../util';
import OID from '../type/oid';
import { Curve } from './public_key/elliptic/curves';
import { UnsupportedError } from '../packet/packet';

/**
* Encrypts data using specified algorithm and public key parameters.
Expand Down Expand Up @@ -105,7 +106,7 @@ export async function publicKeyDecrypt(algo, publicKeyParams, privateKeyParams,
oid, kdfParams, V, C.data, Q, d, fingerprint);
}
default:
throw new Error('Invalid public key encryption algorithm.');
throw new Error('Unknown public key encryption algorithm.');
}
}

Expand Down Expand Up @@ -156,7 +157,7 @@ export function parsePublicKeyParams(algo, bytes) {
return { read: read, publicParams: { oid, Q, kdfParams } };
}
default:
throw new Error('Invalid public key encryption algorithm.');
throw new UnsupportedError('Unknown public key encryption algorithm.');
}
}

Expand Down Expand Up @@ -197,7 +198,7 @@ export function parsePrivateKeyParams(algo, bytes, publicParams) {
return { read, privateParams: { seed } };
}
default:
throw new Error('Invalid public key encryption algorithm.');
throw new UnsupportedError('Unknown public key encryption algorithm.');
}
}

Expand Down Expand Up @@ -234,7 +235,7 @@ export function parseEncSessionKeyParams(algo, bytes) {
return { V, C };
}
default:
throw new Error('Invalid public key encryption algorithm.');
throw new Error('Unknown public key encryption algorithm.');
larabr marked this conversation as resolved.
Show resolved Hide resolved
}
}

Expand Down Expand Up @@ -293,7 +294,7 @@ export function generateParams(algo, bits, oid) {
case enums.publicKey.elgamal:
throw new Error('Unsupported algorithm for key generation.');
default:
throw new Error('Invalid public key algorithm.');
throw new Error('Unknown public key algorithm.');
}
}

Expand Down Expand Up @@ -340,7 +341,7 @@ export async function validateParams(algo, publicParams, privateParams) {
return publicKey.elliptic.eddsa.validateParams(oid, Q, seed);
}
default:
throw new Error('Invalid public key algorithm.');
throw new Error('Unknown public key algorithm.');
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/crypto/public_key/elliptic/curves.js
Expand Up @@ -28,6 +28,7 @@ import util from '../../../util';
import { uint8ArrayToB64, b64ToUint8Array } from '../../../encoding/base64';
import OID from '../../../type/oid';
import { keyFromPublic, keyFromPrivate, getIndutnyCurve } from './indutnyKey';
import { UnsupportedError } from '../../../packet/packet';

const webCrypto = util.getWebCrypto();
const nodeCrypto = util.getNodeCrypto();
Expand Down Expand Up @@ -145,7 +146,7 @@ class Curve {
// by curve name or oid string
this.name = enums.write(enums.curve, oidOrName);
} catch (err) {
throw new Error('Not valid curve');
throw new UnsupportedError('Unknown curve');
}
params = params || curves[this.name];

Expand Down
7 changes: 4 additions & 3 deletions src/crypto/signature.js
Expand Up @@ -7,6 +7,7 @@
import publicKey from './public_key';
import enums from '../enums';
import util from '../util';
import { UnsupportedError } from '../packet/packet';

/**
* Parse signature in binary form to get the parameters.
Expand Down Expand Up @@ -55,7 +56,7 @@ export function parseSignatureParams(algo, signature) {
return { r, s };
}
default:
throw new Error('Invalid signature algorithm.');
throw new UnsupportedError('Unknown signature algorithm.');
}
}

Expand Down Expand Up @@ -101,7 +102,7 @@ export async function verify(algo, hashAlgo, signature, publicParams, data, hash
return publicKey.elliptic.eddsa.verify(oid, hashAlgo, signature, data, Q, hashed);
}
default:
throw new Error('Invalid signature algorithm.');
throw new Error('Unknown signature algorithm.');
}
}

Expand Down Expand Up @@ -151,6 +152,6 @@ export async function sign(algo, hashAlgo, publicKeyParams, privateKeyParams, da
return publicKey.elliptic.eddsa.sign(oid, hashAlgo, data, Q, seed, hashed);
}
default:
throw new Error('Invalid signature algorithm.');
throw new Error('Unknown signature algorithm.');
}
}
2 changes: 1 addition & 1 deletion src/key/helper.js
Expand Up @@ -340,7 +340,7 @@ export function sanitizeKeyOptions(options, subkeyDefaults = {}) {
try {
options.curve = enums.write(enums.curve, options.curve);
} catch (e) {
throw new Error('Invalid curve');
throw new Error('Unknown curve');
}
if (options.curve === enums.curve.ed25519 || options.curve === enums.curve.curve25519) {
options.curve = options.sign ? enums.curve.ed25519 : enums.curve.curve25519;
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
10 changes: 3 additions & 7 deletions src/packet/public_key.js
Expand Up @@ -123,13 +123,9 @@ class PublicKeyPacket {
}

// - A series of values comprising the key material.
try {
const { read, publicParams } = crypto.parsePublicKeyParams(this.algorithm, bytes.subarray(pos));
this.publicParams = publicParams;
pos += read;
} catch (err) {
throw new Error('Error reading MPIs');
}
const { read, publicParams } = crypto.parsePublicKeyParams(this.algorithm, bytes.subarray(pos));
this.publicParams = publicParams;
pos += read;

// we set the fingerprint and keyID already to make it possible to put together the key packets directly in the Key constructor
await this.computeFingerprintAndKeyID();
Expand Down
3 changes: 3 additions & 0 deletions src/packet/secret_key.js
Expand Up @@ -21,6 +21,7 @@ import crypto from '../crypto';
import enums from '../enums';
import util from '../util';
import defaultConfig from '../config';
import { UnsupportedError } from './packet';

/**
* A Secret-Key packet contains all the information that is found in a
Expand Down Expand Up @@ -155,6 +156,8 @@ class SecretKeyPacket extends PublicKeyPacket {
const { privateParams } = crypto.parsePrivateKeyParams(this.algorithm, cleartext, this.publicParams);
this.privateParams = privateParams;
} catch (err) {
if (err instanceof UnsupportedError) throw err;
// avoid throwing potentially sensitive errors
throw new Error('Error reading MPIs');
}
Comment on lines 158 to 162
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same here - can it throw anything else? And, is the UnsupportedError sensitive, here?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is parsing secret key material, so I'd rather not risk it. Even an MPI-bad-length error can be sensitive.
The Unsupported error is only thrown on wrong algo, which is already public information, so it's not sensitive, at least for now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Even an MPI-bad-length error can be sensitive

But do we throw any such error? From looking at the code I think we just return a Uint8Array with a different length in that case

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems the only thing that can throw is the Curve constructor (which can throw "Not valid curve"), but actually that should probably be an UnsupportedError as well

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the declared length is too long, is it guaranteed that .subarray doesn't throw? Is it not up to the platform?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, it's guaranteed, see mdn / tc39

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We're temporarily leaving the if-else here; #1523 is a more permanent solution to avoid not hiding sensitive errors introduced by future changes.

}
Expand Down
2 changes: 1 addition & 1 deletion test/crypto/ecdh.js
Expand Up @@ -69,7 +69,7 @@ module.exports = () => describe('ECDH key exchange @lightweight', function () {
it('Invalid curve oid', function (done) {
expect(decrypt_message(
'', 2, 7, [], [], [], [], []
)).to.be.rejectedWith(Error, /Not valid curve/).notify(done);
)).to.be.rejectedWith(Error, /Unknown curve/).notify(done);
});
it('Invalid ephemeral key', function (done) {
if (!openpgp.config.useIndutnyElliptic && !util.getNodeCrypto()) {
Expand Down
4 changes: 2 additions & 2 deletions test/crypto/elliptic.js
Expand Up @@ -167,10 +167,10 @@ module.exports = () => describe('Elliptic Curve Cryptography @lightweight', func
return Promise.all([
expect(verify_signature(
'invalid oid', 8, [], [], [], []
)).to.be.rejectedWith(Error, /Not valid curve/),
)).to.be.rejectedWith(Error, /Unknown curve/),
expect(verify_signature(
'\x00', 8, [], [], [], []
)).to.be.rejectedWith(Error, /Not valid curve/)
)).to.be.rejectedWith(Error, /Unknown curve/)
]);
});
it('Invalid public key', async function () {
Expand Down