Getting started
Clone this repository onto your local machine or your Andrew home directory.
You will need to use Python 3.10 to complete this lab. You can reuse the virtual environment that you created for Lab 1. The only new dependency that you will need to complete the lab is the cryptography
modulo, which you can install either via pip
or conda
from within your environment.
> [pip|conda] install cryptography
You should then copy your private key, along with the credentials that you will need into the repository directory. Assuming <id>
is your Andrew ID, run the following command:
scp -r <id>@linux.andrew.cmu.edu:/afs/andrew.cmu.edu/usr8/mfredrik/15316-f23/<id>/* .
Credentials
You should now have a credentials
directory with the following contents:
<id>_secret.cred
<id>_shared.cred
<id>_txt.cred
dsduena_secret.cred
justinyo_secret.cred
mdhamank_secret.cred
mfredrik_secret.cred
mfredrik_shared.cred
policy1.cred
policy2.cred
siruih_secret.cred
A credential is a formula expresses the policy intent of an agent, that is signed by their private key to provide authenticity. For example, policy1.cred
contains the following:
{
"p": "(@A . (((#mfredrik says open(A, <shared.txt>)) -> open(A, <shared.txt>))))",
"signator": "#root",
"signature": "c397b1d047279a4f544ae7eecafed3939b2780bc4f61b443849ec62069ce2b8353c5805cb102753f9b309d12541cc25349cced3cb830a137e30610f677068d02"
}
Here, p
contains the policy formula, signator
refers to the agent who signed the formula, and the bytes of the actual digital signature are given in signature
.
We are more familiar working with credentials as sign
formulas, and we can get such a formula from the credential, and pretty-print it using util.stringify
:
> import crypto, util
> cred = crypto.Credential.load_credential('credentials/policy1.cred')
> print(stringify(cred.sign_formula()))
sign(((@A . (((#mfredrik says open(A, <shared.txt>)) -> open(A, <shared.txt>))))), [2b:8f:e8:9b])
The second argument appearing in square brackets is a digest of the signer's public key.
Each policy used in this lab has a corresponding credential; the details of those policies are described in the implementation section of this documentation.
You do not need to worry about manually loading credentials in most cases, as auth.load_all_credentials
will do it for you.
Certificates
You should now have a certs
directory with the following files (assuming <id>
is your Andrew ID).
<id>.cert
ca.cert
dsduena.cert
justinyo.cert
mdhamank.cert
mfredrik.cert
root.cert
siruih.cert
A certificate is a formula in which a certificate authority (identified by #ca
in this lab) signs that a public key belongs to a given agent:
sign(iskey(#agent, [agent_key_fingerprint]), [ca_key_fingerprint])
A key fingerprint is a string that uniquely identifies a public key, for example [68:d7:6c:b7:95:fb:a4:f7:a7:4f:12:44:6f:27:c5:40]
.
What's in a certificate?
Looking at `certs/root.cert`, the contents are as follows.{
"agent": "#root",
"cred": {
"p": "iskey(#root, [2b:8f:e8:9b:8b:76:37:a7:3b:7e:85:49:9d:87:7b:3b])",
"signator": "#ca",
"signature": "4c120014db10eade1ab7a14f6745c94067f13bb38e4424c8084c24688a062268db37a08f480f571c90700afae07070c96428965cd19d20d3d2f105231ee60706"
},
"public_key": "2d2d2d2d2d424547494e205055424c4943204b45592d2d2d2d2d0a4d436f77425159444b32567741794541506844563357384a466d45547767366d69396b416262484e2b5742506c4e586f743372486f36466e5430303d0a2d2d2d2d2d454e44205055424c4943204b45592d2d2d2d2d0a"
}
Notice that the actual formula is given in `cert.cred.p`, in addition to easily-accessible metadata such as which party's public key is being certified (`cert.agent = #root`) and who the CA is (`cert.cred.signator = ca`).
Additionally, certificate files contain the base64-encoded digital signature (`cert.cred.signature`), signed using the public key of `cert.cred.signator`, as well as the public key of `cert.agent` (in this case, `#root`) in `cert.public_key`. If `#root` were to sign a credential, then it could be verified using this public key.
Private keys
Finally, in the private_keys
directory, you should see:
<id>.pem
You do not need to worry about the specific details of private keys, and you will not need to work directly with them: the code handed out with this lab takes care of creating signatures and certificates from private keys that you have access to. The APIs for doing this are discussed in more detail in exploiting a vulnerability.
Authorization Logic
logic.py
defines the authorization logic that we have discussed in class.
There are three types of constants: agents, keys, and resources.
Agent
constants are prefixed with#
.Key
constants appear between brackets[]
.Resource
constants appear between angle brackets<>
.
In formulas, locations expecting a constant of a particular type can also recieve a variable, which is just an alphanumeric string that is not prefixed with any special characters. Both of the following are syntactically valid formulas, where the first has three variables that are replaced by corresponding constants in the second.
A says open(B, R)
#A says open(#B, <R>)
A parser is provided in parser.py
, and we strongly recommend that you use the parser to construct formula objects, rather than building them manually from the constructors in logic.py
.
> parse('A says P')
App(op=<Operator.SAYS: 7>, arity=2, args=[Variable(id='A'), Variable(id='P')])
The logic supports universal quantifiers, and uses the @
symbol for them.
When parsing quantified formulas, note that you should surround the quantified formula with parentheses to avoid potential parser errors.
So the following may result in a parser error.
parse('@x . @y . open(x, y)')
It should be run as follows.
parse('@x . (@y . (open(x, y)))')
Sequents and judgements
When building sequents, our authorization logic distinguishes between affirmation justdments and truth judgements.
Truth judgements are denoted by writing true
after a formula, and are represented by the Proposition
class in logic.py
.
> parse('(P -> Q) true')
Proposition(p=App(op=<Operator.IMPLIES: 4>, arity=2, args=[Variable(id='P'), Variable(id='Q')]))
Affirmation judgements are denoted by juxtaposing a principal, aff
, and a formula.
> parse('A aff P -> Q')
Affirmation(a=Variable(id='A'), p=App(op=<Operator.IMPLIES: 4>, arity=2, args=[Variable(id='P'), Variable(id='Q')]))
Sequents are expressed using |-
, and accept either type of judgement on the right.
> parse('P |- A aff P')
Sequent(gamma=[Proposition(p=Variable(id='P'))], delta=Affirmation(a=Variable(id='A'), p=Variable(id='P')))
Note that if you do not write true
to expressly denote a truth judgement, the parser will assume that this was what you intended to do.
> parse('P |- Q')
Sequent(gamma=[Proposition(p=Variable(id='P'))], delta=Proposition(p=Variable(id='Q')))
Proof rules
The proof rules available in this logic are listed in proofrules.py
.
@dataclass(eq=True, frozen=True)
class Rule:
premises: list[Sequent]
conclusion: Sequent
name: str
As one might expect, a rule is comprised of a list of premises, each its own sequent, and a conclusion sequent. The convention that we use for specifying premises and the conclusion is illustrated by the left implication rule.
Rule(
[
parse('|- P true'),
parse('Q true |- R true')
],
parse('P -> Q true |- R true'),
'->L'
)
Proof rules and sequents are composed to build Proof
objects.
@dataclass(eq=True, frozen=True)
class Proof:
premises: list[Proof | Sequent]
conclusion: Sequent
rule: Rule
Each step of a proof much refer to a Rule
defined in proofrules.py
; you should not invent your own rules to use in proofs. Proofs are generated by tactics in this lab (more on this later), but for the purposes of illustration, we can manually write the following proof to reflect and application of the left implication rule.
Proof(
[
parse('(c -> d) -> e |- a'),
parse('b, (c -> d) -> e |- f')
],
parse('a -> b, (c -> d) -> e |- f'),
impLeftRule
)
The stringify
helper from util.py
will print this out nicely.
((c -> d) -> e) true |- a true b true, ((c -> d) -> e) true |- f true
->L ------------------------------------------------------------------------
(a -> b) true, ((c -> d) -> e) true |- f true
Note that your proofs may become wider than your screen.
You can specify the width that stringify
should be limited to with the pf_width
keyword argument, and it will split premises apart from the main proof and print them separately.
> print(stringify(pf, pf_width=50))
T.0 T.1
->L --------------------------------------------------
(a -> b) true, ((c -> d) -> e) true |- f true
Proof T.1:
b true, ((c -> d) -> e) true |- f true
Proof T.0:
((c -> d) -> e) true |- a true
Looking at the rules available in proofrules.py
, most should be familiar from lecture. The exceptions are impLeftAffRule
, forallLeftAffRule
, and affCutRule
.
impLeftAffRule = Rule(
[
parse('|- P true'),
parse('Q true |- A aff R')
],
parse('P -> Q true |- A aff R'),
'->L'
)
forallLeftAffRule = Rule(
[parse('P(e) |- A aff Q')],
parse('@x . P(x) |- A aff Q'),
'@L'
)
affCutRule = Rule(
[
parse('|- P true'),
parse('P true |- A aff Q')
],
parse('|- A aff Q'),
'affcut'
)
These rules are the same as their "normal" counterparts from lecture, but they match sequents with an affirmation judgement on the right. You are free to use these rules when constructing Proof
objects, and doing so should make developing tactics less intricate, as you have more freedom to decide when to apply the right says
rule.
Full grammar of the logic
<agent> ::= x // variable
| #a // constant
<key> ::= x // variable
| [k] // constant
<resource> ::= x // variable
| <r> // constant
<formula> ::= x // variable
| <formula> -> <formula> // implication
| <agent> says <formula>
| iskey(<agent>, <key>)
| sign(<formula>, <key>)
| ca(<agent>)
| open(<agent>, <resource>)
| @x . <formula> // universal quantifier
<judgement> ::= <formula> true // proposition
| <agent> aff <formula> // affirmation
<sequent> ::= <judgement>* |- <judgement>
Next steps
Now that you're familiar with the building blocks needed to write authorization proofs, check out the authorization goals for the lab.