Restrict addition of signatures

Sending an email to the primary userID to select which signatures to add when new signatures are uploaded.
esisar-restrictions
Simon Vareille 2020-02-10 18:10:10 +01:00
parent d0083a4f57
commit 5a05cecdb8
No known key found for this signature in database
GPG Key ID: 008AE8E706CC19F9
6 changed files with 135 additions and 17 deletions

View File

@ -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"
}

View File

@ -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
});

View File

@ -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};

View File

@ -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

View File

@ -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});
}
}

View File

@ -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