We want to enforce information security properties such as confidentiality and integrity. The problem is that we can't trust programs. Even if they aren't malicious Trojan horses, programmer mistakes can violate these properties. And conventional, discretionary access control doesn't control information propagation.
We've seen that these information security requirements can be expressed to some degree using the idea of noninterference, which is a mathematical formalization of the notion that there is zero information flow from one set of information (the high information) to another set (the low information). Intuitively, noninterference says that you should not be able to learn anything about the high inputs to a system by observing the low inputs and outputs.
Let us use s to denote a state of the system and [s] to be the behavior of the system starting from state s (we used double brackets in class. If your browser fully supports them, these will have the right appearance: 〚s〛 and ⟦s⟧). Then we can describe the observational capability of the low observer as indistinguishability relations on states or behaviors. Typically these are equivalence relations, but not always. Suppose we write s1 ≈L s2 to mean that the low observer cannot distinguish between s1 and s2, and similarly we write [s1] ≈L [s2] to mean that outputs/behaviors [s1] and [s2] are indistinguishable. Then we can express noninterference as an implication:
This is a very abstract description, and its precise significance depends on how we define the states, the behaviors, and the indistinguishability relations. We might consider states and behaviors to be more or less the same thing, where a behavior [s] for a given state s is simply the final state that results after running the system starting from state s. Another natural choice is to consider the behaviors to be the entire traces of states that are encountered as the system runs.
Channels are conduits for information. Since we want to control how information flows (and hopefully enforce noninterference), it is useful to talk about the different ways this can happen. Butler Lampson classifies channels into covert and overt channels. Overt channels are those that are clearly intended to transmit information. Covert channels are those whose ostensible purpose is not the transmission of information.
Lampson also draws a distinction between storage channels and timing channels. The former transmit information through explicit changes of system state; the latter, by changing the amount of time that events take. Timing channels are almost always covert channels; programs that intentionally transmit information through timing of events must exploit unreliable properties of the system they are running on.
In systems with concurrent threads, it becomes possible to convert storage channels into timing channels and vice versa, using synchronization between different threads.
Suppose we have a program we want to analyze, containing both high and low variables. We will consider two states of the running program to be indistinguishable if and only if they agree on all their low variables. We will write a subscript H or L to indicate what sort of variable we are dealing with, e.g. xL or yH. We will write an underscore under a variable when we are talking about the label of that variable, e.g. x is the label of variable x.
Now consider some assignment statements that update the state:
- xH = yH // ok
- xH = yL // ok
- xL = yH // bad
- xL = yL // maybe bad
The only surprise here is perhaps #4. Why could assigning from a low variable to a low variable be a problem? The issue is that information can flow from control flow in the program. In the wrong context, an assignment from low can cause this information flow to be insecure. For example, consider this code:
xL = 0; if (yH) { xL = 1; }
This program contains what is known as an implicit flow from
variable yH
to xL
.
Assuming that yH
is either zero or one, the
program is equivalent to the assignment xL =
yH
. An implicit flow is an example of a covert storage
channel.
One way to statically analyze implicit flows is to introduce a label for
a pseudo-variable pc
, representing the program counter. The
idea is that at a given point in the program, the label
pc
represents the information learned from the fact
that the program counter got to that point. Within an if
test with a guard of yH
, as above, the
program-counter label is set to the label of the guard, i.e, H.
To check information flows created by an assignment x=y
, we
see that there is an explicit from y to x, and so we must check
y
⊑ x
. Note that we are using the simple
partial order here in which L ⊑ H
but not the
reverse. The same check works even if we are using a more complex
partial order.
Here is a more curious case:
while (x) { // do some statements c }
The usual approach to checking this is to treat it like an if statement, checking commands c with the pc adjusted according to x. Consider this program, though:
while (yH != 0) {}
A attacker who can observe whether the program terminates learns the value of a high variable. An analysis that consider this secure is only enforcing termination-insensitive noninterference. Mathematically, termination is considered indistinguishable from all other outcomes. (This means that indistinguishability is not a transitive relation; all that is required of it is that it is reflexive and symmetric.) However, termination-sensitive noninterference is hard to enforce accurately because you have to prove that programs terminate.
This termination channel only leaks a bit of information. It's an instance of a timing channel in which the time taken by code conveys information. For example:
yH = yH * 1000000; while (yH > 0) { yH--; }
An attacker who can time this learns information. If time is not part of the system state, then noninterference as we've defined is also timing-insensitive.
int {L} xThe expression
L
is a label. The type of x is a security type
int{L}
. Labels can be left out:
float cos (float x) { ... }
Here the variable x can be called with a value of any label—the label of x is an implicit parameter to the code, which doesn't get passed at run time. The result of the function isn't labeled either. The default result label is the join of the labels of all the parameters. In this case the result label is the label of x, which is probably what we want for cosine.
In Jif, classes can also have label parameters. Here's how we might declare a storage cell that can hold a value with any label:
class Cell[label L] { int {L} value; int {L} get() { return value; } void set{L}(int newv) { value = newv; } }This class can be instantiated with any label L. The method
set
has a begin label that prevents it from being called from any program
counter less than L. This is important in order to prevent implicit flows into
the field value
.