Cipher-specific key validation (#1116)

Also, check binding signatures for decryption keys.

Also, do not always fallback on Web Crypto ECC errors.
This commit is contained in:
larabr 2020-07-13 19:57:33 +02:00 committed by GitHub
parent 6988fdfee1
commit 00c5f38689
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 1160 additions and 63 deletions

View File

@ -340,7 +340,7 @@ module.exports = {
"no-use-before-define": [ 2, { "functions": false, "classes": true, "variables": false }],
"no-constant-condition": [ 2, { "checkLoops": false } ],
"new-cap": [ 2, { "properties": false, "capIsNewExceptionPattern": "CMAC|CBC|OMAC|CTR", "newIsCapExceptionPattern": "type|hash*"}],
"max-lines": [ 2, { "max": 550, "skipBlankLines": true, "skipComments": true } ],
"max-lines": [ 2, { "max": 600, "skipBlankLines": true, "skipComments": true } ],
"no-unused-expressions": 0,
"chai-friendly/no-unused-expressions": [ 2, { "allowShortCircuit": true } ],

View File

@ -153,8 +153,8 @@ export default {
},
/** Returns the types comprising the private key of an algorithm
* @param {String} algo The public key algorithm
* @returns {Array<String>} The array of types
* @param {module:enums.publicKey} algo The public key algorithm
* @returns {Array<String>} The array of types
*/
getPrivKeyParamTypes: function(algo) {
switch (algo) {
@ -187,8 +187,8 @@ export default {
},
/** Returns the types comprising the public key of an algorithm
* @param {String} algo The public key algorithm
* @returns {Array<String>} The array of types
* @param {module:enums.publicKey} algo The public key algorithm
* @returns {Array<String>} The array of types
*/
getPubKeyParamTypes: function(algo) {
switch (algo) {
@ -230,8 +230,8 @@ export default {
},
/** Returns the types comprising the encrypted session key of an algorithm
* @param {String} algo The public key algorithm
* @returns {Array<String>} The array of types
* @param {module:enums.publicKey} algo The public key algorithm
* @returns {Array<String>} The array of types
*/
getEncSessionKeyParamTypes: function(algo) {
switch (algo) {
@ -257,10 +257,10 @@ export default {
},
/** Generate algorithm-specific key parameters
* @param {String} algo The public key algorithm
* @param {Integer} bits Bit length for RSA keys
* @param {module:type/oid} oid Object identifier for ECC keys
* @returns {Array} The array of parameters
* @param {module:enums.publicKey} algo The public key algorithm
* @param {Integer} bits Bit length for RSA keys
* @param {module:type/oid} oid Object identifier for ECC keys
* @returns {Array} The array of parameters
* @async
*/
generateParams: function(algo, bits, oid) {
@ -297,6 +297,75 @@ export default {
}
},
/**
* Validate algorithm-specific key parameters
* @param {module:enums.publicKey} algo The public key algorithm
* @param {Array} params The array of parameters
* @returns {Promise<Boolean> whether the parameters are valid
* @async
*/
validateParams: async function(algo, params) {
switch (algo) {
case enums.publicKey.rsa_encrypt:
case enums.publicKey.rsa_encrypt_sign:
case enums.publicKey.rsa_sign: {
if (params.length < 6) {
throw new Error('Missing key parameters');
}
const n = params[0].toUint8Array();
const e = params[1].toUint8Array();
const d = params[2].toUint8Array();
const p = params[3].toUint8Array();
const q = params[4].toUint8Array();
const u = params[5].toUint8Array();
return publicKey.rsa.validateParams(n, e, d, p, q, u);
}
case enums.publicKey.dsa: {
if (params.length < 5) {
throw new Error('Missing key parameters');
}
const p = params[0].toUint8Array();
const q = params[1].toUint8Array();
const g = params[2].toUint8Array();
const y = params[3].toUint8Array();
const x = params[4].toUint8Array();
return publicKey.dsa.validateParams(p, q, g, y, x);
}
case enums.publicKey.elgamal: {
if (params.length < 4) {
throw new Error('Missing key parameters');
}
const p = params[0].toUint8Array();
const g = params[1].toUint8Array();
const y = params[2].toUint8Array();
const x = params[3].toUint8Array();
return publicKey.elgamal.validateParams(p, g, y, x);
}
case enums.publicKey.ecdsa:
case enums.publicKey.ecdh: {
const expectedLen = algo === enums.publicKey.ecdh ? 3 : 2;
if (params.length < expectedLen) {
throw new Error('Missing key parameters');
}
const algoModule = publicKey.elliptic[enums.read(enums.publicKey, algo)];
const { oid, Q, d } = algoModule.parseParams(params);
return algoModule.validateParams(oid, Q, d);
}
case enums.publicKey.eddsa: {
const expectedLen = 3;
if (params.length < expectedLen) {
throw new Error('Missing key parameters');
}
const { oid, Q, seed } = publicKey.elliptic.eddsa.parseParams(params);
return publicKey.elliptic.eddsa.validateParams(oid, Q, seed);
}
default:
throw new Error('Invalid public key algorithm.');
}
},
/**
* Generates a random byte prefix for the specified algorithm
* See {@link https://tools.ietf.org/html/rfc4880#section-9.2|RFC 4880 9.2} for algorithms.

View File

@ -26,6 +26,7 @@
import BN from 'bn.js';
import random from '../random';
import util from '../../util';
import prime from './prime';
const one = new BN(1);
const zero = new BN(0);
@ -121,5 +122,67 @@ export default {
const t2 = y.toRed(redp).redPow(u2.fromRed()); // y**u2 mod p
const v = t1.redMul(t2).fromRed().mod(q); // (g**u1 * y**u2 mod p) mod q
return v.cmp(r) === 0;
},
/**
* Validate DSA parameters
* @param {Uint8Array} p DSA prime
* @param {Uint8Array} q DSA group order
* @param {Uint8Array} g DSA sub-group generator
* @param {Uint8Array} y DSA public key
* @param {Uint8Array} x DSA private key
* @returns {Promise<Boolean>} whether params are valid
* @async
*/
validateParams: async function (p, q, g, y, x) {
p = new BN(p);
q = new BN(q);
g = new BN(g);
y = new BN(y);
const one = new BN(1);
// Check that 1 < g < p
if (g.lte(one) || g.gte(p)) {
return false;
}
/**
* Check that subgroup order q divides p-1
*/
if (!p.sub(one).mod(q).isZero()) {
return false;
}
const pred = new BN.red(p);
const gModP = g.toRed(pred);
/**
* g has order q
* Check that g ** q = 1 mod p
*/
if (!gModP.redPow(q).eq(one)) {
return false;
}
/**
* Check q is large and probably prime (we mainly want to avoid small factors)
*/
const qSize = q.bitLength();
if (qSize < 150 || !(await prime.isProbablePrime(q, null, 32))) {
return false;
}
/**
* Re-derive public key y' = g ** x mod p
* Expect y == y'
*
* Blinded exponentiation computes g**{rq + x} to compare to y
*/
x = new BN(x);
const r = await random.getRandomBN(new BN(2).shln(qSize - 1), new BN(2).shln(qSize)); // draw r of same size as q
const rqx = q.mul(r).add(x);
if (!y.eq(gModP.redPow(rqx))) {
return false;
}
return true;
}
};

View File

@ -64,5 +64,74 @@ export default {
const c1red = c1.toRed(redp);
const c2red = c2.toRed(redp);
return c1red.redPow(x).redInvm().redMul(c2red).fromRed();
},
/**
* Validate ElGamal parameters
* @param {Uint8Array} p ElGamal prime
* @param {Uint8Array} g ElGamal group generator
* @param {Uint8Array} y ElGamal public key
* @param {Uint8Array} x ElGamal private exponent
* @returns {Promise<Boolean>} whether params are valid
* @async
*/
validateParams: async function (p, g, y, x) {
p = new BN(p);
g = new BN(g);
y = new BN(y);
const one = new BN(1);
// Check that 1 < g < p
if (g.lte(one) || g.gte(p)) {
return false;
}
// Expect p-1 to be large
const pSize = p.subn(1).bitLength();
if (pSize < 1023) {
return false;
}
const pred = new BN.red(p);
const gModP = g.toRed(pred);
/**
* g should have order p-1
* Check that g ** (p-1) = 1 mod p
*/
if (!gModP.redPow(p.subn(1)).eq(one)) {
return false;
}
/**
* Since p-1 is not prime, g might have a smaller order that divides p-1
* We want to make sure that the order is large enough to hinder a small subgroup attack
*
* We just check g**i != 1 for all i up to a threshold
*/
let res = g;
const i = new BN(1);
const threshold = new BN(2).shln(17); // we want order > threshold
while (i.lt(threshold)) {
res = res.mul(g).mod(p);
if (res.eqn(1)) {
return false;
}
i.iaddn(1);
}
/**
* Re-derive public key y' = g ** x mod p
* Expect y == y'
*
* Blinded exponentiation computes g**{r(p-1) + x} to compare to y
*/
x = new BN(x);
const r = await random.getRandomBN(new BN(2).shln(pSize - 1), new BN(2).shln(pSize)); // draw r of same size as p-1
const rqx = p.subn(1).mul(r).add(x);
if (!y.eq(gModP.redPow(rqx))) {
return false;
}
return true;
}
};

View File

@ -34,7 +34,7 @@ import random from '../../random';
import enums from '../../../enums';
import util from '../../../util';
import OID from '../../../type/oid';
import { getIndutnyCurve } from './indutnyKey';
import { keyFromPublic, keyFromPrivate, getIndutnyCurve } from './indutnyKey';
const webCrypto = util.getWebCrypto();
const nodeCrypto = util.getNodeCrypto();
@ -105,7 +105,7 @@ const curves = {
},
curve25519: {
oid: [0x06, 0x0A, 0x2B, 0x06, 0x01, 0x04, 0x01, 0x97, 0x55, 0x01, 0x05, 0x01],
keyType: enums.publicKey.ecdsa,
keyType: enums.publicKey.ecdh,
hash: enums.hash.sha256,
cipher: enums.symmetric.aes128,
node: false, // nodeCurves.curve25519 TODO
@ -228,10 +228,73 @@ function getPreferredHashAlgo(oid) {
return curves[enums.write(enums.curve, oid.toHex())].hash;
}
/**
* Validate ECDH and EcDSA parameters
* Not suitable for EdDSA (different secret key format)
* @param {module:enums.publicKey} algo EC algorithm, to filter supported curves
* @param {module:type/oid} oid EC object identifier
* @param {Uint8Array} Q EC public point
* @param {Uint8Array} d EC secret scalar
* @returns {Promise<Boolean>} whether params are valid
* @async
*/
async function validateStandardParams(algo, oid, Q, d) {
const supportedCurves = {
p256: true,
p384: true,
p521: true,
secp256k1: true,
curve25519: algo === enums.publicKey.ecdh,
brainpoolP256r1: true,
brainpoolP384r1: true,
brainpoolP512r1: true
};
// Check whether the given curve is supported
const curveName = oid.getName();
if (!supportedCurves[curveName]) {
return false;
}
if (curveName === 'curve25519') {
d = d.slice().reverse();
// Re-derive public point Q'
const { publicKey } = nacl.box.keyPair.fromSecretKey(d);
Q = new Uint8Array(Q);
const dG = new Uint8Array([0x40, ...publicKey]); // Add public key prefix
if (!util.equalsUint8Array(dG, Q)) {
return false;
}
return true;
}
const curve = await getIndutnyCurve(curveName);
try {
// Parse Q and check that it is on the curve but not at infinity
Q = keyFromPublic(curve, Q).getPublic();
} catch (validationErrors) {
return false;
}
/**
* Re-derive public point Q' = dG from private key
* Expect Q == Q'
*/
d = new BN(d);
const dG = keyFromPrivate(curve, d).getPublic();
if (!dG.eq(Q)) {
return false;
}
return true;
}
export default Curve;
export {
curves, webCurves, nodeCurves, generate, getPreferredHashAlgo, jwkToRawPublic, rawPublicToJwk, privateToJwk
curves, webCurves, nodeCurves, generate, getPreferredHashAlgo, jwkToRawPublic, rawPublicToJwk, privateToJwk, validateStandardParams
};
//////////////////////////

View File

@ -32,7 +32,7 @@
import BN from 'bn.js';
import nacl from 'tweetnacl/nacl-fast-light.js';
import Curve, { jwkToRawPublic, rawPublicToJwk, privateToJwk } from './curves';
import Curve, { jwkToRawPublic, rawPublicToJwk, privateToJwk, validateStandardParams } from './curves';
import aes_kw from '../../aes_kw';
import cipher from '../../cipher';
import random from '../../random';
@ -44,6 +44,18 @@ import { keyFromPublic, keyFromPrivate, getIndutnyCurve } from './indutnyKey';
const webCrypto = util.getWebCrypto();
const nodeCrypto = util.getNodeCrypto();
/**
* Validate ECDH parameters
* @param {module:type/oid} oid Elliptic curve object identifier
* @param {Uint8Array} Q ECDH public point
* @param {Uint8Array} d ECDH secret scalar
* @returns {Promise<Boolean>} whether params are valid
* @async
*/
async function validateParams(oid, Q, d) {
return validateStandardParams(enums.publicKey.ecdh, oid, Q, d);
}
// Build Param for ECDH algorithm (RFC 6637)
function buildEcdhParam(public_algo, oid, kdfParams, fingerprint) {
return util.concatUint8Array([
@ -55,6 +67,31 @@ function buildEcdhParam(public_algo, oid, kdfParams, fingerprint) {
]);
}
/**
* Parses MPI params and returns them as byte arrays of fixed length
* @param {Array} params key parameters
* @returns {Object} parameters in the form
* { oid, kdfParams, d: Uint8Array, Q: Uint8Array }
*/
function parseParams(params) {
if (params.length < 3 || params.length > 4) {
throw new Error('Unexpected number of parameters');
}
const oid = params[0];
const curve = new Curve(oid);
const parsedParams = { oid };
// The public point never has leading zeros, as it is prefixed by 0x40 or 0x04
parsedParams.Q = params[1].toUint8Array();
parsedParams.kdfParams = params[2];
if (params.length === 4) {
parsedParams.d = params[3].toUint8Array('be', curve.payloadSize);
}
return parsedParams;
}
// Key Derivation Function (RFC 6637)
async function kdf(hash_algo, X, length, param, stripLeading = false, stripTrailing = false) {
// Note: X is little endian for Curve25519, big-endian for all others.
@ -374,4 +411,4 @@ async function nodePublicEphemeralKey(curve, Q) {
return { publicKey, sharedKey };
}
export default { encrypt, decrypt, genPublicEphemeralKey, genPrivateEphemeralKey, buildEcdhParam, kdf, webPublicEphemeralKey, webPrivateEphemeralKey, ellipticPublicEphemeralKey, ellipticPrivateEphemeralKey, nodePublicEphemeralKey, nodePrivateEphemeralKey };
export default { encrypt, decrypt, genPublicEphemeralKey, genPrivateEphemeralKey, buildEcdhParam, kdf, webPublicEphemeralKey, webPrivateEphemeralKey, ellipticPublicEphemeralKey, ellipticPrivateEphemeralKey, nodePublicEphemeralKey, nodePrivateEphemeralKey, validateParams, parseParams };

View File

@ -28,7 +28,9 @@
import BN from 'bn.js';
import enums from '../../../enums';
import util from '../../../util';
import Curve, { webCurves, privateToJwk, rawPublicToJwk } from './curves';
import random from '../../random';
import hash from '../../hash';
import Curve, { webCurves, privateToJwk, rawPublicToJwk, validateStandardParams } from './curves';
import { getIndutnyCurve, keyFromPrivate, keyFromPublic } from './indutnyKey';
const webCrypto = util.getWebCrypto();
@ -57,7 +59,13 @@ async function sign(oid, hash_algo, message, publicKey, privateKey, hashed) {
// Need to await to make sure browser succeeds
return await webSign(curve, hash_algo, message, keyPair);
} catch (err) {
util.print_debug_error("Browser did not support signing: " + err.message);
// We do not fallback if the error is related to key integrity
// Unfortunaley Safari does not support p521 and throws a DataError when using it
// So we need to always fallback for that curve
if (curve.name !== 'p521' && (err.name === 'DataError' || err.name === 'OperationError')) {
throw err;
}
util.print_debug_error("Browser did not support verifying: " + err.message);
}
break;
}
@ -94,6 +102,12 @@ async function verify(oid, hash_algo, signature, message, publicKey, hashed) {
// Need to await to make sure browser succeeds
return await webVerify(curve, hash_algo, signature, message, publicKey);
} catch (err) {
// We do not fallback if the error is related to key integrity
// Unfortunaley Safari does not support p521 and throws a DataError when using it
// So we need to always fallback for that curve
if (curve.name !== 'p521' && (err.name === 'DataError' || err.name === 'OperationError')) {
throw err;
}
util.print_debug_error("Browser did not support verifying: " + err.message);
}
break;
@ -105,7 +119,66 @@ async function verify(oid, hash_algo, signature, message, publicKey, hashed) {
return ellipticVerify(curve, signature, digest, publicKey);
}
export default { sign, verify, ellipticVerify, ellipticSign };
/**
* Validate EcDSA parameters
* @param {module:type/oid} oid Elliptic curve object identifier
* @param {Uint8Array} Q EcDSA public point
* @param {Uint8Array} d EcDSA secret scalar
* @returns {Promise<Boolean>} whether params are valid
* @async
*/
async function validateParams(oid, Q, d) {
const curve = new Curve(oid);
// Reject curves x25519 and ed25519
if (curve.keyType !== enums.publicKey.ecdsa) {
return false;
}
// To speed up the validation, we try to use node- or webcrypto when available
// and sign + verify a random message
switch (curve.type) {
case 'web':
case 'node': {
const message = await random.getRandomBytes(8);
const hashAlgo = enums.hash.sha256;
const hashed = await hash.digest(hashAlgo, message);
try {
const signature = await sign(oid, hashAlgo, message, Q, d, hashed);
return await verify(oid, hashAlgo, signature, message, Q, hashed);
} catch (err) {
return false;
}
}
default:
return validateStandardParams(enums.publicKey.ecdsa, oid, Q, d);
}
}
/**
* Parses MPI params and returns them as byte arrays of fixed length
* @param {Array} params key parameters
* @returns {Object} parameters in the form
* { oid, d: Uint8Array, Q: Uint8Array }
*/
function parseParams(params) {
if (params.length < 2 || params.length > 3) {
throw new Error('Unexpected number of parameters');
}
const oid = params[0];
const curve = new Curve(oid);
const parsedParams = { oid };
// The public point never has leading zeros, as it is prefixed by 0x40 or 0x04
parsedParams.Q = params[1].toUint8Array();
if (params.length === 3) {
parsedParams.d = params[2].toUint8Array('be', curve.payloadSize);
}
return parsedParams;
}
export default { sign, verify, ellipticVerify, ellipticSign, validateParams, parseParams };
//////////////////////////

View File

@ -69,4 +69,51 @@ async function verify(oid, hash_algo, { R, S }, m, publicKey, hashed) {
return nacl.sign.detached.verify(hashed, signature, publicKey.subarray(1));
}
export default { sign, verify };
/**
* Validate EdDSA parameters
* @param {module:type/oid} oid Elliptic curve object identifier
* @param {Uint8Array} Q EdDSA public point
* @param {Uint8Array} k EdDSA secret seed
* @returns {Promise<Boolean>} whether params are valid
* @async
*/
async function validateParams(oid, Q, k) {
// Check whether the given curve is supported
if (oid.getName() !== 'ed25519') {
return false;
}
/**
* Derive public point Q' = dG from private key
* and expect Q == Q'
*/
const { publicKey } = nacl.sign.keyPair.fromSeed(k);
const dG = new Uint8Array([0x40, ...publicKey]); // Add public key prefix
return util.equalsUint8Array(Q, dG);
}
/**
* Parses MPI params and returns them as byte arrays of fixed length
* @param {Array} params key parameters
* @returns {Object} parameters in the form
* { oid, seed: Uint8Array, Q: Uint8Array }
*/
function parseParams(params) {
if (params.length < 2 || params.length > 3) {
throw new Error('Unexpected number of parameters');
}
const parsedParams = {
oid: params[0],
Q: params[1].toUint8Array('be', 33)
};
if (params.length === 3) {
parsedParams.seed = params[2].toUint8Array('be', 32);
}
return parsedParams;
}
export default { sign, verify, validateParams, parseParams };

View File

@ -286,6 +286,55 @@ export default {
};
},
/**
* Validate RSA parameters
* @param {Uint8Array} n RSA public modulus
* @param {Uint8Array} e RSA public exponent
* @param {Uint8Array} d RSA private exponent
* @param {Uint8Array} p RSA private prime p
* @param {Uint8Array} q RSA private prime q
* @param {Uint8Array} u RSA inverse of p w.r.t. q
* @returns {Promise<Boolean>} whether params are valid
* @async
*/
validateParams: async function (n, e, d, p, q, u) {
n = new BN(n);
p = new BN(p);
q = new BN(q);
// expect pq = n
if (!p.mul(q).eq(n)) {
return false;
}
const one = new BN(1);
const two = new BN(2);
// expect p*u = 1 mod q
u = new BN(u);
if (!p.mul(u).umod(q).eq(one)) {
return false;
}
e = new BN(e);
d = new BN(d);
/**
* In RSA pkcs#1 the exponents (d, e) are inverses modulo lcm(p-1, q-1)
* We check that [de = 1 mod (p-1)] and [de = 1 mod (q-1)]
* By CRT on coprime factors of (p-1, q-1) it follows that [de = 1 mod lcm(p-1, q-1)]
*
* We blind the multiplication with r, and check that rde = r mod lcm(p-1, q-1)
*/
const r = await random.getRandomBN(two, two.shln(n.bitLength() / 3)); // r in [ 2, 2^{|n|/3} ) < p and q
const rde = r.mul(d).mul(e);
const areInverses = rde.umod(p.sub(one)).eq(r) && rde.umod(q.sub(one)).eq(r);
if (!areInverses) {
return false;
}
return true;
},
bnSign: async function (hash_algo, n, d, hashed) {
n = new BN(n);
const m = new BN(await pkcs1.emsa.encode(hash_algo, hashed, n.byteLength()), 16);

View File

@ -51,19 +51,17 @@ export default {
return publicKey.dsa.verify(hash_algo, r, s, hashed, g, p, q, y);
}
case enums.publicKey.ecdsa: {
const oid = pub_MPIs[0];
const { oid, Q } = publicKey.elliptic.ecdsa.parseParams(pub_MPIs);
const signature = { r: msg_MPIs[0].toUint8Array(), s: msg_MPIs[1].toUint8Array() };
const Q = pub_MPIs[1].toUint8Array();
return publicKey.elliptic.ecdsa.verify(oid, hash_algo, signature, data, Q, hashed);
}
case enums.publicKey.eddsa: {
const oid = pub_MPIs[0];
const { oid, Q } = publicKey.elliptic.eddsa.parseParams(pub_MPIs);
// EdDSA signature params are expected in little-endian format
const signature = {
R: msg_MPIs[0].toUint8Array('le', 32),
S: msg_MPIs[1].toUint8Array('le', 32)
};
const Q = pub_MPIs[1].toUint8Array('be', 33);
return publicKey.elliptic.eddsa.verify(oid, hash_algo, signature, data, Q, hashed);
}
default:
@ -117,9 +115,7 @@ export default {
throw new Error('Signing with Elgamal is not defined in the OpenPGP standard.');
}
case enums.publicKey.ecdsa: {
const oid = key_params[0];
const Q = key_params[1].toUint8Array();
const d = key_params[2].toUint8Array();
const { oid, Q, d } = publicKey.elliptic.ecdsa.parseParams(key_params);
const signature = await publicKey.elliptic.ecdsa.sign(oid, hash_algo, data, Q, d, hashed);
return util.concatUint8Array([
util.Uint8Array_to_MPI(signature.r),
@ -127,10 +123,8 @@ export default {
]);
}
case enums.publicKey.eddsa: {
const oid = key_params[0];
const Q = key_params[1].toUint8Array('be', 33);
const d = key_params[2].toUint8Array('be', 32);
const signature = await publicKey.elliptic.eddsa.sign(oid, hash_algo, data, Q, d, hashed);
const { oid, Q, seed } = publicKey.elliptic.eddsa.parseParams(key_params);
const signature = await publicKey.elliptic.eddsa.sign(oid, hash_algo, data, Q, seed, hashed);
return util.concatUint8Array([
util.Uint8Array_to_MPI(signature.R),
util.Uint8Array_to_MPI(signature.S)

View File

@ -339,6 +339,42 @@ Key.prototype.getEncryptionKey = async function(keyId, date = new Date(), userId
throw util.wrapError('Could not find valid encryption key packet in key ' + this.getKeyId().toHex(), exception);
};
/**
* Returns all keys that are available for decryption, matching the keyId when given
* This is useful to retrieve keys for session key decryption
* @param {module:type/keyid} keyId, optional
* @param {Date} date, optional
* @param {String} userId, optional
* @returns {Promise<Array<module:key.Key|module:key~SubKey>>} array of decryption keys
* @async
*/
Key.prototype.getDecryptionKeys = async function(keyId, date = new Date(), userId = {}) {
await this.verifyPrimaryKey(date, userId);
const primaryKey = this.keyPacket;
const keys = [];
for (let i = 0; i < this.subKeys.length; i++) {
if (!keyId || this.subKeys[i].getKeyId().equals(keyId, true)) {
try {
await this.subKeys[i].verify(primaryKey, date);
const dataToVerify = { key: primaryKey, bind: this.subKeys[i].keyPacket };
const bindingSignature = await helper.getLatestValidSignature(this.subKeys[i].bindingSignatures, primaryKey, enums.signature.subkey_binding, dataToVerify, date);
if (bindingSignature && helper.isValidEncryptionKeyPacket(this.subKeys[i].keyPacket, bindingSignature)) {
keys.push(this.subKeys[i]);
}
} catch (e) {}
}
}
// evaluate primary key
const primaryUser = await this.getPrimaryUser(date, userId);
if ((!keyId || primaryKey.getKeyId().equals(keyId, true)) &&
helper.isValidEncryptionKeyPacket(primaryKey, primaryUser.selfCertification)) {
keys.push(this);
}
return keys;
};
/**
* Encrypts all secret key and subkey packets matching keyId
* @param {String|Array<String>} passphrases - if multiple passphrases, then should be in same order as packets each should encrypt
@ -370,6 +406,7 @@ Key.prototype.encrypt = async function(passphrases, keyId = null) {
* @param {String|Array<String>} passphrases
* @param {module:type/keyid} keyId
* @returns {Promise<Boolean>} true if all matching key and subkey packets decrypted successfully
* @throws {Error} if any matching key or subkey packets did not decrypt successfully
* @async
*/
Key.prototype.decrypt = async function(passphrases, keyId = null) {
@ -384,6 +421,8 @@ Key.prototype.decrypt = async function(passphrases, keyId = null) {
await Promise.all(passphrases.map(async function(passphrase) {
try {
await key.keyPacket.decrypt(passphrase);
// If we are decrypting a single key packet, we also validate it directly
if (keyId) await key.keyPacket.validate();
decrypted = true;
} catch (e) {
error = e;
@ -394,31 +433,55 @@ Key.prototype.decrypt = async function(passphrases, keyId = null) {
}
return decrypted;
}));
if (!keyId) {
// The full key should be decrypted and we can validate it all
await this.validate();
}
return results.every(result => result === true);
};
/**
* Check whether the private and public key parameters of the primary key match
* @returns {Promise<Boolean>} true if the primary key parameters correspond
* Check whether the private and public primary key parameters correspond
* Together with verification of binding signatures, this guarantees key integrity
* In case of gnu-dummy primary key, it is enough to validate any signing subkeys
* otherwise all encryption subkeys are validated
* If only gnu-dummy keys are found, we cannot properly validate so we throw an error
* @throws {Error} if validation was not successful and the key cannot be trusted
* @async
*/
Key.prototype.validate = async function() {
if (!this.isPrivate()) {
throw new Error("Can't validate a public key");
throw new Error("Cannot validate a public key");
}
const signingKeyPacket = this.primaryKey;
if (!signingKeyPacket.isDecrypted()) {
throw new Error("Key is not decrypted");
let signingKeyPacket;
if (!this.keyPacket.isDummy()) {
signingKeyPacket = this.primaryKey;
} else {
/**
* It is enough to validate any signing keys
* since its binding signatures are also checked
*/
const signingKey = await this.getSigningKey(null, null);
// This could again be a dummy key
if (signingKey && !signingKey.keyPacket.isDummy()) {
signingKeyPacket = signingKey.keyPacket;
}
}
if (signingKeyPacket) {
return signingKeyPacket.validate();
} else {
const keys = this.getKeys();
const allDummies = keys.map(key => key.keyPacket.isDummy()).every(Boolean);
if (allDummies) {
throw new Error("Cannot validate an all-gnu-dummy key");
}
return Promise.all(keys.map(async key => key.keyPacket.validate()));
}
const data = new packet.Literal();
data.setBytes(new Uint8Array(), 'binary');
const signature = new packet.Signature();
signature.publicKeyAlgorithm = signingKeyPacket.algorithm;
signature.hashAlgorithm = enums.hash.sha256;
const signatureType = enums.signature.binary;
signature.signatureType = signatureType;
await signature.sign(signingKeyPacket, data);
await signature.verify(signingKeyPacket, signatureType, data);
};
/**

View File

@ -196,7 +196,8 @@ Message.prototype.decryptSessionKeys = async function(privateKeys, passwords) {
}
} catch (e) {}
const privateKeyPackets = privateKey.getKeys(keyPacket.publicKeyId).map(key => key.keyPacket);
// do not check key expiration to allow decryption of old messages
const privateKeyPackets = (await privateKey.getDecryptionKeys(keyPacket.publicKeyId, null)).map(key => key.keyPacket);
await Promise.all(privateKeyPackets.map(async function(privateKeyPacket) {
if (!privateKeyPacket) {
return;

View File

@ -233,7 +233,7 @@ SecretKey.prototype.write = function () {
}
arr.push(new Uint8Array(optionalFieldsArr));
if (!this.s2k || this.s2k.type !== 'gnu-dummy') {
if (!this.isDummy()) {
if (!this.s2k_usage) {
const cleartextParams = write_cleartext_params(this.params, this.algorithm);
this.keyMaterial = util.concatUint8Array([
@ -259,6 +259,14 @@ SecretKey.prototype.isDecrypted = function() {
return this.isEncrypted === false;
};
/**
* Check whether this is a gnu-dummy key
* @returns {Boolean}
*/
SecretKey.prototype.isDummy = function() {
return !!(this.s2k && this.s2k.type === 'gnu-dummy');
};
/**
* Encrypt the payload. By default, we use aes256 and iterated, salted string
* to key specifier. If the key is in a decrypted state (isEncrypted === false)
@ -269,7 +277,7 @@ SecretKey.prototype.isDecrypted = function() {
* @async
*/
SecretKey.prototype.encrypt = async function (passphrase) {
if (this.s2k && this.s2k.type === 'gnu-dummy') {
if (this.isDummy()) {
return false;
}
@ -324,7 +332,7 @@ async function produceEncryptionKey(s2k, passphrase, algorithm) {
* @async
*/
SecretKey.prototype.decrypt = async function (passphrase) {
if (this.s2k && this.s2k.type === 'gnu-dummy') {
if (this.isDummy()) {
this.isEncrypted = false;
return false;
}
@ -380,6 +388,27 @@ SecretKey.prototype.generate = async function (bits, curve) {
this.isEncrypted = false;
};
/**
* Checks that the key parameters are consistent
* @throws {Error} if validation was not successful
* @async
*/
SecretKey.prototype.validate = async function () {
if (this.isDummy()) {
return;
}
if (!this.isDecrypted()) {
throw new Error('Key is not decrypted');
}
const algo = enums.write(enums.publicKey, this.algorithm);
const validParams = await crypto.validateParams(algo, this.params);
if (!validParams) {
throw new Error('Key is invalid');
}
};
/**
* Clear private key parameters
*/

View File

@ -93,6 +93,7 @@ function AsyncProxy({ path = 'openpgp.worker.js', n = 1, workers = [], config }
worker.onmessage = handleMessage(workerId++);
worker.onerror = e => {
worker.loadedResolve(false);
// eslint-disable-next-line no-console
console.error('Unhandled error in openpgp worker: ' + e.message + ' (' + e.filename + ':' + e.lineno + ')');
return false;
};

View File

@ -10,4 +10,5 @@ describe('Crypto', function () {
require('./eax.js');
require('./ocb.js');
require('./rsa.js');
require('./validate.js');
});

387
test/crypto/validate.js Normal file
View File

@ -0,0 +1,387 @@
const openpgp = typeof window !== 'undefined' && window.openpgp ? window.openpgp : require('../../dist/openpgp');
const chai = require('chai');
const BN = require('bn.js');
chai.use(require('chai-as-promised'));
const expect = chai.expect;
const armoredDSAKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
lQNTBF69PO8RCACHP4KLQcYOPGsGV9owTZvxnvHvvrY8W0v8xDUL3y6CLc05srF1
kQp/81iUfP5g57BEiDpJV95kMh+ulBthIOGnuMCkodJjuBICB4K6BtFTV4Fw1Q5S
S7aLC9beCaMvvGHXsK6MbknYl+IVJY7Zmml1qUSrBIQFGp5kqdhIX4o+OrzZ1zYj
ALicqzD7Zx2VRjGNQv7UKv4CkBOC8ncdnq/4/OQeOYFzVbCOf+sJhTgz6yxjHJVC
fLk7w8l2v1zV11VJuc8cQiQ9g8tjbKgLMsbyzy7gl4m9MSCdinG36XZuPibZrSm0
H8gKAdd1FT84a3/qU2rtLLR0y8tCxBj89Xx/AQCv7CDmwoU+/yGpBVVl1mh0ZUkA
/VJUhnJfv5MIOIi3AQf8CS9HrEmYJg/A3z0DcvcwIu/9gqpRLTqH1iT5o4BCg2j+
Cog2ExYkQl1OEPkEQ1lKJSnD8MDwO3BlkJ4cD0VSKxlnwd9dsu9m2+F8T+K1hoA7
PfH89TjD5HrEaGAYIdivLYSwoTNOO+fY8FoVC0RR9pFNOmjiTU5PZZedOxAql5Os
Hp2bYhky0G9trjo8Mt6CGhvgA3dAKyONftLQr9HSM0GKacFV+nRd9TGCPNZidKU8
MDa/SB/08y1bBGX5FK5wwiZ6H5qD8VAUobH3kwKlrg0nL00/EqtYHJqvJ2gkT5/v
h8+z4R4TuYiy4kKF2FLPd5OjdA31IVDoVgCwF0WHLgf/X9AiTr/DPs/5dIYN1+hf
UJwqjzr3dlokRwx3CVDcOVsdkWRwb8cvxubbsIorvUrF02IhYjHJMjIHT/zFt2zA
+VPzO4zabUlawWVepPEwrCtXgvn9aXqjhAYbilG3UZamhfstGUmbmvWVDadALwby
EO8u2pfLhI2lep63V/+KtUOLhfk8jKRSvxvxlYAvMi7sK8kB+lYy17XKN+IMYgf8
gMFV6XGKpdmMSV3jOvat8cI6vnRO0i+g3jANP3PfrFEivat/rVgxo67r4rxezfFn
J29qwB9rgbRgMBGsbDvIlQNV/NWFvHy2uQAEKn5eX4CoLsCZoR2VfK3BwBCxhYDp
/wAA/0GSmI9MlMnLadFNlcX2Bm4i15quZAGF8JxwHbj1dhdUEYq0E1Rlc3QgPHRl
c3RAdGVzdC5pbz6IlAQTEQgAPBYhBAq6lCI5EfrbHP1qZCxnOy/rlEGVBQJevTzv
AhsDBQsJCAcCAyICAQYVCgkICwIEFgIDAQIeBwIXgAAKCRAsZzsv65RBlUPoAP9Q
aTCWpHWZkvZzC8VU64O76fHp31rLWlcZFttuDNLyeAEAhOxkQHk6GR88R+EF5mrn
clr63t9Q4wreqOlO0NR5/9k=
=UW2O
-----END PGP PRIVATE KEY BLOCK-----
`;
const armoredElGamalKey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
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
vqBGKJzmO5q3cECw
=X9kJ
-----END PGP PRIVATE KEY BLOCK-----`;
describe('EdDSA parameter validation', function() {
let keyParams;
before(async () => {
keyParams = await openpgp.crypto.generateParams(openpgp.enums.publicKey.eddsa, null, 'ed25519');
});
it('EdDSA params should be valid', async function() {
const { oid, Q, seed } = openpgp.crypto.publicKey.elliptic.eddsa.parseParams(keyParams);
const valid = await openpgp.crypto.publicKey.elliptic.eddsa.validateParams(oid, Q, seed);
expect(valid).to.be.true;
});
it('detect invalid edDSA Q', async function() {
const { oid, Q, seed } = openpgp.crypto.publicKey.elliptic.eddsa.parseParams(keyParams);
Q[0]++;
let valid = await openpgp.crypto.publicKey.elliptic.eddsa.validateParams(oid, Q, seed);
expect(valid).to.be.false;
const infQ = new Uint8Array(Q.length);
valid = await openpgp.crypto.publicKey.elliptic.ecdh.validateParams(oid, infQ, seed);
expect(valid).to.be.false;
});
});
describe('ECC curve validation', function() {
it('EdDSA params are not valid for ECDH', async function() {
const keyParams = await openpgp.crypto.generateParams(
openpgp.enums.publicKey.eddsa,
null,
'ed25519'
);
const { oid, Q, seed } = openpgp.crypto.publicKey.elliptic.eddsa.parseParams(keyParams);
const valid = await openpgp.crypto.publicKey.elliptic.ecdsa.validateParams(oid, Q, seed);
expect(valid).to.be.false;
});
it('EdDSA params are not valid for EcDSA', async function() {
const keyParams = await openpgp.crypto.generateParams(
openpgp.enums.publicKey.eddsa,
null,
'ed25519'
);
const { oid, Q, seed } = openpgp.crypto.publicKey.elliptic.eddsa.parseParams(keyParams);
const valid = await openpgp.crypto.publicKey.elliptic.ecdsa.validateParams(oid, Q, seed);
expect(valid).to.be.false;
});
it('x25519 params are not valid for EcDSA', async function() {
const keyParams = await openpgp.crypto.generateParams(
openpgp.enums.publicKey.ecdsa,
null,
'curve25519'
);
const { oid, Q, d } = openpgp.crypto.publicKey.elliptic.ecdsa.parseParams(keyParams);
const valid = await openpgp.crypto.publicKey.elliptic.ecdsa.validateParams(oid, Q, d);
expect(valid).to.be.false;
});
it('EcDSA params are not valid for EdDSA', async function() {
const keyParams = await openpgp.crypto.generateParams(
openpgp.enums.publicKey.ecdsa, null, 'p256'
);
const { oid, Q, d } = openpgp.crypto.publicKey.elliptic.ecdsa.parseParams(keyParams);
const valid = await openpgp.crypto.publicKey.elliptic.eddsa.validateParams(oid, Q, d);
expect(valid).to.be.false;
});
it('x25519 params are not valid for EdDSA', async function() {
const keyParams = await openpgp.crypto.generateParams(
openpgp.enums.publicKey.ecdsa, null, 'curve25519'
);
const { oid, Q, d } = openpgp.crypto.publicKey.elliptic.ecdsa.parseParams(keyParams);
const valid = await openpgp.crypto.publicKey.elliptic.eddsa.validateParams(oid, Q, d);
expect(valid).to.be.false;
});
});
const curves = ['curve25519', 'p256', 'p384', 'p521', 'secp256k1', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1'];
curves.forEach(curve => {
describe(`ECC ${curve} parameter validation`, () => {
let keyParams;
before(async () => {
// we generate also ecdh params as ecdsa ones since we do not need the kdf params
keyParams = await openpgp.crypto.generateParams(
openpgp.enums.publicKey.ecdsa, null, curve
);
});
if (curve !== 'curve25519') {
it(`EcDSA ${curve} params should be valid`, async function() {
const { oid, Q, d } = openpgp.crypto.publicKey.elliptic.ecdsa.parseParams(keyParams);
const valid = await openpgp.crypto.publicKey.elliptic.ecdsa.validateParams(oid, Q, d);
expect(valid).to.be.true;
});
it('detect invalid EcDSA Q', async function() {
const { oid, Q, d } = openpgp.crypto.publicKey.elliptic.ecdsa.parseParams(keyParams);
Q[16]++;
let valid = await openpgp.crypto.publicKey.elliptic.ecdsa.validateParams(oid, Q, d);
expect(valid).to.be.false;
const infQ = new Uint8Array(Q.length);
valid = await openpgp.crypto.publicKey.elliptic.ecdsa.validateParams(oid, infQ, d);
expect(valid).to.be.false;
});
}
it(`ECDH ${curve} params should be valid`, async function() {
const { oid, Q, d } = openpgp.crypto.publicKey.elliptic.ecdsa.parseParams(keyParams);
const valid = await openpgp.crypto.publicKey.elliptic.ecdh.validateParams(oid, Q, d);
expect(valid).to.be.true;
});
it('detect invalid ECDH Q', async function() {
const { oid, Q, d } = openpgp.crypto.publicKey.elliptic.ecdsa.parseParams(keyParams);
Q[16]++;
let valid = await openpgp.crypto.publicKey.elliptic.ecdh.validateParams(oid, Q, d);
expect(valid).to.be.false;
const infQ = new Uint8Array(Q.length);
valid = await openpgp.crypto.publicKey.elliptic.ecdh.validateParams(oid, infQ, d);
expect(valid).to.be.false;
});
});
});
describe('RSA parameter validation', function() {
let keyParams;
before(async () => {
keyParams = await openpgp.crypto.generateParams(openpgp.enums.publicKey.rsa_sign, 2048);
});
it('generated RSA params are valid', async function() {
const n = keyParams[0].toUint8Array();
const e = keyParams[1].toUint8Array();
const d = keyParams[2].toUint8Array();
const p = keyParams[3].toUint8Array();
const q = keyParams[4].toUint8Array();
const u = keyParams[5].toUint8Array();
const valid = await openpgp.crypto.publicKey.rsa.validateParams(n, e, d, p, q, u);
expect(valid).to.be.true;
});
it('detect invalid RSA n', async function() {
const n = keyParams[0].toUint8Array();
const e = keyParams[1].toUint8Array();
const d = keyParams[2].toUint8Array();
const p = keyParams[3].toUint8Array();
const q = keyParams[4].toUint8Array();
const u = keyParams[5].toUint8Array();
n[0]++;
const valid = await openpgp.crypto.publicKey.rsa.validateParams(n, e, d, p, q, u);
expect(valid).to.be.false;
});
it('detect invalid RSA e', async function() {
const n = keyParams[0].toUint8Array();
const e = keyParams[1].toUint8Array();
const d = keyParams[2].toUint8Array();
const p = keyParams[3].toUint8Array();
const q = keyParams[4].toUint8Array();
const u = keyParams[5].toUint8Array();
e[0]++;
const valid = await openpgp.crypto.publicKey.rsa.validateParams(n, e, d, p, q, u);
expect(valid).to.be.false;
});
});
describe('DSA parameter validation', function() {
let dsaKey;
before(async () => {
dsaKey = (await openpgp.key.readArmored(armoredDSAKey)).keys[0];
});
it('DSA params should be valid', async function() {
const params = dsaKey.keyPacket.params;
const p = params[0].toUint8Array();
const q = params[1].toUint8Array();
const g = params[2].toUint8Array();
const y = params[3].toUint8Array();
const x = params[4].toUint8Array();
const valid = await openpgp.crypto.publicKey.dsa.validateParams(p, q, g, y, x);
expect(valid).to.be.true;
});
it('detect invalid DSA p', async function() {
const params = dsaKey.keyPacket.params;
const p = params[0].toUint8Array();
const q = params[1].toUint8Array();
const g = params[2].toUint8Array();
const y = params[3].toUint8Array();
const x = params[4].toUint8Array();
p[0]++;
const valid = await openpgp.crypto.publicKey.dsa.validateParams(p, q, g, y, x);
expect(valid).to.be.false;
});
it('detect invalid DSA y', async function() {
const params = dsaKey.keyPacket.params;
const p = params[0].toUint8Array();
const q = params[1].toUint8Array();
const g = params[2].toUint8Array();
const y = params[3].toUint8Array();
const x = params[4].toUint8Array();
y[0]++;
const valid = await openpgp.crypto.publicKey.dsa.validateParams(p, q, g, y, x);
expect(valid).to.be.false;
});
it('detect invalid DSA g', async function() {
const params = dsaKey.keyPacket.params;
const p = params[0].toUint8Array();
const q = params[1].toUint8Array();
const g = params[2].toUint8Array();
const y = params[3].toUint8Array();
const x = params[4].toUint8Array();
g[0]++;
let valid = await openpgp.crypto.publicKey.dsa.validateParams(p, q, g, y, x);
expect(valid).to.be.false;
const gOne = new Uint8Array([1]);
valid = await openpgp.crypto.publicKey.dsa.validateParams(p, q, gOne, y, x);
expect(valid).to.be.false;
});
});
describe('ElGamal parameter validation', function() {
let egKey;
before(async () => {
egKey = (await openpgp.key.readArmored(armoredElGamalKey)).keys[0].subKeys[0];
});
it('params should be valid', async function() {
const params = egKey.keyPacket.params;
const p = params[0].toUint8Array();
const g = params[1].toUint8Array();
const y = params[2].toUint8Array();
const x = params[3].toUint8Array();
const valid = await openpgp.crypto.publicKey.elgamal.validateParams(p, g, y, x);
expect(valid).to.be.true;
});
it('detect invalid p', async function() {
const params = egKey.keyPacket.params;
const p = params[0].toUint8Array();
const g = params[1].toUint8Array();
const y = params[2].toUint8Array();
const x = params[3].toUint8Array();
p[0]++;
const valid = await openpgp.crypto.publicKey.elgamal.validateParams(p, g, y, x);
expect(valid).to.be.false;
});
it('detect invalid y', async function() {
const params = egKey.keyPacket.params;
const p = params[0].toUint8Array();
const g = params[1].toUint8Array();
const y = params[2].toUint8Array();
const x = params[3].toUint8Array();
y[0]++;
const valid = await openpgp.crypto.publicKey.elgamal.validateParams(p, g, y, x);
expect(valid).to.be.false;
});
it('detect invalid g', async function() {
const params = egKey.keyPacket.params;
const p = params[0].toUint8Array();
const g = params[1].toUint8Array();
const y = params[2].toUint8Array();
const x = params[3].toUint8Array();
g[0]++;
let valid = await openpgp.crypto.publicKey.elgamal.validateParams(p, g, y, x);
expect(valid).to.be.false;
const gOne = new Uint8Array([1]);
valid = await openpgp.crypto.publicKey.elgamal.validateParams(p, gOne, y, x);
expect(valid).to.be.false;
});
it('detect g with small order', async function() {
const params = egKey.keyPacket.params;
const p = params[0].toUint8Array();
const g = params[1].toUint8Array();
const y = params[2].toUint8Array();
const x = params[3].toUint8Array();
const pBN = new BN(p);
const gModP = new BN(g).toRed(new BN.red(pBN));
// g**(p-1)/2 has order 2
const gOrd2 = gModP.redPow(pBN.subn(1).shrn(1));
const valid = await openpgp.crypto.publicKey.elgamal.validateParams(p, gOrd2.toArrayLike(Uint8Array, 'be'), y, x);
expect(valid).to.be.false;
});
});

View File

@ -1804,6 +1804,118 @@ AxMSJy5Dv9gcVPq6V8fuPw05ODSpbieoIF3d3WuaI39lAZpfuhNaSNAQmzA7
-----END PGP PRIVATE KEY BLOCK-----
`;
const gnuDummyKeySigningSubkey = `
-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: OpenPGP.js VERSION
Comment: https://openpgpjs.org
xZUEWCC+hwEEALu8GwefswqZLoiKJk1Nd1yKmVWBL1ypV35FN0gCjI1NyyJX
UfQZDdC2h0494OVAM2iqKepqht3tH2DebeFLnc2ivvIFmQJZDnH2/0nFG2gC
rSySWHUjVfbMSpmTaXpit8EX/rjNauGOdbePbezOSsAhW7R9pBdtDjPnq2Zm
vDXXABEBAAH+B2UAR05VAc0JR05VIER1bW15wrgEEwECACIFAlggvocCGwMG
CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEJ3XHFanUJgCeMYD/2zKefpl
clQoBdDPJKCYJm8IhuWuoF8SnHAsbhD+U42Gbm+2EATTPj0jyGPkZzl7a0th
S2rSjQ4JF0Ktgdr9585haknpGwr31t486KxXOY4AEsiBmRyvTbaQegwKaQ+C
/0JQYo/XKpsaX7PMDBB9SNFSa8NkhxYseLaB7gbM8w+Lx8EYBFggvpwBBADF
YeeJwp6MAVwVwXX/eBRKBIft6LC4E9czu8N2AbOW97WjWNtXi3OuM32OwKXq
vSck8Mx8FLOAuvVq41NEboeknhptw7HzoQMB35q8NxA9lvvPd0+Ef+BvaVB6
NmweHttt45LxYxLMdXdGoIt3wn/HBY81HnMqfV/KnggZ+imJ0wARAQABAAP7
BA56WdHzb53HIzYgWZl04H3BJdB4JU6/FJo0yHpjeWRQ46Q7w2WJzjHS6eBB
G+OhGzjAGYK7AUr8wgjqMq6LQHt2f80N/nWLusZ00a4lcMd7rvoHLWwRj80a
RzviOvvhP7kZY1TrhbS+Sl+BWaNIDOxS2maEkxexztt4GEl2dWUCAMoJvyFm
qPVqVx2Yug29vuJsDcr9XwnjrYI8PtszJI8Fr+5rKgWE3GJumheaXaug60dr
mLMXdvT/0lj3sXquqR0CAPoZ1Mn7GaUKjPVJ7CiJ/UjqSurrGhruA5ikhehQ
vUB+v4uIl7ICcX8zfiP+SMhWY9qdkmOvLSSSMcTkguMfe68B/j/qf2en5OHy
6NJgMIjMrBHvrf34f6pxw5p10J6nxjooZQxV0P+9MoTHWsy0r6Er8IOSSTGc
WyWJ8wmSqiq/dZSoJcLAfQQYAQIACQUCWCC+nAIbAgCoCRCd1xxWp1CYAp0g
BBkBAgAGBQJYIL6cAAoJEOYZSGiVA/C9CT4D/2Vq2dKxHmzn/UD1MWSLXUbN
ISd8tvHjoVg52RafdgHFmg9AbE0DW8ifwaai7FkifD0IXiN04nER3MuVhAn1
gtMu03m1AQyX/X39tHz+otpwBn0g57NhFbHFmzKfr/+N+XsDRj4VXn13hhqM
qQR8i1wgiWBUFJbpP5M1BPdH4Qfkcn8D/j8A3QKYGGETa8bNOdVTRU+sThXr
imOfWu58V1yWCmLE1kK66qkqmgRVUefqacF/ieMqNmsAY+zmR9D4fg2wzu/d
nPjJXp1670Vlzg7oT5XVYnfys7x4GLHsbaOSjXToILq+3GwI9UjNjtpobcfm
mNG2ibD6lftLOtDsVSDY8a6a
=KjxQ
-----END PGP PRIVATE KEY BLOCK-----
`;
const gnuDummyKey = `
-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: OpenPGP.js VERSION
Comment: https://openpgpjs.org
xZUEWCC+hwEEALu8GwefswqZLoiKJk1Nd1yKmVWBL1ypV35FN0gCjI1NyyJX
UfQZDdC2h0494OVAM2iqKepqht3tH2DebeFLnc2ivvIFmQJZDnH2/0nFG2gC
rSySWHUjVfbMSpmTaXpit8EX/rjNauGOdbePbezOSsAhW7R9pBdtDjPnq2Zm
vDXXABEBAAH+B2UAR05VAc0JR05VIER1bW15wrgEEwECACIFAlggvocCGwMG
CwkIBwMCBhUIAgkKCwQWAgMBAh4BAheAAAoJEJ3XHFanUJgCeMYD/2zKefpl
clQoBdDPJKCYJm8IhuWuoF8SnHAsbhD+U42Gbm+2EATTPj0jyGPkZzl7a0th
S2rSjQ4JF0Ktgdr9585haknpGwr31t486KxXOY4AEsiBmRyvTbaQegwKaQ+C
/0JQYo/XKpsaX7PMDBB9SNFSa8NkhxYseLaB7gbM8w+L
=yGSn
-----END PGP PRIVATE KEY BLOCK-----
`;
const eddsaKeyAsEcdsa = `
-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: OpenPGP.js VERSION
Comment: https://openpgpjs.org
xVgEXseu0BMJKwYBBAHaRw8BAQdA7MOW/AQa3aDGJJw9upY2Qv4cxv4MQHr5
81xmFwvyf+EAAQC0bOQsFDKCdAQ4cACKqbSDO2st+V9s3bEmSZV+fVR6ww44
zRFib2IgPGJvYkBib2IuY29tPsJ3BBATCgAgBQJex69xBgsJBwgDAgQVCAoC
BBYCAQACGQECGwMCHgEACgkQInpzQil4KrkPrQD7BkYxgDzyrfynM8mUSEdr
4iOivzbGo9zmn7cwluO2LI4A9iCJG4Xao9VFlyOAizVzNWlhfptwWVH5awh4
YFDlUSbHXQRex67QEgorBgEEAZdVAQUBAQdAI5fo7gn4y8IybTJ1m+xn90Xs
uqlIEDirKMx7WVJ/2QcDAQgHAAD/cPduuCXzoLkEI6Po4kTfoWXC6w6AEyGg
LA+BABNTPIAPp8JhBBgTCAAJBQJex69xAhsMAAoJECJ6c0IpeCq5D7EA/Ajk
xCrUtYPsmEDcr+1TFQCoOFGEKiYG6wNeuTaLqvPjAPsH8p3BnYBqlELdHU2I
9RFfjwPHLd+fZMQC6+mEaRJ4AA==
=2b6I
-----END PGP PRIVATE KEY BLOCK-----
`;
const dsaGnuDummyKeyWithElGamalSubkey = `-----BEGIN PGP PRIVATE KEY BLOCK-----
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
vqBGKJzmO5q3cECw
=X9kJ
-----END PGP PRIVATE KEY BLOCK-----`;
function versionSpecificTests() {
it('Preferences of generated key', function() {
const testPref = function(key) {
@ -2600,15 +2712,40 @@ describe('Key', function() {
expect(encryptExpirationTime).to.equal(Infinity);
});
it("validate() - don't throw if key parameters correspond", async function() {
const { key } = await openpgp.generateKey({ userIds: {}, curve: 'ed25519' });
await key.validate();
it("decrypt() - throw if key parameters don't correspond", async function() {
const { keys: [key] } = await openpgp.key.readArmored(mismatchingKeyParams);
await expect(key.decrypt('userpass')).to.be.rejectedWith('Key is invalid');
});
it("validate() - throw if key parameters don't correspond", async function() {
it("decrypt(keyId) - throw if key parameters don't correspond", async function() {
const { keys: [key] } = await openpgp.key.readArmored(mismatchingKeyParams);
await key.decrypt('userpass');
await expect(key.validate()).to.be.rejectedWith('Signature verification failed');
const subKeyId = key.subKeys[0].getKeyId()
await expect(key.decrypt('userpass', subKeyId)).to.be.rejectedWith('Key is invalid');
});
it("validate() - don't throw if key parameters correspond", async function() {
const { key } = await openpgp.generateKey({ userIds: {}, curve: 'ed25519' });
expect(key.validate()).to.not.be.rejected;
});
it("validate() - throw if all-gnu-dummy key", async function() {
const { keys: [key] } = await openpgp.key.readArmored(gnuDummyKey);
await expect(key.validate()).to.be.rejectedWith('Cannot validate an all-gnu-dummy key');
});
it("validate() - gnu-dummy primary key with signing subkey", async function() {
const { keys: [key] } = await openpgp.key.readArmored(gnuDummyKeySigningSubkey);
expect(key.validate()).to.not.be.rejected;
});
it("validate() - gnu-dummy primary key with encryption subkey", async function() {
const { keys: [key] } = await openpgp.key.readArmored(dsaGnuDummyKeyWithElGamalSubkey);
expect(key.validate()).to.not.be.rejected;
});
it("validate() - curve ed25519 (eddsa) cannot be used for ecdsa", async function() {
const { keys: [key] } = await openpgp.key.readArmored(eddsaKeyAsEcdsa);
expect(key.validate()).to.be.rejectedWith('Key is invalid');
});
it('clearPrivateParams() - check that private key can no longer be used', async function() {
@ -2625,10 +2762,10 @@ describe('Key', function() {
await key.clearPrivateParams();
key.primaryKey.isEncrypted = false;
key.primaryKey.params = params;
await expect(key.validate()).to.be.rejectedWith('Missing private key parameters');
await expect(key.validate()).to.be.rejectedWith('Missing key parameters');
});
it('clearPrivateParams() - check that private key parameters were zeroed out', async function() {
it('clearPrivateParams() - detect that private key parameters were zeroed out', async function() {
const { keys: [key] } = await openpgp.key.readArmored(priv_key_rsa);
await key.decrypt('hello world');
const params = key.primaryKey.params.slice();
@ -2637,11 +2774,8 @@ describe('Key', function() {
key.primaryKey.params = params;
const use_nativeVal = openpgp.config.use_native;
openpgp.config.use_native = false;
try {
await expect(key.validate()).to.be.rejectedWith('Signature verification failed');
} finally {
openpgp.config.use_native = use_nativeVal;
}
expect(key.validate()).to.be.rejectedWith('Key is invalid');
openpgp.config.use_native = use_nativeVal;
});
it('update() - throw error if fingerprints not equal', async function() {

View File

@ -931,6 +931,23 @@ describe('OpenPGP.js public api tests', function() {
});
});
it('should not decrypt with a key without binding signatures', function() {
return openpgp.encryptSessionKey({
data: sk,
algorithm: 'aes128',
publicKeys: publicKey.keys
}).then(async function(encrypted) {
const invalidPrivateKey = (await openpgp.key.readArmored(priv_key)).keys[0];
invalidPrivateKey.subKeys[0].bindingSignatures = [];
return openpgp.decryptSessionKeys({
message: encrypted.message,
privateKeys: invalidPrivateKey
}).catch(error => {
expect(error.message).to.match(/Error decrypting session keys: Session key decryption failed./);
});
});
});
it('roundtrip workflow: encrypt, decryptSessionKeys, decrypt with pgp key pair', function () {
let msgAsciiArmored;
return openpgp.encrypt({