From 47abed1ff4b9c3060e7bdba51800bae1d66583a2 Mon Sep 17 00:00:00 2001 From: Ben Kibbey Date: Sun, 21 Sep 2008 13:05:20 -0400 Subject: [PATCH] Unofficial pwmd_tcp_connect() to a remote server. GnuTLS is used for encryption and client certificate authentication. --- Makefile.am | 6 +- configure.ac | 3 + libpwmd.c | 369 ++++++++++++++++++++++++++++++++++++++++++++++++++++++----- libpwmd.h | 16 +++ pwmc.c | 42 ++++++- types.h | 7 ++ 6 files changed, 411 insertions(+), 32 deletions(-) diff --git a/Makefile.am b/Makefile.am index 4f2254c9..d361f418 100644 --- a/Makefile.am +++ b/Makefile.am @@ -4,8 +4,10 @@ lib_LTLIBRARIES = libpwmd.la libpwmd_ladir = $(includedir) libpwmd_la_SOURCES = libpwmd.c libpwmd.h types.h gettext.h libpwmd_la_HEADERS = libpwmd.h -libpwmd_la_LDFLAGS = -version-info 5:7:0 @LIBASSUAN_LIBS@ @GPG_ERROR_LIBS@ -libpwmd_la_CFLAGS = -DLOCALEDIR=\"${prefix}/share/locale\" @LIBASSUAN_CFLAGS@ @GPG_ERROR_CFLAGS@ +libpwmd_la_LDFLAGS = -version-info 5:7:0 @LIBASSUAN_LIBS@ @GPG_ERROR_LIBS@ \ + @LIBGNUTLS_LIBS@ +libpwmd_la_CFLAGS = -DLOCALEDIR=\"${prefix}/share/locale\" @LIBASSUAN_CFLAGS@ \ + @GPG_ERROR_CFLAGS@ @LIBGNUTLS_CFLAGS@ dist_man1_MANS = pwmc.1 bin_PROGRAMS = pwmc diff --git a/configure.ac b/configure.ac index 3d92e261..33c64c55 100644 --- a/configure.ac +++ b/configure.ac @@ -41,6 +41,9 @@ AC_TYPE_PID_T AM_PATH_LIBASSUAN(1.0.4,, AC_MSG_ERROR([libassuan not found])) AM_PATH_GPG_ERROR(,, AC_MSG_ERROR([libgpg-error not found])) +AM_PATH_LIBGNUTLS(, AC_DEFINE(WITH_GNUTLS, 1, \ + [Define if you want GNUTLS support.]), + AC_MSG_ERROR([libgnutls not found])) dnl Checks for library functions. AC_FUNC_MALLOC diff --git a/libpwmd.c b/libpwmd.c index f1fd62ca..fc15efdb 100644 --- a/libpwmd.c +++ b/libpwmd.c @@ -35,12 +35,20 @@ #include #include #include +#include +#include +#include #include #ifdef HAVE_CONFIG_H #include #endif +#ifdef WITH_GNUTLS +#include +#include +#endif + #ifdef HAVE_ASSUAN_H #include #endif @@ -98,6 +106,10 @@ const char *pwmd_strerror(gpg_error_t e) gpg_error_t pwmd_init() { +#ifdef WITH_GNUTLS + gnutls_global_set_mem_functions(xmalloc, xmalloc, NULL, xrealloc, xfree); + gnutls_global_init (); +#endif #ifdef ENABLE_NLS bindtextdomain("libpwmd", LOCALEDIR); #endif @@ -107,15 +119,336 @@ gpg_error_t pwmd_init() return 0; } -pwm_t *pwmd_connect(const char *path, gpg_error_t *error) +static pwm_t *_socket_connect_finalize(pwm_t *pwm, assuan_context_t ctx) +{ + int active[2]; + int n = assuan_get_active_fds(ctx, 0, active, sizeof(active)); + + pwm->fd = n <= 0 ? -1 : dup(active[0]); + pwm->ctx = ctx; +#ifdef USE_PINENTRY + pwm->pid = -1; + pwm->pinentry_tries = 3; +#endif + assuan_set_pointer(ctx, pwm); + return pwm; +} + +#ifdef WITH_GNUTLS +static int read_hook(assuan_context_t ctx, assuan_fd_t fd, void *data, + size_t len, ssize_t *ret) +{ + pwm_t *pwm = assuan_get_pointer(ctx); + + if (!pwm || !pwm->session) + *ret = read((int)fd, data, len); + else { + do { + *ret = gnutls_record_recv(pwm->session, data, len); + + if (*ret == GNUTLS_E_REHANDSHAKE) { + *ret = gnutls_rehandshake(pwm->session); + + if (*ret == GNUTLS_E_WARNING_ALERT_RECEIVED || + *ret == GNUTLS_A_NO_RENEGOTIATION) { + fprintf(stderr, "%s", gnutls_strerror(*ret)); + continue; + } + + if (*ret != GNUTLS_E_SUCCESS) { + fprintf(stderr, "%s", gnutls_strerror(*ret)); + *ret = 0; + break; + } + + *ret = gnutls_handshake(pwm->session); + + if (*ret != GNUTLS_E_SUCCESS) { + fprintf(stderr, "%s", gnutls_strerror(*ret)); + *ret = 0; + break; + } + + continue; + } + } while (*ret == GNUTLS_E_INTERRUPTED || *ret == GNUTLS_E_AGAIN); + } + + return *ret <= 0 ? 0 : 1; +} + +static int write_hook(assuan_context_t ctx, assuan_fd_t fd, const void *data, + size_t len, ssize_t *ret) +{ + pwm_t *pwm = assuan_get_pointer(ctx); + + if (!pwm || !pwm->session) + *ret = write((int)fd, data, len); + else { + do { + *ret = gnutls_record_send(pwm->session, data, len); + } while (*ret == GNUTLS_E_INTERRUPTED || *ret == GNUTLS_E_AGAIN); + } + + return *ret <= 0 ? 0 : 1; +} + +static void _tls_deinit(pwm_t *pwm) +{ + //FIXME hangs when missing cert + gnutls_bye(pwm->session, GNUTLS_SHUT_RDWR); + gnutls_deinit(pwm->session); + gnutls_certificate_free_credentials(pwm->x509); +} + +static gpg_error_t verify_certificate(pwm_t *pwm) +{ + unsigned int status; + const gnutls_datum_t *cert_list; + unsigned int cert_list_size; + int i; + gnutls_x509_crt_t cert; + gpg_error_t rc; + + rc = gnutls_certificate_verify_peers2(pwm->session, &status); + + if (rc < 0) + return GPG_ERR_UNKNOWN_ERRNO; + + if (status & GNUTLS_CERT_INVALID) + return GPG_ERR_MISSING_CERT; + + if (status & GNUTLS_CERT_SIGNER_NOT_FOUND) + return GPG_ERR_BAD_SIGNATURE; + + if (status & GNUTLS_CERT_REVOKED) + return GPG_ERR_CERT_REVOKED; + + if (gnutls_certificate_type_get(pwm->session) != GNUTLS_CRT_X509) + return GPG_ERR_UNSUPPORTED_CERT; + + if (gnutls_x509_crt_init(&cert) < 0) + return gpg_error_from_errno(ENOMEM); + + cert_list = gnutls_certificate_get_peers (pwm->session, &cert_list_size); + + if (!cert_list) { + rc = GPG_ERR_MISSING_CERT; + goto done; + } + + for (i = 0; i < cert_list_size; i++) { + if (gnutls_x509_crt_import(cert, &cert_list[i], GNUTLS_X509_FMT_DER) < 0) { + rc = GPG_ERR_BAD_CERT_CHAIN; + goto done; + } + } + + /* Beware here we do not check for errors. + */ + if (gnutls_x509_crt_get_expiration_time(cert) < time (0)) { + rc = GPG_ERR_CERT_EXPIRED; + goto done; + } + + if (gnutls_x509_crt_get_activation_time(cert) > time (0)) { + rc = GPG_ERR_CERT_TOO_YOUNG; + goto done; + } + +#if 0 + if (!gnutls_x509_crt_check_hostname (cert, hostname)) + { + printf ("The certificate's owner does not match hostname '%s'\n", + hostname); + return 1; + } +#endif + +done: + gnutls_x509_crt_deinit(cert); + return rc; +} + +static gpg_error_t _tls_init(pwm_t *pwm, int fd, const char *cert, + const char *key, const char *ca) +{ + gpg_error_t rc; + const char *errstr; + + rc = gnutls_certificate_allocate_credentials(&pwm->x509); + + if (rc) + return gpg_error_from_errno(ENOMEM); + + /* The client certificate must be signed by the CA of the pwmd server + * certificate in order for the client to authenticate successfully. Man in + * the middle attacks are still possible if the attacker is running a pwmd + * that doesn't require client certificate authentication. So require the + * client to verify the server certificate. + */ + gnutls_certificate_set_x509_trust_file (pwm->x509, ca, GNUTLS_X509_FMT_PEM); + + rc = gnutls_certificate_set_x509_key_file(pwm->x509, cert, key, + GNUTLS_X509_FMT_PEM); + rc = gnutls_init(&pwm->session, GNUTLS_CLIENT); + rc = gnutls_priority_set_direct(pwm->session, "SECURE256", &errstr); + rc = gnutls_credentials_set(pwm->session, GNUTLS_CRD_CERTIFICATE, pwm->x509); + gnutls_transport_set_ptr(pwm->session, (gnutls_transport_ptr_t) fd); + rc = gnutls_handshake(pwm->session); + + if (rc < 0) { + gnutls_perror (rc); + rc = GPG_ERR_INV_CIPHER_MODE; + _tls_deinit(pwm); + return rc; + } + + rc = verify_certificate(pwm); + if (rc) + _tls_deinit(pwm); + return rc; +} + +static void _tls_assuan_deinit(assuan_context_t ctx) +{ + pwm_t *pwm = assuan_get_pointer(ctx); + + _tls_deinit(pwm); +} + +pwm_t *pwmd_tcp_connect(const char *host, gpg_error_t *rc, const char *cert, + const char *key, const char *ca) +{ + char *p; + int port = 6466; + int fd; + struct hostent *he; + struct sockaddr_in their_addr; + assuan_context_t ctx; + char *tcert, *tkey, *tca; + char buf[PATH_MAX]; + struct passwd *pw = getpwuid(getuid()); + pwm_t *pwm; + struct assuan_io_hooks io_hooks = {read_hook, write_hook}; + + if (!host) { + *rc = GPG_ERR_INV_ARG; + return NULL; + } + + p = strchr(host, ':'); + + if (p) { + char *pp; + + port = strtol(++p, &pp, 10); + + if (pp) { + *rc = GPG_ERR_INV_ARG; + return NULL; + } + } + + if ((he = gethostbyname(host)) == NULL) { + *rc = gpg_error_from_syserror(); + return NULL; + } + + fd = socket(PF_INET, SOCK_STREAM, 0); + + if (fd == -1) { + *rc = gpg_error_from_syserror(); + return NULL; + } + + their_addr.sin_family = PF_INET; + their_addr.sin_port = htons(port); + their_addr.sin_addr = *((struct in_addr *)he->h_addr); + memset(their_addr.sin_zero, '\0', sizeof their_addr.sin_zero); + + if (connect(fd, (struct sockaddr *)&their_addr, sizeof(their_addr)) == -1) { + *rc = gpg_error_from_syserror(); + return NULL; + } + + if (!cert) { + snprintf(buf, sizeof(buf), "%s/.pwmd/client-cert.pem", pw->pw_dir); + tcert = xstrdup(buf); + } + else + tcert = xstrdup(cert); + + if (!tcert) { + *rc = gpg_error_from_syserror(); + return NULL; + } + + if (!key) { + snprintf(buf, sizeof(buf), "%s/.pwmd/client-key.pem", pw->pw_dir); + tkey = xstrdup(buf); + } + else + tkey = xstrdup(key); + + if (!tkey) { + *rc = gpg_error_from_syserror(); + xfree(tcert); + return NULL; + } + + if (!ca) { + snprintf(buf, sizeof(buf), "%s/.pwmd/ca-cert.pem", pw->pw_dir); + tca = xstrdup(buf); + } + else + tca = xstrdup(ca); + + if (!tca) { + *rc = gpg_error_from_syserror(); + xfree(tcert); + xfree(tkey); + return NULL; + } + + if ((pwm = (pwm_t *)xcalloc(1, sizeof(pwm_t))) == NULL) { + *rc = gpg_error_from_syserror(); + xfree(tcert); + xfree(tkey); + xfree(tca); + return NULL; + } + + *rc = _tls_init(pwm, fd, tcert, tkey, tca); + xfree(tcert); + xfree(tkey); + xfree(tca); + + if (*rc) { + close(fd); + xfree(pwm); + return NULL; + } + + assuan_set_io_hooks(&io_hooks); + *rc = assuan_socket_connect_fd(&ctx, fd, 0, pwm); + + if (*rc) { + xfree(pwm); + return NULL; + } + + assuan_set_finish_handler(ctx, _tls_assuan_deinit); + return _socket_connect_finalize(pwm, ctx); +} +#endif + +pwm_t *pwmd_connect(const char *path, gpg_error_t *rc) { pwm_t *pwm = NULL; char *socketpath = NULL; - time_t now; struct passwd *pw; assuan_context_t ctx; - int rc, n; - int active[2]; if (!path) { pw = getpwuid(getuid()); @@ -125,36 +458,18 @@ pwm_t *pwmd_connect(const char *path, gpg_error_t *error) else socketpath = xstrdup(path); - rc = assuan_socket_connect_ext(&ctx, socketpath, -1, 0); + *rc = assuan_socket_connect_ext(&ctx, socketpath, -1, 0); xfree(socketpath); - if (rc) { - *error = rc; + if (*rc) return NULL; - } - - n = assuan_get_active_fds(ctx, 0, active, sizeof(active)); if ((pwm = (pwm_t *)xcalloc(1, sizeof(pwm_t))) == NULL) { - *error = gpg_error_from_errno(errno); - assuan_disconnect(ctx); + *rc = gpg_error_from_syserror(); return NULL; } - pwm->fd = n <= 0 ? -1 : dup(active[0]); - - if (pwm->fd != -1) - fcntl(pwm->fd, F_SETFL, O_NONBLOCK); - - pwm->ctx = ctx; -#ifdef USE_PINENTRY - pwm->pid = -1; - pwm->pinentry_tries = 3; -#endif - time(&now); - srandom(now); - *error = 0; - return pwm; + return _socket_connect_finalize(pwm, ctx); } gpg_error_t pwmd_pending_line(pwm_t *pwm, char **line, size_t *len) @@ -175,7 +490,7 @@ void pwmd_close(pwm_t *pwm) if (pwm->ctx) assuan_disconnect(pwm->ctx); - + if (pwm->password) xfree(pwm->password); diff --git a/libpwmd.h b/libpwmd.h index 848afdf4..a0ac1ac8 100644 --- a/libpwmd.h +++ b/libpwmd.h @@ -222,6 +222,22 @@ gpg_error_t pwmd_init(void); pwm_t *pwmd_connect(const char *socket_path, gpg_error_t *error) __attribute__ ((warn_unused_result)); /* + * Connects to the remote host "hostname". A port may be specified by + * appending ":port" to the hostname. If not specified the default port 6466 + * will be used. Returns a new handle for use with the other functions or + * NULL if there was an error in which case 'error' is set to an error code + * which may be described by pwmd_strerror(). + * + * The "cert" and "key" parameters should be the location of the client + * certificate and certificate key files to use during the GNUTLS handshake. + * If NULL, then ~/.pwmd/client-cert.pem and ~/.pwmd/client-key.pem will be + * used as the default. The "ca" parameter is the certificate authority the + * server certificate was signed with. The default is ~/.pwmd/ca-cert.pem. + */ +pwm_t *pwmd_tcp_connect(const char *hostname, gpg_error_t *error, + const char *cert, const char *key, const char *ca) __attribute__ ((warn_unused_result)); + +/* * Sets 'ctx' to the assuan context associated with the handle 'pwm' and 'fd' * to the socket file descriptor. Returns 0 on success or an error code. */ diff --git a/pwmc.c b/pwmc.c index 936a79cd..9804e232 100644 --- a/pwmc.c +++ b/pwmc.c @@ -67,11 +67,18 @@ static void usage(const char *pn) #endif "[-PTND ] [-p ]\n" " [-S [-i ]] [-c ] [-t ] [-d ] [-I ]\n" +#ifdef WITH_GNUTLS + " [-H [:port]] [filename]\n" +#else " [filename]\n" +#endif #ifdef DEBUG " -E pinentry method (0=pwmd, 1=pwmd async, 2=libpwmd nb)\n" " -y number of pinentry tries before failing (3)\n" #endif +#ifdef WITH_GNUTLS + " -H connect to hostname with optional port\n" +#endif " -t pinentry timeout\n" " -X disable showing of status messages from the server\n" " -c set the client name\n" @@ -213,6 +220,9 @@ int main(int argc, char *argv[]) char *inquire = NULL; int iter = -2; int timeout = 0; +#ifdef WITH_GNUTLS + char *host = NULL; +#endif #ifdef DEBUG int tries = 0; int method = 0; @@ -223,19 +233,33 @@ int main(int argc, char *argv[]) bindtextdomain("libpwmd", LOCALEDIR); #ifdef DEBUG +#ifdef WITH_GNUTLS + while ((opt = getopt(argc, argv, "H:y:t:E:c:I:XT:N:D:hvP:p:s:Si:d:")) != EOF) { +#else while ((opt = getopt(argc, argv, "y:t:E:c:I:XT:N:D:hvP:p:s:Si:d:")) != EOF) { +#endif +#else +#ifdef WITH_GNUTLS + while ((opt = getopt(argc, argv, "H:t:c:I:XT:N:D:hvP:p:s:Si:d:")) != EOF) { #else while ((opt = getopt(argc, argv, "t:c:I:XT:N:D:hvP:p:s:Si:d:")) != EOF) { #endif +#endif switch (opt) { #ifdef DEBUG case 'E': method = atoi(optarg); + + if (method > 2) + method = 2; break; case 'y': tries = atoi(optarg); break; #endif + case 'H': + host = xstrdup(optarg); + break; case 't': timeout = atoi(optarg); break; @@ -307,10 +331,22 @@ int main(int argc, char *argv[]) filename = argv[optind]; pwmd_init(); - if ((pwm = pwmd_connect(socketpath, &error)) == NULL) { - xfree(password); - errx(EXIT_FAILURE, "pwmd_connect(): %s", pwmd_strerror(error)); +#ifdef WITH_GNUTLS + if (host) { + if ((pwm = pwmd_tcp_connect(host, &error, NULL, NULL, NULL)) == NULL) { + xfree(password); + errx(EXIT_FAILURE, "pwmd_connect(): %s", pwmd_strerror(error)); + } } + else { +#endif + if ((pwm = pwmd_connect(socketpath, &error)) == NULL) { + xfree(password); + errx(EXIT_FAILURE, "pwmd_connect(): %s", pwmd_strerror(error)); + } +#ifdef WITH_GNUTLS + } +#endif error = pwmd_command(pwm, &result, "OPTION CLIENT NAME=%s", clientname ? clientname : "pwmc"); xfree(clientname); diff --git a/types.h b/types.h index c1d66193..46ecf0f6 100644 --- a/types.h +++ b/types.h @@ -20,9 +20,16 @@ #define TYPES_H #include +#ifdef WITH_GNUTLS +#include +#endif struct pwm_s { assuan_context_t ctx; +#ifdef WITH_GNUTLS + gnutls_session_t session; + gnutls_certificate_credentials_t x509; +#endif int fd; pwmd_async_t state; #ifdef USE_PINENTRY -- 2.11.4.GIT