Commit 9120ef6e authored by Cédric Moreau's avatar Cédric Moreau

Add WoT stability checking before accepting a block with newcomers

parent 0ed0158d
......@@ -197,6 +197,18 @@ function KeyHelper (packetList) {
return certifs;
};
this.getCertificationsFromSignatory = function (newcomer){
var primaryUser = key.getPrimaryUser();
var certifs = new PacketList();
if (primaryUser) {
(primaryUser.user.otherCertifications || []).forEach(function(oCert){
if (newcomer.match(new RegExp(oCert.issuerKeyId.toHex().toUpperCase() + '$')))
certifs.push(oCert);
});
}
return certifs;
};
// Give base64 encoded signing subkey packets (subkey + binding)
this.getBase64subkeys = function (){
var bSubkeys = [];
......
......@@ -55,6 +55,10 @@ function GenericParser (captures, multipleLinesFields, rawerFunc, onError) {
error = 'Document has unkown fields or wrong line ending format';
if (error) {
console.log(error);
// console.log('-----------------');
// console.log('Written:', { str: str });
// console.log('Extract:', { raw: raw });
// console.log('-----------------');
}
}
if (typeof done == 'function')
......
......@@ -120,7 +120,7 @@ function extractKeyChanges(raw) {
if (line.match(/^#####----(F|N|U|L|B):[A-Z0-9]{40}----#####$/)) {
// New key block
if (currentKC)
rawKeychanges.push(currentKC);
rawKeychanges.push(unix2dos(currentKC));
currentKC = line + '\n';
} else {
// Adding to current
......@@ -128,7 +128,7 @@ function extractKeyChanges(raw) {
}
});
if (currentKC)
rawKeychanges.push(currentKC);
rawKeychanges.push(unix2dos(currentKC));
rawKeychanges.forEach(function(kc){
var parsers = require('./.');
var obj = parsers.parseKeychange().syncWrite(kc);
......
......@@ -13,9 +13,9 @@ function KeychangeParser (onError) {
var captures = [
{prop: "type", regexp: /#####----([FNULB]):[A-Z0-9]{40}----#####/},
{prop: "fingerprint", regexp: /#####----[FNULB]:([A-Z0-9]{40})----#####/},
{prop: "keypackets", regexp: /KeyPackets:\n([\s\S]*?)[a-zA-Z]+:/, parser: extractBase64Lines},
{prop: "certpackets", regexp: /CertificationPackets:\n([\s\S]*?)[a-zA-Z]+:/, parser: extractBase64Lines},
{prop: "membership", regexp: /Membership:\n([\s\S]*)/, parser: extractMembership},
{prop: "keypackets", regexp: /KeyPackets:\n([\s\S]*)/, parser: extractBase64Lines},
{prop: "certpackets", regexp: /CertificationPackets:\n([\s\S]*)/, parser: extractBase64Lines},
{prop: "membership", regexp: /Membership:\n([\s\S]*)/, parser: extractMembership},
];
var multilineFields = [];
GenericParser.call(this, captures, multilineFields, rawer.getKeychange, onError);
......@@ -50,9 +50,12 @@ function extractBase64Lines(raw) {
var validLines = "";
var splits = raw.split(/\n/);
var lines = splits.slice(0, splits.length - 1);
var lineOfBlock = true;
lines.forEach(function(line){
if (line.match(/^[A-Za-z0-9\/+=]{1,64}$/)) {
if (lineOfBlock && line.match(/^[A-Za-z0-9\/+=]{1,64}$/)) {
validLines += line + '\n';
} else {
lineOfBlock = false;
}
});
return validLines;
......
......@@ -13,6 +13,7 @@ var KeySchema = new Schema({
distanced: [String], // Array of distanced keys fingerprints
certifs: [String], // Array of md5 hashes of packets to integrate
subkeys: [String], // Array of md5 hashes of packets to integrate
signatories: [String], // Array of md5 hashes of packets to integrate
created: { type: Date, default: Date.now },
updated: { type: Date, default: Date.now }
});
......@@ -118,6 +119,11 @@ KeySchema.statics.getMembers = function(done){
Key.find({ member: true }, done);
};
KeySchema.statics.findWhereSignatory = function(signatory, done){
var Key = this.model('Key');
Key.find({ signatories: new RegExp(signatory.substring(24) + '$') }, done);
};
KeySchema.statics.addMember = function(fingerprint, done){
var Key = this.model('Key');
Key.update({ fingerprint: fingerprint }, { member: true }, function (err) {
......
......@@ -43,14 +43,10 @@ LinkSchema.statics.unobsoletesAllLinks = function (done) {
/**
* Mark as obsolete the links with an age equal to or below a given date
**/
LinkSchema.statics.isStillOver3Steps = function (keyToKick, newLinks, done) {
LinkSchema.statics.isStillOver3Steps = function (fpr, ofMembers, newLinks, done) {
var Link = this.model('Link');
var fpr = keyToKick.fingerprint;
var newCertifiers = newLinks[fpr];
var remainingKeys = [];
keyToKick.distanced.forEach(function(m){
remainingKeys.push(m);
});
var newCertifiers = newLinks[fpr] || [];
var remainingKeys = ofMembers.slice();
// Without self
remainingKeys = _(remainingKeys).difference([fpr]);
var dist1Links = [];
......@@ -66,7 +62,7 @@ LinkSchema.statics.isStillOver3Steps = function (keyToKick, newLinks, done) {
Link.find({ target: fpr, obsolete: false }, function (err, links) {
dist1Links = [];
links.forEach(function(lnk){
dist1Links.push(lnk.fingerprint);
dist1Links.push(lnk.source);
});
// Add new certifiers as distance 1 links
dist1Links = _(dist1Links.concat(newCertifiers)).uniq();
......@@ -82,6 +78,12 @@ LinkSchema.statics.isStillOver3Steps = function (keyToKick, newLinks, done) {
async.forEachSeries(remainingKeys, function(member, callback){
// Exists distance 1 link?
async.detect(dist1Links, function (dist1member, callbackDist1) {
// Look in newLinks
var signatories = newLinks[dist1member];
if (~signatories.indexOf(member)) {
callbackDist1(true);
return;
}
// dist1member signed 'fpr', so here we look for (member => dist1member => fpr sigchain)
Link.find({ source: member, target: dist1member, obsolete: false }, function (err, links) {
if (links && links.length > 0) {
......@@ -112,19 +114,34 @@ LinkSchema.statics.isStillOver3Steps = function (keyToKick, newLinks, done) {
async.waterfall([
function (next){
// Step 1. Detect distance 1 members from current member (potential dist 2 from 'fpr')
// Look in database
Link.find({ source: member, obsolete: false }, function (err, links) {
dist2Links = [];
links.forEach(function(lnk){
dist2Links.push(lnk.fingerprint);
dist2Links.push(lnk.source);
});
next(err);
});
// Look in newLinks
_(newLinks).keys().forEach(function(signed){
newLinks[signed].forEach(function(signatories){
if (~signatories.indexOf(member)) {
dist2links.push(signed);
}
});
});
},
function (next){
// Step 2. Detect links between distance 2 & distance 1 members
async.detect(dist2Links, function (dist2member, callbackDist2) {
// Exists distance 1 link?
async.detect(dist1Links, function (dist1member, callbackDist1) {
// Look in newLinks
var signatories = newLinks[dist1member];
if (~signatories.indexOf(dist2member)) {
callbackDist1(true);
return;
}
// dist1member signed 'fpr', so here we look for (member => dist1member => fpr sigchain)
Link.find({ source: dist2member, target: dist1member, obsolete: false }, function (err, links) {
if (links && links.length > 0) {
......
......@@ -158,6 +158,15 @@ function KeyService (conn, conf, PublicKeyService) {
},
function (theNewLinks, next) {
newLinks = theNewLinks;
_(newLinks).keys().forEach(function(target){
newLinks[target].forEach(function(source){
logger.debug('Sig %s --> %s', source, target);
});
});
// Check that new links won't kick other members (existing or incoming)
checkWoTStability(block, newLinks, next);
},
function (next) {
// Check that to be kicked members are kicked
checkKicked(block, newLinks, next);
},
......@@ -190,7 +199,7 @@ function KeyService (conn, conf, PublicKeyService) {
callback('Root block must contain only FOUNDER keychanges');
return;
}
checkKeychange(block, kc, callback);
checkKeychange(block, kc, {}, callback);
}, done);
}
......@@ -204,7 +213,7 @@ function KeyService (conn, conf, PublicKeyService) {
async.waterfall([
function (next){
// Check keychange (certifications verification notably)
checkKeychange(block, kc, next);
checkKeychange(block, kc, {}, next);
},
function (next){
// Memorize new links from signatures
......@@ -219,32 +228,44 @@ function KeyService (conn, conf, PublicKeyService) {
function checkNormalBlockKeychanges(block, done) {
var newLinks = {};
async.forEach(block.keysChanges, function(kc, callback){
if (kc.type == 'F') {
callback('FOUNDER type is reserved for root keyblock');
return;
}
if (kc.type != 'U' && kc.type != 'N') {
callback('Only NEWCOMER & UPDATE blocks are managed for now');
return;
}
async.waterfall([
function (next){
// Check keychange (certifications verification notably)
checkKeychange(block, kc, next);
},
function (next){
// Memorize new links from signatures
newLinks[kc.fingerprint] = kc.certifiers;
next();
},
], callback);
var newKeys = {};
async.forEachSeries(['N', 'U'], function(currentType, packetTypeDone) {
async.forEach(block.keysChanges, function(kc, callback){
if (kc.type == 'F') {
callback('FOUNDER type is reserved for root keyblock');
return;
}
if (kc.type != 'U' && kc.type != 'N') {
callback('Only NEWCOMER & UPDATE blocks are managed for now');
return;
}
// Doing only one type at a time
if (kc.type != currentType) {
callback();
return;
}
async.waterfall([
function (next){
// Check keychange (certifications verification notably)
checkKeychange(block, kc, newKeys, next);
},
function (next){
// Memorize new links from signatures
newLinks[kc.fingerprint] = kc.certifiers;
if (kc.type == 'N') {
var key = keyhelper.fromEncodedPackets(kc.keypackets);
newKeys[kc.fingerprint] = key;
}
next();
},
], callback);
}, packetTypeDone);
}, function (err) {
done(err, newLinks);
});
}
function checkKeychange (block, kc, done) {
function checkKeychange (block, kc, newKeys, done) {
try {
if (kc.type == 'F') {
......@@ -344,7 +365,7 @@ function KeyService (conn, conf, PublicKeyService) {
kc.certifiers = [];
async.waterfall([
function (next){
checkCertificationOfKey(certif, kc.fingerprint, next);
checkCertificationOfKey(certif, kc.fingerprint, newKeys, next);
},
function (certifier, next){
// Add certifier FPR in memory
......@@ -407,7 +428,7 @@ function KeyService (conn, conf, PublicKeyService) {
kc.certifiers = [];
async.waterfall([
function (next){
checkCertificationOfKey(certif, kc.fingerprint, next);
checkCertificationOfKey(certif, kc.fingerprint, newKeys, next);
},
function (certifier, next){
// Add certifier FPR in memory
......@@ -434,6 +455,75 @@ function KeyService (conn, conf, PublicKeyService) {
}
}
function checkWoTStability (block, newLinks, done) {
if (block.number == 0) {
// block#0 is stable by definition
done();
} else {
// other blocks may introduce unstability with new members
async.waterfall([
function (next) {
Key.getMembers(next);
},
function (members, next) {
var newcomers = [];
block.membersChanges.forEach(function (change) {
if (change.match(/^\+/)) {
var fpr = change.substring(1);
newcomers.push(fpr);
members.push({ fingerprint: fpr });
}
});
async.forEachSeries(newcomers, function (newcomer, newcomerTested) {
async.waterfall([
function (next) {
// Check the newcomer IS RECOGNIZED BY the WoT + other newcomers
// (check we have a path WoT => newcomer)
Link.isOver3StepsOfAMember(newcomer, members, next);
},
function (firstCheck, next) {
if (firstCheck.length > 0) {
// This means either:
// 1. WoT does not recognize the newcomer
// 2. Other newcomers do not recognize the newcomer since we haven't taken them into account
// So, we have to test with newcomers' links too
async.waterfall([
function (next) {
Link.isStillOver3Steps(newcomer, firstCheck, newLinks, next);
},
function (secondCheck) {
if (secondCheck.length > 0)
next('Newcomer ' + newcomer + ' is not recognized by the WoT for this block');
else
next();
}
], next);
} else next();
},
function (next) {
// Also check that the newcomer RECOGNIZES the WoT + other newcomers
// (check we have a path newcomer => WoT)
async.forEachSeries(members, function (member, memberRecognized) {
async.waterfall([
function (next) {
Link.isStillOver3Steps(member.fingerprint, [newcomer], newLinks, next);
},
function (distances, next) {
if (distances.length > 0)
next('Newcomer ' + newcomer + ' cannot recognize member ' + member.fingerprint + ': no path found or too much distance');
else
next();
}
], memberRecognized);
}, next);
}
], newcomerTested);
}, next);
}
], done);
}
}
function checkProofOfWork (block, done) {
var powRegexp = new RegExp('^0{' + MINIMUM_ZERO_START + '}');
if (!block.hash.match(powRegexp))
......@@ -452,7 +542,11 @@ function KeyService (conn, conf, PublicKeyService) {
async.forEach(keys, function(key, callback){
async.waterfall([
function (next){
Link.isStillOver3Steps(key, newLinks, next);
var remainingKeys = [];
key.distanced.forEach(function(m){
remainingKeys.push(m);
});
Link.isStillOver3Steps(key.fingerprint, remainingKeys, newLinks, next);
},
function (outdistanced, next) {
var isStill = outdistanced.length > 0;
......@@ -870,7 +964,7 @@ function KeyService (conn, conf, PublicKeyService) {
var packetList = pubkey.getCertificationsFromMD5List(mKey.certifs);
var retainedPackets = new openpgp.packet.List();
async.forEachSeries(packetList, function(certif, callback){
checkCertificationOfKey(certif, mKey.fingerprint, function (err) {
checkCertificationOfKey(certif, mKey.fingerprint, {}, function (err) {
if (!err)
retainedPackets.push(certif);
else
......@@ -901,6 +995,7 @@ function KeyService (conn, conf, PublicKeyService) {
// 1. See available keychanges
var members = [];
var joinData = {};
var updates = {};
var current;
async.waterfall([
function (next) {
......@@ -945,6 +1040,45 @@ function KeyService (conn, conf, PublicKeyService) {
], callback);
}, next);
},
function (next) {
// Look for signatures from newcomers to the WoT
async.forEach(_(joinData).keys(), function(newcomer, searchedSignaturesOfTheWoT){
async.waterfall([
function (next){
Key.findWhereSignatory(newcomer, next);
},
function (keys, next){
async.forEach(keys, function(signedKey, extractedSignatures){
async.waterfall([
function (next){
async.parallel({
trusted: function(callback){
TrustedKey.getTheOne(signedKey.fingerprint, callback);
},
pubkey: function(callback){
PublicKey.getTheOne(signedKey.fingerprint, callback);
},
}, next);
},
function (res, next){
var key = keyhelper.fromArmored(res.pubkey.raw);
var certifs = key.getCertificationsFromSignatory(newcomer);
if (certifs.length > 0) {
updates[res.pubkey.fingerprint] = certifs;
certifs.forEach(function(){
logger.debug('Found WoT certif %s --> %s', newcomer, res.pubkey.fingerprint);
});
}
next();
},
], function () {
extractedSignatures();
});
}, next);
},
], searchedSignaturesOfTheWoT);
}, next);
},
function (next) {
Key.getMembers(next);
},
......@@ -958,12 +1092,12 @@ function KeyService (conn, conf, PublicKeyService) {
members.push(fpr);
});
// Create the block
createNewcomerBlock(current, members, joinData, next);
createNewcomerBlock(current, members, joinData, updates, next);
},
], done);
};
function createNewcomerBlock (current, members, joinData, done) {
function createNewcomerBlock (current, members, joinData, updates, done) {
var block = new KeyBlock();
block.version = 1;
block.currency = current.currency;
......@@ -979,7 +1113,7 @@ function KeyService (conn, conf, PublicKeyService) {
_(joinData).keys().forEach(function(fpr){
block.membersChanges.push('+' + fpr);
});
// Keychanges
// Keychanges - newcomers
block.keysChanges = [];
_(joinData).values().forEach(function(join){
var key = keyhelper.fromArmored(join.pubkey.raw);
......@@ -994,19 +1128,51 @@ function KeyService (conn, conf, PublicKeyService) {
}
});
});
// Keychanges - updates: signatures from newcomers
_(updates).keys().forEach(function(fpr){
block.keysChanges.push({
type: 'U',
fingerprint: fpr,
keypackets: '',
certpackets: base64.encode(updates[fpr].write()),
membership: {}
});
});
done(null, block);
}
function checkCertificationOfKey (certif, certifiedFPR, done) {
function checkCertificationOfKey (certif, certifiedFPR, newKeys, done) {
var found = null;
async.waterfall([
function (next){
var keyID = certif.issuerKeyId.toHex().toUpperCase();
// Check in local newKeys for trusted key (if found, trusted is newcomer here)
_(newKeys).keys().forEach(function(fpr){
if (fpr.match(new RegExp(keyID + '$')))
found = fpr;
});
async.parallel({
pubkeyCertified: function(callback){
PublicKey.getTheOne(certifiedFPR, callback);
if (newKeys[certifiedFPR]) {
// The certified is a newcomer
var key = newKeys[found];
async.waterfall([
function (next){
parsers.parsePubkey(next).asyncWrite(unix2dos(key.getArmored()), next);
},
function (obj, next) {
next(null, new PublicKey(obj));
}
], callback);
}
// The certified is a WoT member
else PublicKey.getTheOne(certifiedFPR, callback);
},
trusted: function(callback){
TrustedKey.getTheOne(keyID, callback);
if (found)
callback(null, { fingerprint: found });
else
TrustedKey.getTheOne(keyID, callback);
}
}, next);
},
......@@ -1018,6 +1184,11 @@ function KeyService (conn, conf, PublicKeyService) {
PublicKey.getTheOne(certifierFPR, callback);
},
isMember: function(callback){
if (found) {
// Is considered a member since valide newcomer
callback(null, res);
return;
}
Key.isMember(certifierFPR, function (err, isMember) {
callback(err || (!isMember && 'Signature from non-member ' + res.trusted.fingerprint), res);
});
......
......@@ -109,10 +109,17 @@ function PublicKeyService (conn, conf, KeyService) {
var recordedSubKeys = _((keyT && keyT.getHashedSubkeyPackets()) || {}).keys();
var availableSubKeys = _(keyN.getHashedSubkeyPackets()).keys();
// Compute new certifications
var hashedCertifs = keyN.getHashedCertifPackets();
var recordedCertifs = _((keyT && keyT.getHashedCertifPackets()) || {}).keys();
var availableCertifs = _(keyN.getHashedCertifPackets()).keys();
var availableCertifs = _(hashedCertifs).keys();
key.subkeys = _(availableSubKeys).difference(recordedSubKeys);
key.certifs = _(availableCertifs).difference(recordedCertifs);
key.signatories = [];
key.certifs.forEach(function(hash){
var certif = keyhelper.toPacketlist(hashedCertifs[hash]);
var issuer = certif[0].issuerKeyId.toHex().toUpperCase();
key.signatories.push(issuer);
});
key.eligible = keyN.hasValidUdid2();
key.save(function (err) {
next(err);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment