Support generating Curve448 and Curve25519 keys (new format)

Neither type is set as default for now, since they are not widely supported.
This commit is contained in:
larabr 2023-09-07 19:34:44 +02:00
parent 1509364a49
commit 24c644207d
9 changed files with 143 additions and 86 deletions

6
openpgp.d.ts vendored
View File

@ -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 {

View File

@ -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 };
}

View File

@ -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<String|Object>} options.userIDs User IDs as strings or objects: 'Jo Doe <info@jo.com>' or { name:'Jo Doe', email:'info@jo.com' }

View File

@ -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;

View File

@ -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;

View File

@ -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<Object>} 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:

View File

@ -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');
});
});
});

View File

@ -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 <a@b.com>');
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 <a@b.com>');
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 <a@b.com>');
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 <a@b.com>');
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;

View File

@ -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({