Commit f4cca17f authored by Robert Ricci's avatar Robert Ricci
Browse files

Merge branch 'tpm-tmcd'

Conflicts:
	db/libdb.pm.in

Conflict resolved by using libdb.pm.in from master, and moving new
constants committed on the tpm-tmcd branch to the new
EmulabConstants.pm.in file.
parents 9b623f72 4cbd3447
......@@ -94,12 +94,14 @@ use vars qw(@ISA @EXPORT);
TBDB_NODESTATE_PXEFAILED TBDB_NODESTATE_PXELIMBO
TBDB_NODESTATE_PXEBOOTING TBDB_NODESTATE_ALWAYSUP
TBDB_NODESTATE_MFSSETUP TBDB_NODESTATE_TBFAILED
TBDB_NODESTATE_POWEROFF
TBDB_NODESTATE_POWEROFF TBDB_NODESTATE_SECVIOLATION
TBDB_NODESTATE_TPMSIGNOFF
TBDB_NODEOPMODE_NORMAL TBDB_NODEOPMODE_DELAYING
TBDB_NODEOPMODE_UNKNOWNOS TBDB_NODEOPMODE_RELOADING
TBDB_NODEOPMODE_NORMALv1 TBDB_NODEOPMODE_MINIMAL TBDB_NODEOPMODE_PCVM
TBDB_NODEOPMODE_RELOAD TBDB_NODEOPMODE_RELOADMOTE TBDB_NODEOPMODE_RELOADPCVM
TBDB_NODEOPMODE_RELOAD TBDB_NODEOPMODE_RELOADMOTE
TBDB_NODEOPMODE_SECURELOAD TBDB_NODEOPMODE_RELOADPCVM
TBDB_NODEOPMODE_DELAY
TBDB_NODEOPMODE_BOOTWHAT
TBDB_NODEOPMODE_ANY
......@@ -399,6 +401,9 @@ sub TBDB_NODESTATE_PXEWAKEUP() { "PXEWAKEUP"; }
sub TBDB_NODESTATE_PXEFAILED() { "PXEFAILED"; }
sub TBDB_NODESTATE_PXEBOOTING() { "PXEBOOTING"; }
sub TBDB_NODESTATE_POWEROFF() { "POWEROFF"; }
sub TBDB_NODESTATE_TPMSIGNOFF() { "TPMSIGNOFF"; }
sub TBDB_NODESTATE_SECVIOLATION(){ "SECVIOLATION"; }
sub TBDB_NODESTATE_MFSBOOTING() { "MFSBOOTING"; }
sub TBDB_NODEOPMODE_ANY { "*"; } # A wildcard opmode
sub TBDB_NODEOPMODE_NORMAL { "NORMAL"; }
......@@ -410,6 +415,7 @@ sub TBDB_NODEOPMODE_MINIMAL { "MINIMAL"; }
sub TBDB_NODEOPMODE_PCVM { "PCVM"; }
sub TBDB_NODEOPMODE_RELOAD { "RELOAD"; }
sub TBDB_NODEOPMODE_RELOADMOTE { "RELOAD-MOTE"; }
sub TBDB_NODEOPMODE_SECURELOAD { "SECURELOAD"; }
sub TBDB_NODEOPMODE_RELOADPCVM { "RELOAD-PCVM"; }
sub TBDB_NODEOPMODE_DELAY { "DELAY"; }
sub TBDB_NODEOPMODE_BOOTWHAT { "_BOOTWHAT_"; } # A redirection opmode
......
Some notes about the TPM-enforced boot path (SECURELOAD state machine).
The current implementation requires that we boot from a flash device
BEFORE we network boot (see the paper for details). Unfortunately,
there is a lot of magic associated with the PXEBOOTING state, which is
assumed to always be the first state we will see when a node boots.
However, in the secure boot path the first thing we see is a secure
transition to the GPXEBOOTING state, so magic had to be added for that!
In particular:
* When stated gets a transition to GPXEBOOTING, it forces the node into
the SECURELOAD op_mode. This is a new trigger called SECURELOAD and
a new trigger table entry:
insert into state_triggers values \
('*','*','GPXEBOOTING','SECURELOAD');
* Later, when we do get a PXEBOOTING state, we DON'T push the machine
into the PXEBOOT op_mode. We do this with an override trigger:
insert into state_triggers values \
('*','SECURELOAD','PXEBOOTING','SECURELOAD');
This is supposed to override the more general any ('*') op_mode trigger
for the PXEBOOTING state and will just make sure we state in SECURELOAD.
......@@ -140,6 +140,9 @@ my $TB_OSID_MBKERNEL = TB_OSID_MBKERNEL;
# Special PXEBOOT state machine that all local nodes use.
my $PXEKERNEL = "PXEKERNEL";
# Even special-er SECURELOAD state machine that local nodes may use.
my $SECURELOAD = "SECURELOAD";
# Protos.
sub debug(@);
sub fatal($);
......@@ -625,6 +628,14 @@ sub stateTransition($$) {
!$valid{$mode}{$oldstate}{$newstate}) {
notify("Invalid transition for node $node from $mode/$oldstate " .
"to $newstate\n");
# XXX: Bad hack for TPM booting paper: this ought to be in the
# database, not harcoded here for a specific op_mode
# Let specifc op_modes drive nodes into particular states on invalid
# transitions
if ($mode eq TBDB_NODEOPMODE_SECURELOAD) {
$newstate = TBDB_NODESTATE_SECVIOLATION;
notify("Moving $node to $newstate because it's in $mode\n");
}
}
my $now = time();
......@@ -718,6 +729,19 @@ sub stateTransition($$) {
}
next;
};
/^SECURELOAD$/ && do {
#
# Force machine into the SECURELOAD op_mode.
# Currently triggered only by receipt of GPXEBOOTING state.
#
debug("Running $SECURELOAD trigger\n");
if ($mode ne $SECURELOAD) {
info("$node: Forcing mode transition into $SECURELOAD!\n");
opModeTransition($node, $SECURELOAD, 1);
$mode=$SECURELOAD;
}
next;
};
/^$BOOTING$/ && do {
#
# See if we are in the right mode/osid.
......@@ -823,6 +847,18 @@ sub stateTransition($$) {
handleCommand($node,$trig);
next;
};
(/^$TBPOWEROFF$/) && do {
handleCommand($node,$TBPOWEROFF);
next;
};
(/^EMAILNOTIFY$/) && do {
SENDMAIL($REALTBOPS,
"STATED: $node entered state $newstate",
"$node entered state $mode/$newstate from " .
"$mode/$oldstate",
"Stated Daemon <".$TBOPS.">");
next;
};
/^RELOADOLDMFS$/ && do {
my $frisbee_osid = TBNodeDiskloadOSID($node);
my $frisbee_name = DBQuerySingleFatal("select osname from os_info where osid=$frisbee_osid");
......@@ -1040,6 +1076,11 @@ sub handleCtrlEvent($$) {
/^$TBTIMEOUTNOTIFY/ && do {
notify("Node $node has timed out in state $mode/$state\n");
last; };
/^STATE:([-\w\/]+)$/ && do {
my $newstate = $1;
# Force the node into a new state
stateTransition($node,$newstate);
last; };
notify("$node: Unknown Timeout Action: $action\n");
}
next;
......
......@@ -152,13 +152,13 @@ dostype-debug: dostype.o
ssl.o: ssl.c ssl.h decls.h
tpm.o: tpm.c tpm.h
$(CC) -c $(CFLAGS) $(TESTBED_SRCDIR)/tmcd/tpm.c \
tpm.o: tpm.c tpm.h ssl.h
$(CC) -c $(CFLAGS) $(SSLFLAGS) $(TESTBED_SRCDIR)/tmcd/tpm.c \
-I$(TESTBED_SRCDIR)/tmcd/
notpm: tpm.c tpm.h
$(CC) -c $(CFLAGS) -DTPMOVERRIDE $(TESTBED_SRCDIR)/tmcd/tpm.c \
-I$(TESTBED_SRCDIR)/tmcd/
notpm: tpm.c tpm.h ssl.h
$(CC) -c $(CFLAGS) $(SSLFLAGS) -DTPMOVERRIDE \
$(TESTBED_SRCDIR)/tmcd/tpm.c -I$(TESTBED_SRCDIR)/tmcd/
version.c: tmcd.c
echo >$@ "char build_info[] = \"Built `date +%d-%b-%Y` by `id -nu`@`hostname | sed 's/\..*//'`:`pwd`\";"
......
......@@ -28,7 +28,9 @@
#include <unistd.h>
#include <netdb.h>
#include <openssl/engine.h>
#include <openssl/rsa.h>
#include <openssl/ssl.h>
#include <openssl/sha.h>
#include <openssl/err.h>
#include "decls.h"
#include "ssl.h"
......@@ -90,6 +92,7 @@ static char nosslbuf[MAXTMCDPACKET];
static int nosslbuflen, nosslbufidx;
static void tmcd_sslerror();
static void tmcd_sslprint(const char *fmt, ...);
static RSA * convpubkey(struct pubkeydata *);
/*
* Init our SSL context. This is done once, in the parent.
......@@ -649,6 +652,89 @@ tmcd_sslrowtocert(char *in, char *nid)
return local;
}
/*
* SHA1 hashes src of length len and stores the result at dst. dst must be at
* least 20 bytes long.
*/
int
tmcd_quote_hash(void *src, size_t len, void *dst)
{
if (SHA1(src, len, dst) == NULL)
return 1;
return 0;
}
/*
* Verifies that buffer sig of length siglen is indeed the buffer final
* encrypted with the private key to pubkey (or checks the signature of buffer
* sig).
*
* Returns 1 if the signature verified, 0 if it failed
*/
int
tmcd_quote_verifysig(void *final, void *sig, size_t siglen, void *pubkey)
{
struct keydata k;
RSA *rsa;
int ret;
if (!pubkey) {
error("NULL pubkey to %s\n", __FUNCTION__);
return 0;
}
if (!final) {
error("NULL final to %s\n", __FUNCTION__);
return 0;
}
if (!sig) {
error("NULL sig to %s\n", __FUNCTION__);
return 0;
}
/* Cannot fail */
tpm_extract_key((unsigned char *)pubkey, &k);
rsa = convpubkey(&k.pub);
if (!rsa) {
error("Error extracting and converting key\n");
return 0;
}
ret = RSA_verify(NID_sha1, final, 20, sig, siglen, rsa);
RSA_free(rsa);
return ret;
}
static RSA *
convpubkey(struct pubkeydata *k)
{
RSA *rsa;
BIGNUM *mod;
BIGNUM *exp;
/* create the necessary structures */
rsa = RSA_new();
mod = BN_new();
exp = BN_new();
if (rsa == NULL || mod == NULL || exp == NULL) {
if (rsa)
RSA_free(rsa);
if (mod)
BN_free(mod);
if (exp)
BN_free(exp);
return NULL;
}
/* convert the raw public key values to BIGNUMS */
BN_bin2bn(k->modulus, k->keylength, mod);
BN_bin2bn(k->exponent, k->expsize, exp);
/* set up the RSA public key structure */
rsa->n = mod;
rsa->e = exp;
return rsa;
}
/*
* Log an SSL error
*/
......
......@@ -17,6 +17,8 @@ int tmcd_sslwrite(int sock, const void *buf, size_t nbytes);
int tmcd_sslread(int sock, void *buf, size_t nbytes);
int tmcd_sslclose(int sock);
int tmcd_sslverify_client(char *, char *, char *, int);
int tmcd_quote_hash(void *, size_t, void *);
int tmcd_quote_verifysig(void *, void *, size_t, void *);
X509* tmcd_sslgetpeercert(void);
X509* tmcd_sslrowtocert(char*, char*);
int isssl;
......
......@@ -41,6 +41,9 @@
#include "event.h"
#endif
/* XXX: Not sure this is okay! */
#include "tpm.h"
/*
* XXX This needs to be localized!
*/
......@@ -111,6 +114,10 @@ CHECKMASK(char *arg)
#define HOSTID_SIZE (32+64)
#define DEFAULT_DBNAME TBDBNAME
/* For secure disk loading */
#define SECURELOAD_OPMODE "SECURELOAD"
#define SECURELOAD_STATE "RELOADSETUP"
int debug = 0;
static int verbose = 0;
static int insecure = 0;
......@@ -293,6 +300,9 @@ COMMAND_PROTOTYPE(dotpmblob);
COMMAND_PROTOTYPE(dotpmpubkey);
COMMAND_PROTOTYPE(dotpmdummy);
COMMAND_PROTOTYPE(dodhcpdconf);
COMMAND_PROTOTYPE(dosecurestate);
COMMAND_PROTOTYPE(doquoteprep);
COMMAND_PROTOTYPE(doimagekey);
/*
* The fullconfig slot determines what routines get called when pushing
......@@ -395,6 +405,9 @@ struct command {
{ "tpmpubkey", FULLCONFIG_ALL, 0, dotpmpubkey },
{ "tpmdummy", FULLCONFIG_ALL, F_REQTPM, dotpmdummy },
{ "dhcpdconf", FULLCONFIG_ALL, 0, dodhcpdconf },
{ "securestate", FULLCONFIG_NONE, F_REMREQSSL, dosecurestate},
{ "quoteprep", FULLCONFIG_NONE, F_REMREQSSL, doquoteprep},
{ "imagekey", FULLCONFIG_NONE, F_REQTPM, doimagekey},
};
static int numcommands = sizeof(command_array)/sizeof(struct command);
......@@ -1350,6 +1363,7 @@ static int checkcerts(char *nid)
return ret;
}
/*
* Accept notification of reboot.
*/
......@@ -4435,6 +4449,8 @@ COMMAND_PROTOTYPE(donseconfigs)
COMMAND_PROTOTYPE(dostate)
{
char newstate[128]; /* More then we will ever need */
MYSQL_RES *res;
int nrows;
int i;
#ifdef EVENTSYS
address_tuple_t tuple;
......@@ -4452,6 +4468,37 @@ COMMAND_PROTOTYPE(dostate)
error("DOSTATE: %s: Bad arguments\n", reqp->nodeid);
return 1;
}
/*
* Check to make sure that this is not a state that must be reported
* by the securestate mechanism - we can tell because there are one
* or more PCR values required in the tpm_quote_values table for
* the state.
*/
res = mydb_query("select q.pcr from nodes as n "
"left join tpm_quote_values as q "
"on n.op_mode = q.op_mode "
"where n.node_id='%s' and q.state ='%s'",
1, reqp->nodeid,newstate);
if (!res){
error("state: %s: DB error check for pcr list\n",
reqp->nodeid);
return 1;
}
nrows = mysql_num_rows(res);
mysql_free_result(res);
if (nrows){
error("state: %s: tried to go into secure state %s using "
"insecure state command\n",reqp->nodeid,newstate);
return 1;
// XXX Probably should send a SECVIOLATION state and/or send
// mail, but this needs more thought before making it the
// default action.
}
/*
* Sanity check. No special or weird chars.
*/
......@@ -4462,7 +4509,7 @@ COMMAND_PROTOTYPE(dostate)
return 1;
}
}
#ifdef EVENTSYS
/*
* Send the state out via an event
......@@ -4493,6 +4540,577 @@ COMMAND_PROTOTYPE(dostate)
}
/* There are probably classic functions available to do this but I couldn't
* find one that will convert two bytes of ACII to one byte. sscanf writes a
* full int and atoi gives us an int too
*/
static unsigned char hextochar(char *in)
{
unsigned char lh, rh;
lh = in[0];
if (lh >= '0' && lh <= '9')
lh = lh - '0';
else if (lh >= 'A' && lh <= 'F')
lh = lh - 'A' + 10;
else if (lh >= 'a' && lh <= 'f')
lh = lh - 'a' + 10;
rh = in[1];
if (rh >= '0' && rh <= '9')
rh = rh - '0';
else if (rh >= 'A' && rh <= 'F')
rh = rh - 'A' + 10;
else if (rh >= 'a' && rh <= 'f')
rh = rh - 'a' + 10;
return (lh << 4) | rh;
}
static int ishex(char in)
{
return ((in >= 'a' && in <= 'f') || (in >= 'A' && in <= 'F') ||
(in >= '0' && in <= '9'));
}
/*
* Report that the node has entered a new state - secure version: the report
* includes a TPM quote that will be checked against the database.
* If this check fails, we report a SECVIOLATION event instead, and tell the
* client so.
* TODO: Should probably reduce code duplication from dostate()
*/
COMMAND_PROTOTYPE(dosecurestate)
{
char newstate[128]; /* More then we will ever need */
char quote[1024];
char pcomp[256];
unsigned char quote_bin[256];
unsigned char pcomp_bin[128];
ssize_t pcomplen, quotelen;
int quote_passed;
char result[16];
ETPM_NONCE nonce;
MYSQL_RES *res;
MYSQL_ROW row;
int nrows;
unsigned long *nlen;
int i,j;
unsigned short wantpcrs;
TPM_PCR *pcrs;
#ifdef EVENTSYS
address_tuple_t tuple;
#endif
/*
* Dig out state that the node is reporting and the quote
*/
if (rdata == NULL ||
sscanf(rdata, "%127s %1023s %255s", newstate, quote, pcomp) != 3 ||
strlen(newstate) + 1 == sizeof(newstate) || strlen(quote) + 1 ==
sizeof(quote) || strlen(pcomp) + 1 == sizeof(pcomp)) {
error("SECURESTATE: %s: Bad arguments\n", reqp->nodeid);
return 1;
}
// Have to covert the hex representations of quote and pcomp into
// simple binary
if ((strlen(quote) % 2) != 0) {
error("SECURESTATE: %s: Malformed quote: odd length\n");
return 1;
}
quotelen = strlen(quote)/2;
printf("quotelen is %d\n",quotelen);
for (i = 0; i < quotelen; i++) {
if (!ishex(quote[i * 2]) || !ishex(quote[i * 2 + 1])) {
error("Error parsing quote\n");
// XXX: Send error to client
return 1;
}
quote_bin[i] = hextochar(&quote[i * 2]);
}
if ((strlen(pcomp) % 2) != 0) {
error("SECURESTATE: %s: Malformed pcomp: odd length\n");
return 1;
}
pcomplen = strlen(pcomp)/2;
for (i = 0; i < pcomplen; i++) {
if (!ishex(pcomp[i * 2]) || !ishex(pcomp[i * 2 + 1])) {
error("Error parsing pcomp\n");
// XXX: Send error to client
return 1;
}
pcomp_bin[i] = hextochar(&pcomp[i * 2]);
}
/*
* Pull the nonce out, verify the exipration date, and clear it so that
* it can't be used again.
*/
res = mydb_query("select nonce, (expires >= UNIX_TIMESTAMP()) "
"from nonces "
"where node_id='%s' and purpose='state-%s'",
2, reqp->nodeid,newstate);
if (!res){
error("SECURESTATE: %s: DB error getting nonce\n",
reqp->nodeid);
return 1;
}
nrows = mysql_num_rows(res);
if (!nrows){
error("%s: no nonce in database for this node.\n",
reqp->nodeid);
mysql_free_result(res);
// XXX: return error to client
return 1;
}
// Delete from the database so that it can't be used again
mydb_update("delete from nonces where node_id='%s' and "
"purpose='state-%s' ", reqp->nodeid,newstate);
row = mysql_fetch_row(res);
nlen = mysql_fetch_lengths(res);
// XXX: Check to make sure the expire check is working
if (strcmp(row[1],"1") != 0) {
error("SECURESTATE: %s: Nonce is expired\n");
mysql_free_result(res);
// XXX: return error to client
return 1;
}
// Have to covert the hex representation in the database back into
// simple binary
if (nlen[0] != TPM_NONCE_BYTES * 2) {
error("SECURESTATE: %s: Nonce length is incorrect (%d)",
reqp->nodeid, nlen[0]);
}
for (i = 0; i < TPM_NONCE_BYTES; i++) {
if (sscanf(row[0] + (i*2),"%2x",&(nonce[i])) != 1) {
error("SECURESTATE: %s: Error parsing nonce\n", reqp->nodeid);
mysql_free_result(res);
// XXX: return error to client
return 1;
}
}
mysql_free_result(res);
/*
* Make a list of the PCR values we need to have verified
*/
res = mydb_query("select q.pcr,q.value from nodes as n "
"left join tpm_quote_values as q "
"on (n.op_mode = q.op_mode or q.op_mode='*') "
"where (q.node_id='%s' and n.node_id='%s' "
"and q.state ='%s') "
"order by q.pcr",
2, reqp->nodeid, reqp->nodeid, newstate);
if (!res){
error("SECURESTATE: %s: DB error getting pcr list\n",
reqp->nodeid);
return 1;
}
nrows = mysql_num_rows(res);
if (!nrows){
error("%s: no TPM quote values in database for state %s\n",
reqp->nodeid,newstate);
mysql_free_result(res);
return 1;
}
wantpcrs = 0;
pcrs = malloc(nrows*sizeof(TPM_PCR));
for (i = 0; i < nrows; i++) {
int pcr;
row = mysql_fetch_row(res);
// XXX: Check for nonsensical values for the pcr index
// XXX: Check for nlen...
// XXX: Check for proper PCR size
pcr = atoi(row[0]);
wantpcrs |= (1 << pcr);
for (j = 0; j < TPM_PCR_BYTES; j++) {
if (sscanf(row[1] + (j*2),"%2x",&(pcrs[i][j])) != 1) {
error("SECURESTATE: %s: Error parsing PCR\n", reqp->nodeid);
free(pcrs);
mysql_free_result(res);
// XXX: return error to client
return 1;
}
}
}
mysql_free_result(res);
/*
* Get the identity key for vertification purposes
*/
res = mydb_query("select tpmidentity "
"from node_hostkeys "
"where node_id='%s' ",
1, reqp->nodeid);
if (!res){
error("securestate: %s: DB error getting tpmidentity\n",
reqp->nodeid);
free(pcrs);
return 1;
}
nrows = mysql_num_rows(res);
if (!nrows){
error("%s: no tpmidentity in database for this node.\n",
reqp->nodeid);
free(pcrs);
mysql_free_result(res);
return 1;
}
row = mysql_fetch_row(res);
nlen = mysql_fetch_lengths(res);
if (!nlen || !nlen[0]){
error("%s: invalid identity length.\n",
reqp->nodeid);
free(pcrs);
mysql_free_result(res);
return 1;
}