hpke.c 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. /*
  2. * OpenConnect (SSL + DTLS) VPN client
  3. *
  4. * Copyright © 2022 David Woodhouse <dwmw2@infradead.org>
  5. *
  6. * This program is free software; you can redistribute it and/or
  7. * modify it under the terms of the GNU Lesser General Public License
  8. * version 2.1, as published by the Free Software Foundation.
  9. *
  10. * This program is distributed in the hope that it will be useful, but
  11. * WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
  13. * Lesser General Public License for more details.
  14. */
  15. #include <config.h>
  16. #include "openconnect-internal.h"
  17. #ifndef HAVE_HPKE_SUPPORT
  18. int handle_external_browser(struct openconnect_info *vpninfo)
  19. {
  20. return -EINVAL;
  21. }
  22. #else
  23. #include <ctype.h>
  24. #define HPKE_TAG_PUBKEY 1
  25. #define HPKE_TAG_AEAD_TAG 2
  26. #define HPKE_TAG_CIPHERTEXT 3
  27. #define HPKE_TAG_IV 4
  28. /*
  29. * Hard-coded HTTP responses
  30. */
  31. static const char response_404[] =
  32. "HTTP/1.1 404 Not Found\r\n"
  33. "Connection: close\r\n"
  34. "Content-Type: text/html\r\n"
  35. "Content-Length: 0\r\n\r\n";
  36. static const char response_302[] =
  37. "HTTP/1.1 302 Found\r\n"
  38. "Connection: close\r\n"
  39. "Content-Type: text/html\r\n"
  40. "Content-Length: 0\r\n"
  41. "Location: %s\r\n\r\n";
  42. static const char response_200[] =
  43. "HTTP/1.1 200 OK\r\n"
  44. "Connection: close\r\n"
  45. "Content-Type: text/html\r\n\r\n"
  46. "<html><title>Success</title><body>Success</body></html>\r\n";
  47. /*
  48. * If we use an external browser where we can't just snoop for cookies
  49. * or completion... how do we get the results back? Cisco's answer:
  50. * We run an HTTP server on http://localhost:29786/ and listen for
  51. * a GET request to /api/sso/<base64 blob>?return=<finalurl>. It
  52. * returns a redirect to that final URL, which is a pretty 'success'
  53. * page. And decodes the base64 blob to obtain the SSO token, qv.
  54. */
  55. int handle_external_browser(struct openconnect_info *vpninfo)
  56. {
  57. int ret = 0;
  58. struct sockaddr_in6 sin6 = { };
  59. sin6.sin6_family = AF_INET6;
  60. sin6.sin6_port = htons(29786);
  61. sin6.sin6_addr = in6addr_loopback;
  62. int listen_fd = socket(AF_INET6, SOCK_STREAM, IPPROTO_TCP);
  63. if (listen_fd < 0) {
  64. char *errstr;
  65. sockerr:
  66. #ifdef _WIN32
  67. errstr = openconnect__win32_strerror(WSAGetLastError());
  68. #else
  69. errstr = strerror(errno);
  70. #endif
  71. vpn_progress(vpninfo, PRG_ERR,
  72. _("Failed to listen on local port 29786: %s\n"),
  73. errstr);
  74. #ifdef _WIN32
  75. free(errstr);
  76. #endif
  77. if (listen_fd >= 0)
  78. closesocket(listen_fd);
  79. return -EIO;
  80. }
  81. int optval = 1;
  82. (void)setsockopt(listen_fd, SOL_SOCKET, SO_REUSEADDR, (void *)&optval, sizeof(optval));
  83. if (bind(listen_fd, (void *)&sin6, sizeof(sin6)) < 0)
  84. goto sockerr;
  85. if (listen(listen_fd, 1))
  86. goto sockerr;
  87. if (set_sock_nonblock(listen_fd))
  88. goto sockerr;
  89. /* Now that we are listening on the socket, we can spawn the browser */
  90. if (vpninfo->open_ext_browser) {
  91. ret = vpninfo->open_ext_browser(vpninfo, vpninfo->sso_login, vpninfo->cbdata);
  92. #if defined(HAVE_POSIX_SPAWN) && defined(DEFAULT_EXTERNAL_BROWSER)
  93. } else {
  94. vpn_progress(vpninfo, PRG_TRACE, _("Spawning external browser '%s'\n"),
  95. DEFAULT_EXTERNAL_BROWSER);
  96. pid_t pid = 0;
  97. char *browser_argv[3] = { (char *)DEFAULT_EXTERNAL_BROWSER, vpninfo->sso_login, NULL };
  98. if (posix_spawn(&pid, DEFAULT_EXTERNAL_BROWSER, NULL, NULL, browser_argv, environ)) {
  99. ret = -errno;
  100. vpn_perror(vpninfo, _("Spawn browser"));
  101. }
  102. #else
  103. } else {
  104. ret = -EINVAL;
  105. #endif
  106. }
  107. if (ret)
  108. vpn_progress(vpninfo, PRG_ERR,
  109. _("Failed to spawn external browser for %s\n"),
  110. vpninfo->sso_login);
  111. char *returl = NULL;
  112. struct oc_text_buf *b64_buf = NULL;
  113. /* There may be other stray connections. Repeat until we have one
  114. * that looks like the actual auth attempt from the browser. */
  115. while (1) {
  116. int accept_fd = cancellable_accept(vpninfo, listen_fd);
  117. if (accept_fd < 0) {
  118. ret = accept_fd;
  119. goto out;
  120. }
  121. vpn_progress(vpninfo, PRG_TRACE,
  122. _("Accepted incoming external-browser connection on port 29786\n"));
  123. char line[4096];
  124. ret = cancellable_gets(vpninfo, accept_fd, line, sizeof(line));
  125. if (ret < 15 || strncmp(line, "GET /", 5) ||
  126. strncmp(line + ret - 9, " HTTP/1.", 8)) {
  127. vpn_progress(vpninfo, PRG_TRACE,
  128. _("Invalid incoming external-browser request\n"));
  129. closesocket(accept_fd);
  130. continue;
  131. }
  132. if (strncmp(line, "GET /api/sso/", 13)) {
  133. give_404:
  134. cancellable_send(vpninfo, accept_fd, response_404, sizeof(response_404) - 1);
  135. closesocket(accept_fd);
  136. continue;
  137. }
  138. /*
  139. * OK, now we have a "GET /api/sso/… HTTP/1.x" that looks sane.
  140. * Kill the " HTTP/1.x" at the end.
  141. * */
  142. line[ret - 9] = 0;
  143. /* Scan for ?return= (and other params that shouldn't be there) */
  144. char *b64 = line + 13;
  145. char *q = strchr(b64, '?');
  146. while (q) {
  147. *q = 0;
  148. q++;
  149. if (!strncmp(q, "return=", 7))
  150. returl = q + 7;
  151. q = strchr(q, '&');
  152. }
  153. /* Attempt to decode the base64 */
  154. urldecode_inplace(b64);
  155. b64_buf = buf_alloc();
  156. if (!b64_buf) {
  157. ret = -ENOMEM;
  158. closesocket(accept_fd);
  159. goto out;
  160. }
  161. b64_buf->data = openconnect_base64_decode(&ret, b64);
  162. if (ret < 0) {
  163. /* If the final part of the URL after /api/sso/ is not
  164. * valid base64, give a 404 and wait for a valid req. */
  165. buf_free(b64_buf);
  166. b64_buf = NULL;
  167. goto give_404;
  168. }
  169. b64_buf->pos = b64_buf->buf_len = ret;
  170. /* Decode and store the returl (since we'll reuse the line buf) */
  171. if (returl) {
  172. urldecode_inplace(returl);
  173. returl = strdup(returl);
  174. }
  175. /* Now consume the rest of the HTTP request lines */
  176. while (cancellable_gets(vpninfo, accept_fd, line, sizeof(line)) > 0) {
  177. vpn_progress(vpninfo, PRG_DEBUG,
  178. "< %s\n", line);
  179. }
  180. /* Finally, send the response to redirect to the success page */
  181. if (returl) {
  182. line[sizeof(line) - 1] = 0;
  183. ret = snprintf(line, sizeof(line) - 1, response_302, returl);
  184. ret = cancellable_send(vpninfo, accept_fd, line, ret);
  185. free(returl);
  186. returl = NULL;
  187. } else {
  188. ret = cancellable_send(vpninfo, accept_fd, response_200, sizeof(response_200) - 1);
  189. }
  190. closesocket(accept_fd);
  191. if (ret < 0)
  192. goto out_b64;
  193. break;
  194. }
  195. vpn_progress(vpninfo, PRG_DEBUG, _("Got encrypted SSO token of %d bytes\n"),
  196. b64_buf->pos);
  197. /* Example encrypted token:
  198. < 0000: 00 01 00 01 00 5b 30 59 30 13 06 07 2a 86 48 ce |.....[0Y0...*.H.|
  199. < 0010: 3d 02 01 06 08 2a 86 48 ce 3d 03 01 07 03 42 00 |=....*.H.=....B.|
  200. < 0020: 04 fa 42 63 40 b6 f4 a6 02 9a dd 57 f5 8c 74 3e |..Bc@......W..t>|
  201. < 0030: 11 82 18 8d 78 c4 b5 13 d0 c7 c0 d7 f9 79 6c 16 |....x........yl.|
  202. < 0040: e9 bc 30 fa f0 ea 09 8d 17 d1 84 e4 08 55 31 28 |..0..........U1(|
  203. < 0050: a6 62 e4 6d c5 7c be 19 d9 14 41 37 20 6e 4c ce |.b.m.|....A7 nL.|
  204. < 0060: 2c 00 02 00 0c ac 81 ce 79 56 6e 4c 00 cc 9b e3 |,.......yVnL....|
  205. < 0070: d0 00 03 00 17 bb 4d 2e 57 61 c0 90 58 86 86 79 |......M.Wa..X..y|
  206. < 0080: 64 05 28 0c c9 f2 c8 c2 2a 2e fb 5c 00 04 00 0c |d.(.....*..\....|
  207. < 0090: 2c 03 5f 13 3c b7 27 7e 36 fe 5a b8 |,._.<.'~6.Z.|
  208. This contains the server's DH pubkey (type 1) at 0x0008,
  209. the AEAD tag (type 2) at 0x0065, the ciphertext (type 3) at 0x0075
  210. and the IV (type 4) at 0x0090.
  211. */
  212. /* tagdata[0] is unused because I can't be doing with all that
  213. * (HPKE_TAG_IV-1) nonsense. */
  214. struct {
  215. void *p;
  216. int len;
  217. } tagdata[HPKE_TAG_IV + 1];
  218. memset(tagdata, 0, sizeof(tagdata));
  219. int pos = 0;
  220. ret = 0;
  221. while (pos < b64_buf->buf_len) {
  222. uint16_t tag, len;
  223. if (pos + 4 > b64_buf->pos) {
  224. ret = -EINVAL;
  225. break;
  226. }
  227. tag = load_be16(b64_buf->data + pos);
  228. len = load_be16(b64_buf->data + pos + 2);
  229. /* Special case, first word must be 0x0001 before the TLVs start */
  230. if (!pos) {
  231. if (tag != 0x0001) {
  232. ret = -EINVAL;
  233. break;
  234. }
  235. pos += 2;
  236. continue;
  237. }
  238. if (tag < HPKE_TAG_PUBKEY || tag > HPKE_TAG_IV ||
  239. tagdata[tag].p || pos + 4 + len > b64_buf->pos) {
  240. ret = -EINVAL;
  241. break;
  242. }
  243. tagdata[tag].p = b64_buf->data + pos + 4;
  244. tagdata[tag].len = len;
  245. pos += len + 4;
  246. }
  247. if (!tagdata[HPKE_TAG_PUBKEY].p || !tagdata[HPKE_TAG_CIPHERTEXT].p ||
  248. !tagdata[HPKE_TAG_AEAD_TAG].p || tagdata[HPKE_TAG_AEAD_TAG].len != 12 ||
  249. !tagdata[HPKE_TAG_IV].p || tagdata[HPKE_TAG_IV].len != 12)
  250. ret = -EINVAL;
  251. if (ret) {
  252. vpn_progress(vpninfo, PRG_ERR, _("Failed to decode SSO token at %d:\n"),
  253. pos);
  254. dump_buf_hex(vpninfo, PRG_ERR, '<', (void *)b64_buf->data, b64_buf->pos);
  255. goto out_b64;
  256. }
  257. unsigned char secret[32];
  258. ret = ecdh_compute_secp256r1(vpninfo, tagdata[HPKE_TAG_PUBKEY].p,
  259. tagdata[HPKE_TAG_PUBKEY].len, secret);
  260. if (ret)
  261. goto out_b64;
  262. const unsigned char info[] = "AC_ECIES";
  263. ret = hkdf_sha256_extract_expand(vpninfo, secret, info, 8);
  264. if (ret)
  265. goto out_b64;
  266. unsigned char *token = tagdata[HPKE_TAG_CIPHERTEXT].p;
  267. int token_len = tagdata[HPKE_TAG_CIPHERTEXT].len;
  268. ret = aes_256_gcm_decrypt(vpninfo, secret, token, token_len,
  269. tagdata[HPKE_TAG_IV].p, tagdata[HPKE_TAG_AEAD_TAG].p);
  270. if (ret)
  271. goto out_b64;
  272. int i;
  273. for (i = 0; i < token_len; i++) {
  274. if (!isalnum(token[i])) {
  275. vpn_progress(vpninfo, PRG_ERR,
  276. _("SSO token not alphanumeric\n"));
  277. ret = -EINVAL;
  278. goto out_b64;
  279. }
  280. }
  281. vpninfo->sso_cookie_value = strndup((char *)token, token_len);
  282. if (!vpninfo->sso_cookie_value)
  283. ret = -ENOMEM;
  284. out_b64:
  285. buf_free(b64_buf);
  286. out:
  287. closesocket(listen_fd);
  288. return ret;
  289. }
  290. #endif /* HAVE_HPKE_SUPPORT */