Implement Key.prototype.addSubkey
(#963)
This commit is contained in:
parent
9b5124d5cd
commit
7f40ab0940
164
src/key.js
164
src/key.js
|
@ -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|
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user