From: Tollef Fog Heen Date: Fri, 9 Oct 2009 08:02:16 +0000 (+0200) Subject: Initial write-up X-Git-Url: https://err.no/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=85c4f2dadc68c367d78e044a7e6961fc244f07ee;p=yubikey-server-c Initial write-up --- diff --git a/configure.ac b/configure.ac index 09e9d8e..e7fd913 100644 --- a/configure.ac +++ b/configure.ac @@ -20,8 +20,10 @@ else AC_MSG_ERROR([$PG_CONFIG is not installed - install it and postgresql headers?]) fi - PKG_CHECK_MODULES([libmicrohttpd], [libmicrohttpd]) +PKG_CHECK_MODULES([glib], [glib-2.0]) + +AC_CHECK_LIB([yubikey], [yubikey_modhex_decode],[], AC_MSG_ERROR([libyubikey is not installed or not new enough])) AC_CONFIG_FILES([ Makefile diff --git a/schema.sql b/schema.sql index eacc6f8..b42fdc2 100644 --- a/schema.sql +++ b/schema.sql @@ -6,17 +6,24 @@ CREATE TABLE yubikey ( yubikey_id serial NOT NULL, active boolean NOT NULL DEFAULT 'f', - secret_id varchar NOT NULL, + public_id bytea, -- fixed public bit of the key's output + secret_uid bytea NOT NULL, -- secret uid bit of the key + secret_key bytea NOT NULL, session_counter int, session_use int ); -INSERT INTO yubikey (yubikey_id, active, secret_id, session_counter, session_use) VALUES (0, 't', 'secret', 0,0); +INSERT INTO yubikey (active, public_id, + secret_uid, secret_key, session_counter, session_use) VALUES + ('t', decode('d4633b', 'hex'), + E'\\000\\000\\000\\000\\000\\000', + decode('baef43c254e9d2217912e80ed71a7b4a', 'hex'), + 0, 0); CREATE TABLE shared_secret ( secret_id serial NOT NULL, - secret_base64 varchar NOT NULL, + secret bytea NOT NULL, active boolean NOT NULL DEFAULT 'f' ); -INSERT INTO shared_secrets (secret_base64, active) VALUES ('MQ6fOy1t/add/wisbu2O+LpPiMs=', 't'); +INSERT INTO shared_secret (secret, active) VALUES (decode('MQ6fOy1t/add/wisbu2O+LpPiMs=', 'base64'), 't'); diff --git a/src/Makefile.am b/src/Makefile.am index 3d3f75d..f698982 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -3,5 +3,5 @@ sbin_PROGRAMS = yubikeyd yubikeyd_SOURCES = main.c -yubikeyd_CFLAGS = @libmicrohttpd_CFLAGS@ @POSTGRESQL_CFLAGS@ -yubikeyd_LDADD = @libmicrohttpd_LIBS@ @POSTGRESQL_LDFLAGS@ \ No newline at end of file +yubikeyd_CFLAGS = @glib_CFLAGS@ @libmicrohttpd_CFLAGS@ @POSTGRESQL_CFLAGS@ +yubikeyd_LDADD = @glib_LIBS@ @libmicrohttpd_LIBS@ @POSTGRESQL_LDFLAGS@ \ No newline at end of file diff --git a/src/main.c b/src/main.c index 6ff5afb..a4811cd 100644 --- a/src/main.c +++ b/src/main.c @@ -27,6 +27,10 @@ #include #include #include +#include +#include +#include +#include PGconn *db_conn; @@ -46,6 +50,41 @@ struct error { const char *info; }; +char *get_timestamp(void) { + struct tm tm; + size_t len = strlen("YYYY-mm-ddTHH:MM:SSZMSMS"); + char *ts = malloc(len + 1); + char ms[5]; + struct timeval tv; + gettimeofday(&tv, NULL); + gmtime_r(&tv.tv_sec, &tm); + strftime(ts, len, "%FT%TZ", &tm); + sprintf(ms, "%.4d", tv.tv_usec / 1000); + strncat(ts, ms, 4); + return ts; +} + +gchar *sign_request(char *key, size_t key_len, char *info, char *status, + char *timestamp) { + char *line, *sig; + gchar *ret; + gcry_md_hd_t hd; + if (info != NULL) { + asprintf(&line, "info=%s&status=%s×tamp=%s", info, + status, timestamp); + } else { + asprintf(&line, "status=%s×tamp=%s", status, timestamp); + } + gcry_md_open(&hd, GCRY_MD_SHA1, GCRY_MD_FLAG_HMAC); + gcry_md_setkey(hd, key, key_len); + gcry_md_write(hd, line, strlen(line)); + gcry_md_final(hd); + ret = g_base64_encode(gcry_md_read(hd, 0), gcry_md_get_algo_dlen(GCRY_MD_SHA1)); + gcry_md_close(hd); + free(line); + return ret; +} + static int handle_request(void *data, struct MHD_Connection *conn, const char *url, @@ -59,24 +98,35 @@ static int handle_request(void *data, int r; struct MHD_Response *response; PGresult *res; - const char *paramValues[1]; + const char *paramValues[3]; + int paramLengths[3]; + int paramFormats[3]; int i; - const char *id = NULL, *otp = NULL, *h = NULL, *shared_secret; + const unsigned char *id = NULL, *otp_mh = NULL, *h = NULL, *secret_id; + unsigned char *aes_key, *otp, *uid, *timestamp, *signature; + unsigned char *shared_secret, *resp_text; + size_t shared_secret_len, otp_len, uid_len; struct error *e; + yubikey_token_st token; + memset(&token, '\0', sizeof(token)); - /* Parse query string, grab id, otp and h (optional) */ + /* Parse query string, grab id, otp_mh and h (optional) */ id = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "id"); - otp = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "otp"); + otp_mh = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "otp"); h = MHD_lookup_connection_value(conn, MHD_GET_ARGUMENT_KIND, "h"); fprintf(stderr, "got params: url=%s id=%s otp=%s, h=%s\n", url, id, - otp, h); + otp_mh, h); /* XXX Handle missing params here */ + otp_len = strlen(otp_mh)/2 + 1; + otp = malloc(otp_len); + yubikey_modhex_decode(otp, otp_mh, otp_len); + /* Do query to grab shared secret, we need this later anyway */ paramValues[0] = id; res = PQexecParams(db_conn, - "SELECT secret_base64 FROM shared_secret WHERE secret_id = $1", + "SELECT secret FROM shared_secret WHERE secret_id = $1", 1, /* one param */ NULL, /* let the backend deduce param type */ paramValues, @@ -84,6 +134,48 @@ static int handle_request(void *data, NULL, /* default to all text params */ 1); /* ask for binary results */ + if (PQresultStatus(res) != PGRES_TUPLES_OK) { + fprintf(stderr, "SELECT failed: %s", PQerrorMessage(db_conn)); + PQclear(res); + return MHD_NO; + /* XXX Better error handling.*/ + /* XXX Handle zero rows too */ + } + if (PQntuples(res) == 0) { + /* XXX Better handling */ + fprintf(stderr, "No such secrets: %s\n", id); + PQclear(res); + return MHD_NO; + } + + /* If h exists, verify. FIXME */ + shared_secret_len = PQgetlength(res, 0, 0); + shared_secret = malloc(shared_secret_len); + memcpy(shared_secret, PQgetvalue(res, 0, 0), shared_secret_len); + PQclear(res); + + /* Validate OTP */ + /* Find public uid, if possible */ + if (otp_len > YUBIKEY_BLOCK_SIZE) { + uid_len = otp_len - YUBIKEY_BLOCK_SIZE - 1; + uid = malloc(uid_len); + memcpy(uid, otp, uid_len); + /* XXX error checking */ + } + + paramValues[0] = uid; + paramLengths[0] = uid_len; + paramFormats[0] = 1; /* Binary */ + res = PQexecParams(db_conn, + "SELECT secret_uid, secret_key, session_counter, " + "session_use FROM yubikey WHERE public_id = $1::bytea", + 1, /* one param */ + NULL, /* let the backend deduce param type */ + paramValues, + paramLengths, + paramFormats, /* default to all text params */ + 1); /* ask for binary results */ + if (PQresultStatus(res) != PGRES_TUPLES_OK) { fprintf(stderr, "SELECT failed: %s", PQerrorMessage(db_conn)); @@ -91,21 +183,63 @@ static int handle_request(void *data, return MHD_NO; /* XXX Better error handling*/ } - /* If h exists, verify FIXME */ - /* Validate OTP */ - /* Update status, if appropriate */ - /* Generate response, sign it */ - for (i = 0; i < PQntuples(res); i++) { - const char *fullname; - - fullname = PQgetvalue(res, i, 0); - response = MHD_create_response_from_data(strlen(fullname), (void*)fullname, - MHD_YES, MHD_YES); - r = MHD_queue_response(conn, MHD_HTTP_OK, response); - MHD_destroy_response(response); - fprintf(stderr, "%s %s\n", method, url); + assert(PQgetlength(res, 0, PQfnumber(res, "secret_key")) == YUBIKEY_KEY_SIZE); + aes_key = PQgetvalue(res, 0, PQfnumber(res, "secret_key")); + + /* Argh, yubikey_parse takes in one modhex-ed token (but + * requires us to strip the public id first, and an unencoded aes key*/ + yubikey_parse(otp_mh+uid_len*2, aes_key, &token); + if (!yubikey_crc_ok_p((void*)&token) || + memcmp(token.uid, + PQgetvalue(res, 0, PQfnumber(res, "secret_uid")), + YUBIKEY_UID_SIZE) != 0) { + /* XXX: FIXME: check counters too */ + /* XXX FIXME - bad token => reject */ + fprintf(stderr, "BAD TOKEN\n"); + return MHD_NO; } PQclear(res); + + /* Update status, if appropriate */ + + asprintf(&(paramValues[0]), "%d", yubikey_counter(token.ctr)); + paramLengths[0] = 0; + paramFormats[0] = 0; /* Text */ + asprintf(&(paramValues[1]), "%d", yubikey_counter(token.use)); + paramLengths[1] = 0; + paramFormats[1] = 0; /* Text */ + paramValues[2] = uid; + paramLengths[2] = uid_len; + paramFormats[2] = 1; /* Binary */ + res = PQexecParams(db_conn, + "UPDATE yubikey SET session_counter = $1, " + "session_use = $2 WHERE public_id = $3", + 3, /* number of params */ + NULL, /* let the backend deduce param type */ + paramValues, + paramLengths, + paramFormats, + 1); /* ask for binary results */ + + if (PQresultStatus(res) != PGRES_COMMAND_OK) { + fprintf(stderr, "UPDATE failed: %s\n", PQerrorMessage(db_conn)); + PQclear(res); + return MHD_NO; + /* XXX Better error handling.*/ + } + assert(PQntuples(res) == 0); + + /* Generate response, sign it */ + timestamp = get_timestamp(); + signature = sign_request(shared_secret, shared_secret_len, + NULL, "OK", timestamp); + asprintf(&resp_text, "h=%s\nstatus=%s\nt=%s\n", signature, "OK", + timestamp); + response = MHD_create_response_from_data(strlen(resp_text), resp_text, + MHD_YES, MHD_YES); + r = MHD_queue_response(conn, MHD_HTTP_OK, response); + MHD_destroy_response(response); + fprintf(stderr, "%s %s\n", method, url); return r; }