-
Peter J. Keleher authoredPeter J. Keleher authored
- Project 3: Crypto
- Overview
- Setup
- New Packages
- Task 1: Generate a public/private keypair
- Task 2: Sign and Verify
- Signing
- Task 3: Mutable signed bindings
- The public key property
- New command: dump
- New command: putraw
- Updating a binding
- Task 4: Verify bindings
- Servers and Trust
- Video walkthrough
- Grading
- Submit
Project 3: Crypto
Due: Oct 8, 2023, 11:59:59 pm, v1.02
v1.02 changes:
- added
putraw
command and text describing how to verify a signed binding. - changed
go run blob.go
tocli
. Createcli
w/go build -o cli cli.go
.
Overview
This project has the following tasks:
- Generate public/private RSA key pairs pete.
- Use public-key crypto to sign and validate signatures
- Build signed bindings of permanent names to changing values.
- Add a new
dump
command to see the raw bindings. - Add a new
verifybinding
command to verify bindings.
Setup
Download files here.
New Packages
The new crypto packages we will be using include:
-
pem: PEM (Privacy Enhanced Mail) encoding is the de facto format
for storing and sending cryptographic keys and certificates. This is
just encoding, like
base32
, and is not encryption. - x509: Widespread standard for public key certificate formats. We will not be using certificates, but we will be using routines from this package.
- rand: random numbers
- rsa: RSA (Rivest-Shamir-Adleman) public keys.
Task 1: Generate a public/private keypair
Signing, verifying, and using JSON blobs (including recipes) is going to be the centerpiece of this project. Public key crypto is asymmetric: they come in pairs with a public key and a private key. For this project, we will use the private key just to sign JSON blobs, and the public key to verify the signatures. As you might expect, public keys are not secret, in fact they need to be widely disseminated in order to be used. Private keys are secret.
-
cli genkeys
: Should generate a 2048-bit RSA private key using thersa
routineGenerateKey
, and write to output filekey.private
. Write the corresponding public key tokey.public
.
To write the public key:
- create a
pem.Block
specifyingType
"RSA PUBLIC KEY" and passing the results ofx509.MarshalPKCS1PublicKey
toBytes
. - encode this using
pem.EncodeToMemory
- write the results to a file.
The resulting public key should look something like this:
-----BEGIN RSA PUBLIC KEY-----
MIIBCgKCAQEAsIYRztz29n63uOAKjnvlmysxfu4YjOq4erx6FsAdHLp+8BLF3YjX
WbJB9IcSsik9oRQ9+Abv9jdc7qHz/gTmdxiBx55isbGWAjLeADSxorumQVqJqaXi
/PNATUCOozdOEzDjxv/OXxOc4uZqjj7i+AbncPFTt22WuD40U0UEKo8Uv4JJtryS
bYvvXTO2R6UEMusxhz2eKA1BdqIl4EEeNhjUpXEk9P6fJ6QXc2dm7sOP42gfhoL5
tLpXM9quAkpVv/L1vwIVpBZTgtsFHfHz06p7ST77+E6lua10km9EVcdfBlwo0uvf
7GAP/V9jBDVUrTfBlZIrKksm4sv27pmt0wIDAQAB
-----END RSA PUBLIC KEY-----
Write the private key out similarly, substituting "private" for "public" where appropriate:
-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEAsIYRztz29n63uOAKjnvlmysxfu4YjOq4erx6FsAdHLp+8BLF
3YjXWbJB9IcSsik9oRQ9+Abv9jdc7qHz/gTmdxiBx55isbGWAjLeADSxorumQVqJ
qaXi/PNATUCOozdOEzDjxv/OXxOc4uZqjj7i+AbncPFTt22WuD40U0UEKo8Uv4JJ
trySbYvvXTO2R6UEMusxhz2eKA1BdqIl4EEeNhjUpXEk9P6fJ6QXc2dm7sOP42gf
hoL5tLpXM9quAkpVv/L1vwIVpBZTgtsFHfHz06p7ST77+E6lua10km9EVcdfBlwo
0uvf7GAP/V9jBDVUrTfBlZIrKksm4sv27pmt0wIDAQABAoIBAQCjqNlv2guaA2gg
no8LBTGeSFCOZv8Z/zBkksP6vcj/7qRrvmv2db17KrrdlDGwPm4mHpdJklz+ANxH
Xp3RYAewWWdRhLXqG0SB61lKBZC/2IxOwTsqr2MgtAJk3TqhdlALmONFh7t1CMEG
2Svnj8RswjHn08aTJKZ9USbAIRe4v/7dgKalffpnro4gQAhcBHhQSqKD+cmUebXI
mOApIjz1waywHSS69DrzJpNG755kJBd4q5ZYwXA6m3jFDTrfI9RLD6gJDtoVpMiE
h293dRS9w/A564sCznUFjMWUQFcHB8PhlDG0SzjStO5eE3f845UjLdfa7HZW7d1S
wLGS7EMxAoGBANqcSPE0aEHyemjR3DcaYv5e2oBDtCDcRKP85PDSnp47haXo0nBe
+iTJL6BCg6sohgF0Ft3xdj87dgn05Mo2NyyXbsdmjj32uQwvMkVPtNCN9KU4FVL0
11M/1ruRsYa6NNQ61zXwj8/MwepM3tOFJU+rofsNp8gpBiyTTxevAwcrAoGBAM63
Cv/lV4EU7njpPt0aIoCQoOQZGreTlqdQ/GgdA/1K+1LCw3dsEhzHZmmkrqQm3Vcr
wPpx0uXPjygWVzyfhFn/nDz4iNyXile3Bi+gCux2iwAWDzoJHkfP7bSNupI1uyuz
cub+D9lxzD5s4FgD2TfNSDjzV9hmlYeJC2zfnJ/5AoGBAMN0Rhc6dxi3VlCPiafO
tMGfRxa08cELj2dbPco/Vcg9iZG75yLHGDl5k1ZjAdwvABkelS9cqw9/91qRlVli
PdRllIs9m2G1TN+i9vxXdl+c/CYYTaB8/mQVSMUtTx8ZLxCthytX6QyukpYMopFV
kvV3i/ytydxBKZ8DGg0f0cI/AoGAB64vX3Ci/q3LanyoFEj7TTGSeMciAf4e09qj
di7VzhxyGBIadx5x0dXqzTQMNRcolCuRAP0nq9g5ZnDmDt+SaFGh+XX2h9OtlTK8
rRpSLZT99yParvpVwK9OEq5NZ09NxALn2wNHjXm37/3VnA+Qi406CLup4OV22tFR
tlrck+kCgYA4Y9dre8u9NYgnK/hPW7xze5vsCKEkRaH8+rnP2jdWYBPs3luvhL+t
Ijm3EBrPsJSNfZMSRNtqeKsd/ZUxAxy/LXKwTP2k6WjpXkLUVlYkgn25M4YHZIes
nERuoaKxLGuM+xAfNole2SPaakNYeWizS+y1Dm5iquHNHHjWH56XtA==
-----END RSA PRIVATE KEY-----
Reading the keys back in requires:
- reading data from the file
- decoding the pem encoding
- x509 parsing (rather than marshalling)
Task 2: Sign and Verify
Signing
Cryptographic signing is accomplished by computing a hash of the material to be signed, and encrypting that hash with a private key. We encrypt a hash rather than the actual text because public key crypto works only over small amounts of data, limited by key size.
For this project we want to include our signatures in the JSON text, meaning that we have a recursion problem: we can not sign something that includes the signature resulting from the signing. The answer is is that we will sign everything except the signature, and then just add the signature on the end.
We define signing on anything in JSON. Let's start with an example file recipe:
io> cli put blob.go
sha256_32_TFBQWYJ7DGDPXPMYUQI7XJCF73FYCB27DP75EPMLBAHXT4JYATEA====
io> cli desc last
{
"Name": "blob.go",
"Size": 3228,
"Mode": 420,
"ModTime": "2023-09-15T21:46:37.753398955-04:00",
"IsDir": false,
"Version": 1,
"PrevSig": "",
"ChildSigs": null,
"ChildNames": null,
"DataBlockSigs": [
"sha256_32_ZXF7E5X2VWUXNMY2ERBBEVG2KEKCB6KYMVBYFDQ5EQTTQBOMEUUA===="
]
}
Signing this blob gives us:
io> cli -q key.private sign last
sha256_32_63W7EPA6FACLUPU6N2NPVE2IQBUXV7UKIYNXVHRZEKGZJG7EX3EA====
io> cli desc last
{
"Name": "blob.go",
"Size": 3228,
"Mode": 420,
"ModTime": "2023-09-15T21:46:37.753398955-04:00",
"IsDir": false,
"Version": 1,
"PrevSig": "",
"ChildSigs": null,
"ChildNames": null,
"DataBlockSigs": [
"sha256_32_ZXF7E5X2VWUXNMY2ERBBEVG2KEKCB6KYMVBYFDQ5EQTTQBOMEUUA===="
]
,
"signature": "RMNDXPYJUASFBSYJ4XQDLLXG64SOEH32CTNMZE3VRXSRJXXHTZO74MRO3MBHRUJKSYMWRCFH22RB3PAGVU4PARQK223II2BFGDUBCMHRD5R4BOAOH2MK7LO446HRKVQHQX3ZT542DSTVSB6ZRA7ASSZLNXPS6WR3VKOED5OEGB7YG45X3LFTZPJUUBG7YRGT7NEXECCOPUNSETU2B2V45KGRSE3MNAU2E3WT62F36AH7YJR2IIVFECPBDJHMMQ7LW6EPQ5PHG4UXPRQJZ4GC37KHB4F44VQIOIMSMRFHQNILRM3DYW3KYQQUH2Y64TKCQAK6NSWCRGWAJO5RP32USYU7AJCDLKS6U6IW5GGKSPJLN55Y2ON2WKAVZVTEOUDMRPPZFVBTDQX63IIETRI53QZPKA======"
}
The signed blob differs only at the end. The procedure for generating the signature should be followed exactly:
- read in the private key
- trim all whitespace off the end of the JSON
- remove the last character (which should be '}')
- take the SHA256 hash
- call
rsa.SignPKCS1v15
on the hash result: this gives the raw signature - encode the signature using base32
- add
,\n\"signature\": \"
, the encoded signature, and"\n}
to the end of the truncated original blob
Verification is mostly the same, but in reverse. Left as an exercise for the reader:
io> cli -p key.public verify last
Verification of "sha256_32_63W7EPA6FACLUPU6N2NPVE2IQBUXV7UKIYNXVHRZEKGZJG7EX3EA====" w/ key.public succeeded
Task 3: Mutable signed bindings
Our series of projects so far describes a write-only store. Blobs can be removed, but the blob corresponding to a hash can not be changed. We'd like to support permanent names, called bindings, that can point to different content over time.
For example, you might define a root
binding that points to the current
root of a file system stored in your blobstore. You might define a
status
binding that shows the current status of your system. In
either case the binding binds the Name to a Value, where Value is the
sig containing the current content.
type Binding struct {
Name "status"
Value "sha256_32_63W7f...."
Date "Tue Nov 10 23:00:00 UTC 2009" // layout time.UnixDate
PublicKeyHash ".....something....." // discussed below
}
Ordinary file recipes are protected in the sense that their names are derived from their content; there is no way to modify either without detection.
Since a binding is designed to evolve over time, we need to have a different way of securing it. We therefore sign the JSON Binding, and include the public key in the JSON. Including the public key has two uses:
- First, public keys are publicly bound to individual principals, thereby identifying the binding's creator.
- Second, the binding is signed by the included public key's private key counterpart. Therefore a binding can be validated by using the included public key to verify the signature.
The following command creates a binding:
-p <publickey.fname> -q <privatekey.fname> binding <raw binding sig>
Say, for example, that we wish a permanent link to our server's status page. We can create an initial page as follows:
io:p3> cli put status.html
sha256_32_ZQOZMQ7KQ3LHR3OCHSBIUIUITUM3HJRNGG6WMTXHSENUQN54PIZA====
io:p3> cli desc last
{
"Name": "status.html",
"Size": 75,
"Mode": 420,
"ModTime": "2023-09-15T21:46:37.754750286-04:00",
"IsDir": false,
"Version": 1,
"PrevSig": "",
"ChildSigs": null,
"ChildNames": null,
"DataBlockSigs": [
"sha256_32_7JXCJT52ARJ7UWE5WOYUEHOP2LZOTSC6KFBZDOHIH7SIX37BQ7VA===="
]
}
This can also be retrieved at:
http://localhost:8000/sha256_32_7JXCJT52ARJ7UWE5WOYUEHOP2LZOTSC6KFBZDOHIH7SIX37BQ7VA====
The above work (w/ the http server listening to port 8000), but it's unwieldy and immutable. We can use a binding to make the status mutable and easier to type/remember:
io:p3> cli -p key.public -q key.private binding status last
status
io:p3> cli desc status
{
"Name": "status.html",
"Size": 75,
"Mode": 420,
"ModTime": "2023-09-15T21:46:37.754750286-04:00",
"IsDir": false,
"Version": 1,
"PrevSig": "",
"ChildSigs": null,
"ChildNames": null,
"DataBlockSigs": [
"sha256_32_7JXCJT52ARJ7UWE5WOYUEHOP2LZOTSC6KFBZDOHIH7SIX37BQ7VA===="
]
}
Also, note that the binding is automatically resolved to it's Value
. This
means that http://localhost:8000/status
now shows the current status page.
io:p3> curl 'localhost:8000/status'
<center>
<h1>Things are looking pretty Bleak.</h1>
We do not (yet) have a way to view an existing binding.
The public key property
The PublicKey
property of the binding is the hash of the
public key corresponding to the private key used to create the
signature. A hash can be used to verify something, but it can not be
reversed to get back the public key on which it is based.
Luckily, we have a means of mapping hashes back to hashed data: our store. Your code binding creation code, therefore, should also:
- write the PEM-encoded public key to the store
- put the resulting hash in the binding as the value for "PublicKeyHash"
dump
New command: Recall we can not view the binding we inserted using desc
, as it
will automatically resolve to the binding's value.
Create a new function dump
. dump
is identical to desc
except that it
retrieves blobs in a raw mode: returning the raw binding text rather than resolving
through to the binding Value
.
Implementation of the dump command requires an extra raw
parameter to be added to
BlobGetRequest
.
When raw
is false
, the server's local blob read should
check to see if the blob is a Binding
(format is JSON, and can successfully
be unmarshalled into a Binding
struct) and resolve this binding by
returning the blob pointed to by the binding's Value
property,
rather than the binding itself.
If raw
is true
, the binding itself should be returned.
io:~/818/projects/p3/solution> cli dump status
{
"Name": "status",
"Value": "sha256_32_55IEZUD6FH4FHUP5YCGXIP7SDGHZDY2RCKEV3EU45JUBLABVOZUA====",
"Date": "Sat Sep 30 21:42:21 EDT 2023",
"PublicKeyHash": "sha256_32_VDFWNRKFDLDLFMN52A54NSYPPVKR2EXMUFGREHKNPGGCLIMY3SWQ===="
,
"signature": "MFPBBVFRDCZHGBW3OIPZ2WUX43K7HIF23CVAHKFW75BSWBKAXJETNKAMYU6GKZ53N2KWYJ76K56QTH2EZMA46D6Y6QB3QFKEFEWWBNIQBVL63LIZQEV4SBXONJCT3YNAL7GOE5BPCQHMAC7ROGQ7AW3NUEMWLM5S65UIWWOJ22GIOYQNJOUR6FVBI3X5X7I55TCDI3QP2NREQG77B3YNBD6ZQSBLBSYYYJARYMA5DGBFGCO7ZXUEFXKGNR75ZE6HOZPD7VKLQ7S76WDZI7H6DVD56NA5DZH5HNUA7E4ZDYSYACDTKWPREOS3RJMICQ6GAJ3HJ2WYYKJJEZ4EXAD4LSV4KYMVUAX3LED2OOYRVYHDN5C6BN73GRXN4A5UKR5QGZJ5JCTOW2XOEUFYTTGVUAL6VY======"}
putraw
New command: You can check your verification by using the following public key
from above. Get
the public key hash of the actual key file contents, not the recipe
pointing to it, but defining a new command putraw <filename>
that
send a blob to the remote store, returns, and prints out the
resulting hash.
This is not what we want, as this hash is of the public key file's recipe:
io:~/818/projects/p3/solution> go run cli put key.public
sha256_32_JUWSBINGL4PNJIKTUOSCJDXBHVBSDZYORGYTCCA52MP7W7QFZGIA====
This is what we want: the hash of the data in the key file itself:
io:~/818/projects/p3/solution> go run cli putraw key.public
sha256_32_VDFWNRKFDLDLFMN52A54NSYPPVKR2EXMUFGREHKNPGGCLIMY3SWQ====
Note that this matches the public key hash in the status binding above. You can verify the signature now by creating
Updating a binding
A binding is updated by replacing it with a new binding using the same name.
We can update the current status shown above by overwriting the status
binding with a new blob name:
io:p3> cli put status2.html
sha256_32_XDZOUHLABSFPCFT456UMXZKKGODPOS4YKOMIGQZOKLQREYTOHJMQ====
io:p3> cli -p key.public -q key.private binding status last
status
io:p3> curl 'localhost:8000/status'
<center>
<h1>Things are all better now. </h1>
Task 4: Verify bindings
A binding can be verified by using PublicKeyHash
property to retrieve
the public key from the blob store, and then using that public key to
verify the binding's signature.
Do this by creating a new command: verifybinding <name>
.
Assuming the public key is in the store, as described above, you will
not need to pass either key to the blob client at the command line.
io:~/818/projects/p3/solution> cli verifybinding status
Verification of "status" w/ key.public succeeded
More pedantically, verify a binding as follows:
- retrieve the actual binding blob (not what it resolves to)
- unmarshal a binding object from the blob
- grab the object's
PublicKeyHash
property and retrieve the associated blob - write that blob to a file in /tmp
- verify the signature on the binding JSON w/ the saved public key
Servers and Trust
Most people of servers as trusted, and with good reason. Trusted servers make life easier. However, even if you trust your server provider's intent, no security is unbreakable.
Our server in this project might be categorized as "trust but verify". Anything we get back from the server can be verified or checked for tampering. Simple blobs must match their hashes. Signed JSON recipes or bindings can be verified.
However, we have no way of checking the server is not throwing blobs away, or returning "blob not found" which it is actually there.
In this course we will be discussing a couple papers ("SUNDr", "Spork") that develop completely untrusted servers. But not trusting your server has consequences, both in terms of complexity and performance. In practice, most systems are somewhere in the middle. For example, we could require clients to authenticate with the server, or require servers to validate signed blobs before accepting them.
Video walkthrough
Grading
I will give roughly even credit for each of the tasks.
Submit
Commit to the repository.