Here's another iteration of my diff, updated to work with the new & improved imsg transfers for tls config that jsing@ committed yesterday.
That helped simplify this diff significantly. Other changes since the last version: * uses simpler config (one new directive instead of three); * drops the tls client config flags in favour of an approach similar to that taken internally by libtls; and * on the fastcgi side drops TLS_VERIFY_MODE (and its unnecessary extra global bit string) in favour of the simpler TLS_PEER_CRL (which works just like HTTPS). As previously, any authorisation (as opposed to authentication) based on tls client certs beyond a simple "holder of any trusted, authentic, non-revoked cert can access anything" remains the province of the fastcgi responders, presumably based on the contents of TLS_PEER_* (see the discussion here back in April). Thoughts? Index: usr.sbin/httpd/config.c =================================================================== RCS file: /cvs/src/usr.sbin/httpd/config.c,v retrieving revision 1.53 diff -u -p -r1.53 config.c --- usr.sbin/httpd/config.c 19 Jul 2017 17:36:25 -0000 1.53 +++ usr.sbin/httpd/config.c 21 Jul 2017 08:25:56 -0000 @@ -304,10 +304,18 @@ config_setserver_tls(struct httpd *env, log_debug("%s: configuring tls for %s", __func__, srv_conf->name); + if (config_settls(env, srv, TLS_CFG_CA, "ca", srv_conf->tls_ca, + srv_conf->tls_ca_len) != 0) + return (-1); + if (config_settls(env, srv, TLS_CFG_CERT, "cert", srv_conf->tls_cert, srv_conf->tls_cert_len) != 0) return (-1); + if (config_settls(env, srv, TLS_CFG_CRL, "crl", srv_conf->tls_crl, + srv_conf->tls_crl_len) != 0) + return (-1); + if (config_settls(env, srv, TLS_CFG_KEY, "key", srv_conf->tls_key, srv_conf->tls_key_len) != 0) return (-1); @@ -655,9 +663,21 @@ config_getserver_tls(struct httpd *env, } switch (tls_conf.tls_type) { + case TLS_CFG_CA: + if (config_gettls(env, srv_conf, &tls_conf, "ca", p, len, + &srv_conf->tls_ca, &srv_conf->tls_ca_len) != 0) + goto fail; + break; + case TLS_CFG_CERT: if (config_gettls(env, srv_conf, &tls_conf, "cert", p, len, &srv_conf->tls_cert, &srv_conf->tls_cert_len) != 0) + goto fail; + break; + + case TLS_CFG_CRL: + if (config_gettls(env, srv_conf, &tls_conf, "crl", p, len, + &srv_conf->tls_crl, &srv_conf->tls_crl_len) != 0) goto fail; break; Index: usr.sbin/httpd/httpd.conf.5 =================================================================== RCS file: /cvs/src/usr.sbin/httpd/httpd.conf.5,v retrieving revision 1.82 diff -u -p -r1.82 httpd.conf.5 --- usr.sbin/httpd/httpd.conf.5 9 Apr 2017 09:13:28 -0000 1.82 +++ usr.sbin/httpd/httpd.conf.5 21 Jul 2017 08:25:56 -0000 @@ -342,6 +342,28 @@ The revision of the HTTP specification u .It Ic SERVER_SOFTWARE The server software name of .Xr httpd 8 . +.It Ic TLS_PEER_CHAIN +The TLS client certificate chain, if any, PEM encoded, with +newlines translated to tabs. +.It Ic TLS_PEER_CRL +A variable that is set to +.Qq on +when the server has been configured to check the client certificate chain +against CRL(s). +This variable is omitted otherwise. +.It Ic TLS_PEER_EXPIRES +The time at which the TLS cient 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. @@ -526,6 +548,17 @@ will be used (strong crypto cipher suite See the CIPHERS section of .Xr openssl 1 for information about SSL/TLS cipher suites and preference lists. +.It Ic client ca Ar cafile Op Ic crl Ar crlfile +Require TLS client certificates whose authenticity can be verified +against the CA certificate(s) in +.Ar cafile +in order to proceed beyond the TLS handshake. +With +.Ic crl +specified, additionally require that no certificate in the client chain be +listed as revoked in the CRL(s) in +.Ar crlfile . +CA certificates and CRLs should be PEM encoded. .It Ic dhe Ar params Specify the DHE parameters to use for DHE cipher suites. Valid parameter values are none, legacy and auto. @@ -688,6 +721,7 @@ server "www.example.com" { .Ed .Sh SEE ALSO .Xr htpasswd 1 , +.Xr tls_peer_cert_hash 3 , .Xr patterns 7 , .Xr httpd 8 , .Xr ocspcheck 8 , Index: usr.sbin/httpd/httpd.h =================================================================== RCS file: /cvs/src/usr.sbin/httpd/httpd.h,v retrieving revision 1.133 diff -u -p -r1.133 httpd.h --- usr.sbin/httpd/httpd.h 19 Jul 2017 17:36:25 -0000 1.133 +++ usr.sbin/httpd/httpd.h 21 Jul 2017 08:25:56 -0000 @@ -476,9 +476,15 @@ 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; + uint8_t *tls_crl; + char *tls_crl_file; + size_t tls_crl_len; char tls_ciphers[NAME_MAX]; char tls_dhe_params[NAME_MAX]; char tls_ecdhe_curve[NAME_MAX]; @@ -520,7 +526,9 @@ struct server_config { TAILQ_HEAD(serverhosts, server_config); enum tls_config_type { + TLS_CFG_CA, TLS_CFG_CERT, + TLS_CFG_CRL, TLS_CFG_KEY, TLS_CFG_OCSP_STAPLE, }; @@ -594,6 +602,8 @@ 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_crl(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: usr.sbin/httpd/parse.y =================================================================== RCS file: /cvs/src/usr.sbin/httpd/parse.y,v retrieving revision 1.90 diff -u -p -r1.90 parse.y --- usr.sbin/httpd/parse.y 25 Mar 2017 17:25:34 -0000 1.90 +++ usr.sbin/httpd/parse.y 21 Jul 2017 08:25:56 -0000 @@ -135,6 +135,7 @@ typedef struct { %token PROTOCOLS REQUESTS ROOT SACK SERVER SOCKET STRIP STYLE SYSLOG TCP TICKET %token TIMEOUT TLS TYPE TYPES HSTS MAXAGE SUBDOMAINS DEFAULT PRELOAD REQUEST %token ERROR INCLUDE AUTHENTICATE WITH BLOCK DROP RETURN PASS +%token CA CLIENT CRL %token <v.string> STRING %token <v.number> NUMBER %type <v.port> port @@ -344,6 +345,22 @@ 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_crl(srv) == -1) { + yyerror("server \"%s\": failed to load crl(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); @@ -737,6 +754,12 @@ tlsopts : CERTIFICATE STRING { } free($2); } + | CLIENT CA STRING tlsclientopt { + free(srv_conf->tls_ca_file); + if ((srv_conf->tls_ca_file = strdup($3)) == NULL) + fatal("out of memory"); + free($3); + } | DHE STRING { if (strlcpy(srv_conf->tls_dhe_params, $2, sizeof(srv_conf->tls_dhe_params)) >= @@ -785,6 +808,14 @@ tlsopts : CERTIFICATE STRING { } ; +tlsclientopt : /* empty */ + | CRL STRING { + free(srv_conf->tls_crl_file); + if ((srv_conf->tls_crl_file = strdup($2)) == NULL) + fatal("out of memory"); + free($2); + } + root : ROOT rootflags | ROOT '{' optnl rootflags_l '}' ; @@ -1217,12 +1248,15 @@ lookup(char *s) { "block", BLOCK }, { "body", BODY }, { "buffer", BUFFER }, + { "ca", CA }, { "certificate", CERTIFICATE }, { "chroot", CHROOT }, { "ciphers", CIPHERS }, + { "client", CLIENT }, { "combined", COMBINED }, { "common", COMMON }, { "connection", CONNECTION }, + { "crl", CRL }, { "default", DEFAULT }, { "dhe", DHE }, { "directory", DIRECTORY }, @@ -2049,6 +2083,15 @@ server_inherit(struct server *src, struc if ((dst->srv_conf.tls_key_file = strdup(src->srv_conf.tls_key_file)) == NULL) fatal("out of memory"); + if (src->srv_conf.tls_ca_file != NULL) { + if ((dst->srv_conf.tls_ca_file = + strdup(src->srv_conf.tls_ca_file)) == NULL) + fatal("out of memory"); + if (src->srv_conf.tls_crl_file != NULL && + (dst->srv_conf.tls_crl_file = + strdup(src->srv_conf.tls_crl_file)) == NULL) + fatal("out of memory"); + } if (src->srv_conf.tls_ocsp_staple_file != NULL) { if ((dst->srv_conf.tls_ocsp_staple_file = strdup(src->srv_conf.tls_ocsp_staple_file)) == NULL) @@ -2087,6 +2130,22 @@ 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); + return (NULL); + } + + if (server_tls_load_crl(dst) == -1) { + yyerror("failed to load crl(s) " "for server %s", dst->srv_conf.name); serverconfig_free(&dst->srv_conf); free(dst); Index: usr.sbin/httpd/server.c =================================================================== RCS file: /cvs/src/usr.sbin/httpd/server.c,v retrieving revision 1.110 diff -u -p -r1.110 server.c --- usr.sbin/httpd/server.c 19 Jul 2017 17:36:25 -0000 1.110 +++ usr.sbin/httpd/server.c 21 Jul 2017 08:25:57 -0000 @@ -197,6 +197,40 @@ 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_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_load_crl(struct server *srv) +{ + if ((srv->srv_conf.flags & SRVFLAG_TLS) == 0 || + srv->srv_conf.tls_crl_file == NULL) + return (0); + + if ((srv->srv_conf.tls_crl = tls_load_file( + srv->srv_conf.tls_crl_file, + &srv->srv_conf.tls_crl_len, NULL)) == NULL) + return (-1); + log_debug("%s: using crl(s) from %s", __func__, + srv->srv_conf.tls_crl_file); + + return (0); +} + +int server_tls_init(struct server *srv) { struct server_config *srv_conf; @@ -254,6 +288,24 @@ server_tls_init(struct server *srv) return (-1); } + if (srv->srv_conf.tls_ca != NULL) { + 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); + + if (srv->srv_conf.tls_crl != NULL) { + if (tls_config_set_crl_mem(srv->srv_tls_config, + srv->srv_conf.tls_crl, + srv->srv_conf.tls_crl_len) != 0) { + log_warnx("%s: failed to add crl(s)", __func__); + return (-1); + } + } + } + TAILQ_FOREACH(srv_conf, &srv->srv_hosts, entry) { if (srv_conf->tls_cert == NULL || srv_conf->tls_key == NULL) continue; @@ -267,6 +319,28 @@ server_tls_init(struct server *srv) log_warnx("%s: failed to add tls keypair", __func__); return (-1); } + + if (srv->srv_conf.tls_ca == NULL) + continue; + + 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); + } + + if (srv->srv_conf.tls_crl == NULL) + continue; + + log_debug("%s: adding crl(s) for server %s", __func__, + srv->srv_conf.name); + if (tls_config_set_crl_mem(srv->srv_tls_config, + srv_conf->tls_crl, srv_conf->tls_crl_len) != 0) { + log_warnx("%s: failed to add crl(s)", __func__); + return (-1); + } } /* set common session ID among all processes */ @@ -412,7 +486,11 @@ 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_crl_file); + free(srv_conf->tls_crl); free(srv_conf->tls_key_file); free(srv_conf->tls_ocsp_staple_file); free(srv_conf->tls_ocsp_staple); @@ -425,8 +503,12 @@ 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_crl = NULL; + srv_conf->tls_crl_file = NULL; srv_conf->tls_key = NULL; srv_conf->tls_key_file = NULL; srv_conf->tls_ocsp_staple = NULL; Index: usr.sbin/httpd/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 --- usr.sbin/httpd/server_fcgi.c 21 Jan 2017 11:32:04 -0000 1.74 +++ usr.sbin/httpd/server_fcgi.c 21 Jul 2017 08:25:57 -0000 @@ -93,11 +93,12 @@ server_fcgi(struct httpd *env, struct cl struct fcgi_record_header *h; struct fcgi_begin_request_body *begin; char hbuf[HOST_NAME_MAX+1]; - size_t scriptlen; + size_t chainlen, scriptlen; int pathlen; int fd = -1, ret; const char *stripped, *p, *alias, *errstr = NULL; - char *str, *script = NULL; + char *chainc, *chainp, *str, *script = NULL; + const uint8_t *chain; if (srv_conf->socket[0] == ':') { struct sockaddr_storage ss; @@ -282,11 +283,57 @@ 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(¶m, "HTTPS", "on", clt) == -1) { errstr = "failed to encode param"; goto fail; } + if (srv_conf->tls_ca != NULL) { + chain = tls_peer_cert_chain_pem(clt->clt_tls_ctx, + &chainlen); + if ((chainc = strndup(chain, chainlen)) == NULL) + goto fail; + chainp = chainc; + while ((chainp = strchr(chainp, '\n')) != NULL) + *chainp = '\t'; + ret = fcgi_add_param(¶m, "TLS_PEER_CHAIN", chainc, + clt); + free(chainc); + if (ret == -1) { + errstr = "failed to encode param"; + goto fail; + } + if (fcgi_add_param(¶m, "TLS_PEER_SUBJECT", + tls_peer_cert_subject(clt->clt_tls_ctx), clt) == -1 + || fcgi_add_param(¶m, "TLS_PEER_ISSUER", + tls_peer_cert_issuer(clt->clt_tls_ctx), clt) == -1 + || fcgi_add_param(¶m, "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(¶m, "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(¶m, "TLS_PEER_OCSP_URL", + tls_peer_ocsp_url(clt->clt_tls_ctx), clt) == -1) { + errstr = "failed to encode param"; + goto fail; + } + if (srv_conf->tls_crl != NULL && fcgi_add_param(¶m, + "TLS_PEER_CRL", "on", clt) == -1) { + errstr = "failed to encode param"; + goto fail; + } + } + } (void)print_host(&clt->clt_ss, hbuf, sizeof(hbuf)); if (fcgi_add_param(¶m, "REMOTE_ADDR", hbuf, clt) == -1) { Index: regress/usr.sbin/httpd/tests/Client.pm =================================================================== RCS file: /cvs/src/regress/usr.sbin/httpd/tests/Client.pm,v retrieving revision 1.1 diff -u -p -r1.1 Client.pm --- regress/usr.sbin/httpd/tests/Client.pm 16 Jul 2015 16:35:57 -0000 1.1 +++ regress/usr.sbin/httpd/tests/Client.pm 21 Jul 2017 08:25:57 -0000 @@ -59,6 +59,11 @@ sub child { PeerAddr => $self->{connectaddr}, PeerPort => $self->{connectport}, SSL_verify_mode => SSL_VERIFY_NONE, + SSL_use_cert => $self->{offertlscert} ? 1 : 0, + SSL_cert_file => $self->{offertlscert} ? + $self->{chroot}."/client.crt" : "", + SSL_key_file => $self->{offertlscert} ? + $self->{chroot}."/client.key" : "", ) or die ref($self), " $iosocket socket connect failed: $!,$SSL_ERROR"; print STDERR "connect sock: ",$cs->sockhost()," ",$cs->sockport(),"\n"; print STDERR "connect peer: ",$cs->peerhost()," ",$cs->peerport(),"\n"; Index: regress/usr.sbin/httpd/tests/Httpd.pm =================================================================== RCS file: /cvs/src/regress/usr.sbin/httpd/tests/Httpd.pm,v retrieving revision 1.2 diff -u -p -r1.2 Httpd.pm --- regress/usr.sbin/httpd/tests/Httpd.pm 30 Jan 2017 21:18:24 -0000 1.2 +++ regress/usr.sbin/httpd/tests/Httpd.pm 21 Jul 2017 08:25:57 -0000 @@ -72,6 +72,8 @@ sub new { print $fh "\n"; print $fh "\ttls certificate \"".$args{chroot}."/server.crt\"\n"; print $fh "\ttls key \"".$args{chroot}."/server.key\""; + $self->{verifytls} + and print $fh "\ntls client ca \"".$args{chroot}."/ca.crt\""; } print $fh "\n\troot \"/\""; print $fh "\n\tlog style combined"; Index: regress/usr.sbin/httpd/tests/Makefile =================================================================== RCS file: /cvs/src/regress/usr.sbin/httpd/tests/Makefile,v retrieving revision 1.8 diff -u -p -r1.8 Makefile --- regress/usr.sbin/httpd/tests/Makefile 14 Jul 2017 13:31:44 -0000 1.8 +++ regress/usr.sbin/httpd/tests/Makefile 21 Jul 2017 08:25:57 -0000 @@ -77,10 +77,16 @@ ca.crt: server.req: openssl req -batch -new -subj /L=OpenBSD/O=httpd-regress/OU=server/CN=localhost/ -nodes -newkey rsa -keyout server.key -out server.req +client.req: + openssl req -batch -new -subj /L=OpenBSD/O=httpd-regress/OU=client/CN=localhost/ -nodes -newkey rsa -keyout client.key -out $@ + server.crt: ca.crt server.req openssl x509 -CAcreateserial -CAkey ca.key -CA ca.crt -req -in server.req -out server.crt -${REGRESS_TARGETS:M*tls*} ${REGRESS_TARGETS:M*https*}: server.crt +client.crt: ca.crt client.req + openssl x509 -CAcreateserial -CAkey ca.key -CA ca.crt -req -in client.req -out $@ + +${REGRESS_TARGETS:M*tls*} ${REGRESS_TARGETS:M*https*}: server.crt client.crt # make perl syntax check for all args files Index: regress/usr.sbin/httpd/tests/args-tls-verify.pl =================================================================== RCS file: regress/usr.sbin/httpd/tests/args-tls-verify.pl diff -N regress/usr.sbin/httpd/tests/args-tls-verify.pl --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ regress/usr.sbin/httpd/tests/args-tls-verify.pl 21 Jul 2017 08:25:57 -0000 @@ -0,0 +1,20 @@ +# test https connection, verifying client cert + +use strict; +use warnings; + +our %args = ( + client => { + tls => 1, + offertlscert => 1, + loggrep => 'Issuer.*/OU=ca/', + }, + httpd => { + listentls => 1, + verifytls => 1, + }, + len => 512, + md5 => path_md5("512") +); + +1;