From 919a984471b56a71eac205a7506c14ca5212091a Mon Sep 17 00:00:00 2001 From: Simon Vareille Date: Sat, 8 Feb 2020 12:15:36 +0100 Subject: [PATCH 1/2] Add regex for restriction and use. --- src/service/pgp.js | 12 ++++++------ src/service/util.js | 13 +++++++++++++ 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/service/pgp.js b/src/service/pgp.js index 6a8316a..7368b76 100644 --- a/src/service/pgp.js +++ b/src/service/pgp.js @@ -70,7 +70,7 @@ class PGP { 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 Esisar'); + util.throw(400, 'Invalid PGP key: no user ID comes from a valid organisation'); } else { util.throw(400, 'Invalid PGP key: invalid user IDs'); @@ -125,7 +125,7 @@ class PGP { * @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. - * Values of status : 0 if no error, 1 if no address comes from Esisar. + * Values of status : 0 if no error, 1 if no address comes from a specific organisation. */ async parseUserIds(users, primaryKey, verifyDate = new Date()) { if (!users || !users.length) { @@ -133,7 +133,7 @@ class PGP { } // at least one user id must be valid, revoked or expired const result = []; - var isFromEsisar = false; + var isFromOrganisation = false; for (const user of users) { const userStatus = await user.verify(primaryKey, verifyDate); if (userStatus !== openpgp.enums.keyStatus.invalid && user.userId && user.userId.userid) { @@ -147,14 +147,14 @@ class PGP { email: util.normalizeEmail(uid.email), verified: false }); - if(/^([a-z0-9\-.]+)@([a-z0-9.\-]*)esisar\.grenoble-inp\.fr$/.test(util.normalizeEmail(uid.email))) - isFromEsisar = true; + if(util.isFromOrganisation(util.normalizeEmail(uid.email))) + isFromOrganisation = true; } } catch (e) {} } } var status = 0; - if(!isFromEsisar){ + if(!isFromOrganisation){ result.length = 0; status = 1; } diff --git a/src/service/util.js b/src/service/util.js index b3330c5..8ac7076 100644 --- a/src/service/util.js +++ b/src/service/util.js @@ -78,6 +78,19 @@ exports.isEmail = function(data) { return re.test(data); }; +/** + * Checks for a valid specific organisation email address. + * @param {string} data The email address + * @return {boolean} Wether the email address comes from organisation + */ +exports.isFromOrganisation = function(data) { + if (!this.isString(data)) { + return false; + } + const re = /^([a-z0-9\-.]+)@([a-z0-9.\-]*)esisar\.grenoble-inp\.fr$/; + return re.test(data); +}; + /** * Normalize email address to lowercase. * @param {string} email The email address From d0083a4f5746a4e033157687807b63ed13703129 Mon Sep 17 00:00:00 2001 From: Simon Vareille Date: Sat, 8 Feb 2020 14:16:28 +0100 Subject: [PATCH 2/2] Add validation of Esisar user ids first. --- src/route/rest.js | 2 +- src/service/public-key.js | 69 +++++++++++++++++++++++++++++++++++---- 2 files changed, 64 insertions(+), 7 deletions(-) diff --git a/src/route/rest.js b/src/route/rest.js index 9ae6faf..04f51ed 100644 --- a/src/route/rest.js +++ b/src/route/rest.js @@ -74,7 +74,7 @@ class REST { if (!util.isKeyId(q.keyId) || !util.isString(q.nonce)) { ctx.throw(400, 'Invalid request!'); } - const {email} = await this._publicKey.verify(q); + const {email} = await this._publicKey.verify(q, util.origin(ctx), ctx); // create link for sharing const link = util.url(util.origin(ctx), `/pks/lookup?op=get&search=${email}`); await ctx.render('verify-success', {email, link}); diff --git a/src/service/public-key.js b/src/service/public-key.js index 36efcde..de39e10 100644 --- a/src/service/public-key.js +++ b/src/service/public-key.js @@ -91,6 +91,10 @@ class PublicKey { 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); + // send mails to verify all user ids + await this._sendVerifyEmail(key, origin, ctx); + // store key in database + await this._persistKey(key); } else { key.userIds = key.userIds.filter(userId => userId.status === KEY_STATUS_VALID); if (!key.userIds.length) { @@ -99,11 +103,11 @@ 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); } - // send mails to verify user ids - await this._sendVerifyEmail(key, origin, ctx); - // store key in database - await this._persistKey(key); } /** @@ -175,6 +179,24 @@ class PublicKey { } } } + + /** + * Send verification emails to the organisation's public keys user ids for verification. + * If a primary email address is provided only one email will be sent. + * @param {Array} userIds user id documents containg the verification nonces + * @param {Object} origin the server's origin (required for email links) + * @param {Object} ctx Context + * @return {Promise} + */ + async _sendVerifyOrganisationEmail({userIds, keyId}, origin, ctx) { + for (const userId of userIds) { + 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}); + } + } + } /** * Persist the public key and its user ids in the database. @@ -184,7 +206,7 @@ class PublicKey { async _persistKey(key) { // delete old/unverified key await this._mongo.remove({keyId: key.keyId}, DB_TYPE); - // generate nonces for verification + for (const userId of key.userIds) { // remove status from user delete userId.status; @@ -197,26 +219,61 @@ class PublicKey { util.throw(500, 'Failed to persist key'); } } + + /** + * Persist the public key and its user ids in the database. + * Mark all uids as unprocessed, except the ones with the organisation email. + * @param {Object} key public key parameters + * @return {Promise} + */ + async _persistKeyOrganisation(key) { + // delete old/unverified key + await this._mongo.remove({keyId: key.keyId}, DB_TYPE); + + for (const userId of key.userIds) { + if(util.isFromOrganisation(userId.email)) + { + // remove status from user + delete userId.status; + // remove notify flag from user + delete userId.notify; + } + } + // persist new key + const r = await this._mongo.create(key, DB_TYPE); + if (r.insertedCount !== 1) { + util.throw(500, 'Failed to persist key'); + } + } /** * Verify a user id by proving knowledge of the nonce. * @param {string} keyId Correspronding public key id * @param {string} nonce The verification nonce proving email address ownership + * @param {Object} origin the server's origin (required for email links) + * @param {Object} ctx Context * @return {Promise} The email that has been verified */ - async verify({keyId, nonce}) { + async verify({keyId, nonce}, origin, ctx) { // look for verification nonce in database const query = {keyId, 'userIds.nonce': nonce}; const key = await this._mongo.get(query, DB_TYPE); if (!key) { util.throw(404, 'User ID not found'); } + + // send mails to verify all unnotified user ids + await this._sendVerifyEmail(key, origin, ctx); + // store key in database + await this._persistKey(key); + await this._removeKeysWithSameEmail(key, nonce); let {publicKeyArmored, email} = key.userIds.find(userId => userId.nonce === nonce); // update armored key if (key.publicKeyArmored) { publicKeyArmored = await this._pgp.updateKey(key.publicKeyArmored, publicKeyArmored); } + // flag the user id as verified await this._mongo.update(query, { publicKeyArmored,