From 5140a946e5e77fbd058d08b7ccb04aa37f839912 Mon Sep 17 00:00:00 2001 From: Aydar Zartdinov Date: Fri, 10 Feb 2017 12:12:05 +0300 Subject: [PATCH] Added ability to sign and verify public keys --- src/key.js | 84 ++++++++++++++++++++++++++++++++++++++- src/openpgp.js | 68 ++++++++++++++++++++++++++++++- test/general/openpgp.js | 68 +++++++++++++++++++++++++------ test/general/signature.js | 37 +++++++++++++++++ 4 files changed, 241 insertions(+), 16 deletions(-) diff --git a/src/key.js b/src/key.js index 9bcc1e68..afbe435a 100644 --- a/src/key.js +++ b/src/key.js @@ -507,11 +507,11 @@ function getExpirationTime(keyPacket, selfCertificate) { Key.prototype.getPrimaryUser = function() { var primUser = []; for (var i = 0; i < this.users.length; i++) { - if (!this.users[i].userId || !this.users[i].selfCertifications) { + if ((!this.users[i].userId && !this.users[i].userAttribute) || !this.users[i].selfCertifications) { continue; } for (var j = 0; j < this.users[i].selfCertifications.length; j++) { - primUser.push({user: this.users[i], selfCertificate: this.users[i].selfCertifications[j]}); + primUser.push({index: i, user: this.users[i], selfCertificate: this.users[i].selfCertifications[j]}); } } // sort by primary user flag and signature creation time @@ -638,6 +638,86 @@ Key.prototype.revoke = function() { }; +/** + * Signs the public key + * @param {Array} privateKey decrypted private keys for signing + * @return {module:key~Key} new public key with new certificate signature + */ +Key.prototype.sign = function(privateKeys) { + var primaryUser, primaryKey, dataToSign, signingKeyPacket, signaturePacket, user, key; + if (this.isPrivate()) { + throw new Error('Only public key can be signed'); + } + primaryUser = this.getPrimaryUser(); + if (!primaryUser) { + throw new Error('Could not find primary user'); + } + primaryKey = this.primaryKey; + dataToSign = {}; + dataToSign.userid = primaryUser.user.userId || primaryUser.user.userAttribute; + dataToSign.key = primaryKey; + user = new User(primaryUser.user.userId || primaryUser.user.userAttribute); + user.otherCertifications = []; + privateKeys.forEach(function(privateKey) { + if (privateKey.isPublic()) { + throw new Error('Need private key for signing'); + } + if (privateKey.primaryKey.getKeyId().equals(primaryKey.getKeyId())) { + throw new Error('No need to self signinig'); + } + signingKeyPacket = privateKey.getSigningKeyPacket(); + if (!signingKeyPacket) { + throw new Error('Could not find valid signing key packet'); + } + if (!signingKeyPacket.isDecrypted) { + throw new Error('Private key is not decrypted.'); + } + signaturePacket = new packet.Signature(); + // Most OpenPGP implementations use generic certification (0x10) + signaturePacket.signatureType = enums.write(enums.signature, enums.signature.cert_generic); + signaturePacket.keyFlags = [enums.keyFlags.certify_keys | enums.keyFlags.sign_data]; + signaturePacket.hashAlgorithm = privateKey.getPreferredHashAlgorithm(); + signaturePacket.publicKeyAlgorithm = signingKeyPacket.algorithm; + signaturePacket.signingKeyId = signingKeyPacket.getKeyId(); + signaturePacket.sign(signingKeyPacket, dataToSign); + user.otherCertifications.push(signaturePacket); + }); + user.update(primaryUser.user, this.primaryKey); + key = new Key(this.toPacketlist()); + key.users[primaryUser.index] = user; + return key; +}; + +/** + * Verifies public key + * @param {Array} keys array of keys to verify certificate signatures + * @return {Array<({keyid: module:type/keyid, valid: Boolean})>} list of signer's keyid and validity of signature + */ +Key.prototype.verify = function(keys) { + var primaryKey, primaryUser, user; + if (this.isPrivate()) { + throw new Error('Only public key can be verified'); + } + primaryKey = this.primaryKey; + primaryUser = this.getPrimaryUser(); + if (!primaryUser) { + throw new Error('Could not find primary user'); + } + user = primaryUser.user; + if (!user.otherCertifications) { + return []; + } + return user.otherCertifications.map(function(signaturePacket) { + var keyPacket = keys.find(function(key) { + return key.getSigningKeyPacket(signaturePacket.issuerKeyId); + }) || null; + return { + keyid: signaturePacket.issuerKeyId, + valid: keyPacket && signaturePacket.verify(keyPacket.primaryKey, {userid: user.userId || user.userAttribute, key: primaryKey}) + }; + }); +}; + /** * @class * @classdesc Class that represents an user ID or attribute packet and the relevant signatures. diff --git a/src/openpgp.js b/src/openpgp.js index b027991e..fd0d5b68 100644 --- a/src/openpgp.js +++ b/src/openpgp.js @@ -34,7 +34,7 @@ import * as messageLib from './message.js'; import * as cleartext from './cleartext.js'; -import * as key from './key.js'; +import * as keyLib from './key.js'; import config from './config/config.js'; import util from './util'; import AsyncProxy from './worker/async_proxy.js'; @@ -104,7 +104,7 @@ export function generateKey({ userIds=[], passphrase, numBits=2048, unlocked=fal return asyncProxy.delegate('generateKey', options); } - return key.generate(options).then(newKey => ({ + return keyLib.generate(options).then(newKey => ({ key: newKey, privateKeyArmored: newKey.armor(), @@ -361,6 +361,65 @@ export function decryptSessionKey({ message, privateKey, password }) { } +///////////////////////////////////////////// +// // +// Public key signing and verification // +// // +///////////////////////////////////////////// + + +/** + * Signs a paublic key. + * @param {Key} publicKey public key to be signed + * @param {Key|Array} privateKeys array of keys or single key with decrypted secret key data to sign public key + * @return {Promise} Public key object in form: + * { publicKey:Key, publicKeyArmored:String } + * @static + */ +export function signPublicKey({ publicKey, privateKeys }) { + checkKey(publicKey, 'publicKey'); + privateKeys = toArray(privateKeys); + + if (asyncProxy) { // use web worker if available + return asyncProxy.delegate('signPublicKey', { publicKey, privateKeys }); + } + + return execute(() => { + + const signedPublicKey = publicKey.sign(privateKeys); + + return { + publicKey: signedPublicKey, + publicKeyArmored: signedPublicKey.armor() + }; + + }, 'Error signing public key'); +} + +/** + * Verifies public key + * @param {Key} publicKey public key object with signatures + * @param {Key|Array} publicKeys array of publicKeys or single key, to verify signatures + * @return {Promise} cleartext with status of verified signatures in the form of: + * { signatures: [{ keyid:String, valid:Boolean|null }] } + * @static + */ +export function verifyPublicKey({ publicKey, publicKeys }) { + checkKey(publicKey, 'publicKey'); + publicKeys = toArray(publicKeys); + + if (asyncProxy) { // use web worker if available + return asyncProxy.delegate('verifyPublicKey', { publicKey, publicKeys }); + } + + return execute(() => ({ + + signatures: publicKey.verify(publicKeys) + + }), 'Error verifying signed public key'); +} + + ////////////////////////// // // // Helper functions // @@ -396,6 +455,11 @@ function checkCleartextMessage(message) { throw new Error('Parameter [message] needs to be of type CleartextMessage'); } } +function checkKey(key, name) { + if (!keyLib.Key.prototype.isPrototypeOf(key)) { + throw new Error('Parameter [' + (name || 'key') + '] needs to be of type Key'); + } +} /** * Format user ids for internal use. diff --git a/test/general/openpgp.js b/test/general/openpgp.js index 9cde0211..ebab63e7 100644 --- a/test/general/openpgp.js +++ b/test/general/openpgp.js @@ -158,6 +158,20 @@ var priv_key_de = '=kyeP', '-----END PGP PRIVATE KEY BLOCK-----'].join('\n'); + +var wrong_pubkey = [ + '-----BEGIN PGP PUBLIC KEY BLOCK-----', + 'Version: OpenPGP.js v0.9.0', + 'Comment: Hoodiecrow - https://hoodiecrow.com', + '', + 'xk0EUlhMvAEB/2MZtCUOAYvyLFjDp3OBMGn3Ev8FwjzyPbIF0JUw+L7y2XR5', + 'RVGvbK88unV3cU/1tOYdNsXI6pSp/Ztjyv7vbBUAEQEAAc0pV2hpdGVvdXQg', + 'VXNlciA8d2hpdGVvdXQudGVzdEB0LW9ubGluZS5kZT7CXAQQAQgAEAUCUlhM', + 'vQkQ9vYOm0LN/0wAAAW4Af9C+kYW1AvNWmivdtr0M0iYCUjM9DNOQH1fcvXq', + 'IiN602mWrkd8jcEzLsW5IUNzVPLhrFIuKyBDTpLnC07Loce1', + '=6XMW', + '-----END PGP PUBLIC KEY BLOCK-----'].join('\n'); + var passphrase = 'hello world'; var plaintext = 'short message\nnext line\n한국어/조선말'; var password1 = 'I am a password'; @@ -607,18 +621,6 @@ describe('OpenPGP.js public api tests', function() { }); describe('AES / RSA encrypt, decrypt, sign, verify', function() { - var wrong_pubkey = '-----BEGIN PGP PUBLIC KEY BLOCK-----\r\n' + - 'Version: OpenPGP.js v0.9.0\r\n' + - 'Comment: Hoodiecrow - https://hoodiecrow.com\r\n' + - '\r\n' + - 'xk0EUlhMvAEB/2MZtCUOAYvyLFjDp3OBMGn3Ev8FwjzyPbIF0JUw+L7y2XR5\r\n' + - 'RVGvbK88unV3cU/1tOYdNsXI6pSp/Ztjyv7vbBUAEQEAAc0pV2hpdGVvdXQg\r\n' + - 'VXNlciA8d2hpdGVvdXQudGVzdEB0LW9ubGluZS5kZT7CXAQQAQgAEAUCUlhM\r\n' + - 'vQkQ9vYOm0LN/0wAAAW4Af9C+kYW1AvNWmivdtr0M0iYCUjM9DNOQH1fcvXq\r\n' + - 'IiN602mWrkd8jcEzLsW5IUNzVPLhrFIuKyBDTpLnC07Loce1\r\n' + - '=6XMW\r\n' + - '-----END PGP PUBLIC KEY BLOCK-----\r\n\r\n'; - beforeEach(function() { expect(privateKey.keys[0].decrypt(passphrase)).to.be.true; }); @@ -909,6 +911,48 @@ describe('OpenPGP.js public api tests', function() { }); }); }); + + describe('signPublicKey, verifyPublicKey', function() { + beforeEach(function() { + expect(privateKey.keys[0].decrypt(passphrase)).to.be.true; + }); + + it('should sign and verify public key', function(done) { + var signOpt = { + publicKey: openpgp.key.readArmored(pub_key_de).keys[0], + privateKeys: privateKey.keys + }; + var verifyOpt = { + publicKeys: publicKey.keys + }; + openpgp.signPublicKey(signOpt).then(function(signed) { + verifyOpt.publicKey = signed.publicKey; + return openpgp.verifyPublicKey(verifyOpt); + }).then(function(verified) { + expect(verified.signatures[0].valid).to.be.true; + expect(verified.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex()); + done(); + }); + }); + + it('should sign and fail to verify public key with wrong public key', function(done) { + var signOpt = { + publicKey: openpgp.key.readArmored(pub_key_de).keys[0], + privateKeys: privateKey.keys + }; + var verifyOpt = { + publicKeys: openpgp.key.readArmored(wrong_pubkey).keys + }; + openpgp.signPublicKey(signOpt).then(function(signed) { + verifyOpt.publicKey = signed.publicKey; + return openpgp.verifyPublicKey(verifyOpt); + }).then(function(verified) { + expect(verified.signatures[0].valid).to.be.null; + expect(verified.signatures[0].keyid.toHex()).to.equal(privateKey.keys[0].getSigningKeyPacket().getKeyId().toHex()); + done(); + }); + }); + }); } }); diff --git a/test/general/signature.js b/test/general/signature.js index 7370380e..81eb6343 100644 --- a/test/general/signature.js +++ b/test/general/signature.js @@ -654,5 +654,42 @@ describe("Signature", function() { done(); }); }); + + it('Verify signed public key', function(done) { + var signedArmor = [ + '-----BEGIN PGP PUBLIC KEY BLOCK-----', + 'Version: GnuPG v1', + '', + 'mI0EUmEvTgEEANyWtQQMOybQ9JltDqmaX0WnNPJeLILIM36sw6zL0nfTQ5zXSS3+', + 'fIF6P29lJFxpblWk02PSID5zX/DYU9/zjM2xPO8Oa4xo0cVTOTLj++Ri5mtr//f5', + 'GLsIXxFrBJhD/ghFsL3Op0GXOeLJ9A5bsOn8th7x6JucNKuaRB6bQbSPABEBAAG0', + 'JFRlc3QgTWNUZXN0aW5ndG9uIDx0ZXN0QGV4YW1wbGUuY29tPoi5BBMBAgAjBQJS', + 'YS9OAhsvBwsJCAcDAgEGFQgCCQoLBBYCAwECHgECF4AACgkQSmNhOk1uQJQwDAP6', + 'AgrTyqkRlJVqz2pb46TfbDM2TDF7o9CBnBzIGoxBhlRwpqALz7z2kxBDmwpQa+ki', + 'Bq3jZN/UosY9y8bhwMAlnrDY9jP1gdCo+H0sD48CdXybblNwaYpwqC8VSpDdTndf', + '9j2wE/weihGp/DAdy/2kyBCaiOY1sjhUfJ1GogF49rCIRgQQEQIABgUCVuXBfQAK', + 'CRARJ5QDyxae+O0fAJ9hUQPejXvZv6VW1Q3/Pm3+x2wfJACgwFg9NlrPPfejoC1w', + 'P+z+vE5NFA24jQRSYS9OAQQA6R/PtBFaJaT4jq10yqASk4sqwVMsc6HcifM5lSdx', + 'zExFP74naUMMyEsKHP53QxTF0GrqusagQg/ZtgT0CN1HUM152y7ACOdp1giKjpMz', + 'OTQClqCoclyvWOFB+L/SwGEIJf7LSCErwoBuJifJc8xAVr0XX0JthoW+uP91eTQ3', + 'XpsAEQEAAYkBPQQYAQIACQUCUmEvTgIbLgCoCRBKY2E6TW5AlJ0gBBkBAgAGBQJS', + 'YS9OAAoJEOCE90RsICyXuqIEANmmiRCASF7YK7PvFkieJNwzeK0V3F2lGX+uu6Y3', + 'Q/Zxdtwc4xR+me/CSBmsURyXTO29OWhPGLszPH9zSJU9BdDi6v0yNprmFPX/1Ng0', + 'Abn/sCkwetvjxC1YIvTLFwtUL/7v6NS2bZpsUxRTg9+cSrMWWSNjiY9qUKajm1tu', + 'zPDZXAUEAMNmAN3xXN/Kjyvj2OK2ck0XW748sl/tc3qiKPMJ+0AkMF7Pjhmh9nxq', + 'E9+QCEl7qinFqqBLjuzgUhBU4QlwX1GDAtNTq6ihLMD5v1d82ZC7tNatdlDMGWnI', + 'dvEMCv2GZcuIqDQ9rXWs49e7tq1NncLYhz3tYjKhoFTKEIq3y3Pp', + '=fvK7', + '-----END PGP PUBLIC KEY BLOCK-----' + ].join('\n'); + + var sig_key = openpgp.key.readArmored(signedArmor).keys[0]; + var pub_key = openpgp.key.readArmored(priv_key_arm1).keys[0].toPublic(); + openpgp.verifyPublicKey({ publicKey: sig_key, publicKeys: [pub_key] }).then(function(verified) { + expect(verified.signatures[0].valid).to.be.true; + expect(verified.signatures[0].keyid.toHex()).to.equal(pub_key.primaryKey.getKeyId().toHex()); + done(); + }); + }); });