[ Introduction | Software Setup | Lab Specification | Design Guidelines | Hand-in Procedure | Collaboration Policy | Useful Functions ]
In this lab, you will build a simple utility that uses public-key cryptography to securely generate shared secret keys between two parties. You will then be able to reuse your code from Lab 1 to create encrypted files that only the other party (and you) will be able to decrypt. By doing the lab, you will familiarize yourself with the functionalities enabled by public-key cryptography, and with the some of the issues associated with key management and certification.
You will be using the same libraries as in Lab 1. We have set up the appropriate libraries and header files so that you can compile against (libdcrypt) on the Linux lab machines. If you are instead working on your own machine, you should install the dcrypt library in your system. (You will also need to have the GNU MP and dmalloc libraries installed in your machine.)
The files you will need for this lab are available in lab2.tar.gz. To install them in your account, download lab2.tar.gz and type the following:
% tar xzf lab2.tar.gz % cd lab2 % ls Makefile pv_misc.c skgu_cert.c skgu_nidh.c pv.h skgu.h skgu_misc.c skgu_pki.c % make gcc -g -O2 -ansi -Wall -Wsign-compare -Wchar-subscripts -Werror -I. -I/usr/local /include/ -I/home/nicolosi/cs579/devel/include/ -c skgu_pki.c ... gcc -g -O2 -ansi -Wall -Wsign-compare -Wchar-subscripts -Werror -o skgu_nidh skg u_nidh.o skgu_cert.o skgu_misc.o pv_misc.o -L. -L/usr/local/lib/ -L/home/nicolo si/cs579/devel/lib/ -ldcrypt -lgmp %
As we will see when discussing the lab
specification, the cryptographic scheme that you will implement
for this lab relies on a Public-Key Infrastructure. What we
mean by this is that, before starting to use the utility to generate
shared secrets and exchange encrypted files, both parties, Alice and
Bob, know the public key of a Certification Authority, whose job is to
produce certificates—signed statements binding identities
to public keys.
We have already implemented for you a simple certificate generation
utility to setup a toy Public-Key Infrastructure: take a look at its
source files (skgu_cert.c,
skgu_misc.c,
skgu_pki.c).
To use the skgu_pki utility to generate certificates,
first run it as follows:
% ./skgu_pki init
%
This will create a directory named .pki/ within the
current directory, along with two files: .pki/ca.sk and
.pki/ca.vk, holding keys needed respectively to sign and
to verify certificates.
Once the Public-Key Infrastructure has been initialized using the
init command for skgu_pki, you can issue
certificates using the cert command:
% ./skgu_pki cert -g alice.priv alice.pub alice
%
This will create three files: alice.priv,
alice.pub and alice.cert. The first two
files store a newly generated private key/public key pair for
non-interactive Diffie-Hellman key generation (cf.
the lab specification). The last file contains
a certificate binding the public key just placed
in alice.pub to the identity alice. Such
certificate is signed using the signing key that was generated the
last time you ran skgu_pki init.
If you don't provide the -g option, then skgu_pki
cert will not generate a new private/public key pair for you.
This is useful so that you can create certificates for existing
public keys as well: for example, Alice might want to create a
certificate (under her certification authority) for a public key that
Bob got from his own certification authority.
You can choose a different name for the certificate file using the
-o option:
% ./skgu_pki cert -o alice.cert.new -g alice.priv alice.pub alice
% ls alice.cert*
alice.cert alice.cert.new
By default, certificates generated with skgu_pki cert are
valid for 30 days. You can change this behavior using the
-e option; for example, the following creates a
certificate set to expire in 100 days:
% ./skgu_pki cert -e 100 -g bob.priv bob.pub bob
%
To check the validity of a certificate created using skgu_pki
cert, give the check command to skgu_pki:
% ./skgu_pki check alice.cert alice.pub alice
Valid certificate
Your shared key generation utility will consist of a single program,
skgu_nidh, implementing a
non-interactive variant of the Diffie-Hellman protocol on top
of a toy Plublic-Key Infrastructure.
After completing coding of skgu_nidh, you will build the
utility using make with the provided Makefile:
% make gcc -g -O2 -ansi -Wall -Wsign-compare -Wchar-subscripts -Werror -I. -I/usr/local /include/ -I/home/nicolosi/cs579/devel/include/ -c skgu_nidh.c gcc -g -O2 -ansi -Wall -Wsign-compare -Wchar-subscripts -Werror -o skgu_nidh skg u_nidh.o skgu_cert.o skgu_misc.o pv_misc.o -L. -L/usr/local/lib/ -L/home/nicolo si/cs579/devel/lib/ -ldcrypt -lgmp %
(Notice that you may have to fiddle with the Makefile if you are working on
your own linux box.)
Now, initialize the PKI (if you haven't done so yet), and create certificates for Alice and Bob:
% ./skgu_pki init % ./skgu_pki cert -g alice.priv alice.pub alice % ./skgu_pki cert -g bob.priv bob.pub bob %
Next, use Alice's private key with Bob's public key:
% ./skgu_nidh alice.priv alice.cert alice bob.pub bob.cert bob example1 % ls example1.* example1.b64 % cat example1.b64 ahPKOu+Nb5xLBhubOOVp+n7c/HS9vjAeMGuQxQu7QcU= % rm example1.b64 %
A brief explanation of what should happen here is the following.
skgu_nidh imports the public key in bob.pub
and the certificate in bob.cert, and checks that the
certificate is valid for the public key with respect to the id
bob. Next, the private key in alice.priv
and the corresponding certificate in alice.cert are
loaded, and a check is made to ensure that the certification
authority that issued bob's certificate is the same as the one that
issued alice's. (Note that in general this is not the only case in
which alice should trust someone's certificate, but for simplicity
will stick with that for this lab.)
If the certificate checks out, skgu_nidh computes the
shared key (as described below), and stores it
base64-encoded in example1.b64 (or
skgu_key.b64, if no label is specified as last argument
to skgu_nidh).
Using Bob's private key with Alice's public key should result in the same outcome:
% ./skgu_nidh bob.priv bob.cert bob alice.pub alice.cert alice example1 % ls example1.* example1.b64 % cat example1.b64 ahPKOu+Nb5xLBhubOOVp+n7c/HS9vjAeMGuQxQu7QcU= %
In a non-interactive Diffie-Hellman key exchange, each party is
assumed to have preliminary obtained (e.g.,, via email) the
public key for the other party, certified under the signing key of a
mutually trusted certification authority. After having verified the
certificate and the public key for the other party, you will derive
the shared key Ks according to the following
equation:
Km = SHA1 (DH(Alice.pub,Bob.pub), first_id, second_id) |
Ks0 = HMAC-SHA1 (Km, label, "AES-CBC")
|
Ks1 = HMAC-SHA1 (Km, label, "HMAC-SHA1")
|
Ks = < concatenation of first 16 bytes
of Ks0 with first 16 bytes
of Ks1 >
|
DH(.,.) here represents the Diffie-Hellman function:
DH(ga mod p, gb mod p) =
gab mod p. first_id and
second_id correspond's to the id used for the two parties
in the command line of skgu_nidh (in lexicographical
order, to ensure that both parties will be computing the same value).
The string label is a piece of side information that is
specified on the command line of skgu_nidh (defaults to
"skgu_key" when no label is specified). The two strings
"AES-CBC" and "HMAC-SHA1" are needed to make
sure that two chunks of pseudo-random bytes can be derived from the
master key, as per the design
guidelines of Lab 1.
Note that an eavesdropper who knows both public keys (but neither
private key) can easily obtain the values ya=ga
mod p and yb=gb mod p:
however, it is widely believed that no efficient computation can
recover DH(ya,yb)=gabmod
p: this is known as the Computational Diffie-Hellman
Assumption.
You must submit two files:
To build a software distribution, run the following commands (from the directory where your source files are located):
% cd .. % tar cf skgu.tar lab2/ % gzip skgu.tar %
(If the name of the directory containing your sources is not
lab2, then substitute the appropriate name in the
second command above.)
To create a script file, use the script command. When you run script, everything you type gets saved in a file called typescript. Press CTRL-D to finish the script. For example:
% script Script started, output file is typescript % ./skgu_pki init % ./skgu_pki cert -g alice.priv alice.pub alice % ./skgu_pki cert -g bob.priv bob.pub bob % ./skgu_nidh alice.priv alice.cert alice bob.pub bob.cert bob example1 % ls example1.* example1.b64 % cat example1.b64 ahPKOu+Nb5xLBhubOOVp+n7c/HS9vjAeMGuQxQu7QcU= % rm example1.b64 % % ./skgu_nidh bob.priv bob.cert bob alice.pub alice.cert alice example1 % ls example1.* example1.b64 % cat example1.b64 ahPKOu+Nb5xLBhubOOVp+n7c/HS9vjAeMGuQxQu7QcU= % % ^D % ^D Script done, output file is typescript %
To turn in your distribution and script file, e-mail the files
skgu.tar.gz and typescript to me at
"nicolosi AT cs DOT stevens DOT edu"
by 11:55pm on Thursday, May 7, 2009.
This completes the lab.
You must write all the code you hand in for the programming assignments, except for code that we give you as part of the assigment. You are not allowed to look at anyone else's solution. You may discuss the assignments with other students, but you may not look at or copy each others' code.
Below is a description of some of the functions implemented in the dcrypt library that you may find useful in completing the assignment. You will need to include the dcrypt.h header file to access these functions. You may also want to take a look at these sample programs (tst.c, tst_sha1.c) to see some of these functions in action.
void putint (void *dp, u_int32_t val);
void puthyper (void *dp, u_int64_t val);putint function puts the 32-bit integer value of
val into memory in big-endian order at location
dp. dp does not need to be aligned. The
bytes stored at dp will be the same on big- and
little-endian machines. puthyper is like
putint but puts a 64-bit value into 8 bytes of memory.
u_int32_t getint (const void *dp);
u_int64_t gethyper (const void *dp);getint and gethyper routines retrieve
values stored by putint and puthyper
respectively.
char *armor64 (const void *dp, size_t len);len bytes from the binary string pointed by
dp to a longer, base-64, printable ASCII string. You
will need to use this to transform random session keys (which could
contain zero-bytes) into a NULL-terminated ANSI C string.
ssize_t dearmor64 (void *out, const char *s);armor64 function, and return the number of
bytes that were placed at out. The return value is
negative if the NULL-terminated ANSI C string
s is not the output of armor64.
ssize_t armor64len (const char *s);s. If some prefix of s represents a valid
armor64 string, then the length of such prefix is returned. Otherwise,
-1 is returned, indicating that s is not the output of armor64.
ssize_t dearmor64len (const char *s);s. If some prefix of s represents a valid
armor64 string, then the length of the decoded data that would result
by "dearmoring" s is returned. Otherwise, -1 is
returned, indicating that s is not the output of armor64.
int cat_mpz (char **dstp, const MP_INT *mp);\0'-terminated
char *) with the hex-representation of the bignum held in
mp, and makes *dstp point to that.
mpz_powm (mpz_ptr r, mpz_srcptr b, mpz_srcptr e,
mpz_srcptr m);dst the result of computing
be mod m.
mpz_t, which can be automatically
converted into mpz_ptr and mpz_srcptr.)
The SHA-1 [FIPS-180-1] hash function hashes an arbitrary-length input (up to 2^64 bytes) to a 20-byte output. SHA-1 is known as a cryptographic hash function. While nothing has been formally proven about the function, it is generally assumed that SHA-1 is one-way and collision-resistant. These properties are defined as follows:
For someone who steals the file of password hashes, there is no known way of recovering passwords more efficient than guessing passwords and verifying the guesses. (Of course, the fact that users often choose easily-guessed passwords is a problem.)
Collision-resistant functions have many uses, stemming from the fact that the short output value effectively uniquely specifies an arbitrary-length input. One cannot recover the input from the output, but given the input, one can verify that it does, indeed, match the output. One might, for instance, implement a web cache in which contents is indexed by a SHA-1 hash of the URL. Having fixed-length names for stored content would simplify the implementation.
The libraries you are using contain an implementation of SHA-1.
void sha1_hash (void *digest, const void *buf, size_t
len);len bytes of data at buf, and places
the resulting 20 bytes at digest.
Sometimes the input that you want to hash is so long that it is
inconvenient to store it entirely in memory before being able to
hash it. This is the case for example when hashing the entire content
of a file into a short digest.
For this reason, the libraries you are using allow you to process a
long input "one chunk at a time." To do that, you should use a
struct called sha1_ctx, which will store the
"partial digest" as you keep providing new input to be hashed. You
should manipulate sha1_ctx structs with the
following functions:
void sha1_init (sha1_ctx *sc);sha1_ctx struct that will contain the
partial hash.
void sha1_update (sha1_ctx *sc, const void *data, size_t
len);len bytes at data to the input being
hashed, but does not produce a result. Thus, one can hash a large
amount of data without having it all in memory, by calling
sha1_update on one chunk at a time.
void sha1_final (sha1_ctx *sc, void *digest);digest.
Pseudo-Random Functions (PRFs) are a deterministic symmetric-key primitive that securely "stretches" a single (pseudo-)random "master" key into several (computationally) independent pseudo-random keys. (In fact, the amount of derivable pseudo-random keys is exponential in the bitlength of the master key, something not possible with Pseudo-Random Number Generators).
You use PRFs as follows. Let's say that you want to derive a
key k_s for a specific purpose from a master
key k_m. You cook up a string, or label,
to represent the task at hand. Any convention would do, as long as
the labels correspond uniquely to the tasks. One possible convention
is the following: Say that the key that you are deriving will be used
to encrypt files from Alice to Bob. Then you would use a label like:
"MY-APP:task=encryption,opt=(sndr=alice,rcvr=bob)".
(In fact, it would be a good idea to also include some timestamp
information in the label. That complicates slightly the process of
reconstructing the label at a later time, so we omit that from this
toy example.)
Then, you use the master key k_m to key the PRF, and
give the label as input to the PRF. The resulting output can be
safely used as a key, and it will have the property of looking random
to any (efficient) attackers who knows the label, and possibly even the
keys derived from other labels, as long as the attacker has
never seen the master key.
As it turns out, the Keyed-Hash Message Authentication Code (HMAC) [FIPS-198a] is a not just a secure Message Authentication Code, but also a secure pseudo-random function, based on the use of any cryptographic hash function, like SHA-1. The libraries you are using contain an implementation of HMAC, instantiated with the SHA-1 cryptographic hash function.
void hmac_sha1 (const char *key, size_t keylen, void *out, const void
*data, size_t dlen);len bytes of data at
buf using the key key, and places
the resulting 20 bytes at out.
Similarly to what discussed for the case of SHA-1, the libraries you are using allow you to process a long input "one chunk at a time."
void hmac_sha1_init (const char *key, size_t keylen,
sha1_ctx *sc);sha1_ctx struct that will contain the
partial HMAC under the key key.
void hmac_sha1_update (sha1_ctx *sc, const void *data, size_t
len);len bytes from data to the input being
hmac'ed, but does not produce a result. Thus, one can hmac a large
amount of data without having it all in memory, by calling
hmac_sha1_update on one chunk at a time.
void hmac_sha1_final (const char *key, size_t keylen,
sha1_ctx *sc, void *out);out. key used in
hmac_sha1_final is different from the one initially used in
hmac_sha1_init.
If you only used symmetric-key cryptography, you would need to
exchange a secret key with all the friends with whom you want to have
confidential communication. With Public-Key Cryptography, instead,
you can give all your friend the same public key, and they will be
able to send you encrypted content that only you can recover.
Similarly, you only need to know your friend's public key to create a
ciphertext that only he/she will be able to decrypt.
The libraries you are going to use contain an implementation of two
very well-known Public-Key Encryption schemes: Rabin [Wil80] and
ElGamal [ElG86].
Although you won't need them to complete the assignment, below we
describe the interface provided by libdcrypt:
dckey *dckeygen (const char *type, size_t k, const char
*extra);struct that holds a new private
key/public key pair, of the type specified by type. The
value of type should be one of the constants defined in
dcrypt.h, namely
DC_ELGAMAL or DC_RABIN.
The cryptographic strength of the key pair generated can be tuned
using the parameter k: a value of at least 1024 is
recommended to get a reasonable level of security.
The value of extra should be either NULL, or
an ASCII string representing information about the parameters that
should be used in generating the private key/public key pair.
For
this assignment, you can always supply a value of NULL.
Notice that the dckeygen function internally makes use of
the pseudo-random number generator provided by libdcrypt:
therefore, you shouldn't call dckeygen before
initializing the pseudo-random number generator with prng_seed.
char *dcexport_pub (const dckey *key);
char *dcexport_priv (const dckey *key);dcexport_pub and dcexport_priv functions
return a base-64, printable, NULL-terminated ANSI C
string representing the public key or private key stored within
key, respectively.
dckey *dcimport_pub (const char *asc);
dckey *dcimport_priv (const char *asc);dcimport_pub and dcimport_priv functions
retrieve the dckey that was previously exported into
the ASCII string asc using dcexport_pub or
dcexport_priv, respectively.
If asc is not of the form expected,
dcimport_pub and dcimport_priv return
NULL.
char *dcencrypt (const dckey *key, const char *msg);dcencrypt uses the public key contained in
key to transform the plaintext data contained in the
NULL-terminated ANSI C string msg into a
NULL-terminated ANSI C ciphertext string, which is
returned as output.
The encryption algorithm used is CCA-secure.
Since dcencrypt expects the plaintext to be a
NULL-terminated ANSI C string, you should use
the armor64 function if you need to encrypt an arbitrary
bit string (such as a secret key for AES).
char *dcdecrypt (const dckey *key, const char *cmsg);aes_decrypt decrypts the ciphertext contained in the
NULL-terminated ANSI C string cmsg,
inverting the dcencrypt function.
void dcfree (dckey *key);key.
Once you are done with a private key, you
should always wipe out the memory location where it was stored!
int dcispriv (const dckey *key);key
is a private key; otherwise it returns 0.
| [ElG85] |
T. ElGamal, A public key cryptosystem and a signature
scheme based on discrete logarithms. IEEE Transactions on Information Theory, Vol. IT-31, No. 4, pp. 469--472, July 1985. |
|
|---|---|---|
| [FIPS-180-1] |
FIPS-180-1, Secure Hash Standard. U.S. Department of Commerce/N.I.S.T., 1994 |
|
| [FIPS-197] |
FIPS-197, Announcing the Advanced Encryption Standard. U.S. Department of Commerce/N.I.S.T., 2001 |
|
| [FIPS-198a] |
FIPS-198a, The Keyed-Hash Message Authentication Code
(HMAC). U.S. Department of Commerce/N.I.S.T., 2002 |
|
| [Wil80] |
H. C. Williams, A Modification of the RSA Public-Key
Encryption Procedure. IEEE Transactions on Information Theory, Vol. IT-26, No. 6, November 1980. |