SEA.certify wire logic + unit tests (#1110)

* SEA.certify wire logic + unit tests

* picking white hair
This commit is contained in:
mimiza 2021-08-10 14:40:46 +07:00 committed by GitHub
parent 3efa02f1a3
commit 333dd745f7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 277 additions and 26 deletions

124
sea.js
View File

@ -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 + "'.") }

View File

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