From 356877b98c7abb10df15fd9c99a8d9c9fa5af202 Mon Sep 17 00:00:00 2001 From: Bob Mottram Date: Sun, 7 Jul 2019 20:25:38 +0100 Subject: [PATCH] Test for strict capabilities enforcement --- capabilities.py | 3 ++ daemon.py | 2 +- inbox.py | 82 +++++++++++++++++-------------------- posts.py | 5 ++- tests.py | 105 +++++++++++++++++++++++++++++++++++++++++++----- 5 files changed, 141 insertions(+), 56 deletions(-) diff --git a/capabilities.py b/capabilities.py index 1f8f58a2..d1f4c3ae 100644 --- a/capabilities.py +++ b/capabilities.py @@ -14,6 +14,9 @@ import commentjson from auth import createPassword def getOcapFilename(baseDir :str,nickname: str,domain: str,actor :str,subdir: str) -> str: + if ':' in domain: + domain=domain.split(':')[0] + if not os.path.isdir(baseDir+'/accounts'): os.mkdir(baseDir+'/accounts') diff --git a/daemon.py b/daemon.py index 15689a45..16a7bb51 100644 --- a/daemon.py +++ b/daemon.py @@ -6,7 +6,7 @@ __maintainer__ = "Bob Mottram" __email__ = "bob@freedombone.net" __status__ = "Production" -from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer +from http.server import BaseHTTPRequestHandler,ThreadingHTTPServer #import socketserver import commentjson import json diff --git a/inbox.py b/inbox.py index 25355acb..253b7ee7 100644 --- a/inbox.py +++ b/inbox.py @@ -179,62 +179,54 @@ def runInboxQueue(baseDir: str,httpPrefix: str,sendThreads: [],postLog: [],cache # check that capabilities are accepted capabilitiesPassed=False - if queueJson['post'].get('capabilities'): - if queueJson['post']['type']!='Accept': - if isinstance(queueJson['post']['capabilities'], dict): + if queueJson['post'].get('capability'): + if isinstance(queueJson['post']['capability'], dict): + if debug: + print('DEBUG: capability is a dictionary when it should be a string') + os.remove(queueFilename) + queue.pop(0) + continue + ocapFilename= \ + getOcapFilename(baseDir, \ + queueJson['nickname'],queueJson['domain'], \ + queueJson['post']['actor'],'accept') + if not os.path.isfile(ocapFilename): + if debug: + print('DEBUG: capabilities for '+ \ + queueJson['post']['actor']+' do not exist') + os.remove(queueFilename) + queue.pop(0) + continue + with open(ocapFilename, 'r') as fp: + oc=commentjson.load(fp) + if not oc.get('id'): if debug: - print('DEBUG: received post capabilities should be a string, not a dict') - pprint(queueJson['post']) + print('DEBUG: capabilities for '+queueJson['post']['actor']+' do not contain an id') os.remove(queueFilename) queue.pop(0) continue - if not queueJson['post'].get('actor'): + if oc['id']!=queueJson['post']['capability']: if debug: - print('DEBUG: post should have an actor') + print('DEBUG: capability id mismatch') os.remove(queueFilename) queue.pop(0) - continue - ocapFilename= \ - getOcapFilename(baseDir, \ - queueJson['nickname'],queueJson['domain'], \ - queueJson['post']['actor'],'accept') - if not os.path.isfile(ocapFilename): + continue + if not oc.get('capability'): if debug: - print('DEBUG: capabilities for '+ \ - queueJson['post']['actor']+' do not exist') + print('DEBUG: missing capability list') os.remove(queueFilename) queue.pop(0) - continue - with open(ocapFilename, 'r') as fp: - oc=commentjson.load(fp) - if not oc.get('id'): - if debug: - print('DEBUG: capabilities for '+queueJson['post']['actor']+' do not contain an id') - os.remove(queueFilename) - queue.pop(0) - continue - if oc['id']!=queueJson['post']['capabilities']: - if debug: - print('DEBUG: capabilities id mismatch') - os.remove(queueFilename) - queue.pop(0) - continue - if not queueJson['post']['capabilities'].get('capability'): - if debug: - print('DEBUG: missing capability list') - os.remove(queueFilename) - queue.pop(0) - continue - if 'inbox:write' not in queueJson['post']['capabilities']['capability']: - if debug: - print('DEBUG: insufficient capabilities to write to inbox from '+ \ - queueJson['post']['actor']) - os.remove(queueFilename) - queue.pop(0) - continue + continue + if 'inbox:write' not in oc['capability']: if debug: - print('DEBUG: object capabilities check success') - capabilitiesPassed=True + print('DEBUG: insufficient capabilities to write to inbox from '+ \ + queueJson['post']['actor']) + os.remove(queueFilename) + queue.pop(0) + continue + if debug: + print('DEBUG: object capabilities check success') + capabilitiesPassed=True if ocapAlways and not capabilitiesPassed: # Allow follow types through diff --git a/posts.py b/posts.py index f135080f..190b72f4 100644 --- a/posts.py +++ b/posts.py @@ -351,12 +351,15 @@ def createPostBase(baseDir: str,nickname: str, domain: str, port: int, \ # if capabilities have been granted for this actor # then get the corresponding id capabilityId=None - ocapFilename= getOcapFilename(baseDir,nickname,domain,actorUrl,'granted') + ocapFilename= getOcapFilename(baseDir,nickname,domain,toUrl,'granted') + #print('ocapFilename: '+ocapFilename) if os.path.isfile(ocapFilename): with open(ocapFilename, 'r') as fp: oc=commentjson.load(fp) if oc.get('id'): capabilityId=oc['id'] + #else: + # print('ocapFilename: '+ocapFilename+' not found') newPost = { 'id': newPostId+'/activity', diff --git a/tests.py b/tests.py index 432176a8..a521ca16 100644 --- a/tests.py +++ b/tests.py @@ -41,6 +41,7 @@ from auth import storeBasicCredentials testServerAliceRunning = False testServerBobRunning = False +testServerEveRunning = False def testHttpsigBase(withDigest): print('testHttpsig(' + str(withDigest) + ')') @@ -160,6 +161,25 @@ def createServerBob(path: str,domain: str,port: int,federationList: [],ocapGrant print('Server running: Bob') runDaemon(path,domain,port,httpPrefix,federationList,ocapAlways,ocapGranted,useTor,True) +def createServerEve(path: str,domain: str,port: int,federationList: [],ocapGranted: {},hasFollows: bool,hasPosts :bool,ocapAlways :bool): + print('Creating test server: Eve on port '+str(port)) + if os.path.isdir(path): + shutil.rmtree(path) + os.mkdir(path) + os.chdir(path) + nickname='eve' + httpPrefix='http' + useTor=False + clientToServer=False + password='evepass' + privateKeyPem,publicKeyPem,person,wfEndpoint=createPerson(path,nickname,domain,port,httpPrefix,True,password) + deleteAllPosts(path,nickname,domain,'inbox') + deleteAllPosts(path,nickname,domain,'outbox') + global testServerEveRunning + testServerEveRunning = True + print('Server running: Eve') + runDaemon(path,domain,port,httpPrefix,federationList,ocapAlways,ocapGranted,useTor,True) + def testPostMessageBetweenServers(): print('Testing sending message from one server to the inbox of another') @@ -250,12 +270,14 @@ def testFollowBetweenServers(): global testServerAliceRunning global testServerBobRunning + global testServerEveRunning testServerAliceRunning = False testServerBobRunning = False + testServerEveRunning = False httpPrefix='http' useTor=False - federationList=['127.0.0.42','127.0.0.64'] + federationList=[] ocapGranted={} baseDir=os.getcwd() @@ -276,20 +298,35 @@ def testFollowBetweenServers(): bobPort=61936 thrBob = threadWithTrace(target=createServerBob,args=(bobDir,bobDomain,bobPort,federationList,ocapGranted,False,False,ocapAlways),daemon=True) + eveDir=baseDir+'/.tests/eve' + eveDomain='127.0.0.55' + evePort=61937 + thrEve = threadWithTrace(target=createServerEve,args=(eveDir,eveDomain,evePort,federationList,ocapGranted,False,False,False),daemon=True) + thrAlice.start() thrBob.start() + thrEve.start() assert thrAlice.isAlive()==True assert thrBob.isAlive()==True + assert thrEve.isAlive()==True # wait for both servers to be running - while not (testServerAliceRunning and testServerBobRunning): + ctr=0 + while not (testServerAliceRunning and testServerBobRunning and testServerEveRunning): time.sleep(1) - + ctr+=1 + if ctr>10: + break + print('Alice online: '+str(testServerAliceRunning)) + print('Bob online: '+str(testServerBobRunning)) + print('Eve online: '+str(testServerEveRunning)) + assert ctr<=10 time.sleep(1) # In the beginning all was calm and there were no follows print('Alice sends a follow request to Bob') + print('Both are strictly enforcing object capabilities') os.chdir(aliceDir) sessionAlice = createSession(aliceDomain,alicePort,useTor) inReplyTo=None @@ -317,11 +354,61 @@ def testFollowBetweenServers(): for t in range(10): if os.path.isfile(bobDir+'/accounts/bob@'+bobDomain+'/followers.txt'): if os.path.isfile(aliceDir+'/accounts/alice@'+aliceDomain+'/following.txt'): - if os.path.isfile(bobDir+'/accounts/bob@'+bobDomain+':'+str(bobPort)+'/ocap/accept/'+httpPrefix+':##'+aliceDomain+':'+str(alicePort)+'#users#alice.json'): - if os.path.isfile(aliceDir+'/accounts/alice@'+aliceDomain+':'+str(alicePort)+'/ocap/granted/'+httpPrefix+':##'+bobDomain+':'+str(bobPort)+'#users#bob.json'): + if os.path.isfile(bobDir+'/accounts/bob@'+bobDomain+'/ocap/accept/'+httpPrefix+':##'+aliceDomain+':'+str(alicePort)+'#users#alice.json'): + if os.path.isfile(aliceDir+'/accounts/alice@'+aliceDomain+'/ocap/granted/'+httpPrefix+':##'+bobDomain+':'+str(bobPort)+'#users#bob.json'): break time.sleep(1) - + + print('\n\nEve tries to send to Bob') + sessionEve = createSession(eveDomain,evePort,useTor) + eveSendThreads = [] + evePostLog = [] + evePersonCache={} + eveCachedWebfingers={} + eveSendThreads=[] + evePostLog=[] + sendResult = sendPost(sessionEve,eveDir,'eve', eveDomain, evePort, 'bob', bobDomain, bobPort, ccUrl, httpPrefix, 'Eve message', followersOnly, saveToFile, clientToServer, federationList, ocapGranted, eveSendThreads, evePostLog, eveCachedWebfingers,evePersonCache,inReplyTo, inReplyToAtomUri, subject) + print('sendResult: '+str(sendResult)) + + queuePath=bobDir+'/accounts/bob@'+bobDomain+'/queue' + inboxPath=bobDir+'/accounts/bob@'+bobDomain+'/inbox' + eveMessageArrived=False + for i in range(5): + time.sleep(1) + if os.path.isdir(inboxPath): + if len([name for name in os.listdir(inboxPath) if os.path.isfile(os.path.join(inboxPath, name))])>1: + eveMessageArrived=True + print('Eve message sent to Bob!') + break + + # capabilities should have prevented delivery + assert eveMessageArrived==False + print('Message from Eve to Bob was correctly rejected by object capabilities') + + + aliceSendThreads = [] + alicePostLog = [] + alicePersonCache={} + aliceCachedWebfingers={} + aliceSendThreads=[] + alicePostLog=[] + sendResult = sendPost(sessionAlice,aliceDir,'alice', aliceDomain, alicePort, 'bob', bobDomain, bobPort, ccUrl, httpPrefix, 'Alice message', followersOnly, saveToFile, clientToServer, federationList, ocapGranted, aliceSendThreads, alicePostLog, aliceCachedWebfingers,alicePersonCache,inReplyTo, inReplyToAtomUri, subject) + print('sendResult: '+str(sendResult)) + + queuePath=bobDir+'/accounts/bob@'+bobDomain+'/queue' + inboxPath=bobDir+'/accounts/bob@'+bobDomain+'/inbox' + aliceMessageArrived=False + for i in range(5): + time.sleep(1) + if os.path.isdir(inboxPath): + if len([name for name in os.listdir(inboxPath) if os.path.isfile(os.path.join(inboxPath, name))])>1: + aliceMessageArrived=True + print('Alice message sent to Bob!') + break + + assert aliceMessageArrived==True + print('Message from Alice to Bob succeeded, since it was granted capabilities') + # stop the servers thrAlice.kill() thrAlice.join() @@ -330,9 +417,9 @@ def testFollowBetweenServers(): thrBob.kill() thrBob.join() assert thrBob.isAlive()==False - - assert os.path.isfile(bobDir+'/accounts/bob@'+bobDomain+':'+str(bobPort)+'/ocap/accept/'+httpPrefix+':##'+aliceDomain+':'+str(alicePort)+'#users#alice.json') - assert os.path.isfile(aliceDir+'/accounts/alice@'+aliceDomain+':'+str(alicePort)+'/ocap/granted/'+httpPrefix+':##'+bobDomain+':'+str(bobPort)+'#users#bob.json') + + assert os.path.isfile(bobDir+'/accounts/bob@'+bobDomain+'/ocap/accept/'+httpPrefix+':##'+aliceDomain+':'+str(alicePort)+'#users#alice.json') + assert os.path.isfile(aliceDir+'/accounts/alice@'+aliceDomain+'/ocap/granted/'+httpPrefix+':##'+bobDomain+':'+str(bobPort)+'#users#bob.json') assert 'alice@'+aliceDomain in open(bobDir+'/accounts/bob@'+bobDomain+'/followers.txt').read() assert 'bob@'+bobDomain in open(aliceDir+'/accounts/alice@'+aliceDomain+'/following.txt').read()