diff --git a/Makefile.am b/Makefile.am index 3b59e0d9..734f45d1 100644 --- a/Makefile.am +++ b/Makefile.am @@ -5,15 +5,19 @@ openfortivpn_SOURCES = src/config.c src/config.h src/hdlc.c src/hdlc.h \ src/http.c src/http.h src/io.c src/io.h src/ipv4.c \ src/ipv4.h src/log.c src/log.h src/tunnel.c \ src/tunnel.h src/main.c src/ssl.h src/xml.c \ + src/xml.h src/userinput.c src/userinput.h \ + src/saml.c src/saml.h src/xml.h src/userinput.c src/userinput.h + openfortivpn_CPPFLAGS = -DSYSCONFDIR=\"$(sysconfdir)\" \ -DPPP_PATH=\"@PPP_PATH@\" \ -DNETSTAT_PATH=\"@NETSTAT_PATH@\" \ -DRESOLVCONF_PATH=\"@RESOLVCONF_PATH@\" \ -DREVISION=\"@REVISION@\" \ - $(OPENSSL_CFLAGS) $(LIBSYSTEMD_CFLAGS) + $(OPENSSL_CFLAGS) $(LIBSYSTEMD_CFLAGS) \ + $(LIBGTK_CFLAGS) $(LIBWEBKIT_CFLAGS) openfortivpn_CFLAGS = -Wall -pedantic -openfortivpn_LDADD = $(OPENSSL_LIBS) $(LIBSYSTEMD_LIBS) +openfortivpn_LDADD = $(OPENSSL_LIBS) $(LIBSYSTEMD_LIBS) $(LIBGTK_LIBS) $(LIBWEBKIT_LIBS) PATHFILES = CLEAN_LOCALS = diff --git a/README.md b/README.md index fd542430..9b039c99 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,11 @@ Examples openfortivpn vpn-gateway:8443 --username=foo --realm=bar ``` +* Connect using saml: + ``` + openfortivpn vpn-gateway:8443 --saml + ``` + * Store password securely with a pinentry program: ``` openfortivpn vpn-gateway:8443 --username=foo --pinentry=pinentry-mac @@ -131,11 +136,11 @@ For other distros, you'll need to build and install from source: 1. Install build dependencies. - * RHEL/CentOS/Fedora: `gcc` `automake` `autoconf` `openssl-devel` `make` `pkg-config` - * Debian/Ubuntu: `gcc` `automake` `autoconf` `libssl-dev` `make` `pkg-config` - * Arch Linux: `gcc` `automake` `autoconf` `openssl` `pkg-config` - * Gentoo Linux: `net-dialup/ppp` `pkg-config` - * openSUSE: `gcc` `automake` `autoconf` `libopenssl-devel` `pkg-config` + * RHEL/CentOS/Fedora: `gcc` `automake` `autoconf` `openssl-devel` `make` `pkg-config` `gtk3-devel` `webkit2gtk4.0-devel` + * Debian/Ubuntu: `gcc` `automake` `autoconf` `libssl-dev` `make` `pkg-config` `libgtk-3-dev` `libwebkitgtk-3.0-dev` + * Arch Linux: `gcc` `automake` `autoconf` `openssl` `pkg-config` `extra/webkit2gtk` + * Gentoo Linux: `net-dialup/ppp` `pkg-config` `net-libs/webkit-gtk` + * openSUSE: `gcc` `automake` `autoconf` `libopenssl-devel` `pkg-config` `webkit2gtk3` * macOS (Homebrew): `automake` `autoconf` `openssl@1.1` `pkg-config` * FreeBSD: `automake` `autoconf` `libressl` `pkgconf` diff --git a/configure.ac b/configure.ac index 7bccdfe4..ecd16b01 100644 --- a/configure.ac +++ b/configure.ac @@ -35,6 +35,8 @@ PKG_CHECK_MODULES(OPENSSL, [libssl >= 1.0.2 libcrypto >= 1.0.2], [], [AC_MSG_ERR AC_CHECK_LIB([pthread], [pthread_create], [], [AC_MSG_ERROR([Cannot find libpthread.])]) AC_CHECK_LIB([util], [forkpty], [], [AC_MSG_ERROR([Cannot find libutil.])]) PKG_CHECK_MODULES(LIBSYSTEMD, [libsystemd], [AC_DEFINE(HAVE_SYSTEMD)], [AC_MSG_RESULT([libsystemd not present])]) +PKG_CHECK_MODULES(LIBGTK, [gtk+-3.0], [AC_DEFINE(HAVE_GTK)], [AC_MSG_RESULT([gtk not present])]) +PKG_CHECK_MODULES(LIBWEBKIT, [webkit2gtk-4.0], [AC_DEFINE(HAVE_WEBKIT)], [AC_MSG_RESULT([webkit2gtk not present])]) # we assume presence of the following C standard headers # and omit them in the following header checks diff --git a/doc/openfortivpn.1.in b/doc/openfortivpn.1.in index abed22f0..94f26081 100644 --- a/doc/openfortivpn.1.in +++ b/doc/openfortivpn.1.in @@ -10,6 +10,7 @@ openfortivpn \- Client for PPP+SSL VPN tunnel services [\fB\-p\fR \fI\fR] [\fB\-\-cookie=\fI\fR] [\fB\-\-cookie\-on\-stdin\fR] +[\fB\-\-saml\fR] [\fB\-\-pinentry=\fI\fR] [\fB\-\-otp=\fI\fR] [\fB\-\-otp\-prompt=\fI\fR] @@ -79,6 +80,8 @@ A valid cookie (SVPNCOOKIE) to use in place of username and password. .TP \fB\-\-cookie\-on\-stdin\fR Read the cookie (SVPNCOOKIE) from standard input. +\fB\-\-saml\fR +Use saml for authentication. .TP \fB\-\-pinentry=\fI\fR The pinentry program to use. Allows supplying the password in a secure manner. diff --git a/src/config.c b/src/config.c index 94f0d6aa..7aa0c28e 100644 --- a/src/config.c +++ b/src/config.c @@ -77,6 +77,7 @@ const struct vpn_config invalid_cfg = { .pem_passphrase = {'\0'}, .pem_passphrase_set = 0, .insecure_ssl = -1, + .saml = -1, .cipher_list = NULL, .min_tls = -1, .seclevel_1 = -1, @@ -282,6 +283,15 @@ int load_config(struct vpn_config *cfg, const char *filename) continue; } cfg->no_ftm_push = no_ftm_push; + } else if (strcmp(key, "saml") == 0) { + int saml = strtob(val); + + if (saml < 0) { + log_warn("Bad saml in configuration file: \"%s\".\n", + val); + continue; + } + cfg->saml = saml; } else if (strcmp(key, "pinentry") == 0) { free(cfg->pinentry); cfg->pinentry = strdup(val); @@ -590,6 +600,10 @@ void merge_config(struct vpn_config *dst, struct vpn_config *src) } if (src->insecure_ssl != invalid_cfg.insecure_ssl) dst->insecure_ssl = src->insecure_ssl; + + if (src->saml != invalid_cfg.saml) + dst->saml = src->saml; + if (src->cipher_list) { free(dst->cipher_list); dst->cipher_list = src->cipher_list; diff --git a/src/config.h b/src/config.h index 6e47ce5e..c2caa3fe 100644 --- a/src/config.h +++ b/src/config.h @@ -58,12 +58,13 @@ struct x509_digest { char data[SHA256STRLEN]; }; -#define GATEWAY_HOST_SIZE 253 -#define USERNAME_SIZE 64 -#define PASSWORD_SIZE 256 -#define OTP_SIZE 64 -#define REALM_SIZE 63 -#define PEM_PASSPHRASE_SIZE 31 +#define GATEWAY_HOST_SIZE 253 +#define USERNAME_SIZE 64 +#define PASSWORD_SIZE 256 +#define OTP_SIZE 64 +#define REALM_SIZE 63 +#define PEM_PASSPHRASE_SIZE 31 +#define SAML_BROWSER_SIZE 8 /* * RFC 6265 does not limit the size of cookies: @@ -126,6 +127,7 @@ struct vpn_config { int pem_passphrase_set; int insecure_ssl; int min_tls; + int saml; int seclevel_1; char *cipher_list; struct x509_digest *cert_whitelist; diff --git a/src/main.c b/src/main.c index 7e7efa1d..a6e0e398 100644 --- a/src/main.c +++ b/src/main.c @@ -19,6 +19,7 @@ #include "tunnel.h" #include "userinput.h" #include "log.h" +#include "saml.h" #include @@ -75,7 +76,7 @@ #define usage \ "Usage: openfortivpn [[:]] [-u ] [-p ]\n" \ -" [--cookie=] [--cookie-on-stdin]\n" \ +" [--cookie=] [--cookie-on-stdin] [--saml]\n" \ " [--otp=] [--otp-delay=] [--otp-prompt=]\n" \ " [--pinentry=] [--realm=]\n" \ " [--ifname=] [--set-routes=<0|1>]\n" \ @@ -113,6 +114,7 @@ PPPD_USAGE \ " " SYSCONFDIR "/openfortivpn/config).\n" \ " -u , --username= VPN account username.\n" \ " -p , --password= VPN account password.\n" \ +" --saml Use saml login.\n" \ " --cookie= A valid session cookie (SVPNCOOKIE).\n" \ " --cookie-on-stdin Read the cookie (SVPNCOOKIE) from standard input.\n" \ " -o , --otp= One-Time-Password.\n" \ @@ -243,6 +245,7 @@ int main(int argc, char **argv) .use_engine = 0, .user_agent = NULL, }; + struct vpn_config cli_cfg = invalid_cfg; const struct option long_options[] = { @@ -255,6 +258,7 @@ int main(int argc, char **argv) {"password", required_argument, NULL, 'p'}, {"cookie", required_argument, NULL, 0}, {"cookie-on-stdin", no_argument, NULL, 0}, + {"saml", no_argument, &cli_cfg.saml, 1}, {"otp", required_argument, NULL, 'o'}, {"otp-prompt", required_argument, NULL, 0}, {"otp-delay", required_argument, NULL, 0}, @@ -634,15 +638,21 @@ int main(int argc, char **argv) log_error("Specify a valid host:port couple.\n"); goto user_error; } - // Check username - if (cfg.username[0] == '\0' && !cfg.cookie) + + if (geteuid() != 0) { + log_error("This process was not spawned with root privileges, which are required.\n"); + ret = EXIT_FAILURE; + goto exit; + } + + if (cfg.username[0] == '\0' && !cfg.cookie && !cfg.saml) // Need either username or cert if (cfg.user_cert == NULL) { log_error("Specify a username.\n"); goto user_error; } // If username but no password given, interactively ask user - if (!cfg.password_set && cfg.username[0] != '\0' && !cfg.cookie) { + if (!cfg.password_set && cfg.username[0] != '\0' && !cfg.cookie && !cfg.saml) { char hint[USERNAME_SIZE + 1 + REALM_SIZE + 1 + GATEWAY_HOST_SIZE + 10]; sprintf(hint, "%s_%s_%s_password", @@ -654,18 +664,13 @@ int main(int argc, char **argv) log_debug("Configuration host = \"%s\"\n", cfg.gateway_host); log_debug("Configuration realm = \"%s\"\n", cfg.realm); log_debug("Configuration port = \"%d\"\n", cfg.gateway_port); + if (cfg.username[0] != '\0') log_debug("Configuration username = \"%s\"\n", cfg.username); log_debug_all("Configuration password = \"%s\"\n", cfg.password); if (cfg.otp[0] != '\0') log_debug("One-time password = \"%s\"\n", cfg.otp); - if (geteuid() != 0) { - log_error("This process was not spawned with root privileges, which are required.\n"); - ret = EXIT_FAILURE; - goto exit; - } - do { if (run_tunnel(&cfg) != 0) ret = EXIT_FAILURE; diff --git a/src/saml.c b/src/saml.c new file mode 100644 index 00000000..648697c0 --- /dev/null +++ b/src/saml.c @@ -0,0 +1,222 @@ +#include "log.h" +#include "config.h" +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +// Global variables that need to be shared with the child process. +char *svpncookie = NULL; +size_t svpncookie_size = 0; + +static void destroy_window_cb(GtkWidget *widget, GtkWidget *window) +{ + gtk_main_quit(); +} + +static gboolean close_web_view_cb(WebKitWebView *webView, GtkWidget *window) +{ + gtk_widget_destroy(window); + return TRUE; +} + +static void cookie_ready_callback(GObject *obj, GAsyncResult *res, + gpointer user_data) +{ + WebKitCookieManager *cookie_mgr = (WebKitCookieManager *)obj; + + GError *err = NULL; + GList *cookies = + webkit_cookie_manager_get_cookies_finish(cookie_mgr, res, &err); + + if (err != NULL) { + printf("There was an error while getting the cookies: %s\n", + err->message); + return; + } + + // There was no cookie for the specified domain. + if (!cookies) + return; + + GList *cur = cookies; + bool found_cookie = false; + + while (cur) { + if (strcmp(soup_cookie_get_name(cur->data), "SVPNCOOKIE") == + 0) { + strcpy(svpncookie, "SVPNCOOKIE="); + + strncpy(svpncookie + + sizeof(char) * strlen("SVPNCOOKIE="), + soup_cookie_get_value(cur->data), + svpncookie_size); + + // Just in case that strncpy doesn't set the null terminator. + svpncookie[svpncookie_size - 1] = '\0'; + found_cookie = true; + break; + } + + cur = cur->next; + } + + g_list_free(cookies); + + // Exit the browser and gtk when we got the cookie. + if (found_cookie) + gtk_main_quit(); +} + +static void cookie_changed_cb(WebKitCookieManager *self, gpointer *data) +{ + char url[strlen("https://") + strlen((const char *)data) + 1]; + sprintf(url, "https://%s", (char *)data); + + webkit_cookie_manager_get_cookies(self, url, NULL, + cookie_ready_callback, data); +} + +/* Returns the given directory/file under the home directory. + * Return value must be manyally freed */ +static char *get_under_home_dir(char *dir) +{ + char *username = getlogin(); + char *result = + malloc(strlen("/home//") + strlen(username) + strlen(dir) + 1); + + sprintf(result, "/home/%s/%s", username, dir); + + return result; +} + +static int webkit_get_cookie(char *gateway_host, uint16_t gateway_port, + char *realm, char *website_cert) +{ + char *cookie_file = get_under_home_dir(".openfortivpncookies"); + + gtk_init(0, NULL); + GtkWidget *main_window = gtk_window_new(GTK_WINDOW_TOPLEVEL); + gtk_window_set_default_size(GTK_WINDOW(main_window), 800, 600); + + WebKitWebsiteDataManager *data_mgr = + webkit_website_data_manager_new(NULL); + + WebKitCookieManager *cookie_mgr = + webkit_website_data_manager_get_cookie_manager(data_mgr); + + webkit_cookie_manager_set_persistent_storage( + cookie_mgr, cookie_file, WEBKIT_COOKIE_PERSISTENT_STORAGE_TEXT); + + WebKitWebContext *web_context = + webkit_web_context_new_with_website_data_manager(data_mgr); + WebKitWebView *web_view = + WEBKIT_WEB_VIEW(webkit_web_view_new_with_context(web_context)); + + gtk_container_add(GTK_CONTAINER(main_window), GTK_WIDGET(web_view)); + g_signal_connect(main_window, "destroy", G_CALLBACK(destroy_window_cb), + NULL); + g_signal_connect(web_view, "close", G_CALLBACK(close_web_view_cb), + main_window); + g_signal_connect(cookie_mgr, "changed", G_CALLBACK(cookie_changed_cb), + gateway_host); + + GTlsCertificate *cert = + g_tls_certificate_new_from_pem(website_cert, -1, NULL); + + webkit_web_context_allow_tls_certificate_for_host(web_context, cert, + gateway_host); + + // Maximum possible port length is 5 (65536/XXXXX) + char saml_url[strlen("https://XXXXX/remote/saml/start") + strlen(gateway_host) + + strlen("?realm=") + strlen(realm) + 1]; + + if (realm) { + sprintf(saml_url, "https://%s:%d/remote/saml/start?realm=%s", gateway_host, + gateway_port, realm); + } else { + sprintf(saml_url, "https://%s:%d/remote/saml/start", gateway_host, gateway_port); + } + + webkit_web_view_load_uri(web_view, saml_url); + + gtk_widget_grab_focus(GTK_WIDGET(web_view)); + gtk_widget_show_all(main_window); + + gtk_main(); + + // Don't allow other users to read the cookies. + chmod(cookie_file, 0600); + + free(cookie_file); + return 0; +} + +/* Returns 0 if the cookie was set successfully. -1 if there was an error. */ +int saml_get_cookie(char *gateway_host, uint16_t gateway_port, char *realm, + char **dst_cookie, char *cert) +{ + svpncookie_size = sizeof(char) * (COOKIE_SIZE + 1); + + // This is needed because the browser (child process) needs to set the + // cookie (write to the memory) which is not possible with malloc, etc. + svpncookie = mmap(NULL, svpncookie_size, PROT_READ | PROT_WRITE, + MAP_SHARED | MAP_ANONYMOUS, -1, 0); + + char *user_id = getenv("SUDO_UID"); + + if (!user_id) { + log_error( + "Could not find the SUDO_UID enviroment variable." + "Please set it to your UID if you're not running with sudo (required to run the browser)\n"); + + goto exit_error; + } + + uid_t browser_uid = atoi(user_id); + + if (browser_uid == 0) { + log_error( + "Cannot run the browser as root. Please set SUDO_UID to an appropiate user.\n"); + goto exit_error; + } + + if (fork() == 0) { + char *home_dir = get_under_home_dir(""); + char *xdg_runtime_dir = malloc(strlen("/run/user/XXXXXXXXXX") + 1); + sprintf(xdg_runtime_dir, "/run/user/%d", browser_uid); + + clearenv(); + setenv("HOME", home_dir, 1); + setenv("DISPLAY", ":0", 1); + + // Needed for wayland + setenv("XDG_RUNTIME_DIR", xdg_runtime_dir, 1); + + setuid(browser_uid); + webkit_get_cookie(gateway_host, gateway_port, realm, cert); + + free(home_dir); + free(xdg_runtime_dir); + exit(EXIT_SUCCESS); + } + + wait(NULL); + + *dst_cookie = strndup(svpncookie, COOKIE_SIZE); + + int ret = 0; + goto exit; +exit_error: + ret = -1; + goto exit; +exit: + munmap(svpncookie, svpncookie_size); + return ret; +} diff --git a/src/saml.h b/src/saml.h new file mode 100644 index 00000000..77f05e5a --- /dev/null +++ b/src/saml.h @@ -0,0 +1,8 @@ +#ifndef OPENFORTIVPN_SAML_H +#define OPENFORTIVPN_SAML_H + +#include + +int saml_get_cookie(char *gateway_host, uint16_t gateway_port, char *realm, char **dst_cookie, char *cert); + +#endif diff --git a/src/tunnel.c b/src/tunnel.c index 421fbfb9..743a15dd 100644 --- a/src/tunnel.c +++ b/src/tunnel.c @@ -30,6 +30,7 @@ #include "http.h" #include "log.h" #include "userinput.h" +#include "saml.h" #include #ifndef OPENSSL_NO_ENGINE @@ -37,6 +38,7 @@ #endif #include #include +#include #if HAVE_SYSTEMD #include #endif @@ -1273,10 +1275,30 @@ int run_tunnel(struct vpn_config *config) // Step 2: connect to the HTTP interface and authenticate to get a // cookie + if (config->saml) { + X509 *cert = SSL_get_peer_certificate(tunnel.ssl_handle); + + BIO *b = BIO_new(BIO_s_mem()); + PEM_write_bio_X509(b, cert); + + char *cert_buffer; + BIO_get_mem_data(b, &cert_buffer); + + BIO_set_close(b, BIO_NOCLOSE); + BIO_free(b); + X509_free(cert); + + saml_get_cookie(config->gateway_host, config->gateway_port, + config->realm, &config->cookie, cert_buffer); + + free(cert_buffer); + } + if (config->cookie) ret = auth_set_cookie(&tunnel, config->cookie); else ret = auth_log_in(&tunnel); + if (ret != 1) { log_error("Could not authenticate to gateway. Please check the password, client certificate, etc.\n"); log_debug("%s (%d)\n", err_http_str(ret), ret);