From 24c644207d5d5fba6534010bcb38795568510134 Mon Sep 17 00:00:00 2001 From: larabr <7375870+larabr@users.noreply.github.com> Date: Thu, 7 Sep 2023 19:34:44 +0200 Subject: [PATCH] Support generating Curve448 and Curve25519 keys (new format) Neither type is set as default for now, since they are not widely supported. --- openpgp.d.ts | 6 +- src/crypto/public_key/elliptic/eddsa.js | 4 +- src/key/factory.js | 4 +- src/key/helper.js | 8 ++- src/key/private_key.js | 29 ++++++++- src/openpgp.js | 6 +- test/crypto/validate.js | 83 +++++++++++-------------- test/general/key.js | 58 +++++++++++++++++ test/general/openpgp.js | 31 ++------- 9 files changed, 143 insertions(+), 86 deletions(-) diff --git a/openpgp.d.ts b/openpgp.d.ts index d38bd10d..6a2e5418 100644 --- a/openpgp.d.ts +++ b/openpgp.d.ts @@ -816,7 +816,7 @@ export namespace enums { aeadEncryptedData = 20, } - export type publicKeyNames = 'rsaEncryptSign' | 'rsaEncrypt' | 'rsaSign' | 'elgamal' | 'dsa' | 'ecdh' | 'ecdsa' | 'eddsa' | 'aedh' | 'aedsa'; + export type publicKeyNames = 'rsaEncryptSign' | 'rsaEncrypt' | 'rsaSign' | 'elgamal' | 'dsa' | 'ecdh' | 'ecdsa' | 'eddsa' | 'ed25519Legacy' | 'aedh' | 'aedsa' | 'ed25519' | 'x25519' | 'ed448' | 'x448'; enum publicKey { rsaEncryptSign = 1, rsaEncrypt = 2, @@ -830,6 +830,10 @@ export namespace enums { eddsaLegacy = 22, aedh = 23, aedsa = 24, + x25519 = 25, + x448 = 26, + ed25519 = 27, + ed448 = 28 } enum curve { diff --git a/src/crypto/public_key/elliptic/eddsa.js b/src/crypto/public_key/elliptic/eddsa.js index bcab724f..8b08a671 100644 --- a/src/crypto/public_key/elliptic/eddsa.js +++ b/src/crypto/public_key/elliptic/eddsa.js @@ -36,14 +36,14 @@ ed25519.hash = bytes => sha512(bytes); * @returns {Promise<{ A: Uint8Array, seed: Uint8Array }>} */ export async function generate(algo) { - const seed = getRandomBytes(getPayloadSize(algo)); - switch (algo) { case enums.publicKey.ed25519: { + const seed = getRandomBytes(getPayloadSize(algo)); const { publicKey: A } = ed25519.sign.keyPair.fromSeed(seed); return { A, seed }; } case enums.publicKey.ed448: { + const seed = ed448.utils.randomPrivateKey(); const A = ed448.getPublicKey(seed); return { A, seed }; } diff --git a/src/key/factory.js b/src/key/factory.js index 6c1d7ee1..c485b35c 100644 --- a/src/key/factory.js +++ b/src/key/factory.js @@ -64,9 +64,9 @@ function createKey(packetlist) { /** - * Generates a new OpenPGP key. Supports RSA and ECC keys. + * Generates a new OpenPGP key. Supports RSA and ECC keys, as well as the newer Curve448 and Curve25519 keys. * By default, primary and subkeys will be of same type. - * @param {ecc|rsa} options.type The primary key algorithm type: ECC or RSA + * @param {ecc|rsa|curve448|curve25519} options.type The primary key algorithm type: ECC, RSA, Curve448 or Curve25519 (new format). * @param {String} options.curve Elliptic curve for ECC keys * @param {Integer} options.rsaBits Number of bits for RSA keys * @param {Array} options.userIDs User IDs as strings or objects: 'Jo Doe ' or { name:'Jo Doe', email:'info@jo.com' } diff --git a/src/key/helper.js b/src/key/helper.js index e887ed19..a23bbaae 100644 --- a/src/key/helper.js +++ b/src/key/helper.js @@ -326,7 +326,7 @@ export function sanitizeKeyOptions(options, subkeyDefaults = {}) { options.sign = options.sign || false; switch (options.type) { - case 'ecc': + case 'ecc': // NB: this case also handles legacy eddsa and x25519 keys, based on `options.curve` try { options.curve = enums.write(enums.curve, options.curve); } catch (e) { @@ -341,6 +341,12 @@ export function sanitizeKeyOptions(options, subkeyDefaults = {}) { options.algorithm = enums.publicKey.ecdh; } break; + case 'curve25519': + options.algorithm = options.sign ? enums.publicKey.ed25519 : enums.publicKey.x25519; + break; + case 'curve448': + options.algorithm = options.sign ? enums.publicKey.ed448 : enums.publicKey.x448; + break; case 'rsa': options.algorithm = enums.publicKey.rsaEncryptSign; break; diff --git a/src/key/private_key.js b/src/key/private_key.js index 055de008..7b86945b 100644 --- a/src/key/private_key.js +++ b/src/key/private_key.js @@ -198,8 +198,10 @@ class PrivateKey extends PublicKey { /** * Generates a new OpenPGP subkey, and returns a clone of the Key object with the new subkey added. - * Supports RSA and ECC keys. Defaults to the algorithm and bit size/curve of the primary key. DSA primary keys default to RSA subkeys. - * @param {ecc|rsa} options.type The subkey algorithm: ECC or RSA + * Supports RSA and ECC keys, as well as the newer Curve448 and Curve25519. + * Defaults to the algorithm and bit size/curve of the primary key. DSA primary keys default to RSA subkeys. + * @param {ecc|rsa|curve25519|curve448} options.type The subkey algorithm: ECC, RSA, Curve448 or Curve25519 (new format). + * Note: Curve448 and Curve25519 are not widely supported yet. * @param {String} options.curve (optional) Elliptic curve for ECC keys * @param {Integer} options.rsaBits (optional) Number of bits for RSA subkeys * @param {Number} options.keyExpirationTime (optional) Number of seconds from the key creation time after which the key expires @@ -225,7 +227,7 @@ class PrivateKey extends PublicKey { throw new Error('Key is not decrypted'); } const defaultOptions = secretKeyPacket.getAlgorithmInfo(); - defaultOptions.type = defaultOptions.curve ? 'ecc' : 'rsa'; // DSA keys default to RSA + defaultOptions.type = getDefaultSubkeyType(defaultOptions.algorithm); defaultOptions.rsaBits = defaultOptions.bits || 4096; defaultOptions.curve = defaultOptions.curve || 'curve25519'; options = helper.sanitizeKeyOptions(options, defaultOptions); @@ -238,4 +240,25 @@ class PrivateKey extends PublicKey { } } +function getDefaultSubkeyType(algoName) { + const algo = enums.write(enums.publicKey, algoName); + // NB: no encryption-only algos, since they cannot be in primary keys + switch (algo) { + case enums.publicKey.rsaEncrypt: + case enums.publicKey.rsaEncryptSign: + case enums.publicKey.rsaSign: + case enums.publicKey.dsa: + return 'rsa'; + case enums.publicKey.ecdsa: + case enums.publicKey.eddsaLegacy: + return 'ecc'; + case enums.publicKey.ed25519: + return 'curve25519'; + case enums.publicKey.ed448: + return 'curve448'; + default: + throw new Error('Unsupported algorithm'); + } +} + export default PrivateKey; diff --git a/src/openpgp.js b/src/openpgp.js index 70dfd240..7fad9aaa 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -32,11 +32,13 @@ import { checkKeyRequirements } from './key/helper'; /** - * Generates a new OpenPGP key pair. Supports RSA and ECC keys. By default, primary and subkeys will be of same type. + * Generates a new OpenPGP key pair. Supports RSA and ECC keys, as well as the newer Curve448 and Curve25519 keys. + * By default, primary and subkeys will be of same type. * The generated primary key will have signing capabilities. By default, one subkey with encryption capabilities is also generated. * @param {Object} options * @param {Object|Array} options.userIDs - User IDs as objects: `{ name: 'Jo Doe', email: 'info@jo.com' }` - * @param {'ecc'|'rsa'} [options.type='ecc'] - The primary key algorithm type: ECC (default) or RSA + * @param {'ecc'|'rsa'|'curve448'|'curve25519'} [options.type='ecc'] - The primary key algorithm type: ECC (default), RSA, Curve448 or Curve25519 (new format). + * Note: Curve448 and Curve25519 (new format) are not widely supported yet. * @param {String} [options.passphrase=(not protected)] - The passphrase used to encrypt the generated private key. If omitted or empty, the key won't be encrypted. * @param {Number} [options.rsaBits=4096] - Number of bits for RSA keys * @param {String} [options.curve='curve25519'] - Elliptic curve for ECC keys: diff --git a/test/crypto/validate.js b/test/crypto/validate.js index 3388f26b..9b9b883e 100644 --- a/test/crypto/validate.js +++ b/test/crypto/validate.js @@ -193,13 +193,13 @@ export default () => { }); }); - const curves = ['curve25519', 'p256', 'p384', 'p521', 'secp256k1', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1']; + const curves = ['curve25519Legacy', 'p256', 'p384', 'p521', 'secp256k1', 'brainpoolP256r1', 'brainpoolP384r1', 'brainpoolP512r1']; curves.forEach(curve => { describe(`ECC ${curve} parameter validation`, () => { let ecdsaKey; let ecdhKey; before(async () => { - if (curve !== 'curve25519') { + if (curve !== 'curve25519Legacy') { ecdsaKey = await generatePrivateKeyObject({ curve }); ecdhKey = ecdsaKey.subkeys[0]; } else { @@ -247,58 +247,45 @@ export default () => { }); }); - describe('Ed448/X448 parameter validation', function() { - let eddsaKey; - let ecdhXKey; - before(async () => { - eddsaKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- + // new EdDSA/XECDH algos + ['25519', '448'].forEach(curveID => { + describe(`Ed${curveID}/X${curveID} parameter validation`, function() { + let eddsaKey; + let ecdhXKey; + before(async () => { + eddsaKey = await generatePrivateKeyObject({ type: `curve${curveID}` }); + ecdhXKey = eddsaKey.subkeys[0]; + }); -xXsEZCWHXBwwtqciq6ZFU13s+dyhkWR5tOEmF1oX8OiP1B5ypfqyGVM8DkQh -5eTIMwB1oqJCROANoyA0q2dSigAAbDA5xr74DeClPPXC4ZXJ9uzuJWKvQvE8 -x3EflhgoQCGBM7JfvH5zwdrJvPt8RKDvm0QkZzhPvnFoHnzNBHRlc3TCugQQ -HAgAPgWCZCWHXAQLCQcICZDsN6h/ys3ppwMVCAoEFgACAQIZAQKbAwIeARYh -BOJyE9P2eIcU2N2Ne+w3qH/KzemnAAAh1hTFCcEU77bU3YelrJTCNIOQnvt7 -Hs6yZz2053CQTOC+wHkUQLaYYBEXSNyLZxoyv+NuGTiwbuYtAOlbE2erM7Cx -8B2Qz7M29UkFLMBUfb+yi+gTYYUWCXVQ7Um7MGjjgUG8+9p452i6f28mhRD8 -tTgNAMd5BGQlh1wavTIFgILtbzrqQCiwDGx0YcFNzu9+FZ8vK5Mmm7UEZj0a -y7FWQtZw8tTaU6mY+RrSa52RjzkGLtQAQO++tgYqc+BnCFdCZ3ZYPRvD3mof -ffoo3l4xmto+iyvJZbQ4wQPXttg7VjCpEfOsL9TW9Xs09aIbysKmBBgcCAAq -BYJkJYdcCZDsN6h/ys3ppwKbDBYhBOJyE9P2eIcU2N2Ne+w3qH/KzemnAAC0 -6/eZhh/Oj2gRdab2JeFGWACGIRDKxPXsWRCXR4YrSxcvCKK6rOvsyxQsgIsJ -JyPYkRPfmbKcseUDAEkSBLAfeizDGh7ea0GOdIMhwE/CW4f/H8ULbwi36y13 -x3oMNVaYsI9dZ588Gpi8XYy2jOtqIPQ1AA== ------END PGP PRIVATE KEY BLOCK-----` }); - ecdhXKey = eddsaKey.subkeys[0]; - }); + it(`Ed${curveID} params should be valid`, async function() { + await expect(eddsaKey.keyPacket.validate()).to.not.be.rejected; + }); - it('Ed448 params should be valid', async function() { - await expect(eddsaKey.keyPacket.validate()).to.not.be.rejected; - }); + it(`detect invalid Ed${curveID} public point`, async function() { + const eddsaKeyPacket = await cloneKeyPacket(eddsaKey); + const A = eddsaKeyPacket.publicParams.A; + A[0]++; + await expect(eddsaKeyPacket.validate()).to.be.rejectedWith('Key is invalid'); - it('detect invalid Ed448 public point', async function() { - const eddsaKeyPacket = await cloneKeyPacket(eddsaKey); - const A = eddsaKeyPacket.publicParams.A; - A[0]++; - await expect(eddsaKeyPacket.validate()).to.be.rejectedWith('Key is invalid'); + const infA = new Uint8Array(A.length); + eddsaKeyPacket.publicParams.A = infA; + await expect(eddsaKeyPacket.validate()).to.be.rejectedWith('Key is invalid'); + }); - const infA = new Uint8Array(A.length); - eddsaKeyPacket.publicParams.A = infA; - await expect(eddsaKeyPacket.validate()).to.be.rejectedWith('Key is invalid'); - }); + it(`X${curveID} params should be valid`, async function() { + await expect(ecdhXKey.keyPacket.validate()).to.not.be.rejected; + }); - it('X448 params should be valid', async function() { - await expect(ecdhXKey.keyPacket.validate()).to.not.be.rejected; - }); + it(`detect invalid X${curveID} public point`, async function() { + const ecdhXKeyPacket = await cloneKeyPacket(ecdhXKey); + const A = ecdhXKeyPacket.publicParams.A; + A[0]++; + await expect(ecdhXKeyPacket.validate()).to.be.rejectedWith('Key is invalid'); - it('detect invalid x448 public point', async function() { - const ecdhXKeyPacket = await cloneKeyPacket(ecdhXKey); - const A = ecdhXKeyPacket.publicParams.A; - A[0]++; - await expect(ecdhXKeyPacket.validate()).to.be.rejectedWith('Key is invalid'); - - const infA = new Uint8Array(A.length); - ecdhXKeyPacket.publicParams.A = infA; - await expect(ecdhXKeyPacket.validate()).to.be.rejectedWith('Key is invalid'); + const infA = new Uint8Array(A.length); + ecdhXKeyPacket.publicParams.A = infA; + await expect(ecdhXKeyPacket.validate()).to.be.rejectedWith('Key is invalid'); + }); }); }); diff --git a/test/general/key.js b/test/general/key.js index adc11528..3fedd73f 100644 --- a/test/general/key.js +++ b/test/general/key.js @@ -2509,6 +2509,54 @@ function versionSpecificTests() { } }); + it('Generate Ed25519 key (new format) - default subkey', async function() { + const userID = { name: 'test', email: 'a@b.com' }; + const opt = { type: 'curve25519', userIDs: [userID], passphrase: '123', format: 'object' }; + const { privateKey: key } = await openpgp.generateKey(opt); + expect(key.users.length).to.equal(1); + expect(key.users[0].userID.userID).to.equal('test '); + expect(key.users[0].selfCertifications[0].isPrimaryUserID).to.be.true; + expect(key.getAlgorithmInfo().algorithm).to.equal('ed25519'); + expect(key.subkeys).to.have.length(1); + expect(key.subkeys[0].getAlgorithmInfo().algorithm).to.equal('x25519'); + }); + + it('Generate Ed25519 key (new format) - one signing subkey', async function() { + const userID = { name: 'test', email: 'a@b.com' }; + const opt = { type: 'curve25519', userIDs: [userID], passphrase: '123', format: 'object', subkeys:[{ sign: true }] }; + const { privateKey: key } = await openpgp.generateKey(opt); + expect(key.users.length).to.equal(1); + expect(key.users[0].userID.userID).to.equal('test '); + expect(key.users[0].selfCertifications[0].isPrimaryUserID).to.be.true; + expect(key.getAlgorithmInfo().algorithm).to.equal('ed25519'); + expect(key.subkeys).to.have.length(1); + expect(key.subkeys[0].getAlgorithmInfo().algorithm).to.equal('ed25519'); + }); + + it('Generate Ed448 key - default subkey', async function() { + const userID = { name: 'test', email: 'a@b.com' }; + const opt = { type: 'curve448', userIDs: [userID], passphrase: '123', format: 'object' }; + const { privateKey: key } = await openpgp.generateKey(opt); + expect(key.users.length).to.equal(1); + expect(key.users[0].userID.userID).to.equal('test '); + expect(key.users[0].selfCertifications[0].isPrimaryUserID).to.be.true; + expect(key.getAlgorithmInfo().algorithm).to.equal('ed448'); + expect(key.subkeys).to.have.length(1); + expect(key.subkeys[0].getAlgorithmInfo().algorithm).to.equal('x448'); + }); + + it('Generate Ed448 key - one signing subkey', async function() { + const userID = { name: 'test', email: 'a@b.com' }; + const opt = { type: 'curve448', userIDs: [userID], passphrase: '123', format: 'object', subkeys:[{ sign: true }] }; + const { privateKey: key } = await openpgp.generateKey(opt); + expect(key.users.length).to.equal(1); + expect(key.users[0].userID.userID).to.equal('test '); + expect(key.users[0].selfCertifications[0].isPrimaryUserID).to.be.true; + expect(key.getAlgorithmInfo().algorithm).to.equal('ed448'); + expect(key.subkeys).to.have.length(1); + expect(key.subkeys[0].getAlgorithmInfo().algorithm).to.equal('ed448'); + }); + it('Generate key - one signing subkey', function() { const userID = { name: 'test', email: 'a@b.com' }; const opt = { userIDs: [userID], passphrase: '123', format: 'object', subkeys:[{}, { sign: true }] }; @@ -4196,6 +4244,16 @@ VYGdb3eNlV8CfoEC expect(newKey.subkeys[0].getAlgorithmInfo().curve).to.equal('curve25519'); }); + it('Add a new default subkey to an Ed488 key', async function() { + const userID = { name: 'test', email: 'a@b.com' }; + const opt = { type: 'curve448', userIDs: [userID], format: 'object', subkeys: [] }; + const { privateKey: key } = await openpgp.generateKey(opt); + expect(key.subkeys).to.have.length(0); + const newKey = await key.addSubkey(); + expect(newKey.subkeys[0].getAlgorithmInfo().algorithm).to.equal('x448'); + expect(newKey.subkeys[0].getAlgorithmInfo().curve).to.be.undefined; + }); + it('Add a new default subkey to a dsa key', async function() { const key = await openpgp.readKey({ armoredKey: dsaPrivateKey }); const total = key.subkeys.length; diff --git a/test/general/openpgp.js b/test/general/openpgp.js index e2ff3128..e97a21f5 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -4758,17 +4758,8 @@ sEj+v9LKoMTYZGMfp3qDVFLtkBE88eVmVjgJOoLhrsv7yh0PAA== }); it('sign/verify with new Ed25519 format', async function () { - // v4 key, which we do not support generating - const privateKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- - -xUkEZBw5PBscroGar9fsilA0q9AX979pBhTNkGQ69vQGGW7kxRxNuABB+eAw -JrQ9A3o1gUJg28ORTQd72+kFo87184qR97a6rRGFzQR0ZXN0wogEEBsIAD4F -gmQcOTwECwkHCAmQT/m+Rl22Ps8DFQgKBBYAAgECGQECmwMCHgEWIQSUlOfm -G7MWJd2909ZP+b5GXbY+zwAAVs/4pWH4l7pWcTATBavVqSATMKi4A+usp89G -J/qaHc+qmcEpIMmPNvLQ7n4F4kEXk8Zwz+OXovVWLQ+Njl5gzooF -=wYg1 ------END PGP PRIVATE KEY BLOCK----- - ` }); + const userIDs = { name: 'Alice', email: 'info@alice.com' }; + const { privateKey } = await openpgp.generateKey({ type: 'curve25519', userIDs, format: 'object' }); const plaintext = 'plaintext'; const signed = await openpgp.sign({ @@ -4786,22 +4777,8 @@ J/qaHc+qmcEpIMmPNvLQ7n4F4kEXk8Zwz+OXovVWLQ+Njl5gzooF }); it('sign/verify with Ed448', async function () { - const privateKey = await openpgp.readKey({ armoredKey: `-----BEGIN PGP PRIVATE KEY BLOCK----- - -xX0GZRqLYhwAAAA5U/IaIOge/FoLzCetXKx029bdJHCz2hMFBRMuzq4msjaT -+hLeV6puyC/PeSEfaanqTuo31vvsti2AAIttr4GDGXF4vfPzbzkWV9dT4VVs -IU7QqLv1hzwZ+k7pHroRyXnUiYxRYHuzlg7Vw4CrAtN/8T65OMLAHgYfHAoA -AAA9BQJlGotiIqEGAxidsHRHDsyFTw1Q7OoGEAEnRnxthKMwVBqhIL2o+HUC -GwMCHgkCCwcDFQoIAhYAAycHAgAAAAA2KiC+Y+fhQ/48CkT9WrXTX9SCn3vH -z43Wb++AkmpWL1HQmrJE3S4gGltezZK2E9ovagzxKxVrL14uC6hs6kJ0JIiW -QSeMeexCTy+Gdr6j0wb4FhFNnoIu3yu2ABmZpFX/5/191YeWUryKFDAoUZmK -gQTSOzJEvyO0ACR5L4vV3ADceOAdG8/sqhE89rTSevFXng4JAM0XVXNlckEg -PFVzZXJBQHRlc3QudGVzdD7CwA0GExwKAAAALAUCZRqLYiKhBgMYnbB0Rw7M -hU8NUOzqBhABJ0Z8bYSjMFQaoSC9qPh1AhkBAAAAAFw/IH72M1iyzMWhbgtw -v0SR/XxvOIW/ZrT4Ix9236lvoOE4taL/D46CbZOjm7VAeOSfSdxt1xSKnoAL -RsCNQ8tVPjPXclzqr6R8MbPIgBWxKcMS2eStYpBbG5qAmc+K5jdA2xcl9iW5 -bWleZ1LTah4lF6qCiD73IffADXtzw8iAMTX+0wM5N1tJUEGvgqe00ohRKiQA ------END PGP PRIVATE KEY BLOCK-----` }); + const userIDs = { name: 'Alice', email: 'info@alice.com' }; + const { privateKey } = await openpgp.generateKey({ type: 'curve448', userIDs, format: 'object' }); const plaintext = 'plaintext'; const signed = await openpgp.sign({