verifier. In Nicola, R.D., ed.: ESOP 2007. ... Darvas, ´A., Hähnle, R., Sands, D.: A theorem proving approach to analysis of secure infor- mation flow. In Hutter, D.
A Certificate Infrastructure for Machine-Checked Proofs of Conditional Information Flow Torben Amtoft1 , Josiah Dodds2 , Zhi Zhang1 , Andrew Appel2 , Lennart Beringer2 , John Hatcliff1 , Xinming Ou1 , Andrew Cousino1 1
Kansas State University (CIS Department, 234 Nichols Hall, Manhattan KS 66506) {tamtoft,zhangzhi,hatcliff,xou,acousino}@ksu.edu 2 Princeton University (Dept. of Comp. Sci., 35 Olden Street, Princeton NJ 08540) {jdodds,appel,eberinge}@cs.princeton.edu
Abstract. In previous work, we have proposed a compositional framework for stating and automatically verifying complex conditional information flow policies using a relational Hoare logic. The framework allows developers and verifiers to work directly with the source code using source-level code contracts. In this work, we extend that approach so that the algorithm for verifying code compliance to an information flow contract emits formal certificates of correctness that are checked in the Coq proof assistant. This framework is implemented in the context of SPARK – a subset of Ada that has been used in a number of industrial contexts for implementing certified safety and security critical systems.
1
Introduction
Network and embedded security devices have complex information flow policies that are crucial to fulfilling device requirements. We have previously explained [1, §1] how devices (such as “separation kernels”) developed following the MILS (Multiple Independent Levels of Security) architecture must be certified to very stringent criteria such as Common Criteria EAL 6/7 and DCID 6/3, and that many previous information-flow analyses (based on type systems) are too weak to specify these systems; the notion of conditional information flow is needed. We have also explained [2] that in these real applications, one must be able to trace information flow through individual array elements, rather than “contaminating” the entire array whenever an assignment is done. SPARK (a safety-critical subset of Ada) is being used by various organizations, including Rockwell Collins3 and the US National Security Agency (NSA) [3], to engineer information assurance systems including cryptographic controllers, network guards, and key management systems. To guarantee analyzability and conformance to embedded system resource bounds, SPARK does not include pointers and heap-based data. Thus, SPARK programs use arrays and for-loops to implement complex data structures. SPARK provides automatically checked procedure annotations that specify information flows (dependences) between procedure inputs and outputs. In the certification process, these annotations play a key role in justifying conformance to information flow and 3 See the 2006 press release at http://212.113.201.96/sparkada/pdfs/praxis_rockwell_final_pr.pdf
separation policies relevant to MILS development; however, the standard SPARK annotation language is too weak to express the flow policies needed to verify/certify many real embedded information assurance applications. Due to the lack of precision in SPARK and other conventional language-based security frameworks, policy adherence arguments are often reduced to informal claims substantiated by manual inspections that are time-consuming, tedious, and error-prone. Some past certification efforts have created models of software code in theorem provers and proved that the models of code comply with security policies. While this strategy can provide high degrees of confidence and support very precise policy declarations, it has the disadvantages of (a) leaving “trust gaps” between source code and models (their correspondence has in past efforts been only manually verified by inspection), (b) requiring intensive manual efforts, and (c) inhibiting developers from proceeding with information flow specification and verification during the development process. In our previous work [2, 1] we extended SPARK’s procedure annotations to conditional information flow and fine-grained treatment of structured data, necessary for the automatic analysis and verification of many programs, and we developed a compositional framework for stating and automatically verifying complex array-oriented and conditional information flow policies using a relational Hoare logic. Although our Secure Information Flow Logic (SIFL) is language-neutral, we have chosen to cast our work as an enhancement to the SPARK information flow framework. Indeed, this work has been inspired by challenge problems provided by our industrial collaborators at Rockwell Collins who are using SPARK on several projects. Here we extend our framework with new functionality to generate machinecheckable proofs of the information-flow properties that it derives. Our framework is much more automated than tactical theorem-proving in a proof assistant. In our framework, engineers work directly with the source code using code contracts to specify/check with greater precision than in conventional language-based information flow frameworks. We believe that most units (e.g., procedures) of real embedded applications can be handled directly by our analysis—and those units that cannot may smoothly be handed off to verification in a proof assistant; the compositional nature of our system will eventually allow the whole system to be checked end-to-end in the proof assistant. Contributions: (a 50+ pages technical report describing the details of the approach, as well as Coq proofs for evidence soundness, is available at [4].) – We enhance our previously developed precondition-generation algorithm for SIFL assertions to emit evidence that program units conform to their (conditional) information flow contracts. This evidence can be viewed as an application of rules of a relational logic for information flow that encodes the algorithm’s reasoning steps. – We provide an implementation of the evidence-emitting precondition generation algorithm for SPARK. – We encode the derived logic in Coq, and prove it sound with respect to an operational semantics for a core subset of SPARK. We thus have a foundational machinechecked proof that whenever our evidence-checker accepts evidence about a program, then that program really does conform to the given information flow policy. – We evaluate the framework on a collection of methods from embedded applications, including applications from industrial research projects. 2
Fig. 1. Structure of Information Flow Evidence Generation and Checking
2
Background
SPARK is a safety critical subset of Ada developed by Altran Praxis and supported by AdaCore. SPARK provides (a) an annotation language for writing functional as well as information-flow software contracts, and (b) automated static analyses and semiautomated proof assistants for proving absence of run-time exceptions, and conformance of code to contracts. SPARK has been used to build a number of high-assurance systems; Altran Praxis is currently using it to implement the next generation of the UK air traffic control system. We are using SPARK due to our strong collaborative ties with Rockwell Collins, who uses SPARK to develop safety and security critical components of a number of embedded systems. Figure 1 illustrates the structure of our SIFL contract checking and evidence generation framework. An Eclipse-based integrated development environment allows programmers to develop information-assurance applications in SPARK. Our logic-based approach allows us to extend the SPARK information-flow contract language to include support for conditional information flow and quantified flow policies that describe flows through individual components of arrays. Behind the scenes, the enhanced SPARK information flow contracts are represented using relational agreement assertions (explained below). Our tool framework includes a precondition generation algorithm for agreement assertions that allows us to infer SIFL contracts or check user-supplied contracts; in the latter mode, preconditions are inferred from postconditions, and then the tool checks that the user-supplied preconditions imply the inferred preconditions. The precondition generation algorithm uses a collection of SMT solvers via the Sireum Topi interface (www.sireum.org). As the precondition generation executes, it builds evidence, in the form of a Coq data structure, that relates postconditions to generated preconditions. This language of evidence is a relational Hoare logic derived from the basic reasoning steps in the algorithm. Coq type checking acts as an “evidence checker” confirming that the evidence emitted by the algorithm is indeed well-formed. The Coq evidence representation is proved sound in Coq wrt. an operational semantics for an imperative language representing core features of SPARK. Thus, given a SPARK program and a SIFL contract, if the contract checking algorithm produces evidence that 3
p r o c e d u r e MACHINE STEP −− INFORMATION FLOW CONTRACT ( F i g u r e 3 ) i s D 0 , D 1 : CHARACTER; begin i f IN 0 RDY and n o t OUT 1 RDY t h e n D 0 : = IN 0 DAT ; IN 0 RDY : = FALSE ; OUT 1 DAT : = D 0 ; OUT 1 RDY : = TRUE ; end i f ; i f IN 1 RDY and n o t OUT 0 RDY t h e n D 1 : = IN 1 DAT ; IN 1 RDY : = FALSE ; OUT 0 DAT : = D 1 ; OUT 0 RDY : = TRUE ; end i f ; end MACHINE STEP ;
Fig. 2. Simple MLS Guard - mailbox mediates communication between partitions. −−# d e r i v e s −−# OUT 0 DAT f r o m −−# IN 1 DAT when −−# ( IN 1 RDY and −−# OUT 0 DAT when −−# ( n o t IN 1 RDY −−# OUT 0 RDY , IN 1 −−# OUT 1 DAT f r o m −−# IN 0 DAT when −−# ( IN 0 RDY and −−# OUT 1 DAT when −−# ( n o t IN 0 RDY −−# OUT 1 RDY , IN 0
n o t OUT 0 RDY ) , o r OUT 0 RDY ) , RDY &
n o t OUT 1 RDY ) , o r OUT 1 RDY ) , RDY
(a)
{IN 1 RDY ∧ ¬OUT 0 RDY ⇒ IN 1 DATn, ¬IN 1 RDY ∨ OUT 0 RDY ⇒ OUT 0 DATn, IN 1 RDYn, OUT 0 RDYn} 1. if IN 1 RDY and not OUT 0 RDY then {IN 1 DATn} 2. DATA 1 := IN 1 DAT; {DATA 1n} 3. IN 1 RDY := false; {DATA 1n} 4. OUT 0 DAT := DATA 1; {OUT 0 DATn} 5. OUT 0 RDY := true; {OUT 0 DATn} 6. end if ; {OUT 0 DATn} (b)
Fig. 3. (a) Fragment of conditional information flow contract. (b) Corresponding derivation with SIFL assertions.
type checks in Coq, we have a machine-checked proof that the program’s behavior (as defined by the operational semantics) conforms to the information-flow contract. Observe that the precondition generator is not part of the trusted code base and hence in principle might fail to produce well-typed evidence, but we are exploring (cf. the end of Sect. 4) an implementation of the precondition generator inside Coq. Once verified, this Gallina implementation cannot fail to produce valid evidence. Figure 2 illustrates the conceptual information flows in a fragment of a simplistic MLS (Multiple Levels of Security) component, described in our earlier work [1]. Rockwell Collins engineers constructed this example to illustrate, to NSA and industry representatives, the specification and verification challenges facing the developers of MLS software. The “Mailbox” component in the center of the diagram mediates communication between two client processes – each running on its own partition in the separation kernel. Client 0 writes data to communicate in the memory segment Input 0 that is shared between Client 0 and the mailbox, then it sets the Input 0 Ready flag. The mailbox process polls its ready flags; when it finds that, e.g., Input 0 Ready is set and Output 1 Ready is cleared (indicating that Client 1 has already consumed data deposited in the Output 1 slot in a previous communication), then it copies the data from Input 0 to Output 1 and clears Input 0 Ready and sets Output 1 Ready. The communication from Client 1 to Client 0 follows a symmetric set of steps. The actions to be taken in each execution frame are encoded in SPARK by the MACHINE STEP procedure of Fig. 2. While upper levels of the MILS architecture require reasoning about lattices of security levels (e.g., unclassified, secret, top secret), the policies of infrastructure compo4
nents such as separation kernels and guard applications usually focus on data separation policies (reasoning about flows between components of program state), and we restrict ourselves to such reasoning in this paper. Figure 3(a) displays a fragment of an information flow contract for the mailbox example written in our contract language that enhances the original SPARK contract language with the ability to specify conditional information flows. This specification states from which input values (and under which conditions) the final values of OUT 0 DAT and OUT 1 DAT are derived. For example, OUT 0 DAT always derives from IN 1 RDY and OUT 0 RDY because these ”guarding variables” determine whether or not the body of the conditional that assigns to OUT 0 DAT is executed, i.e., OUT 0 DAT is control dependent on IN 1 RDY and OUT 0 RDY. In addition, the final value of OUT 0 DAT depends on the initial value of IN 1 DAT when the flag IN 1 RDY is set and the flag OUT 0 RDY is cleared; otherwise, it depends on the initial value of OUT 0 DAT. The derives clauses in SPARK, like most formal specification mechanisms for information flow, are unconditional (e.g., they do not include the when clauses illustrated in Figure 3). Thus, they cannot distinguish the flag variables as guards nor phrase the conditions under which the guards allow information to pass or be blocked. This means that guarding logic, which is central to many security applications including those developed at Rockwell Collins, is completely absent from the checkable specifications in SPARK. In general, the lack of ability to express conditional information flow not only inhibits automatic verification of guarding logic specifications, but also results in imprecision which cascades and builds throughout the specifications in the application. To capture conditional information flow as well as other forms of information that cannot be specified in SPARK, we have been building [1, 2] on a reasoning framework based on conditional agreement assertions, also called 2-assertions, originally introduced by Amtoft and Banerjee [5]. These SIFL assertions are of the form φ ⇒ En, where φ is a boolean expression and E is any kind of expression (to be defined in the next section), which is satisfied by a pair of stores if either at least one of them does not satisfy φ, or they agree on the value of E: Definition 1 s1 &s2 |= φ ⇒ En iff [[E]]s1 = [[E]]s2 whenever s1 |= φ and s2 |= φ. We use θ ∈ twoAssn to range over 2-assertions. For θ = (φ ⇒ En), we call φ the antecedent of θ and write φ = ant(θ), and we call E the consequent of θ and write E = con(θ). We use Θ ∈ P(twoAssn) to range over sets of 2-assertions, with conjunction implicit. Thus, s&s1 |= Θ iff ∀θ ∈ Θ : s&s1 |= θ. We often write En for true ⇒ En, and often write θ for the singleton set {θ}. Fig. 3(b) illustrates a simple derivation using SIFL assertions that answers the question: what is the source of information flowing into variable OUT 0 DAT? The natural way to read the derivation is from the bottom up (since our algorithm works “backwards”). Thus, for OUT 0 DATn to hold after execution of P , we must have DATA 1n before line 4 (since data flows from DATA 1 to OUT 0 DAT), IN 1 DATn before line 2 (since data flows from IN 1 DAT to DATA 1), and before line 1 IN 1 RDYn and OUT 0 RDYn (since they control which branch of the condition is taken), along with conditional assertions. The precondition shows, just as we would expect, that the value of OUT 0 DAT depends unconditionally on IN 1 RDY and OUT 0 RDY, and conditionally on IN 1 DAT and OUT 0 DAT. 5
3
Evidence Representations
One of our primary goals in this paper is to design evidence terms η whose types correspond to triples: if we can establish that η has type {Θ} C {Θ0 } then the command C has information flow property given by precondition Θ and postcondition Θ0 where both are sets of 2-assertions. Intuitively, evidence represents the primary reasoning steps taken in the precondition generation algorithm when constructing a derivation such as the one displayed in Fig. 3(b). We shall need several auxiliary kinds of evidence, described later but summarized below: ` η : {Θ} C {Θ0 } ` ` ` ` 3.1
C
ν : φ ⇐ φ0 ι : φ ⇒ 1 φ0 τ : Θ ⇒2 Θ0 µ : C mods only X
η shows C has information flow pre/post-condition Θ/Θ0 ν shows φ is NPC for φ0 wrt. C ι shows φ logically implies φ0 τ shows the 2-assertions in Θ logically imply Θ0 µ shows C modifies at most X
Preliminaries
We shall now describe our language, sufficient to represent the primary features of SPARK. We shall consider only one-dimensional4 arrays, and model such an array as a total mapping from integers into integers that is zero except for a finite number of places; we do thus not try to model array bounds which is an orthogonal issue and we assume that SPARK development tools have been applied to prove that there are no index range nor arithmetic overflow violations. Basic Syntax. Commands C are given by the abstract syntax C ::= skip | C ; C | assert(B) | x := A | h := H | if B then C else C | while B do C | for q ← 1 to m do C In for q ← 1 to m do C we require q and m to be different identifiers, neither modified by C. We use x (and y, z) to range over scalar identifiers, h to range over array identifiers, and z, w to range over either kind of identifier; we use A to range over arithmetic expressions, B and φ to range over boolean expressions which are also called 1-assertions, H to range over array expressions, and E to range over any kind of expression. Those are given by the syntax A ::= c | x | A op A | H[A] B ::= A bop A | true | false | B ∧ B | B ∨ B | ¬B H ::= h | Z | H{A : A} where c ranges over constants, op ranges over binary arithmetic operators, bop ranges over binary comparison operators, and Z denotes the array which is zero everywhere. Note that an array identifier can be assigned an arbitrary array expression but we will typically only do a one-place update: we use the standard notation h[A0 ] := A as a shorthand for h := h{A0 : A}. 4
Multi-dimensional arrays are supported in the complete SPARK language, but they are not yet supported in our theory nor in our tool implementation.
6
Semantics. A value is just an integer in Int. Thus a store s is a (partial) mapping from scalar identifiers into values, and from array identifiers into total functions in Int → Int where we shall use a to range over members of that function space. Then [[A]]s denotes the value resulting from evaluating A in store s, [[H]]s denotes the function resulting from evaluating H in store s, and [[B]]s denotes the boolean resulting from evaluating B in store s. We we say that s satisfies φ, written s |= φ, iff [[φ]]s = True. Below, we shall list the semantic clauses that deal with arrays (the other clauses are straightforward): [[H[A]]]s = [[H]]s ([[A]]s ) [[H{A0 : A}]]s = [[[H]]s | [[A0 ]]s 7→ [[A]]s ]
[[Z]]s = λn.0
We write s [[C]] s0 if the command C transforms the store s into store s0 . For example, s [[h := H]] s0 iff for some a we have a = [[H]]s and s0 = [s | h 7→ a]. Given C and s, there exists at most one s0 such that s [[C]] s0 holds; if C is a while loop that loops on s or an assert command that fails then no such s0 will exist. In for loops, we allow zero iterations and let the final value of the counter q be one above the bound; then one can prove that a for loop can be expressed as a while loop: for all s and s0 , s [[for q ← 1 to m do C]] s0 iff s [[q := 1 ; Cw ]] s0 where Cw is given by while q ≤ m do (C ; q := q + 1). To analyze for loops, we could thus rely on an analysis for while loops, but we shall present (Sect. 3.6) a specialized analysis of for loops that often gives more precise information than analyzing the equivalent while loop would have done. 3.2
Evidence
We shall provide rules, numbered below from (1) to (12), for inferring judgements of the form ` η : {Θ} C {Θ0 }. Each rule corresponds to an evidence construct and is designed so as to enable the following soundness property: Theorem 2. Assume that ` η : {Θ} C {Θ0 }. Then |= {Θ} C {Θ0 }. Here |= {Θ} C {Θ0 } denotes the desired semantic soundness result: if s1 &s2 |= Θ, and si [[C]] s0i for i = 1, 2, then s01 &s02 |= Θ0 . Also, evidence has unique type: if ` η : {Θi } Ci {Θi0 } for i = 1, 2 then Θ1 = Θ2 , C1 = C2 , and Θ10 = Θ20 . Syntax-Directed Evidence. For each syntactic construct there is a corresponding piece of evidence. For the most basic constructs, the inference rules are listed in Fig. 4. (For space reasons, we omit the evidence for while loops, and we postpone for loops until Sect. 3.6.) We let add∧ B (Θ) denote the result of conjoining B to the antecedent of ∧ ∧ each assertion in Θ: add∧ B (Θ) = {addB (θ) | θ ∈ Θ} where addB ((φ ⇒ En)) = (φ ∧ B) ⇒ En. Before presenting the rule for conditionals, we need to introduce the notion of “necessary precondition” (NPC). We say that φ is a NPC for φ0 wrt. C if whenever s [[C]] s0 and s0 |= φ0 then s |= φ. It is easy to see that the set of NPCs for given C and φ0 forms a Moore family (closed under arbitrary conjunction) and hence there exists a smallest (strongest) NPC, which is equal to wp(C, φ0 ) with wp denoting “weakest precondition” (satisfied by a store s if there exists s0 with s [[C]] s0 and s0 |= φ0 ). It may be infeasible 7
(1)
` SkipE(Θ) : {Θ} skip {Θ} ` η1 : {Θ1 } C1 {Θ} ` η2 : {Θ} C2 {Θ2 } ` SeqE(η1 , η2 ) : {Θ1 } C1 ; C2 {Θ2 }
(2) (3)
` AssignE(Θ, x, A) : {Θ[A/x]} x := A {Θ}
(4)
` HAssignE(Θ, h, H) : {Θ[H/h]} h := H {Θ}
(5)
` AssertE(Θ, B) : {add∧ B (Θ)} assert(B) {Θ} Fig. 4. Simple rules for syntax-directed evidence.
to compute wp(C, φ0 ) exactly but then any weaker assertion (and trivially true) can be used as NPC. We use ν to range over evidence for NPC, described in Sect. 3.3. The general rule for a conditional C = if B then C1 else C2 is C
Θ = {φ ⇒ En} ` ηi : {Θi } Ci {Θ} (i = 1, 2) ` ν : φ0 ⇐ φ (6) ∧ ` CondE(η1 , η2 , ν, B) : {add∧ (Θ ) ∪ add (Θ ) ∪ {φ 1 2 0 ⇒ Bn}} C {Θ} B ¬B which demands the postcondition to be a singleton; if not, we decompose it and then recombine using the UnionE evidence construct (9). The assertion φ0 ⇒ Bn occurring in the precondition expresses that if two runs must agree on the consequent E then they must also agree on the test B; this may be too restrictive if E is not modified by either branch in which case we can instead use the ConseqNotModE evidence construct (11). We now look back at the derivation in Fig. 3(b). The analysis of the command in line 4 was done using rule (3), with x = OUT 0 DAT and A = DATA 1 and Θ = OUT 0 DATn, giving the precondition Θ[A/x] = DATA 1n. The analysis of the conditional in line 1 was done using rule (6), with B = (IN 1 RDY ∧ ¬OUT 0 RDY) and using evidence η1 for the analysis of C1 (the lines 2–5) and evidence η2 for the analysis of C2 (skip), both with postcondition Θ = OUT 0 DATn. From Θ1 = IN 1 DATn and Θ2 = OUT 0 DATn, and from true being a NPC for true, we get the precondition Θ0 = { (true ∧ (IN 1 RDY ∧ ¬OUT 0 RDY)) ⇒ IN 1 DATn, (true ∧ ¬(IN 1 RDY ∧ ¬OUT 0 RDY)) ⇒ OUT 0 DATn, true ⇒ (IN 1 RDY ∧ ¬OUT 0 RDY)n} Non-Syntax Directed Evidence. Some additional evidence constructs are given by the inference rules listed in Fig. 5 which we shall now explain and motivate. The rules (7,8) allow us to strengthen the precondition or weaken the postcondition; here τ is evidence (described in Sect. 3.3) for 2-implication: if ` τ : Θ ⇒2 Θ0 then Θ logically implies Θ0 (that is, for all stores s1 , s2 , if s1 &s2 |= Θ then s1 &s2 |= Θ0 ). Two derivations may be combined using (9) which can trivially be generalized to an evidence construct combining an arbitrary number of elements: UnionE(η1 . . . ηn ). The rule (10) allows a simple treatment of 2-assertions when no identifier is modified; it uses evidence µ for not-modification: if ` µ : C mods only X then all identi8
` η : {Θ00 } C {Θ0 } ` τ : Θ ⇒2 Θ00 ` PreImplyE(τ, η) : {Θ} C {Θ0 }
(7)
` η : {Θ} C {Θ00 } ` τ : Θ00 ⇒2 Θ0 ` PostImplyE(η, τ ) : {Θ} C {Θ0 }
(8)
` η1 : {Θ1 } C {Θ10 } ` η2 : {Θ2 } C {Θ20 } ` UnionE(η1 , η2 ) : {Θ1 ∪ Θ2 } C {Θ10 ∪ Θ20 }
(9)
` µ : C mods only X fv(Θ) ∩ X = ∅ ` NotModE(µ, Θ) : {Θ} C {Θ}
(10)
C
` ν : φ ⇐ φ0 fv(E) ∩ X = ∅ ` µ : C mods only X ` ConseqNotModE(µ, ν, E) : {φ ⇒ En} C {φ0 ⇒ En}
(11)
C
` η : {Θ} C {Θ0 } ` ν : φ ⇐ φ0 ∧ 0 ` AntecStrongerE(η, ν) : {addφ (Θ)} C {add∧ φ0 (Θ )}
(12)
Fig. 5. Rules for non-syntax-directed evidence.
fiers5 possibly modified by C are included in X. We shall not represent such evidence explicitly, since not-modification is a syntactic property which can be checked easily by a simple Gallina function in Coq. Again looking back at the derivation in Fig. 3(b), we observe that line 5 could have been analyzed using rule (3) but can also be analyzed using rule (10) which is particularly powerful if applied to a whole block of code. For example, for the program in Figure 2, the precondition (shown in Figure 3) of the second conditional does not contain any identifiers that are modified by the first conditional, and hence by a single application of (10) can be shown to be also the precondition of the whole program. Another rule (11) addresses the more general case where antecedents, but not consequents, may be modified. We then need to ensure that whenever two post-states are required to agree on the consequent, also the two pre-states are required to agree on the consequent. This is expressed using the notion of NPC, which is also used in (12) to allow us to make pre-and postconditions “more conditional”, by strengthening the antecedents.
3.3
Auxiliary Evidence
Evidence for 2-Implication. In order to justify the simplication of assertions, or showing that a user-supplied precondition is correct in that it implies the precondition generated by our inference algorithm, we need evidence that a given assertion set logically implies another assertion set. Such evidence can be built using a number of constructs (more might be added) whose inference rules are listed in Fig. 6. 5
If C modifies just one entry of h then h has to be included in X. This may seem very imprecise, but we shall present (in Sect. 3.6) an analysis that in many cases does allow us to get precise information about how individual array elements are affected by for loops.
9
Θ ⊇ Θ0 ` Superset2I(Θ, Θ0 ) : Θ ⇒2 Θ0
(13)
for all φ ⇒ En ∈ Θ, there exists no s with s |= φ ` Vacuous2I(Θ) : ∅ ⇒2 Θ
(14)
fv(E) = ∅ ` Const2I(E, φ) : ∅ ⇒2 (φ ⇒ En)
(15)
` τ1 : Θ1 ⇒2 Θ10 ` τ2 : Θ2 ⇒2 Θ20 ` Union2I(τ1 , τ2 ) : Θ1 ∪ Θ2 ⇒2 Θ10 ∪ Θ20
(16)
` τ1 : Θ1 ⇒2 Θ10
` τ 2 : Θ2 ⇒2 Θ 0 Θ = (Θ2 \ Θ10 ) ∪ Θ1 ` Trans2I(τ1 , τ2 ) : Θ ⇒2 Θ0
(17)
` ι : φ ⇒1 φ0 ` Contravar2I(ι, E) : {φ0 ⇒ En} ⇒2 {φ ⇒ En}
(18)
E is of the form E1 op E2 or E1 bop E2 or E1 ∧ E2 or E1 ∨ E2 or ¬E1 ` BinOp2I(E, φ) : {φ ⇒ E1 n, φ ⇒ E2 n} ⇒2 {φ ⇒ En}
(19)
Fig. 6. Rules for 2-implication evidence.
Here (13) says that any set of 2-assertions logically implies a smaller set, while (14) allows us to discard (replace by the empty set) 2-assertions that are vacuously true, and (15) allows us to discard 2-assertions whose consequent are constants. Derivations can be combined “horizontally” by (16) which easily can be generalized to take an arbitrary number of arguments, and “vertically” by (17) which as a special case (when Θ10 = Θ2 ) has the “standard” transitivity rule. The rule (18) allows us to lift simplications on antecedents to simplifications on 2-assertions, and expresses that 2-implication is contravariant in the antecedent; here ι ranges over evidence (described later) for logical implication: if ` ι : φ ⇒1 φ0 then φ logically implies φ0 (that is, whenever s |= φ then also s |= φ0 ). For example, for the precondition Θ0 computed above, one can easily verify that true ∧ (IN 1 RDY ∧ ¬OUT 0 RDY) ⇒ IN 1 RDY ∧ ¬OUT 0 RDY true ∧ ¬(IN 1 RDY ∧ ¬OUT 0 RDY) ⇒ ¬(IN 1 RDY ∧ ¬OUT 0 RDY) and hence rule (18), together with rule (16), allows us to simplify Θ0 to Θ00 = { (IN 1 RDY ∧ ¬OUT 0 RDY) ⇒ IN 1 DATn, ¬(IN 1 RDY ∧ ¬OUT 0 RDY) ⇒ OUT 0 DATn, (IN 1 RDY ∧ ¬OUT 0 RDY)n}. Complex consequents can be decomposed using (19). For example, we can split the last consequent of Θ00 to reach the final precondition of the code segment: Θ000 = { (IN 1 RDY ∧ ¬OUT 0 RDY) ⇒ IN 1 DATn, ¬(IN 1 RDY ∧ ¬OUT 0 RDY) ⇒ OUT 0 DATn, IN 1 RDYn, OUT 0 RDYn}. Evidence for Necessary Precondition. Recall that we need, for the rules (6) and (11) and C (12), evidence ν such that if ` ν : φ ⇐ φ0 then whenever s [[C]] s0 and s0 |= φ0 then 10
also s |= φ. To build such evidence, we use one construct for each language construct, with one extra to make a shortcut when the command does not modify the 1-assertion. Two typical rules are listed below: C
C
` ν1 : φ1 ⇐1 φ
` ν2 : φ2 ⇐2 φ
` CondNPC(ν1 , ν2 , B) : (φ1 ∧ B) ∨ (φ2 ∧ ¬B) C
` ν : φ0 ⇐ φ
` ι0 : φ0 ∧ B ⇒1 φ
` WhileNPC(ν, ι0 , ι1 ) : φ
if B then C1 else C2
` ι1 : while B do C ⇐
⇐
(20) φ
φ0 ∧ ¬B ⇒1 φ
(21)
φ0
Evidence for Logical Implication. We have two kinds of evidence: 1. rules that resemble axiomatization of propositional logic; 2. the evidence CheckLI(φ, φ0 ) which says that “a decision procedure has verified that φ logically implies φ0 ”. 3.4
Manipulating 2-Assertions
It is often useful to transform a set of 2-assertions into a set which is “simpler” and which satisfies certain properties; for example, to analyze a while loop (or a method call), all (modified) consequents in the postcondition must be identifiers. Ideally, we would like the result to be equivalent to the original (like Θ00 to Θ0 in the previous example), but often it will be strictly stronger (as is Θ000 when (E1 ∧ ¬E2 )n is decomposed into E1 n and E2 n). Hence6 our overall approach does in general not calculate the weakest precondition. We have written an algorithm that transforms a set of 2-assertions Θ0 into a more manageable form, as may be required by the command C for which Θ0 is the postcondition, while producing evidence that the result is at least as strong as the original. The algorithm as input also takes a set X, to be thought of as the identifiers that are modified by C. The algorithm returns τ , and also Θu (assertions whose consequents are unmodified) and Θn , such that ` τ : Θn ∪ Θu ⇒2 Θ0 and – all array expressions inside Θn and Θu are identifiers; thus there are no occurrences of Z or H{A0 : A} which may be introduced by rule (4) but hamper readability; – all assertions in Θn are of the form φ ⇒ wn or of the form φ ⇒ h[A]n, as is required if C is a while or for loop; – if φ ⇒ En ∈ Θu then fv(E) ∩ X = ∅; – if φ ⇒ h[A]n ∈ Θn then fv(A) ∩ X = ∅; – if φ1 ⇒ En ∈ Θn and φ2 ⇒ En ∈ Θn then φ1 = φ2 . 3.5
Generating Evidence
For any command and postcondition, it is possible to compute a precondition, together with evidence that the resulting triple is indeed semantically sound. To help with that, 6
Another reason is the approximation needed to efficiently handle loops.
11
we need an algorithm NpcEvdGen generating evidence for necessary precondition. For the nonlooping constructs, such an algorithm is straightforward to write, but for a while loop a precise analysis involves guessing an invariant; we expect that we might be able to use some of the emerging tools for finding loop invariants even though our perspective is dual. We shall first present a nondeterministic algorithm EvdGen such that for all commands C (without while and for) and postconditions Θ, a call EvdGen(C, Θ) returns evidence η such that ` η : {Θ0 } C {Θ} for some Θ0 . Below we list the possible actions of EvdGen, the applicability of which depend on the form of C and/or Θ. Decompose Postcondition. The enabling condition is that Θ has at least two elements. Let Θ10 , Θ20 be nonempty disjoint sets such that Θ = Θ10 ∪ Θ20 . For each i ∈ {1..2}, recursively call EvdGen(C, Θi0 ) to produce ηi such that ` ηi : {Θi } C {Θi0 } for some Θi . Define η = UnionE(η1 , η2 ); we thus have ` η : {Θ1 ∪ Θ2 } C {Θ}. Push Through Postcondition. The enabling condition is that fv(Θ)∩X = ∅ where X is such that ` µ : C mods only X for some µ. Then we can define η = NotModE(µ, Θ) and achieve ` η : {Θ} C {Θ}. Push Through Consequent of Postcondition. The enabling condition is that Θ is a singleton {φ0 ⇒ En}, and that fv(E) ∩ X = ∅ with X such that ` µ : C mods only X C for some µ. Let ν = NpcEvdGen(C, φ0 ). There thus exists φ with ` ν : φ ⇐ φ0 . Now define η = ConseqNotModE(µ, ν, E) and get ` η : {φ ⇒ En} C {Θ}. Syntax-Directed Actions. Two typical cases are as follows: If C = x := A then EvdGen(C, Θ) returns AssignE(Θ, x, A). If C = if B then C1 else C2 , and Θ is a singleton {φ0 ⇒ En}, we for each i ∈ {1, 2} recursively call EvdGen(Ci , Θ) to produce ηi such that ` ηi : {Θi } Ci {Θ} for some Θi , and call NpcEvdGen(C, φ0 ) to compute ν such that for some φ we have C ` ν : φ ⇐ φ0 . We then return η = CondE(η1 , η2 , ν, B) which by the typing rules satisfies the desired ` η : {Θ0 } C {Θ} for some Θ0 . Properties of EvdGen. The precondition Θ0 for an assignment statement x := A depends only on the postcondition Θ, but not on the kind of evidence that was chosen; we will always have Θ0 = Θ[A/x]. However, we do not have a similar result for conditionals C = if B then C1 else C2 : for example, if E is not modified by C then EvdGen(C, Θ) may either produce evidence of the form CondE(η1 , η2 , ν, B) whose type has a precondition containing an assertion with B as consequent, or evidence of the form ConseqNotModE(µ, ν, E) whose type does not have that property. We thus need to restrict the non-determinism present in the definition of EvdGen. For conditionals, syntax-directed evidence CondE should only be generated when the postcondition is a singleton whose consequent has been modified. For other constructs, we may be free either to split the postcondition or to apply the syntax-directed rules directly. The advantage of the former is that then the evidence provides fine-grained information about which preconditions come from which postconditions. The advantage of the latter is that then the evidence becomes more compact. 12
3.6
For Loops
We shall introduce 3 extra evidence constructs: η ::= . . . | ForAsWhileE(η, µ) | ForPolyE(. . .) | InstantiateE(η, µ, A) Here ForAsWhileE just analyzes a for loop as a while loop; this involves splitting an assertion φ ⇒ h[A]n into φ ⇒ hn and φ ⇒ An and thus we lose any information about individual array elements. We shall now present a method, first given in [2] which contains further motivation and examples, that in certain cases allows us to reason about individual array elements. To do so in a finite way, we need the concept of polymorphic identifiers. Those may occur in pre/post conditions but never in commands; we shall use u to range over them. We shall extend |= {Θ} C {Θ0 } to cover the case where Θ and/or Θ0 contains a polymorphic identifier u: then |= {Θ} C {Θ0 } holds iff |= {Θ[c/u]} C {Θ0 [c/u]} holds for all constants c. We have the inference rule fv(A) ∩ X = ∅ ` η : {Θ} C {Θ0 } ` µ : C mods only X ` InstantiateE(η, µ, A) : {{An} ∪ Θ[A/u]} C {Θ0 [A/u]} which is applicable not just when C is a for-loop. We shall design ForPolyE such that ` ForPolyE(. . .) : {Θ} for q ← 1 to m do C {h[u]} if certain requirements, to be motivated and detailed below, are fulfilled. Let µ with ` µ : C mods only X where q, m ∈ / X, and a polymorphic identifier u, be given. Index Sets. There must exist a set of arithmetic expressions, {Aj | j ∈ J} with fv(Aj )∩ X = ∅, such that for all array assignments h := H in C there exists j and A such that H = h{Aj : A} (and thus the assignment may be written h[Aj ] := A). Linearity. We shall assume, as is very often the case for practical applications, that each Aj is a linear function in q. That is, there exists integer constants (or identifiers not in X) bj 6= 0 and kj such that Aj is given by bj q + kj . Then for each j ∈ J, we define u − kj bj φj = (u − kj ) mod bj = 0 ∧ u − kj ≥ bj ∧ u − kj ≤ mbj
A0j =
with the intention that whereas Aj computes an index value from the iteration number, A0j computes the iteration number from the index value while φj denotes the set of index values, as formalized by the following properties (to be suitably quantified): 1. if c = [[Aj ]][s|q7→i] for i ∈ {1 . . . s(m)} then [[A0j [c/u]]]s = i. 2. s |= φj [c/u] iff c ∈ {[[Aj ]][s|q7→i] | i ∈ {1 . . . s(m)}}. 13
Local Preconditions. For all j ∈ J, there must exists ηj and Θj such that 1. ` ηj : {Θj } C {h[Aj ]n} with u 6= fv(Θj ) 2. fv(Θj ) ∩ X ⊆ {h} 3. if h occurs in Θj it is in a context of the form h[A] where for all j 0 ∈ J, all i, i0 ∈ {1 . . . s(m)}, all stores s: if [[A]][s|q7→i] = [[Aj 0 ]][s|q7→i0 ] then i ≤ i0 . Requirement 2 excludes loop-carried dependencies in the body such as h[q] := x; x := y in which case h[1] depends on the initial value of x while h[2], h[3],. . . depends on the initial value of y. Requirement 3 is designed to exclude loop-carried dependencies within the array h; it is possible to list some cases that are easily checkable and which each is a sufficient condition for this requirement to hold: 1. when J is a singleton {j}, and the only occurrence of h in Θj is in the context h[bq + k] where b ≥ bj and b ≥ 1 and k ≥ kj ; 2. when bj = 1 for all j ∈ J, and if h occurs in some Θj it is in a context of the form h[A] with A of the form q + c where c satisfies: ∀j ∈ J : c ≥ kj or c ≤ kj − m. Both conditions will accept a loop body containing (only) h[q] := h[q + 1] and reject a loop body containing h[q] := h[q − 1]. We are now ready to construct the precondition Θ, as the union of BOUND {true ⇒ mn} INDEX {trueV⇒ wn | w ∈ ∪j∈J fv(Aj ) \ {q}} OUTSIDE { j∈J ¬φj ⇒ h[u]n} 0 UPDATED for each j ∈ J, the set add∧ φj (Θj [Aj /q]). Here BOUND ensures that the two runs agree on the number of iterations, while INDEX ensures that the two runs agree on which indices are updated. For an index that might not be updated, the two runs must agree on the original value, as expressed by OUTSIDE. But for an index that may be updated, we apply the computed preconditions, as expressed by UPDATED. Example. As in [2], we can analyze a for loop whose body swaps7 h[q] and h[q + m] and where we therefore have J = {1, 2}, A1 = q, A2 = q + m, b1 = b2 = 1, k1 = 0, and k2 = m. We compute φ1 = u ≥ 1 ∧ u ≤ m, φ2 = u − m ≥ 1 ∧ u − m ≤ m, A01 = u, and A02 = u − m; we also get Θ1 = {h[q + m]n} and Θ2 = {h[q]n}. The abovementioned sufficient condition 2 amounts to the 4 claims 0 ≥ 0 or 0 ≤ 0 − m
0 ≥ m or 0 ≤ m − m
m ≥ 0 or m ≤ 0 − m
m ≥ m or m ≤ m − m
which are all easily verified. Hence we may generate the expected precondition { mn, (u < 1 ∨ u > 2m) ⇒ h[u]n, 1 ≤ u ≤ m ⇒ h[u + m]n, m + 1 ≤ u ≤ 2m ⇒ h[u − m]n }. 7
Since each position participates in at most one swap there is no loop-carried dependency.
14
4
Machine-Checked Evidence and Soundness Overview
We now discuss how the evidence constructors of the previous section are represented and proven sound in Coq. Our technical report [4] provides detailed correctness proofs for all the evidence constructors. At the time of writing, we have completed the corresponding formalization of the proofs in Coq for assignments, conditionals, arrays, polymorphic tuples and almost all of the for loop, and we do not anticipate problems completing the remaining soundness proofs (remainder of for plus while). 4.1
Representation of evidence
Our representation of evidence is based on deep embeddings of the language and the logic in Coq, using type-respecting categories of variables (e.g. SkalVar), expressions Expr (separated into arithmetic, boolean and array expressions AExpr, BExpr, HExpr), and Commands. Based on these definitions, we define expression evaluation and the operational semantics in direct correspondence to the definitions in Section 3; for example, Opsem s C t means that command C transforms state s into state t. A 2-assertion is made up of a BExpr and an Expr: Definition TwoAssn :=prod BExpr Expr.
Abbreviating the type of lists of 2-assertions as TwoAssns, we introduce the inductive type of pre/postconditions as Inductive assns := | Assns : TwoAssns → TwoAssns → assns | APoly : (AExpr → assns)→ assns.
Here, the first constructor carries a precondition/postcondition pair and will be used for standard triples. The second constructor allows the assertions to be parametrized by a shared variable, and will be required for implementing for-loops. Evidence takes the form of an inductive proposition with constructors corresponding to the rules in Figures 4 and 5. Inductive TEvid (X: list SkalVar) : Command → assns → Prop := | TSkipE ... | TAAssignE ... | TCondE ... ...
For example, the constructor for TAAssignE, | TAAssignE : ∀ Θ x A, TEvid X (Assign x (AExp A)) (Assns(TwoAssnsSubstA Θ x A) Θ)
is a direct translation of rule (3), where TwoAssnsSubstA Θ x A represents the substitution (code omitted) of arithmetic expression A for x in the 2-assertion Θ. Evidence for conditionals (rule 6) is translated similarly; the rule takes three explicit arguments of evidence type, one for each possible outcome of the branch, and one for the necessary precondition. | TCondEN: ∀ {Θ1 φ E Θ2 Θ0 C1 C2 φ0 } (η1 : TEvid X C1 (Assns Θ1 [(φ, E)])) (η2 : TEvid X C2 (Assns Θ2 [(φ, E)])) B (ν : NPCEvid X φ0 (Cond B C1 C2) φ), andIntoTheta Θ1 B ++ andIntoTheta Θ2 (NotExpr B) ++ [(φ0 , BExp B)] = Θ0 → allVarsIn (BFv φ0 ) X =true → TEvid X (Cond B C1 C2) (Assns Θ0 [(φ, E)])
15
We have evidence forms declared for all syntax but while and for loops, and we have also declared constructors for all the rules mentioned in Fig. 5. There are similar inductive types and accompanying soundness proofs for NPC, 1-implication, 2-implication and expression equivalence. Rules representing decision-procedure-validated evidence such as CheckLI are currently axiomatically admitted, although future work will aim to verify the output of decision procedures using methods similar to [6]. 4.2
Soundness
The soundness of constructed evidence terms rests on the interpretation of 2-assertions Definition twoSatisfies(s1 s2 :State) (asn: TwoAssn) := let (φ, E) = asn in BEval φ s1 = Some true → BEval φ s2 = Some true → (Eval E s1 = Eval E s2 )
which corresponds to the informal definition given earlier in the paper.8 The interpretation is naturally extended to lists of 2-assertions (where Forall is the universal quantification over list elements, taken from the Coq library): Definition twoAssnsInterpretation (s1 s2 : State) (a:TwoAssns) : Prop := Forall (twoSatisfies s1 s2) a.
We are now ready to model the definition (Sect. 3.2) of the predicate |= {Θ} C {Θ0 }. Definition validHoareTriple (X: list SkalVar) C (asns: assns): Prop := match asns with | Assns pre post ⇒ ∀ s s’ t t’, (∀ x, In x X → ∃ v1 , lookup s x = Some v1 ∧ ∃ v2 , lookup s’ x = Some v2 ) → Opsem s C t → Opsem s’ C t’ → twoAssnsInterpretation s s’ pre → twoAssnsInterpretation t t’ post | ... end.
Our soundness statement (Theorem 2 in Sect. 3) is formulated as follows. Theorem soundness : ∀ X C asns, TEvid X C asns → (validHoareTriple X C asns).
Whenever we apply this soundness result to a defined concrete piece of evidence, the type of the resulting construction explicitly witnesses the validity of the triple. For example, applying the soundness result to a piece of evidence named assignEvid Definition evidSound := soundness
assignEvid.
guarantees our intended security property in that it yields a term of type validHoareTriple X cmd (Assns pre post).
Performing the proof of the soundness theorem constitutes a major engineering task even once all the definitions are set up correctly. As an indication of the effort, the Coq code has around 2300 lines of soundness proof and only 590 lines of trusted definitions; it formalizes a manual correctness proof in [4] which is about 5.5 pages from about 610 lines of LATEX source. 8
This direct correspondence between informal and formal definitions is crucial, as the formal definitions introduced here form part of the trusted code base of our system.
16
In order to address the concern that certificates may be unacceptably large, we have also explored an implementation of the precondition generator inside Coq, in the style of proof-by-reflection. Our precondition generator consists of a Gallina function Fixpoint generatePrecondition (c:Command) (post:TwoAssns) (X: list SkalVar): (TwoAssns ∗ list SkalVar) := ...
with defining clauses that closely resemble Figures 4 and 5. The soundness theorem Theorem generatePreconditionEvidence: ∀ C Y X Z post pre, allVarsIn X Y = true→ (pre, X) = generatePrecondition C post Z → TEvid Y C (Assns pre post).
expresses that any result (pre, X) from a call to generatePrecondition yields evidence for the triple made up from the inferred precondition and the supplied command and postcondition. Thus, evidence terms need not be explicitly constructed, as valid evidence can be constructed automatically for any command and postcondition.
5
Evaluation
We summarize our initial experience in applying our SIFL deduction engine for evidence generation. The SIFL precondition generation algorithm supports assignments, conditionals, arrays, for and while loops, polymorphic flow contracts, and procedure calls. We tested this implementation on procedures from a collection of embedded applications (an Autopilot, a Minepump, a Water Boiler monitor, and a Missile Guidance system – all developed outside of our research group), and a collection of small programs that we developed ourselves to highlight common array idioms that we discovered in information assurance applications. Approximately 6-15 procedures from each of these examples were selected due to having the richest control flow and loop structures. The security-critical sections to be certified from code bases in this domain are often relatively small, e.g., roughly 1000 LOC (non-comment lines of code) for a Rockwell Collins high assurance guard and 3000 LOC for an (undisclosed) device certified by Naval Research Labs researchers [7]. The average LOC per procedure in our examples is 22. In this evaluation, we focused on running the tool in a mode that infers information flow contracts. For each procedure P , and each output variable w, the algorithm analyzes the body wrt. post-condition wn. All experiments were run under JDK 1.6 on a 2x2.6 GHz Quad Core Intel Xeon Mac Pro with 32 GB of RAM. We were most interested in evaluating (a) the size of the generated evidence, (b) the number and structure of assertions in the inferred precondition (for the purpose of minimizing its size), (c) the time required for the algorithm to infer a contract and generate evidence, and (d) the time required for Coq to type-check (i.e., establish the correctness) of the evidence. In the subsequent paragraphs, we shall summarize the outcomes for the first 3 measures (see [4] for detailed data and evidence outputs for all examples we considered); for (d), no procedure required more than 3 seconds. In contrast to other proof-carrying code applications such as mobile code, contract size is not as significant an issue in our context since contracts are not being transmitted or checked at run-time. Instead, the focus is on leveraging contracts for greater assurance in the certification process. We consider three different metrics for the size of evidence: the number of evidence constructors, the total number of Coq abstract syntax 17
tree (AST) nodes (which captures the size of assertion expressions and program ASTs in the evidence), and the number of bytes in the text file holding the evidence. For the full version of the mailbox example of Section 2 which has 24 LOC, the generated evidence includes 318 evidence constructors, 3729 Coq AST nodes, and 89 KB of text. For a slightly longer example (30 LOC) from the autopilot codebase that has one of the longest analysis run-times, the presence of four conditions leads to larger generated evidence due to more conditional preconditions: 326 evidence constructors, 19461 Coq AST nodes, and 211 KB of text. Our current contract representation includes the syntax tree of the program as well as the 2-assertions generated between each command. Thus, many program expressions are repeated numerous times across the evidence structure for a procedure. There is significant opportunity to optimize the size by, e.g., common subexpression elimination. Initial examination of the generated preconditions identified a number of minimization opportunities (e.g., simplifying assertions of the form true ∧ φ ⇒ En to φ ⇒ En or removing a 2-assertion when it is implied by another within the precondition). In the mailbox example, there are 52 AST nodes in the precondition for OUT 0 DAT without minimization and 28 when minimization applied. Our current strategy for expressing minimization includes many fine-grained reasoning steps using the rules of Figure 6. Thus, the number of evidence constructors in the OUT 0 DAT derivation actually increases from 25 to 83. Potentially, this can be reduced by making the Coq checker “smarter” by having it do more manipulation of logical expressions without direct instruction from the evidence generator. The time required for processing a procedure ranged from 3 to 140 secs. As previously discussed, the correctness of minimization of assertions was validated with calls to SMT solvers. The repeated calls to the SMT solvers were the dominating factor in the time required to infer contracts, and we have not yet devoted any effort to optimize this. Many of the minimization steps can be implemented using simple syntactic checks, and we are in the process of implementing and proving correct a minimizer in Coq that will allow us to dramatically reduce the number of SMT solver calls. Experimental results from our earlier work [1] in which we used only syntactic scans to minimize showed that inference for almost all the procedures could be completed in less than a second. Our approach is compositional which greatly aids scalability when considering the overall time requirements for a complete application.
6
Related Work
Bergeretti and Carr´e [8] present a compositional method for inferring and checking dependencies among variables in SPARK programs. That approach is flow-sensitive, unlike most security type systems [9] that rely on assigning a security level (“high” or “low”) to each variable. Chapman and Hilton [10] present an approach, now implemented in the latest SPARK release, for extending SPARK information flow contracts with lattices of security levels and enhancing the SPARK Examiner accordingly. Agreement assertions (inherently flow-sensitive) were introduced in [11] and later extended in [5] to introduce conditional agreement assertions (for a heap-manipulating language). In [1] that approach was applied to the (heap-free) SPARK setting and 18
worked out extensively, with an algorithm for computing loop invariants and with reports from an implementation; then arrays were handled in subsequent work [2]. Our evidence-checker is an example of the proof-carrying code paradigm [12]; it is foundational [13] in that the rules used by the evidence checker are themselves proved sound with a machine-checked proof. Although the original PCC generated proofs mainly via type-checking, more recently the PCC paradigm has been extended to policies of mobile code concerning resource consumption and information flow [14, 15]. Certificate generation for such systems was obtained by formalizing static analyses (refined type systems or abstract interpretation frameworks) either directly at the level of virtual machine code, or by providing compiler-mediated interpretations of appropriate high-level analyses [16–18]. Wildmoser and Nipkow developed verified VCGens for bytecode for a deeply embedded assertion language for bytecode [19]. In the context of abstract-interpretation-based PCC, Besson et al. [20] employed certificates in the form of strategies for (re-)recomputing fixed points at the consumer side. Techniques for reducing the size of evidence representations using oracles [21] and small witnesses [22] developed into reflective PCC [15] where the evidence checker (or even a partial evidence reconstruction algorithm) is implemented in the tactic language of the proof assistant, and proved sound by the principle of reflection. We have found (cf. Section 4) that our current evidence checker permits this approach. In addition to direct justification of static analyses wrt. operational semantics, several of the above-mentioned formalizations employ program logics and/or VCGen’s as intermediate representations. In order to employ these for the verification of information flow, the relational nature of information flow security must be taken into account, either by direct use of relational program logics [23], or by suitable encodings [24, 25] in nonrelational logics based on the idea of self-composition [26, 27]. In contrast to (typically not foundationally validated) efforts to relax baseline security policies to more permissive notions (e.g. declassification), our conditional information flow analysis aims to improve the precision and trustworthiness of static analysis results for the baseline policy, in the setting of an existing domain-specific tool flow methodology. Dufay, Felty, and Matwin [28] and Terauchi and Aiken [29] provide tool support for the verification of noninterference based on self-composition. In [28], the Krakatoa/Why verification framework is extended by variable-agreement assertions and corresponding loop annotations, and emits verification conditions in Coq that are typically interactively discharged by the user. In [29], information inherent in type systems for noninterference is exploited to limit the application of the program-duplication to smaller subphrases, obtaining self-composed programs that are better amenable to fully automated state-space-exploring techniques. Neither system produces foundationally validated and independently checkable artefacts of evidence relating the source program to user-level specifications, and it is at present unclear whether either could be extended to support conditional information flow policies.
7
Conclusions and Future Work
By implementing an evidence emitting algorithm and an associated evidence checking framework in Coq, we have provided a solution that allows developers to work 19
at source level to specify/check rich information flow contracts while still enabling machine-checked proofs that source code implementations conform to contracts. This work puts in place a crucial element of our larger vision for end-to-end security assurance – namely, the ability eventually to leverage our other work on formally verified compilers [30] to provide a tool chain that enables us to prove that deployed executable code conforms to complex information flow policies stated as source-level contracts. Our next steps include adding a higher-level information flow policy specification language on top of our framework, enlarging the subset of SPARK that our tools can handle, and engineering a connection to the CompCert verified compiler stack [31]. We are also working with our industrial partners to evaluate our tools on additional examples. Acknowledgements: This work is funded in part by the Air Force Office of Scientific Research (FA9550-09-1-0138), and by Rockwell Collins whose engineers David Hardin, David Greve, Ray Richards, and Matt Wilding we would like to thank for feedback on earlier versions of this work. We would also like to thank the anonymous referees for useful and detailed comments.
References 1. Amtoft, T., Hatcliff, J., Rodr´ıguez, E., Robby, Hoag, J., Greve, D.: Specification and checking of software contracts for conditional information flow. In: 15th International Symposium on Formal Methods (FM’08). Volume 5014 of LNCS., Springer (2008) 229–245 2. Amtoft, T., Hatcliff, J., Rodr´ıguez, E.: Precise and automated contract-based reasoning for verification and certification of information flow properties of programs with arrays. In Gordon, A.D., ed.: ESOP’10. Volume 6012 of LNCS. (2010) 43–63 3. Barnes, J., Chapman, R., Johnson, R., Widmaier, J., Cooper, D., Everett, B.: Engineering the Tokeneer enclave protection software. In: Proceedings of the IEEE International Symposium on Secure Software Engineering (ISSSE 2006), IEEE Press (2006) 4. Amtoft, T., Dodds, J., Zhang, Z., Appel, A., Beringer, L., Hatcliff, J., Ou, X., Cousino, A.: A certificate infrastructure for machine-checked proofs of conditional information flow (2012) Available from: (http://santos.cis.ksu.edu/papers/ Amtoft-al-POST12/). 5. Amtoft, T., Banerjee, A.: Verification condition generation for conditional information flow. In: 5th ACM Workshop on Formal Methods in Security Engineering (FMSE’07), George Mason University, ACM (2007) 2–11 6. Armand, M., Faure, G., Gregoire, B., Keller, C., Thery, L., Werner, B.: A modular integration of SAT/SMT solvers to Coq through proof witnesses. In Jouannaud, J.P., Shao, Z., eds.: Certified Programs and Proofs (CPP’11). Volume 7086 of LNCS., Springer (2011) 7. Heitmeyer, C.L., Archer, M., Leonard, E.I., McLean, J.: Formal specification and verification of data separation in a separation kernel for an embedded system. In: 13th ACM Conference on Computer and Communications Security (CCS’06). (2006) 346–355 8. Bergeretti, J.F., Carr´e, B.A.: Information-flow and data-flow analysis of while-programs. ACM Transactions on Programming Languages and Systems 7 (1985) 37–61 9. Volpano, D.M., Smith, G.: A type-based approach to program security. In: Proceedings of the 7th International Joint Conference CAAP/FASE on Theory and Practice of Software Development. Volume 1214 of LNCS., Springer (1997) 607–621 10. Chapman, R., Hilton, A.: Enforcing security and safety models with an information flow analysis tool. ACM SIGAda Ada Letters XXIV (2004) 39–46
20
11. Amtoft, T., Banerjee, A.: Information flow analysis in logical form. In Giacobazzi, R., ed.: SAS 2004. Volume 3148 of LNCS., Springer (2004) 100–115 12. Necula, G.C.: Proof-carrying code. In: POPL’97, ACM Press (1997) 106–119 13. Appel, A.W.: Foundational proof-carrying code. In: LICS 2001, IEEE Computer Society (2001) 14. Sannella, D., Hofmann, M., Aspinall, D., Gilmore, S., Stark, I., Beringer, L., Loidl, H.W., MacKenzie, K., Momigliano, A., Shkaravska, O.: Mobile resource guarantees. In van Eekelen, M.C.J.D., ed.: Revised Selected Papers from the Sixth Symposium on Trends in Functional Programming (TFP 2005), Intellect (2007) 211–226 15. Barthe, G., Cr´egut, P., Gr´egoire, B., Jensen, T.P., Pichardie, D.: The MOBIUS proof carrying code infrastructure. In: Formal Methods for Components and Objects, 6th International Symposium (FMCO 2007). Volume 5382 of LNCS., Springer (2008) 1–24 16. Beringer, L., Hofmann, M., Momigliano, A., Shkaravska, O.: Automatic certification of heap consumption. [32] 347–362 17. Albert, E., Puebla, G., Hermenegildo, M.V.: Abstraction-carrying code. [32] 380–397 18. Barthe, G., Pichardie, D., Rezk, T.: A certified lightweight non-interference Java bytecode verifier. In Nicola, R.D., ed.: ESOP 2007. Volume 4421 of LNCS., Springer (2007) 125–140 19. Wildmoser, M., Nipkow, T.: Asserting bytecode safety. In Sagiv, M., ed.: Proceedings of ESOP 2005. Volume 3444 of LNCS., Springer (2005) 326–341 20. Besson, F., Jensen, T.P., Pichardie, D.: Proof-carrying code from certified abstract interpretation and fixpoint compression. Theor. Comput. Sci. 364 (2006) 273–291 21. Necula, G.C., Rahul, S.P.: Oracle-based checking of untrusted software. In: POPL’01. (2001) 142–154 22. Wu, D., Appel, A.W., Stump, A.: Foundational proof checkers with small witnesses. In: Proceedings of the 5th International ACM SIGPLAN Conference on Principles and Practice of Declarative Programming (PPDP’03), ACM (2003) 264–274 23. Benton, N.: Simple relational correctness proofs for static analyses and program transformations. In Jones, N.D., Leroy, X., eds.: POPL 2004, ACM (2004) 14–25 24. Beringer, L., Hofmann, M.: Secure information flow and program logics. In: CSF 2007, IEEE Computer Society (2007) 233–248 25. Beringer, L.: Relational decomposition. In: ITP 2011. Volume 6898 of LNCS., Springer (2011) 39–54 ´ H¨ahnle, R., Sands, D.: A theorem proving approach to analysis of secure infor26. Darvas, A., mation flow. In Hutter, D., Ullmann, M., eds.: 2nd International Conference on Security in Pervasive Computing (SPC’05). Volume 3450 of LNCS., Springer (2005) 193–209 27. Barthe, G., D’Argenio, P.R., Rezk, T.: Secure information flow by self-composition. In: 17th IEEE Computer Security Foundations Workshop, (CSFW-17 2004), IEEE Computer Society (2004) 100–114 28. Dufay, G., Felty, A.P., Matwin, S.: Privacy-sensitive information flow with JML. In: 20th International Conference on Automated Deduction (CADE 20). Volume 3632 of LNCS., Springer (2005) 116–130 29. Terauchi, T., Aiken, A.: Secure information flow as a safety problem. In: SAS 2005. Volume 3672 of LNCS. (2005) 352–367 30. Appel, A.W.: Verified software toolchain. In Barthe, G., ed.: ESOP’11. Volume 6602 of LNCS., Springer (2011) 1–17 31. Leroy, X.: Formal certification of a compiler back-end or: programming a compiler with a proof assistant. In: POPL’06. (2006) 42–54 32. Baader, F., Voronkov, A., eds.: Logic for Programming, Artificial Intelligence, and Reasoning, 11th Intl. Conf. (LPAR 2004). In Baader, F., Voronkov, A., eds.: Logic for Programming, Artificial Intelligence, and Reasoning, 11th International Conference (LPAR 2004). Volume 3452 of LNCS., Springer (2005)
21