Verifying cryptographic protocols, II: a simple example

02 Nov 2022

[ general  verification  automation  ]

Not long ago I wrote about cryptographic protocols and their verification. In this post, we shall see a simple example: the famous Needham-Schroeder public key protocol and its verification using Isabelle/HOL. The protocol will be the version as corrected by Lowe: the original provides weaker guarantees and is harder to reason about. Only highlights can be shown here. The proofs rely on a lot of formal machinery, which is described in the journal paper (also available here). For many people, crypto protocol verification rather than Isabelle seems to be my main research achievement, and yet they can’t really be separated: these techniques don’t seem to be reproducible in those proof assistants that have weaker automation, namely, all of them. I know that an attempt was made using a Certain Verification System.

Messages and operations on them

Lowe’s work was the starting point for much of the work on protocol verification undertaken during the late 1990s, and sometimes it was even acknowledged. It completely superseded earlier work on authentication logics such as BAN, which although celebrated at first, didn’t yield reliable results. In particular, BAN validates the incorrect version of the protocol discussed here.

First, we set up a common framework for analysing security protocols in general. The principals or agents consist of a trusted authentication server (required by many protocols), infinitely many friendly agents, and the intruder or Spy.

datatype agent = Server | Friend nat | Spy

Messages are constructed by hashing, concatenation or encryption over the four primitive message elements: agent names, etc.

datatype
     msg = Agent  agent     ― ‹Agent names
         | Number nat       ― ‹Ordinary integers, timestamps, ...
         | Nonce  nat       ― ‹Unguessable nonces
         | Key    key       ― ‹Crypto keys
         | Hash   msg       ― ‹Hashing
         | MPair  msg msg   ― ‹Compound messages
         | Crypt  key msg   ― ‹Encryption, public- or shared-key

The formalisation of crypto keys is omitted here. Briefly: keys are integers and every key has an inverse, which in the case of shared-key encryption is identical to the key itself. No encryption algorithms are formalised.

Several operators are defined inductively to specify what can be derived from a set of (presumably intercepted) messages. For reasoning about secrecy, analz specifies the set of message components that can be extracted from a given set. The body of an encrypted message becomes available if the decryption key is available.

inductive_set
  analz :: "msg set  msg set"
  for H :: "msg set"
  where
    Inj [intro,simp]: "X  H  X  analz H"
  | Fst:     "X,Y  analz H  X  analz H"
  | Snd:     "X,Y  analz H  Y  analz H"
  | Decrypt: "Crypt K X  analz H; Key(invKey K)  analz H  X  analz H"

The function parts (omitted here) is an over-approximation of analz, defined similarly except that decryption does not require a key. It is useful for reasoning about all secrets that have been communicated, whereas analz is for reasoning about secrets that are no longer secret.

The function synth describes the set of messages that could be synthesised from a given set of message components. It is here that the “unguessable” property of keys and nonces is formalised. All numbers are guessable (this makes sense for small integers, and timestamps). To create an encrypted message, you need the encryption key.

inductive_set
  synth :: "msg set => msg set"
  for H :: "msg set"
  where
    Inj    [intro]:   "X  H  X  synth H"
  | Agent  [intro]:   "Agent agt  synth H"
  | Number [intro]:   "Number n   synth H"
  | Hash   [intro]:   "X  synth H  Hash X  synth H"
  | MPair  [intro]:   "X  synth H;  Y  synth H  X,Y  synth H"
  | Crypt  [intro]:   "X  synth H;  Key(K)  H  Crypt K X  synth H"

Innumerable properties are for these operators by simple inductions. They turn out to be idempotent and monotonic. Identities are proved to simplify the arguments of the operators, for example to pull out inserted elements. We can pull out an inserted nonce, for example:

lemma analz_insert_Nonce [simp]:
  "analz (insert (Nonce N) H) = insert (Nonce N) (analz H)"

There is no analogous law for inserted keys because keys can be used to decrypt.

Among the more interesting laws is the following, which shows that synthesis can be decoupled from analysis. Suitable laws are proved for all combinations of the operators in order to simplify nested expressions.

lemma analz_synth [simp]: "analz (synth H) = analz H  synth H"

The most critical combination is synthesis of analysis, for that is what the Spy does: break down past traffic, then combine the components into new messages. The following theorem allows us to eliminate an occurrence of such a fake message X from the argument of analz, which turns out to provide a general method for solving the subgoal corresponding to a fake message.

lemma Fake_analz_insert:
  "X  synth (analz G)  analz (insert X H)  synth (analz G)  analz (G  H)"

Events, traces and other basic notions

Protocols are formalised with respect to a “God’s eye” trace model. The trace holds all message send attempts from the beginning of time, including multiple protocol runs possibly interleaved with any number of parties.

datatype
  event = Says  agent agent msg
        | Gets  agent       msg
        | Notes agent       msg

The event Says A B X represents the attempt by agent A to send message X to agent B. This was at one point the only event in the model. Later I introduced Notes, to represent the local storage of an agent and also information leakage outside the protocol. Giampaolo Bella, one of my PhD students, introduced Gets to signify the reception of a message by a specific agent, who (because the Spy controls the network) has no way of knowing who the true sender was. Giampaolo felt that the explicit Gets event made for clearer protocol specifications. Giampaolo went on to do an enormous amount of work on protocol verification, including timestamp-based protocols, smartcard protocols and other advanced configurations, and even wrote a book on the subject. (Later still, Jean Martina investigated multicast protocols.) But I never updated the Needham-Schroeder formalisation, so we don’t need Gets here.

The basic model includes several other primitives, which can be briefly described as follows:

Formalising the protocol

The earlier post described the Needham–Schroeder public key protocol and mentioned the flaw found by Gavin Lowe. The corrected protocol mentions $B$ in message 2:

\[\newcommand\Na{\mathit{Na}} \newcommand\Nb{\mathit{Nb}} \newcommand\Ka{\mathit{Ka}} \newcommand\Kb{\mathit{Kb}} \def\lbb{\mathopen{\{\kern-.30em|}} \def\rbb{\mathclose{|\kern-.32em\}}} \def\comp#1{\lbb#1\rbb} \begin{alignat*}{2} &1.&\quad A\to B &: \comp{\Na,A}_{\Kb} \\ &2.&\quad B\to A &: \comp{\Na,\Nb,B}_{\Ka} \\ &3.&\quad A\to B &: \comp{\Nb}_{\Kb} \end{alignat*}\]

A reminder of the notation. Some arbitrary principal “Alice” decides for some reason to run the protocol with some other principal, “Bob” and sends the first message (which contains a fresh random number). Upon the receipt of what appears to be an instance of message 1, Bob can continue the protocol by sending message 2 (containing another random number) back to Alice. Alice, upon receiving message 2, checks that it has the correct form, which for this version of the protocol involves confirming both her nonce and the name $B$. If everything is okay, she sends message 3, and if Bob actually receives this message, he will check for his nonce and that will constitute a successful run. If any of the messages fail to get through or do not have the required form, the protocol attempt will simply be abandoned.

We reason informally about such a protocol by saying for example “Bob knows Alice is present by message 3, when his nonce challenge was correctly returned to him. Only Alice could have done this, because that nonce was encrypted using her public key.” This is inductive reasoning, so it’s not surprising that a cryptographic protocol can naturally be modelled by an inductive definition, effectively an operational semantics.

inductive_set ns_public :: "event list set"
  where
   Nil:  "[]  ns_public"
   ― ‹Initial trace is empty
 | Fake: "evsf  ns_public;  X  synth (analz (spies evsf))
           Says Spy B X  # evsf  ns_public"
   ― ‹The spy can say almost anything.
 | NS1:  "evs1  ns_public;  Nonce NA  used evs1
           Says A B (Crypt (pubEK B) Nonce NA, Agent A)
                # evs1    ns_public"
   ― ‹Alice initiates a protocol run, sending a nonce to Bob
 | NS2:  "evs2  ns_public;  Nonce NB  used evs2;
           Says A' B (Crypt (pubEK B) Nonce NA, Agent A)  set evs2
           Says B A (Crypt (pubEK A) Nonce NA, Nonce NB, Agent B)
                # evs2    ns_public"
   ― ‹Bob responds to Alice's message with a further nonce
 | NS3:  "evs3  ns_public;
           Says A  B (Crypt (pubEK B) Nonce NA, Agent A)  set evs3;
           Says B' A (Crypt (pubEK A) Nonce NA, Nonce NB, Agent B)  set evs3
           Says A B (Crypt (pubEK B) (Nonce NB)) # evs3  ns_public"
   ― ‹Alice proves her existence by sending @{term NB} back to Bob.

We formalise a protocol by specifying the possible traces of messages that could be sent over the network. Starting with the empty trace our possibilities are any of the three protocol messages or a Fake message from the Spy, using the synth and analz operators to generate arbitrary messages. Some aspects of the formalisation of the protocol messages deserve comment.

  1. Protocol message NS1, says that any trace can be extended by a message containing a nonce NA that has never appeared before. That knowledge is not available, but the constraint can be achieved with high probability simply by generating a random number.
  2. Message NS2 extends the trace (including another random number) provided a suitable copy of message 1 has already appeared. The decryption of that message is implicit in requiring that it be encrypted with B’s public key. The sender of the message 1 is written as A’ because it is not possible to know who the true sender of a message is (ascertaining this is the very purpose of authentication).
  3. Again for message NS3, the comparison between the instance message 2 just received and the message 1 originally sent is implicit in the formulation itself, with no decryption or comparison operations necessary.

It’s nice that the formal specification is so close to the conventional notation. It’s not cluttered with any implementation details. That’s also a reminder that the proof offers no guarantees against implementation errors.

Some properties proved of this protocol

Our protocol model only allows the proof of safety properties, i.e., that all executions are safe. It cannot prove liveness properties, because the Spy has the power to prevent anything from happening by intercepting all messages. With this sort of model, if you make a mistake and specify a protocol that simply cannot run (typically because the message formats aren’t consistent), then all the safety properties will hold vacuously. Therefore the first thing to do is run a little sanity check. The following result isn’t interesting except to show that traces exist in which the final protocol message is actually sent. The proof involves simply joining up the protocol rules in the correct order and checking that the side conditions can be satisfied.

lemma "NB. evs  ns_public. Says A B (Crypt (pubEK B) (Nonce NB))  set evs"

The objectives of the protocol are sufficiently vague (“Alice and Bob are authenticated to one another”) that we need to decide for ourselves what to prove. The following are technical properties that turned out to be necessary in order to prove more clear-cut properties about secrecy. As so often happens in machine proofs, they look too easy to bother with. First we prove that it is impossible for a nonce used in message 1 to be identical to a nonce used in message 2 (intuitively, because they are chosen randomly).

lemma no_nonce_NS1_NS2:
      "evs  ns_public;
        Crypt (pubEK C) NA', Nonce NA, Agent D  parts (spies evs);
        Crypt (pubEK B) Nonce NA, Agent A  parts (spies evs)
        Nonce NA  analz (spies evs)"
  by (induct rule: ns_public.induct) (auto intro: analz_insertI)

Next comes what I have called a unicity property: that a particular nonce uniquely identifies all the message components bound up with it. This fact relies on the assumption that the nonce in question is not known to the Spy, who could otherwise do anything with it. Honest agents generate a fresh nonce every time, hence this property.

lemma unique_NA:
  assumes NA: "Crypt(pubEK B)  Nonce NA, Agent A   parts(spies evs)"
              "Crypt(pubEK B') Nonce NA, Agent A'  parts(spies evs)"
              "Nonce NA  analz (spies evs)"
    and evs: "evs  ns_public"
  shows "A=A'  B=B'"

The proofs start to get messier, so I prefer to omit some of them. A dogmatic approach to structured proofs doesn’t work in verification generally, where proof steps can easily generate gigantic subgoals. And by the way: the reason the trace variable has distinctive names evs1, evs2, evs3 is to make it easy to see which protocol rule we are talking about in a messy, non-structured induction.

The following theorem is a key protocol objective: Alice’s nonce from message 1 will remain forever secret from the Spy, provided both Alice and Bob are trustworthy.

theorem Spy_not_see_NA:
  assumes NA: "Says A B (Crypt(pubEK B) Nonce NA, Agent A)  set evs"
              "A  bad" "B  bad"
    and evs: "evs  ns_public"
  shows "Nonce NA  analz (spies evs)"
  using evs NA
proof (induction rule: ns_public.induct)
  case (Fake evsf X B)
  then show ?case
    by spy_analz
next
  case (NS2 evs2 NB A' B NA A)
  then show ?case
    by simp (metis Says_imp_analz_Spy analz_into_parts parts.simps unique_NA usedI)
next
  case (NS3 evs3 A B NA B' NB)
  then show ?case
    by simp (meson Says_imp_analz_Spy analz_into_parts no_nonce_NS1_NS2)
qed auto

An analogous property can be proved for Bob’s nonce from message 2. It doesn’t hold for the original, flawed version of Needham–Schroeder.

The following theorem authenticates Bob to Alice. More formally, if Alice has sent message 1 and receives a copy of message 2 containing a copy of the nonce that she sent, then it was in fact sent by Bob.

theorem A_trusts_NS2:
     "Says A  B (Crypt(pubEK B) Nonce NA, Agent A)  set evs;
       Says B' A (Crypt(pubEK A) Nonce NA, Nonce NB, Agent B)  set evs;
       A  bad;  B  bad;  evs  ns_public
       Says B A (Crypt(pubEK A) Nonce NA, Nonce NB, Agent B)  set evs"

And this theorem authenticates Alice to Bob.

theorem B_trusts_NS3:
     "Says B A  (Crypt (pubEK A) Nonce NA, Nonce NB, Agent B)  set evs;
       Says A' B (Crypt (pubEK B) (Nonce NB))  set evs;
       A  bad;  B  bad;  evs  ns_public
       Says A B (Crypt (pubEK B) (Nonce NB))  set evs"

The last two properties above guarantee to Alice and Bob that the other person indeed participated in the protocol. The secrecy properties assure them that the nonces they exchanged are available to them alone. A practical application of this protocol might involve calculating a session key from those nonces in order to transmit a serious amount of data.

Postscript

It’s careless to talk about “verifying” anything unless it has a definitive formal specification. A cryptographic protocol — the last time I looked, admittedly quite a while ago — is typically proposed through an RFC, a mixture of prose and technical diagrams showing the formats of messages down to bitfield boundaries. The aims will be taken for granted (“establish secure communications”) with little discussion of specific protocol objectives and no abstract protocol design independent of its machine implementation. As discussed in the earlier post, ambiguity about the original protocol’s security assumptions resulted in disagreements among experts as to whether it was correct or not.

If you check the corresponding formal development online, you will find much uglier proofs than those shown here. I took the opportunity to beautify them, but the new proofs will not be visible until the release of Isabelle2023.

My colleagues and I wrote numerous papers on protocol verification. But my proofs are too labour intensive. Others have taken the field forward, and crypto protocol verification is today done almost exclusively by fully automatic techniques.