Hi there, A year or two ago I submitted a patch for adding TLS client certificate validation to relayd. At the time it didn't make it in, and I stopped pursuing it further. (https://marc.info/?l=openbsd-tech&m=154509330608643&w=2)
I'd still like to see this landed, if at all possible. I'm continuing to use this feature on my own personal websites, and it works well. The latest diff is attached, or can be viewed online here: https://github.com/openbsd/src/compare/master...kivikakk:relayd-client-verification.patch I've added a test that confirms client failure to connect without a certificate at regress/usr.sbin/relayd/args-ssl-client-verify-fail.pl -- it's a bit awkward. Let me know if I can redo it better. Best, Ashe --- >From c63bca7ba7889b43e0a9317e807499eb8ca0db55 Mon Sep 17 00:00:00 2001 From: Asherah Connor <a...@kivikakk.ee> Date: Thu, 15 Oct 2020 17:23:15 +1100 Subject: [PATCH] TLS client certificate validation --- regress/usr.sbin/relayd/Client.pm | 13 ++++++++++ regress/usr.sbin/relayd/Makefile | 18 ++++++++++++- regress/usr.sbin/relayd/Relayd.pm | 3 +++ .../relayd/args-ssl-client-verify-fail.pl | 25 +++++++++++++++++++ .../usr.sbin/relayd/args-ssl-client-verify.pl | 19 ++++++++++++++ usr.sbin/relayd/config.c | 21 ++++++++++++++++ usr.sbin/relayd/parse.y | 15 ++++++++++- usr.sbin/relayd/relay.c | 21 ++++++++++++++++ usr.sbin/relayd/relayd.c | 9 +++++++ usr.sbin/relayd/relayd.conf.5 | 4 +++ usr.sbin/relayd/relayd.h | 14 +++++++---- 11 files changed, 155 insertions(+), 7 deletions(-) create mode 100644 regress/usr.sbin/relayd/args-ssl-client-verify-fail.pl create mode 100644 regress/usr.sbin/relayd/args-ssl-client-verify.pl diff --git a/regress/usr.sbin/relayd/Client.pm b/regress/usr.sbin/relayd/Client.pm index b3e011de13d..ec6fa64274e 100644 --- a/regress/usr.sbin/relayd/Client.pm +++ b/regress/usr.sbin/relayd/Client.pm @@ -57,6 +57,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} ? + "client.crt" : "", + SSL_key_file => $self->{offertlscert} ? + "client.key" : "", ) or die ref($self), " $iosocket socket connect failed: $!,$SSL_ERROR"; if ($self->{sndbuf}) { setsockopt($cs, SOL_SOCKET, SO_SNDBUF, @@ -86,6 +91,14 @@ sub child { print STDERR "ssl cipher: ",$cs->get_cipher(),"\n"; print STDERR "ssl peer certificate:\n", $cs->dump_peer_certificate(); + + if ($self->{offertlscert}) { + print STDERR "ssl client certificate:\n"; + print STDERR "Subject Name: ", + "${\$cs->sock_certificate('subject')}\n"; + print STDERR "Issuer Name: ", + "${\$cs->sock_certificate('issuer')}\n"; + } } *STDIN = *STDOUT = $self->{cs} = $cs; diff --git a/regress/usr.sbin/relayd/Makefile b/regress/usr.sbin/relayd/Makefile index cd01aa3fb63..f2198f43cc9 100644 --- a/regress/usr.sbin/relayd/Makefile +++ b/regress/usr.sbin/relayd/Makefile @@ -96,7 +96,23 @@ server.req: 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*ssl*} ${REGRESS_TARGETS:M*https*}: server.crt +client-ca.crt: + openssl req -batch -new \ + -subj /L=OpenBSD/O=relayd-regress/OU=client-ca/CN=root/ \ + -nodes -newkey rsa -keyout client-ca.key -x509 \ + -out client-ca.crt + +client.req: + openssl req -batch -new \ + -subj /L=OpenBSD/O=relayd-regress/OU=client/CN=localhost/ \ + -nodes -newkey rsa -keyout client.key \ + -out client.req + +client.crt: client-ca.crt client.req + openssl x509 -CAcreateserial -CAkey client-ca.key -CA client-ca.crt \ + -req -in client.req -out client.crt + +${REGRESS_TARGETS:M*ssl*} ${REGRESS_TARGETS:M*https*}: server.crt client.crt .if empty (REMOTE_SSH) ${REGRESS_TARGETS:M*ssl*} ${REGRESS_TARGETS:M*https*}: 127.0.0.1.crt .else diff --git a/regress/usr.sbin/relayd/Relayd.pm b/regress/usr.sbin/relayd/Relayd.pm index 98f2ada5db9..896c0b401be 100644 --- a/regress/usr.sbin/relayd/Relayd.pm +++ b/regress/usr.sbin/relayd/Relayd.pm @@ -84,6 +84,9 @@ sub new { print $fh "\n\ttls ca cert ca.crt"; print $fh "\n\ttls ca key ca.key password ''"; } + if ($self->{verifyclient}) { + print $fh "\n\ttls client ca client-ca.crt"; + } # substitute variables in config file foreach (@protocol) { s/(\$[a-z]+)/$1/eeg; diff --git a/regress/usr.sbin/relayd/args-ssl-client-verify-fail.pl b/regress/usr.sbin/relayd/args-ssl-client-verify-fail.pl new file mode 100644 index 00000000000..93c9b22a63d --- /dev/null +++ b/regress/usr.sbin/relayd/args-ssl-client-verify-fail.pl @@ -0,0 +1,25 @@ +# test client ssl certificate verification + +use strict; +use warnings; + +our %args = ( + client => { + ssl => 1, + offertlscert => 0, + up => 'failed', + down => 'connect attempt failed', + dryrun => 1, + nocheck => 1, + }, + relayd => { + listenssl => 1, + verifyclient => 1, + }, + server => { + noserver => 1, + nocheck => 1, + }, +); + +1; diff --git a/regress/usr.sbin/relayd/args-ssl-client-verify.pl b/regress/usr.sbin/relayd/args-ssl-client-verify.pl new file mode 100644 index 00000000000..c5cc3110724 --- /dev/null +++ b/regress/usr.sbin/relayd/args-ssl-client-verify.pl @@ -0,0 +1,19 @@ +# test client ssl certificate verification + +use strict; +use warnings; + +our %args = ( + client => { + ssl => 1, + offertlscert => 1, + }, + relayd => { + listenssl => 1, + verifyclient => 1, + }, + len => 251, + md5 => "bc3a3f39af35fe5b1687903da2b00c7f", +); + +1; diff --git a/usr.sbin/relayd/config.c b/usr.sbin/relayd/config.c index 3e60d63ef52..5b409aa1134 100644 --- a/usr.sbin/relayd/config.c +++ b/usr.sbin/relayd/config.c @@ -956,6 +956,15 @@ config_setrelay(struct relayd *env, struct relay *rlay) rlay->rl_conf.name); return (-1); } + if (rlay->rl_tls_client_ca_fd != -1 && + config_setrelayfd(ps, id, n, 0, + rlay->rl_conf.id, RELAY_FD_CLIENTCACERT, + rlay->rl_tls_client_ca_fd) == -1) { + log_warn("%s: fd passing failed for " + "`%s'", __func__, + rlay->rl_conf.name); + return (-1); + } /* Prevent fd exhaustion in the parent. */ if (proc_flush_imsg(ps, id, n) == -1) { log_warn("%s: failed to flush " @@ -989,6 +998,10 @@ config_setrelay(struct relayd *env, struct relay *rlay) close(rlay->rl_s); rlay->rl_s = -1; } + if (rlay->rl_tls_client_ca_fd != -1) { + close(rlay->rl_tls_client_ca_fd); + rlay->rl_tls_client_ca_fd = -1; + } if (rlay->rl_tls_cacert_fd != -1) { close(rlay->rl_tls_cacert_fd); rlay->rl_tls_cacert_fd = -1; @@ -1014,6 +1027,10 @@ config_setrelay(struct relayd *env, struct relay *rlay) cert->cert_ocsp_fd = -1; } } + if (rlay->rl_tls_client_ca_fd != -1) { + close(rlay->rl_tls_client_ca_fd); + rlay->rl_tls_client_ca_fd = -1; + } return (0); } @@ -1036,6 +1053,7 @@ config_getrelay(struct relayd *env, struct imsg *imsg) rlay->rl_s = imsg->fd; rlay->rl_tls_ca_fd = -1; rlay->rl_tls_cacert_fd = -1; + rlay->rl_tls_client_ca_fd = -1; if (ps->ps_what[privsep_process] & CONFIG_PROTOS) { if (rlay->rl_conf.proto == EMPTY_ID) @@ -1165,6 +1183,9 @@ config_getrelayfd(struct relayd *env, struct imsg *imsg) case RELAY_FD_CAFILE: rlay->rl_tls_cacert_fd = imsg->fd; break; + case RELAY_FD_CLIENTCACERT: + rlay->rl_tls_client_ca_fd = imsg->fd; + break; } DPRINTF("%s: %s %d received relay fd %d type %d for relay %s", __func__, diff --git a/usr.sbin/relayd/parse.y b/usr.sbin/relayd/parse.y index ce219245731..a524447aa72 100644 --- a/usr.sbin/relayd/parse.y +++ b/usr.sbin/relayd/parse.y @@ -179,7 +179,7 @@ typedef struct { %token TIMEOUT TLS TO ROUTER RTLABEL TRANSPARENT TRAP URL WITH TTL RTABLE %token MATCH PARAMS RANDOM LEASTSTATES SRCHASH KEY CERTIFICATE PASSWORD ECDHE %token EDH TICKETS CONNECTION CONNECTIONS CONTEXT ERRORS STATE CHANGES CHECKS -%token WEBSOCKETS +%token WEBSOCKETS CLIENT %token <v.string> STRING %token <v.number> NUMBER %type <v.string> context hostname interface table value optstring path @@ -1373,6 +1373,16 @@ tlsflags : SESSION TICKETS { proto->tickets = 1; } name->name = $2; TAILQ_INSERT_TAIL(&proto->tlscerts, name, entry); } + | CLIENT CA STRING { + if (strlcpy(proto->tlsclientca, $3, + sizeof(proto->tlsclientca)) >= + sizeof(proto->tlsclientca)) { + yyerror("tlsclientca truncated"); + free($3); + YYERROR; + } + free($3); + } | NO flag { proto->tlsflags &= ~($2); } | flag { proto->tlsflags |= $1; } ; @@ -1830,6 +1840,7 @@ relay : RELAY STRING { r->rl_conf.dstretry = 0; r->rl_tls_ca_fd = -1; r->rl_tls_cacert_fd = -1; + r->rl_tls_client_ca_fd = -1; TAILQ_INIT(&r->rl_tables); if (last_relay_id == INT_MAX) { yyerror("too many relays defined"); @@ -2423,6 +2434,7 @@ lookup(char *s) { "check", CHECK }, { "checks", CHECKS }, { "ciphers", CIPHERS }, + { "client", CLIENT }, { "code", CODE }, { "connection", CONNECTION }, { "context", CONTEXT }, @@ -3408,6 +3420,7 @@ relay_inherit(struct relay *ra, struct relay *rb) if (!(rb->rl_conf.flags & F_TLS)) { rb->rl_tls_cacert_fd = -1; rb->rl_tls_ca_fd = -1; + rb->rl_tls_client_ca_fd = -1; } TAILQ_INIT(&rb->rl_tables); diff --git a/usr.sbin/relayd/relay.c b/usr.sbin/relayd/relay.c index 43b5c377fa5..2f9f293368c 100644 --- a/usr.sbin/relayd/relay.c +++ b/usr.sbin/relayd/relay.c @@ -2259,6 +2259,27 @@ relay_tls_ctx_create(struct relay *rlay) } rlay->rl_tls_cacert_fd = -1; + if (rlay->rl_tls_client_ca_fd != -1) { + if ((buf = relay_load_fd(rlay->rl_tls_client_ca_fd, + &len)) == + NULL) { + log_warn( + "failed to read tls client CA certificate"); + goto err; + } + + if (tls_config_set_ca_mem(tls_cfg, buf, len) != 0) { + log_warnx( + "failed to set tls client CA cert: %s", + tls_config_error(tls_cfg)); + goto err; + } + purge_key(&buf, len); + + tls_config_verify_client(tls_cfg); + } + rlay->rl_tls_client_ca_fd = -1; + tls = tls_server(); if (tls == NULL) { log_warnx("unable to allocate TLS context"); diff --git a/usr.sbin/relayd/relayd.c b/usr.sbin/relayd/relayd.c index 9390b2e4606..52093f6cdc9 100644 --- a/usr.sbin/relayd/relayd.c +++ b/usr.sbin/relayd/relayd.c @@ -1359,6 +1359,15 @@ relay_load_certfiles(struct relayd *env, struct relay *rlay, const char *name) if ((rlay->rl_conf.flags & F_TLS) == 0) return (0); + if (strlen(proto->tlsclientca) && + rlay->rl_tls_client_ca_fd == -1) { + if ((rlay->rl_tls_client_ca_fd = + open(proto->tlsclientca, O_RDONLY)) == -1) + return (-1); + log_debug("%s: using client ca %s", __func__, + proto->tlsclientca); + } + if (name == NULL && print_host(&rlay->rl_conf.ss, hbuf, sizeof(hbuf)) == NULL) goto fail; diff --git a/usr.sbin/relayd/relayd.conf.5 b/usr.sbin/relayd/relayd.conf.5 index 491fc216b71..fe6dff92174 100644 --- a/usr.sbin/relayd/relayd.conf.5 +++ b/usr.sbin/relayd/relayd.conf.5 @@ -946,6 +946,10 @@ will be used (strong crypto cipher suites without anonymous DH). See the CIPHERS section of .Xr openssl 1 for information about SSL/TLS cipher suites and preference lists. +.It Ic client ca Ar path +Require TLS client certificates whose authenticity can be verified +against the CA certificate(s) in the specified file in order to +proceed beyond the TLS handshake. .It Ic client-renegotiation Allow client-initiated renegotiation. To mitigate a potential DoS risk, diff --git a/usr.sbin/relayd/relayd.h b/usr.sbin/relayd/relayd.h index 354c4e7d01f..5d7c71dcbff 100644 --- a/usr.sbin/relayd/relayd.h +++ b/usr.sbin/relayd/relayd.h @@ -139,11 +139,12 @@ struct ctl_relaytable { }; enum fd_type { - RELAY_FD_CERT = 1, - RELAY_FD_CACERT = 2, - RELAY_FD_CAFILE = 3, - RELAY_FD_KEY = 4, - RELAY_FD_OCSP = 5 + RELAY_FD_CERT = 1, + RELAY_FD_CACERT = 2, + RELAY_FD_CAFILE = 3, + RELAY_FD_KEY = 4, + RELAY_FD_OCSP = 5, + RELAY_FD_CLIENTCACERT = 6 }; struct ctl_relayfd { @@ -401,6 +402,7 @@ union hashkey { #define F_TLSINSPECT 0x04000000 #define F_HASHKEY 0x08000000 #define F_AGENTX_TRAPONLY 0x10000000 +#define F_TLSVERIFY 0x20000000 #define F_BITS \ "\10\01DISABLE\02BACKUP\03USED\04DOWN\05ADD\06DEL\07CHANGED" \ @@ -744,6 +746,7 @@ struct protocol { char tlscacert[PATH_MAX]; char tlscakey[PATH_MAX]; char *tlscapass; + char tlsclientca[PATH_MAX]; struct keynamelist tlscerts; char name[MAX_NAME_SIZE]; int tickets; @@ -833,6 +836,7 @@ struct relay { int rl_tls_ca_fd; int rl_tls_cacert_fd; + int rl_tls_client_ca_fd; EVP_PKEY *rl_tls_pkey; X509 *rl_tls_cacertx509; char *rl_tls_cakey;