From 5a05cecdb8cdc20d9270ad967a13a15302a4f392 Mon Sep 17 00:00:00 2001 From: Simon Vareille Date: Mon, 10 Feb 2020 18:10:10 +0100 Subject: [PATCH 01/12] Restrict addition of signatures Sending an email to the primary userID to select which signatures to add when new signatures are uploaded. --- locales/en.json | 4 +++- src/email/email.js | 5 +++-- src/email/templates.js | 10 ++++++++- src/service/pgp.js | 47 ++++++++++++++++++++++++++++++++++----- src/service/public-key.js | 40 +++++++++++++++++++++++++++------ src/service/util.js | 46 +++++++++++++++++++++++++++++++++++++- 6 files changed, 135 insertions(+), 17 deletions(-) diff --git a/locales/en.json b/locales/en.json index a5599c4..6f30ed6 100644 --- a/locales/en.json +++ b/locales/en.json @@ -6,5 +6,7 @@ "verify_success_link": "Your public OpenPGP key is now available at the following link:", "verify_removal_subject": "Verify key removal", "verify_removal_text": "Hello {0},\n\nplease verify removal of your email address {1} from our key server ({2}) by clicking on the following link:\n\n{3}\n\nGreetings from the Mailvelope Team", - "removal_success": "Email address {0} removed from the key directory" + "removal_success": "Email address {0} removed from the key directory", + "confirm_signatures_subject": "Confirm new signatures", + "confirm_signatures_text": "Hello {0},\n\n{1} new signature(s) have been uploaded to your keys on our keyserver ({3}) ! Please select the ones you want to add by clicking on the following link:\n\n{2}\n\nGreetings from the Mailvelope Team" } diff --git a/src/email/email.js b/src/email/email.js index 8b8d4d7..4115c3c 100644 --- a/src/email/email.js +++ b/src/email/email.js @@ -53,12 +53,13 @@ class Email { * @param {Object} template the email template function to use * @param {Object} userId recipient user id object: { name:'Jon Smith', email:'j@smith.com', publicKeyArmored:'...' } * @param {string} keyId key id of public key + * @param {Object} data data used by template * @param {Object} origin origin of the server * @yield {Object} reponse object containing SMTP info */ - async send({template, userId, keyId, origin, publicKeyArmored}) { + async send({template, userId, keyId, data, origin, publicKeyArmored}) { const compiled = template({ - ...userId, + ...data, origin, keyId }); diff --git a/src/email/templates.js b/src/email/templates.js index 0accfa0..87f5217 100644 --- a/src/email/templates.js +++ b/src/email/templates.js @@ -18,4 +18,12 @@ function verifyRemove(ctx, {name, email, nonce, origin, keyId}) { }; } -module.exports = {verifyKey, verifyRemove}; +function confirmNewSigs(ctx, {name, sigsNb, nonce, origin, keyId}) { + const link = `${util.url(origin)}/api/v1/key?op=confirmSignatures&keyId=${keyId}&nonce=${nonce}`; + return { + subject: ctx.__('confirm_signatures_subject'), + text: ctx.__('confirm_signatures_text', [name, sigsNb, link, origin.host]) + }; +} + +module.exports = {verifyKey, verifyRemove, confirmNewSigs}; diff --git a/src/service/pgp.js b/src/service/pgp.js index 7368b76..8bb15d7 100644 --- a/src/service/pgp.js +++ b/src/service/pgp.js @@ -124,7 +124,7 @@ class PGP { * @param {Array} users A list of openpgp.js user objects * @param {Object} primaryKey The primary key packet of the key * @param {Date} verifyDate Verify user IDs at this point in time - * @return {Array, integer} An array of user id objects and a satus indicator. + * @return {Array, integer} An array of user id objects and a satus indicator * Values of status : 0 if no error, 1 if no address comes from a specific organisation. */ async parseUserIds(users, primaryKey, verifyDate = new Date()) { @@ -175,10 +175,10 @@ class PGP { } /** - * Merge (update) armored key blocks + * Merge (update) armored key blocks without adding new signatures * @param {String} srcArmored source amored key block * @param {String} dstArmored destination armored key block - * @return {String} merged armored key block + * @return {String, newSigs} merged armored key block, list of new signatures */ async updateKey(srcArmored, dstArmored) { const {keys: [srcKey], err: srcErr} = await openpgp.key.readArmored(srcArmored); @@ -191,10 +191,47 @@ class PGP { log.error('pgp', 'Failed to parse destination PGP key for update:\n%s', dstArmored, dstErr); util.throw(500, 'Failed to parse PGP key'); } + + // list new signatures + const newSigs=[]; + if(dstKey.hasSameFingerprintAs(srcKey)) { + const source = srcKey.directSignatures; + const dest = dstKey.directSignatures; + if(source) { + for(const sourceSig of source) { + if (!sourceSig.isExpired() && !dest.some(function(destSig) { + return util.equalsUint8Array(destSig.signature, sourceSig.signature); + })) { + newSigs.push(sourceSig); + } + } + } + // do not add new signatures + source = source.filter(sourceSig => !newSigs.some(function(sig) { + return util.equalsUint8Array(sig.signature, sourceSig.signature); + })); + } + await dstKey.update(srcKey); - return dstKey.armor(); + return {armored: dstKey.armor(), newSigs: newSigs}; } - + + /** + * Returns primary user and most significant (latest valid) self signature + * - if multiple primary users exist, returns the one with the latest self signature + * - otherwise, returns the user with the latest self signature + * @return {Object} The primary userId object: { name, email} + */ + async getPrimaryUser(publicKeyArmored) { + const {keys: [key], err: srcErr} = await openpgp.key.readArmored(publicKeyArmored); + if (srcErr) { + log.error('pgp', 'Failed to parse PGP key for getPrimaryUser:\n%s', publicKeyArmored, srcErr); + util.throw(500, 'Failed to parse PGP key'); + } + const primaryUser = key.getPrimaryUser(); + return {name: primaryUser.name, email: primaryUser.email}; + } + /** * Remove user ID from armored key block * @param {String} email email of user ID to be removed diff --git a/src/service/public-key.js b/src/service/public-key.js index de39e10..c272d44 100644 --- a/src/service/public-key.js +++ b/src/service/public-key.js @@ -89,10 +89,21 @@ class PublicKey { key.userIds = await this._mergeUsers(verified.userIds, key.userIds, key.publicKeyArmored); // reduce new key to verified user IDs const filteredPublicKeyArmored = await this._pgp.filterKeyByUserIds(key.userIds.filter(({verified}) => verified), key.publicKeyArmored); - // update verified key with new key - key.publicKeyArmored = await this._pgp.updateKey(verified.publicKeyArmored, filteredPublicKeyArmored); + // update verified key with new key and get new signatures + let newSigs = []; + {armored: key.publicKeyArmored, newSigs: newSigs} = await this._pgp.updateKey(verified.publicKeyArmored, filteredPublicKeyArmored); + // store pending signatures in key and generate nounce for confirmation + if(!key.pendingSignatures) + key.pendingSignatures = {sigs: newSigs, nonce: util.random()}; + else { + key.pendingSignatures = key.pendingSignatures.concat(newSigs.filter(sourceSig => !key.pendingSignatures.some(function(sig) { + return util.equalsUint8Array(sig.signature, sourceSig.signature); + }))); + } // send mails to verify all user ids await this._sendVerifyEmail(key, origin, ctx); + // send mail to confirm all new signatures + await this._sendNewSigsEmail(key, origin, ctx); // store key in database await this._persistKey(key); } else { @@ -175,7 +186,7 @@ class PublicKey { if (userId.notify && userId.notify === true) { // generate nonce for verification userId.nonce = util.random(); - await this._email.send({template: tpl.verifyKey.bind(null, ctx), userId, keyId, origin, publicKeyArmored: userId.publicKeyArmored}); + await this._email.send({template: tpl.verifyKey.bind(null, ctx), userId, keyId, userId, origin, publicKeyArmored: userId.publicKeyArmored}); } } } @@ -193,14 +204,29 @@ class PublicKey { if (userId.notify && userId.notify === true && util.isFromOrganisation(userId.email)) { // generate nonce for verification userId.nonce = util.random(); - await this._email.send({template: tpl.verifyKey.bind(null, ctx), userId, keyId, origin, publicKeyArmored: userId.publicKeyArmored}); + await this._email.send({template: tpl.verifyKey.bind(null, ctx), userId, keyId, userId, origin, publicKeyArmored: userId.publicKeyArmored}); } } } + + /** + * Send confirmation email to the public keys primary user ids for confirmation + * of new signatures addition. + * @param {Object} key key documents containg all the needed data + * @param {Object} origin the server's origin (required for email links) + * @param {Object} ctx Context + * @return {Promise} + */ + async _sendNewSigsEmail(key, origin, ctx) { + if(key.pendingSignatures.sigs.length){ + const primaryUser = this._pgp.getPrimaryUser(key.publicKeyArmored); + await this._email.send({template: tpl.confirmNewSigs.bind(null, ctx), primaryUser, key.keyId, {name: primaryUser.name, sigsNb: key.pendingSignatures.sigs.length, nonce: key.pendingSignatures.nonce}, origin, publicKeyArmored: key.publicKeyArmored}); + } + } /** * Persist the public key and its user ids in the database. - * @param {Object} key public key parameters + * @param {Object} key public key parameters * @return {Promise} */ async _persistKey(key) { @@ -271,7 +297,7 @@ class PublicKey { let {publicKeyArmored, email} = key.userIds.find(userId => userId.nonce === nonce); // update armored key if (key.publicKeyArmored) { - publicKeyArmored = await this._pgp.updateKey(key.publicKeyArmored, publicKeyArmored); + publicKeyArmored = await this._pgp.updateKey(key.publicKeyArmored, publicKeyArmored).armored; } // flag the user id as verified @@ -383,7 +409,7 @@ class PublicKey { // send verification mails keyId = key.keyId; // get keyId in case request was by email for (const userId of key.userIds) { - await this._email.send({template: tpl.verifyRemove.bind(null, ctx), userId, keyId, origin}); + await this._email.send({template: tpl.verifyRemove.bind(null, ctx), userId, keyId, userId, origin}); } } diff --git a/src/service/util.js b/src/service/util.js index 8ac7076..2304699 100644 --- a/src/service/util.js +++ b/src/service/util.js @@ -36,7 +36,29 @@ exports.isString = function(data) { exports.isTrue = function(data) { if (this.isString(data)) { return data === 'true'; - } else { + } else /** + * Check Uint8Array equality + * @param {Uint8Array} first array + * @param {Uint8Array} second array + * @returns {Boolean} equality + */ + equalsUint8Array: function (array1, array2) { + if (!util.isUint8Array(array1) || !util.isUint8Array(array2)) { + throw new Error('Data must be in the form of a Uint8Array'); + } + + if (array1.length !== array2.length) { + return false; + } + + for (let i = 0; i < array1.length; i++) { + if (array1[i] !== array2[i]) { + return false; + } + } + return true; + }, +{ return Boolean(data); } }; @@ -103,6 +125,28 @@ exports.normalizeEmail = function(email) { return email; }; +/** + * Check Uint8Array equality + * @param {Uint8Array} first array + * @param {Uint8Array} second array + * @returns {Boolean} equality + */ +exports.equalsUint8Array: function (array1, array2) { + try { + if (array1.length !== array2.length) { + return false; + } + + for (let i = 0; i < array1.length; i++) { + if (array1[i] !== array2[i]) { + return false; + } + } + } catch (e) {return false;} + return true; +}; + + /** * Create an error with a custom status attribute e.g. for http codes. * @param {number} status The error's http status code From 2a04c2486cfbc7a3fe644d5f241b05639b58fa89 Mon Sep 17 00:00:00 2001 From: Simon Vareille Date: Mon, 10 Feb 2020 21:17:58 +0100 Subject: [PATCH 02/12] Fix syntax error --- src/service/util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service/util.js b/src/service/util.js index 2304699..0d4c62f 100644 --- a/src/service/util.js +++ b/src/service/util.js @@ -131,7 +131,7 @@ exports.normalizeEmail = function(email) { * @param {Uint8Array} second array * @returns {Boolean} equality */ -exports.equalsUint8Array: function (array1, array2) { +exports.equalsUint8Array = function (array1, array2) { try { if (array1.length !== array2.length) { return false; From 5e288b98982ecda3fcb3d7baee6a181d577fd2ca Mon Sep 17 00:00:00 2001 From: Simon Vareille Date: Mon, 10 Feb 2020 21:21:41 +0100 Subject: [PATCH 03/12] Fix wrong paste --- src/service/util.js | 24 +----------------------- 1 file changed, 1 insertion(+), 23 deletions(-) diff --git a/src/service/util.js b/src/service/util.js index 0d4c62f..19dc93b 100644 --- a/src/service/util.js +++ b/src/service/util.js @@ -36,29 +36,7 @@ exports.isString = function(data) { exports.isTrue = function(data) { if (this.isString(data)) { return data === 'true'; - } else /** - * Check Uint8Array equality - * @param {Uint8Array} first array - * @param {Uint8Array} second array - * @returns {Boolean} equality - */ - equalsUint8Array: function (array1, array2) { - if (!util.isUint8Array(array1) || !util.isUint8Array(array2)) { - throw new Error('Data must be in the form of a Uint8Array'); - } - - if (array1.length !== array2.length) { - return false; - } - - for (let i = 0; i < array1.length; i++) { - if (array1[i] !== array2[i]) { - return false; - } - } - return true; - }, -{ + } else { return Boolean(data); } }; From cc6025baa159a0698d11760f34b771f83a3beb0e Mon Sep 17 00:00:00 2001 From: Simon Vareille Date: Mon, 10 Feb 2020 21:47:56 +0100 Subject: [PATCH 04/12] Fix syntax errors --- src/service/public-key.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/service/public-key.js b/src/service/public-key.js index c272d44..019a02f 100644 --- a/src/service/public-key.js +++ b/src/service/public-key.js @@ -90,8 +90,8 @@ class PublicKey { // reduce new key to verified user IDs const filteredPublicKeyArmored = await this._pgp.filterKeyByUserIds(key.userIds.filter(({verified}) => verified), key.publicKeyArmored); // update verified key with new key and get new signatures - let newSigs = []; - {armored: key.publicKeyArmored, newSigs: newSigs} = await this._pgp.updateKey(verified.publicKeyArmored, filteredPublicKeyArmored); + const {armored, newSigs} = await this._pgp.updateKey(verified.publicKeyArmored, filteredPublicKeyArmored); + key.publicKeyArmored = armored; // store pending signatures in key and generate nounce for confirmation if(!key.pendingSignatures) key.pendingSignatures = {sigs: newSigs, nonce: util.random()}; @@ -186,7 +186,7 @@ class PublicKey { if (userId.notify && userId.notify === true) { // generate nonce for verification userId.nonce = util.random(); - await this._email.send({template: tpl.verifyKey.bind(null, ctx), userId, keyId, userId, origin, publicKeyArmored: userId.publicKeyArmored}); + await this._email.send({template: tpl.verifyKey.bind(null, ctx), userId, keyId, data: userId, origin, publicKeyArmored: userId.publicKeyArmored}); } } } @@ -204,7 +204,7 @@ class PublicKey { if (userId.notify && userId.notify === true && util.isFromOrganisation(userId.email)) { // generate nonce for verification userId.nonce = util.random(); - await this._email.send({template: tpl.verifyKey.bind(null, ctx), userId, keyId, userId, origin, publicKeyArmored: userId.publicKeyArmored}); + await this._email.send({template: tpl.verifyKey.bind(null, ctx), userId, keyId, data: userId, origin, publicKeyArmored: userId.publicKeyArmored}); } } } @@ -220,7 +220,7 @@ class PublicKey { async _sendNewSigsEmail(key, origin, ctx) { if(key.pendingSignatures.sigs.length){ const primaryUser = this._pgp.getPrimaryUser(key.publicKeyArmored); - await this._email.send({template: tpl.confirmNewSigs.bind(null, ctx), primaryUser, key.keyId, {name: primaryUser.name, sigsNb: key.pendingSignatures.sigs.length, nonce: key.pendingSignatures.nonce}, origin, publicKeyArmored: key.publicKeyArmored}); + await this._email.send({template: tpl.confirmNewSigs.bind(null, ctx), primaryUser, keyId: key.keyId, data: {name: primaryUser.name, sigsNb: key.pendingSignatures.sigs.length, nonce: key.pendingSignatures.nonce}, origin, publicKeyArmored: key.publicKeyArmored}); } } @@ -409,7 +409,7 @@ class PublicKey { // send verification mails keyId = key.keyId; // get keyId in case request was by email for (const userId of key.userIds) { - await this._email.send({template: tpl.verifyRemove.bind(null, ctx), userId, keyId, userId, origin}); + await this._email.send({template: tpl.verifyRemove.bind(null, ctx), userId, keyId, data: userId, origin}); } } From 00e756795dea8743ec10a14d945dc7ae2d53cfd0 Mon Sep 17 00:00:00 2001 From: Simon Vareille Date: Fri, 14 Feb 2020 20:58:19 +0100 Subject: [PATCH 05/12] Effective signatures additions restriction. Link in email not implemented. --- src/service/pgp.js | 80 +++++++++++++++++++++------------ src/service/public-key.js | 53 ++++++++++++++++------ src/service/util.js | 93 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 185 insertions(+), 41 deletions(-) diff --git a/src/service/pgp.js b/src/service/pgp.js index 8bb15d7..b6d1660 100644 --- a/src/service/pgp.js +++ b/src/service/pgp.js @@ -173,12 +173,59 @@ class PGP { key.users = key.users.filter(({userId}) => !userId || emails.includes(util.normalizeEmail(userId.email))); return key.armor(); } + + /** + * Remove signatures from source armored key which are not in compared armored key + * @param {String} srcArmored armored key block to be filtered + * @param {String} cmpArmored armored key block to be compare with + * @return {String, newSigs} filterd armored key block, list of new signatures + */ + async filterKeyBySignatures(srcArmored, cmpArmored) { + const {keys: [srcKey], err: srcErr} = await openpgp.key.readArmored(srcArmored); + if (srcErr) { + log.error('pgp', 'Failed to parse source PGP key:\n%s', srcArmored, srcErr); + util.throw(500, 'Failed to parse PGP key'); + } + const {keys: [cmpKey], err: cmpErr} = await openpgp.key.readArmored(cmpArmored); + if (cmpErr) { + log.error('pgp', 'Failed to parse destination PGP key:\n%s', cmpArmored, cmpErr); + util.throw(500, 'Failed to parse PGP key'); + } + + const newSigs=[]; + if(cmpKey.hasSameFingerprintAs(srcKey)) { + await Promise.all(srcKey.users.map(async srcUser => { + await Promise.all(cmpKey.users.map(async dstUser => { + if ((srcUser.userId && dstUser.userId && + (srcUser.userId.userid === dstUser.userId.userid)) || + (srcUser.userAttribute && (srcUser.userAttribute.equals(dstUser.userAttribute)))) { + const source = srcUser.otherCertifications; + const dest = dstUser.otherCertifications; + for(let i = source.length-1; i >= 0; i--) { + const sourceSig = source[i]; + if (!sourceSig.isExpired() && !dest.some(function(destSig) { + return util.equalsUint8Array(destSig.signature, sourceSig.signature); + })) { + // list new signatures + let userId = (srcUser.userId) ? srcUser.userId.userid : null; + let userAttribute = (srcUser.userAttribute) ? srcUser.userAttribute : null; + newSigs.push({user: {userId: userId, userAttribute: userAttribute}, signature: sourceSig}); + // do not add new signatures + source.splice(i, 1); + } + } + } + })); + })); + } + return {armored: srcKey.armor(), newSigs: newSigs}; + } /** * Merge (update) armored key blocks without adding new signatures * @param {String} srcArmored source amored key block * @param {String} dstArmored destination armored key block - * @return {String, newSigs} merged armored key block, list of new signatures + * @return {String} merged amored key block */ async updateKey(srcArmored, dstArmored) { const {keys: [srcKey], err: srcErr} = await openpgp.key.readArmored(srcArmored); @@ -190,37 +237,16 @@ class PGP { if (dstErr) { log.error('pgp', 'Failed to parse destination PGP key for update:\n%s', dstArmored, dstErr); util.throw(500, 'Failed to parse PGP key'); - } - - // list new signatures - const newSigs=[]; - if(dstKey.hasSameFingerprintAs(srcKey)) { - const source = srcKey.directSignatures; - const dest = dstKey.directSignatures; - if(source) { - for(const sourceSig of source) { - if (!sourceSig.isExpired() && !dest.some(function(destSig) { - return util.equalsUint8Array(destSig.signature, sourceSig.signature); - })) { - newSigs.push(sourceSig); - } - } - } - // do not add new signatures - source = source.filter(sourceSig => !newSigs.some(function(sig) { - return util.equalsUint8Array(sig.signature, sourceSig.signature); - })); - } - + } await dstKey.update(srcKey); - return {armored: dstKey.armor(), newSigs: newSigs}; + return dstKey.armor(); } /** * Returns primary user and most significant (latest valid) self signature * - if multiple primary users exist, returns the one with the latest self signature * - otherwise, returns the user with the latest self signature - * @return {Object} The primary userId object: { name, email} + * @return {Object} The primary userId */ async getPrimaryUser(publicKeyArmored) { const {keys: [key], err: srcErr} = await openpgp.key.readArmored(publicKeyArmored); @@ -228,8 +254,8 @@ class PGP { log.error('pgp', 'Failed to parse PGP key for getPrimaryUser:\n%s', publicKeyArmored, srcErr); util.throw(500, 'Failed to parse PGP key'); } - const primaryUser = key.getPrimaryUser(); - return {name: primaryUser.name, email: primaryUser.email}; + const primaryUser = await key.getPrimaryUser(); + return primaryUser; } /** diff --git a/src/service/public-key.js b/src/service/public-key.js index 019a02f..069ad17 100644 --- a/src/service/public-key.js +++ b/src/service/public-key.js @@ -88,18 +88,25 @@ class PublicKey { if (verified) { key.userIds = await this._mergeUsers(verified.userIds, key.userIds, key.publicKeyArmored); // reduce new key to verified user IDs - const filteredPublicKeyArmored = await this._pgp.filterKeyByUserIds(key.userIds.filter(({verified}) => verified), key.publicKeyArmored); - // update verified key with new key and get new signatures - const {armored, newSigs} = await this._pgp.updateKey(verified.publicKeyArmored, filteredPublicKeyArmored); - key.publicKeyArmored = armored; + let filteredPublicKeyArmored = await this._pgp.filterKeyByUserIds(key.userIds.filter(({verified}) => verified), key.publicKeyArmored); + // reduce new key to verified signatures and get new signatures + const {armored, newSigs} = await this._pgp.filterKeyBySignatures(filteredPublicKeyArmored, verified.publicKeyArmored); + filteredPublicKeyArmored = armored; + // update verified key with new key + key.publicKeyArmored = await this._pgp.updateKey(verified.publicKeyArmored, filteredPublicKeyArmored); // store pending signatures in key and generate nounce for confirmation - if(!key.pendingSignatures) - key.pendingSignatures = {sigs: newSigs, nonce: util.random()}; - else { - key.pendingSignatures = key.pendingSignatures.concat(newSigs.filter(sourceSig => !key.pendingSignatures.some(function(sig) { - return util.equalsUint8Array(sig.signature, sourceSig.signature); - }))); + if(newSigs.length) { + await this._formatArrays(newSigs); + if(!verified.pendingSignatures) + key.pendingSignatures = {sigs: newSigs, nonce: util.random()}; + else { + key.pendingSignatures = verified.pendingSignatures; + key.pendingSignatures.sigs = verified.pendingSignatures.sigs.concat(newSigs.filter(sourceSig => !verified.pendingSignatures.sigs.some(function(pendingSig) { + return pendingSig.signature.signature === sourceSig.signature.signature; + }))); + } } + // send mails to verify all user ids await this._sendVerifyEmail(key, origin, ctx); // send mail to confirm all new signatures @@ -172,6 +179,21 @@ class PublicKey { _includeEmail(users, user) { return users.find(({email}) => email === user.email); } + + /** + * Convert all Uint8Array in every signatures to base64. + * @param {Array} signatures list of signatures to convert + * @return {Promise} + */ + async _formatArrays(signatures) { + signatures.map(function(sig) { + const signature = sig.signature; + signature.signatureData = util.base64EncArr(signature.signatureData); + signature.signedHashValue = util.base64EncArr(signature.signedHashValue); + signature.issuerFingerprint = util.base64EncArr(signature.issuerFingerprint); + signature.signature = util.base64EncArr(signature.signature); + }); + } /** * Send verification emails to the public keys user ids for verification. @@ -218,9 +240,10 @@ class PublicKey { * @return {Promise} */ async _sendNewSigsEmail(key, origin, ctx) { - if(key.pendingSignatures.sigs.length){ - const primaryUser = this._pgp.getPrimaryUser(key.publicKeyArmored); - await this._email.send({template: tpl.confirmNewSigs.bind(null, ctx), primaryUser, keyId: key.keyId, data: {name: primaryUser.name, sigsNb: key.pendingSignatures.sigs.length, nonce: key.pendingSignatures.nonce}, origin, publicKeyArmored: key.publicKeyArmored}); + if(key.pendingSignatures && key.pendingSignatures.sigs.length){ + let primaryUser = await this._pgp.getPrimaryUser(key.publicKeyArmored); + const userId = primaryUser.user.userId; + await this._email.send({template: tpl.confirmNewSigs.bind(null, ctx), userId, keyId: key.keyId, data: {name: userId.name, sigsNb: key.pendingSignatures.sigs.length, nonce: key.pendingSignatures.nonce}, origin, publicKeyArmored: key.publicKeyArmored}); } } @@ -297,7 +320,7 @@ class PublicKey { let {publicKeyArmored, email} = key.userIds.find(userId => userId.nonce === nonce); // update armored key if (key.publicKeyArmored) { - publicKeyArmored = await this._pgp.updateKey(key.publicKeyArmored, publicKeyArmored).armored; + publicKeyArmored = await this._pgp.updateKey(key.publicKeyArmored, publicKeyArmored); } // flag the user id as verified @@ -386,6 +409,8 @@ class PublicKey { email: uid.email, verified: uid.verified })); + if(key.pendingSignatures) + delete key.pendingSignatures.nonce return key; } diff --git a/src/service/util.js b/src/service/util.js index 19dc93b..ea7ab8a 100644 --- a/src/service/util.js +++ b/src/service/util.js @@ -124,6 +124,99 @@ exports.equalsUint8Array = function (array1, array2) { return true; }; +/** + * Decode a base64 byte to uint6 + * @param {Integer} nChr base64 byte to decode + * @returns {Integer} the uint6 integer + * Comes from https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding + * All thanks to madmurphy + */ +function b64ToUint6 (nChr) { + return nChr > 64 && nChr < 91 ? + nChr - 65 + : nChr > 96 && nChr < 123 ? + nChr - 71 + : nChr > 47 && nChr < 58 ? + nChr + 4 + : nChr === 43 ? + 62 + : nChr === 47 ? + 63 + : + 0; +}; + +/** + * Decode a base64 String to Uint8Array + * @param {String} sBase64 base64 string to decode + * @returns {Uint8Array} decoded data + * Comes from https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding + * All thanks to madmurphy + */ +exports.base64DecToArr = function (sBase64) { + var sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length, + nOutLen = nInLen * 3 + 1 >> 2, taBytes = new Uint8Array(nOutLen); + + for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) { + nMod4 = nInIdx & 3; + nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4; + if (nMod4 === 3 || nInLen - nInIdx === 1) { + for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { + taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255; + } + nUint24 = 0; + } + } + return taBytes; +}; + +/** + * Encode a uint6 to base64 byte + * @param {Integer} nUint6 integer to encode + * @returns {Integer} the base64 byte + * Comes from https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding + * All thanks to madmurphy + */ +function uint6ToB64 (nUint6) { + return nUint6 < 26 ? + nUint6 + 65 + : nUint6 < 52 ? + nUint6 + 71 + : nUint6 < 62 ? + nUint6 - 4 + : nUint6 === 62 ? + 43 + : nUint6 === 63 ? + 47 + : + 65; +}; + + +/** + * Encode a Uint8Array to base64 + * @param {Uint8Array} aBytes array to encode + * @returns {String} base64 String + * Comes from https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding + * All thanks to madmurphy + */ +exports.base64EncArr = function (aBytes) { + var eqLen = (3 - (aBytes.length % 3)) % 3, sB64Enc = ""; + for (var nMod3, nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) { + nMod3 = nIdx % 3; + /* Uncomment the following line in order to split the output in lines 76-character long: */ + /* + if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) { sB64Enc += "\r\n"; } + */ + nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24); + if (nMod3 === 2 || aBytes.length - nIdx === 1) { + sB64Enc += String.fromCharCode(uint6ToB64(nUint24 >>> 18 & 63), uint6ToB64(nUint24 >>> 12 & 63), uint6ToB64(nUint24 >>> 6 & 63), uint6ToB64(nUint24 & 63)); + nUint24 = 0; + } + } + return eqLen === 0 ? sB64Enc : sB64Enc.substring(0, sB64Enc.length - eqLen) + (eqLen === 1 ? "=" : "=="); +}; + /** * Create an error with a custom status attribute e.g. for http codes. From 86da9572a1f6d9ff761b7d16a45558badfc7ac50 Mon Sep 17 00:00:00 2001 From: Simon Vareille Date: Sat, 15 Feb 2020 15:15:15 +0100 Subject: [PATCH 06/12] Toggle organisation restriction from config file. --- config/default.js | 4 +++- config/development.js | 5 +++++ src/service/pgp.js | 3 ++- src/service/public-key.js | 16 ++++++++++++---- src/service/util.js | 3 ++- 5 files changed, 24 insertions(+), 7 deletions(-) diff --git a/config/default.js b/config/default.js index ee2ff16..5a9307d 100644 --- a/config/default.js +++ b/config/default.js @@ -41,7 +41,9 @@ module.exports = { }, publicKey: { - purgeTimeInDays: process.env.PUBLIC_KEY_PURGE_TIME || 30 + purgeTimeInDays: process.env.PUBLIC_KEY_PURGE_TIME || 30, + restrictUserOrigin: process.env.RESTRICT_USER_ORIGIN || false, + restrictionRegEx: process.env.RESTRICTION_REGEX } }; diff --git a/config/development.js b/config/development.js index 4e28b5d..8af9737 100755 --- a/config/development.js +++ b/config/development.js @@ -22,6 +22,11 @@ module.exports = { name: 'OpenPGP Key Server', email: 'user@gmail.com' } + }, + + publicKey: { + restrictUserOrigin: true, + restrictionRegEx: '^([a-z0-9\-.]+)@([a-z0-9.\-]*)esisar\.grenoble-inp\.fr$' } }; diff --git a/src/service/pgp.js b/src/service/pgp.js index b6d1660..4bdcbe2 100644 --- a/src/service/pgp.js +++ b/src/service/pgp.js @@ -19,6 +19,7 @@ const log = require('winston'); const util = require('./util'); +const config = require('config'); const openpgp = require('openpgp'); const KEY_BEGIN = '-----BEGIN PGP PUBLIC KEY BLOCK-----'; @@ -154,7 +155,7 @@ class PGP { } } var status = 0; - if(!isFromOrganisation){ + if(config.publicKey.restrictUserOrigin && !isFromOrganisation ){ result.length = 0; status = 1; } diff --git a/src/service/public-key.js b/src/service/public-key.js index 069ad17..b5f921f 100644 --- a/src/service/public-key.js +++ b/src/service/public-key.js @@ -121,10 +121,18 @@ class PublicKey { await this._addKeyArmored(key.userIds, key.publicKeyArmored); // new key, set armored to null key.publicKeyArmored = null; - // send mails to verify organisation's user ids - await this._sendVerifyOrganisationEmail(key, origin, ctx); - // store key in database - await this._persistKeyOrganisation(key); + if(config.publicKey.restrictUserOrigin) { + // send mails to verify organisation's user ids + await this._sendVerifyOrganisationEmail(key, origin, ctx); + // store key in database + await this._persistKeyOrganisation(key); + } + else { + // send mails to verify all user ids + await this._sendVerifyEmail(key, origin, ctx); + // store key in database + await this._persistKey(key); + } } } diff --git a/src/service/util.js b/src/service/util.js index ea7ab8a..fb6beee 100644 --- a/src/service/util.js +++ b/src/service/util.js @@ -18,6 +18,7 @@ 'use strict'; const crypto = require('crypto'); +const config = require('config'); /** * Checks for a valid string @@ -87,7 +88,7 @@ exports.isFromOrganisation = function(data) { if (!this.isString(data)) { return false; } - const re = /^([a-z0-9\-.]+)@([a-z0-9.\-]*)esisar\.grenoble-inp\.fr$/; + const re = new RegExp(config.publicKey.restrictionRegEx, 'g'); return re.test(data); }; From 595e2b66a87e0ac40566ab40d131fb930cb9624c Mon Sep 17 00:00:00 2001 From: Simon Vareille Date: Mon, 17 Feb 2020 17:27:17 +0100 Subject: [PATCH 07/12] Update base64 encoding to use Buffer class --- src/service/public-key.js | 9 ++-- src/service/util.js | 94 --------------------------------------- 2 files changed, 5 insertions(+), 98 deletions(-) diff --git a/src/service/public-key.js b/src/service/public-key.js index b5f921f..6ef7415 100644 --- a/src/service/public-key.js +++ b/src/service/public-key.js @@ -196,10 +196,11 @@ class PublicKey { async _formatArrays(signatures) { signatures.map(function(sig) { const signature = sig.signature; - signature.signatureData = util.base64EncArr(signature.signatureData); - signature.signedHashValue = util.base64EncArr(signature.signedHashValue); - signature.issuerFingerprint = util.base64EncArr(signature.issuerFingerprint); - signature.signature = util.base64EncArr(signature.signature); + const attributes = ['signatureData', 'unhashedSubpackets', 'signedHashValue', 'issuerFingerprint', 'signature', ]; + for (const attrib of attributes) { + if(signature[attrib] != null) + signature[attrib] = Buffer.from(signature[attrib]).toString('base64'); + } }); } diff --git a/src/service/util.js b/src/service/util.js index fb6beee..3f38a52 100644 --- a/src/service/util.js +++ b/src/service/util.js @@ -125,100 +125,6 @@ exports.equalsUint8Array = function (array1, array2) { return true; }; -/** - * Decode a base64 byte to uint6 - * @param {Integer} nChr base64 byte to decode - * @returns {Integer} the uint6 integer - * Comes from https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding - * All thanks to madmurphy - */ -function b64ToUint6 (nChr) { - return nChr > 64 && nChr < 91 ? - nChr - 65 - : nChr > 96 && nChr < 123 ? - nChr - 71 - : nChr > 47 && nChr < 58 ? - nChr + 4 - : nChr === 43 ? - 62 - : nChr === 47 ? - 63 - : - 0; -}; - -/** - * Decode a base64 String to Uint8Array - * @param {String} sBase64 base64 string to decode - * @returns {Uint8Array} decoded data - * Comes from https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding - * All thanks to madmurphy - */ -exports.base64DecToArr = function (sBase64) { - var sB64Enc = sBase64.replace(/[^A-Za-z0-9\+\/]/g, ""), nInLen = sB64Enc.length, - nOutLen = nInLen * 3 + 1 >> 2, taBytes = new Uint8Array(nOutLen); - - for (var nMod3, nMod4, nUint24 = 0, nOutIdx = 0, nInIdx = 0; nInIdx < nInLen; nInIdx++) { - nMod4 = nInIdx & 3; - nUint24 |= b64ToUint6(sB64Enc.charCodeAt(nInIdx)) << 18 - 6 * nMod4; - if (nMod4 === 3 || nInLen - nInIdx === 1) { - for (nMod3 = 0; nMod3 < 3 && nOutIdx < nOutLen; nMod3++, nOutIdx++) { - taBytes[nOutIdx] = nUint24 >>> (16 >>> nMod3 & 24) & 255; - } - nUint24 = 0; - } - } - return taBytes; -}; - -/** - * Encode a uint6 to base64 byte - * @param {Integer} nUint6 integer to encode - * @returns {Integer} the base64 byte - * Comes from https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding - * All thanks to madmurphy - */ -function uint6ToB64 (nUint6) { - return nUint6 < 26 ? - nUint6 + 65 - : nUint6 < 52 ? - nUint6 + 71 - : nUint6 < 62 ? - nUint6 - 4 - : nUint6 === 62 ? - 43 - : nUint6 === 63 ? - 47 - : - 65; -}; - - -/** - * Encode a Uint8Array to base64 - * @param {Uint8Array} aBytes array to encode - * @returns {String} base64 String - * Comes from https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding - * All thanks to madmurphy - */ -exports.base64EncArr = function (aBytes) { - var eqLen = (3 - (aBytes.length % 3)) % 3, sB64Enc = ""; - for (var nMod3, nLen = aBytes.length, nUint24 = 0, nIdx = 0; nIdx < nLen; nIdx++) { - nMod3 = nIdx % 3; - /* Uncomment the following line in order to split the output in lines 76-character long: */ - /* - if (nIdx > 0 && (nIdx * 4 / 3) % 76 === 0) { sB64Enc += "\r\n"; } - */ - nUint24 |= aBytes[nIdx] << (16 >>> nMod3 & 24); - if (nMod3 === 2 || aBytes.length - nIdx === 1) { - sB64Enc += String.fromCharCode(uint6ToB64(nUint24 >>> 18 & 63), uint6ToB64(nUint24 >>> 12 & 63), uint6ToB64(nUint24 >>> 6 & 63), uint6ToB64(nUint24 & 63)); - nUint24 = 0; - } - } - return eqLen === 0 ? sB64Enc : sB64Enc.substring(0, sB64Enc.length - eqLen) + (eqLen === 1 ? "=" : "=="); -}; - - /** * Create an error with a custom status attribute e.g. for http codes. * @param {number} status The error's http status code From 87b6ca326fbed8ed18027e753d80a082217c576a Mon Sep 17 00:00:00 2001 From: Simon Vareille Date: Tue, 25 Feb 2020 17:14:33 +0100 Subject: [PATCH 08/12] Armor every UInt8Array for pending signatures --- src/service/public-key.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/service/public-key.js b/src/service/public-key.js index 6ef7415..41aecc1 100644 --- a/src/service/public-key.js +++ b/src/service/public-key.js @@ -196,7 +196,7 @@ class PublicKey { async _formatArrays(signatures) { signatures.map(function(sig) { const signature = sig.signature; - const attributes = ['signatureData', 'unhashedSubpackets', 'signedHashValue', 'issuerFingerprint', 'signature', ]; + const attributes = ['signatureData', 'unhashedSubpackets', 'signedHashValue', 'preferredSymmetricAlgorithms', 'revocationKeyFingerprint', 'preferredHashAlgorithms', 'preferredCompressionAlgorithms', 'keyServerPreferences', 'keyFlags', 'features', 'issuerFingerprint', 'preferredAeadAlgorithms', 'signature']; for (const attrib of attributes) { if(signature[attrib] != null) signature[attrib] = Buffer.from(signature[attrib]).toString('base64'); From 2496094432b3525cc619eecc47bb911e65947dc9 Mon Sep 17 00:00:00 2001 From: Simon Vareille Date: Thu, 11 Jun 2020 17:51:16 +0200 Subject: [PATCH 09/12] Prevent anybody to flood a key with signatures Ask for key owner's permission to add signatures to key. Doesn't manage well any key. --- src/static/js/verify-cert.js | 47 +++++++++++++++++++++++++++++++++++ src/view/verify-certs.html | 48 ++++++++++++++++++++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 src/static/js/verify-cert.js create mode 100755 src/view/verify-certs.html diff --git a/src/static/js/verify-cert.js b/src/static/js/verify-cert.js new file mode 100644 index 0000000..a191c0a --- /dev/null +++ b/src/static/js/verify-cert.js @@ -0,0 +1,47 @@ +/* eslint-disable */ + +;(function($) { + 'use strict'; + + // POST signatures form + $('#signatures form').submit(function(e) { + e.preventDefault(); + $('#signatures .alert').addClass('hidden'); + var elements = $('#signatures form')[0]; + var obj = {sig: []}; + for(var elem of elements){ + switch(elem.name) { + case "op": + case "keyId": + case "nonce": + obj[elem.name] = elem.value; + break; + case "sig": + if(elem.checked) + obj["sig"].push(elem.value); + break; + } + } + $.ajax({ + method: 'POST', + url: '/api/v1/key', + data: JSON.stringify(obj), + contentType: 'application/json', + }).done(function(data, textStatus, xhr) { + if (xhr.status === 304) { + alert('signatures', 'danger', 'Key already exists!'); + } else { + alert('signatures', 'success', xhr.responseText); + } + }) + .fail(function(xhr) { + alert('signatures', 'danger', xhr.responseText); + }); + }); + + function alert(region, outcome, text) { + $('#' + region + ' .alert-' + outcome + ' span').html(text); + $('#' + region + ' .alert-' + outcome).removeClass('hidden'); + } + +}(jQuery)); diff --git a/src/view/verify-certs.html b/src/view/verify-certs.html new file mode 100755 index 0000000..6f81939 --- /dev/null +++ b/src/view/verify-certs.html @@ -0,0 +1,48 @@ + +
+ +
+ +

Mailvelope Key Server

+
+ +
+
+

Select the signatures you want to add to your key

+ + +
+
pub 0x<%= keyId %> 
+<% for(const [userid, signatures] of sigs) { %>
+uid <%= userid %>
+<% for(sig of signatures) { %>
+  
+<% } %> +<% } %> +
+ +
+
+
+ + <%- include('footer') %> + +
+ + + From 4a5b8cc003f603acc050cfa65f514a9ff55f1625 Mon Sep 17 00:00:00 2001 From: Simon Vareille Date: Fri, 12 Jun 2020 21:21:29 +0200 Subject: [PATCH 10/12] Prevention of flooding keys with signatures Manage unusual signatures. --- locales/en.json | 4 +- src/app/index.js | 2 +- src/email/templates.js | 10 ++-- src/route/rest.js | 55 ++++++++++++++++-- src/service/pgp.js | 42 +++++++++++++- src/service/public-key.js | 115 +++++++++++++++++++++++++++++++------- 6 files changed, 192 insertions(+), 36 deletions(-) diff --git a/locales/en.json b/locales/en.json index 6f30ed6..4e88879 100644 --- a/locales/en.json +++ b/locales/en.json @@ -7,6 +7,6 @@ "verify_removal_subject": "Verify key removal", "verify_removal_text": "Hello {0},\n\nplease verify removal of your email address {1} from our key server ({2}) by clicking on the following link:\n\n{3}\n\nGreetings from the Mailvelope Team", "removal_success": "Email address {0} removed from the key directory", - "confirm_signatures_subject": "Confirm new signatures", - "confirm_signatures_text": "Hello {0},\n\n{1} new signature(s) have been uploaded to your keys on our keyserver ({3}) ! Please select the ones you want to add by clicking on the following link:\n\n{2}\n\nGreetings from the Mailvelope Team" + "check_signatures_subject": "Confirm new signatures", + "check_signatures_text": "Hello {0},\n\n{1} new signature(s) have been uploaded to your keys on our keyserver ({3}) ! Please select the ones you want to add by clicking on the following link:\n\n{2}\n\nGreetings from the Mailvelope Team" } diff --git a/src/app/index.js b/src/app/index.js index 7ce71c8..b0c59f6 100644 --- a/src/app/index.js +++ b/src/app/index.js @@ -62,7 +62,7 @@ router.get('/manage.html', ctx => ctx.render('manage')); // HKP and REST api routes router.post('/pks/add', ctx => hkp.add(ctx)); router.get('/pks/lookup', ctx => hkp.lookup(ctx)); -router.post('/api/v1/key', ctx => rest.create(ctx)); +router.post('/api/v1/key', ctx => rest.postHandler(ctx)); router.get('/api/v1/key', ctx => rest.query(ctx)); router.del('/api/v1/key', ctx => rest.remove(ctx)); diff --git a/src/email/templates.js b/src/email/templates.js index 87f5217..3462f41 100644 --- a/src/email/templates.js +++ b/src/email/templates.js @@ -18,12 +18,12 @@ function verifyRemove(ctx, {name, email, nonce, origin, keyId}) { }; } -function confirmNewSigs(ctx, {name, sigsNb, nonce, origin, keyId}) { - const link = `${util.url(origin)}/api/v1/key?op=confirmSignatures&keyId=${keyId}&nonce=${nonce}`; +function checkNewSigs(ctx, {name, sigsNb, nonce, origin, keyId}) { + const link = `${util.url(origin)}/api/v1/key?op=checkSignatures&keyId=${keyId}&nonce=${nonce}`; return { - subject: ctx.__('confirm_signatures_subject'), - text: ctx.__('confirm_signatures_text', [name, sigsNb, link, origin.host]) + subject: ctx.__('check_signatures_subject'), + text: ctx.__('check_signatures_text', [name, sigsNb, link, origin.host]) }; } -module.exports = {verifyKey, verifyRemove, confirmNewSigs}; +module.exports = {verifyKey, verifyRemove, checkNewSigs}; diff --git a/src/route/rest.js b/src/route/rest.js index 04f51ed..4bf1fba 100644 --- a/src/route/rest.js +++ b/src/route/rest.js @@ -32,13 +32,26 @@ class REST { constructor(publicKey) { this._publicKey = publicKey; } - + + /** + * http POST handler + * @param {Object} ctx The koa request/response context + */ + async postHandler(ctx) { + const json = await parse.json(ctx, {limit: '1mb'}); + if(json.op === 'confirmSignatures') + return this[json.op](ctx, json);//delegate operation + + await this.create(ctx, json); + } + /** * Public key / user ID upload via http POST * @param {Object} ctx The koa request/response context + * @param {Object} json The json content of the request */ - async create(ctx) { - const {emails, publicKeyArmored} = await parse.json(ctx, {limit: '1mb'}); + async create(ctx, json) { + const {emails, publicKeyArmored} = json || await parse.json(ctx, {limit: '1mb'}); if (!publicKeyArmored) { ctx.throw(400, 'Invalid request!'); } @@ -54,7 +67,8 @@ class REST { */ async query(ctx) { const op = ctx.query.op; - if (op === 'verify' || op === 'verifyRemove') { + if (op === 'verify' || op === 'verifyRemove' || op === 'confirmSignatures' || + op === 'checkSignatures') { return this[op](ctx); // delegate operation } // do READ if no 'op' provided @@ -79,7 +93,38 @@ class REST { const link = util.url(util.origin(ctx), `/pks/lookup?op=get&search=${email}`); await ctx.render('verify-success', {email, link}); } - + + /** + * Check public key's signatures via http GET + * @param {Object} ctx The koa request/response context + */ + async checkSignatures(ctx) { + const q = {keyId: ctx.query.keyId, nonce: ctx.query.nonce}; + if (!util.isKeyId(q.keyId) || !util.isString(q.nonce)) { + ctx.throw(400, 'Invalid request!'); + } + + const sigs = await this._publicKey.getPendingSignatures(q, ctx); + // create link for confirmation + const link = util.url(util.origin(ctx), `/api/v1/key`); + await ctx.render('verify-certs', {keyId: q.keyId, link, nonce: q.nonce, sigs}); + } + + /** + * Confirm public key's signatures via http POST + * @param {Object} ctx The koa request/response context + * @param {Object} json The json content of the request + */ + async confirmSignatures(ctx, json) { + const post = json || await parse.json(ctx, {limit: '1mb'}); + const q = {keyId: post.keyId, nonce: post.nonce, sigs: post.sig}; + const {email} = await this._publicKey.verifySignatures(q, util.origin(ctx), ctx); + // create link for sharing + const link = util.url(util.origin(ctx), `/pks/lookup?op=get&search=${email}`); + ctx.body = `Update successful. You can find your key here.`; + ctx.status = 201; + } + /** * Request public key removal via http DELETE * @param {Object} ctx The koa request/response context diff --git a/src/service/pgp.js b/src/service/pgp.js index 4bdcbe2..0cd7648 100644 --- a/src/service/pgp.js +++ b/src/service/pgp.js @@ -178,8 +178,8 @@ class PGP { /** * Remove signatures from source armored key which are not in compared armored key * @param {String} srcArmored armored key block to be filtered - * @param {String} cmpArmored armored key block to be compare with - * @return {String, newSigs} filterd armored key block, list of new signatures + * @param {String} cmpArmored armored key block to be compared with + * @return {String, newSigs} filtered {armored key block, list of new signatures} */ async filterKeyBySignatures(srcArmored, cmpArmored) { const {keys: [srcKey], err: srcErr} = await openpgp.key.readArmored(srcArmored); @@ -210,7 +210,7 @@ class PGP { // list new signatures let userId = (srcUser.userId) ? srcUser.userId.userid : null; let userAttribute = (srcUser.userAttribute) ? srcUser.userAttribute : null; - newSigs.push({user: {userId: userId, userAttribute: userAttribute}, signature: sourceSig}); + newSigs.push({user: {userId: userId, userAttribute: userAttribute}, signature: Buffer.from(sourceSig.write()).toString('base64')}); // do not add new signatures source.splice(i, 1); } @@ -243,6 +243,42 @@ class PGP { return dstKey.armor(); } + /** + * Add new signature to key + * @param {String} publicKeyArmored source amored key block + * @param {Object} signature signature to add + * @return {String} updated armored key block + */ + async addSignature(publicKeyArmored, {user, signature}) { + const {keys: [key], err: srcErr} = await openpgp.key.readArmored(publicKeyArmored); + const signaturePacket = await this.getSignatureFromBase64(signature); + if (srcErr) { + log.error('pgp', 'Failed to parse source PGP key for update:\n%s', publicKeyArmored, srcErr); + util.throw(500, 'Failed to parse PGP key'); + } + for(const srcUser of key.users) { + if((srcUser.userId && user.userId === srcUser.userId.userid) || + (user.userAttribute && user.userAttribute === srcUser.userAttribute)) { + if(!srcUser.otherCertifications.some(certSig => util.equalsUint8Array(certSig.signature, signaturePacket.signature))) { + srcUser.otherCertifications.push(signaturePacket); + } + } + } + + return key.armor(); + } + + /** + * Get openpgp.packet.Signature object from base64 encoded signature + * @param {String} signature base64 encoded signature + * @return {openpgp.packet.Signature} Signature object + */ + async getSignatureFromBase64(signature) { + const signaturePacket = new openpgp.packet.Signature(); + signaturePacket.read(new Uint8Array(Buffer.from(signature, 'base64'))); + return signaturePacket; + } + /** * Returns primary user and most significant (latest valid) self signature * - if multiple primary users exist, returns the one with the latest self signature diff --git a/src/service/public-key.js b/src/service/public-key.js index 41aecc1..2895e8a 100644 --- a/src/service/public-key.js +++ b/src/service/public-key.js @@ -20,6 +20,7 @@ const config = require('config'); const util = require('./util'); const tpl = require('../email/templates'); +const crypto = require('crypto'); /** * Database documents have the format: @@ -96,13 +97,12 @@ class PublicKey { key.publicKeyArmored = await this._pgp.updateKey(verified.publicKeyArmored, filteredPublicKeyArmored); // store pending signatures in key and generate nounce for confirmation if(newSigs.length) { - await this._formatArrays(newSigs); if(!verified.pendingSignatures) key.pendingSignatures = {sigs: newSigs, nonce: util.random()}; else { key.pendingSignatures = verified.pendingSignatures; key.pendingSignatures.sigs = verified.pendingSignatures.sigs.concat(newSigs.filter(sourceSig => !verified.pendingSignatures.sigs.some(function(pendingSig) { - return pendingSig.signature.signature === sourceSig.signature.signature; + return pendingSig.signature === sourceSig.signature; }))); } } @@ -188,22 +188,6 @@ class PublicKey { return users.find(({email}) => email === user.email); } - /** - * Convert all Uint8Array in every signatures to base64. - * @param {Array} signatures list of signatures to convert - * @return {Promise} - */ - async _formatArrays(signatures) { - signatures.map(function(sig) { - const signature = sig.signature; - const attributes = ['signatureData', 'unhashedSubpackets', 'signedHashValue', 'preferredSymmetricAlgorithms', 'revocationKeyFingerprint', 'preferredHashAlgorithms', 'preferredCompressionAlgorithms', 'keyServerPreferences', 'keyFlags', 'features', 'issuerFingerprint', 'preferredAeadAlgorithms', 'signature']; - for (const attrib of attributes) { - if(signature[attrib] != null) - signature[attrib] = Buffer.from(signature[attrib]).toString('base64'); - } - }); - } - /** * Send verification emails to the public keys user ids for verification. * If a primary email address is provided only one email will be sent. @@ -241,7 +225,7 @@ class PublicKey { } /** - * Send confirmation email to the public keys primary user ids for confirmation + * Send email to the public key's primary user ids for confirmation * of new signatures addition. * @param {Object} key key documents containg all the needed data * @param {Object} origin the server's origin (required for email links) @@ -252,7 +236,7 @@ class PublicKey { if(key.pendingSignatures && key.pendingSignatures.sigs.length){ let primaryUser = await this._pgp.getPrimaryUser(key.publicKeyArmored); const userId = primaryUser.user.userId; - await this._email.send({template: tpl.confirmNewSigs.bind(null, ctx), userId, keyId: key.keyId, data: {name: userId.name, sigsNb: key.pendingSignatures.sigs.length, nonce: key.pendingSignatures.nonce}, origin, publicKeyArmored: key.publicKeyArmored}); + await this._email.send({template: tpl.checkNewSigs.bind(null, ctx), userId, keyId: key.keyId, data: {name: userId.name, sigsNb: key.pendingSignatures.sigs.length, nonce: key.pendingSignatures.nonce}, origin, publicKeyArmored: key.publicKeyArmored}); } } @@ -341,6 +325,46 @@ class PublicKey { }, DB_TYPE); return {email}; } + + /** + * Verify signatures by proving knowledge of the nonce. + * @param {string} keyId Correspronding public key id + * @param {string} nonce The verification nonce proving email address ownership + * @param {Array} sigs The list of signatures to verify + * @param {Object} origin The server's origin (required for email links) + * @param {Object} ctx Context + * @return {Promise} The email that has been verified + */ + async verifySignatures({keyId, nonce, sigs}, origin, ctx) { + // look for verification nonce in database + const query = {keyId, 'pendingSignatures.nonce': nonce}; + const key = await this._mongo.get(query, DB_TYPE); + if (!key) { + util.throw(404, 'Signatures not found on key'); + } + + let publicKeyArmored = key.publicKeyArmored; + + for(const {user, signature} of key.pendingSignatures.sigs) { + // update armored key + let hash = crypto.createHash('md5'); + hash.update(signature, 'base64'); + hash = hash.digest('hex'); + if(sigs.includes(hash)) { + publicKeyArmored = await this._pgp.addSignature(key.publicKeyArmored, {user, signature}); + publicKeyArmored = await this._pgp.updateKey(key.publicKeyArmored, publicKeyArmored); + } + } + + key.pendingSignatures = null; + + await this._mongo.update(query, { + publicKeyArmored, + 'pendingSignatures': null + }, DB_TYPE); + const email = (await this._pgp.getPrimaryUser(publicKeyArmored)).user.userId.email; + return {email}; + } /** * Removes keys with the same email address @@ -423,6 +447,57 @@ class PublicKey { return key; } + /** + * Fetch all pending signatures of a public key from the database. Either the + * key fingerprint, id or the email address muss be provided. + * @param {string} keyId Correspronding public key id + * @param {string} nonce The verification nonce proving legitimity of the request + * @param {Object} ctx Context + * @return {Map} The list of userId and associated signatures + */ + async getPendingSignatures({fingerprint, keyId, email, nonce}, ctx) { + // look for verified key + const userIds = email ? [{email}] : undefined; + const key = await this.getVerified({keyId, fingerprint, userIds}); + if (!key) { + util.throw(404, ctx.__('key_not_found')); + } + if(!key.pendingSignatures) + util.throw(404, "No pending signatures"); + if(key.pendingSignatures.nonce != nonce) + util.throw(403, "Invalid nonce"); + + const signatures = new Map(); + + for(const {user, signature} of key.pendingSignatures.sigs) { + const signedUserID = user.userId; + + let hash = crypto.createHash('md5'); + hash.update(signature, 'base64'); + hash = hash.digest('hex') + + const signaturePacket = await this._pgp.getSignatureFromBase64(signature); + + const fingerprint = Buffer.from(signaturePacket.issuerFingerprint).toString('HEX'); + + const verified = await this.getVerified({fingerprint: fingerprint}); + + const issuerUID = (verified)? await this._pgp.getPrimaryUser(verified.publicKeyArmored): "[unknown identity]"; + + const sig = {issuerFingerprint: fingerprint, + created: signaturePacket.created.toDateString(), + userId: issuerUID, + hash: hash + }; + if(!signatures.has(signedUserID)) { + signatures.set(signedUserID, []); + } + signatures.get(signedUserID).push(sig); + } + + return signatures; + } + /** * Request removal of the public key by flagging all user ids and sending * a verification email to the primary email address. Only one email From 22bf076bffd2c51698e16fa846578d4bda66fdcf Mon Sep 17 00:00:00 2001 From: Simon Vareille Date: Sat, 13 Jun 2020 10:45:29 +0200 Subject: [PATCH 11/12] Fix typo of file name --- src/static/js/{verify-cert.js => verify-certs.js} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/static/js/{verify-cert.js => verify-certs.js} (100%) diff --git a/src/static/js/verify-cert.js b/src/static/js/verify-certs.js similarity index 100% rename from src/static/js/verify-cert.js rename to src/static/js/verify-certs.js From 80721b237e5dbad2d1077e3a2eda96754683c76e Mon Sep 17 00:00:00 2001 From: Simon Vareille Date: Sat, 13 Jun 2020 11:11:25 +0200 Subject: [PATCH 12/12] Fix merge bug --- src/service/pgp.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/service/pgp.js b/src/service/pgp.js index 0ebbf3b..0cd7648 100644 --- a/src/service/pgp.js +++ b/src/service/pgp.js @@ -68,7 +68,7 @@ class PGP { } // check for at least one valid user id - const userIds = await this.parseUserIds(key.users, primaryKey, verifyDate); + const {userIds, status} = await this.parseUserIds(key.users, primaryKey, verifyDate); if (!userIds.length) { if (status == 1) { util.throw(400, 'Invalid PGP key: no user ID comes from a valid organisation'); @@ -122,7 +122,7 @@ class PGP { /** * Parse an array of user ids and verify signatures - * @param {Array} users A list of openpgp.js user objects + * @param {Array} users A list of openpgp.js user objects * @param {Object} primaryKey The primary key packet of the key * @param {Date} verifyDate Verify user IDs at this point in time * @return {Array, integer} An array of user id objects and a satus indicator