Skip to content

Commit

Permalink
WIP: Policy system, take 3: JSON
Browse files Browse the repository at this point in the history
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!
  • Loading branch information
nicowilliams committed Sep 27, 2021
1 parent d70820b commit 4705847
Showing 1 changed file with 397 additions and 0 deletions.
397 changes: 397 additions & 0 deletions policy.jq
Original file line number Diff line number Diff line change
@@ -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
# / \
# / \
# / \
# / \
# / \
# <sub-policy #0> <sub-policy #1>
# ... ...
#
# 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))
)
)

0 comments on commit 4705847

Please sign in to comment.