#!/usr/bin/env python """ Scyther : An automatic verifier for security protocols. Copyright (C) 2007 Cas Cremers This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. """ """ Example script to show how to perform large-scale tests using the Scyther Python API (contained in the Scyther subdirectory) In this example, multi-protocol attack analysis is performed on a small test set. Author: Cas Cremers """ from Scyther import Scyther from optparse import OptionParser, SUPPRESS_HELP import time import os.path try: from progressbar import * PROGRESSBAR = True except ImportError: from progressbarDummy import * PROGRESSBAR = False print """ Missing the progressbar library. It can be downloaded from: http://code.google.com/p/python-progressbar/ """ FOUND = [] OPTS = None ARGS = None #--------------------------------------------------------------------------- def parseArgs(): usage = "usage: %s [options] [inputfile]" % sys.argv[0] description = "test-mpa.py is a test script to help with multi-protocol analysis." parser = OptionParser(usage=usage,description=description) # command parser.add_option("-t","--typed",dest="types",default=[0],action="store_const",const=[0], help="Verify protocols with respect to a typed model (-m 0)") parser.add_option("-b","--basic-types",dest="types",default=[1],action="store_const",const=[1], help="Verify protocols with respect to basic type flaws only (-m 1)") parser.add_option("-u","--untyped",dest="types",default=[2],action="store_const",const=[2], help="Verify protocols with respect to an untyped model (-m 2)") parser.add_option("-T","--all-types",dest="types",default=[0],action="store_const",const=[0,1,2], help="Verify protocols with respect to all matching types") parser.add_option("-m","--max-protocols",type="int",dest="maxprotocols",default=3, help="Define maximum number of protocols in a multi-protocol attack.") parser.add_option("-U","--init-unique",dest="initunique",default=False,action="store_true", help="Use Scythers --init-unique switch to filter out initiators talking to themselves.") parser.add_option("-E","--extravert",dest="extravert",default=False,action="store_true", help="Use Scythers --extravert switch to filter out agents talking to themselves.") parser.add_option("","--self-communication",dest="selfcommunication",default=False,action="store_true", help="Explore self-communication effects.") parser.add_option("-D","--debug",dest="debug",default=False,action="store_true", help="Enable debugging features.") parser.add_option("-p","--plain",dest="plain",default=False,action="store_true", help="Ensure plain output, e.g., no progress bars.") return parser.parse_args() #--------------------------------------------------------------------------- class Attack(object): def __init__(self,claim,mpalist): self.claim = claim self.mpalist = mpalist def protocol(self): return self.claim.protocol def mpashort(self): s = [] for fn in self.mpalist: ptn = os.path.normpath(fn) (head,tail) = os.path.split(ptn) s.append(tail) return s def __str__(self): s = "(%s,%s)" % (self.claim.id, self.mpashort()) return s def fullstr(self): s = "%s,%s" % (self.claim.id, self.mpalist) return s def __cmp__(self,other): s1 = self.fullstr() s2 = other.fullstr() if (s1 == s2): return 0 else: if s1 < s2: return -1 else: return 1 #--------------------------------------------------------------------------- def MyScyther(protocollist,filt=None,options=None): """ Evaluate the composition of the protocols in protocollist. If there is a filter, i.e. "ns3,I1" then only this specific claim will be evaluated. """ s = Scyther.Scyther() if (options == None) or (options == ""): # untyped matching s.options = "--match=2" else: s.options = options.strip() # Some cleanup of string (idiot's normalization) for protocol in protocollist: s.addFile(protocol) s.verifyOne(filt) return s def getCorrectIsolatedClaims(protocolset,options=None): """ Given a set of protocols, determine the correct claims when run in isolation. Returns a tuple, consisting of - a list of compiling protocols - a list of tuples (protocol,claimid) wich denote correct claims """ correctclaims = [] goodprotocols = [] if not OPTS.plain: widgets = ['Scanning for claims that are correct in isolation: ', Percentage(), ' ', Bar(marker='#',left='[',right=']') ] pbar = ProgressBar(widgets=widgets, maxval=len(protocolset)) pbar.start() count = 0 cpcount = 0 for protocol in protocolset: # verify protocol in isolation s = MyScyther([protocol],options=options) # investigate the results goodprotocols.append(protocol) allfalse = True for claim in s.claims: if claim.okay: correctclaims.append((protocol,claim.id)) allfalse = False count += 1 if not allfalse: cpcount += 1 if not OPTS.plain: pbar.update(count) if not OPTS.plain: pbar.finish() return (goodprotocols,correctclaims,cpcount) def verifyMPAlist(mpalist,claimid,options=None): """ Verify the existence of an attack in this context If an attack is found, we return False, otherwise True. This is needed for the iteration later. """ global OPTS, ARGS if OPTS.debug: print time.asctime(), mpalist, claimid, options s = MyScyther(mpalist,claimid,options) claim = s.getClaim(claimid) if claim: if not claim.okay: global FOUND # This is an MPA attack! if OPTS.debug: print "I've found a multi-protocol attack on claim %s in the context %s." % (claimid,str(mpalist)) FOUND.append(Attack(claim,mpalist)) return False else: return True def constructMPAlist(protocolset,claimid,mpalist,length,start,callback,options=None): """ Append a list of parallel protocols, without duplicates, such that the added part is lexicographically ordered (from index 'start' in the protocol list) For each possible list, the function callback is called. If the callback returns true, iteration proceeds (returning true in the end), otherwise it aborts and returns false. """ if len(mpalist) < length: # list is not long enough yet for pn in range(start,len(protocolset)): p = protocolset[pn] if p not in mpalist: if not constructMPAlist(protocolset,claimid,mpalist + [p],length,pn+1,callback,options=options): return False return True else: # list is long enough: callback return callback(mpalist,claimid,options) def findMPA(protocolset,protocol,claimid,maxcount=3,options=None): """ The protocol claim is assumed to be correct. When does it break? """ # First we examine 2-protocol attacks, and then increase the # number of parallel protocols if we don't find any attacks on the # claim. count = 2 if len(protocolset) < maxcount: # we cannot have more protocols in parallel than there are # protocols. maxcount = len(protocolset) # the actual incremental search loop while count <= maxcount: constructMPAlist(protocolset,claimid,[protocol],count,0,verifyMPAlist,options) count += 1 return None def findAllMPA(protocolset,maxcount=3,options="",mpaoptions=""): """ Given a set of protocols, find multi-protocol attacks """ global FOUND FOUND = [] # Find all correct claims in each protocol (protocolset,correct,cpcount) = getCorrectIsolatedClaims(protocolset,options) print "Investigating %i correct claims in %i protocols." % (len(correct), cpcount) # For all these claims... if not OPTS.plain: widgets = ['Scanning for MPA attacks: ', Percentage(), ' ', Bar(marker='#',left='[',right=']') ] pbar = ProgressBar(widgets=widgets, maxval=len(correct)) pbar.start() count = 0 # Concatenate options but add space iff needed alloptions = (options + " " + mpaoptions).strip() for (protocol,claimid) in correct: # Try to find multi-protocol attacks findMPA(protocolset,protocol,claimid,maxcount,options=alloptions) count += 1 if not OPTS.plain: pbar.update(count) if not OPTS.plain: pbar.finish() """ The below computation assumes protocol names are unique to files, but if they are not, some other errors should have been reported by the Scyther backend anyway (conflicting protocol definitions in MPA analysis). """ mpaprots = [] res = [] for att in FOUND: pn = att.protocol() if pn not in mpaprots: mpaprots.append(pn) res.append(att) print "-" * 70 print "Summary:" print print "We scanned %i protocols with options [%s]." % (len(protocolset),options) print "We found %i correct claims." % (len(correct)) print "We then scanned combinations of at most %i protocols with options [%s]." % (maxcount,alloptions) print "We found %i MPA attacks." % (len(FOUND)) print "The attacks involve the claims of %i protocols." % (len(mpaprots)) print "-" * 70 print return res def showDiff(reslist): """ Show difference between (attacklist,descr) tuples in list """ for i in range(0,len(reslist)-1): (al1,descr1) = reslist[i] (al2,descr2) = reslist[i+1] print "-" * 70 print "Comparing the attacks for [%s] with [%s]:" % (descr1,descr2) print len(al1), len(al2) print repr(al1) print repr(al2) for a in al2: if a not in al1: print "New attack: %s" % (a) for a in al1: if a not in al2: print "[Strange] disappeared attack: %s" % (a) print "-" * 70 print def scanTypes(l = [],defopts = "",mpaopts = ""): global OPTS, ARGS reslist = [] if 0 in OPTS.types: # First typed print "Scanning without type flaws" res = findAllMPA(l,maxcount = OPTS.maxprotocols,options = defopts + " --match=0", mpaoptions = mpaopts) reslist.append((res,"no type flaws")) if 1 in OPTS.types: # Basic type flaws print "Scanning for basic type flaws" res = findAllMPA(l,maxcount = OPTS.maxprotocols,options = defopts + " --match=1", mpaoptions = mpaopts) reslist.append((res,"basic type flaws")) if 2 in OPTS.types: # All type flaws print "Scanning for any type flaws" res = findAllMPA(l,maxcount = OPTS.maxprotocols,options = defopts + " --match=2", mpaoptions = mpaopts) reslist.append((res,"type flaws")) if len(reslist) < 1: import sys print "Error: we didn't do anything." sys.exit() return reslist def scanSelfComm(l = [],defopts = "",mpaopts = ""): global OPTS, ARGS reslist = [] print "Scanning without AA-comm." res = findAllMPA(l,maxcount = OPTS.maxprotocols,options = defopts, mpaoptions = mpaopts + " --extravert") reslist.append((res,"Allow Alice-Alice")) print "Scanning for AA-resp only." res = findAllMPA(l,maxcount = OPTS.maxprotocols,options = defopts, mpaoptions = mpaopts + " --init-unique") reslist.append((res,"Allow Alice-Alice")) print "Scanning for AA-comm." res = findAllMPA(l,maxcount = OPTS.maxprotocols,options = defopts, mpaoptions = mpaopts) reslist.append((res,"Allow Alice-Alice")) return reslist def scanSC(l,defopts,mpaopts): if (len(OPTS.types) > 1) and (OPTS.selfcommunication): import sys print "Error: cannot analyze multiple types as well as self-communication at the same time." sys.exit() if OPTS.selfcommunication: reslist = scanSelfComm(l,defopts,mpaopts) else: reslist = scanTypes(l,defopts,mpaopts) if len(reslist) > 1: showDiff(reslist) def bigTest(): """ Perform the tests as reported in the book. """ import os global OPTS, ARGS testpath = "Protocols/MultiProtocolAttacks/" fl = os.listdir(testpath) nl = [] for fn in fl: if fn.endswith(".spdl"): nl.append(fn) # Report list print "Performing multi-protocol analysis for the following protocols:", nl # Prepend again the path l = [] for fn in nl: l.append(testpath+fn) # Initialize mpa options mpaopts = "" ### Simplified test setup #defopts = "--max-runs=3 -T 360" ### Full test setup #defopts = "--max-runs=4 -T 600" ### Full test setup with --init-unique defopts = "--max-runs=4 -T 600" if OPTS.extravert: mpaopts = (mpaopts + " --extravert").strip() if OPTS.initunique: mpaopts = (mpaopts + " --init-unique").strip() scanSC(l,defopts,mpaopts) def simpleTest(): """ Simple test case with a few protocols """ l = ['nsl3-broken.spdl','ns3.spdl','nsl3.spdl'] print "Performing multi-protocol analysis for the following protocols:", l print findAllMPA(l) print print "Analysis complete." def main(): global OPTS, ARGS (OPTS,ARGS) = parseArgs() bigTest() #simpleTest() if __name__ == '__main__': main() # vim: set ts=4 sw=4 et list lcs=tab\:>-: