Add TOTP (RFC6238) one-time password support

John Morrissey jwm at horde.net
Fri Mar 8 21:48:51 EST 2013


On Thu, Mar 07, 2013 at 11:55:18PM +0000, David Woodhouse wrote:
> On Thu, 2013-03-07 at 18:39 -0500, John Morrissey wrote:
> > - openconnect_set_stoken_mode no longer accepts the use_stoken argument
> >   and instead always tries to initialize libstoken when called. This
> >   makes sense in openconnect(8), but I'm not sure how much of a concern
> >   this API change is for upstream consumers of libopenconnect. I also
> >   wasn't sure how to account for this in libopenconnect.map.in.
> 
> You can't account for it. It's an ABI break and it would take us to
> libopenconnect.so.3. I'd like to avoid this change, if possible.

Sure, it's easy enough. See this iteration of the patch.

> > Other than that, I think it does what it says on the box. It builds when
> > libstoken (only) is present, libstoken and liboath are both present, and
> > when neither library is present. I don't have a SecureID installation to
> > actually test with, but the code changes to the libstoken path are
> > minimal, so I think they're OK.
> 
> I've already received complaints about the way that stoken support is
> automatically built if libstoken is present, and silently omitted if
> not. It would be nice to have a --disable-oath argument to configure:
> http://www.gentoo.org/proj/en/qa/automagic.xml

Also in this iteration of the patch. Though I think packagers who complain
about a lack of --disable-$THING should be introduced to clean build
chroots. :-)

john


>From c5133558c57f5a34061d37df2faf07f9341dbbc5 Mon Sep 17 00:00:00 2001
From: John Morrissey <jwm at horde.net>
Date: Sat, 2 Mar 2013 22:44:06 -0500
Subject: [PATCH] add oath totp support


Signed-off-by: John Morrissey <jwm at horde.net>
---
 Makefile.am            |    8 +-
 auth.c                 |  174 +++++++++++++++++++++++++++++++++++++++---------
 configure.ac           |   11 +++
 http.c                 |    2 +-
 libopenconnect.map.in  |    2 +
 library.c              |   65 +++++++++++++++++-
 main.c                 |  101 ++++++++++++++++++++--------
 openconnect-internal.h |   21 +++++--
 openconnect.8.in       |   19 ++++--
 openconnect.h          |   11 ++-
 www/building.xml       |    1 +
 www/changelog.xml      |    1 +
 www/features.xml       |    1 +
 13 files changed, 336 insertions(+), 81 deletions(-)

diff --git a/Makefile.am b/Makefile.am
index 463df52..06aae42 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -14,8 +14,8 @@ man8_MANS = openconnect.8
 AM_CPPFLAGS = -DLOCALEDIR="\"$(localedir)\""
 openconnect_SOURCES = xml.c main.c dtls.c cstp.c mainloop.c tun.c
 
-openconnect_CFLAGS = $(SSL_CFLAGS) $(DTLS_SSL_CFLAGS) $(LIBXML2_CFLAGS) $(LIBPROXY_CFLAGS) $(ZLIB_CFLAGS) $(LIBSTOKEN_CFLAGS)
-openconnect_LDADD = libopenconnect.la $(SSL_LIBS) $(DTLS_SSL_LIBS) $(LIBXML2_LIBS) $(LIBPROXY_LIBS) $(ZLIB_LIBS) $(LIBINTL) $(LIBSTOKEN_LIBS)
+openconnect_CFLAGS = $(SSL_CFLAGS) $(DTLS_SSL_CFLAGS) $(LIBXML2_CFLAGS) $(LIBPROXY_CFLAGS) $(ZLIB_CFLAGS) $(LIBSTOKEN_CFLAGS) $(LIBOATH_CFLAGS)
+openconnect_LDADD = libopenconnect.la $(SSL_LIBS) $(DTLS_SSL_LIBS) $(LIBXML2_LIBS) $(LIBPROXY_LIBS) $(ZLIB_LIBS) $(LIBINTL) $(LIBSTOKEN_LIBS) $(LIBOATH_LIBS)
 
 library_srcs = ssl.c http.c auth.c library.c compat.c
 lib_srcs_gnutls = gnutls.c gnutls_pkcs12.c gnutls_tpm.c
@@ -30,8 +30,8 @@ if OPENCONNECT_OPENSSL
 library_srcs += $(lib_srcs_openssl)
 endif
 libopenconnect_la_SOURCES = version.c $(library_srcs)
-libopenconnect_la_CFLAGS = $(SSL_CFLAGS) $(LIBXML2_CFLAGS) $(LIBPROXY_CFLAGS) $(P11KIT_CFLAGS) $(TSS_CFLAGS) $(LIBSTOKEN_CFLAGS)
-libopenconnect_la_LIBADD = $(SSL_LIBS) $(LIBXML2_LIBS) $(LIBPROXY_LIBS) $(LIBINTL) $(P11KIT_LIBS) $(TSS_LIBS) $(LIBSTOKEN_LIBS)
+libopenconnect_la_CFLAGS = $(SSL_CFLAGS) $(LIBXML2_CFLAGS) $(LIBPROXY_CFLAGS) $(P11KIT_CFLAGS) $(TSS_CFLAGS) $(LIBSTOKEN_CFLAGS) $(LIBOATH_CFLAGS)
+libopenconnect_la_LIBADD = $(SSL_LIBS) $(LIBXML2_LIBS) $(LIBPROXY_LIBS) $(LIBINTL) $(P11KIT_LIBS) $(TSS_LIBS) $(LIBSTOKEN_LIBS) $(LIBOATH_LIBS)
 if OPENBSD_LIBTOOL
 # OpenBSD's libtool doesn't have -version-number, but its -version-info arg
 # does what GNU libtool's -version-number does. Which arguably is what the
diff --git a/auth.c b/auth.c
index 361f656..de5d222 100644
--- a/auth.c
+++ b/auth.c
@@ -1,6 +1,7 @@
 /*
  * OpenConnect (SSL + DTLS) VPN client
  *
+ * Copyright © 2013 John Morrissey <jwm at horde.net>
  * Copyright © 2008-2011 Intel Corporation.
  * Copyright © 2008 Nick Andrew <nick at nick-andrew.net>
  *
@@ -36,6 +37,10 @@
 #include LIBSTOKEN_HDR
 #endif
 
+#ifdef LIBOATH_HDR
+#include LIBOATH_HDR
+#endif
+
 #include <libxml/parser.h>
 #include <libxml/tree.h>
 
@@ -233,13 +238,15 @@ static int parse_form(struct openconnect_info *vpninfo, struct oc_auth_form *for
 		if (!strcmp(input_type, "hidden")) {
 			opt->type = OC_FORM_OPT_HIDDEN;
 			opt->value = (char *)xmlGetProp(xml_node, (unsigned char *)"value");
-		} else if (!strcmp(input_type, "text"))
+		} else if (!strcmp(input_type, "text")) {
 			opt->type = OC_FORM_OPT_TEXT;
-		else if (!strcmp(input_type, "password")) {
-			if (vpninfo->use_stoken && !can_gen_tokencode(vpninfo, form, opt))
-				opt->type = OC_FORM_OPT_STOKEN;
-			else
+		} else if (!strcmp(input_type, "password")) {
+			if (vpninfo->token_mode != TOKEN_MODE_NONE &&
+			    (can_gen_tokencode(vpninfo, form, opt) == 0)) {
+				opt->type = OC_FORM_OPT_TOKEN;
+			} else {
 				opt->type = OC_FORM_OPT_PASSWORD;
+			}
 		} else {
 			vpn_progress(vpninfo, PRG_INFO,
 				     _("Unknown input type %s in form\n"),
@@ -631,7 +638,7 @@ void free_auth_form(struct oc_auth_form *form)
 		if (form->opts->type == OC_FORM_OPT_TEXT ||
 		    form->opts->type == OC_FORM_OPT_PASSWORD ||
 		    form->opts->type == OC_FORM_OPT_HIDDEN ||
-		    form->opts->type == OC_FORM_OPT_STOKEN)
+		    form->opts->type == OC_FORM_OPT_TOKEN)
 			free(form->opts->value);
 		else if (form->opts->type == OC_FORM_OPT_SELECT) {
 			struct oc_form_opt_select *sel = (void *)form->opts;
@@ -872,8 +879,8 @@ int prepare_stoken(struct openconnect_info *vpninfo)
 	form.opts = opts;
 	form.message = _("Enter credentials to unlock software token.");
 
-	vpninfo->stoken_tries = 0;
-	vpninfo->stoken_bypassed = 0;
+	vpninfo->token_tries = 0;
+	vpninfo->token_bypassed = 0;
 
 	if (stoken_devid_required(vpninfo->stoken_ctx)) {
 		opt->type = OC_FORM_OPT_TEXT;
@@ -923,7 +930,7 @@ int prepare_stoken(struct openconnect_info *vpninfo)
 			if (all_empty) {
 				vpn_progress(vpninfo, PRG_INFO,
 					     _("User bypassed soft token.\n"));
-				vpninfo->stoken_bypassed = 1;
+				vpninfo->token_bypassed = 1;
 				ret = 0;
 				break;
 			}
@@ -981,22 +988,23 @@ int prepare_stoken(struct openconnect_info *vpninfo)
  *  < 0, if unable to generate a tokencode
  *  = 0, on success
  */
-static int can_gen_tokencode(struct openconnect_info *vpninfo, struct oc_auth_form *form,
-			     struct oc_form_opt *opt)
+static int can_gen_stoken_code(struct openconnect_info *vpninfo,
+                               struct oc_auth_form *form,
+                               struct oc_form_opt *opt)
 {
 #ifdef LIBSTOKEN_HDR
 	if ((strcmp(opt->name, "password") && strcmp(opt->name, "answer")) ||
-	    vpninfo->stoken_bypassed)
+	    vpninfo->token_bypassed)
 		return -EINVAL;
-	if (vpninfo->stoken_tries == 0) {
+	if (vpninfo->token_tries == 0) {
 		vpn_progress(vpninfo, PRG_DEBUG,
 			     _("OK to generate INITIAL tokencode\n"));
-		vpninfo->stoken_time = 0;
-	} else if (vpninfo->stoken_tries == 1 && form->message &&
+		vpninfo->token_time = 0;
+	} else if (vpninfo->token_tries == 1 && form->message &&
 		   strcasestr(form->message, "next tokencode")) {
 		vpn_progress(vpninfo, PRG_DEBUG,
 			     _("OK to generate NEXT tokencode\n"));
-		vpninfo->stoken_time += 60;
+		vpninfo->token_time += 60;
 	} else {
 		/* limit the number of retries, to avoid account lockouts */
 		vpn_progress(vpninfo, PRG_INFO,
@@ -1013,35 +1021,139 @@ static int can_gen_tokencode(struct openconnect_info *vpninfo, struct oc_auth_fo
  *  < 0, if unable to generate a tokencode
  *  = 0, on success
  */
-static int do_gen_tokencode(struct openconnect_info *vpninfo, struct oc_auth_form *form)
+static int can_gen_totp_code(struct openconnect_info *vpninfo,
+                             struct oc_auth_form *form,
+                             struct oc_form_opt *opt)
 {
-#ifdef LIBSTOKEN_HDR
-	char tokencode[STOKEN_MAX_TOKENCODE + 1];
-	struct oc_form_opt *opt;
+#if defined(LIBOATH_HDR)
+	if ((strcmp(opt->name, "secondary_password") != 0) ||
+	    vpninfo->token_bypassed)
+		return -EINVAL;
+	if (vpninfo->token_tries == 0) {
+		vpn_progress(vpninfo, PRG_DEBUG,
+			     _("OK to generate INITIAL tokencode\n"));
+		vpninfo->token_time = 0;
+	} else if (vpninfo->token_tries == 1) {
+		vpn_progress(vpninfo, PRG_DEBUG,
+			     _("OK to generate NEXT tokencode\n"));
+		vpninfo->token_time += OATH_TOTP_DEFAULT_TIME_STEP_SIZE;
+	} else {
+		/* limit the number of retries, to avoid account lockouts */
+		vpn_progress(vpninfo, PRG_INFO,
+			     _("Server is rejecting the soft token; switching to manual entry\n"));
+		return -ENOENT;
+	}
+	return 0;
+#else
+	return -EOPNOTSUPP;
+#endif
+}
 
-	for (opt = form->opts; ; opt = opt->next) {
-		/* this form might not have anything for us to do */
-		if (!opt)
-			return 0;
-		if (opt->type == OC_FORM_OPT_STOKEN)
-			break;
+/* Return value:
+ *  < 0, if unable to generate a tokencode
+ *  = 0, on success
+ */
+static int can_gen_tokencode(struct openconnect_info *vpninfo,
+                             struct oc_auth_form *form,
+                             struct oc_form_opt *opt)
+{
+	switch (vpninfo->token_mode) {
+	case TOKEN_MODE_STOKEN:
+		return can_gen_stoken_code(vpninfo, form, opt);
+
+	case TOKEN_MODE_TOTP:
+		return can_gen_totp_code(vpninfo, form, opt);
+
+	default:
+		return -EINVAL;
 	}
+}
 
-	if (!vpninfo->stoken_time)
-		vpninfo->stoken_time = time(NULL);
-	vpn_progress(vpninfo, PRG_INFO, _("Generating tokencode\n"));
+static int do_gen_stoken_code(struct openconnect_info *vpninfo,
+                              struct oc_auth_form *form,
+                              struct oc_form_opt *opt)
+{
+#ifdef LIBSTOKEN_HDR
+	char tokencode[STOKEN_MAX_TOKENCODE + 1];
+
+	if (!vpninfo->token_time)
+		vpninfo->token_time = time(NULL);
+	vpn_progress(vpninfo, PRG_INFO, _("Generating RSA token code\n"));
 
 	/* This doesn't normally fail */
-	if (stoken_compute_tokencode(vpninfo->stoken_ctx, vpninfo->stoken_time,
+	if (stoken_compute_tokencode(vpninfo->stoken_ctx, vpninfo->token_time,
 				     vpninfo->stoken_pin, tokencode) < 0) {
 		vpn_progress(vpninfo, PRG_ERR, _("General failure in libstoken.\n"));
 		return -EIO;
 	}
 
-	vpninfo->stoken_tries++;
+	vpninfo->token_tries++;
 	opt->value = strdup(tokencode);
 	return opt->value ? 0 : -ENOMEM;
 #else
 	return 0;
 #endif
 }
+
+static int do_gen_totp_code(struct openconnect_info *vpninfo,
+                            struct oc_auth_form *form,
+                            struct oc_form_opt *opt)
+{
+#if defined(LIBOATH_HDR)
+	int oath_err;
+	char tokencode[7];
+
+	if (!vpninfo->token_time) {
+		vpninfo->token_time = time(NULL);
+	}
+	vpn_progress(vpninfo, PRG_INFO, _("Generating OATH TOTP token code\n"));
+
+	oath_err = oath_totp_generate(vpninfo->oath_secret,
+	                              vpninfo->oath_secret_len,
+	                              vpninfo->token_time,
+	                              OATH_TOTP_DEFAULT_TIME_STEP_SIZE,
+	                              OATH_TOTP_DEFAULT_START_TIME,
+	                              6, tokencode);
+	if (oath_err != OATH_OK) {
+		vpn_progress(vpninfo, PRG_ERR,
+		             _("Unable to generate OATH TOTP token code: %s\n"),
+		             oath_strerror(oath_err));
+		return -EIO;
+	}
+
+	vpninfo->token_tries++;
+	opt->value = strdup(tokencode);
+	return opt->value ? 0 : -ENOMEM;
+#else
+	return 0;
+#endif
+}
+
+/* Return value:
+ *  < 0, if unable to generate a tokencode
+ *  = 0, on success
+ */
+static int do_gen_tokencode(struct openconnect_info *vpninfo,
+                            struct oc_auth_form *form)
+{
+	struct oc_form_opt *opt;
+
+	for (opt = form->opts; ; opt = opt->next) {
+		/* this form might not have anything for us to do */
+		if (!opt)
+			return 0;
+		if (opt->type == OC_FORM_OPT_TOKEN)
+			break;
+	}
+
+	switch (vpninfo->token_mode) {
+	case TOKEN_MODE_STOKEN:
+		return do_gen_stoken_code(vpninfo, form, opt);
+
+	case TOKEN_MODE_TOTP:
+		return do_gen_totp_code(vpninfo, form, opt);
+
+	default:
+		return -EINVAL;
+	}
+}
diff --git a/configure.ac b/configure.ac
index 89887f5..0a2460c 100644
--- a/configure.ac
+++ b/configure.ac
@@ -490,6 +490,17 @@ PKG_CHECK_MODULES(LIBSTOKEN, stoken,
 		 libstoken_pkg=yes],
 		 libstoken_pkg=no)
 
+AC_ARG_WITH([liboath],
+	AS_HELP_STRING([--without-liboath],
+		[Build without liboath library (default: test)]))
+AS_IF([test "x$with_liboath" != "xno"], [
+	PKG_CHECK_MODULES(LIBOATH, liboath,
+		[AC_SUBST(LIBOATH_PC, liboath)
+		 AC_DEFINE([LIBOATH_HDR], ["liboath/oath.h"])
+		 liboath_pkg=yes],
+		 liboath_pkg=no)
+])
+
 AC_CHECK_HEADER([if_tun.h],
     [AC_DEFINE([IF_TUN_HDR], ["if_tun.h"])],
     [AC_CHECK_HEADER([linux/if_tun.h],
diff --git a/http.c b/http.c
index 9bdc9d9..a7bc522 100644
--- a/http.c
+++ b/http.c
@@ -938,7 +938,7 @@ int openconnect_obtain_cookie(struct openconnect_info *vpninfo)
 	int xmlpost = 0;
 
 	/* Step 1: Unlock software token (if applicable) */
-	if (vpninfo->use_stoken) {
+	if (vpninfo->token_mode == TOKEN_MODE_STOKEN) {
 		result = prepare_stoken(vpninfo);
 		if (result)
 			return result;
diff --git a/libopenconnect.map.in b/libopenconnect.map.in
index eae3e70..f38c380 100644
--- a/libopenconnect.map.in
+++ b/libopenconnect.map.in
@@ -33,7 +33,9 @@ OPENCONNECT_2.0 {
 OPENCONNECT_2.1 {
  global:
 	openconnect_has_stoken_support;
+	openconnect_has_oath_support;
 	openconnect_set_stoken_mode;
+	openconnect_set_oath_mode;
 	openconnect_set_reported_os;
 } OPENCONNECT_2.0;
 
diff --git a/library.c b/library.c
index be4b119..00e6d38 100644
--- a/library.c
+++ b/library.c
@@ -1,6 +1,7 @@
 /*
  * OpenConnect (SSL + DTLS) VPN client
  *
+ * Copyright © 2013 John Morrissey <jwm at horde.net>
  * Copyright © 2008-2012 Intel Corporation.
  *
  * Authors: David Woodhouse <dwmw2 at infradead.org>
@@ -30,6 +31,10 @@
 #include LIBSTOKEN_HDR
 #endif
 
+#ifdef LIBOATH_HDR
+#include LIBOATH_HDR
+#endif
+
 #include <libxml/tree.h>
 
 #include "openconnect-internal.h"
@@ -145,6 +150,10 @@ void openconnect_vpninfo_free (struct openconnect_info *vpninfo)
 	if (vpninfo->stoken_ctx)
 		stoken_destroy(vpninfo->stoken_ctx);
 #endif
+#ifdef LIBOATH_HDR
+	if (vpninfo->oath_secret)
+		oath_done();
+#endif
 	/* No need to free deflate streams; they weren't initialised */
 	free(vpninfo);
 }
@@ -321,8 +330,17 @@ int openconnect_has_stoken_support(void)
 #endif
 }
 
+int openconnect_has_oath_support(void)
+{
+#ifdef LIBOATH_HDR
+	return 1;
+#else
+	return 0;
+#endif
+}
+
 /*
- * Enable software token generation if use_stoken == 1.
+ * Enable libstoken token generation if use_stoken == 1.
  *
  * If token_str is not NULL, try to parse the string.  Otherwise, try to read
  * the token data from ~/.stokenrc
@@ -335,12 +353,12 @@ int openconnect_has_stoken_support(void)
  *  = 0, on success
  */
 int openconnect_set_stoken_mode (struct openconnect_info *vpninfo,
-				 int use_stoken, const char *token_str)
+                                 int use_stoken, const char *token_str)
 {
 #ifdef LIBSTOKEN_HDR
 	int ret;
 
-	vpninfo->use_stoken = 0;
+	vpninfo->token_mode = TOKEN_MODE_NONE;
 	if (!use_stoken)
 		return 0;
 
@@ -356,7 +374,46 @@ int openconnect_set_stoken_mode (struct openconnect_info *vpninfo,
 	if (ret)
 		return ret;
 
-	vpninfo->use_stoken = 1;
+	vpninfo->token_mode = TOKEN_MODE_STOKEN;
+	return 0;
+#else
+	return -EOPNOTSUPP;
+#endif
+}
+
+/*
+ * Enable OATH software token generation.
+ *
+ * Return value:
+ *  = -EOPNOTSUPP, if libstoken is not available
+ *  = -EINVAL, if the token string is invalid (token_str was provided)
+ *  = -EIO, for other libstoken failures
+ *  = 0, on success
+ */
+int openconnect_set_oath_mode (struct openconnect_info *vpninfo,
+                               const char *token_str)
+{
+#ifdef LIBOATH_HDR
+	int ret;
+
+	ret = oath_init();
+	if (ret != OATH_OK) {
+		return -EIO;
+	}
+
+	if (strncasecmp(token_str, "base32:", strlen("base32:")) == 0) {
+		ret = oath_base32_decode(token_str + strlen("base32:"),
+		                         strlen(token_str) - strlen("base32:"),
+		                         &vpninfo->oath_secret,
+		                         &vpninfo->oath_secret_len);
+		if (ret != OATH_OK) {
+			return -EINVAL;
+		}
+	} else {
+	    vpninfo->oath_secret = strdup(token_str);
+	    vpninfo->oath_secret_len = strlen(token_str);
+	}
+
 	return 0;
 #else
 	return -EOPNOTSUPP;
diff --git a/main.c b/main.c
index d40207b..405bc0d 100644
--- a/main.c
+++ b/main.c
@@ -1,6 +1,7 @@
 /*
  * OpenConnect (SSL + DTLS) VPN client
  *
+ * Copyright © 2013 John Morrissey <jwm at horde.net>
  * Copyright © 2008-2012 Intel Corporation.
  * Copyright © 2008 Nick Andrew <nick at nick-andrew.net>
  *
@@ -66,7 +67,7 @@ static int validate_peer_cert(void *_vpninfo,
 			      const char *reason);
 static int process_auth_form(void *_vpninfo,
 			     struct oc_auth_form *form);
-static void init_stoken(struct openconnect_info *vpninfo,
+static void init_token(struct openconnect_info *vpninfo,
 			const char *token_str);
 
 /* A sanity check that the openconnect executable is running against a
@@ -110,7 +111,8 @@ enum {
 	OPT_USERAGENT,
 	OPT_NON_INTER,
 	OPT_DTLS_LOCAL_PORT,
-	OPT_STOKEN,
+	OPT_TOKEN_MODE,
+	OPT_TOKEN_SECRET,
 	OPT_OS,
 };
 
@@ -175,7 +177,10 @@ static struct option long_options[] = {
 	OPTION("force-dpd", 1, OPT_FORCE_DPD),
 	OPTION("non-inter", 0, OPT_NON_INTER),
 	OPTION("dtls-local-port", 1, OPT_DTLS_LOCAL_PORT),
-	OPTION("stoken", 2, OPT_STOKEN),
+	OPTION("token-mode", 1, OPT_TOKEN_MODE),
+	/* Alias --stoken to --token-secret for backwards compatibility. */
+	OPTION("stoken", 2, OPT_TOKEN_SECRET),
+	OPTION("token-secret", 2, OPT_TOKEN_SECRET),
 	OPTION("os", 1, OPT_OS),
 	OPTION(NULL, 0, 0)
 };
@@ -281,10 +286,15 @@ static void usage(void)
 	printf("      --no-cert-check             %s\n", _("Do not require server SSL cert to be valid"));
 	printf("      --non-inter                 %s\n", _("Do not expect user input; exit if it is required"));
 	printf("      --passwd-on-stdin           %s\n", _("Read password from standard input"));
-	printf("      --stoken[=TOKENSTRING]      %s\n", _("Use software token to generate password"));
+	printf("      --token-mode=MODE           %s\n", _("Software token type: stoken (default) or totp"));
+	printf("      --token-secret[=STRING]     %s\n", _("Software token secret (can be empty for stoken mode"));
+	printf("                                  %s\n", _("    to read from ~/.stokenrc)"));
 #ifndef LIBSTOKEN_HDR
 	printf("                                  %s\n", _("(NOTE: libstoken disabled in this build)"));
 #endif
+#ifndef LIBOATH_HDR
+	printf("                                  %s\n", _("(NOTE: liboath (TOTP) disabled in this build)"));
+#endif
 	printf("      --reconnect-timeout         %s\n", _("Connection retry timeout in seconds"));
 	printf("      --servercert=FINGERPRINT    %s\n", _("Server's certificate SHA1 fingerprint"));
 	printf("      --useragent=STRING          %s\n", _("HTTP header User-Agent: field"));
@@ -448,7 +458,6 @@ int main(int argc, char **argv)
 	char *pidfile = NULL;
 	FILE *fp = NULL;
 	char *config_arg;
-	int use_stoken = 0;
 	char *token_str = NULL;
 
 #ifdef ENABLE_NLS
@@ -491,6 +500,7 @@ int main(int argc, char **argv)
 	vpninfo->cert_expire_warning = 60 * 86400;
 	vpninfo->vpnc_script = DEFAULT_VPNCSCRIPT;
 	vpninfo->cancel_fd = -1;
+	vpninfo->token_mode = TOKEN_MODE_NONE;
 
 	if (!uname(&utsbuf))
 		vpninfo->localname = utsbuf.nodename;
@@ -712,8 +722,18 @@ int main(int argc, char **argv)
 		case OPT_DTLS_LOCAL_PORT:
 			vpninfo->dtls_local_port = atoi(config_arg);
 			break;
-		case OPT_STOKEN:
-			use_stoken = 1;
+		case OPT_TOKEN_MODE:
+			if (strcasecmp(config_arg, "stoken") == 0) {
+				vpninfo->token_mode = TOKEN_MODE_STOKEN;
+			} else if (strcasecmp(config_arg, "totp") == 0) {
+				vpninfo->token_mode = TOKEN_MODE_TOTP;
+			} else {
+				fprintf(stderr, _("Invalid software token mode \"%s\"\n"),
+					config_arg);
+				exit(1);
+			}
+			break;
+		case OPT_TOKEN_SECRET:
 			token_str = keep_config_arg();
 			break;
 		case OPT_OS:
@@ -750,8 +770,8 @@ int main(int argc, char **argv)
 #endif
 	}
 
-	if (use_stoken)
-		init_stoken(vpninfo, token_str);
+	if (vpninfo->token_mode != TOKEN_MODE_NONE)
+		init_token(vpninfo, token_str);
 
 	if (proxy && openconnect_set_http_proxy(vpninfo, strdup(proxy)))
 		exit(1);
@@ -1225,25 +1245,52 @@ static int process_auth_form(void *_vpninfo,
 	return -EINVAL;
 }
 
-static void init_stoken(struct openconnect_info *vpninfo,
-			const char *token_str)
+static void init_token(struct openconnect_info *vpninfo,
+                       const char *token_str)
 {
-	int ret = openconnect_set_stoken_mode(vpninfo, 1, token_str);
+	int ret;
 
-	switch (ret) {
-	case 0:
-		return;
-	case -EINVAL:
-		fprintf(stderr, _("Soft token string is invalid\n"));
-		exit(1);
-	case -ENOENT:
-		fprintf(stderr, _("Can't open ~/.stokenrc file\n"));
-		exit(1);
-	case -EOPNOTSUPP:
-		fprintf(stderr, _("OpenConnect was not built with soft token support\n"));
-		exit(1);
-	default:
-		fprintf(stderr, _("General failure in libstoken\n"));
-		exit(1);
+	switch (vpninfo->token_mode) {
+	case TOKEN_MODE_STOKEN:
+		ret = openconnect_set_stoken_mode(vpninfo, 1, token_str);
+
+		switch (ret) {
+		case 0:
+			return;
+		case -EINVAL:
+			fprintf(stderr, _("Soft token string is invalid\n"));
+			exit(1);
+		case -ENOENT:
+			fprintf(stderr, _("Can't open ~/.stokenrc file\n"));
+			exit(1);
+		case -EOPNOTSUPP:
+			fprintf(stderr, _("OpenConnect was not built with soft token support\n"));
+			exit(1);
+		default:
+			fprintf(stderr, _("General failure in liboath\n"));
+			exit(1);
+		}
+
+		break;
+    case TOKEN_MODE_TOTP:
+		ret = openconnect_set_oath_mode(vpninfo, token_str);
+
+		switch (ret) {
+		case 0:
+			return;
+		case -EINVAL:
+			fprintf(stderr, _("Soft token string is invalid\n"));
+			exit(1);
+		case -EOPNOTSUPP:
+			fprintf(stderr, _("OpenConnect was not built with liboath support\n"));
+			exit(1);
+		default:
+			fprintf(stderr, _("General failure in libstoken\n"));
+			exit(1);
+		}
+
+        break;
+
+	/* Option parsing already checked for invalid modes. */
 	}
 }
diff --git a/openconnect-internal.h b/openconnect-internal.h
index ab3926e..4bc70d1 100644
--- a/openconnect-internal.h
+++ b/openconnect-internal.h
@@ -1,6 +1,7 @@
 /*
  * OpenConnect (SSL + DTLS) VPN client
  *
+ * Copyright © 2013 John Morrissey <jwm at horde.net>
  * Copyright © 2008-2012 Intel Corporation.
  * Copyright © 2008 Nick Andrew <nick at nick-andrew.net>
  *
@@ -65,6 +66,12 @@
 #include LIBSTOKEN_HDR
 #endif
 
+enum {
+	TOKEN_MODE_NONE,
+	TOKEN_MODE_STOKEN,
+	TOKEN_MODE_TOTP,
+};
+
 #ifdef ENABLE_NLS
 #include <locale.h>
 #include <libintl.h>
@@ -180,14 +187,18 @@ struct openconnect_info {
 	int uid_csd_given;
 	int no_http_keepalive;
 
+	int token_mode;
+	int token_bypassed;
+	int token_tries;
+	time_t token_time;
 #ifdef LIBSTOKEN_HDR
 	struct stoken_ctx *stoken_ctx;
-#endif
-	int use_stoken;
-	int stoken_bypassed;
-	int stoken_tries;
-	time_t stoken_time;
 	char *stoken_pin;
+#endif
+#ifdef LIBOATH_HDR
+	char *oath_secret;
+	size_t oath_secret_len;
+#endif
 
 	OPENCONNECT_X509 *peer_cert;
 
diff --git a/openconnect.8.in b/openconnect.8.in
index b941144..5703099 100644
--- a/openconnect.8.in
+++ b/openconnect.8.in
@@ -49,7 +49,8 @@ openconnect \- Connect to Cisco AnyConnect VPN
 .OP \-\-no\-passwd
 .OP \-\-non\-inter
 .OP \-\-passwd\-on\-stdin
-.OP \-\-stoken[=\fItoken-string\fP]
+.OP \-\-token-mode=\fIstoken|totp\fP
+.OP \-\-token-secret=\fIsecret\fP
 .OP \-\-reconnect\-timeout
 .OP \-\-servercert sha1
 .OP \-\-useragent string
@@ -324,11 +325,17 @@ Do not expect user input; exit if it is required.
 .B \-\-passwd\-on\-stdin
 Read password from standard input
 .TP
-.B \-\-stoken[=\fItoken-string\fP]
-Use libstoken to generate one-time passwords compatible with the RSA SecurID
-system (when built with libstoken support).  If \fItoken-string\fP is omitted,
-libstoken will try to use the software token seed stored in \fI~/.stokenrc\fP,
-if this file exists.
+.B \-\-token\-mode=\fIstoken|totp\fP
+Select the algorithm to use to generate one-time passwords/verification
+codes. \fIstoken\fP for RSA SecureID requires libstoken, and \fItotp\fP
+for RFC 6238 requires liboath.
+.TP
+.B \-\-token\-secret[=\fIsecret\fP]
+The secret to use when generating one-time passwords/verification codes.
+If \fIsecret\fP is omitted and \-\-token-mode is \fIstoken\fP, libstoken
+will try to use the software token seed stored in \fI~/.stokenrc\fP, if this
+file exists. Base 32-encoded TOTP secrets can be specified by specifying
+"base32:" at the beginning of the secret.
 .TP
 .B \-\-reconnect\-timeout
 Keep reconnect attempts until so much seconds are elapsed. The default
diff --git a/openconnect.h b/openconnect.h
index 95cb6e8..d4467d6 100644
--- a/openconnect.h
+++ b/openconnect.h
@@ -1,6 +1,7 @@
 /*
  * OpenConnect (SSL + DTLS) VPN client
  *
+ * Copyright © 2013 John Morrissey <jwm at horde.net>
  * Copyright © 2008-2012 Intel Corporation.
  * Copyright © 2008 Nick Andrew <nick at nick-andrew.net>
  *
@@ -86,7 +87,7 @@
 #define OC_FORM_OPT_PASSWORD	2
 #define OC_FORM_OPT_SELECT	3
 #define OC_FORM_OPT_HIDDEN	4
-#define OC_FORM_OPT_STOKEN	5
+#define OC_FORM_OPT_TOKEN	5
 
 /* char * fields are static (owned by XML parser) and don't need to be
    freed by the form handling code -- except for value, which for TEXT
@@ -163,10 +164,13 @@ void openconnect_set_hostname (struct openconnect_info *, char *);
 char *openconnect_get_urlpath (struct openconnect_info *);
 void openconnect_set_urlpath (struct openconnect_info *, char *);
 
-/* This function does *not* take ownership of the string; it is parsed
+/* These functions do *not* take ownership of the string; it is parsed
    and then discarded. */
 int openconnect_set_stoken_mode (struct openconnect_info *,
-				 int use_stoken, const char *token_str);
+                                 int use_stoken,
+                                 const char *token_str);
+int openconnect_set_oath_mode (struct openconnect_info *,
+                               const char *token_str);
 
 /* This function does *not* take ownership of the string; it's copied
    into a static buffer in the vpninfo. The size must be 41 bytes,
@@ -262,5 +266,6 @@ int openconnect_has_tss_blob_support(void);
 
 /* Software token capabilities. */
 int openconnect_has_stoken_support(void);
+int openconnect_has_oath_support(void);
 
 #endif /* __OPENCONNECT_H__ */
diff --git a/www/building.xml b/www/building.xml
index 07f3689..ac06694 100644
--- a/www/building.xml
+++ b/www/building.xml
@@ -33,6 +33,7 @@ And <em>optionally</em> also:
   <li><b><tt><a href="http://code.google.com/p/libproxy/">libproxy</a></tt></b></li>
   <li><b><tt><a href="http://trousers.sourceforge.net/">trousers</a></tt></b> <i>(for TPM support if using GnuTLS)</i></li>
   <li><b><tt><a href="http://stoken.sourceforge.net/">libstoken</a></tt></b> <i>(for SecurID software token support)</i></li>
+  <li><b><tt><a href="http://www.nongnu.org/oath-toolkit/">liboath</a></tt></b> <i>(for RFC6238 TOTP support)</i></li>
 </ul>
 <p>OpenConnect supports the use of HTTP and SOCKS proxies to connect to the
 AnyConnect service, even without using libproxy. You may wish to use libproxy
diff --git a/www/changelog.xml b/www/changelog.xml
index be07c95..13eb07f 100644
--- a/www/changelog.xml
+++ b/www/changelog.xml
@@ -22,6 +22,7 @@
        <li>Fix compatibility issues with XML POST authentication.</li>
        <li>Fix memory leaks on <tt>realloc()</tt> failure.</li>
        <li>Fix certificate validation problem caused by hostname canonicalisation.</li>
+       <li>Add RFC6238 TOTP token support using <a href="http://www.nongnu.org/oath-toolkit/">liboath</a>.</li>
      </ul><br/>
   </li>
   <li><b><a href="ftp://ftp.infradead.org/pub/openconnect/openconnect-4.99.tar.gz">OpenConnect v4.99</a></b>
diff --git a/www/features.xml b/www/features.xml
index 0f8eeec..4060c4a 100644
--- a/www/features.xml
+++ b/www/features.xml
@@ -18,6 +18,7 @@
   <li>Authentication via HTTP forms.</li>
   <li>Authentication using SSL certificates — from local file, <a href="http://en.wikipedia.org/wiki/Trusted_Platform_Module">Trusted Platform Module</a> and <i>(when built with GnuTLS)</i> PKCS#11 smartcards.</li>
   <li>Authentication using SecurID software tokens <i>(when built with libstoken)</i></li>
+  <li>Authentication using RFC6238 TOTP software tokens <i>(when built with liboath)</i></li>
   <li><i>UserGroup</i> support for selecting between multiple configurations on a single VPN server.</li>
   <li>Data transport over TCP <i>(HTTPS)</i> or UDP <i>(DTLS)</i>.</li>
   <li>Keepalive and Dead Peer Detection on both HTTPS and DTLS.</li>
-- 
1.7.2.5

-- 
John Morrissey          _o            /\         ----  __o
jwm at horde.net        _-< \_          /  \       ----  <  \,
www.horde.net/    __(_)/_(_)________/    \_______(_) /_(_)__



More information about the openconnect-devel mailing list