(** printing -> #→# *) (** printing (policy a b) #a ⇝ b# *) (** Formalisation of "Noninterference, Transitivity, and Channel-Control Security Policies" by J. Rushby www.csl.sri.com/papers/csl-92-2/ *) (** We use Robbert Krebbers' prelude, see https://github.com/robbertkrebbers/ch2o/tree/master/prelude **) Require Import list relations collections fin_collections. Parameter FinSet : Type -> Type. (** begin hide **) Context `{forall A, ElemOf A (FinSet A)}. Context `{forall A, Empty (FinSet A)}. Context `{forall A, Singleton A (FinSet A)}. Context `{forall A, Union (FinSet A)}. Context `{forall A, Intersection (FinSet A)}. Context `{forall A, Difference (FinSet A)}. Context `{forall A, Elements A (FinSet A)}. Context `{forall A, Collection A (FinSet A)}. (* TODO: i wrote this line down so that there is a Collection -> SimpleCollection -> JoinSemiLattice instance for FinSet; how come this is not automatically picked up by the next assumption? *) Context `{forall A (H : (forall (a b :A), Decision (a = b))), FinCollection A (FinSet A)}. (** end hide **) (** * Mealy machines *) (** A Mealy machine is a state machine with labels and outputs on arrows *) Class Mealy (state action out : Type) := { initial : state; step : state -> action -> state; output : state -> action -> out }. (** ** Auxiliary functions We define a [run] of a machine [M], which is an extension of the [step] function to lists of actions. We use a shortcut [do_actions] for running the machine from the [initial] state. *) Fixpoint run `{Mealy state action out} (s : state) (ls : list action) : state := match ls with | nil => s | a::tl => run (step s a) tl end. Definition do_actions `{Mealy state action out} : list action -> state := run initial. (** The [test] function runs the required list of actions and examines the output of the resulting state on a specified action. *) Definition test `{Mealy state action out} (ls : list action) : action -> out := output (do_actions ls). Section Rushby. (** We assume for the rest of the formalisation that we have a Mealy machine [M]. Thus, we parameterize our main development module by a machine [M]. *) Context `{MM : Mealy state action out}. (** * Security policies *) Section Policy. (** In order to define the notion of security, we assume a type [domain] of security domains. Those can be, e.g. clearance levels. For each action [a] we assign a domain [dom a] of that action. For instance, if we interpret domains as clearance levels, the domain of an action can signify the clearance level required to observe/preform the action. A *security policy* is defined to be a reflexive decidable relation on the type of domains. For instance, the policy relation can state that the clearance level [u] is below the clearance level [v]. *) Class Policy (domain : Type) := { dom : action -> domain; (** Sidenote: I had a lot of issues with Coq not finding relevant typeclass instances because the next field was declared as [domain_dec : TYPE] instead of [domain_dec :> TYPE]. So we actually do need implicit coercions to get automatic resolution of typeclass instances. *) domain_dec :> forall (x y : domain), Decision (x = y); policy :> relation domain; policy_dec :> (forall v w, Decision (policy v w)); policy_refl :> Reflexive policy }. (** TODO: Make this notation span over different sections? *) Delimit Scope policy_scope with P. Open Scope policy_scope. Infix "⇝" := policy (at level 70) : policy_scope. (** Quoting Rushby: << We wish to define security in terms of information flow, so the next step is to capture the idea of the "flow of information" formally. The key observation is that information can be said to flow from a domain [u] to a domain [v] exactly when the acions submitted by domain [u] cause the behavior of the system percieved by domain [v] to be different from that perceived when those actions are not present. >> Hence, we define a function [purge] that removes from the given list all the actions that are not supposed to "influence" the domain [v]. *) Fixpoint purge `{Policy domain} (ls : list action) (v : domain) : list action := match ls with | [] => [] | a::tl => if (decide (dom a ⇝ v)) then a::(purge tl v) else purge tl v end. (** Then, the system is _secure_ w.r.t. a given policy if for any set of actions that can be performed, the system cannot tell the difference between preforming the given actions and only performing the actions that are supposed to influence the outcome *) Definition security `{Policy domain} := forall (ls : list action) (a : action), test ls a = test (purge ls (dom a)) a. End Policy. (** * View partitions As we have seen, the non-interference notion of security is defined by quantifying over all the possible paths of the system. We wish to develop techniques for establishing the security property by putting conditions on individual state transformations. As a first step, we define a notion of a "view partition", which is an equivalence relation on the type of states of the system, siginfying that two states are "identitcal" or indistinguishable for a given domain. *) Section view_partitions. (** Formally, a view partition is an assignment of an equivalence relation [≈{u}] for every domain [u] *) Class ViewPartition (domain : Type) := { view_partition :> domain -> relation state; view_partition_is_equiv :> forall v, Equivalence (view_partition v) }. Notation "S ≈{ U } T" := (view_partition U S T) (at level 70, format "S ≈{ U } T") : policy_scope. Open Scope policy_scope. (** We say that a system is _output consistent_ if for every two states [s, t] that are indistinguishable w.r.t. the domain [dom a], the output of the system [output s a] is the same as [output t a] *) Class OutputConsistent `{P : Policy domain} `(ViewPartition domain) := output_consistent : (forall a s t, s ≈{dom a} t -> output s a = output t a). (** Our first lemma states that if we have a view partitioned system that is output consistent and satisfies [[(do_actions ls) ≈{u} (do_actions (purge ls u))]] then the system is secure. *) (* Lemma 1 *) Lemma output_consist_security `{P : Policy domain} `{VP : ViewPartition domain} {OC : @OutputConsistent domain P VP} : (forall ls u, (do_actions ls) ≈{u} (do_actions (purge ls u))) -> security. Proof. intros L ls a. apply output_consistent. apply (L _ (dom a)). Qed. (** Note that the conditions of Lemma [output_consist_security] still require us to quantify over all possible paths of the system. We wish to replace this condition with the following two "local" conditions. The first condition, stating that the view partitioned system _locally respects policy_: if the domain [dom a] is not suppose to interfere with the domain [u], then, from the point of view of [u], the states [s] and [step s a] are indistinguishable. *) Definition locally_respects_policy `{Policy domain} `{ViewPartition domain} := forall a u s, ¬(policy (dom a) u) -> s ≈{u} (step s a). (** We say that the system is *step consistent*, if the view partition is closed under the [step] function. *) Definition step_consistent `{Policy domain} `{ViewPartition domain} := forall a s t u, s ≈{u} t -> (step s a) ≈{u} (step t a). (** Given those two conditions we can prove that the system is secure, by applying the previous lemma *) (* Theorem 1 *) Theorem unwinding `{P: Policy domain} `{VP: ViewPartition domain} `{@OutputConsistent domain P VP} : locally_respects_policy -> step_consistent -> security. Proof. intros LRP SC. apply output_consist_security. assert (forall ls u s t, view_partition u s t -> view_partition u (run s ls) (run t (purge ls u))) as General. (* TODO: a simple generalize would not suffice, because we actually need the s ≈ t assumption *) induction ls; simpl; auto. intros u s t HI. destruct (decide (policy (dom a) u)). (* DOESNT WORK (Lexer) : destruct (decide ((dom a) ⇝ u)). *) (** Case [(dom a) ~> u] *) apply IHls. apply SC; assumption. (** Case [(dom a) ~/> u] *) apply IHls. transitivity s. symmetry. unfold locally_respects_policy in LRP. apply LRP; assumption. assumption. unfold do_actions. intros ls u. apply General. reflexivity. Qed. End view_partitions. (** * Access control interpretation of the view partition. *) Section ACI. (** In this section we consider a formalisation of the access control mechansisms. We say that the machine has _structured state_ if we have a collection of [name]s and [value]s (the latter being decidable), and some sort of the memory assigned to each state, which is formally given by the [contents] function. For each domain [u] we assign sets [observe u] and [alter u] of names that are allowed to be observed and altered, respectively, by the domain [u]. *) Class StructuredState (domain : Type) := { name : Type; value : Type; contents : state -> name -> value; value_dec :> forall (v1 v2 : value), Decision (v1 = v2); observe : domain -> FinSet name; alter : domain -> FinSet name }. (** This induces the view partition relation as follows: two state [s] and [t] are indistinguishable for the given domain, if all the observable contents at [s] is the same as the observable content at [t] *) Definition RMA_partition `{@StructuredState domain} (u : domain) s t := (forall n, (n ∈ observe u) -> contents s n = contents t n). Transparent RMA_partition. Instance RMA `{@StructuredState domain} : ViewPartition domain := { view_partition := RMA_partition }. (* begin hide *) intro u. split; unfold RMA_partition. (* Reflexivity *) unfold Reflexive. auto. (* Symmetry *) unfold Symmetric. intros x y Sy. symmetry. apply Sy. assumption. (* Transitivity *) unfold Transitive. intros x y z T1 T2 n. transitivity (contents y n); [apply T1 | apply T2]; assumption. Defined. (* We have to use 'Defined' here instead of 'Qed' so that we can unfold 'RMA' later on *) (* end hide *) Hint Resolve RMA. (** ** Reference monitor assumptions *) (* TODO: explain those assumptions *) Class RefMonAssumptions `{Policy domain} `{StructuredState domain} := { rma1 : forall (a : action) s t, view_partition (dom a) s t -> output s a = output t a ; rma2 : forall a s t n, view_partition (dom a) s t -> ((contents (step s a) n) ≠ (contents s n) ∨ (contents (step t a) n) ≠ (contents t n)) -> contents (step s a) n = contents (step t a) n ; rma3 : forall a s n, contents (step s a) n ≠ contents s n -> n ∈ alter (dom a) }. (** If the reference monitor assumptions are satisfied, then the system is output-consistent *) Global Instance OC `{RefMonAssumptions}: OutputConsistent RMA. exact rma1. Defined. (** The reference mointor assumptions provide the security for the system, if, furthermore, two additional requirements are satisfied: - [u ~> v] implies [observe u ⊆ observe v]; that is, if [u] is supposed to interfere with [v], then [v] can observe at least as much as [u] - if [n ∈ alter u] and [n ∈ observe v], then [u ~> v]; that is, if [u] is allowed to alter a location that is observable by [v], then [u] is allowed to interfere with [v] *) (* Theorem 2 *) Theorem RMA_secutity `{RefMonAssumptions} : (forall u v, (policy u v) → observe u ⊆ observe v) -> (forall u v n, (n ∈ alter u) → (n ∈ observe v) → (policy u v)) -> security. Proof. intros Cond1 Cond2. apply unwinding. (** We apply the unwinding theorem, so we have to verify that we locally respect policy and that we have step-consistency *) unfold locally_respects_policy. intros a u s. (** In order to prove that the system locally respects the policy relation, we first state the contrapositive, which we can prove because the policy relation and type of values are decidable *) assert ((exists n, (n ∈ observe u ∧ (contents s n ≠ contents (step s a) n))) -> policy (dom a) u) as CP. intros opH. destruct opH as [n [??]]. eapply Cond2. eapply rma3. eauto. assumption. intros NPolicy. unfold view_partition, RMA, RMA_partition. (* TODO: why can't decide automatically pick the instance value_dec? *) intros. destruct (decide (contents s n = contents (step s a) n)) as [e|Ne]. assumption. exfalso. apply NPolicy. apply CP. exists n; split; assumption. (** In order to prove step-consistency we wish to apply the second reference monitor assumption. For that we must distinguish three cases: (contents (step s a) n ≠ contents s n), (contents (step t a) n ≠ contents t n), or if both of the equalities hold *) intros a s t u A. unfold view_partition, RMA, RMA_partition in *. intros n nO. destruct (decide (contents (step s a) n = contents s n)) as [E1 | NE1]. destruct (decide (contents (step t a) n = contents t n)) as [E2 | NE2]. (* Both equalities hold *) rewrite E1, E2. apply A; assumption. (* NE2 *) (* We use the Second RM assmption to deal with this case *) apply rma2. (* for this we have to show that s ~_(dom a) t *) unfold view_partition, RMA, RMA_partition. intros m L. apply A. apply Cond1 with (u:=dom a). apply Cond2 with (n:=n); [eapply rma3 | ]; eassumption. assumption. right. auto. (* NE1 case is similar *) (* TODO: repetition *) apply rma2. (* for this we have to show that s ~_(dom a) t *) unfold view_partition, RMA, RMA_partition. intros m L. apply A. apply Cond1 with (u:=dom a). apply Cond2 with (n:=n); [eapply rma3| ]; eassumption. assumption. left. auto. Qed. End ACI. (** * Intransitive security policy *) Section Intransitive. (** Auxiliary definitions *) Fixpoint sources `{Policy domain} (ls : list action) (d : domain) : FinSet domain := match ls with | [] => {[ d ]} | a::tl => let src := sources tl d in if (decide (exists (v: domain), ((v ∈ src) ∧ (policy (dom a) v)))) then src ∪ {[ dom a ]} else src end. (** The two properties of the [sources] function: it is monotone and [d \in sources ls d] *) Lemma sources_mon `{Policy} : forall a ls d, sources ls d ⊆ sources (a::ls) d. Proof. intros. simpl. destruct (decide _); [apply union_subseteq_l |]; auto. Qed. Hint Resolve sources_mon. Lemma sources_monotone `{Policy} : forall ls js d, ls `sublist` js → sources ls d ⊆ sources js d. Proof. intros ls js d M. induction M. simpl. reflexivity. simpl. destruct (decide (∃ v : domain, v ∈ sources l1 d ∧ policy (dom x) v)); destruct (decide (∃ v : domain, v ∈ sources l2 d ∧ policy (dom x) v)). apply union_preserving_r. assumption. exfalso. apply n. destruct e as [v [e1 e2]]. exists v; split; try (apply (IHM v)); assumption. transitivity (sources l2 d). assumption. apply union_subseteq_l. assumption. transitivity (sources l2 d); auto. Qed. Lemma sources_in `{Policy} : forall ls d, d ∈ sources ls d. Proof. intros. induction ls; simpl. apply elem_of_singleton_2; auto. destruct (decide (∃ v : domain, v ∈ sources ls d ∧ policy (dom a) v)); simpl. apply elem_of_union_l. assumption. assumption. Qed. Hint Resolve sources_in. Fixpoint ipurge `{Policy} (ls : list action) (d : domain) := match ls with | [] => [] | a::tl => if (decide ((dom a) ∈ sources ls d)) then a::(ipurge tl d) else ipurge tl d end. (** The non-interference notion of security for intransitive policies is very similar to the transitive case, bu uses the [ipurge] function instead of [purge] *) Definition isecurity `{Policy} := forall ls a, test ls a = test (ipurge ls (dom a)) a. (** We can prove lemmas similar to the transitive case *) Lemma output_consist_isecurity `{P : Policy domain} `{VP : ViewPartition domain} {OC : @OutputConsistent domain P VP} : (forall ls u, view_partition u (do_actions ls) (do_actions (ipurge ls u))) -> isecurity. Proof. unfold isecurity. intros H ls a. unfold test. apply output_consistent. apply (H ls (dom a)). Qed. Definition view_partition_general `{ViewPartition domain} (C : FinSet domain) s t := forall (u: domain), u ∈ C -> view_partition u s t. Global Instance view_partition_general_equiv `{ViewPartition domain}: forall V, Equivalence (view_partition_general V). Proof. intro V. split. intros x v A. reflexivity. intros x y A1 v A2. symmetry. apply (A1 v); assumption. intros x y z A1 A2 v A3. transitivity y. apply (A1 v); assumption. apply (A2 v); assumption. Qed. Definition weakly_step_consistent `{Policy domain} `{ViewPartition domain} := forall s t u a, view_partition u s t -> view_partition (dom a) s t -> view_partition u (step s a) (step t a). Ltac exists_inside v := let H := fresh "Holds" in let nH := fresh "notHolds" in destruct (decide _) as [H | []]; [ try reflexivity | exists v; try auto]. Local Hint Resolve sources_mon. Local Hint Extern 1 (_ ∈ sources (_::_) _) => eapply sources_mon. Local Hint Immediate elem_of_singleton. (* Local Hint Extern 1 (_ ∈ {[ _ ]}) => apply elem_of_singleton; trivial. *) Local Hint Resolve elem_of_union. (* Lemma 3 *) Lemma weakly_step_consistent_general `{Policy domain} `{ViewPartition domain} (s t : state) (a : action) ls (u: domain) : weakly_step_consistent -> locally_respects_policy -> view_partition_general (sources (a::ls) u) s t -> view_partition_general (sources ls u) (step s a) (step t a). Proof. intros WSC LRP P v vIn. unfold view_partition_general in P. unfold locally_respects_policy in LRP. destruct (decide (policy (dom a) v)). (* Case [dom a ~> v] *) apply WSC; apply P. auto. (* we need to show that [dom a ∈ sources (a::ls) v] *) simpl. exists_inside v. apply elem_of_union. right. auto. apply elem_of_singleton; trivial. (* Case [dom a ~/> v] *) transitivity s. symmetry. apply LRP; assumption. transitivity t. apply P. auto. apply LRP; assumption. Qed. (* Lemma 4 *) Lemma locally_respects_gen `{Policy domain} `{ViewPartition domain} (WSC: weakly_step_consistent) (LRP : locally_respects_policy) s a ls u : ¬ ((dom a) ∈ sources (a::ls) u) -> view_partition_general (sources ls u) s (step s a). Proof. intros domN v vIn. apply LRP. intro. (* If [dom a ~> v], then [dom a ∈ sources (a::ls) u], because [v ∈ sources ls u] *) (* again, a clusterfuck *) apply domN. simpl. exists_inside v; auto. apply elem_of_union; right; apply elem_of_singleton; reflexivity. Qed. (* Lemma 5 *) Lemma unwinding_gen `{Policy domain} `{ViewPartition domain} (WSC: weakly_step_consistent) (LRP : locally_respects_policy) s t ls u : view_partition_general (sources ls u) s t -> view_partition u (run s ls) (run t (ipurge ls u)). Proof. generalize dependent s. generalize dependent t. induction ls; intros s t. simpl. intro A. apply (A u). apply elem_of_singleton; reflexivity. intro VPG. simpl. unfold sources. fold (sources (a::ls) u). destruct (decide _). (** Case [dom a ∈ sources (a::ls) u] *) simpl. apply IHls. apply weakly_step_consistent_general; auto. (** Case [dom a ∉ sources (a::ls) u] *) apply IHls. symmetry. transitivity t. - intros v vIn. symmetry. apply VPG. apply sources_mon; exact vIn. - apply locally_respects_gen; try(assumption). Qed. Theorem unwinding_intrans `{P : Policy domain} `{VP : ViewPartition domain} `{OC : @OutputConsistent domain P VP} (WSC : weakly_step_consistent) (LRP : locally_respects_policy) : isecurity. Proof. apply output_consist_isecurity. intros. apply unwinding_gen; try assumption. reflexivity. Qed. Theorem rma_secure_intransitive `{RefMonAssumptions} : (forall u v n, n ∈ alter u -> n ∈ observe v -> policy u v) -> isecurity. Proof. intro policyA. apply @unwinding_intrans with (VP:=RMA). exact rma1. intros s t u a A1 A2. unfold view_partition. unfold RMA_partition; simpl. intros n L. destruct (decide (contents (step s a) n = contents s n)) as [E1 | NE1]. destruct (decide (contents (step t a) n = contents t n)) as [E2 | NE2]. (* Case [contents (step s a) n = contents s n /\ contents (step t a) n = contents t n] *) rewrite E1, E2. apply A1; assumption. (* Case [contents (step t a) n ≠ contents t n] *) apply rma2; [ | right]; assumption. (* Case [contents (step s a) n ≠ contents s n] *) apply rma2; [ | left]; assumption. intros a u s A. assert ((exists n, (n ∈ observe u ∧ (contents s n ≠ contents (step s a) n))) -> policy (dom a) u) as CP. intros opH. destruct opH as [n [??]]. eapply policyA. eapply rma3. eauto. assumption. intros n L. destruct (decide (contents s n = contents (step s a) n)). assumption. exfalso. apply A. apply CP. exists n. auto. Qed. End Intransitive. End Rushby.