Merge pull request #1033 from mimiza/master

SEA.certify: replace RegEx with RAD/LEX, force path to contain Certificant Pub
This commit is contained in:
Mark Nadal 2020-12-13 12:33:05 -08:00 committed by GitHub
commit c17a14b53a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 92 additions and 101 deletions

76
sea.js
View File

@ -654,46 +654,60 @@
var SEA = USE('./root');
// This is to certify that a group of "certificants" can "put" anything at a group of matched "paths" to the certificate authority's graph
SEA.certify = SEA.certify || (async (certificants, patterns, authority, cb, opt = {}) => { try {
SEA.certify = SEA.certify || (async (certificants, policy = {}, authority, cb, opt = {}) => { try {
/*
IMPORTANT: A Certificate is like a Signature. No one knows who (authority) created/signed a cert until you put it into their graph.
"certificants": A string (~Bobpub) || a pair || an array of pubs/pairs. These people will have the rights.
"patterns": A string (^inbox.*), or an array of strings [^inbox.*, ^secret\-group.*]. These patterns will be used to check against soul+'/'+key
"authority": Key pair or priv of the certificate authority
"cb": A callback function after all things are done
"opt": If opt.expiry (a timestamp) is set, SEA won't sync data after opt.expiry
"certificants": '*' or a String (Bob.pub) || an Object that contains "pub" as a key || an array of [object || string]. These people will have the rights.
"policy": A string ('inbox'), or a RAD/LEX object {'*': 'inbox'}, or an Array of RAD/LEX objects or strings. RAD/LEX object can contain key "?" with indexOf("*") > -1 to force key equals certificant pub. This rule is used to check against soul+'/'+key using Gun.text.match or String.match.
"authority": Key pair or priv of the certificate authority.
"cb": A callback function after all things are done.
"opt": If opt.expiry (a timestamp) is set, SEA won't sync data after opt.expiry. If opt.blacklist is set, SEA will look for blacklist before syncing.
*/
// We need some logic here to verify that all params are valid
console.log('SEA.certify() is an early experimental community supported method that may change API behavior without warning in any future version.')
certificants = (() => {
var data = []
if (certificants) {
if ((typeof certificants === 'string' || Array.isArray(certificants)) && certificants.indexOf('*')) return '*'
if (typeof certificants === 'string') {
data.push(certificants)
return certificants
}
if (Array.isArray(certificants)) {
certificants.map(person => {
if (typeof person ==='string') data.push(person)
else if (typeof person === 'object' && person.pub) data.push(person.pub)
if (certificants.length === 1 && certificants[0]) return typeof certificants[0] === 'object' && certificants[0].pub ? certificants[0].pub : typeof certificants[0] === 'string' ? certificants[0] : null
certificants.map(certificant => {
if (typeof certificant ==='string') data.push(certificant)
else if (typeof certificant === 'object' && certificant.pub) data.push(certificant.pub)
})
}
if (typeof certificants === 'object' && certificants.pub) data.push(certificants.pub)
if (typeof certificants === 'object' && certificants.pub) return certificants.pub
return data.length > 0 ? data : null
}
return data
return null
})()
patterns = patterns ? typeof patterns === 'string' ? [patterns] : Array.isArray(patterns) ? patterns : null : null
if (!certificants) return console.log("No certificant found.")
const expiry = opt.expiry && (typeof opt.expiry === 'number' || typeof opt.expiry === 'string') ? parseFloat(opt.expiry) : null
const readPolicy = (policy || {}).read ? policy.read : null
const writePolicy = (policy || {}).write ? policy.write : typeof policy === 'string' || Array.isArray(policy) || (policy["?"] || policy["#"] || policy["."] || policy["="] || policy["*"] || policy[">"] || policy["<"]) ? policy : null
const readBlacklist = ((opt || {}).blacklist || {}).read && (typeof opt.blacklist.read === 'string' || opt.blacklist.read['#']) ? opt.blacklist.read : null
const writeBlacklist = typeof (opt || {}).blacklist === 'string' || (((opt || {}).blacklist || {}).write || {})['#'] ? opt.blacklist : ((opt || {}).blacklist || {}).write && (typeof opt.blacklist.write === 'string' || opt.blacklist.write['#']) ? opt.blacklist.write : null
if (!readPolicy && !writePolicy) return console.log("No policy found.")
// reserved keys: c, e, r, w, rb, wb
const data = JSON.stringify({
c: certificants,
p: patterns,
...(opt.expiry && typeof opt.expiry === 'number' ? {e: parseFloat(opt.expiry)} : {}), // inject expiry if possible
...(opt.blacklist && typeof opt.blacklist === 'string' ? {b: opt.blacklist} : {}) // inject blacklist if possible
...(expiry ? {e: expiry} : {}), // inject expiry if possible
...(readPolicy ? {r: readPolicy } : {}), // "r" stands for read, which means read permission.
...(writePolicy ? {w: writePolicy} : {}), // "w" stands for write, which means write permission.
...(readBlacklist ? {rb: readBlacklist} : {}), // inject READ blacklist if possible
...(writeBlacklist ? {wb: writeBlacklist} : {}), // inject WRITE blacklist if possible
})
const certificate = await SEA.sign(data, authority, null, {raw:1})
@ -1320,19 +1334,23 @@
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.")
// "data.c" = a list of certificants/certified users, "data.p" = a list of allowed patterns
if (u !== data && data.c && data.p && (data.c.indexOf('*') || data.c.indexOf(certificant))) {
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 + '/' + key
path = path.replace(path.substring(0, path.indexOf('/') + 1), '')
for (p of data.p) {
if (new RegExp(p).test(path)) {
// path is allowed, but is there any blacklist? Check blacklist
if (data.b && typeof data.b === 'string') { // "data.b" = path to the blacklist
let path = soul.indexOf('/') > -1 ? soul.replace(soul.substring(0, soul.indexOf('/') + 1), '') : ''
String.match = String.match || Gun.text.match
const w = typeof data.w === 'object' || typeof data.w === 'string' ? [data.w] : Array.isArray(data.w) ? 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("Key not same as certificant pub.")
// 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 = user.back(-1)
if ('~' !== data.b.slice(0,1)) root = root.get('~'+pub)
root.get(data.b).get(certificant).once(value => {
if (typeof data.wb === 'string' && '~' !== data.wb.slice(0, 1)) root = root.get('~' + pub)
root.get(data.wb).get(certificant).once(value => {
if (value && (value === 1 || value === true)) return no("Certificant blacklisted.")
return cb(data)
})

View File

@ -526,10 +526,11 @@ describe('SEA', function(){
});
});
it('Certify: Alice certs Bob, Bob writes to Alice', function(done){(async function(){
it('Certify: Simple', function(done){(async function(){
var alice = await SEA.pair()
var bob = await SEA.pair()
var cert = await SEA.certify(bob, ["^private.*"], alice)
var dave = await SEA.pair()
var cert = await SEA.certify(bob, {"*": "private"}, alice)
user.auth(bob, () => {
var data = Gun.state.lex()
@ -538,73 +539,39 @@ describe('SEA', function(){
.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()
done()
})
}, { opt: { cert } })
})
}())})
it('Certify: Alice certs Bob, Bob writes to Alice, everyone reads from Alice', function(done){(async function(){
var alice = await SEA.pair()
var bob = await SEA.pair()
var cert = await SEA.certify(bob, ["^private.*"], alice)
user.auth(bob, () => {
var data = Gun.state.lex()
gun.get("~" + alice.pub)
.get("private")
.get("asdf")
.get("qwerty")
.put(data, () => {
user.leave()
gun.get("~" + alice.pub)
.get("private")
.get("asdf")
.get("qwerty").once(_data=>{
expect(_data).to.be(data)
done()
})
}, { opt: { cert } })
})
}())})
it('Certify: Alice certs Bob, Bob writes to Alice, Dave reads from Alice', 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.auth(bob, () => {
var data = Gun.state.lex()
gun.get("~" + alice.pub)
.get("private")
.get("asdf")
.get("qwerty")
.put(data, () => {
user.leave()
user.auth(dave, () => {
// everyone reads
gun.get("~" + alice.pub)
.get("private")
.get("asdf")
.get("qwerty").once(_data=>{
expect(_data).to.be(data)
done()
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)
done()
})
})
})
})
}, { opt: { cert } })
})
}())})
it('Certify: Simple Cert (without Expiry + Blacklist), Bob hacks Alice', function(done){(async function(){
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)
var cert = await SEA.certify(bob, {"*": "private"}, alice)
user.auth(bob, () => {
var data = Gun.state.lex()
@ -622,8 +589,8 @@ describe('SEA', function(){
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, // expires in 100 miliseconds
var cert = await SEA.certify(bob, {"*": "private"}, alice, null, {
expiry: Gun.state() - 100, // expired 100 miliseconds ago
})
user.auth(bob, () => {
@ -639,36 +606,42 @@ describe('SEA', function(){
})
}())})
it('Certify: SIMPLE Blacklist', function(done){(async function(){
it('Certify: Path 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, null, {
expiry: Gun.state() + 5000, // expires in 5 seconds
blacklist: '~'+alice.pub+'/blacklist' // path to blacklist
})
var cert = await SEA.certify(bob, {"*": "private", "?": "*"}, alice)
user.auth(alice, async () => {
await user.get('blacklist').get(bob.pub).put(true)
await user.leave()
user.auth(bob, async () => {
var data = Gun.state.lex()
gun.get("~" + alice.pub)
user.auth(bob, () => {
var data = Gun.state.lex()
gun.get("~" + alice.pub)
.get("private")
.get('wrongway')
.put(data, ack => {
expect(ack.err).to.be.ok()
gun.get("~" + alice.pub)
.get("private")
.get("asdf")
.get("qwerty")
.get(bob.pub)
.get('today')
.put(data, ack => {
expect(ack.err).to.be.ok()
done()
expect(ack.ok).to.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, Alice\'s graph, Dave\'s blacklist, Bob\'s put', function(done){(async function(){
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, {
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
})
@ -678,7 +651,7 @@ describe('SEA', function(){
await user.get('blacklist').put({'#': '~'+dave.pub+'/blacklist'})
await user.leave()
// Dave logins, he add Bob to his blacklist, which is connected to the certificate that Alice issued for Bob
// 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()