/* * 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. */ /** * *@file arachne.c * * Introduces a method for proofs akin to the Athena modelchecker * http://www.ece.cmu.edu/~dawnsong/athena/ * */ #include #include #include #include #include "mymalloc.h" #include "term.h" #include "termlist.h" #include "role.h" #include "system.h" #include "knowledge.h" #include "compiler.h" #include "states.h" #include "mgu.h" #include "arachne.h" #include "error.h" #include "claim.h" #include "debug.h" #include "binding.h" #include "warshall.h" #include "timer.h" #include "type.h" #include "switches.h" #include "specialterm.h" #include "cost.h" #include "dotout.h" #include "prune_bounds.h" #include "prune_theorems.h" #include "arachne.h" #include "hidelevel.h" #include "depend.h" #include "xmlout.h" #include "heuristic.h" extern int *graph; extern int nodes; extern int graph_uordblks; static System sys; //!< local buffer for the system pointer int attack_length; //!< length of the attack int attack_leastcost; //!< cost of the best attack sofar \sa cost.c Protocol INTRUDER; //!< intruder protocol Role I_M; //!< Initial knowledge role of the intruder Role I_RRS; //!< Encrypt role of the intruder Role I_RRSD; //!< Decrypt role of the intruder int proofDepth; //!< Current depth of the proof int max_encryption_level; //!< Maximum encryption level of any term static int indentDepth; static int prevIndentDepth; static int indentDepthChanges; static FILE *attack_stream; /* * Forward declarations */ int iterate (); /* * Program code */ //! Init Arachne engine void arachneInit (const System mysys) { Roledef rd; void add_event (int event, Term message) { rd = roledefAdd (rd, event, NULL, NULL, NULL, message, NULL); } Role add_role (const char *rolenamestring) { Role r; Term rolename; rolename = makeGlobalConstant (rolenamestring); r = roleCreate (rolename); r->roledef = rd; rd = NULL; r->next = INTRUDER->roles; INTRUDER->roles = r; // compute_role_variables (sys, INTRUDER, r); return r; } sys = mysys; // make sys available for this module as a global /** * Very important: turn role terms that are local to a run, into variables. */ term_rolelocals_are_variables (); /* * Add intruder protocol roles */ INTRUDER = protocolCreate (makeGlobalConstant (" INTRUDER ")); // Initially empty roledef rd = NULL; add_event (SEND, NULL); I_M = add_role ("I_M: Atomic message"); add_event (READ, NULL); add_event (READ, NULL); add_event (SEND, NULL); I_RRS = add_role ("I_E: Encrypt"); add_event (READ, NULL); add_event (READ, NULL); add_event (SEND, NULL); I_RRSD = add_role ("I_D: Decrypt"); sys->num_regular_runs = 0; sys->num_intruder_runs = 0; max_encryption_level = 0; indentDepth = 0; prevIndentDepth = 0; indentDepthChanges = 0; return; } //! Close Arachne engine void arachneDone () { return; } //------------------------------------------------------------------------ // Detail //------------------------------------------------------------------------ //! Just a defined integer for invalid #define INVALID -1 //! can this roledef constitute a read Goal? #define isGoal(rd) (rd->type == READ && !rd->internal) //! is this roledef already bound? #define isBound(rd) (rd->bound) //! Indent prefix print void indentPrefixPrint (const int annotate, const int jumps) { void counterPrint () { statesFormat (sys->current_claim->states); eprintf ("\t"); eprintf ("%i", annotate); eprintf ("\t"); } if (switches.output == ATTACK && globalError == 0) { // Arachne, attack, not an error // We assume that means DOT output eprintf ("// "); counterPrint (); } else { // If it is not to stdout, or it is not an attack... int i; counterPrint (); for (i = 0; i < jumps; i++) { if (i % 3 == 0) eprintf ("|"); else eprintf (" "); eprintf (" "); } } } //! Indent print /** * More subtle than before. Indentlevel changes now cause a counter to be increased, which is printed. Nice to find stuff in attacks. */ void indentPrint () { if (indentDepth != prevIndentDepth) { indentDepthChanges++; while (indentDepth != prevIndentDepth) { if (prevIndentDepth < indentDepth) { indentPrefixPrint (indentDepthChanges, prevIndentDepth); eprintf ("{\n"); prevIndentDepth++; } else { prevIndentDepth--; indentPrefixPrint (indentDepthChanges, prevIndentDepth); eprintf ("}\n"); } } } indentPrefixPrint (indentDepthChanges, indentDepth); } //! Print indented binding void binding_indent_print (const Binding b, const int flag) { indentPrint (); if (flag) eprintf ("!! "); binding_print (b); eprintf ("\n"); } //! Keylevel tester: can this term ever be sent at this keylevel? int isKeylevelRight (Term t, const int kl) { t = deVar (t); if (realTermLeaf (t)) { // Leaf if (isTermVariable (t)) { // Variables are okay return 1; } else { // Constant, does it have a keylevel? int mykl; mykl = TermSymb (t)->keylevel; if (mykl < INT_MAX) { // Sensible keylevel, so it must be possible return (mykl <= kl); } else { // Never sent? // So we can not expect it to come from that return 0; } } } else { // Node if (realTermTuple (t)) { // Tuple return isKeylevelRight (TermOp1 (t), kl) && isKeylevelRight (TermOp2 (t), kl); } else { // Crypt return isKeylevelRight (TermOp1 (t), kl) && isKeylevelRight (TermOp2 (t), kl + 1); } } } //! Keylevel tester: can this term ever be sent at this keylevel? /** * Depends on the keylevel lemma (so this will not be called when those lemmas * are disabled) and the keylevel constructors in symbol.c The idea is that * certain terms will never be sent. */ int isPossiblySent (Term t) { return isKeylevelRight (t, 0); } //! Wrapper for roleInstance /** *@return Returns the run number */ int semiRunCreate (const Protocol p, const Role r) { int run; if (p == INTRUDER) sys->num_intruder_runs++; else sys->num_regular_runs++; #ifdef DEBUG if (DEBUGL (5)) { globalError++; eprintf ("Adding a run %i with semiRunCreate, ", sys->maxruns); termPrint (p->nameterm); eprintf (", "); termPrint (r->nameterm); eprintf ("\n"); globalError--; } #endif roleInstance (sys, p, r, NULL, NULL); run = sys->maxruns - 1; sys->runs[run].height = 0; return run; } //! Wrapper for roleDestroy void semiRunDestroy () { if (sys->maxruns > 0) { Protocol p; p = sys->runs[sys->maxruns - 1].protocol; roleInstanceDestroy (sys); if (p == INTRUDER) sys->num_intruder_runs--; else sys->num_regular_runs--; } } //! Fix the keylevels of any agents /** * We simply extract the agent names from m0 (ugly hack) */ void fixAgentKeylevels (void) { Termlist tl, m0tl; m0tl = knowledgeSet (sys->know); tl = m0tl; while (tl != NULL) { Term t; t = deVar (tl->term); if (realTermLeaf (t)) { { // a real agent type thing if (TermSymb (t)->keylevel == INT_MAX) { // Fix the keylevel TermSymb (t)->keylevel = 0; } } } tl = tl->next; } termlistDelete (m0tl); } //! After a role instance, or an extension of a run, we might need to add some goals /** * From old to new. Sets the new height to new. *@returns The number of goals added (for destructions) */ int add_read_goals (const int run, const int old, const int new) { if (new <= sys->runs[run].height) { return 0; } else { int count; int i; Roledef rd; sys->runs[run].height = new; i = old; rd = eventRoledef (sys, run, i); count = 0; while (i < new && rd != NULL) { if (rd->type == READ) { if (switches.output == PROOF) { if (count == 0) { indentPrint (); eprintf ("Thus, we must also produce "); } else { eprintf (", "); } termPrint (rd->message); } count = count + goal_add (rd->message, run, i, 0); } rd = rd->next; i++; } if ((count > 0) && switches.output == PROOF) { eprintf ("\n"); } return count; } } //! Determine trace length int get_semitrace_length () { int run; int length; run = 0; length = 0; while (run < sys->maxruns) { if (sys->runs[run].protocol != INTRUDER) { // Non-intruder run: count length // Subtract 'firstReal' to ignore chooses. length = length + sys->runs[run].height - sys->runs[run].firstReal; } run++; } return length; } //! Count intruder events int countIntruderActions () { int count; int run; count = 0; run = 0; while (run < sys->maxruns) { if (sys->runs[run].protocol == INTRUDER) { // Only intruder roles if (sys->runs[run].role != I_M) { // The M_0 (initial knowledge) events don't count. count++; } } run++; } return count; } //------------------------------------------------------------------------ // Proof reporting //------------------------------------------------------------------------ //! Protocol/role name of a run void role_name_print (const int run) { eprintf ("protocol "); termPrint (sys->runs[run].protocol->nameterm); eprintf (", role "); termPrint (sys->runs[run].role->nameterm); } //! Adding a run/extending a run void proof_suppose_run (const int run, const int oldlength, const int newlength) { if (switches.output == PROOF) { int reallength; indentPrint (); eprintf ("Suppose "); if (oldlength == 0) eprintf ("there is a "); else eprintf ("we extend "); reallength = roledef_length (sys->runs[run].start); if (reallength > newlength) eprintf ("semi-"); eprintf ("run #%i of ", run); role_name_print (run); if (reallength > newlength) { if (oldlength == 0) eprintf (" of"); else eprintf (" to"); eprintf (" length %i", newlength); } eprintf ("\n"); } } //! Select a goal void proof_select_goal (Binding b) { if (switches.output == PROOF) { Roledef rd; rd = roledef_shift (sys->runs[b->run_to].start, b->ev_to); indentPrint (); eprintf ("Selected goal: Where does term "); termPrint (b->term); eprintf (" occur first as an interm?\n"); indentPrint (); eprintf ("* It is required for "); roledefPrint (rd); eprintf (" at index %i in run %i\n", b->ev_to, b->run_to); } } //! Cannot bind because of cycle void proof_cannot_bind (const Binding b, const int run, const int index) { if (switches.output == PROOF) { indentPrint (); eprintf ("Cannot bind this to run %i, index %i because that introduces a cycle.\n", run, index); } } //! Test a binding void proof_suppose_binding (Binding b) { if (switches.output == PROOF) { Roledef rd; indentPrint (); rd = roledef_shift (sys->runs[b->run_from].start, b->ev_from); eprintf ("Suppose it originates in run %i, at index %i\n", b->run_from, b->ev_from); indentPrint (); eprintf ("* I.e. event "); roledefPrint (rd); eprintf ("\n"); indentPrint (); eprintf ("* from "); role_name_print (b->run_from); eprintf ("\n"); } } //------------------------------------------------------------------------ // Sub //------------------------------------------------------------------------ //! Iterate over all events in the roles (including the intruder ones) /** * Function is called with (protocol pointer, role pointer, roledef pointer, index) * and returns an integer. If it is false, iteration aborts. */ int iterate_role_events (int (*func) ()) { Protocol p; p = sys->protocols; while (p != NULL) { Role r; r = p->roles; while (r != NULL) { Roledef rd; int index; rd = r->roledef; index = 0; while (rd != NULL) { if (!func (p, r, rd, index)) return 0; index++; rd = rd->next; } r = r->next; } p = p->next; } return 1; } //! Iterate over all send types in the roles (including the intruder ones) /** * Function is called with (protocol pointer, role pointer, roledef pointer, index) * and returns an integer. If it is false, iteration aborts. */ int iterate_role_sends (int (*func) ()) { int send_wrapper (Protocol p, Role r, Roledef rd, int i) { if (rd->type == SEND) { return func (p, r, rd, i); } else { return 1; } } return iterate_role_events (send_wrapper); } //! Create decryption role instance /** * Note that this does not add any bindings for the reads. * *@param term The term to be decrypted (implies decryption key) *@param key The key that is needed to decrypt the term * *@returns The run id of the decryptor instance */ int create_decryptor (const Term term, const Term key) { if (term != NULL && isTermEncrypt (term)) { Roledef rd; int run; #ifdef DEBUG if (DEBUGL (5)) { globalError++; eprintf ("Creating decryptor for term "); termPrint (term); eprintf (" and key "); termPrint (key); eprintf ("\n"); globalError--; } #endif run = semiRunCreate (INTRUDER, I_RRSD); rd = sys->runs[run].start; rd->message = termDuplicateUV (term); rd->next->message = termDuplicateUV (key); rd->next->next->message = termDuplicateUV (TermOp (term)); sys->runs[run].height = 3; proof_suppose_run (run, 0, 3); return run; } globalError++; eprintf ("Term for which a decryptor instance is requested: "); termPrint (term); eprintf ("\n"); error ("Trying to build a decryptor instance for a non-encrypted term."); return -1; } //! Get the priority level of a key that is needed for a term (typical pk/sk distinction) int getPriorityOfNeededKey (const System sys, const Term keyneeded) { int prioritylevel; /* Normally, a key gets higher priority, but unfortunately this is not propagated at the moment. Maybe later. */ prioritylevel = 1; if (realTermEncrypt (keyneeded)) { /* the key is a construction itself */ if (inKnowledge (sys->know, TermKey (keyneeded))) { /* the key is constructed by a public thing */ /* typically, this is a public key, so we postpone it */ prioritylevel = -1; } } return prioritylevel; } //! Report failed binding void report_failed_binding (Binding b, int run, int index) { if (switches.output == PROOF) { indentPrint (); eprintf ("Failed to bind the binding at r%ii%i with term ", b->run_to, b->ev_to); termPrint (b->term); eprintf (" to the source r%ii%i because of orderings.\n", run, index); #ifdef DEBUG if (DEBUGL (5)) { dependPrint (); } #endif } } //! Make a decryption chain from a binding to some run,index using the key list, and callback if this works. /** * The key goals are bound to the goal, and then we iterate on that. * *@param b binding to fix (bind), destination filled in *@param run run of binding start *@param index index in run of binding start * Callback return value is int, but is effectively ignored. */ void createDecryptionChain (const Binding b, const int run, const int index, Termlist keylist, int (*callback) (void)) { if (keylist == NULL) { // Immediate binding, no key needed. if (goal_bind (b, run, index)) { callback (); goal_unbind (b); return; } else { report_failed_binding (b, run, index); } } else { Term tdecr, tkey; int smallrun; // Some decryptor is needed for the term in the list indentDepth++; tdecr = keylist->term; tkey = inverseKey (sys->know->inverses, TermKey (tdecr)); smallrun = create_decryptor (tdecr, tkey); { Roledef rddecrypt; Binding bnew; int newgoals; int prioritylevel; /* * 2. Add goal bindings */ rddecrypt = sys->runs[smallrun].start; // Add goal for tdecr copy newgoals = goal_add (rddecrypt->message, smallrun, 0, 0); if (newgoals != 1) { error ("Added %i goals (instead of one) for decryptor goal 1, weird.", newgoals); } // This is the unique new goal bnew = (Binding) sys->bindings->data; // Add goal for needed key copy prioritylevel = getPriorityOfNeededKey (sys, tkey); newgoals += goal_add (rddecrypt->next->message, smallrun, 1, prioritylevel); if (switches.output == PROOF) { indentPrint (); eprintf ("This introduces the obligation to decrypt the following subterm: "); termPrint (tdecr); eprintf (" to be decrypted using "); termPrint (tkey); eprintf ("\n"); indentPrint (); eprintf ("To this end, we added two new goals and one new send: "); termPrint (rddecrypt->message); eprintf (","); termPrint (rddecrypt->next->message); eprintf (","); termPrint (rddecrypt->next->next->message); eprintf ("\n"); } /* * 3. Bind open goal to decryptor? */ if (goal_bind (b, smallrun, 2)) { if (switches.output == PROOF) { indentPrint (); eprintf ("Bound "); termPrint (b->term); eprintf (" to r%ii%i: trying new createDecryptionChain.\n", smallrun, 2); } // Iterate with the new goal createDecryptionChain (bnew, run, index, keylist->next, callback); goal_unbind (b); } else { report_failed_binding (b, smallrun, 2); } /* * clean up */ goal_remove_last (newgoals); } semiRunDestroy (); termDelete (tkey); indentDepth--; } } //! Try to bind a specific existing run to a goal. /** * The idea is that we try to bind it this specific run and index. If this * requires keys, then we should add such goals as well with the required * decryptor things. * * The 'newdecr' boolean signals the addition of decryptors. If it is false, we should not add any. * * The key goals are bound to the goal. Iterates on success. */ void bind_existing_to_goal (const Binding b, const int run, const int index, int newdecr) { Term bigterm; int unifiesWithKeys (Termlist substlist, Termlist keylist) { int old_length; int newgoals; // TODO this is a hack: in this case we really should not use subterm // unification but interm instead. However, this effectively does the same // by avoiding branches that get immediately pruned anyway. if (!newdecr && keylist != NULL) { return true; } // We need some adapting because the height would increase; we therefore // have to add read goals before we know whether it unifies. old_length = sys->runs[run].height; newgoals = add_read_goals (run, old_length, index + 1); { // wrap substitution lists void wrapSubst (Termlist sl) { if (sl == NULL) { if (switches.output == PROOF) { Roledef rd; indentPrint (); eprintf ("Suppose "); termPrint (b->term); eprintf (" originates first at run %i, event %i, as part of ", run, index); rd = roledef_shift (sys->runs[run].start, index); termPrint (rd->message); eprintf ("\n"); } // new create key goals, bind etc. createDecryptionChain (b, run, index, keylist, iterate); } else { int neworders; int allgood; Term tvar; // the idea is, that a substitution in run x with // something containing should be wrapped; this // occurs for all subterms of other runs. int makeDepend (Term tsmall) { Term tsubst; tsubst = deVar (tsmall); if (!realTermVariable (tsubst)) { // Only for non-variables (i.e. local constants) int r1, e1; r1 = TermRunid (tsubst); e1 = firstOccurrence (sys, r1, tsubst, SEND); if (e1 >= 0) { int r2, e2; r2 = TermRunid (tvar); e2 = firstOccurrence (sys, r2, tsubst, READ); if (e2 >= 0) { if (dependPushEvent (r1, e1, r2, e2)) { neworders++; return true; } else { allgood = false; if (switches.output == PROOF) { indentPrint (); eprintf ("Substitution for "); termSubstPrint (sl->term); eprintf (" (subterm "); termPrint (tsmall); eprintf (") could not be safely bound.\n"); } return false; } } } } return true; } neworders = 0; allgood = true; tvar = sl->term; iterateTermOther (run, tvar, makeDepend); if (allgood) { wrapSubst (sl->next); } while (neworders > 0) { neworders--; dependPopEvent (); } } } wrapSubst (substlist); } // undo goal_remove_last (newgoals); sys->runs[run].height = old_length; return true; } bigterm = roledef_shift (sys->runs[run].start, index)->message; subtermUnify (bigterm, b->term, NULL, NULL, unifiesWithKeys); } //! Bind a goal to an existing regular run, if possible, by adding decr events int bind_existing_run (const Binding b, const Protocol p, const Role r, const int index) { int run, flag; int found; flag = 1; found = 0; for (run = 0; run < sys->maxruns; run++) { if (sys->runs[run].protocol == p && sys->runs[run].role == r) { found++; if (switches.output == PROOF) { if (found == 1) { indentPrint (); eprintf ("Can we bind it to an existing regular run of "); termPrint (p->nameterm); eprintf (", "); termPrint (r->nameterm); eprintf ("?\n"); } indentPrint (); eprintf ("%i. Can we bind it to run %i?\n", found, run); } indentDepth++; bind_existing_to_goal (b, run, index, true); indentDepth--; } } if (switches.output == PROOF && found == 0) { indentPrint (); eprintf ("There is no existing run for "); termPrint (p->nameterm); eprintf (", "); termPrint (r->nameterm); eprintf ("\n"); } return flag; } //! Bind a goal to a new run, possibly adding decr events int bind_new_run (const Binding b, const Protocol p, const Role r, const int index) { int run; run = semiRunCreate (p, r); proof_suppose_run (run, 0, index + 1); { int newgoals; newgoals = add_read_goals (run, 0, index + 1); indentDepth++; bind_existing_to_goal (b, run, index, true); indentDepth--; goal_remove_last (newgoals); } semiRunDestroy (); return true; } //! Print the current semistate void printSemiState () { int run; int open; int binding_state_print (void *dt) { binding_indent_print ((Binding) dt, 1); return 1; } indentPrint (); eprintf ("!! --=[ Semistate ]=--\n"); indentPrint (); eprintf ("!!\n"); indentPrint (); eprintf ("!! Trace length: %i\n", get_semitrace_length ()); open = 0; for (run = 0; run < sys->maxruns; run++) { int index; Role r; Roledef rd; Term oldagent; indentPrint (); eprintf ("!!\n"); indentPrint (); eprintf ("!! [ Run %i, ", run); termPrint (sys->runs[run].protocol->nameterm); eprintf (", "); r = sys->runs[run].role; oldagent = r->nameterm->subst; r->nameterm->subst = NULL; termPrint (r->nameterm); r->nameterm->subst = oldagent; if (oldagent != NULL) { eprintf (": "); termPrint (oldagent); } eprintf (" ]\n"); index = 0; rd = sys->runs[run].start; while (index < sys->runs[run].height) { indentPrint (); eprintf ("!! %i ", index); roledefPrint (rd); eprintf ("\n"); if (isGoal (rd) && !isBound (rd)) open++; index++; rd = rd->next; } } if (sys->bindings != NULL) { indentPrint (); eprintf ("!!\n"); list_iterate (sys->bindings, binding_state_print); } indentPrint (); eprintf ("!!\n"); indentPrint (); eprintf ("!! - open: %i -\n", open); } //! Check if a binding duplicates an old one: if so, simply connect /** * If it returns true, it has bound the b_new binding, which we must unbind later. */ int bind_old_goal (const Binding b_new) { if (!b_new->done) { List bl; bl = sys->bindings; while (bl != NULL) { Binding b_old; b_old = (Binding) bl->data; if (b_old->done && isTermEqual (b_new->term, b_old->term)) { // Old is done and has the same term! // So we try to copy this binding, and fix it. if (goal_bind (b_new, b_old->run_from, b_old->ev_from)) { return true; } } bl = bl->next; } } // No old binding to connect to return false; } //! Bind an intruder goal by intruder composition construction /** * Handles the case where the intruder constructs a composed term himself. */ int bind_goal_new_encrypt (const Binding b) { Term term; int flag; int can_be_encrypted; flag = 1; term = deVar (b->term); can_be_encrypted = 0; if (!realTermLeaf (term)) { Term t1, t2; if (switches.intruder && (!realTermEncrypt (term))) { // tuple construction error ("Goal that is a tuple should not occur!"); } // must be encryption t1 = TermOp (term); t2 = TermKey (term); if (t2 != TERM_Hidden) { int run; can_be_encrypted = 1; run = semiRunCreate (INTRUDER, I_RRS); { int index; Roledef rd; rd = sys->runs[run].start; rd->message = termDuplicateUV (t1); rd->next->message = termDuplicateUV (t2); rd->next->next->message = termDuplicateUV (term); index = 2; proof_suppose_run (run, 0, index + 1); if (switches.output == PROOF) { indentPrint (); eprintf ("* Encrypting "); termPrint (term); eprintf (" using term "); termPrint (t1); eprintf (" and key "); termPrint (t2); eprintf ("\n"); } { int newgoals; newgoals = add_read_goals (run, 0, index + 1); { indentDepth++; if (goal_bind (b, run, index)) { proof_suppose_binding (b); flag = flag && iterate (); goal_unbind (b); } else { proof_cannot_bind (b, run, index); } indentDepth--; } goal_remove_last (newgoals); } } semiRunDestroy (); } } if (!can_be_encrypted) { if (switches.output == PROOF) { indentPrint (); eprintf ("Term "); termPrint (b->term); eprintf (" cannot be constructed by encryption.\n"); } } return flag; } //! Bind an intruder goal by intruder construction /** * Handles the case where the intruder constructs a composed term himself, or retrieves it from m0. * However, it must not already have been created in an intruder run; then it gets bound to that. */ int bind_goal_new_intruder_run (const Binding b) { int flag; if (switches.output == PROOF) { indentPrint (); eprintf ("Can we bind "); termPrint (b->term); eprintf (" from a new intruder run?\n"); } indentDepth++; //flag = flag && bind_goal_new_encrypt (b); flag = bind_goal_new_encrypt (b); indentDepth--; return flag; } //! Bind a regular goal /** * Problem child. Valgrind does not like it. */ int bind_goal_regular_run (const Binding b) { int flag; int found; /* * This is a local function so we have access to goal */ int bind_this_role_send (Protocol p, Role r, Roledef rd, int index) { int test_sub_unification (Termlist substlist, Termlist keylist) { // A unification exists; return the signal return false; } if (p == INTRUDER) { // No intruder roles here return true; } // Test for interm unification #ifdef DEBUG if (DEBUGL (5)) { indentPrint (); eprintf ("Checking send candidate with message "); termPrint (rd->message); eprintf (" from "); termPrint (p->nameterm); eprintf (", "); termPrint (r->nameterm); eprintf (", index %i\n", index); } #endif if (!subtermUnify (rd->message, b->term, NULL, NULL, test_sub_unification)) { int sflag; // A good candidate found++; if (switches.output == PROOF && found == 1) { indentPrint (); eprintf ("The term ", found); termPrint (b->term); eprintf (" matches patterns from the role definitions. Investigate.\n"); } if (switches.output == PROOF) { indentPrint (); eprintf ("%i. It matches the pattern ", found); termPrint (rd->message); eprintf (" from "); termPrint (p->nameterm); eprintf (", "); termPrint (r->nameterm); eprintf (", at %i\n", index); } indentDepth++; // Bind to existing run #ifdef DEBUG debug (5, "Trying to bind to existing run."); #endif sflag = bind_existing_run (b, p, r, index); // bind to new run #ifdef DEBUG debug (5, "Trying to bind to new run."); #endif sflag = sflag && bind_new_run (b, p, r, index); indentDepth--; return sflag; } else { return true; } } // Bind to all possible sends of regular runs found = 0; flag = iterate_role_sends (bind_this_role_send); if (switches.output == PROOF && found == 0) { indentPrint (); eprintf ("The term "); termPrint (b->term); eprintf (" does not match any pattern from the role definitions.\n"); } return flag; } //! Bind to all possible sends of intruder runs int bind_goal_old_intruder_run (Binding b) { int run; int flag; int found; found = 0; flag = 1; for (run = 0; run < sys->maxruns; run++) { if (sys->runs[run].protocol == INTRUDER) { int ev; Roledef rd; rd = sys->runs[run].start; ev = 0; while (ev < sys->runs[run].height) { if (rd->type == SEND) { found++; if (switches.output == PROOF && found == 1) { indentPrint (); eprintf ("Suppose it is from an existing intruder run.\n"); } indentDepth++; bind_existing_to_goal (b, run, ev, (sys->runs[run].role != I_RRS)); indentDepth--; } rd = rd->next; ev++; } } } if (switches.output == PROOF && found == 0) { indentPrint (); eprintf ("No existing intruder runs to match to.\n"); } return flag; } //! Bind a goal in all possible ways int bind_goal_all_options (const Binding b) { if (b->blocked) { error ("Trying to bind a blocked goal!"); } if (!b->done) { int flag; flag = 1; proof_select_goal (b); indentDepth++; // Consider a duplicate goal that we already bound before (C-minimality) // if (1 == 0) if (bind_old_goal (b)) { if (switches.output == PROOF) { indentPrint (); eprintf ("Goal for term "); termPrint (b->term); eprintf (" was bound once before, linking up to #%i, %i.\n", b->run_from, b->ev_from); } flag = flag && iterate (); // Unbind again goal_unbind (b); indentDepth--; return flag; } else { int know_only; know_only = false; if (1 == 0) // blocked for now { // Prune: if it is an SK type construct, ready // No regular run will apply SK for you. //!@todo This still needs a lemma, and a more generic (correct) algorithm!! It is currently // actually false, e.g. for signing protocols, and password-like functions. // Term function; function = getTermFunction (b->term); if (function != NULL) { if (!inKnowledge (sys->know, function)) { // Prune because we didn't know it before, and it is never subterm-sent if (switches.output == PROOF) { indentPrint (); eprintf ("* Because "); termPrint (b->term); eprintf (" is never sent from a regular run, so we only intruder construct it.\n"); } know_only = true; } } } if (switches.experimental & 16) { // Keylevel lemmas: improves on the previous one if (!isPossiblySent (b->term)) { if (switches.output == PROOF) { eprintf ("Rejecting a term as a regular bind because key levels are off: "); termPrint (b->term); if (know_only) { eprintf (" [in accordance with function lemma]"); } else { eprintf (" [stronger than function lemma]"); } eprintf ("\n"); } know_only = true; } } if (!(switches.experimental & 32)) { /** * Note: this is slightly weaker than the previous & 16, * but it actually differs in such minimal cases that it * might be better to simply have the (much cleaner) * keylevel lemma. * * That's why this is default and the other isn't. */ // Hidelevel variant int hlf; hlf = hidelevelFlag (sys, b->term); if (hlf == HLFLAG_NONE || hlf == HLFLAG_KNOW) { know_only = true; } } // Allright, proceed proofDepth++; if (know_only) { // Special case: only from intruder flag = flag && bind_goal_old_intruder_run (b); //flag = flag && bind_goal_new_intruder_run (b); } else { // Normal case { flag = bind_goal_regular_run (b); } flag = flag && bind_goal_old_intruder_run (b); flag = flag && bind_goal_new_intruder_run (b); } proofDepth--; indentDepth--; return flag; } } else { return 1; } } //! Create a generic new term of the same type, with a new run identifier. /** * Output: the first element of the returned list. */ Termlist createNewTermGeneric (Termlist tl, Term t) { int freenumber; Termlist tlscan; Term newterm; /* Determine first free number */ freenumber = sys->maxruns; tlscan = tl; while (tlscan != NULL) { Term ts; ts = tlscan->term; if (isLeafNameEqual (t, ts)) { if (TermRunid (ts) >= freenumber) { freenumber = TermRunid (ts) + 1; } } tlscan = tlscan->next; } /* Make a new term with the free number */ newterm = (Term) malloc (sizeof (struct term)); memcpy (newterm, t, sizeof (struct term)); TermRunid (newterm) = freenumber; /* The type of the new term should be that of the parent! */ newterm->stype = termlistAppend (NULL, t); /* return */ return termlistPrepend (tl, newterm); } //! Construct a list of already used constants Termlist findUsedConstants (const System sys) { int run; Termlist tl; Termlist tlconst; tl = NULL; tlconst = NULL; for (run = 0; run < sys->maxruns; run++) { tl = termlistAddBasics (tl, sys->runs[run].rho); tl = termlistAddBasics (tl, sys->runs[run].sigma); } while (tl != NULL) { Term t; t = tl->term; if (!realTermVariable (t)) { tlconst = termlistAddNew (tlconst, t); } tl = tl->next; } termlistDelete (tl); return tlconst; } //! Create a new term with incremented run rumber, starting at sys->maxruns. /** * This is a rather intricate function that tries to generate new terms of a * certain type. It first looks up things in the initial knowledge, checking * whether they are used already. After that, new ones are generated. * * Output: the first element of the returned list. */ Termlist createNewTerm (Termlist tl, Term t, int isagent) { /* Does if have an explicit type? * If so, we try to find a fresh name from the intruder knowledge first. */ if (isagent) { Termlist knowlist; Termlist kl; knowlist = knowledgeSet (sys->know); kl = knowlist; while (kl != NULL) { Term k; k = kl->term; if (isAgentType (k->stype)) { /* agent */ /* We don't want to instantiate untrusted agents. */ if (!inTermlist (sys->untrusted, k)) { /* trusted agent */ if (!inTermlist (tl, k)) { /* This agent name is not in the list yet. */ return termlistPrepend (tl, k); } } } kl = kl->next; } termlistDelete (knowlist); } /* Not an agent or no free one found */ return createNewTermGeneric (tl, t); } //! Delete a term made in the previous constructions /** * \sa createNewTerm */ void deleteNewTerm (Term t) { if (TermRunid (t) >= 0) { /* if it has a positive runid, it did not come from the intruder * knowledge, so it must have been constructed. */ free (t); } } //! Make a trace concrete /** * People find reading variables in attack outputs difficult. * Thus, we instantiate them in a sensible way to make things more readable. * * This happens after sys->maxruns is fixed. Intruder constants thus are numbered from sys->maxruns onwards. * * \sa makeTraceClass */ Termlist makeTraceConcrete (const System sys) { Termlist changedvars; Termlist tlnew; int run; changedvars = NULL; tlnew = findUsedConstants (sys); run = 0; while (run < sys->maxruns) { Termlist tl; tl = sys->runs[run].locals; while (tl != NULL) { /* variable, and of some run? */ if (isTermVariable (tl->term) && TermRunid (tl->term) >= 0) { Term var; Term name; Termlist vartype; var = deVar (tl->term); vartype = var->stype; // Determine class name if (vartype != NULL) { // Take first type name name = vartype->term; } else { // Just a generic name name = TERM_Data; } // We should turn this into an actual term tlnew = createNewTerm (tlnew, name, isAgentType (var->stype)); var->subst = tlnew->term; // Store for undo later changedvars = termlistAdd (changedvars, var); } tl = tl->next; } run++; } termlistDelete (tlnew); return changedvars; } //! Make a trace a class again /** * \sa makeTraceConcrete */ void makeTraceClass (const System sys, Termlist varlist) { Termlist tl; tl = varlist; while (tl != NULL) { Term var; var = tl->term; if (realTermVariable (var)) { deleteNewTerm (var->subst); var->subst = NULL; } tl = tl->next; } termlistDelete (varlist); } //! Determine whether to filter to a single attack int useAttackBuffer (void) { if (switches.useAttackBuffer) { // it is possible if (switches.prune != 0) { // it is also desired return true; } } return false; } //! Start attack output void attackOutputStart (void) { if (useAttackBuffer ()) { FILE *fd; // Close old file (if any) if (attack_stream != NULL) { fclose (attack_stream); // this automatically discards the old temporary file } // Create new file fd = (FILE *) scyther_tempfile (); attack_stream = fd; globalStream = (char *) attack_stream; } } //! Stop attack output void attackOutputStop (void) { // Nothing to do, just leave the opened tmpfile } //! Copy one (finite) stream from beginning to end to another /** * Ugly first implementation, something to improve later (although it is not * crucial code in any way) */ void fcopy (FILE * fromstream, FILE * tostream) { int c; // 'Just to be sure' fflush (fromstream); fseek (fromstream, 0, SEEK_SET); // Urgh, using the assignment in the loop condition, brrr. Fugly. // Discourage. while ((c = fgetc (fromstream)) != EOF) { fputc (c, tostream); } } //! Output an attack in the desired way void arachneOutputAttack () { Termlist varlist; // Make concrete if (switches.concrete) { varlist = makeTraceConcrete (sys); } else { varlist = NULL; } // Wrapper for the real output attackOutputStart (); // Generate the output, already! if (switches.xml) { xmlOutSemitrace (sys); } else { dotSemiState (sys); } // End wrapper attackOutputStop (); // Undo concretization makeTraceClass (sys, varlist); } //------------------------------------------------------------------------ // Main logic core //------------------------------------------------------------------------ //! Selector to select the first tuple goal. /** * Basically to get rid of -m2 tuple goals. * Nice iteration, I'd suppose */ Binding select_tuple_goal () { List bl; Binding tuplegoal; bl = sys->bindings; tuplegoal = NULL; while (bl != NULL && tuplegoal == NULL) { Binding b; b = (Binding) bl->data; // Ignore done stuff if (!b->blocked && !b->done) { if (isTermTuple (b->term)) { tuplegoal = b; } } bl = bl->next; } return tuplegoal; } //! Iterate a binding /** * For DY model, we unfold any tuples first, otherwise we skip that. */ int iterateOneBinding (void) { Binding btup; int flag; // marker flag = true; // Are there any tuple goals? if (switches.intruder) { // Maybe... (well, test) btup = select_tuple_goal (); } else { // No, there are non that need to be expanded (no intruder) btup = NULL; } if (btup != NULL) { /* Substitution or something resulted in a tuple goal: we immediately split them into compounds. */ Term tuple; tuple = deVar (btup->term); if (realTermTuple (tuple)) { int count; Term tupletermbuffer; tupletermbuffer = btup->term; /* * We solve this by replacing the tuple goal by the left term, and adding a goal for the right term. */ btup->term = TermOp1 (tuple); count = goal_add (TermOp2 (tuple), btup->run_to, btup->ev_to, btup->level); // Show this in output if (switches.output == PROOF) { indentPrint (); eprintf ("Expanding tuple goal "); termPrint (tupletermbuffer); eprintf (" into %i subgoals.\n", count); } // iterate flag = iterate (); // undo goal_remove_last (count); btup->term = tupletermbuffer; } } else { // No tuple goals; good Binding b; /** * Not pruned: count */ sys->states = statesIncrease (sys->states); sys->current_claim->states = statesIncrease (sys->current_claim->states); /** * Check whether its a final state (i.e. all goals bound) */ b = (Binding) select_goal (sys); if (b == NULL) { /* * all goals bound, check for property */ if (switches.output == PROOF) { indentPrint (); eprintf ("All goals are now bound.\n"); } sys->claims = statesIncrease (sys->claims); sys->current_claim->count = statesIncrease (sys->current_claim->count); flag = property_check (sys); } else { /* * bind this goal in all possible ways and iterate */ flag = bind_goal_all_options (b); } } return flag; } //! Unfold this particular name in this way void iterateAgentUnfoldThis (const Term rolevar, const Term agent) { Term buffer; buffer = rolevar->subst; rolevar->subst = agent; iterate (); rolevar->subst = buffer; } //! Unfold this particular name void iterateAgentUnfolding (const System sys, const Term rolevar) { Termlist kl; int count; iterateAgentUnfoldThis (rolevar, AGENT_Eve); kl = knowledgeSet (sys->know); count = 0; while (kl != NULL && count < switches.agentUnfold) { Term t; t = deVar (kl->term); if (realTermLeaf (t) && inTermlist (t->stype, TERM_Agent)) { if (!inTermlist (sys->untrusted, t)) { iterateAgentUnfoldThis (rolevar, t); count++; } } kl = kl->next; } termlistDelete (kl); } //! Unfold names /** * Returns true if nothing was unfolded and the iteration must be done. * Returns false when the iteration should not be done. */ int doAgentUnfolding (const System sys) { int run; for (run = 0; run < sys->maxruns; run++) { Termlist tl; tl = sys->runs[run].rho; while (tl != NULL) { Term t; t = deVar (tl->term); if (realTermVariable (t)) { // Hey, this role name is still a variable. // We don't want that and so we unfold it as expected. iterateAgentUnfolding (sys, t); return false; } tl = tl->next; } } return true; } //! Main recursive procedure for Arachne int iterate () { int flag; flag = 1; // check unfolding agent names if (switches.agentUnfold > 0) { if (!doAgentUnfolding (sys)) return flag; } if (!prune_theorems (sys)) { if (!prune_claim_specifics (sys)) { if (!prune_bounds (sys)) { // Go and pick a binding for iteration flag = iterateOneBinding (); } else { // Pruned because of bound! sys->current_claim->complete = 0; } } } #ifdef DEBUG if (DEBUGL (5) && !flag) { warning ("Flag has turned 0!"); } #endif return flag; } //! Just before starting output of an attack. // //! A wrapper for the case in which we need to buffer attacks. int iterate_buffer_attacks (void) { if (useAttackBuffer ()) { // We are pruning attacks, so they should go into a temporary file. /* * Set up the temporary file pointer */ char *buffer; int result; // Push the old situation onto the stack buffer = globalStream; // Start stuff attack_stream = NULL; attackOutputStart (); // Finally, proceed with iteration procedure result = iterate (); /* Now, if it has been set, we need to copy the output to the normal streams. */ fcopy (attack_stream, (FILE *) buffer); // Close fclose (attack_stream); attack_stream = NULL; // Restore globalStream = buffer; return result; } else { // No attack buffering, just output all of them return iterate (); } } //! Arachne single claim test void arachneClaimTest (Claimlist cl) { // others we simply test... int run; int newruns; Protocol p; Role r; newruns = 0; sys->current_claim = cl; attack_length = INT_MAX; attack_leastcost = INT_MAX; cl->complete = 1; p = (Protocol) cl->protocol; r = (Role) cl->role; if (switches.output == PROOF) { indentPrint (); eprintf ("Testing Claim "); termPrint (cl->type); eprintf (" from "); termPrint (p->nameterm); eprintf (", "); termPrint (r->nameterm); eprintf (" at index %i.\n", cl->ev); } indentDepth++; run = semiRunCreate (p, r); newruns++; { int newgoals; int realStart (void) { #ifdef DEBUG if (DEBUGL (5)) { printSemiState (); } #endif return iterate_buffer_attacks (); } proof_suppose_run (run, 0, cl->ev + 1); newgoals = add_read_goals (run, 0, cl->ev + 1); /** * Add initial knowledge node */ { Termlist m0tl; Term m0t; int m0run; m0tl = knowledgeSet (sys->know); if (m0tl != NULL) { m0t = termlist_to_tuple (m0tl); // eprintf("Initial intruder knowledge node for "); // termPrint(m0t); // eprintf("\n"); I_M->roledef->message = m0t; m0run = semiRunCreate (INTRUDER, I_M); newruns++; proof_suppose_run (m0run, 0, 1); sys->runs[m0run].height = 1; } else { m0run = -1; } { /** * Add specific goal info and iterate algorithm */ add_claim_specifics (sys, cl, roledef_shift (sys->runs[run].start, cl->ev), realStart); } if (m0run != -1) { // remove initial knowledge node termDelete (m0t); termlistDelete (m0tl); semiRunDestroy (); newruns--; } } // remove claiming run goals goal_remove_last (newgoals); semiRunDestroy (); newruns--; } //! Destroy while (sys->maxruns > 0 && newruns > 0) { semiRunDestroy (); newruns--; } #ifdef DEBUG if (sys->bindings != NULL) { error ("sys->bindings NOT empty after claim test."); } if (sys->maxruns != 0) { error ("%i undestroyed runs left after claim test.", sys->maxruns); } if (newruns != 0) { error ("Lost %i runs after claim test.", newruns); } #endif //! Indent back indentDepth--; if (switches.output == PROOF) { indentPrint (); eprintf ("Proof complete for this claim.\n"); } } //! Arachne single claim inspection int arachneClaim (Claimlist cl) { // Skip the dummy claims if (!isTermEqual (cl->type, CLAIM_Empty)) { // Some claims are always true! if (!cl->alwaystrue) { // others we simply test... arachneClaimTest (cl); } claimStatusReport (sys, cl); if (switches.xml) { xmlOutClaim (sys, cl); } return true; } return false; } //! Main code for Arachne /** * For this test, we manually set up some stuff. * * But later, this will just iterate over all claims. * * @TODO what does it return? And is that -1 valid, if nothing is tested? */ int arachne () { Claimlist cl; int count; int print_send (Protocol p, Role r, Roledef rd, int index) { eprintf ("IRS: "); termPrint (p->nameterm); eprintf (", "); termPrint (r->nameterm); eprintf (", %i, ", index); roledefPrint (rd); eprintf ("\n"); return 1; } int determine_encrypt_max (Protocol p, Role r, Roledef rd, int index) { int tlevel; tlevel = term_encryption_level (rd->message); #ifdef DEBUG if (DEBUGL (3)) { eprintf ("Encryption level %i found for term ", tlevel); termPrint (rd->message); eprintf ("\n"); } #endif if (tlevel > max_encryption_level) max_encryption_level = tlevel; return 1; } /* * set up claim role(s) */ if (switches.runs == 0) { // No real checking. return -1; } if (sys->maxruns > 0) { error ("Something is wrong, number of runs >0."); } sys->num_regular_runs = 0; sys->num_intruder_runs = 0; max_encryption_level = 0; iterate_role_events (determine_encrypt_max); #ifdef DEBUG if (DEBUGL (1)) { eprintf ("Maximum encryption level: %i\n", max_encryption_level); } #endif fixAgentKeylevels (); indentDepth = 0; proofDepth = 0; cl = sys->claimlist; count = 0; while (cl != NULL) { /** * Check each claim */ if (arachneClaim (cl)) { count++; } // next cl = cl->next; } return count; } //! Construct knowledge set at some event, based on a semitrace. /** * This is a very 'stupid' algorithm; it is just there because GijsH * requested it. It does in no way guarantee that this is the actual * knowledge set at the given point. It simply gives an underapproximation, * that will be correct in most cases. The main reason for this is that it * completely ignores any information on unbound variables, and regards them * as bound constants. * * Because everything is supposed to be bound, we conclude that even 'read' * events imply a certain knowledge. * * If aftercomplete is 0 or false, we actually check the ordering; otherwise we * just assume the trace has finished. * * Use knowledgeDelete later to clean up. */ Knowledge knowledgeAtArachne (const System sys, const int myrun, const int myindex, const int aftercomplete) { Knowledge know; int run; know = knowledgeDuplicate (sys->know); // duplicate initial knowledge run = 0; while (run < sys->maxruns) { int index; int maxheight; Roledef rd; index = 0; rd = sys->runs[run].start; maxheight = sys->runs[run].height; if (run == myrun && myindex > maxheight) { // local run index can override real step maxheight = myindex; } while (rd != NULL && index < maxheight) { // Check whether this event precedes myevent if (aftercomplete || isDependEvent (run, index, myrun, myindex)) { // If it is a send (trivial) or a read (remarkable, but true // because of bindings) we can add the message and the agents to // the knowledge. if (rd->type == SEND || rd->type == READ) { knowledgeAddTerm (know, rd->message); if (rd->from != NULL) knowledgeAddTerm (know, rd->from); if (rd->to != NULL) knowledgeAddTerm (know, rd->to); } index++; rd = rd->next; } else { // Not ordered before anymore, so we skip to the next run. rd = NULL; } } run++; } return know; } //! Determine whether a term is trivially known at some event in a partially ordered structure. /** * Important: read disclaimer at knowledgeAtArachne() * * Returns true iff the term is certainly known at that point in the * semitrace. */ int isTriviallyKnownAtArachne (const System sys, const Term t, const int run, const int index) { int result; Knowledge knowset; knowset = knowledgeAtArachne (sys, run, index, false); result = inKnowledge (knowset, t); knowledgeDelete (knowset); return result; } //! Determine whether a term is trivially known after execution of some partially ordered structure. /** * Important: read disclaimer at knowledgeAtArachne() * * Returns true iff the term is certainly known after all events in the * semitrace. */ int isTriviallyKnownAfterArachne (const System sys, const Term t, const int run, const int index) { int result; Knowledge knowset; knowset = knowledgeAtArachne (sys, run, index, true); result = inKnowledge (knowset, t); knowledgeDelete (knowset); return result; } //! Mark that we have no full proof /** * Provides an interface for marking incomplete proofs. * Currently used in mgusubterm in mgu.c */ void markNoFullProof (const Term tbig, const Term tsmall) { // Comment in proof if (switches.output == PROOF) { indentPrint (); eprintf ("Note: the pattern set will be incomplete, because "); termPrint (tbig); eprintf (" allows for infinitely many ways to subtermUnify "); termPrint (tsmall); eprintf (".\n"); } sys->current_claim->complete = false; }