From 333dd745f74400426e107dec606b1c3ae9c617ea Mon Sep 17 00:00:00 2001 From: mimiza Date: Tue, 10 Aug 2021 14:40:46 +0700 Subject: [PATCH] SEA.certify wire logic + unit tests (#1110) * SEA.certify wire logic + unit tests * picking white hair --- sea.js | 124 ++++++++++++++++++++++++++------- test/sea/sea.js | 179 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 277 insertions(+), 26 deletions(-) diff --git a/sea.js b/sea.js index b55b38f8..4a366931 100644 --- a/sea.js +++ b/sea.js @@ -1260,7 +1260,7 @@ })(USE, './share'); ;USE(function(module){ - var SEA = USE('./sea'), noop = function(){}, u; + var SEA = USE('./sea'), S = USE('./settings'), noop = function() {}, u; var Gun = (''+u != typeof window)? (window.Gun||{on:noop}) : USE((''+u === typeof MODULE?'.':'')+'./gun', 1); // After we have a GUN extension to make user registration/login easy, we then need to handle everything else. @@ -1291,7 +1291,6 @@ function check(msg){ // REVISE / IMPROVE, NO NEED TO PASS MSG/EVE EACH SUB? var eve = this, at = eve.as, put = msg.put, soul = put['#'], key = put['.'], val = put[':'], state = put['>'], id = msg['#'], tmp; if(!soul || !key){ return } - //console.log('check', put, msg); if((msg._||'').faith && (at.opt||'').faith && 'function' == typeof msg._){ SEA.opt.pack(put, function(raw){ SEA.verify(raw, false, function(data){ // this is synchronous if false @@ -1343,33 +1342,106 @@ if(key === link_is(val)){ return eve.to.next(msg) } // and the ID must be EXACTLY equal to its property no("Alias not same!"); // that way nobody can tamper with the list of public keys. }; - check.pub = function(eve, msg, val, key, soul, at, no, user, pub){ var tmp; // Example: {_:#~asdf, hello:'world'~fdsa}} - if('pub' === key && '~'+pub === soul){ - if(val === pub){ return eve.to.next(msg) } // the account MUST match `pub` property that equals the ID of the public key. - return no("Account not same!"); - } - if((tmp = user.is) && pub === tmp.pub){// && (tmp = msg._.$) && (tmp = tmp._) && tmp !== tmp.root){ - SEA.opt.pack(msg.put, function(raw){ - SEA.sign(raw, (user._).sea, function(data){ - if(u === data){ return no(SEA.err || 'Signature fail.') } - if(tmp = link_is(val)){ (at.sea.own[tmp] = at.sea.own[tmp] || {})[pub] = 1 } - JSON.stringifyAsync({':': tmp = SEA.opt.unpack(data.m), '~': data.s}, function(err,s){ - if(err){ return no(err || "Stringify error.") } - msg.put['='] = tmp; - msg.put[':'] = s; - eve.to.next(msg); + check.pub = async function(eve, msg, val, key, soul, at, no, user, pub){ var tmp // Example: {_:#~asdf, hello:'world'~fdsa}} + const raw = await S.parse(val) || {} + const verify = (certificate, certificant, cb) => { + if (certificate.m && certificate.s && certificant && pub) + // now verify certificate + return SEA.verify(certificate, pub, data => { // check if "pub" (of the graph owner) really issued this cert + if (u !== data && u !== data.e && msg.put['>'] && msg.put['>'] > parseFloat(data.e)) return no("Certificate expired.") // certificate expired + // "data.c" = a list of certificants/certified users + // "data.w" = lex WRITE permission, in the future, there will be "data.r" which means lex READ permission + if (u !== data && data.c && data.w && (data.c === certificant || data.c.indexOf('*' || certificant) > -1)) { + // ok, now "certificant" is in the "certificants" list, but is "path" allowed? Check path + let path = soul.indexOf('/') > -1 ? soul.replace(soul.substring(0, soul.indexOf('/') + 1), '') : '' + String.match = String.match || Gun.text.match + const w = Array.isArray(data.w) ? data.w : typeof data.w === 'object' || typeof data.w === 'string' ? [data.w] : [] + for (const lex of w) { + if ((String.match(path, lex['#']) && String.match(key, lex['.'])) || (!lex['.'] && String.match(path, lex['#'])) || (!lex['#'] && String.match(key, lex['.'])) || String.match((path ? path + '/' + key : key), lex['#'] || lex)) { + // is Certificant forced to present in Path + if (lex['+'] && lex['+'].indexOf('*') > -1 && path && path.indexOf(certificant) == -1 && key.indexOf(certificant) == -1) return no(`Path "${path}" or key "${key}" must contain string "${certificant}".`) + // path is allowed, but is there any WRITE blacklist? Check it out + if (data.wb && (typeof data.wb === 'string' || ((data.wb || {})['#']))) { // "data.wb" = path to the WRITE blacklist + var root = at.$.back(-1) + if (typeof data.wb === 'string' && '~' !== data.wb.slice(0, 1)) root = root.get('~' + pub) + return root.get(data.wb).get(certificant).once(value => { + if (value && (value === 1 || value === true)) return no("Certificant blacklisted.") + return cb(data) + }) + } + return cb(data) + } + } + return no("Certificate verification fail.") + } }) - }, {raw: 1})}); + return + } + + if ('pub' === key && '~' + pub === soul) { + if (val === pub) return eve.to.next(msg) // the account MUST match `pub` property that equals the ID of the public key. + return no("Account not same!") + } + + if ((tmp = user.is) && tmp.pub && !raw['*'] && !raw['+'] && (pub === tmp.pub || (pub !== tmp.pub && ((msg._.msg || {}).opt || {}).cert))){ + SEA.opt.pack(msg.put, packed => { + SEA.sign(packed, (user._).sea, async function(data) { + if (u === data) return no(SEA.err || 'Signature fail.') + msg.put[':'] = {':': tmp = SEA.opt.unpack(data.m), '~': data.s} + msg.put['='] = tmp + + // if writing to own graph, just allow it + if (pub === user.is.pub) { + if (tmp = link_is(val)) (at.sea.own[tmp] = at.sea.own[tmp] || {})[pub] = 1 + JSON.stringifyAsync(msg.put[':'], function(err,s){ + if(err){ return no(err || "Stringify error.") } + msg.put[':'] = s; + return eve.to.next(msg); + }) + return + } + + // if writing to other's graph, check if cert exists then try to inject cert into put, also inject self pub so that everyone can verify the put + if (pub !== user.is.pub && ((msg._.msg || {}).opt || {}).cert) { + const cert = await S.parse(msg._.msg.opt.cert) + // even if cert exists, we must verify it + if (cert && cert.m && cert.s) + verify(cert, user.is.pub, _ => { + msg.put[':']['+'] = cert // '+' is a certificate + msg.put[':']['*'] = user.is.pub // '*' is pub of the user who puts + JSON.stringifyAsync(msg.put[':'], function(err,s){ + if(err){ return no(err || "Stringify error.") } + msg.put[':'] = s; + return eve.to.next(msg); + }) + return + }) + } + }, {raw: 1}) + }) return; } - SEA.opt.pack(msg.put, function(raw){ - SEA.verify(raw, pub, function(data){ var tmp; - data = SEA.opt.unpack(data); - if(u === data){ return no("Unverified data.") } // make sure the signature matches the account it claims to be on. // reject any updates that are signed with a mismatched account. - if((tmp = link_is(data)) && pub === SEA.opt.pub(tmp)){ (at.sea.own[tmp] = at.sea.own[tmp] || {})[pub] = 1 } - msg.put['='] = data; - eve.to.next(msg); - })}); + + SEA.opt.pack(msg.put, packed => { + SEA.verify(packed, raw['*'] || pub, function(data){ var tmp; + data = SEA.opt.unpack(data); + if (u === data) return no("Unverified data.") // make sure the signature matches the account it claims to be on. // reject any updates that are signed with a mismatched account. + if ((tmp = link_is(data)) && pub === SEA.opt.pub(tmp)) (at.sea.own[tmp] = at.sea.own[tmp] || {})[pub] = 1 + + // check if cert ('+') and putter's pub ('*') exist + if (raw['+'] && raw['+']['m'] && raw['+']['s'] && raw['*']) + // now verify certificate + verify(raw['+'], raw['*'], _ => { + msg.put['='] = data; + return eve.to.next(msg); + }) + else { + msg.put['='] = data; + return eve.to.next(msg); + } + }); + }) + return }; check.any = function(eve, msg, val, key, soul, at, no, user){ var tmp, pub; if(at.opt.secure){ return no("Soul missing public key at '" + key + "'.") } diff --git a/test/sea/sea.js b/test/sea/sea.js index 9b4e60fd..85078ba5 100755 --- a/test/sea/sea.js +++ b/test/sea/sea.js @@ -541,6 +541,185 @@ describe('SEA', function(){ gun.user().auth(alice); }); }); + + describe('CERTIFY', function () { + var gun = Gun() + var user = gun.user() + + it('Certify: Simple', function(done){(async function(){ + var alice = await SEA.pair() + var bob = await SEA.pair() + var dave = await SEA.pair() + var cert = await SEA.certify(bob, {"*": "private"}, alice) + + user.leave() + user.auth(bob, () => { + var data = Gun.state().toString(36) + gun.get("~" + alice.pub) + .get("private") + .get("asdf") + .get("qwerty") + .put(data, () => { + // Bob reads + gun.get("~" + alice.pub) + .get("private") + .get("asdf") + .get("qwerty").once(_data=>{ + expect(_data).to.be(data) + user.leave() + // everyone reads + gun.get("~" + alice.pub) + .get("private") + .get("asdf") + .get("qwerty").once(_data=>{ + expect(_data).to.be(data) + user.auth(dave, () => { + // Dave reads + gun.get("~" + alice.pub) + .get("private") + .get("asdf") + .get("qwerty").once(_data=>{ + expect(_data).to.be(data) + user.leave() + done() + }) + }) + }) + }) + }, { opt: { cert } }) + }) + }())}) + + it('Certify: Attack', function(done){(async function(){ + var alice = await SEA.pair() + var bob = await SEA.pair() + var cert = await SEA.certify(bob, {"*": "private"}, alice) + + user.leave() + user.auth(bob, () => { + var data = Gun.state().toString(36) + gun.get("~" + alice.pub) + .get("wrongway") + .get("asdf") + .get("qwerty") + .put(data, ack => { + expect(ack.err).to.be.ok() + user.leave() + done() + }, { opt: { cert } }) + }) + }())}) + + it('Certify: Public inbox', function(done){(async function(){ + var alice = await SEA.pair() + var bob = await SEA.pair() + var cert = await SEA.certify('*', [{"*": "test", "+": "*"}, {"*": "inbox", "+": "*"}], alice) + + user.leave() + user.auth(bob, () => { + var data = Gun.state().toString(36) + gun.get("~" + alice.pub) + .get("inbox") + .get(user.is.pub) + .put(data, ack => { + expect(ack.err).to.not.be.ok() + user.leave() + done() + }, { opt: { cert } }) + }) + }())}) + + it('Certify: Expiry', function(done){(async function(){ + var alice = await SEA.pair() + var bob = await SEA.pair() + var cert = await SEA.certify(bob, {"*": "private"}, alice, null, { + expiry: Gun.state() - 100, // expired 100 miliseconds ago + }) + + user.leave() + user.auth(bob, () => { + var data = Gun.state().toString(36) + gun.get("~" + alice.pub) + .get("private") + .get("asdf") + .get("qwerty") + .put(data, ack => { + expect(ack.err).to.be.ok() + user.leave() + done() + }, { opt: { cert } }) + }) + }())}) + + it('Certify: Path or Key must contain Certificant Pub', function(done){(async function(){ + var alice = await SEA.pair() + var bob = await SEA.pair() + var cert = await SEA.certify(bob, {"*": "private", "+": "*"}, alice) + + user.leave() + user.auth(bob, () => { + var data = Gun.state().toString(36) + gun.get("~" + alice.pub) + .get("private") + .get('wrongway') + .put(data, ack => { + expect(ack.err).to.be.ok() + gun.get("~" + alice.pub) + .get("private") + .get(bob.pub) + .get('today') + .put(data, ack => { + expect(ack.err).to.not.be.ok() + gun.get("~" + alice.pub) + .get("private") + .get(bob.pub) + .get('today') + .once(_data => { + expect(_data).to.be(data) + done() + }) + }, { opt: { cert } }) + }, { opt: { cert } }) + }) + }())}) + + it('Certify: Advanced - Blacklist', function(done){(async function(){ + var alice = await SEA.pair() + var dave = await SEA.pair() + var bob = await SEA.pair() + var cert = await SEA.certify(bob, {"*": "private"}, alice, null, { + expiry: Gun.state() + 5000, // expires in 5 seconds + blacklist: 'blacklist' // path to blacklist in Alice's graph + }) + + // Alice points her blacklist to Dave's graph + user.leave() + user.auth(alice, async () => { + await user.get('blacklist').put({'#': '~'+dave.pub+'/blacklist'}) + await user.leave() + + // Dave logins, he adds Bob to his blacklist, which is connected to the certificate that Alice issued for Bob + user.auth(dave, async () => { + await user.get('blacklist').get(bob.pub).put(true) + await user.leave() + + // Bob logins and tries to hack Alice + user.auth(bob, async () => { + var data = Gun.state().toString(36) + gun.get("~" + alice.pub) + .get("private") + .get("asdf") + .get("qwerty") + .put(data, ack => { + expect(ack.err).to.be.ok() + user.leave() + done() + }, { opt: { cert } }) + }) + }) + }) + }())}) + }); }); })