From 4705847b1509015659cc5e7d34152aad73b8637e Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Sun, 26 Sep 2021 19:09:38 -0500 Subject: [PATCH] WIP: Policy system, take 3: JSON This is a sketch of how to represent and implement policies expressed in JSON using "take 2" as a model. Policies are represented as JSON objects that have: - zero, one, or more named policy parameters, which have type information - zero, one, or more bindings for a policy's parameters -- these can be default values, or values for parameters of other policies referred to by this one - an actual policy AST A policy can refer to other policies. This is especially necessary for TPM2_PolicyAuthorize() and TPM2_PolicyAuthorizeNV(), where the referred-to policy may not be known until run-time, so we really have to be able to separate the referrent and the referred-to policies. This may also be useful for TPM2_PolicyOr() even though its alternatives are static -- it may help organize policies, and to DRY. We treat TPM2_PolicyOr() as AST interior nodes. Interior nodes have to be singular TPM2_PolicyOr() commands. Leaf nodes are sequences of commands the first of which is allowed to be a hole, like TPM2_PolicyAuthorize() or TPM2_PolicyAuthorizeNV(). See ./policy.jq! --- policy.jq | 397 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 397 insertions(+) create mode 100644 policy.jq diff --git a/policy.jq b/policy.jq new file mode 100644 index 00000000..dbc7b41b --- /dev/null +++ b/policy.jq @@ -0,0 +1,397 @@ +# This is a jq program to handle TPM 2.0 EA policies. +# +# Currently a WORK IN PROGRESS. +# +# Status: +# +# - policy merging is working +# +# - policy traversal to generate execution traces is working +# +# - TBD: implement all the commands +# +# - TBD: how to represent ancillary non-policy commands needed to execute a +# policy, such as importing and/or loading external keys referenced by the +# policy commands (e.g., signer keys for TPM2_PolicyAuthorize(), objects +# referenced by TPM2_PolicySecret(), etc.) +# +# One option is to refer to them explicitly among policy commands. This may +# not be a great option as it complicates validation of a policy's form. +# +# Goals: +# +# - Implement a jq program with various subcommands that ultimately allows for +# the representation of complex parametrized TPM 2.0 EA policies and the +# execution of those policies in trial and/or authorization sessions. +# +# The jq program itself would execute no TPM commands -- instead a trace of +# commands to execute would be output and executed by a bash script (or +# whatever). +# +# Desired sub-commands: +# +# list-policy-references -- List URIs of policies that need to be +# fetched by the caller. +# +# Useful for fetching policies as needed at +# run-time, then merging into a single working +# policy. +# +# For example, a policy referred to by +# TPM2_PolicyAuthorize() might have to be +# downloaded at run-time because... that's that +# command's point, that the policy is to be +# determined at run-time. +# +# merge-policies -- merge N given policies into one +# +# trace-policy-trial -- given a policy, emit a trace of commands to execute +# in order to evaluate the policy in trial sessions +# +# trace-policy-exec -- given a policy and a path through the PolicyOr AST, +# emit a trace of commands to execute in order to +# evaluate the policy (including evaluation in trial +# sessions of PolicyOr alternatives not-taken) +# +# +# Some useful observations about TPM 2.0 EA policies +# +# - there are "holes", namely: TPM2_PolicyOr(), TPM2_PolicyAuthorizeNV(), and +# TPM2_PolicyAuthorize() +# +# - any sequence of TPM2_Policy*() commands is a conjunction, except for +# TPM2_PolicyOr() which... is special +# +# - TPM2_PolicyOr() defines an alternation of policies (up to 8) +# +# - the way holes work is that one must first execute the referred-to policy's +# policy commands, then the hole command itself as it will replace rather +# than extend the session's policyDigest +# +# - if we view the policies referred to by holes as separate from the policy +# containing the hole, we can treat the former are children of the latter to +# form an AST +# +# - given an AST where interior nodes are holes, then the "leaves" are +# sequences non-hole commands +# +# - in our case here we're allowing the first command of such a sequence to be +# a hole +# +# so a policy AST for us looks like: +# +# PolicyOr, followed by non-hole commands +# / \ +# / \ +# / \ +# / \ +# / \ +# +# ... ... +# +# with each sub-policy being a sequence of policy commands the first one of +# which may be a hole +# +# - only PolicyOr has multiple alternatives -- the other holes have just one +# +# +# Here a policy is a JSON object with the following keys: +# +# params, bindings, policies, policyDef +# +# Params define parameters that may be referenced by commands in the policy. +# +# Bindings are values for those parameters. A policy may be fragmentary and +# include only parameters, and it may include default bindings for some or all +# of those parameters. +# +# A policy may refer to other policies by name. The `policies` key's value +# should be an object whose keys are policy names and whose values are objects +# with a uri key and an optional digest key. +# +# A policyDef is a recursive data structure, and if its value is a string then +# it names a policy to substitute in. A policyDef may be either an object with +# another policyDef key, or an array of policy commands. +# +# A policy command is an object with a command key and various not-yet- +# developed keys for command input arguments. + + +# Some useful utilities +def debug($x): ($x|debug|empty),.; +def cond(c; t): if c then t else . end; +def cond(c; t; f): if c then t else f end; +def cond(c0; t0; c1; t1; f): cond(c0; t0; cond(c1; t1; f)); +def cond(c0; t0; c1; t1; c2; t2; f): cond(c0; t0; c1; t1; cond(c2; t2; f)); +def typecase(T; t; f): cond(type==T; t; f); +def typecase(T; t): typecase(T; t; empty); +def typecase(T0; t0; T1; t1; f): typecase(T0; t0; typecase(T1; t1; f)); +def typecase(T0; t0; T1; t1): typecase(T0; t0; T1; t1; empty); +def typecase(T0; t0; T1; t1; T2; t2; f): typecase(T0; t0; T1; t1; typecase(T2; t2; f)); +def typecase(T0; t0; T1; t1; T2; t2): typecase(T0; t0; T1; t1; T2; t2; empty); +def check(c; e): cond(c; .; e|error); + +# Check that the input array (or string) is a prefix of a given one ($of) +def isPrefix($of): + (length) as $inlen + | ($inlen <= ($of | length)) and (.==$of[0:$inlen]); + +# Convert an array of objects into an object where the keys in the object at +# the values of the `.[$i]|k` for each $i'th element of the array +def a2o(k): + typecase("array"; reduce .[] as $v ({}; .[$v|k] = $v); + "object"; .; + "null"; {}; + "Expected array or object; got \(type)"|error); + +# Merge a set of (e.g., "params"). Duplicates not allowed. +def merge_unique(a; k; e): + a2o(k) + | reduce (a|a2o(k)) as $o (.; + reduce ($o|keys_unsorted[]) as $k (.; + cond(has($k) and ($o|has($k)); $k|e|error) + | .[$k] //= $o[$k] + ) + ); +def merge_unique(a; k): merge_unique(a; k; "Key \(.) is not unique"); + +# Merge a set of (e.g., "bindings"). Duplicates are allowed -- first value +# wins. +def merge(a; k): + a2o(k) + | reduce (a|a2o(k)) as $o (.; + reduce ($o|keys_unsorted[]) as $k (.; .[$k] //= $o[$k]) + ); + +# Some policy checking functions: +def holes: "PolicyOr", "PolicyAuthorize", "PolicyAuthorizeNV"; +def isHole: . as $command | any(holes; .==$command); +def checkPolicyHole($i): + cond($i == 0; .; cond(.command | isHole; error("Holes must come first"))); + +# Merge the given policies, using the name given as input (`.`) as the name of +# the main policy. +def mergePolicies(policies): + # Internal utility to resolve references to policies (O(N)) + def fix_refs: + reduce path(..[]?|.policyDef?|select(type=="string")) as $path (.; + (getpath($path)) as $reference + | .policyDefs[$reference] as $target + # debug("Resolving reference to \($reference) at \($path) to \($target)") + | cond($target == null; "Missing policy \($reference)"|error) + | setpath($path; $target.policyDef) + ) + ; + + # Save the name of the main policy + . as $main_policy + + # Skeleton of merged policy + | {params:{},bindings:{},policyDefs:{}} + + # First the policies' params (must be unique) + | reduce policies as $p (.; + (.params |= merge_unique($p.params; .name)) + ) + + # First the policies' bindings (need not be unique; first setting wins) + | reduce policies as $p (.; + (.bindings |= merge($p.bindings; .name)) + ) + + # TODO: Check that there are no unbound parameters? + + # Now index policies by name as prep for the policy reference resolution + # step + | reduce policies as $p (.; + (.policyDefs |= + merge_unique([{name:$p.name,policyDef:$p.policyDef}]; + .name; + "Policy name \(.) is not unique")) + ) + + # Resolve policy references + | fix_refs + + # The main policy, with policy references resolved, _is_ the merged policy + | .policyDef = .policyDefs[$main_policy] + + # Delete the temporary index of policies (XXX should just have used a + # local $binding) + | del(.policyDefs) + ; + +# Post-order traversal of policies for generating execution traces. +# +# The callback `trace` does the tracing of commands. It gets an object as +# input with a `path` key and a `policy` key containing a command to execute +# (XXX rename to `command` then). The path is the path of PolicyOr +# alternations taken through the policy's AST to get to the TPM command it's +# given. This means the `trace` callback can emit a trace of commands where +# each is associated with a policy or trial session according to whether we're +# executing the whole policy in a trial session or according to the desired +# path through the PolicyOr AST. +def postTraversePolicyDef(trace): + def policyAuthorizeNV: [.,{command:"policyAuthorizeNV"}]; + def policyAuthorize: [.,{command:"policyAuthorize"}]; + def policyOr: [.,{command:"PolicyOr"}]; + def traverse($path): + # XXX This is because sometimes we end up having policyDef as an + # object, and sometimes as an array. + # FIXME Make it consistent. + def getPolicyDef: + typecase("object"; + cond(has("command"); .; + has("policyDef"); .policyDef; + "Object doesn't resemble a policyDef"); + "array"; cond(length>0; .; + "Zero-length policyDef!"|error); + "Expected policy as array or object"|error); + + getPolicyDef + # XXX This stinks. We're trying to check that any policy command + # that's not the first in a conjunction must also not be a "hole", + # but this way of checking that is not exactly idiomatic... + | typecase("object"; 0; "array"; range(length)) as $i + | typecase("object"; .; "array"; .[$i]) + | checkPolicyHole($i) + + | check((.command|type)=="string"; "Not a policy") + | if .command == "PolicyOr" + then + [ + range(length) as $i + | .policyDef[$i] + | [traverse($path + [$i])] + ] + | policyOr + elif .command == "PolicyAuthorize" + then + [ + .policyDef + | traverse($path) + ] + | policyAuthorize + elif .command == "PolicyAuthorizeNV" + then + [ + .policyDef + | traverse($path) + ] + | policyAuthorizeNV + elif .command == "And" + then [.policyDef|traverse($path)] + else + # XXX Implement all the comamnds here. Check their arguments and + # use bindings to supply values for any parameter references. + . + end + | {path:$path,policy:.} + | trace + ; + + traverse([]) +; + +# Output some test toy policies +def testPolicies: + { name:"first", + bindings:[ + { name:"attest_signer", + type:"PK", + encoding:"PEM", + value:"foo" + }, + { name:"policy_authority_signer", + type:"PK", + encoding:"PEM", + value:"bar" + } + ], + params:[], + policyDef:[{command:"PolicySigned",x:2}]}, + { name:"second", + bindings:[], + params:[ + { name:"attest_signer", + type:"PK", + encoding:"PEM" + } + ], + policyDef:[{command:"PolicySecret",x:1},{command:"PolicyPCR",y:2}]}, + { name:"third", + bindings:[], + params:[ + { name:"policy_authority_signer", + type:"PK", + encoding:"PEM" + } + ], + policyDef:[{command:"PolicySecret",x:2},{command:"PolicyPCR",y:3}]}, + { name:"main", + bindings:[], + params:[], + policyDef:[ + { command:"PolicyOr", + policyDef:[ + { command:"And", + policyDef:"first" }, + { command:"PolicyOr", + policyDef:[ + { command:"And", + policyDef:"second" + }, + { command:"And", + policyDef:"third" + } + ] + } + ] + } + ] + }; + + + # Merge the test policies + # XXX This is just a demo. A main program is needed that implements the + # sub-commands mentioned above. + "main" +| mergePolicies(testPolicies) +| ( + # Show the merged policy + ( + debug("Merged policy") + | . + ), + + # Show post-order traversal trace of the merged policy + ( + debug("Post-traversal of policy") + | .policyDef + | postTraversePolicyDef(.) + ), + + # Show post-order traversal of the merged policy using only one path + # through the PolicyOr tree + ( + debug("Post-traversal of policy using path [0]") + | .policyDef + | postTraversePolicyDef(cond(.path|debug|isPrefix([0]); .; debug("Pruning path \(.path) as it's not a prefix of [0]")|empty)) + ), + + # Show post-order traversal of the merged policy using only another path + # through the PolicyOr tree + ( + debug("Post-traversal of policy using path [1,0]") + | .policyDef + | postTraversePolicyDef(cond(.path|isPrefix([1,0]); .; debug("Pruning path \(.path) as it's not a prefix of [1,0]")|empty)) + ), + + # Show post-order traversal of the merged policy using only yet another + # path through the PolicyOr tree + ( + debug("Post-traversal of policy using path [1,1]") + | .policyDef + | postTraversePolicyDef(cond(.path|isPrefix([1,1]); .; debug("Pruning path \(.path) as it's not a prefix of [1,1]")|empty)) + ) + )