One of our sites has a need to require/verify TLS client certs,
without the overhead & complexity of apache / nginx, etc.

OpenBSD's httpd seemed the obvious candidate, and I figured that the
feature would be useful to others too -- see attached diff for an
initial implementation.

Of course, there are two parts to verifying TLS client certs:

1. Checking that the certificate was issued by a locally-trusted CA and
has not yet expired. That's fairly straightforward and (I would hope)
non-controversial. The attached diff adds that to httpd and also
exposes as new fastcgi variables some of the more useful client cert
data that libtls provides (since fastcgi responders are likely where
identity is of most interest).

2. Validating that the certificate has not yet been revoked. The
attached diff does NOT address that part, but that's what I intend to
turn my attention to next, if there's any interest in adding TLS client
cert support to httpd. I'll send a separate post with my thoughts on
that shortly (as there's a bunch of ways to do it, so some discussion
on which to prefer is probably in order).

So, my question here for the devs is: does this fit in with the intended
direction of httpd(8)? (if so, I'll work on part 2 next; if not, I'll
stop bothering you)

This is the first time I've submitted a diff with a proposed new feature
(as opposed to just a bug fix) for any part of OpenBSD, so if I've gone
about any part of it in the wrong way, please correct me & I'll try to
do better next time.

Index: config.c
===================================================================
RCS file: /cvs/src/usr.sbin/httpd/config.c,v
retrieving revision 1.51
diff -u -p -r1.51 config.c
--- config.c    25 Mar 2017 17:25:34 -0000      1.51
+++ config.c    30 Mar 2017 10:03:42 -0000
@@ -326,6 +326,28 @@ config_settls(struct httpd *env, struct 
                }
        }
 
+       if (srv_conf->tls_ca_len != 0) {
+               DPRINTF("%s: sending ca cert(s) for \"%s[%u]\" to %s fd %d",
+                   __func__, srv_conf->name, srv_conf_id,
+                   ps->ps_title[PROC_SERVER], srv->srv_s);
+
+               memset(&tls, 0, sizeof(tls));
+               tls.id = srv_conf->id;
+               tls.tls_ca_len = srv_conf->tls_ca_len;
+
+               c = 0;
+               iov[c].iov_base = &tls;
+               iov[c++].iov_len = sizeof(tls);
+               iov[c].iov_base = srv_conf->tls_ca;
+               iov[c++].iov_len = srv_conf->tls_ca_len;
+
+               if (proc_composev(ps, PROC_SERVER, IMSG_CFG_TLS, iov, c) != 0) {
+                       log_warn("%s: failed to compose IMSG_CFG_TLS imsg for "
+                           "`%s'", __func__, srv_conf->name);
+                       return (-1);
+               }
+       }
+
        return (0);
 }
 
@@ -644,6 +666,13 @@ config_gettls(struct httpd *env, struct 
                    tls_conf.tls_ocsp_staple_len)) == NULL)
                        goto fail;
                s += tls_conf.tls_ocsp_staple_len;
+       }
+       if (tls_conf.tls_ca_len != 0) {
+               srv_conf->tls_ca_len = tls_conf.tls_ca_len;
+               if ((srv_conf->tls_ca = get_data(p + s,
+                   tls_conf.tls_ca_len)) == NULL)
+                       goto fail;
+               s += tls_conf.tls_ca_len;
        }
 
        return (0);
Index: httpd.conf.5
===================================================================
RCS file: /cvs/src/usr.sbin/httpd/httpd.conf.5,v
retrieving revision 1.81
diff -u -p -r1.81 httpd.conf.5
--- httpd.conf.5        25 Mar 2017 17:25:34 -0000      1.81
+++ httpd.conf.5        30 Mar 2017 10:03:43 -0000
@@ -342,6 +342,18 @@ The revision of the HTTP specification u
 .It Ic SERVER_SOFTWARE
 The server software name of
 .Xr httpd 8 .
+.It Ic TLS_PEER_EXPIRES
+The time at which the TLS client certificate, if any, expires,
+in seconds since the epoch.
+.It Ic TLS_PEER_HASH
+A hash of the TLS client certificate, if any. See
+.Xr tls_peer_cert_hash 3 .
+.It Ic TLS_PEER_ISSUER
+The issuer of the TLS client certificate, if any.
+.It Ic TLS_PEER_OCSP_URL
+The OCSP URL from the TLS client certificate, if any.
+.It Ic TLS_PEER_SUBJECT
+The subject of the TLS client certificate, if any.
 .El
 .It Ic hsts Oo Ar option Oc
 Enable HTTP Strict Transport Security.
@@ -511,6 +523,14 @@ Set the TLS configuration for the server
 These options are only used if TLS has been enabled via the listen directive.
 Valid options are:
 .Bl -tag -width Ds
+.It Ic ca Ar file
+Specify the CA certificate(s) for this server to use when verifying client
+certificates.
+The
+.Ar file
+should contain one or more PEM encoded CA certificates.
+The default is
+.Pa /etc/ssl/ca.crt .
 .It Ic certificate Ar file
 Specify the certificate to use for this server.
 The
@@ -571,6 +591,11 @@ session lifetime.
 It is possible to set
 .Ar seconds
 to default to use the httpd default timeout of 2 hours.
+.It Ic verify
+Require a valid TLS client certificate (checked using the CA certificate(s)
+specified by the
+.Ic tls ca
+option) for a TLS connection to proceed beyond the handshake.
 .El
 .El
 .Sh TYPES
Index: httpd.h
===================================================================
RCS file: /cvs/src/usr.sbin/httpd/httpd.h,v
retrieving revision 1.131
diff -u -p -r1.131 httpd.h
--- httpd.h     25 Mar 2017 17:25:34 -0000      1.131
+++ httpd.h     30 Mar 2017 10:03:43 -0000
@@ -55,6 +55,7 @@
 #define HTTPD_ERROR_LOG                "error.log"
 #define HTTPD_DEFAULT_TYPE     { "bin", "application", "octet-stream", NULL }
 #define HTTPD_LOGVIS           VIS_NL|VIS_TAB|VIS_CSTYLE
+#define HTTPD_TLS_CA           "/etc/ssl/ca.crt"
 #define HTTPD_TLS_CERT         "/etc/ssl/server.crt"
 #define HTTPD_TLS_KEY          "/etc/ssl/private/server.key"
 #define HTTPD_TLS_CIPHERS      "compat"
@@ -475,10 +476,14 @@ struct server_config {
        uint32_t                 maxrequests;
        size_t                   maxrequestbody;
 
+       uint8_t                 *tls_ca;
+       char                    *tls_ca_file;
+       size_t                   tls_ca_len;
        uint8_t                 *tls_cert;
        size_t                   tls_cert_len;
        char                    *tls_cert_file;
        char                     tls_ciphers[NAME_MAX];
+       int                      tls_client_verify;
        char                     tls_dhe_params[NAME_MAX];
        char                     tls_ecdhe_curve[NAME_MAX];
        uint8_t                 *tls_key;
@@ -521,6 +526,7 @@ TAILQ_HEAD(serverhosts, server_config);
 struct tls_config {
        uint32_t                 id;
 
+       size_t                   tls_ca_len;
        size_t                   tls_cert_len;
        size_t                   tls_key_len;
        size_t                   tls_ocsp_staple_len;
@@ -586,6 +592,7 @@ int  cmdline_symset(char *);
 /* server.c */
 void    server(struct privsep *, struct privsep_proc *);
 int     server_tls_cmp(struct server *, struct server *, int);
+int     server_tls_load_ca(struct server *);
 int     server_tls_load_keypair(struct server *);
 int     server_tls_load_ocsp(struct server *);
 void    server_generate_ticket_key(struct server_config *);
Index: parse.y
===================================================================
RCS file: /cvs/src/usr.sbin/httpd/parse.y,v
retrieving revision 1.90
diff -u -p -r1.90 parse.y
--- parse.y     25 Mar 2017 17:25:34 -0000      1.90
+++ parse.y     30 Mar 2017 10:03:43 -0000
@@ -129,11 +129,12 @@ typedef struct {
 
 %}
 
-%token ACCESS ALIAS AUTO BACKLOG BODY BUFFER CERTIFICATE CHROOT CIPHERS COMMON
-%token COMBINED CONNECTION DHE DIRECTORY ECDHE ERR FCGI INDEX IP KEY LIFETIME
-%token LISTEN LOCATION LOG LOGDIR MATCH MAXIMUM NO NODELAY OCSP ON PORT PREFORK
-%token PROTOCOLS REQUESTS ROOT SACK SERVER SOCKET STRIP STYLE SYSLOG TCP TICKET
-%token TIMEOUT TLS TYPE TYPES HSTS MAXAGE SUBDOMAINS DEFAULT PRELOAD REQUEST
+%token ACCESS ALIAS AUTO BACKLOG BODY BUFFER CA CERTIFICATE CHROOT CIPHERS
+%token COMMON COMBINED CONNECTION DHE DIRECTORY ECDHE ERR FCGI INDEX IP KEY
+%token LIFETIME LISTEN LOCATION LOG LOGDIR MATCH MAXIMUM NO NODELAY OCSP ON
+%token PORT PREFORK PROTOCOLS REQUESTS ROOT SACK SERVER SOCKET STRIP STYLE
+%token SYSLOG TCP TICKET TIMEOUT TLS TYPE TYPES HSTS MAXAGE SUBDOMAINS VERIFY
+%token DEFAULT PRELOAD REQUEST
 %token ERROR INCLUDE AUTHENTICATE WITH BLOCK DROP RETURN PASS
 %token <v.string>      STRING
 %token  <v.number>     NUMBER
@@ -254,6 +255,9 @@ server              : SERVER optmatch STRING        {
                                s->srv_conf.flags |= SRVFLAG_SERVER_MATCH;
                        s->srv_conf.logformat = LOG_FORMAT_COMMON;
                        s->srv_conf.tls_protocols = TLS_PROTOCOLS_DEFAULT;
+                       if ((s->srv_conf.tls_ca_file =
+                          strdup(HTTPD_TLS_CA)) == NULL)
+                               fatal("out of memory");
                        if ((s->srv_conf.tls_cert_file =
                            strdup(HTTPD_TLS_CERT)) == NULL)
                                fatal("out of memory");
@@ -344,6 +348,14 @@ server             : SERVER optmatch STRING        {
                                YYERROR;
                        }
 
+                       if (server_tls_load_ca(srv) == -1) {
+                               yyerror("server \"%s\": failed to load "
+                                   "ca cert(s)", srv->srv_conf.name);
+                               serverconfig_free(srv_conf);
+                               free(srv);
+                               YYERROR;
+                       }
+
                        if (server_tls_load_ocsp(srv) == -1) {
                                yyerror("server \"%s\": failed to load "
                                    "ocsp staple", srv->srv_conf.name);
@@ -727,6 +739,15 @@ tlsopts            : CERTIFICATE STRING            {
                                fatal("out of memory");
                        free($2);
                }
+               | CA STRING                     {
+                       free(srv_conf->tls_ca_file);
+                       if ((srv_conf->tls_ca_file = strdup($2)) == NULL)
+                               fatal("out of memory");
+                       free($2);
+               }
+               | VERIFY                        {
+                       srv_conf->tls_client_verify = 1;
+               }
                | CIPHERS STRING                {
                        if (strlcpy(srv_conf->tls_ciphers, $2,
                            sizeof(srv_conf->tls_ciphers)) >=
@@ -1217,6 +1238,7 @@ lookup(char *s)
                { "block",              BLOCK },
                { "body",               BODY },
                { "buffer",             BUFFER },
+               { "ca",                 CA },
                { "certificate",        CERTIFICATE },
                { "chroot",             CHROOT },
                { "ciphers",            CIPHERS },
@@ -1269,6 +1291,7 @@ lookup(char *s)
                { "tls",                TLS },
                { "type",               TYPE },
                { "types",              TYPES },
+               { "verify",             VERIFY },
                { "with",               WITH }
        };
        const struct keywords   *p;
@@ -2043,6 +2066,9 @@ server_inherit(struct server *src, struc
 
        /* Copy the source server and assign a new Id */
        memcpy(&dst->srv_conf, &src->srv_conf, sizeof(dst->srv_conf));
+       if ((dst->srv_conf.tls_ca_file =
+           strdup(src->srv_conf.tls_ca_file)) == NULL)
+               fatal("out of memory");
        if ((dst->srv_conf.tls_cert_file =
            strdup(src->srv_conf.tls_cert_file)) == NULL)
                fatal("out of memory");
@@ -2080,6 +2106,7 @@ server_inherit(struct server *src, struc
                dst->srv_conf.flags |= SRVFLAG_TLS;
        else
                dst->srv_conf.flags &= ~SRVFLAG_TLS;
+       dst->srv_conf.tls_client_verify = src->srv_conf.tls_client_verify;
 
        /* Don't inherit the "match" option, use it from the alias */
        dst->srv_conf.flags &= ~SRVFLAG_SERVER_MATCH;
@@ -2087,6 +2114,14 @@ server_inherit(struct server *src, struc
 
        if (server_tls_load_keypair(dst) == -1) {
                yyerror("failed to load public/private keys "
+                   "for server %s", dst->srv_conf.name);
+               serverconfig_free(&dst->srv_conf);
+               free(dst);
+               return (NULL);
+       }
+
+       if (server_tls_load_ca(dst) == -1) {
+               yyerror("failed to load ca cert(s) "
                    "for server %s", dst->srv_conf.name);
                serverconfig_free(&dst->srv_conf);
                free(dst);
Index: server.c
===================================================================
RCS file: /cvs/src/usr.sbin/httpd/server.c,v
retrieving revision 1.108
diff -u -p -r1.108 server.c
--- server.c    25 Mar 2017 17:25:34 -0000      1.108
+++ server.c    30 Mar 2017 10:03:44 -0000
@@ -197,6 +197,26 @@ server_tls_load_ocsp(struct server *srv)
 }
 
 int
+server_tls_load_ca(struct server *srv)
+{
+       if ((srv->srv_conf.flags & SRVFLAG_TLS) == 0 ||
+           srv->srv_conf.tls_client_verify == 0)
+               return (0);
+
+       if (srv->srv_conf.tls_ca_file == NULL)
+               return (0);
+
+       if ((srv->srv_conf.tls_ca = tls_load_file(
+           srv->srv_conf.tls_ca_file,
+           &srv->srv_conf.tls_ca_len, NULL)) == NULL)
+               return (-1);
+       log_debug("%s: using ca cert(s) from %s", __func__,
+           srv->srv_conf.tls_ca_file);
+
+       return (0);
+}
+
+int
 server_tls_init(struct server *srv)
 {
        struct server_config *srv_conf;
@@ -254,6 +274,15 @@ server_tls_init(struct server *srv)
                return (-1);
        }
 
+       if (srv->srv_conf.tls_ca != NULL && srv->srv_conf.tls_client_verify) {
+               if (tls_config_set_ca_mem(srv->srv_tls_config,
+                   srv->srv_conf.tls_ca, srv->srv_conf.tls_ca_len) != 0) {
+                       log_warnx("%s: failed to add ca cert(s)", __func__);
+                       return (-1);
+               }
+               tls_config_verify_client(srv->srv_tls_config);
+       }
+
        TAILQ_FOREACH(srv_conf, &srv->srv_hosts, entry) {
                if (srv_conf->tls_cert == NULL || srv_conf->tls_key == NULL)
                        continue;
@@ -267,6 +296,16 @@ server_tls_init(struct server *srv)
                        log_warnx("%s: failed to add tls keypair", __func__);
                        return (-1);
                }
+               if (srv_conf->tls_ca != NULL) {
+                       log_debug("%s: adding ca cert(s) for server %s",
+                           __func__, srv->srv_conf.name);
+                       if (tls_config_set_ca_mem(srv->srv_tls_config,
+                           srv_conf->tls_ca, srv_conf->tls_ca_len) != 0) {
+                               log_warnx("%s: failed to add ca cert(s)",
+                                   __func__);
+                               return (-1);
+                       }
+               }
        }
 
        /* set common session ID among all processes */
@@ -414,6 +453,8 @@ void
 serverconfig_free(struct server_config *srv_conf)
 {
        free(srv_conf->return_uri);
+       free(srv_conf->tls_ca_file);
+       free(srv_conf->tls_ca);
        free(srv_conf->tls_cert_file);
        free(srv_conf->tls_key_file);
        free(srv_conf->tls_ocsp_staple_file);
@@ -435,6 +476,8 @@ serverconfig_reset(struct server_config 
 {
        srv_conf->auth = NULL;
        srv_conf->return_uri = NULL;
+       srv_conf->tls_ca = NULL;
+       srv_conf->tls_ca_file = NULL;
        srv_conf->tls_cert = NULL;
        srv_conf->tls_cert_file = NULL;
        srv_conf->tls_key = NULL;
Index: server_fcgi.c
===================================================================
RCS file: /cvs/src/usr.sbin/httpd/server_fcgi.c,v
retrieving revision 1.74
diff -u -p -r1.74 server_fcgi.c
--- server_fcgi.c       21 Jan 2017 11:32:04 -0000      1.74
+++ server_fcgi.c       30 Mar 2017 10:03:44 -0000
@@ -282,11 +282,38 @@ server_fcgi(struct httpd *env, struct cl
                goto fail;
        }
 
-       if (srv_conf->flags & SRVFLAG_TLS)
+       if (srv_conf->flags & SRVFLAG_TLS) {
                if (fcgi_add_param(&param, "HTTPS", "on", clt) == -1) {
                        errstr = "failed to encode param";
                        goto fail;
                }
+               if (srv_conf->tls_client_verify) {
+                       if (fcgi_add_param(&param, "TLS_PEER_SUBJECT",
+                           tls_peer_cert_subject(clt->clt_tls_ctx), clt) == -1
+                           || fcgi_add_param(&param, "TLS_PEER_ISSUER",
+                           tls_peer_cert_issuer(clt->clt_tls_ctx), clt) == -1
+                           || fcgi_add_param(&param, "TLS_PEER_HASH",
+                           tls_peer_cert_hash(clt->clt_tls_ctx), clt) == -1) {
+                               errstr = "failed to encode param";
+                               goto fail;
+                       }
+                       if (asprintf(&str, "%lld",
+                           tls_peer_cert_notafter(clt->clt_tls_ctx)) == -1
+                           || fcgi_add_param(&param, "TLS_PEER_EXPIRES", str,
+                           clt) == -1) {
+                               errstr = "failed to encode param";
+                               goto fail;
+                       }
+                       free(str);
+                       str = NULL;
+                       if (tls_peer_ocsp_url(clt->clt_tls_ctx) != NULL
+                           && fcgi_add_param(&param, "TLS_PEER_OCSP_URL",
+                           tls_peer_ocsp_url(clt->clt_tls_ctx), clt) == -1) {
+                               errstr = "failed to encode param";
+                               goto fail;
+                       }
+               }
+       }
 
        (void)print_host(&clt->clt_ss, hbuf, sizeof(hbuf));
        if (fcgi_add_param(&param, "REMOTE_ADDR", hbuf, clt) == -1) {

Reply via email to