diff options
author | James Yonan | 2011-06-03 21:21:20 +0000 |
---|---|---|
committer | James Yonan | 2011-06-03 21:21:20 +0000 |
commit | eab3e22f8261c07d5f906c05fce69917034d9e53 (patch) | |
tree | b1d5d26dcb1edd657f75f4fb03fc46123157be60 | |
parent | a114cb750e26e96a727253f316d7415fe34447f6 (diff) | |
download | openvpn-eab3e22f8261c07d5f906c05fce69917034d9e53.zip openvpn-eab3e22f8261c07d5f906c05fce69917034d9e53.tar.gz |
Added support for static challenge/response protocol.
This includes the new "static-challenge" directive.
See management/management-notes.txt for details on both
static and dynamic challenge/response protocols.
All client-side challenge/response code is #ifdefed on
ENABLE_CLIENT_CR and can be removed from the build
by commenting out the definition of ENABLE_CLIENT_CR
in syshead.h.
Version 2.1.3x.
git-svn-id: http://svn.openvpn.net/projects/openvpn/branches/BETA21/openvpn@7316 e7ae566f-a301-0410-adde-c780ea21d3b5
-rw-r--r-- | base64.c | 10 | ||||
-rw-r--r-- | init.c | 12 | ||||
-rw-r--r-- | manage.c | 47 | ||||
-rw-r--r-- | manage.h | 6 | ||||
-rw-r--r-- | management/management-notes.txt | 110 | ||||
-rw-r--r-- | misc.c | 72 | ||||
-rw-r--r-- | misc.h | 15 | ||||
-rw-r--r-- | openvpn.8 | 15 | ||||
-rw-r--r-- | options.c | 10 | ||||
-rw-r--r-- | options.h | 3 | ||||
-rw-r--r-- | ssl.c | 32 | ||||
-rw-r--r-- | ssl.h | 6 | ||||
-rw-r--r-- | syshead.h | 2 | ||||
-rw-r--r-- | version.m4 | 2 |
14 files changed, 274 insertions, 68 deletions
@@ -42,6 +42,11 @@ static char base64_chars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; +/* + * base64 encode input data of length size to malloced + * buffer which is returned as *str. Returns string + * length of *str. + */ int base64_encode(const void *data, int size, char **str) { @@ -116,6 +121,11 @@ token_decode(const char *token) return (marker << 24) | val; } +/* + * Decode base64 str, outputting data to buffer + * at data of length size. Return length of + * decoded data written or -1 on error or overflow. + */ int base64_decode(const char *str, void *data, int size) { @@ -343,7 +343,13 @@ init_query_passwords (struct context *c) #if P2MP /* Auth user/pass input */ if (c->options.auth_user_pass_file) - auth_user_pass_setup (c->options.auth_user_pass_file); + { +#ifdef ENABLE_CLIENT_CR + auth_user_pass_setup (c->options.auth_user_pass_file, &c->options.sc_info); +#else + auth_user_pass_setup (c->options.auth_user_pass_file, NULL); +#endif + } #endif } @@ -2085,6 +2091,10 @@ do_init_crypto_tls (struct context *c, const unsigned int flags) to.x509_track = options->x509_track; #endif +#ifdef ENABLE_CLIENT_CR + to.sci = &options->sc_info; +#endif + /* TLS handshake authentication (--tls-auth) */ if (options->tls_auth_file) { @@ -606,25 +606,19 @@ man_up_finalize (struct management *man) { switch (man->connection.up_query_mode) { - case UP_QUERY_DISABLED: - man->connection.up_query.defined = false; - break; case UP_QUERY_USER_PASS: - if (strlen (man->connection.up_query.username) && strlen (man->connection.up_query.password)) - man->connection.up_query.defined = true; - break; + if (!strlen (man->connection.up_query.username)) + break; + /* fall through */ case UP_QUERY_PASS: - if (strlen (man->connection.up_query.password)) - man->connection.up_query.defined = true; - break; case UP_QUERY_NEED_OK: - if (strlen (man->connection.up_query.password)) - man->connection.up_query.defined = true; - break; case UP_QUERY_NEED_STR: if (strlen (man->connection.up_query.password)) man->connection.up_query.defined = true; break; + case UP_QUERY_DISABLED: + man->connection.up_query.defined = false; + break; default: ASSERT (0); } @@ -665,16 +659,17 @@ man_query_user_pass (struct management *man, static void man_query_username (struct management *man, const char *type, const char *string) { - const bool needed = (man->connection.up_query_mode == UP_QUERY_USER_PASS && man->connection.up_query_type); + const bool needed = ((man->connection.up_query_mode == UP_QUERY_USER_PASS + ) && man->connection.up_query_type); man_query_user_pass (man, type, string, needed, "username", man->connection.up_query.username, USER_PASS_LEN); } static void man_query_password (struct management *man, const char *type, const char *string) { - const bool needed = ((man->connection.up_query_mode == UP_QUERY_USER_PASS - || man->connection.up_query_mode == UP_QUERY_PASS) - && man->connection.up_query_type); + const bool needed = ((man->connection.up_query_mode == UP_QUERY_PASS + || man->connection.up_query_mode == UP_QUERY_USER_PASS + ) && man->connection.up_query_type); if (!string[0]) /* allow blank passwords to be passed through using the blank_up tag */ string = blank_up; man_query_user_pass (man, type, string, needed, "password", man->connection.up_query.password, USER_PASS_LEN); @@ -2843,7 +2838,8 @@ bool management_query_user_pass (struct management *man, struct user_pass *up, const char *type, - const unsigned int flags) + const unsigned int flags, + const char *static_challenge) { struct gc_arena gc = gc_new (); bool ret = false; @@ -2856,7 +2852,9 @@ management_query_user_pass (struct management *man, const char *alert_type = NULL; const char *prefix = NULL; unsigned int up_query_mode = 0; - +#ifdef ENABLE_CLIENT_CR + const char *sc = NULL; +#endif ret = true; man->persist.standalone_disabled = false; /* This is so M_CLIENT messages will be correctly passed through msg() */ man->persist.special_state_msg = NULL; @@ -2886,6 +2884,10 @@ management_query_user_pass (struct management *man, up_query_mode = UP_QUERY_USER_PASS; prefix = "PASSWORD"; alert_type = "username/password"; +#ifdef ENABLE_CLIENT_CR + if (static_challenge) + sc = static_challenge; +#endif } buf_printf (&alert_msg, ">%s:Need '%s' %s", prefix, @@ -2895,6 +2897,13 @@ management_query_user_pass (struct management *man, if (flags & (GET_USER_PASS_NEED_OK | GET_USER_PASS_NEED_STR)) buf_printf (&alert_msg, " MSG:%s", up->username); +#ifdef ENABLE_CLIENT_CR + if (sc) + buf_printf (&alert_msg, " SC:%d,%s", + BOOL_CAST(flags & GET_USER_PASS_STATIC_CHALLENGE_ECHO), + sc); +#endif + man_wait_for_client_connection (man, &signal_received, 0, MWCC_PASSWORD_WAIT); if (signal_received) ret = false; @@ -2908,7 +2917,7 @@ management_query_user_pass (struct management *man, man->connection.up_query_mode = up_query_mode; man->connection.up_query_type = type; - /* run command processing event loop until we get our username/password */ + /* run command processing event loop until we get our username/password/response */ do { man_standalone_event_loop (man, &signal_received, 0); @@ -365,7 +365,11 @@ void management_set_callback (struct management *man, void management_clear_callback (struct management *man); -bool management_query_user_pass (struct management *man, struct user_pass *up, const char *type, const unsigned int flags); +bool management_query_user_pass (struct management *man, + struct user_pass *up, + const char *type, + const unsigned int flags, + const char *static_challenge); bool management_should_daemonize (struct management *man); bool management_would_hold (struct management *man); diff --git a/management/management-notes.txt b/management/management-notes.txt index 1f4cbd0..6e1e7cd 100644 --- a/management/management-notes.txt +++ b/management/management-notes.txt @@ -836,3 +836,113 @@ mappings, when not in single quotations: interpret it as enclosing a parameter. \[SPACE] Pass a literal space or tab character, don't interpret it as a parameter delimiter. + +Challenge/Response Protocol +--------------------------- + +The OpenVPN Challenge/Response Protocol allows an OpenVPN server to +generate challenge questions that are shown to the user, and to see +the user's responses to those challenges. Based on the responses, the +server can allow or deny access. + +In this way, the OpenVPN Challenge/Response Protocol can be used +to implement multi-factor authentication. Two different +variations on the challenge/response protocol are supported: the +"Dynamic" and "Static" protocols. + +The basic idea of Challenge/Response is that the user must enter an +additional piece of information, in addition to the username and +password, to successfully authenticate. Normally, this information +is used to prove that the user posesses a certain key-like device +such as cryptographic token or a particular mobile phone. + +Dynamic protocol: + +The OpenVPN dynamic challenge/response protocol works by returning +a specially formatted error message after initial successful +authentication. This error message contains the challenge question, +and is formatted as such: + + CRV1:<flags>:<state_id>:<username_base64>:<challenge_text> + +flags: a series of optional, comma-separated flags: + E : echo the response when the user types it + R : a response is required + +state_id: an opaque string that should be returned to the server + along with the response. + +username_base64 : the username formatted as base64 + +challenge_text : the challenge text to be shown to the user + +Example challenge: + + CRV1:R,E:Om01u7Fh4LrGBS7uh0SWmzwabUiGiW6l:Y3Ix:Please enter token PIN + +After showing the challenge_text and getting a response from the user +(if R flag is specified), the client should submit the following +auth creds back to the OpenVPN server: + +Username: [username decoded from username_base64] +Password: CRV1::<state_id>::<response_text> + +Where state_id is taken from the challenge request and response_text +is what the user entered in response to the challenge_text. +If the R flag is not present, response_text may be the empty +string. + +Example response (suppose the user enters "8675309" for the token PIN): + + Username: cr1 ("Y3Ix" base64 decoded) + Password: CRV1::Om01u7Fh4LrGBS7uh0SWmzwabUiGiW6l::8675309 + +Static protocol: + +The static protocol differs from the dynamic protocol in that the +challenge question and response field is given to the user in the +initial username/password dialog, and the username, password, and +response are delivered back to the server in a single transaction. + +The "static-challenge" directive is used to give the challenge text +to OpenVPN and indicate whether or not the response should be echoed. + +When the "static-challenge" directive is used, the management +interface will respond as such when credentials are needed: + + >PASSWORD:Need 'Auth' username/password SC:<ECHO>,<TEXT> + + ECHO: "1" if response should be echoed, "0" to not echo + TEXT: challenge text that should be shown to the user to + facilitate their response + +For example: + + >PASSWORD:Need 'Auth' username/password SC:1,Please enter token PIN + +The above notification indicates that OpenVPN needs a --auth-user-pass +password plus a response to a static challenge ("Please enter token PIN"). +The "1" after the "SC:" indicates that the response should be echoed. + +The management interface client in this case should add the static +challenge text to the auth dialog followed by a field for the user to +enter a response. Then the client should pack the password and response +together into an encoded password: + + username "Auth" foo + password "Auth" "SCRV1:<BASE64_PASSWORD>:<BASE64_RESPONSE>" + +For example, if the user entered "bar" as the password and 8675309 +as the PIN, the following management interface commands should be +issued: + + username "Auth" foo + password "Auth" "SCRV1:Zm9v:ODY3NTMwOQ==" + +Client-side support for challenge/response protocol: + +Currently, the Access Server client and standalone OpenVPN +client support both static and dynamic challenge/response +protocols. However, any OpenVPN client UI that drives OpenVPN +via the management interface needs to add explicit support +for the challenge/response protocol. @@ -1387,10 +1387,16 @@ get_user_pass_cr (struct user_pass *up, && ((auth_file && streq (auth_file, "management")) || (from_stdin && (flags & GET_USER_PASS_MANAGEMENT))) && management_query_user_pass_enabled (management)) { + const char *sc = NULL; + if (flags & GET_USER_PASS_PREVIOUS_CREDS_FAILED) management_auth_failure (management, prefix, "previous auth credentials failed"); - if (!management_query_user_pass (management, up, prefix, flags)) +#ifdef ENABLE_CLIENT_CR + if (auth_challenge && (flags & GET_USER_PASS_STATIC_CHALLENGE)) + sc = auth_challenge; +#endif + if (!management_query_user_pass (management, up, prefix, flags, sc)) { if ((flags & GET_USER_PASS_NOFATAL) != 0) return false; @@ -1422,7 +1428,7 @@ get_user_pass_cr (struct user_pass *up, else if (from_stdin) { #ifdef ENABLE_CLIENT_CR - if (auth_challenge) + if (auth_challenge && (flags & GET_USER_PASS_DYNAMIC_CHALLENGE)) { struct auth_challenge_info *ac = get_auth_challenge (auth_challenge, &gc); if (ac) @@ -1431,7 +1437,7 @@ get_user_pass_cr (struct user_pass *up, struct buffer packed_resp; buf_set_write (&packed_resp, (uint8_t*)up->password, USER_PASS_LEN); - msg (M_INFO, "CHALLENGE: %s", ac->challenge_text); + msg (M_INFO|M_NOPREFIX, "CHALLENGE: %s", ac->challenge_text); if (!get_console_input ("Response:", BOOL_CAST(ac->flags&CR_ECHO), response, USER_PASS_LEN)) msg (M_FATAL, "ERROR: could not read challenge response from stdin"); strncpynt (up->username, ac->user, USER_PASS_LEN); @@ -1461,6 +1467,28 @@ get_user_pass_cr (struct user_pass *up, if (!get_console_input (BSTR (&pass_prompt), false, up->password, USER_PASS_LEN)) msg (M_FATAL, "ERROR: could not not read %s password from stdin", prefix); + +#ifdef ENABLE_CLIENT_CR + if (auth_challenge && (flags & GET_USER_PASS_STATIC_CHALLENGE)) + { + char *response = (char *) gc_malloc (USER_PASS_LEN, false, &gc); + struct buffer packed_resp; + char *pw64=NULL, *resp64=NULL; + + msg (M_INFO|M_NOPREFIX, "CHALLENGE: %s", auth_challenge); + if (!get_console_input ("Response:", BOOL_CAST(flags & GET_USER_PASS_STATIC_CHALLENGE_ECHO), response, USER_PASS_LEN)) + msg (M_FATAL, "ERROR: could not read static challenge response from stdin"); + if (base64_encode(up->password, strlen(up->password), &pw64) == -1 + || base64_encode(response, strlen(response), &resp64) == -1) + msg (M_FATAL, "ERROR: could not base64-encode password/static_response"); + buf_set_write (&packed_resp, (uint8_t*)up->password, USER_PASS_LEN); + buf_printf (&packed_resp, "SCRV1:%s:%s", pw64, resp64); + string_clear(pw64); + free(pw64); + string_clear(resp64); + free(resp64); + } +#endif } } else @@ -1528,42 +1556,8 @@ get_user_pass_cr (struct user_pass *up, #ifdef ENABLE_CLIENT_CR /* - * Parse a challenge message returned along with AUTH_FAILED. - * The message is formatted as such: - * - * CRV1:<flags>:<state_id>:<username_base64>:<challenge_text> - * - * flags: a series of optional, comma-separated flags: - * E : echo the response when the user types it - * R : a response is required - * - * state_id: an opaque string that should be returned to the server - * along with the response. - * - * username_base64 : the username formatted as base64 - * - * challenge_text : the challenge text to be shown to the user - * - * Example challenge: - * - * CRV1:R,E:Om01u7Fh4LrGBS7uh0SWmzwabUiGiW6l:Y3Ix:Please enter token PIN - * - * After showing the challenge_text and getting a response from the user - * (if R flag is specified), the client should submit the following - * auth creds back to the OpenVPN server: - * - * Username: [username decoded from username_base64] - * Password: CRV1::<state_id>::<response_text> - * - * Where state_id is taken from the challenge request and response_text - * is what the user entered in response to the challenge_text. - * If the R flag is not present, response_text may be the empty - * string. - * - * Example response (suppose the user enters "8675309" for the token PIN): - * - * Username: cr1 ("Y3Ix" base64 decoded) - * Password: CRV1::Om01u7Fh4LrGBS7uh0SWmzwabUiGiW6l::8675309 + * See management/management-notes.txt for more info on the + * the dynamic challenge/response protocol implemented here. */ struct auth_challenge_info * get_auth_challenge (const char *auth_challenge, struct gc_arena *gc) @@ -268,8 +268,19 @@ struct auth_challenge_info { struct auth_challenge_info *get_auth_challenge (const char *auth_challenge, struct gc_arena *gc); +/* + * Challenge response info on client as pushed by server. + */ +struct static_challenge_info { +# define SC_ECHO (1<<0) /* echo response when typed by user */ + unsigned int flags; + + const char *challenge_text; +}; + #else struct auth_challenge_info {}; +struct static_challenge_info {}; #endif bool get_console_input (const char *prompt, const bool echo, char *input, const int capacity); @@ -285,6 +296,10 @@ bool get_console_input (const char *prompt, const bool echo, char *input, const #define GET_USER_PASS_NEED_STR (1<<5) #define GET_USER_PASS_PREVIOUS_CREDS_FAILED (1<<6) +#define GET_USER_PASS_DYNAMIC_CHALLENGE (1<<7) /* CRV1 protocol -- dynamic challenge */ +#define GET_USER_PASS_STATIC_CHALLENGE (1<<8) /* SCRV1 protocol -- static challenge */ +#define GET_USER_PASS_STATIC_CHALLENGE_ECHO (1<<9) /* SCRV1 protocol -- echo response */ + bool get_user_pass_cr (struct user_pass *up, const char *auth_file, const char *prefix, @@ -3362,6 +3362,21 @@ Note that while this option cannot be pushed, it can be controlled from the management interface. .\"********************************************************* .TP +.B \-\-static\-challenge t e +Enable static challenge/response protocol using challenge text +.B t, +with +echo flag given by +.B e +(0|1). + +The echo flag indicates whether or not the user's response +to the challenge should be echoed. + +See management\-notes.txt in the OpenVPN distribution for a +description of the OpenVPN challenge/response protocol. +.\"********************************************************* +.TP .B --server-poll-timeout n when polling possible remote servers to connect to in a round-robin fashion, spend no more than @@ -443,6 +443,8 @@ static const char usage_message[] = " when connecting to a '--mode server' remote host.\n" "--auth-retry t : How to handle auth failures. Set t to\n" " none (default), interact, or nointeract.\n" + "--static-challenge t e : Enable static challenge/response protocol using\n" + " challenge text t, with e indicating echo flag (0|1)\n" "--server-poll-timeout n : when polling possible remote servers to connect to\n" " in a round-robin fashion, spend no more than n seconds\n" " waiting for a response before trying the next server.\n" @@ -5251,6 +5253,14 @@ add_option (struct options *options, VERIFY_PERMISSION (OPT_P_GENERAL); auth_retry_set (msglevel, p[1]); } +#ifdef ENABLE_CLIENT_CR + else if (streq (p[0], "static-challenge") && p[1] && p[2]) + { + options->sc_info.challenge_text = p[1]; + if (atoi(p[2])) + options->sc_info.flags |= SC_ECHO; + } +#endif #endif #ifdef WIN32 else if (streq (p[0], "win-sys") && p[1]) @@ -426,6 +426,9 @@ struct options const char *auth_user_pass_verify_script; bool auth_user_pass_verify_script_via_file; +#ifdef ENABLE_CLIENT_CR + struct static_challenge_info sc_info; +#endif #if PORT_SHARE char *port_share_host; int port_share_port; @@ -292,17 +292,35 @@ static char *auth_challenge; /* GLOBAL */ #endif void -auth_user_pass_setup (const char *auth_file) +auth_user_pass_setup (const char *auth_file, const struct static_challenge_info *sci) { auth_user_pass_enabled = true; if (!auth_user_pass.defined) { #if AUTO_USERID get_user_pass_auto_userid (&auth_user_pass, auth_file); -#elif defined(ENABLE_CLIENT_CR) - get_user_pass_cr (&auth_user_pass, auth_file, UP_TYPE_AUTH, GET_USER_PASS_MANAGEMENT|GET_USER_PASS_SENSITIVE, auth_challenge); #else - get_user_pass (&auth_user_pass, auth_file, UP_TYPE_AUTH, GET_USER_PASS_MANAGEMENT|GET_USER_PASS_SENSITIVE); +# ifdef ENABLE_CLIENT_CR + if (auth_challenge) /* dynamic challenge/response */ + get_user_pass_cr (&auth_user_pass, + auth_file, + UP_TYPE_AUTH, + GET_USER_PASS_MANAGEMENT|GET_USER_PASS_SENSITIVE|GET_USER_PASS_DYNAMIC_CHALLENGE, + auth_challenge); + else if (sci) /* static challenge response */ + { + int flags = GET_USER_PASS_MANAGEMENT|GET_USER_PASS_SENSITIVE|GET_USER_PASS_STATIC_CHALLENGE; + if (sci->flags & SC_ECHO) + flags |= GET_USER_PASS_STATIC_CHALLENGE_ECHO; + get_user_pass_cr (&auth_user_pass, + auth_file, + UP_TYPE_AUTH, + flags, + sci->challenge_text); + } + else +# endif + get_user_pass (&auth_user_pass, auth_file, UP_TYPE_AUTH, GET_USER_PASS_MANAGEMENT|GET_USER_PASS_SENSITIVE); #endif } } @@ -3945,7 +3963,11 @@ key_method_2_write (struct buffer *buf, struct tls_session *session) /* write username/password if specified */ if (auth_user_pass_enabled) { - auth_user_pass_setup (NULL); +#ifdef ENABLE_CLIENT_CR + auth_user_pass_setup (NULL, session->opt->sci); +#else + auth_user_pass_setup (NULL, NULL); +#endif if (!write_string (buf, auth_user_pass.username, -1)) goto error; if (!write_string (buf, auth_user_pass.password, -1)) @@ -516,6 +516,10 @@ struct tls_options const struct x509_track *x509_track; #endif +#ifdef ENABLE_CLIENT_CR + const struct static_challenge_info *sci; +#endif + /* --gremlin bits */ int gremlin; }; @@ -723,7 +727,7 @@ void get_highest_preference_tls_cipher (char *buf, int size); void pem_password_setup (const char *auth_file); int pem_password_callback (char *buf, int size, int rwflag, void *u); -void auth_user_pass_setup (const char *auth_file); +void auth_user_pass_setup (const char *auth_file, const struct static_challenge_info *sc_info); void ssl_set_auth_nocache (void); void ssl_set_auth_token (const char *token); void ssl_purge_auth (const bool auth_user_pass_only); @@ -683,7 +683,7 @@ socket_defined (const socket_descriptor_t sd) #endif /* - * Do we support challenge/response authentication, as a console-based client? + * Do we support challenge/response authentication as client? */ #define ENABLE_CLIENT_CR @@ -1,5 +1,5 @@ dnl define the OpenVPN version -define(PRODUCT_VERSION,[2.1.3w]) +define(PRODUCT_VERSION,[2.1.3x]) dnl define the TAP version define(PRODUCT_TAP_ID,[tap0901]) define(PRODUCT_TAP_WIN32_MIN_MAJOR,[9]) |