diff --git a/src/crypto/hkdf.js b/src/crypto/hkdf.js index 361703b1..69a85ae7 100644 --- a/src/crypto/hkdf.js +++ b/src/crypto/hkdf.js @@ -6,9 +6,8 @@ import enums from '../enums'; import util from '../util'; -const webCrypto = util.getWebCrypto(); - export default async function computeHKDF(hashAlgo, inputKey, salt, info, outLen) { + const webCrypto = util.getWebCrypto(); const hash = enums.read(enums.webHash, hashAlgo); if (!hash) throw new Error('Hash algo not supported with HKDF'); diff --git a/src/crypto/public_key/elliptic/ecdh.js b/src/crypto/public_key/elliptic/ecdh.js index 66937b2c..4b3d6606 100644 --- a/src/crypto/public_key/elliptic/ecdh.js +++ b/src/crypto/public_key/elliptic/ecdh.js @@ -30,9 +30,6 @@ import * as pkcs5 from '../../pkcs5'; import { getCipherParams } from '../../cipher'; import { generateEphemeralEncryptionMaterial as ecdhXGenerateEphemeralEncryptionMaterial, recomputeSharedSecret as ecdhXRecomputeSharedSecret } from './ecdh_x'; -const webCrypto = util.getWebCrypto(); -const nodeCrypto = util.getNodeCrypto(); - /** * Validate ECDH parameters * @param {module:type/oid} oid - Elliptic curve object identifier @@ -238,6 +235,7 @@ async function jsPublicEphemeralKey(curve, Q) { * @async */ async function webPrivateEphemeralKey(curve, V, Q, d) { + const webCrypto = util.getWebCrypto(); const recipient = privateToJWK(curve.payloadSize, curve.web, Q, d); let privateKey = webCrypto.importKey( 'jwk', @@ -289,6 +287,7 @@ async function webPrivateEphemeralKey(curve, V, Q, d) { * @async */ async function webPublicEphemeralKey(curve, Q) { + const webCrypto = util.getWebCrypto(); const jwk = rawPublicToJWK(curve.payloadSize, curve.web, Q); let keyPair = webCrypto.generateKey( { @@ -338,6 +337,7 @@ async function webPublicEphemeralKey(curve, Q) { * @async */ async function nodePrivateEphemeralKey(curve, V, d) { + const nodeCrypto = util.getNodeCrypto(); const recipient = nodeCrypto.createECDH(curve.node); recipient.setPrivateKey(d); const sharedKey = new Uint8Array(recipient.computeSecret(V)); @@ -354,6 +354,7 @@ async function nodePrivateEphemeralKey(curve, V, d) { * @async */ async function nodePublicEphemeralKey(curve, Q) { + const nodeCrypto = util.getNodeCrypto(); const sender = nodeCrypto.createECDH(curve.node); sender.generateKeys(); const sharedKey = new Uint8Array(sender.computeSecret(Q)); diff --git a/src/crypto/public_key/elliptic/ecdh_x.js b/src/crypto/public_key/elliptic/ecdh_x.js index 43ce91b2..ba959380 100644 --- a/src/crypto/public_key/elliptic/ecdh_x.js +++ b/src/crypto/public_key/elliptic/ecdh_x.js @@ -3,9 +3,7 @@ * @module crypto/public_key/elliptic/ecdh */ -import x25519 from '@openpgp/tweetnacl'; import * as aesKW from '../../aes_kw'; -import { getRandomBytes } from '../../random'; import enums from '../../../enums'; import util from '../../../util'; @@ -55,9 +53,9 @@ export async function generate(algo) { if (err.name !== 'NotSupportedError') { throw err; } + const { default: x25519 } = await import('@openpgp/tweetnacl'); // k stays in little-endian, unlike legacy ECDH over curve25519 - const k = getRandomBytes(32); - const { publicKey: A } = x25519.box.keyPair.fromSecretKey(k); + const { secretKey: k, publicKey: A } = x25519.box.keyPair(); return { A, k }; } @@ -82,14 +80,19 @@ export async function generate(algo) { */ export async function validateParams(algo, A, k) { switch (algo) { - case enums.publicKey.x25519: { - /** - * Derive public point A' from private key - * and expect A == A' - */ - const { publicKey } = x25519.box.keyPair.fromSecretKey(k); - return util.equalsUint8Array(A, publicKey); - } + case enums.publicKey.x25519: + // Validation is typically not run for ECDH, since encryption subkeys are only validated + // for gnu-dummy keys. + // So, for simplicity, we do an encrypt-decrypt round even if WebCrypto support is not available + try { + const { ephemeralPublicKey, sharedSecret } = await generateEphemeralEncryptionMaterial(algo, A); + const recomputedSharedSecret = await recomputeSharedSecret(algo, ephemeralPublicKey, A, k); + + return util.equalsUint8Array(sharedSecret, recomputedSharedSecret); + } catch (_) { + return false; + } + case enums.publicKey.x448: { const x448 = await util.getNobleCurve(enums.publicKey.x448); /** @@ -235,10 +238,10 @@ export async function generateEphemeralEncryptionMaterial(algo, recipientA) { if (err.name !== 'NotSupportedError') { throw err; } - const ephemeralSecretKey = getRandomBytes(getPayloadSize(algo)); + const { default: x25519 } = await import('@openpgp/tweetnacl'); + const { secretKey: ephemeralSecretKey, publicKey: ephemeralPublicKey } = x25519.box.keyPair(); const sharedSecret = x25519.scalarMult(ephemeralSecretKey, recipientA); assertNonZeroArray(sharedSecret); - const { publicKey: ephemeralPublicKey } = x25519.box.keyPair.fromSecretKey(ephemeralSecretKey); return { ephemeralPublicKey, sharedSecret }; } case enums.publicKey.x448: { @@ -273,6 +276,7 @@ export async function recomputeSharedSecret(algo, ephemeralPublicKey, A, k) { if (err.name !== 'NotSupportedError') { throw err; } + const { default: x25519 } = await import('@openpgp/tweetnacl'); const sharedSecret = x25519.scalarMult(k, ephemeralPublicKey); assertNonZeroArray(sharedSecret); return sharedSecret; diff --git a/src/crypto/public_key/elliptic/eddsa.js b/src/crypto/public_key/elliptic/eddsa.js index 86d9f17e..d94924b8 100644 --- a/src/crypto/public_key/elliptic/eddsa.js +++ b/src/crypto/public_key/elliptic/eddsa.js @@ -20,7 +20,6 @@ * @module crypto/public_key/elliptic/eddsa */ -import ed25519 from '@openpgp/tweetnacl'; import util from '../../../util'; import enums from '../../../enums'; import { getHashByteLength } from '../../hash'; @@ -59,7 +58,9 @@ export async function generate(algo) { if (err.name !== 'NotSupportedError') { throw err; } + const { default: ed25519 } = await import('@openpgp/tweetnacl'); const seed = getRandomBytes(getPayloadSize(algo)); + // not using `ed25519.sign.keyPair` since it returns the expanded secret, so using `fromSeed` instead is more straightforward const { publicKey: A } = ed25519.sign.keyPair.fromSeed(seed); return { A, seed }; } @@ -111,6 +112,7 @@ export async function sign(algo, hashAlgo, message, publicKey, privateKey, hashe if (err.name !== 'NotSupportedError') { throw err; } + const { default: ed25519 } = await import('@openpgp/tweetnacl'); const secretKey = util.concatUint8Array([privateKey, publicKey]); const signature = ed25519.sign.detached(hashed, secretKey); return { RS: signature }; @@ -157,6 +159,7 @@ export async function verify(algo, hashAlgo, { RS }, m, publicKey, hashed) { if (err.name !== 'NotSupportedError') { throw err; } + const { default: ed25519 } = await import('@openpgp/tweetnacl'); return ed25519.sign.detached.verify(hashed, RS, publicKey); } @@ -179,15 +182,34 @@ export async function verify(algo, hashAlgo, { RS }, m, publicKey, hashed) { */ export async function validateParams(algo, A, seed) { switch (algo) { - case enums.publicKey.ed25519: { - /** - * Derive public point A' from private key - * and expect A == A' - * TODO: move to sign-verify using WebCrypto (same as ECDSA) when curve is more widely implemented - */ - const { publicKey } = ed25519.sign.keyPair.fromSeed(seed); - return util.equalsUint8Array(A, publicKey); - } + case enums.publicKey.ed25519: + // If webcrypto support is available, we sign-verify random data, as the import-export + // functions might not implement validity checks. + // If we need to fallback to JS, we instead only re-derive the public key, + // as this is much faster than sign-verify. + try { + const webCrypto = util.getWebCrypto(); + const jwkPrivate = privateKeyToJWK(algo, A, seed); + const jwkPublic = publicKeyToJWK(algo, A); + + const privateCryptoKey = await webCrypto.importKey('jwk', jwkPrivate, 'Ed25519', false, ['sign']); + const publicCryptoKey = await webCrypto.importKey('jwk', jwkPublic, 'Ed25519', false, ['verify']); + + const randomData = getRandomBytes(8); + const signature = new Uint8Array( + await webCrypto.sign('Ed25519', privateCryptoKey, randomData) + ); + + const verified = await webCrypto.verify('Ed25519', publicCryptoKey, signature, randomData); + return verified; + } catch (err) { + if (err.name !== 'NotSupportedError') { + return false; + } + const { default: ed25519 } = await import('@openpgp/tweetnacl'); + const { publicKey } = ed25519.sign.keyPair.fromSeed(seed); + return util.equalsUint8Array(A, publicKey); + } case enums.publicKey.ed448: { const ed448 = await util.getNobleCurve(enums.publicKey.ed448); diff --git a/src/crypto/public_key/elliptic/eddsa_legacy.js b/src/crypto/public_key/elliptic/eddsa_legacy.js index 682f53bb..9ab45da8 100644 --- a/src/crypto/public_key/elliptic/eddsa_legacy.js +++ b/src/crypto/public_key/elliptic/eddsa_legacy.js @@ -21,12 +21,11 @@ * @module crypto/public_key/elliptic/eddsa_legacy */ -import nacl from '@openpgp/tweetnacl'; import util from '../../../util'; import enums from '../../../enums'; import { getHashByteLength } from '../../hash'; import { CurveWithOID, checkPublicPointEnconding } from './oid_curves'; -import { sign as eddsaSign, verify as eddsaVerify } from './eddsa'; +import { sign as eddsaSign, verify as eddsaVerify, validateParams as eddsaValidateParams } from './eddsa'; /** * Sign a message using the provided legacy EdDSA key @@ -97,12 +96,9 @@ export async function validateParams(oid, Q, k) { 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); - + // First byte is relevant for encoding purposes only + if (Q.length < 1 || Q[0] !== 0x40) { + return false; + } + return eddsaValidateParams(enums.publicKey.ed25519, Q.subarray(1), k); } diff --git a/src/crypto/public_key/elliptic/oid_curves.js b/src/crypto/public_key/elliptic/oid_curves.js index 7404cbe6..38edb609 100644 --- a/src/crypto/public_key/elliptic/oid_curves.js +++ b/src/crypto/public_key/elliptic/oid_curves.js @@ -19,14 +19,13 @@ * @fileoverview Wrapper of an instance of an Elliptic Curve * @module crypto/public_key/elliptic/curve */ -import nacl from '@openpgp/tweetnacl'; import enums from '../../../enums'; import util from '../../../util'; import { uint8ArrayToB64, b64ToUint8Array } from '../../../encoding/base64'; import OID from '../../../type/oid'; import { UnsupportedError } from '../../../packet/packet'; import { generate as eddsaGenerate } from './eddsa'; -import { generate as ecdhXGenerate } from './ecdh_x'; +import { generate as ecdhXGenerate, validateParams as ecdhXValidateParams } from './ecdh_x'; const webCrypto = util.getWebCrypto(); const nodeCrypto = util.getNodeCrypto(); @@ -252,17 +251,12 @@ async function validateStandardParams(algo, oid, Q, d) { } if (curveName === enums.curve.curve25519Legacy) { - 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)) { + const dLittleEndian = d.slice().reverse(); + // First byte is relevant for encoding purposes only + if (Q.length < 1 || Q[0] !== 0x40) { return false; } - - return true; + return ecdhXValidateParams(enums.publicKey.x25519, Q.subarray(1), dLittleEndian); } const nobleCurve = await util.getNobleCurve(enums.publicKey.ecdsa, curveName); // excluding curve25519Legacy, ecdh and ecdsa use the same curves diff --git a/test/crypto/ecdh.js b/test/crypto/ecdh.js index 0e73cdcb..de17e9a8 100644 --- a/test/crypto/ecdh.js +++ b/test/crypto/ecdh.js @@ -323,7 +323,10 @@ export default () => describe('ECDH key exchange @lightweight', function () { const disableNative = () => { enableNative(); // stubbed functions return undefined - getWebCryptoStub = sinonSandbox.stub(util, 'getWebCrypto'); + getWebCryptoStub = sinonSandbox.stub(util, 'getWebCrypto').returns({ + generateKey: () => { const e = new Error('getWebCrypto is mocked'); e.name = 'NotSupportedError'; throw e; }, + importKey: () => { const e = new Error('getWebCrypto is mocked'); e.name = 'NotSupportedError'; throw e; } + }); getNodeCryptoStub = sinonSandbox.stub(util, 'getNodeCrypto'); }; const enableNative = () => { @@ -331,6 +334,46 @@ export default () => describe('ECDH key exchange @lightweight', function () { getNodeCryptoStub && getNodeCryptoStub.restore(); }; + /** + * Test that the result of `encryptFunction` can be decrypted by `decryptFunction` + * with and without native crypto support. + * @param encryptFunction - `(data: Uint8Array) => encryptFunctionResult` + * @param decryptFunction - `(encryptFunctionResult) => ` + * @param expectNative - whether native usage is expected for the algorithm + */ + const testRountripWithAndWithoutNative = async ( + encryptFunction, + decryptFunction, // (encryptFunctionResult) => decryption result + expectNative + ) => { + const nodeCrypto = util.getNodeCrypto(); + const webCrypto = util.getWebCrypto(); + const data = random.getRandomBytes(16); + + const nativeSpy = webCrypto ? sinonSandbox.spy(webCrypto, 'deriveBits') : sinonSandbox.spy(nodeCrypto, 'createECDH'); // functions used both for encryption and decryption + const nativeResult = await encryptFunction(data); + const expectedNativeEncryptCallCount = nativeSpy.callCount; + disableNative(); + const nonNativeResult = await encryptFunction(data); + expect(nativeSpy.callCount).to.equal(expectedNativeEncryptCallCount); // assert that fallback implementation was called + if (expectNative) { + expect(nativeSpy.calledOnce).to.be.true; + } + + enableNative(); + expect(await decryptFunction(nativeResult)).to.deep.equal(data); + expect(await decryptFunction(nonNativeResult)).to.deep.equal(data); + const expectedNativeCallCount = nativeSpy.callCount; + disableNative(); + expect(await decryptFunction(nativeResult)).to.deep.equal(data); + expect(await decryptFunction(nonNativeResult)).to.deep.equal(data); + expect(nativeSpy.callCount).to.equal(expectedNativeCallCount); // assert that fallback implementation was called + if (expectNative) { + expect(nativeSpy.callCount).to.equal(3); // one encryption + two decryptions + } + }; + + allCurves.forEach(curveName => { it(`${curveName}`, async function () { const nodeCrypto = util.getNodeCrypto(); @@ -344,21 +387,27 @@ export default () => describe('ECDH key exchange @lightweight', function () { const curve = new elliptic_curves.CurveWithOID(curveName); const oid = new OID(curve.oid); const kdfParams = new KDFParams({ hash: curve.hash, cipher: curve.cipher }); - const data = random.getRandomBytes(16); const Q = key_data[curveName].pub; const d = key_data[curveName].priv; - const { publicKey: V, wrappedKey: C } = await ecdh.encrypt(oid, kdfParams, data, Q, fingerprint1); - const nativeDecryptSpy = webCrypto ? sinonSandbox.spy(webCrypto, 'deriveBits') : sinonSandbox.spy(nodeCrypto, 'createECDH'); - expect(await ecdh.decrypt(oid, kdfParams, V, C, Q, d, fingerprint1)).to.deep.equal(data); - const expectedNativeCallCount = nativeDecryptSpy.callCount; - disableNative(); - expect(await ecdh.decrypt(oid, kdfParams, V, C, Q, d, fingerprint1)).to.deep.equal(data); - expect(nativeDecryptSpy.callCount).to.equal(expectedNativeCallCount); // assert that fallback implementation was called - if (expectNativeWeb.has(curveName)) { - expect(nativeDecryptSpy.calledOnce).to.be.true; - } + await testRountripWithAndWithoutNative( + data => ecdh.encrypt(oid, kdfParams, data, Q, fingerprint1), + encryptResult => ecdh.decrypt(oid, kdfParams, encryptResult.publicKey, encryptResult.wrappedKey, Q, d, fingerprint1), + expectNativeWeb.has(curveName) // all major browsers implement x25519 + ); }); }); + + it('Successful exchange x25519 (legacy)', async function () { + const curve = new elliptic_curves.CurveWithOID(openpgp.enums.curve.curve25519Legacy); + const oid = new OID(curve.oid); + const kdfParams = new KDFParams({ hash: curve.hash, cipher: curve.cipher }); + + await testRountripWithAndWithoutNative( + data => ecdh.encrypt(oid, kdfParams, data, Q1, fingerprint1), + encryptResult => ecdh.decrypt(oid, kdfParams, encryptResult.publicKey, encryptResult.wrappedKey, Q1, d1, fingerprint1), + false // all major browsers implement x25519, but webkit linux falls back due to bugs + ); + }); }); }); diff --git a/test/crypto/elliptic.js b/test/crypto/ecdsa_eddsa.js similarity index 59% rename from test/crypto/elliptic.js rename to test/crypto/ecdsa_eddsa.js index 7f98cd3e..5b10621f 100644 --- a/test/crypto/elliptic.js +++ b/test/crypto/ecdsa_eddsa.js @@ -4,17 +4,59 @@ import chaiAsPromised from 'chai-as-promised'; // eslint-disable-line import/new chaiUse(chaiAsPromised); import openpgp from '../initOpenpgp.js'; -import * as elliptic_curves from '../../src/crypto/public_key/elliptic'; -import { computeDigest } from '../../src/crypto/hash'; -import config from '../../src/config'; +import * as elliptic_curves from '../../src/crypto/public_key/elliptic/index.js'; +import { computeDigest } from '../../src/crypto/hash/index.js'; +import config from '../../src/config/index.js'; import util from '../../src/util.js'; -import elliptic_data from './elliptic_data'; +import elliptic_data from './elliptic_data.js'; import OID from '../../src/type/oid.js'; +import { getRandomBytes } from '../../src/crypto/random.js'; + +/** + * Test that the result of `signFunction` can be verified by `verifyFunction` + * with and without native crypto support. + * @param signFunction - `(data: Uint8Array) => signFunctionResult` + * @param verifyFunction - `(encryptFunctionResult) => ` + * @param expectNative - whether native usage is expected for the algorithm + */ +const testRountripWithAndWithoutNative = async ( + { sinonSandbox, enableNative, disableNative }, + signFunction, + verifyFunction, // (signFunctionResult) => verification result + expectNative +) => { + const nodeCrypto = util.getNodeCrypto(); + const webCrypto = util.getWebCrypto(); + const data = getRandomBytes(16); + const dataDigest = await computeDigest(openpgp.enums.hash.sha512, data); + + const nativeSpySign = webCrypto ? sinonSandbox.spy(webCrypto, 'sign') : sinonSandbox.spy(nodeCrypto, 'createSign'); + const nativeResult = await signFunction(data, dataDigest); + const expectedNativeSignCallCount = nativeSpySign.callCount; + disableNative(); + const nonNativeResult = await signFunction(data, dataDigest); + expect(nativeSpySign.callCount).to.equal(expectedNativeSignCallCount); // assert that fallback implementation was called + if (expectNative) { + expect(nativeSpySign.calledOnce).to.be.true; + } + + const nativeSpyVerify = webCrypto ? sinonSandbox.spy(webCrypto, 'verify') : sinonSandbox.spy(nodeCrypto, 'createVerify'); + enableNative(); + expect(await verifyFunction(nativeResult, data, dataDigest)).to.be.true; + expect(await verifyFunction(nonNativeResult, data, dataDigest)).to.be.true; + const expectedNativeVerifyCallCount = nativeSpyVerify.callCount; + disableNative(); + expect(await verifyFunction(nativeResult, data, dataDigest)).to.be.true; + expect(await verifyFunction(nonNativeResult, data, dataDigest)).be.true; + expect(nativeSpyVerify.callCount).to.equal(expectedNativeVerifyCallCount); // assert that fallback implementation was called + if (expectNative) { + expect(nativeSpyVerify.callCount).to.equal(2); + } +}; const key_data = elliptic_data.key_data; -/* eslint-disable no-invalid-this */ -export default () => describe('Elliptic Curve Cryptography @lightweight', function () { +export default () => describe('ECC signatures', function () { const signature_data = { priv: new Uint8Array([ 0x14, 0x2B, 0xE2, 0xB7, 0x4D, 0xBD, 0x1B, 0x22, @@ -241,19 +283,103 @@ export default () => describe('Elliptic Curve Cryptography @lightweight', functi ); }); const curves = ['secp256k1' , 'nistP256', 'nistP384', 'nistP521', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1']; - curves.forEach(curveName => it(`${curveName} - Sign and verify message`, async function () { + curves.forEach(curveName => it(`${curveName} - Sign and verify message with generated key`, async function () { + const sinonState = { sinonSandbox, enableNative, disableNative }; + const curve = new elliptic_curves.CurveWithOID(curveName); const oid = new OID(curve.oid); - const { Q: keyPublic, secret: keyPrivate } = await elliptic_curves.generate(curveName); - const message = new Uint8Array([ - 0x00, 0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, - 0x88, 0x99, 0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF - ]); - const messageDigest = await computeDigest(openpgp.enums.hash.sha512, message); - await testNativeAndFallback(async () => { - const signature = await elliptic_curves.ecdsa.sign(oid, openpgp.enums.hash.sha512, message, keyPublic, keyPrivate, messageDigest); - await expect(elliptic_curves.ecdsa.verify(oid, openpgp.enums.hash.sha512, signature, message, keyPublic, messageDigest)).to.eventually.be.true; + const expectNativeWeb = new Set(['nistP256', 'nistP384']); // older versions of safari do not implement nistP521 + + const nativeKey = await elliptic_curves.generate(curveName); + await testRountripWithAndWithoutNative( + sinonState, + (data, dataDigest) => elliptic_curves.ecdsa.sign(oid, openpgp.enums.hash.sha512, data, nativeKey.Q, nativeKey.secret, dataDigest), + (signature, data, dataDigest) => elliptic_curves.ecdsa.verify(oid, openpgp.enums.hash.sha512, signature, data, nativeKey.Q, dataDigest), + expectNativeWeb.has(curveName) + ); + + sinonSandbox.restore(); // reset spies + disableNative(); + const nonNativeKey = await elliptic_curves.generate(curveName); + enableNative(); + await testRountripWithAndWithoutNative( + sinonState, + (data, dataDigest) => elliptic_curves.ecdsa.sign(oid, openpgp.enums.hash.sha512, data, nonNativeKey.Q, nonNativeKey.secret, dataDigest), + (signature, data, dataDigest) => elliptic_curves.ecdsa.verify(oid, openpgp.enums.hash.sha512, signature, data, nonNativeKey.Q, dataDigest), + expectNativeWeb.has(curveName) + ); + })); + }); + + describe('EdDSA signature', function () { + let sinonSandbox; + let getWebCryptoStub; + let getNodeCryptoStub; + + beforeEach(function () { + sinonSandbox = sinon.createSandbox(); + }); + + afterEach(function () { + sinonSandbox.restore(); + }); + + const disableNative = () => { + enableNative(); + // stubbed functions return undefined + getWebCryptoStub = sinonSandbox.stub(util, 'getWebCrypto').returns({ + generateKey: () => { const e = new Error('getWebCrypto is mocked'); e.name = 'NotSupportedError'; throw e; }, + importKey: () => { const e = new Error('getWebCrypto is mocked'); e.name = 'NotSupportedError'; throw e; } }); + getNodeCryptoStub = sinonSandbox.stub(util, 'getNodeCrypto'); + }; + const enableNative = () => { + getWebCryptoStub && getWebCryptoStub.restore(); + getNodeCryptoStub && getNodeCryptoStub.restore(); + }; + + it('ed25519Legacy - Sign and verify message with generated key', async function () { + const sinonState = { sinonSandbox, enableNative, disableNative }; + const curve = new elliptic_curves.CurveWithOID(openpgp.enums.curve.ed25519Legacy); + const oid = new OID(curve.oid); + + const nativeKey = await elliptic_curves.generate(openpgp.enums.curve.ed25519Legacy); + await testRountripWithAndWithoutNative( + sinonState, + (data, dataDigest) => elliptic_curves.eddsaLegacy.sign(oid, openpgp.enums.hash.sha512, data, nativeKey.Q, nativeKey.secret, dataDigest), + (signature, data, dataDigest) => elliptic_curves.eddsaLegacy.verify(oid, openpgp.enums.hash.sha512, signature, data, nativeKey.Q, dataDigest) + ); + + sinonSandbox.restore(); // reset spies + disableNative(); + const nonNativeKey = await elliptic_curves.generate(openpgp.enums.curve.ed25519Legacy); + enableNative(); + await testRountripWithAndWithoutNative( + sinonState, + (data, dataDigest) => elliptic_curves.eddsaLegacy.sign(oid, openpgp.enums.hash.sha512, data, nonNativeKey.Q, nonNativeKey.secret, dataDigest), + (signature, data, dataDigest) => elliptic_curves.eddsaLegacy.verify(oid, openpgp.enums.hash.sha512, signature, data, nonNativeKey.Q, dataDigest) + ); + }); + + ['ed25519', 'ed448'].forEach(algoName => it(`${algoName} - Sign and verify message with native generated key`, async function () { + const sinonState = { sinonSandbox, enableNative, disableNative }; + const algo = openpgp.enums.publicKey[algoName]; + const nativeKey = await elliptic_curves.eddsa.generate(algo); + await testRountripWithAndWithoutNative( + sinonState, + (data, dataDigest) => elliptic_curves.eddsa.sign(algo, openpgp.enums.hash.sha512, data, nativeKey.A, nativeKey.seed, dataDigest), + (signature, data, dataDigest) => elliptic_curves.eddsa.verify(algo, openpgp.enums.hash.sha512, signature, data, nativeKey.A, dataDigest) + ); + + sinonSandbox.restore(); // reset spies + disableNative(); + const nonNativeKey = await elliptic_curves.eddsa.generate(algo); + enableNative(); + await testRountripWithAndWithoutNative( + sinonState, + (data, dataDigest) => elliptic_curves.eddsa.sign(algo, openpgp.enums.hash.sha512, data, nonNativeKey.A, nonNativeKey.seed, dataDigest), + (signature, data, dataDigest) => elliptic_curves.eddsa.verify(algo, openpgp.enums.hash.sha512, signature, data, nonNativeKey.A, dataDigest) + ); })); }); }); diff --git a/test/crypto/index.js b/test/crypto/index.js index a4c73ae5..5e7567ff 100644 --- a/test/crypto/index.js +++ b/test/crypto/index.js @@ -2,7 +2,7 @@ import testBigInteger from './biginteger'; import testCipher from './cipher'; import testHash from './hash'; import testCrypto from './crypto'; -import testElliptic from './elliptic'; +import testElliptic from './ecdsa_eddsa'; import testBrainpoolRFC7027 from './brainpool_rfc7027'; import testECDH from './ecdh'; import testPKCS5 from './pkcs5'; diff --git a/test/crypto/validate.js b/test/crypto/validate.js index 8ed63089..7fd74124 100644 --- a/test/crypto/validate.js +++ b/test/crypto/validate.js @@ -90,8 +90,10 @@ async function generatePrivateKeyObject(options) { export default () => { describe('EdDSA parameter validation (legacy format)', function() { let eddsaKey; + let anotherEddsaKey; before(async () => { eddsaKey = await generatePrivateKeyObject({ curve: 'ed25519Legacy' }); + anotherEddsaKey = await generatePrivateKeyObject({ curve: 'ed25519Legacy' }); }); it('EdDSA params should be valid', async function() { @@ -100,11 +102,10 @@ export default () => { it('detect invalid edDSA Q', async function() { const eddsaKeyPacket = await cloneKeyPacket(eddsaKey); - const Q = eddsaKeyPacket.publicParams.Q; - Q[0]++; + eddsaKeyPacket.publicParams.Q = anotherEddsaKey.keyPacket.publicParams.Q; await expect(eddsaKeyPacket.validate()).to.be.rejectedWith('Key is invalid'); - const infQ = new Uint8Array(Q.length); + const infQ = new Uint8Array(eddsaKeyPacket.publicParams.Q.length); eddsaKeyPacket.publicParams.Q = infQ; await expect(eddsaKeyPacket.validate()).to.be.rejectedWith('Key is invalid'); }); @@ -198,13 +199,19 @@ export default () => { describe(`ECC ${curve} parameter validation`, () => { let ecdsaKey; let ecdhKey; + let anotherEcdsaKey; + let anotherEcdhKey; before(async () => { if (curve !== 'curve25519Legacy') { ecdsaKey = await generatePrivateKeyObject({ curve }); ecdhKey = ecdsaKey.subkeys[0]; + anotherEcdsaKey = await generatePrivateKeyObject({ curve }); + anotherEcdhKey = anotherEcdsaKey.subkeys[0]; } else { const eddsaKey = await generatePrivateKeyObject({ curve: 'ed25519Legacy' }); ecdhKey = eddsaKey.subkeys[0]; + const anotherEddsaKey = await generatePrivateKeyObject({ curve: 'ed25519Legacy' }); + anotherEcdhKey = anotherEddsaKey.subkeys[0]; } }); @@ -220,10 +227,9 @@ export default () => { this.skip(); } const keyPacket = await cloneKeyPacket(ecdsaKey); - const Q = keyPacket.publicParams.Q; - Q[16]++; + keyPacket.publicParams.Q = anotherEcdsaKey.keyPacket.publicParams.Q; await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid'); - const infQ = new Uint8Array(Q.length); + const infQ = new Uint8Array(anotherEcdsaKey.keyPacket.publicParams.Q.length); infQ[0] = 4; keyPacket.publicParams.Q = infQ; await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid'); @@ -235,11 +241,10 @@ export default () => { it(`ECDH ${curve} - detect invalid Q`, async function() { const keyPacket = await cloneKeyPacket(ecdhKey); - const Q = keyPacket.publicParams.Q; - Q[16]++; + keyPacket.publicParams.Q = anotherEcdhKey.keyPacket.publicParams.Q; await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid'); - const infQ = new Uint8Array(Q.length); + const infQ = new Uint8Array(keyPacket.publicParams.Q.length); keyPacket.publicParams.Q = infQ; infQ[0] = 4; await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid'); @@ -252,9 +257,13 @@ export default () => { describe(`Ed${curveID}/X${curveID} parameter validation`, function() { let eddsaKey; let ecdhXKey; + let anotherEddsaKey; + let anotherEcdhXKey; before(async () => { eddsaKey = await generatePrivateKeyObject({ type: `curve${curveID}` }); ecdhXKey = eddsaKey.subkeys[0]; + anotherEddsaKey = await generatePrivateKeyObject({ type: `curve${curveID}` }); + anotherEcdhXKey = anotherEddsaKey.subkeys[0]; }); it(`Ed${curveID} params should be valid`, async function() { @@ -263,11 +272,10 @@ export default () => { it(`detect invalid Ed${curveID} public point`, async function() { const eddsaKeyPacket = await cloneKeyPacket(eddsaKey); - const A = eddsaKeyPacket.publicParams.A; - A[0]++; + eddsaKeyPacket.publicParams.A = anotherEddsaKey.keyPacket.publicParams.A; await expect(eddsaKeyPacket.validate()).to.be.rejectedWith('Key is invalid'); - const infA = new Uint8Array(A.length); + const infA = new Uint8Array(eddsaKeyPacket.publicParams.A.length); eddsaKeyPacket.publicParams.A = infA; await expect(eddsaKeyPacket.validate()).to.be.rejectedWith('Key is invalid'); }); @@ -278,11 +286,10 @@ export default () => { it(`detect invalid X${curveID} public point`, async function() { const ecdhXKeyPacket = await cloneKeyPacket(ecdhXKey); - const A = ecdhXKeyPacket.publicParams.A; - A[0]++; + ecdhXKeyPacket.publicParams.A = anotherEcdhXKey.keyPacket.publicParams.A; await expect(ecdhXKeyPacket.validate()).to.be.rejectedWith('Key is invalid'); - const infA = new Uint8Array(A.length); + const infA = new Uint8Array(ecdhXKeyPacket.publicParams.A.length); ecdhXKeyPacket.publicParams.A = infA; await expect(ecdhXKeyPacket.validate()).to.be.rejectedWith('Key is invalid'); }); @@ -291,8 +298,10 @@ export default () => { describe('RSA parameter validation', function() { let rsaKey; + let anotherRsaKey; before(async () => { rsaKey = await generatePrivateKeyObject({ type: 'rsa', rsaBits: 2048 }); + anotherRsaKey = await generatePrivateKeyObject({ type: 'rsa', rsaBits: 2048 }); }); it('generated RSA params are valid', async function() { @@ -301,15 +310,14 @@ export default () => { it('detect invalid RSA n', async function() { const keyPacket = await cloneKeyPacket(rsaKey); - const n = keyPacket.publicParams.n; - n[0]++; + keyPacket.publicParams.n = anotherRsaKey.keyPacket.publicParams.n; await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid'); }); it('detect invalid RSA e', async function() { const keyPacket = await cloneKeyPacket(rsaKey); const e = keyPacket.publicParams.e; - e[0]++; + e[0]++; // e is hard-coded so we don't take it from `anotherRsaKey` await expect(keyPacket.validate()).to.be.rejectedWith('Key is invalid'); }); });