]> git.ipfire.org Git - thirdparty/strongswan.git/commitdiff
pki: use libtls for pki --est
authorAndreas Steffen <andreas.steffen@strongswan.org>
Fri, 19 Aug 2022 00:04:58 +0000 (02:04 +0200)
committerAndreas Steffen <andreas.steffen@strongswan.org>
Thu, 25 Aug 2022 05:02:29 +0000 (07:02 +0200)
configure.ac
src/pki/Makefile.am
src/pki/commands/est.c
src/pki/est/est_tls.c [new file with mode: 0644]
src/pki/est/est_tls.h [new file with mode: 0644]

index 7cbc96b6e57a3c20c3d0fadb67195745665d0be1..4c44bd0d0841c35bfdf3b7fa77ce87215eef2221 100644 (file)
@@ -452,7 +452,7 @@ if test x$tnc_imc = xtrue -o x$tnc_imv = xtrue -o x$tnccs_11 = xtrue -o x$tnccs_
        tnc_tnccs=true;
 fi
 
-if test x$eap_tls = xtrue -o x$eap_ttls = xtrue -o x$eap_peap = xtrue -o x$tnc_tnccs = xtrue; then
+if test x$eap_tls = xtrue -o x$eap_ttls = xtrue -o x$eap_peap = xtrue -o x$tnc_tnccs = xtrue -o x$pki = xtrue; then
        tls=true;
 fi
 
index 382cd42c889f4e4513feba7124ce8baaacfbb339..22fb11fe8d89a61205142eb99f56c3c3dc9595b0 100644 (file)
@@ -20,15 +20,17 @@ pki_SOURCES = pki.c pki.h pki_cert.c pki_cert.h command.c command.h \
        commands/self.c \
        commands/signcrl.c \
        commands/verify.c \
-       est/est.h est/est.c \
+       est/est.h est/est.c est/est_tls.h est/est_tls.c \
        scep/scep.h scep/scep.c
 
 pki_LDADD = \
        $(top_builddir)/src/libstrongswan/libstrongswan.la \
+       $(top_builddir)/src/libtls/libtls.la \
        $(PTHREADLIB) $(ATOMICLIB) $(DLLIB)
 
 pki.o :        $(top_builddir)/config.status
 
 AM_CPPFLAGS = \
        -I$(top_srcdir)/src/libstrongswan \
+       -I$(top_srcdir)/src/libtls \
        -DPLUGINS=\""${pki_plugins}\""
index 55ba4dc09085358114308183dffc82f5e9deecbe..ceee5b8106eaebae2023f88d10ccee7d474e7b93 100644 (file)
 #include "pki.h"
 #include "pki_cert.h"
 #include "est/est.h"
+#include "est/est_tls.h"
 
 #include <credentials/certificates/certificate.h>
 #include <credentials/sets/mem_cred.h>
 
-#define HTTP_CODE_OK         200
-#define HTTP_CODE_ACCEPTED   202
-
 /* default polling time interval in EST manual mode */
 #define DEFAULT_POLL_INTERVAL    60      /* seconds */
 
@@ -43,9 +41,10 @@ static int est()
        mem_cred_t *creds = NULL;
        private_key_t *client_key = NULL;
        est_op_t est_op = EST_SIMPLE_ENROLL;
+       est_tls_t *est_tls;
        u_int poll_interval = DEFAULT_POLL_INTERVAL;
        u_int max_poll_time = 0, poll_start = 0;
-       u_int http_code = 0;
+       u_int http_code = 0, retry_after = 0;
        int status = 1;
 
        /* initialize CA certificate storage */
@@ -165,6 +164,7 @@ static int est()
                        DBG1(DBG_APP, "could not load client cert file '%s'", client_cert_file);
                        goto end;
                }
+               creds->add_cert(creds, FALSE, client_cert->get_ref(client_cert));
 
                /* load old client private key */
                client_key = lib->creds->create(lib->creds, CRED_PRIVATE_KEY, KEY_ANY,
@@ -174,19 +174,30 @@ static int est()
                        DBG1(DBG_APP, "parsing client private key failed");
                        goto end;
                }
+               creds->add_key(creds, client_key->get_ref(client_key));
                est_op = EST_SIMPLE_REENROLL;
        }
 
-       if (!est_https_request(url, est_op, TRUE, pkcs10_encoding, &est_response,
-                                                  &http_code))
+       est_tls = est_tls_create(url, client_cert, NULL);
+       if (!est_tls)
        {
-               DBG1(DBG_APP, "did not receive a valid EST response: HTTP %u", http_code);
+               DBG1(DBG_APP, "TLS connection to EST server was not established");
+               goto end;
+       }
+       if (!est_tls->request(est_tls, est_op, pkcs10_encoding, &est_response,
+                                                 &http_code, &retry_after))
+       {
+               DBG1(DBG_APP, "EST request failed: HTTP %u", http_code);
                goto end;
        }
 
        /* in case of manual mode, we are going into a polling loop */
-       if (http_code == HTTP_CODE_ACCEPTED)
+       if (http_code == EST_HTTP_CODE_ACCEPTED)
        {
+               if (retry_after > 0 && poll_interval < retry_after)
+               {
+                       poll_interval = retry_after;
+               }
                if (max_poll_time > 0)
                {
                        DBG1(DBG_APP, "  EST request pending, polling every %d seconds"
@@ -200,7 +211,7 @@ static int est()
                poll_start = time_monotonic(NULL);
        }
 
-       while (http_code == HTTP_CODE_ACCEPTED)
+       while (http_code == EST_HTTP_CODE_ACCEPTED)
        {
                if (max_poll_time > 0 &&
                   (time_monotonic(NULL) - poll_start) >= max_poll_time)
@@ -211,16 +222,23 @@ static int est()
                DBG1(DBG_APP, "  going to sleep for %d seconds", poll_interval);
                sleep(poll_interval);
                chunk_free(&est_response);
-               if (!est_https_request(url, est_op, TRUE, pkcs10_encoding, &est_response,
-                                                          &http_code))
+
+               est_tls->destroy(est_tls);
+               est_tls = est_tls_create(url, client_cert, NULL);
+               if (!est_tls)
+               {
+                       DBG1(DBG_APP, "TLS connection to EST server was not established");
+                       goto end;
+               }
+               if (!est_tls->request(est_tls, est_op, pkcs10_encoding, &est_response,
+                                                         &http_code, &retry_after))
                {
-                       DBG1(DBG_APP, "did not receive a valid EST response: HTTP %u",
-                                                  http_code);
+                       DBG1(DBG_APP, "EST request failed: HTTP %u", http_code);
                        goto end;
                }
        }
 
-       if (http_code == HTTP_CODE_OK)
+       if (http_code == EST_HTTP_CODE_OK)
        {
                status = pki_cert_extract_cert(est_response, form, creds) ? 0 : 1;
        }
@@ -228,6 +246,7 @@ static int est()
 end:
        lib->credmgr->remove_set(lib->credmgr, &creds->set);
        creds->destroy(creds);
+       DESTROY_IF(est_tls);
        DESTROY_IF(client_cert);
        DESTROY_IF(client_key);
        DESTROY_IF(pkcs10);
diff --git a/src/pki/est/est_tls.c b/src/pki/est/est_tls.c
new file mode 100644 (file)
index 0000000..2546bb0
--- /dev/null
@@ -0,0 +1,426 @@
+/*
+ * Copyright (C) 2013-2022 Andreas Steffen
+ *
+ * Copyright (C) secunet Security Networks AG
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * 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 General Public License
+ * for more details.
+ */
+
+#define _GNU_SOURCE /* for asprintf() */
+#include <stdio.h>
+#include <sys/types.h>
+#include <sys/socket.h>
+#include <unistd.h>
+#include <errno.h>
+
+#include "est_tls.h"
+
+#include <utils/debug.h>
+#include <utils/lexparser.h>
+#include <tls_socket.h>
+
+static const char *operations[] = {
+       "cacerts",
+       "simpleenroll",
+       "simplereenroll",
+       "fullcmc",
+       "serverkeygen",
+       "csrattrs"
+};
+
+static const char *request_types[] = {
+       "",
+       "application/pkcs10",
+       "application/pkcs10",
+       "application/pkcs7-mime",
+       "application/pkcs10",
+       ""
+};
+
+typedef struct private_est_tls_t private_est_tls_t;
+
+/**
+ * Private data of an est_tls_t object.
+ */
+struct private_est_tls_t {
+
+       /**
+        * Public est_tls_t interface.
+        */
+       est_tls_t public;
+
+       /**
+        * EST Server (IP address and port)
+        */
+       host_t *host;
+
+       /**
+        * File descriptor for secure TCP socket
+        */
+       int fd;
+
+       /**
+        * TLS socket
+        */
+       tls_socket_t *tls;
+
+       /**
+        * Host string of the form <hostname:port> used for http requests
+        */
+       char *http_host;
+
+       /**
+        * Path string used for http requests
+        */
+       char *http_path;
+
+       /**
+        * Optional <username:password> for http basic authentication
+        */
+       char *user_pass;
+};
+
+static chunk_t build_http_request(private_est_tls_t *this, est_op_t op, chunk_t in)
+{
+       char  *http_header, http_auth[256];
+       chunk_t request = chunk_empty, data;
+       int len;
+
+       /* Use Basic Authentication? */
+       if (this->user_pass)
+       {
+               snprintf(http_auth, sizeof(http_auth), "Authorization: Basic %s\r\n",
+                                this->user_pass);
+       }
+       else
+       {
+               *http_auth = '\0';
+       }
+
+       if (strlen(request_types[op]) > 0)  /* create HTTP POST request */
+       {
+               data = chunk_to_base64(in, NULL);
+
+               len = asprintf(&http_header,
+                               "POST %s/.well-known/est/%s HTTP/1.1\r\n"
+                               "Host: %s\r\n"
+                               "%s"
+                               "Content-Type: %s\r\n"
+                               "Content-Transfer-Encoding: base64\r\n"
+                               "Content-Length: %d\r\n"
+                               "\r\n",
+                               this->http_path, operations[op], this->http_host, http_auth,
+                               request_types[op], (int)data.len);
+               if (len > 0)
+               {
+                       request = chunk_cat("mm", chunk_create(http_header, len), data);
+               }
+               else
+               {
+                       chunk_free(&data);
+               }
+       }
+       else                                /* create HTTP GET request */
+       {
+               len = asprintf(&http_header,
+                               "GET %s/.well-known/est/%s HTTP/1.1\r\n"
+                               "Host: %s\r\n"
+                               "%s",
+                               this->http_path, operations[op], this->http_host, http_auth);
+               if (len > 0)
+               {
+                       request = chunk_create(http_header, len);
+               }
+       }
+       return request;
+}
+
+static bool parse_http_header(chunk_t *in,  u_int *http_code, u_int *content_len,
+                                                         bool *base64, u_int *retry_after)
+{
+       chunk_t line, version, parameter;
+       u_int len;
+
+       /*initialize output parameters */
+       *http_code = 0;
+       *content_len = 0;
+       *base64 = FALSE;
+
+       /* Process HTTP protocol version and HTTP status code */
+       if (!fetchline(in, &line) || !extract_token(&version, ' ', &line) ||
+               !match("HTTP/1.1", &version) || sscanf(line.ptr, "%d", http_code) != 1)
+       {
+               DBG1(DBG_APP, "malformed http response header");
+               return FALSE;
+       }
+
+       /* Process HTTP header line by line until the HTTP body is reached */
+       while (fetchline(in, &line))
+       {
+               if (line.len == 0)
+               {
+                       break;
+               }
+               if (extract_token(&parameter, ':', &line) && eat_whitespace(&line))
+               {
+                       if (matchcase("Content-Length", &parameter))
+                       {
+                               if (sscanf(line.ptr, "%u", &len) == 1)
+                       {
+                                       *content_len = len;
+                               }
+                       }
+                       else if (matchcase("Content-Transfer-Encoding", &parameter) &&
+                                        matchcase("Base64", &line))
+                       {
+                               *base64 = TRUE;
+                       }
+                       else if (matchcase("Retry-After", &parameter))
+                       {
+                               if (sscanf(line.ptr, "%u", &len) == 1 && retry_after)
+                               {
+                                       *retry_after = len;
+                               }
+                       }
+               }
+       }
+
+       return (*http_code < 300);
+}
+
+
+METHOD(est_tls_t, request, bool,
+       private_est_tls_t *this, est_op_t op, chunk_t in, chunk_t *out,
+       u_int *http_code, u_int *retry_after)
+{
+       chunk_t http = chunk_empty, data = chunk_empty, response;
+       u_int content_len;
+       char buf[1024];
+       bool base64;
+       int len;
+
+       /* initialize output variables */
+       *out = chunk_empty;
+       *http_code = 0;
+       *retry_after = 0;
+
+       http = build_http_request(this, op, in);
+
+       if (http.len == 0)
+       {
+               return FALSE;
+       }
+       DBG1(DBG_APP, "http: %B", &http);
+
+       /* send https request */
+       if (this->tls->write(this->tls, http.ptr, http.len) != http.len)
+       {
+               DBG1(DBG_APP, "TLS socket write failed");
+               chunk_free(&http);
+               return FALSE;
+       }
+       chunk_free(&http);
+
+       /* receive first part of https response */
+       len = this->tls->read(this->tls, buf, sizeof(buf), TRUE);
+       if (len <= 0)
+       {
+               DBG1(DBG_APP, "TLS socket first read failed");
+               return FALSE;
+       }
+       response = chunk_create(buf, len);
+       DBG1(DBG_APP, "response: %B", &response);
+
+       if (!parse_http_header(&response, http_code, &content_len, &base64,
+                                                  retry_after))
+       {
+               return FALSE;
+       }
+       if (*http_code == EST_HTTP_CODE_OK)
+       {
+               if (content_len == 0)
+               {
+                       DBG1(DBG_APP, "no content-length defined in http header");
+                       return FALSE;
+               }
+               if (response.len > content_len)
+               {
+                       DBG1(DBG_APP, "http body is larger than content-length");
+                       return FALSE;
+               }
+
+               data = chunk_alloc(content_len);
+               memcpy(data.ptr, response.ptr, response.len);
+
+               if (data.len > response.len)
+               {
+                       /* read remaining part of https response */
+                       len = this->tls->read(this->tls, data.ptr + response.len,
+                                                                 data.len - response.len, TRUE);
+                       if (len < data.len - response.len)
+                       {
+                               DBG1(DBG_APP, "TLS socket second read failed");
+                               chunk_free(&data);
+                               return FALSE;
+                       }
+               }
+
+               if (base64)
+               {
+                       *out = chunk_from_base64(data, NULL);
+                       chunk_free(&data);
+               }
+               else
+               {
+                       *out = data;
+               }
+       }
+       return TRUE;
+}
+
+METHOD(est_tls_t, destroy, void,
+       private_est_tls_t *this)
+{
+       DESTROY_IF(this->tls);
+       DESTROY_IF(this->host);
+       if (this->fd != -1)
+       {
+               close(this->fd);
+       }
+       free(this->http_host);
+       free(this->http_path);
+       free(this->user_pass);
+       free(this);
+}
+
+static bool est_tls_init(private_est_tls_t *this, char *uri,
+                                                certificate_t *client_cert)
+{
+       identification_t *client_id = NULL, *server_id = NULL;
+       char *host_str, *port_str, *path_str;
+       int port = 443;
+       bool success = FALSE;
+
+       /* check for "https://" prefix and remove it */
+       if (strlen(uri) < 8 || !strncaseeq(uri, "https://", 8))
+       {
+               DBG1(DBG_APP, "'%s' is not an https URI", uri);
+               return FALSE;
+       }
+       uri += 8;
+
+       /* any trailing path or command? */
+       path_str = strchr(uri, '/');
+
+       this->http_path =
+               strdup( (path_str == NULL || path_str[1] == '\0') ? "" : path_str );
+
+       if (path_str)
+       {
+               /* NUL-terminate host_str */
+               *path_str = '\0';
+       }
+
+       /* duplicate <hostname:port> string since we are going to manipulate it */
+       host_str = strdup(uri);
+
+       /* another duplicate for http requests */
+       this->http_host = strdup(host_str);
+
+       /* extract hostname and port from URI */
+       port_str = strchr(host_str, ':');
+
+       if (port_str)
+       {
+               /* NUL-terminate hostname */
+               *port_str++ = '\0';
+
+               /* extract port */
+               if (sscanf(port_str, "%d", &port) != 1)
+               {
+                       DBG1(DBG_APP, "parsing server port %s failed", port_str);
+                       goto end;
+               }
+       }
+
+       /* open TCP socket and connect to EST server */
+       this->host = host_create_from_dns(host_str, 0, port);
+       if (!this->host)
+       {
+               DBG1(DBG_APP, "resolving hostname %s failed", host_str);
+               goto end;
+       }
+
+       this->fd = socket(this->host->get_family(this->host), SOCK_STREAM, 0);
+       if (this->fd == -1)
+       {
+               DBG1(DBG_APP, "opening socket failed: %s", strerror(errno));
+               goto end;
+       }
+
+       if (connect(this->fd, this->host->get_sockaddr(this->host),
+                                                *this->host->get_sockaddr_len(this->host)) == -1)
+       {
+               DBG1(DBG_APP, "connecting to %#H failed: %s",
+                                          this->host, strerror(errno));
+               goto end;
+       }
+
+       if (client_cert)
+       {
+               client_id = client_cert->get_subject(client_cert);
+       }
+       server_id = identification_create_from_string(host_str);
+
+       /* open TLS socket */
+       this->tls = tls_socket_create(FALSE, server_id, client_id, this->fd,
+                                                                 NULL, TLS_UNSPEC, TLS_UNSPEC, 0);
+       server_id->destroy(server_id);
+       if (!this->tls)
+       {
+               DBG1(DBG_APP, "creating TLS socket failed");
+               goto end;
+       }
+       success = TRUE;
+
+end:
+       free(host_str);
+
+       return success;
+}
+
+/**
+ * See header
+ */
+est_tls_t *est_tls_create(char *uri, certificate_t *client_cert, char *user_pass)
+{
+       private_est_tls_t *this;
+
+       INIT(this,
+               .public = {
+                       .request = _request,
+                       .destroy = _destroy,
+               },
+       );
+
+       if (user_pass)
+       {
+               this->user_pass = strdup(user_pass);
+       }
+
+       if (!est_tls_init(this, uri, client_cert))
+       {
+               destroy(this);
+               return NULL;
+       }
+
+       return &this->public;
+}
diff --git a/src/pki/est/est_tls.h b/src/pki/est/est_tls.h
new file mode 100644 (file)
index 0000000..d05831c
--- /dev/null
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2022 Andreas Steffen
+ *
+ * Copyright (C) secunet Security Networks AG
+ *
+ * This program is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 2 of the License, or (at your
+ * option) any later version.  See <http://www.fsf.org/copyleft/gpl.txt>.
+ *
+ * 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 General Public License
+ * for more details.
+ */
+
+/**
+ * @defgroup est_tls est_tls
+ * @{ @ingroup pki
+ */
+
+#ifndef EST_TLS_H_
+#define EST_TLS_H_
+
+#include "est.h"
+
+#include <library.h>
+#include <credentials/certificates/certificate.h>
+
+#define EST_HTTP_CODE_OK         200
+#define EST_HTTP_CODE_ACCEPTED   202
+
+typedef struct est_tls_t est_tls_t;
+
+/**
+ * TLS Interface for sending and receiving HTTPS messages
+ */
+struct est_tls_t {
+
+       /**
+        * Send a https request and get a response back
+        *
+        * @param option                EST operation
+        * @param in                    HTTP POST input data
+        * @param out                   HTTP response
+        * @param http_code             HTTP status code
+     * @param retry_after   Retry time in seconds
+        * @result                              TRUE if successful
+        */
+       bool (*request)(est_tls_t *this, est_op_t op, chunk_t in, chunk_t *out,
+                                       u_int *http_code, u_int *retry_after);
+
+       /**
+        * Destroy an est_tls_t object.
+        */
+       void (*destroy)(est_tls_t *this);
+};
+
+/**
+ * Create a est_tls instance.
+ *
+ * @param uri                  URI (https://...)
+ * @param client_cert   Optional client certificate
+ * @param user_pass            Optional username:password for HTTP Basic Authentication
+ */
+est_tls_t *est_tls_create(char *uri, certificate_t *client_cert,
+                                                 char *user_pass);
+
+#endif /** EST_TLS_H_ @}*/