--- /dev/null
+/*
+ * OpenConnect (SSL + DTLS) VPN client
+ *
+ * Copyright © 2022 David Woodhouse <dwmw2@infradead.org>
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public License
+ * version 2.1, as published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ */
+
+#include <config.h>
+
+#include "openconnect-internal.h"
+
+#ifndef HAVE_HPKE_SUPPORT
+int handle_external_browser(struct openconnect_info *vpninfo)
+{
+ return -EINVAL;
+}
+#else
+
+#include <ctype.h>
+
+#define HPKE_TAG_PUBKEY 1
+#define HPKE_TAG_AEAD_TAG 2
+#define HPKE_TAG_CIPHERTEXT 3
+#define HPKE_TAG_IV 4
+/*
+ * Hard-coded HTTP responses
+ */
+static const char response_404[] =
+ "HTTP/1.1 404 Not Found\r\n"
+ "Connection: close\r\n"
+ "Content-Type: text/html\r\n"
+ "Content-Length: 0\r\n\r\n";
+
+static const char response_302[] =
+ "HTTP/1.1 302 Found\r\n"
+ "Connection: close\r\n"
+ "Content-Type: text/html\r\n"
+ "Content-Length: 0\r\n"
+ "Location: %s\r\n\r\n";
+
+static const char response_200[] =
+ "HTTP/1.1 200 OK\r\n"
+ "Connection: close\r\n"
+ "Content-Type: text/html\r\n\r\n"
+ "<html><title>Success</title><body>Success</body></html>\r\n";
+
+
+/*
+ * If we use an external browser where we can't just snoop for cookies
+ * or completion... how do we get the results back? Cisco's answer:
+ * We run an HTTP server on http://localhost:29786/ and listen for
+ * a GET request to /api/sso/<base64 blob>?return=<finalurl>. It
+ * returns a redirect to that final URL, which is a pretty 'success'
+ * page. And decodes the base64 blob to obtain the SSO token, qv.
+ */
+int handle_external_browser(struct openconnect_info *vpninfo)
+{
+ int ret = 0;
+ struct sockaddr_in6 sin6 = { };
+ sin6.sin6_family = AF_INET6;
+ sin6.sin6_port = htons(29786);
+ sin6.sin6_addr = in6addr_loopback;
+
+ int listen_fd = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
+ if (listen_fd < 0) {
+ char *errstr;
+ sockerr:
+#ifdef _WIN32
+ errstr = openconnect__win32_strerror(WSAGetLastError());
+#else
+ errstr = strerror(errno);
+#endif
+
+ vpn_progress(vpninfo, PRG_ERR,
+ _("Failed to listen on local port 29786: %s\n"),
+ errstr);
+#ifdef _WIN32
+ free(errstr);
+#endif
+ if (listen_fd >= 0)
+ closesocket(listen_fd);
+ return -EIO;
+ }
+
+ int optval = 1;
+ setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval));
+
+ if (bind(listen_fd, (void *)&sin6, sizeof(sin6)) < 0)
+ goto sockerr;
+
+ if (listen(listen_fd, 1))
+ goto sockerr;
+
+ if (set_sock_nonblock(listen_fd))
+ goto sockerr;
+
+
+ /* Now that we are listening on the socket, we can spawn the browser...
+ or just tell the user to, for now */
+ vpn_progress(vpninfo, PRG_ERR,
+ _("Point browser to this URL:\n%s\n"),
+ vpninfo->sso_login);
+
+ char *returl = NULL;
+ struct oc_text_buf *b64_buf = NULL;
+
+ /* There may be other stray connections. Repeat until we have one
+ * that looks like the actual auth attempt from the browser. */
+ while (1) {
+ int accept_fd = cancellable_accept(vpninfo, listen_fd);
+ if (accept_fd < 0) {
+ ret = accept_fd;
+ goto out;
+ }
+ vpn_progress(vpninfo, PRG_TRACE,
+ _("Accepted incoming external-browser connection on port 29786\n"));
+
+ char line[4096];
+ ret = cancellable_gets(vpninfo, accept_fd, line, sizeof(line));
+ if (ret < 15 || strncmp(line, "GET /", 5) ||
+ strncmp(line + ret - 9, " HTTP/1.", 8)) {
+ vpn_progress(vpninfo, PRG_TRACE,
+ _("Invalid incoming external-browser request\n"));
+ closesocket(accept_fd);
+ continue;
+ }
+ if (strncmp(line, "GET /api/sso/", 13)) {
+ give_404:
+ cancellable_send(vpninfo, accept_fd, response_404, sizeof(response_404) - 1);
+ closesocket(accept_fd);
+ continue;
+ }
+
+ /*
+ * OK, now we have a "GET /api/sso/… HTTP/1.x" that looks sane.
+ * Kill the " HTTP/1.x" at the end.
+ * */
+ line[ret - 9] = 0;
+
+ /* Scan for ?return= (and other params that shouldn't be there) */
+ char *b64 = line + 13;
+ char *q = strchr(b64, '?');
+ while (q) {
+ *q = 0;
+ q++;
+ if (!strncmp(q, "return=", 7))
+ returl = q + 7;
+ q = strchr(q, '&');
+ }
+
+ /* Attempt to decode the base64 */
+ urldecode_inplace(b64);
+ b64_buf = buf_alloc();
+ if (!b64_buf) {
+ ret = -ENOMEM;
+ closesocket(accept_fd);
+ goto out;
+ }
+
+ b64_buf->data = openconnect_base64_decode(&ret, b64);
+ if (ret < 0) {
+ /* If the final part of the URL after /api/sso/ is not
+ * valid base64, give a 404 and wait for a valid req. */
+ buf_free(b64_buf);
+ b64_buf = NULL;
+ goto give_404;
+ }
+ b64_buf->pos = b64_buf->buf_len = ret;
+
+ /* Decode and store the returl (since we'll reuse the line buf) */
+ if (returl) {
+ urldecode_inplace(returl);
+ returl = strdup(returl);
+ }
+
+ /* Now consume the rest of the HTTP request lines */
+ while (cancellable_gets(vpninfo, accept_fd, line, sizeof(line)) > 0) {
+ vpn_progress(vpninfo, PRG_DEBUG,
+ "< %s\n", line);
+ }
+
+ /* Finally, send the response to redirect to the success page */
+ if (returl) {
+ line[sizeof(line) - 1] = 0;
+ ret = snprintf(line, sizeof(line) - 1, response_302, returl);
+ ret = cancellable_send(vpninfo, accept_fd, line, ret);
+ free(returl);
+ returl = NULL;
+ } else {
+ ret = cancellable_send(vpninfo, accept_fd, response_200, sizeof(response_200) - 1);
+ }
+ closesocket(accept_fd);
+ if (ret < 0)
+ goto out_b64;
+
+ break;
+ }
+
+ vpn_progress(vpninfo, PRG_DEBUG, _("Got encrypted SSO token of %d bytes\n"),
+ b64_buf->pos);
+
+ /* Example encrypted token:
+ < 0000: 00 01 00 01 00 5b 30 59 30 13 06 07 2a 86 48 ce |.....[0Y0...*.H.|
+ < 0010: 3d 02 01 06 08 2a 86 48 ce 3d 03 01 07 03 42 00 |=....*.H.=....B.|
+ < 0020: 04 fa 42 63 40 b6 f4 a6 02 9a dd 57 f5 8c 74 3e |..Bc@......W..t>|
+ < 0030: 11 82 18 8d 78 c4 b5 13 d0 c7 c0 d7 f9 79 6c 16 |....x........yl.|
+ < 0040: e9 bc 30 fa f0 ea 09 8d 17 d1 84 e4 08 55 31 28 |..0..........U1(|
+ < 0050: a6 62 e4 6d c5 7c be 19 d9 14 41 37 20 6e 4c ce |.b.m.|....A7 nL.|
+ < 0060: 2c 00 02 00 0c ac 81 ce 79 56 6e 4c 00 cc 9b e3 |,.......yVnL....|
+ < 0070: d0 00 03 00 17 bb 4d 2e 57 61 c0 90 58 86 86 79 |......M.Wa..X..y|
+ < 0080: 64 05 28 0c c9 f2 c8 c2 2a 2e fb 5c 00 04 00 0c |d.(.....*..\....|
+ < 0090: 2c 03 5f 13 3c b7 27 7e 36 fe 5a b8 |,._.<.'~6.Z.|
+
+ This contains the server's DH pubkey (type 1) at 0x0008,
+ the AEAD tag (type 2) at 0x0065, the ciphertext (type 3) at 0x0075
+ and the IV (type 4) at 0x0090.
+ */
+
+ /* tagdata[0] is unused because I can't be doing with all that
+ * (HPKE_TAG_IV-1) nonsense. */
+ struct {
+ void *p;
+ int len;
+ } tagdata[HPKE_TAG_IV + 1];
+
+ memset(tagdata, 0, sizeof(tagdata));
+
+ int pos = 0;
+ ret = 0;
+ while (pos < b64_buf->buf_len) {
+ uint16_t tag, len;
+ if (pos + 4 > b64_buf->pos) {
+ ret = -EINVAL;
+ break;
+ }
+
+ tag = load_be16(b64_buf->data + pos);
+ len = load_be16(b64_buf->data + pos + 2);
+
+ /* Special case, first word must be 0x0001 before the TLVs start */
+ if (!pos) {
+ if (tag != 0x0001) {
+ ret = -EINVAL;
+ break;
+ }
+ pos += 2;
+ continue;
+ }
+
+ if (tag < HPKE_TAG_PUBKEY || tag > HPKE_TAG_IV ||
+ tagdata[tag].p || pos + 4 + len > b64_buf->pos) {
+ ret = -EINVAL;
+ break;
+ }
+
+ tagdata[tag].p = b64_buf->data + pos + 4;
+ tagdata[tag].len = len;
+ pos += len + 4;
+ }
+
+ if (!tagdata[HPKE_TAG_PUBKEY].p || !tagdata[HPKE_TAG_CIPHERTEXT].p ||
+ !tagdata[HPKE_TAG_AEAD_TAG].p || tagdata[HPKE_TAG_AEAD_TAG].len != 12 ||
+ !tagdata[HPKE_TAG_IV].p || tagdata[HPKE_TAG_IV].len != 12)
+ ret = -EINVAL;
+
+ if (ret) {
+ vpn_progress(vpninfo, PRG_ERR, _("Failed to decode SSO token at %d:\n"),
+ pos);
+ dump_buf_hex(vpninfo, PRG_ERR, '<', (void *)b64_buf->data, b64_buf->pos);
+ goto out_b64;
+ }
+
+ unsigned char secret[32];
+ ret = ecdh_compute_secp256r1(vpninfo, tagdata[HPKE_TAG_PUBKEY].p,
+ tagdata[HPKE_TAG_PUBKEY].len, secret);
+ if (ret)
+ goto out_b64;
+
+ ret = hkdf_sha256_extract_expand(vpninfo, secret, "AC_ECIES", 8);
+ if (ret)
+ goto out_b64;
+
+ unsigned char *token = tagdata[HPKE_TAG_CIPHERTEXT].p;
+ int token_len = tagdata[HPKE_TAG_CIPHERTEXT].len;
+ ret = aes_256_gcm_decrypt(vpninfo, secret, token, token_len,
+ tagdata[HPKE_TAG_IV].p, tagdata[HPKE_TAG_AEAD_TAG].p);
+ if (ret)
+ goto out_b64;
+
+ int i;
+ for (i = 0; i < token_len; i++) {
+ if (!isalnum(token[i])) {
+ vpn_progress(vpninfo, PRG_ERR,
+ _("SSO token not alphanumeric\n"));
+ ret = -EINVAL;
+ goto out_b64;
+ }
+ }
+
+ vpninfo->sso_cookie_value = strndup((char *)token, token_len);
+ if (!vpninfo->sso_cookie_value)
+ ret = -ENOMEM;
+
+ out_b64:
+ buf_free(b64_buf);
+ out:
+ closesocket(listen_fd);
+ return ret;
+}
+#endif /* HAVE_HPKE_SUPPORT */
free_strap_keys(vpninfo);
free(vpninfo->strap_pubkey);
free(vpninfo->strap_dh_pubkey);
+
+ free(vpninfo->sso_username);
+ free(vpninfo->sso_cookie_value);
+ free(vpninfo->sso_browser_mode);
+ free(vpninfo->sso_login);
+ free(vpninfo->sso_login_final);
+ free(vpninfo->sso_error_cookie);
+ free(vpninfo->sso_token_cookie);
+
free(vpninfo->ppp);
buf_free(vpninfo->ppp_tls_connect_req);
buf_free(vpninfo->ppp_dtls_connect_req);
nuke_opt_values(form->opts);
if (do_sso) {
- if (vpninfo->open_webview) {
- free(vpninfo->sso_cookie_value);
- free(vpninfo->sso_username);
- vpninfo->sso_cookie_value = NULL;
- vpninfo->sso_username = NULL;
+ free(vpninfo->sso_cookie_value);
+ free(vpninfo->sso_username);
+ vpninfo->sso_cookie_value = NULL;
+ vpninfo->sso_username = NULL;
+
+ /* Handle the special Cisco external browser mode */
+ if (vpninfo->sso_browser_mode && !strcmp(vpninfo->sso_browser_mode, "external")) {
+ ret = handle_external_browser(vpninfo);
+ } else if (vpninfo->open_webview) {
ret = vpninfo->open_webview(vpninfo, vpninfo->sso_login, vpninfo->cbdata);
+ } else {
+ vpn_progress(vpninfo, PRG_ERR,
+ _("No SSO handler\n")); /* XX: print more debugging info */
+ ret = -EINVAL;
+ }
+ if (!ret) {
for (opt = form->opts; opt; opt = opt->next) {
if (opt->type == OC_FORM_OPT_SSO_TOKEN) {
free(opt->_value);
opt->_value = vpninfo->sso_cookie_value;
+ vpninfo->sso_cookie_value = NULL;
} else if (opt->type == OC_FORM_OPT_SSO_USER) {
free(opt->_value);
opt->_value = vpninfo->sso_username;
+ vpninfo->sso_username = NULL;
}
}
- vpninfo->sso_username = NULL;
- vpninfo->sso_cookie_value = NULL;
- } else {
- vpn_progress(vpninfo, PRG_ERR,
- _("No SSO handler\n")); /* XX: print more debugging info */
- ret = -EINVAL;
}
+ free(vpninfo->sso_username);
+ vpninfo->sso_username = NULL;
+ free(vpninfo->sso_cookie_value);
+ vpninfo->sso_cookie_value = NULL;
+ free(vpninfo->sso_browser_mode);
+ vpninfo->sso_browser_mode = NULL;
}
return ret;