Implement Key.prototype.addSubkey (#963)

This commit is contained in:
Ilya Chesnokov 2019-09-16 20:53:19 +07:00 committed by Daniel Huigens
parent 9b5124d5cd
commit 7f40ab0940
2 changed files with 273 additions and 61 deletions

View File

@ -826,6 +826,37 @@ Key.prototype.verifyAllUsers = async function(keys) {
return results;
};
/**
* 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.
* @param {Integer} options.numBits number of bits for the key creation.
* @param {Number} [options.keyExpirationTime=0]
* The number of seconds after the key creation time that the key expires
* @param {String} curve (optional) Elliptic curve for ECC keys
* @param {Date} date (optional) Override the creation date of the key and the key signatures
* @param {Boolean} subkeys (optional) Indicates whether the subkey should sign rather than encrypt. Defaults to false
* @returns {Promise<module:key.Key>}
* @async
*/
Key.prototype.addSubkey = async function(options = {}) {
if (!this.isPrivate()) {
throw new Error("Cannot add a subkey to a public key");
}
const defaultOptions = this.primaryKey.getAlgorithmInfo();
defaultOptions.numBits = defaultOptions.bits;
const secretKeyPacket = this.primaryKey;
if (!secretKeyPacket.isDecrypted()) {
throw new Error("Key is not decrypted");
}
options = sanitizeKeyOptions(options, defaultOptions);
const keyPacket = await generateSecretSubkey(options);
const bindingSignature = await createBindingSignature(keyPacket, secretKeyPacket, options);
const packetList = this.toPacketlist();
packetList.push(keyPacket);
packetList.push(bindingSignature);
return new Key(packetList);
};
/**
* @class
* @classdesc Class that represents an user ID or attribute packet and the relevant signatures.
@ -1328,53 +1359,53 @@ export async function generate(options) {
let promises = [generateSecretKey(options)];
promises = promises.concat(options.subkeys.map(generateSecretSubkey));
return Promise.all(promises).then(packets => wrapKeyObject(packets[0], packets.slice(1), options));
}
function sanitizeKeyOptions(options, subkeyDefaults = {}) {
options.curve = options.curve || subkeyDefaults.curve;
options.numBits = options.numBits || subkeyDefaults.numBits;
options.keyExpirationTime = options.keyExpirationTime !== undefined ? options.keyExpirationTime : subkeyDefaults.keyExpirationTime;
options.passphrase = util.isString(options.passphrase) ? options.passphrase : subkeyDefaults.passphrase;
options.date = options.date || subkeyDefaults.date;
function sanitizeKeyOptions(options, subkeyDefaults = {}) {
options.curve = options.curve || subkeyDefaults.curve;
options.numBits = options.numBits || subkeyDefaults.numBits;
options.keyExpirationTime = options.keyExpirationTime !== undefined ? options.keyExpirationTime : subkeyDefaults.keyExpirationTime;
options.passphrase = util.isString(options.passphrase) ? options.passphrase : subkeyDefaults.passphrase;
options.date = options.date || subkeyDefaults.date;
options.sign = options.sign || false;
options.sign = options.sign || false;
if (options.curve) {
try {
options.curve = enums.write(enums.curve, options.curve);
} catch (e) {
throw new Error('Not valid curve.');
}
if (options.curve === enums.curve.ed25519 || options.curve === enums.curve.curve25519) {
options.curve = options.sign ? enums.curve.ed25519 : enums.curve.curve25519;
}
if (options.sign) {
options.algorithm = options.curve === enums.curve.ed25519 ? enums.publicKey.eddsa : enums.publicKey.ecdsa;
} else {
options.algorithm = enums.publicKey.ecdh;
}
} else if (options.numBits) {
options.algorithm = enums.publicKey.rsa_encrypt_sign;
} else {
throw new Error('Unrecognized key type');
if (options.curve) {
try {
options.curve = enums.write(enums.curve, options.curve);
} catch (e) {
throw new Error('Not valid curve.');
}
return options;
if (options.curve === enums.curve.ed25519 || options.curve === enums.curve.curve25519) {
options.curve = options.sign ? enums.curve.ed25519 : enums.curve.curve25519;
}
if (options.sign) {
options.algorithm = options.curve === enums.curve.ed25519 ? enums.publicKey.eddsa : enums.publicKey.ecdsa;
} else {
options.algorithm = enums.publicKey.ecdh;
}
} else if (options.numBits) {
options.algorithm = enums.publicKey.rsa_encrypt_sign;
} else {
throw new Error('Unrecognized key type');
}
return options;
}
async function generateSecretKey(options) {
const secretKeyPacket = new packet.SecretKey(options.date);
secretKeyPacket.packets = null;
secretKeyPacket.algorithm = enums.read(enums.publicKey, options.algorithm);
await secretKeyPacket.generate(options.numBits, options.curve);
return secretKeyPacket;
}
async function generateSecretKey(options) {
const secretKeyPacket = new packet.SecretKey(options.date);
secretKeyPacket.packets = null;
secretKeyPacket.algorithm = enums.read(enums.publicKey, options.algorithm);
await secretKeyPacket.generate(options.numBits, options.curve);
return secretKeyPacket;
}
async function generateSecretSubkey(options) {
const secretSubkeyPacket = new packet.SecretSubkey(options.date);
secretSubkeyPacket.packets = null;
secretSubkeyPacket.algorithm = enums.read(enums.publicKey, options.algorithm);
await secretSubkeyPacket.generate(options.numBits, options.curve);
return secretSubkeyPacket;
}
async function generateSecretSubkey(options) {
const secretSubkeyPacket = new packet.SecretSubkey(options.date);
secretSubkeyPacket.packets = null;
secretSubkeyPacket.algorithm = enums.read(enums.publicKey, options.algorithm);
await secretSubkeyPacket.generate(options.numBits, options.curve);
return secretSubkeyPacket;
}
/**
@ -1541,27 +1572,7 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options) {
await Promise.all(secretSubkeyPackets.map(async function(secretSubkeyPacket, index) {
const subkeyOptions = options.subkeys[index];
const dataToSign = {};
dataToSign.key = secretKeyPacket;
dataToSign.bind = secretSubkeyPacket;
const subkeySignaturePacket = new packet.Signature(subkeyOptions.date);
subkeySignaturePacket.signatureType = enums.signature.subkey_binding;
subkeySignaturePacket.publicKeyAlgorithm = secretKeyPacket.algorithm;
subkeySignaturePacket.hashAlgorithm = await getPreferredHashAlgo(null, secretSubkeyPacket);
if (subkeyOptions.sign) {
subkeySignaturePacket.keyFlags = [enums.keyFlags.sign_data];
subkeySignaturePacket.embeddedSignature = await createSignaturePacket(dataToSign, null, secretSubkeyPacket, {
signatureType: enums.signature.key_binding
}, subkeyOptions.date);
} else {
subkeySignaturePacket.keyFlags = [enums.keyFlags.encrypt_communication | enums.keyFlags.encrypt_storage];
}
if (subkeyOptions.keyExpirationTime > 0) {
subkeySignaturePacket.keyExpirationTime = subkeyOptions.keyExpirationTime;
subkeySignaturePacket.keyNeverExpires = false;
}
await subkeySignaturePacket.sign(secretKeyPacket, dataToSign);
const subkeySignaturePacket = await createBindingSignature(secretSubkeyPacket, secretKeyPacket, subkeyOptions);
return { secretSubkeyPacket, subkeySignaturePacket };
})).then(packets => {
packets.forEach(({ secretSubkeyPacket, subkeySignaturePacket }) => {
@ -1594,6 +1605,37 @@ async function wrapKeyObject(secretKeyPacket, secretSubkeyPackets, options) {
return new Key(packetlist);
}
/**
* Create subkey binding signature
* @see {@link https://tools.ietf.org/html/rfc4880#section-5.2.1|RFC4880 Section 5.2.1}
* @param {module:packet.SecretSubkey} subkey Subkey key packet
* @param {module:packet.SecretKey} primaryKey Primary key packet
* @param {Object} options
*/
async function createBindingSignature(subkey, primaryKey, options) {
const dataToSign = {};
dataToSign.key = primaryKey;
dataToSign.bind = subkey;
const subkeySignaturePacket = new packet.Signature(options.date);
subkeySignaturePacket.signatureType = enums.signature.subkey_binding;
subkeySignaturePacket.publicKeyAlgorithm = primaryKey.algorithm;
subkeySignaturePacket.hashAlgorithm = await getPreferredHashAlgo(null, subkey);
if (options.sign) {
subkeySignaturePacket.keyFlags = [enums.keyFlags.sign_data];
subkeySignaturePacket.embeddedSignature = await createSignaturePacket(dataToSign, null, subkey, {
signatureType: enums.signature.key_binding
}, options.date);
} else {
subkeySignaturePacket.keyFlags = [enums.keyFlags.encrypt_communication | enums.keyFlags.encrypt_storage];
}
if (options.keyExpirationTime > 0) {
subkeySignaturePacket.keyExpirationTime = options.keyExpirationTime;
subkeySignaturePacket.keyNeverExpires = false;
}
await subkeySignaturePacket.sign(primaryKey, dataToSign);
return subkeySignaturePacket;
}
/**
* Checks if a given certificate or binding signature is revoked
* @param {module:packet.SecretKey|

View File

@ -2865,3 +2865,173 @@ VYGdb3eNlV8CfoEC
})()).to.be.rejectedWith('Key packet is already encrypted');
});
});
describe('addSubkey functionality testing', function(){
it('create and add a new rsa subkey to a rsa key', async function() {
const privateKey = (await openpgp.key.readArmored(priv_key_rsa)).keys[0];
await privateKey.decrypt('hello world');
const total = privateKey.subKeys.length;
let newPrivateKey = await privateKey.addSubkey();
const armoredKey = newPrivateKey.armor();
newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0];
const subKey = newPrivateKey.subKeys[total];
expect(subKey).to.exist;
expect(newPrivateKey.subKeys.length).to.be.equal(total+1);
const subkeyN = subKey.keyPacket.params[0];
const pkN = privateKey.primaryKey.params[0];
expect(subkeyN.byteLength()).to.be.equal(pkN.byteLength());
expect(subKey.getAlgorithmInfo().algorithm).to.be.equal('rsa_encrypt_sign');
expect(await subKey.verify(newPrivateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid);
});
it('encrypt and decrypt key with added subkey', async function() {
const privateKey = (await openpgp.key.readArmored(priv_key_rsa)).keys[0];
await privateKey.decrypt('hello world');
const total = privateKey.subKeys.length;
let newPrivateKey = await privateKey.addSubkey();
newPrivateKey = (await openpgp.key.readArmored(newPrivateKey.armor())).keys[0];
await newPrivateKey.encrypt('12345678');
const armoredKey = newPrivateKey.armor();
let importedPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0];
await importedPrivateKey.decrypt('12345678');
const subKey = importedPrivateKey.subKeys[total];
expect(subKey).to.exist;
expect(importedPrivateKey.subKeys.length).to.be.equal(total+1);
const subkeyN = subKey.keyPacket.params[0];
const pkN = privateKey.primaryKey.params[0];
expect(subkeyN.byteLength()).to.be.equal(pkN.byteLength());
expect(subKey.getAlgorithmInfo().algorithm).to.be.equal('rsa_encrypt_sign');
expect(await subKey.verify(importedPrivateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid);
});
it('create and add a new ec subkey to a ec key', async function() {
const userId = 'test <a@b.com>';
const opt = {curve: 'curve25519', userIds: [userId], subkeys:[]};
const privateKey = (await openpgp.generateKey(opt)).key;
const total = privateKey.subKeys.length;
const opt2 = {curve: 'curve25519', userIds: [userId], sign: true};
let newPrivateKey = await privateKey.addSubkey(opt2);
const subKey1 = newPrivateKey.subKeys[total];
await newPrivateKey.encrypt('12345678');
const armoredKey = newPrivateKey.armor();
newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0];
await newPrivateKey.decrypt('12345678');
const subKey = newPrivateKey.subKeys[total];
expect(subKey.isDecrypted()).to.be.true;
expect(subKey1.getKeyId().toHex()).to.be.equal(subKey.getKeyId().toHex());
expect(subKey).to.exist;
expect(newPrivateKey.subKeys.length).to.be.equal(total+1);
const subkeyOid = subKey.keyPacket.params[0];
const pkOid = privateKey.primaryKey.params[0];
expect(subkeyOid.getName()).to.be.equal(pkOid.getName());
expect(subKey.getAlgorithmInfo().algorithm).to.be.equal('eddsa');
expect(await subKey.verify(privateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid);
});
it('create and add a new ec subkey to a rsa key', async function() {
const privateKey = (await openpgp.key.readArmored(priv_key_rsa)).keys[0];
privateKey.subKeys = [];
await privateKey.decrypt('hello world');
const total = privateKey.subKeys.length;
const opt2 = {curve: 'curve25519'};
let newPrivateKey = await privateKey.addSubkey(opt2);
const armoredKey = newPrivateKey.armor();
newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0];
const subKey = newPrivateKey.subKeys[total];
expect(subKey).to.exist;
expect(newPrivateKey.subKeys.length).to.be.equal(total+1);
expect(subKey.keyPacket.params[0].getName()).to.be.equal(openpgp.enums.curve.curve25519);
expect(subKey.getAlgorithmInfo().algorithm).to.be.equal('ecdh');
expect(await subKey.verify(privateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid);
});
it('sign/verify data with the new subkey correctly using curve25519', async function() {
const userId = 'test <a@b.com>';
const opt = {curve: 'curve25519', userIds: [userId], subkeys:[]};
const privateKey = (await openpgp.generateKey(opt)).key;
const total = privateKey.subKeys.length;
const opt2 = {sign: true};
let newPrivateKey = await privateKey.addSubkey(opt2);
const armoredKey = newPrivateKey.armor();
newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0];
const subKey = newPrivateKey.subKeys[total];
const subkeyOid = subKey.keyPacket.params[0];
const pkOid = newPrivateKey.primaryKey.params[0];
expect(subkeyOid.getName()).to.be.equal(pkOid.getName());
expect(subKey.getAlgorithmInfo().algorithm).to.be.equal('eddsa');
expect(await subKey.verify(newPrivateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid);
expect(await newPrivateKey.getSigningKey()).to.be.equal(subKey);
const signed = await openpgp.sign({message: openpgp.cleartext.fromText('the data to signed'), privateKeys: newPrivateKey, armor:false});
const verified = await signed.message.verify([newPrivateKey.toPublic()]);
expect(verified).to.exist;
expect(verified.length).to.be.equal(1);
expect(await verified[0].keyid).to.be.equal(subKey.getKeyId());
expect(await verified[0].verified).to.be.true;
});
it('encrypt/decrypt data with the new subkey correctly using curve25519', async function() {
const userId = 'test <a@b.com>';
const vData = 'the data to encrypted!';
const opt = {curve: 'curve25519', userIds: [userId], subkeys:[]};
const privateKey = (await openpgp.generateKey(opt)).key;
const total = privateKey.subKeys.length;
let newPrivateKey = await privateKey.addSubkey();
const armoredKey = newPrivateKey.armor();
newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0];
const subKey = newPrivateKey.subKeys[total];
const publicKey = newPrivateKey.toPublic();
expect(await subKey.verify(newPrivateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid);
expect(await newPrivateKey.getEncryptionKey()).to.be.equal(subKey);
const encrypted = await openpgp.encrypt({message: openpgp.message.fromText(vData), publicKeys: publicKey, armor:false});
expect(encrypted.message).to.be.exist;
const pkSessionKeys = encrypted.message.packets.filterByTag(openpgp.enums.packet.publicKeyEncryptedSessionKey);
expect(pkSessionKeys).to.exist;
expect(pkSessionKeys.length).to.be.equal(1);
expect(pkSessionKeys[0].publicKeyId.toHex()).to.be.equals(subKey.keyPacket.getKeyId().toHex());
const decrypted = await openpgp.decrypt({message: encrypted.message, privateKeys: newPrivateKey})
expect(decrypted).to.exist;
expect(decrypted.data).to.be.equal(vData);
});
it('sign/verify data with the new subkey correctly using rsa', async function() {
const privateKey = (await openpgp.key.readArmored(priv_key_rsa)).keys[0];
await privateKey.decrypt('hello world');
const total = privateKey.subKeys.length;
const opt2 = {sign: true};
let newPrivateKey = await privateKey.addSubkey(opt2);
const armoredKey = newPrivateKey.armor();
newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0];
const subKey = newPrivateKey.subKeys[total];
expect(subKey.getAlgorithmInfo().algorithm).to.be.equal('rsa_encrypt_sign');
expect(await subKey.verify(newPrivateKey.primaryKey)).to.be.equal(openpgp.enums.keyStatus.valid);
expect(await newPrivateKey.getSigningKey()).to.be.equal(subKey);
const signed = await openpgp.sign({message: openpgp.cleartext.fromText('the data to signed'), privateKeys: newPrivateKey, armor:false});
const verified = await signed.message.verify([newPrivateKey.toPublic()]);
expect(verified).to.exist;
expect(verified.length).to.be.equal(1);
expect(await verified[0].keyid).to.be.equal(subKey.getKeyId());
expect(await verified[0].verified).to.be.true;
});
it('encrypt/decrypt data with the new subkey correctly using rsa', async function() {
const privateKey = (await openpgp.key.readArmored(priv_key_rsa)).keys[0];
await privateKey.decrypt('hello world');
const total = privateKey.subKeys.length;
let newPrivateKey = await privateKey.addSubkey();
const armoredKey = newPrivateKey.armor();
newPrivateKey = (await openpgp.key.readArmored(armoredKey)).keys[0];
const subKey = newPrivateKey.subKeys[total];
const publicKey = newPrivateKey.toPublic();
const vData = 'the data to encrypted!';
expect(await newPrivateKey.getEncryptionKey()).to.be.equal(subKey);
const encrypted = await openpgp.encrypt({message: openpgp.message.fromText(vData), publicKeys: publicKey, armor:false});
expect(encrypted.message).to.be.exist;
const pkSessionKeys = encrypted.message.packets.filterByTag(openpgp.enums.packet.publicKeyEncryptedSessionKey);
expect(pkSessionKeys).to.exist;
expect(pkSessionKeys.length).to.be.equal(1);
expect(pkSessionKeys[0].publicKeyId.toHex()).to.be.equals(subKey.keyPacket.getKeyId().toHex());
const decrypted = await openpgp.decrypt({message: encrypted.message, privateKeys: newPrivateKey})
expect(decrypted).to.exist;
expect(decrypted.data).to.be.equal(vData);
});
});