diff --git a/.eslintrc.js b/.eslintrc.js index edb9ff46..38d93b09 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -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 } ], diff --git a/src/crypto/crypto.js b/src/crypto/crypto.js index d3bae0d3..c19dcba7 100644 --- a/src/crypto/crypto.js +++ b/src/crypto/crypto.js @@ -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} The array of types + * @param {module:enums.publicKey} algo The public key algorithm + * @returns {Array} 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} The array of types + * @param {module:enums.publicKey} algo The public key algorithm + * @returns {Array} 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} The array of types + * @param {module:enums.publicKey} algo The public key algorithm + * @returns {Array} 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 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. diff --git a/src/crypto/public_key/dsa.js b/src/crypto/public_key/dsa.js index 91b44cd8..15f1c94e 100644 --- a/src/crypto/public_key/dsa.js +++ b/src/crypto/public_key/dsa.js @@ -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} 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; } }; diff --git a/src/crypto/public_key/elgamal.js b/src/crypto/public_key/elgamal.js index 542208ff..692ac9f7 100644 --- a/src/crypto/public_key/elgamal.js +++ b/src/crypto/public_key/elgamal.js @@ -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} 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; } }; diff --git a/src/crypto/public_key/elliptic/curves.js b/src/crypto/public_key/elliptic/curves.js index f65dc8f1..4ba6b617 100644 --- a/src/crypto/public_key/elliptic/curves.js +++ b/src/crypto/public_key/elliptic/curves.js @@ -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} 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 }; ////////////////////////// diff --git a/src/crypto/public_key/elliptic/ecdh.js b/src/crypto/public_key/elliptic/ecdh.js index 834af7d5..9c9566c6 100644 --- a/src/crypto/public_key/elliptic/ecdh.js +++ b/src/crypto/public_key/elliptic/ecdh.js @@ -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} 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 }; diff --git a/src/crypto/public_key/elliptic/ecdsa.js b/src/crypto/public_key/elliptic/ecdsa.js index 6b3d4471..40ecbf1e 100644 --- a/src/crypto/public_key/elliptic/ecdsa.js +++ b/src/crypto/public_key/elliptic/ecdsa.js @@ -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} 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 }; ////////////////////////// diff --git a/src/crypto/public_key/elliptic/eddsa.js b/src/crypto/public_key/elliptic/eddsa.js index 8e431f79..7dc158ec 100644 --- a/src/crypto/public_key/elliptic/eddsa.js +++ b/src/crypto/public_key/elliptic/eddsa.js @@ -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} 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 }; diff --git a/src/crypto/public_key/rsa.js b/src/crypto/public_key/rsa.js index 95d0c1f9..68cbad74 100644 --- a/src/crypto/public_key/rsa.js +++ b/src/crypto/public_key/rsa.js @@ -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} 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); diff --git a/src/crypto/signature.js b/src/crypto/signature.js index 615c418a..55976be4 100644 --- a/src/crypto/signature.js +++ b/src/crypto/signature.js @@ -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) diff --git a/src/key/key.js b/src/key/key.js index 86d55b15..8c8d29d4 100644 --- a/src/key/key.js +++ b/src/key/key.js @@ -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 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} 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} passphrases * @param {module:type/keyid} keyId * @returns {Promise} 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} 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); }; /** diff --git a/src/message.js b/src/message.js index 50256c65..f14738e2 100644 --- a/src/message.js +++ b/src/message.js @@ -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; diff --git a/src/packet/secret_key.js b/src/packet/secret_key.js index fade2f2a..2904f6a0 100644 --- a/src/packet/secret_key.js +++ b/src/packet/secret_key.js @@ -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 */ diff --git a/src/worker/async_proxy.js b/src/worker/async_proxy.js index 4b904a29..059f794b 100644 --- a/src/worker/async_proxy.js +++ b/src/worker/async_proxy.js @@ -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; }; diff --git a/test/crypto/index.js b/test/crypto/index.js index dba5377d..4f737fc2 100644 --- a/test/crypto/index.js +++ b/test/crypto/index.js @@ -10,4 +10,5 @@ describe('Crypto', function () { require('./eax.js'); require('./ocb.js'); require('./rsa.js'); + require('./validate.js'); }); diff --git a/test/crypto/validate.js b/test/crypto/validate.js new file mode 100644 index 00000000..e9fcc3bd --- /dev/null +++ b/test/crypto/validate.js @@ -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; + }); +}); diff --git a/test/general/key.js b/test/general/key.js index a6ab8f06..fff3019b 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -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() { diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 6b8eab1d..261dfb8f 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -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({