nfs-over-tls: add user space daemons rpc.tlsclntd and rpc.tlsservd

The kernel changes needed for nfs-over-tls have been committed to main.
However, nfs-over-tls requires user space daemons to handle the
TLS handshake and other non-application data TLS records.
There is one daemon (rpc.tlsclntd) for the client side and one daemon
(rpc.tlsservd) for the server side, although they share a fair amount
of code found in rpc.tlscommon.c and rpc.tlscommon.h.
They use a KTLS enabled OpenSSL to perform the actual work and, as such,
are only built when MK_OPENSSL_KTLS is set.
Communication with the kernel is done via upcall RPCs done on AF_LOCAL
sockets and the custom system call rpctls_syscall.

Reviewed by:	gbe (man pages only), jhb (usr.sbin/Makefile only)
Comments by:	jhb
MFC after:	2 weeks
Differential Revision:	https://reviews.freebsd.org/D28430
Relnotes:	yes
This commit is contained in:
Rick Macklem 2021-02-18 14:08:19 -08:00
parent c67a2909a6
commit b9cbc85d72
9 changed files with 2588 additions and 0 deletions

View file

@ -182,6 +182,8 @@ SUBDIR.${MK_NIS}+= ypserv
SUBDIR.${MK_NIS}+= ypset
SUBDIR.${MK_NTP}+= ntp
SUBDIR.${MK_OPENSSL}+= keyserv
SUBDIR.${MK_OPENSSL_KTLS}+= rpc.tlsclntd
SUBDIR.${MK_OPENSSL_KTLS}+= rpc.tlsservd
SUBDIR.${MK_PF}+= ftp-proxy
SUBDIR.${MK_PKGBOOTSTRAP}+= pkg
SUBDIR.${MK_PMC}+= pmc pmcannotate pmccontrol pmcstat pmcstudy

View file

@ -0,0 +1,29 @@
# $FreeBSD$
.include <src.opts.mk>
PROG= rpc.tlsclntd
MAN= rpc.tlsclntd.8
SRCS= rpc.tlsclntd.c rpc.tlscommon.c rpctlscd.h rpctlscd_svc.c rpctlscd_xdr.c
CFLAGS+= -I. -I${SRCTOP}/usr.sbin/rpc.tlsservd
LIBADD= ssl crypto util
CLEANFILES= rpctlscd_svc.c rpctlscd_xdr.c rpctlscd.h
RPCSRC= ${SRCTOP}/sys/rpc/rpcsec_tls/rpctlscd.x
RPCGEN= RPCGEN_CPP=${CPP:Q} rpcgen -L -C -M
rpctlscd_svc.c: ${RPCSRC} rpctlscd.h
${RPCGEN} -m -o ${.TARGET} ${RPCSRC}
rpctlscd_xdr.c: ${RPCSRC} rpctlscd.h
${RPCGEN} -c -o ${.TARGET} ${RPCSRC}
rpctlscd.h: ${RPCSRC}
${RPCGEN} -h -o ${.TARGET} ${RPCSRC}
.PATH: ${SRCTOP}/sys/rpc/rpcsec_tls ${SRCTOP}/usr.sbin/rpc.tlsservd
.include <bsd.prog.mk>

View file

@ -0,0 +1,201 @@
.\" Copyright (c) 2008 Isilon Inc http://www.isilon.com/
.\" Authors: Doug Rabson <dfr@rabson.org>
.\" Developed with Red Inc: Alfred Perlstein <alfred@FreeBSD.org>
.\"
.\" Redistribution and use in source and binary forms, with or without
.\" modification, are permitted provided that the following conditions
.\" are met:
.\" 1. Redistributions of source code must retain the above copyright
.\" notice, this list of conditions and the following disclaimer.
.\" 2. Redistributions in binary form must reproduce the above copyright
.\" notice, this list of conditions and the following disclaimer in the
.\" documentation and/or other materials provided with the distribution.
.\"
.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
.\" $FreeBSD$
.\"
.\" Modified from gssd.8 for rpc.tlsclntd.8 by Rick Macklem.
.Dd February 17, 2021
.Dt RPC.TLSCLNTD 8
.Os
.Sh NAME
.Nm rpc.tlsclntd
.Nd "Sun RPC over TLS Client Daemon"
.Sh SYNOPSIS
.Nm
.Op Fl C Ar preferred_ciphers
.Op Fl D Ar certdir
.Op Fl d
.Op Fl l Ar CAfile
.Op Fl m
.Op Fl p Ar CApath
.Op Fl r Ar CRLfile
.Op Fl v
.Sh DESCRIPTION
The
.Nm
program provides support for the client side of the kernel Sun RPC over TLS
implementation.
This daemon must be running for the kernel RPC to be able to do a TLS
connection to a server for an NFS over TLS mount.
This daemon requires that the kernel be built with
.Dq options KERNEL_TLS
and be running on an architecture such as
.Dq amd64
that supports a direct map (not i386) with
.Xr ktls 4
enabled.
.Pp
If either of the
.Fl l
or
.Fl p
options have been specified, the daemon will require the server's
certificate to verify
and have a Fully Qualified Domain Name (FQDN) in it.
This FQDN must match
the reverse DNS name for the IP address that
the server is using for the TCP connection.
The FQDN may be
in either the DNS field of the subjectAltName or the CN field of the
subjectName in the certificate and
cannot have a wildcard
.Dq *
in it.
.Pp
If a SIGHUP signal is sent to the daemon it will reload the
.Dq CRLfile
and will shut down any extant connections that presented certificates
during TLS handshake that have been revoked.
If the
.Fl r
option was not specified, the SIGHUP signal will be ignored.
.Pp
The daemon will log failed certificate verifications via
.Xr syslogd 8
using LOG_INFO | LOG_DAEMON when the
.Fl l
or
.Fl p
option has been specified.
.Pp
The options are as follows:
.Bl -tag -width indent
.It Fl C Ar preferred_ciphers , Fl Fl ciphers= Ns Ar preferred_ciphers
Specify what preferred ciphers are to be used.
If this option is specified,
.Dq SSL_CTX_set_cipher_list()
will be called with
.Dq preferred_ciphers
as the argument.
If this option is not specified, the cipher will be chosen by
.Xr ssl 7 .
.It Fl D Ar certdir , Fl Fl certdir= Ns Ar certdir
Use
.Dq certdir
instead of /etc/rpc.tlsclntd for the
.Fl m
option.
.It Fl d , Fl Fl debuglevel
Run in debug mode.
In this mode,
.Nm
will not fork when it starts.
.It Fl l Ar CAfile , Fl Fl verifylocs= Ns Ar CAfile
This specifies the path name of a CAfile which holds the information
for server certificate verification.
This path name is used in
.Dq SSL_CTX_load_verify_locations(ctx,CAfile,NULL)
and
.Dq SSL_CTX_set0_CA_list(ctx,SSL_load_client_CA_file(CAfile))
openssl library calls.
Note that this is a path name for the file and is not assumed to be
in
.Dq certdir .
.It Fl m , Fl Fl mutualverf
Enable support for mutual authentication.
A certificate and associated key must be found in /etc/rpc.tlsclntd
(or the directory specified by the
.Fl D
option)
in case a server requests a peer certificate.
The first certificate needs to be in a file named
.Dq cert.pem
and the associated key in a file named
.Dq certkey.pem .
The
.Xr mount_nfs 8
option
.Fl tlscertname
can be used to override the default certificate for a given
NFS mount, where the files use the alternate naming specified by the option.
If there is a passphrase on the
.Dq certkey.pem
file, this daemon will prompt for the passphrase during startup.
The keys for alternate certificates cannot have passphrases.
.It Fl p Ar CApath , Fl Fl verifydir= Ns Ar CApath
This option is similar to the
.Fl l
option, but specifies the path of a directory with CA
certificates in it.
When this option is used,
.Dq SSL_CTX_set0_CA_list(ctx,SSL_load_client_CA_file())
is not called, so a list of CA names is not be passed
to the server during the TLS handshake.
The openssl documentation indicates this call is rarely needed.
.It Fl r Ar CRLfile , Fl Fl crl= Ns Ar CRLfile
This option specifies a Certificate Revocation List (CRL) file
that is to be loaded into the verify certificate store and
checked during verification of the server's certificate.
This option is meaningless unless either the
.Fl l
or
.Fl p
have been specified.
.It Fl v , Fl Fl verbose
Run in verbose mode.
In this mode,
.Nm
will log activity messages to syslog using LOG_INFO | LOG_DAEMON or to
stderr, if the
.Fl d
option has also been specified.
.El
.Sh EXIT STATUS
.Ex -std
.Sh SEE ALSO
.Xr openssl 1 ,
.Xr ktls 4 ,
.Xr mount_nfs 8 ,
.Xr rpc.tlsservd 8 ,
.Xr ssl 7 ,
.Xr syslogd 8
.Sh STANDARDS
The implementation is based on the specification in
.Rs
.%B "RFC NNNN"
.%T "Towards Remote Procedure Call Encryption By Default"
.Re
.Sh HISTORY
The
.Nm
manual page first appeared in
.Fx 13.0 .
.Sh BUGS
This daemon cannot be safely shut down and restarted if there are
any active RPC-over-TLS connections.
Doing so will orphan the KERNEL_TLS connections, so that they
can no longer do upcalls successfully, since the
.Dq SSL *
structures in userspace have been lost.

View file

@ -0,0 +1,730 @@
/*-
* SPDX-License-Identifier: BSD-2-Clause-FreeBSD
*
* Copyright (c) 2008 Isilon Inc http://www.isilon.com/
* Authors: Doug Rabson <dfr@rabson.org>
* Developed with Red Inc: Alfred Perlstein <alfred@freebsd.org>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
/*
* Extensively modified from /usr/src/usr.sbin/gssd.c r344402 for
* the client side of kernel RPC-over-TLS by Rick Macklem.
*/
#include <sys/cdefs.h>
__FBSDID("$FreeBSD$");
#include <sys/param.h>
#include <sys/types.h>
#include <sys/queue.h>
#include <sys/linker.h>
#include <sys/module.h>
#include <sys/stat.h>
#include <sys/sysctl.h>
#include <sys/syslog.h>
#include <sys/time.h>
#include <err.h>
#include <getopt.h>
#include <libutil.h>
#include <netdb.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <rpc/rpc.h>
#include <rpc/rpc_com.h>
#include <rpc/rpcsec_tls.h>
#include <openssl/opensslconf.h>
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/x509v3.h>
#include "rpctlscd.h"
#include "rpc.tlscommon.h"
#ifndef _PATH_RPCTLSCDSOCK
#define _PATH_RPCTLSCDSOCK "/var/run/rpc.tlsclntd.sock"
#endif
#ifndef _PATH_CERTANDKEY
#define _PATH_CERTANDKEY "/etc/rpc.tlsclntd/"
#endif
#ifndef _PATH_RPCTLSCDPID
#define _PATH_RPCTLSCDPID "/var/run/rpc.tlsclntd.pid"
#endif
/* Global variables also used by rpc.tlscommon.c. */
int rpctls_debug_level;
bool rpctls_verbose;
SSL_CTX *rpctls_ctx = NULL;
const char *rpctls_verify_cafile = NULL;
const char *rpctls_verify_capath = NULL;
char *rpctls_crlfile = NULL;
bool rpctls_cert = false;
bool rpctls_gothup = false;
struct ssl_list rpctls_ssllist;
static struct pidfh *rpctls_pfh = NULL;
static const char *rpctls_certdir = _PATH_CERTANDKEY;
static const char *rpctls_ciphers = NULL;
static uint64_t rpctls_ssl_refno = 0;
static uint64_t rpctls_ssl_sec = 0;
static uint64_t rpctls_ssl_usec = 0;
static void rpctlscd_terminate(int);
static SSL_CTX *rpctls_setupcl_ssl(void);
static SSL *rpctls_connect(SSL_CTX *ctx, int s, char *certname,
u_int certlen, X509 **certp);
static void rpctls_huphandler(int sig __unused);
extern void rpctlscd_1(struct svc_req *rqstp, SVCXPRT *transp);
static struct option longopts[] = {
{ "certdir", required_argument, NULL, 'D' },
{ "ciphers", required_argument, NULL, 'C' },
{ "debuglevel", no_argument, NULL, 'd' },
{ "verifylocs", required_argument, NULL, 'l' },
{ "mutualverf", no_argument, NULL, 'm' },
{ "verifydir", required_argument, NULL, 'p' },
{ "crl", required_argument, NULL, 'r' },
{ "verbose", no_argument, NULL, 'v' },
{ NULL, 0, NULL, 0 }
};
int
main(int argc, char **argv)
{
/*
* We provide an RPC service on a local-domain socket. The
* kernel rpctls code will upcall to this daemon to do the initial
* TLS handshake.
*/
struct sockaddr_un sun;
int ch, fd, oldmask;
SVCXPRT *xprt;
bool tls_enable;
struct timeval tm;
struct timezone tz;
pid_t otherpid;
size_t tls_enable_len;
/* Check that another rpctlscd isn't already running. */
rpctls_pfh = pidfile_open(_PATH_RPCTLSCDPID, 0600, &otherpid);
if (rpctls_pfh == NULL) {
if (errno == EEXIST)
errx(1, "rpctlscd already running, pid: %d.", otherpid);
warn("cannot open or create pidfile");
}
/* Check to see that the ktls is enabled. */
tls_enable_len = sizeof(tls_enable);
if (sysctlbyname("kern.ipc.tls.enable", &tls_enable, &tls_enable_len,
NULL, 0) != 0 || !tls_enable)
errx(1, "Kernel TLS not enabled");
/* Get the time when this daemon is started. */
gettimeofday(&tm, &tz);
rpctls_ssl_sec = tm.tv_sec;
rpctls_ssl_usec = tm.tv_usec;
rpctls_verbose = false;
while ((ch = getopt_long(argc, argv, "CD:dl:mp:r:v", longopts, NULL)) !=
-1) {
switch (ch) {
case 'C':
rpctls_ciphers = optarg;
break;
case 'D':
rpctls_certdir = optarg;
break;
case 'd':
rpctls_debug_level++;
break;
case 'l':
rpctls_verify_cafile = optarg;
break;
case 'm':
rpctls_cert = true;
break;
case 'p':
rpctls_verify_capath = optarg;
break;
case 'r':
rpctls_crlfile = optarg;
break;
case 'v':
rpctls_verbose = true;
break;
default:
fprintf(stderr, "usage: %s "
"[-C/--ciphers preferred_ciphers] "
"[-D/--certdir certdir] [-d/--debuglevel] "
"[-l/--verifylocs CAfile] [-m/--mutualverf] "
"[-p/--verifydir CApath] [-r/--crl CRLfile] "
"[-v/--verbose]\n", argv[0]);
exit(1);
break;
}
}
if (rpctls_crlfile != NULL && rpctls_verify_cafile == NULL &&
rpctls_verify_capath == NULL)
errx(1, "-r requires the -l <CAfile> and/or "
"-p <CApath> options");
if (modfind("krpc") < 0) {
/* Not present in kernel, try loading it */
if (kldload("krpc") < 0 || modfind("krpc") < 0)
errx(1, "Kernel RPC is not available");
}
/*
* Set up the SSL_CTX *.
* Do it now, before daemonizing, in case the private key
* is encrypted and requires a passphrase to be entered.
*/
rpctls_ctx = rpctls_setupcl_ssl();
if (rpctls_ctx == NULL) {
if (rpctls_debug_level == 0) {
syslog(LOG_ERR, "Can't set up TLS context");
exit(1);
}
err(1, "Can't set up TLS context");
}
LIST_INIT(&rpctls_ssllist);
if (!rpctls_debug_level) {
if (daemon(0, 0) != 0)
err(1, "Can't daemonize");
signal(SIGINT, SIG_IGN);
signal(SIGQUIT, SIG_IGN);
signal(SIGHUP, SIG_IGN);
}
signal(SIGTERM, rpctlscd_terminate);
signal(SIGPIPE, SIG_IGN);
signal(SIGHUP, rpctls_huphandler);
pidfile_write(rpctls_pfh);
memset(&sun, 0, sizeof sun);
sun.sun_family = AF_LOCAL;
unlink(_PATH_RPCTLSCDSOCK);
strcpy(sun.sun_path, _PATH_RPCTLSCDSOCK);
sun.sun_len = SUN_LEN(&sun);
fd = socket(AF_LOCAL, SOCK_STREAM, 0);
if (fd < 0) {
if (rpctls_debug_level == 0) {
syslog(LOG_ERR, "Can't create local rpctlscd socket");
exit(1);
}
err(1, "Can't create local rpctlscd socket");
}
oldmask = umask(S_IXUSR|S_IRWXG|S_IRWXO);
if (bind(fd, (struct sockaddr *)&sun, sun.sun_len) < 0) {
if (rpctls_debug_level == 0) {
syslog(LOG_ERR, "Can't bind local rpctlscd socket");
exit(1);
}
err(1, "Can't bind local rpctlscd socket");
}
umask(oldmask);
if (listen(fd, SOMAXCONN) < 0) {
if (rpctls_debug_level == 0) {
syslog(LOG_ERR,
"Can't listen on local rpctlscd socket");
exit(1);
}
err(1, "Can't listen on local rpctlscd socket");
}
xprt = svc_vc_create(fd, RPC_MAXDATASIZE, RPC_MAXDATASIZE);
if (!xprt) {
if (rpctls_debug_level == 0) {
syslog(LOG_ERR,
"Can't create transport for local rpctlscd socket");
exit(1);
}
err(1, "Can't create transport for local rpctlscd socket");
}
if (!svc_reg(xprt, RPCTLSCD, RPCTLSCDVERS, rpctlscd_1, NULL)) {
if (rpctls_debug_level == 0) {
syslog(LOG_ERR,
"Can't register service for local rpctlscd socket");
exit(1);
}
err(1, "Can't register service for local rpctlscd socket");
}
rpctls_syscall(RPCTLS_SYSC_CLSETPATH, _PATH_RPCTLSCDSOCK);
rpctls_svc_run();
rpctls_syscall(RPCTLS_SYSC_CLSHUTDOWN, "");
SSL_CTX_free(rpctls_ctx);
EVP_cleanup();
return (0);
}
bool_t
rpctlscd_null_1_svc(__unused void *argp, __unused void *result,
__unused struct svc_req *rqstp)
{
rpctls_verbose_out("rpctlscd_null: done\n");
return (TRUE);
}
bool_t
rpctlscd_connect_1_svc(struct rpctlscd_connect_arg *argp,
struct rpctlscd_connect_res *result, __unused struct svc_req *rqstp)
{
int s;
SSL *ssl;
struct ssl_entry *newslp;
X509 *cert;
rpctls_verbose_out("rpctlsd_connect: started\n");
/* Get the socket fd from the kernel. */
s = rpctls_syscall(RPCTLS_SYSC_CLSOCKET, "");
if (s < 0) {
result->reterr = RPCTLSERR_NOSOCKET;
return (TRUE);
}
/* Do a TLS connect handshake. */
ssl = rpctls_connect(rpctls_ctx, s, argp->certname.certname_val,
argp->certname.certname_len, &cert);
if (ssl == NULL) {
rpctls_verbose_out("rpctlsd_connect: can't do TLS "
"handshake\n");
result->reterr = RPCTLSERR_NOSSL;
} else {
result->reterr = RPCTLSERR_OK;
result->sec = rpctls_ssl_sec;
result->usec = rpctls_ssl_usec;
result->ssl = ++rpctls_ssl_refno;
/* Hard to believe this will ever wrap around.. */
if (rpctls_ssl_refno == 0)
result->ssl = ++rpctls_ssl_refno;
}
if (ssl == NULL) {
/*
* For RPC-over-TLS, this upcall is expected
* to close off the socket.
*/
close(s);
return (TRUE);
}
/* Maintain list of all current SSL *'s */
newslp = malloc(sizeof(*newslp));
newslp->refno = rpctls_ssl_refno;
newslp->s = s;
newslp->shutoff = false;
newslp->ssl = ssl;
newslp->cert = cert;
LIST_INSERT_HEAD(&rpctls_ssllist, newslp, next);
return (TRUE);
}
bool_t
rpctlscd_handlerecord_1_svc(struct rpctlscd_handlerecord_arg *argp,
struct rpctlscd_handlerecord_res *result, __unused struct svc_req *rqstp)
{
struct ssl_entry *slp;
int ret;
char junk;
slp = NULL;
if (argp->sec == rpctls_ssl_sec && argp->usec ==
rpctls_ssl_usec) {
LIST_FOREACH(slp, &rpctls_ssllist, next) {
if (slp->refno == argp->ssl)
break;
}
}
if (slp != NULL) {
rpctls_verbose_out("rpctlscd_handlerecord fd=%d\n",
slp->s);
/*
* An SSL_read() of 0 bytes should fail, but it should
* handle the non-application data record before doing so.
*/
ret = SSL_read(slp->ssl, &junk, 0);
if (ret <= 0) {
/* Check to see if this was a close alert. */
ret = SSL_get_shutdown(slp->ssl);
if ((ret & (SSL_SENT_SHUTDOWN |
SSL_RECEIVED_SHUTDOWN)) == SSL_RECEIVED_SHUTDOWN)
SSL_shutdown(slp->ssl);
} else {
if (rpctls_debug_level == 0)
syslog(LOG_ERR, "SSL_read returned %d", ret);
else
fprintf(stderr, "SSL_read returned %d\n", ret);
}
result->reterr = RPCTLSERR_OK;
} else
result->reterr = RPCTLSERR_NOSSL;
return (TRUE);
}
bool_t
rpctlscd_disconnect_1_svc(struct rpctlscd_disconnect_arg *argp,
struct rpctlscd_disconnect_res *result, __unused struct svc_req *rqstp)
{
struct ssl_entry *slp;
int ret;
slp = NULL;
if (argp->sec == rpctls_ssl_sec && argp->usec ==
rpctls_ssl_usec) {
LIST_FOREACH(slp, &rpctls_ssllist, next) {
if (slp->refno == argp->ssl)
break;
}
}
if (slp != NULL) {
rpctls_verbose_out("rpctlscd_disconnect: fd=%d closed\n",
slp->s);
LIST_REMOVE(slp, next);
if (!slp->shutoff) {
ret = SSL_get_shutdown(slp->ssl);
/*
* Do an SSL_shutdown() unless a close alert has
* already been sent.
*/
if ((ret & SSL_SENT_SHUTDOWN) == 0)
SSL_shutdown(slp->ssl);
}
SSL_free(slp->ssl);
if (slp->cert != NULL)
X509_free(slp->cert);
/*
* For RPC-over-TLS, this upcall is expected
* to close off the socket.
*/
if (!slp->shutoff)
shutdown(slp->s, SHUT_WR);
close(slp->s);
free(slp);
result->reterr = RPCTLSERR_OK;
} else
result->reterr = RPCTLSERR_NOCLOSE;
return (TRUE);
}
int
rpctlscd_1_freeresult(__unused SVCXPRT *transp, __unused xdrproc_t xdr_result,
__unused caddr_t result)
{
return (TRUE);
}
static void
rpctlscd_terminate(int sig __unused)
{
rpctls_syscall(RPCTLS_SYSC_CLSHUTDOWN, "");
pidfile_remove(rpctls_pfh);
exit(0);
}
static SSL_CTX *
rpctls_setupcl_ssl(void)
{
SSL_CTX *ctx;
long flags;
char path[PATH_MAX];
size_t len, rlen;
int ret;
SSL_library_init();
SSL_load_error_strings();
OpenSSL_add_all_algorithms();
ctx = SSL_CTX_new(TLS_client_method());
if (ctx == NULL) {
rpctls_verbose_out("rpctls_setupcl_ssl: SSL_CTX_new "
"failed\n");
return (NULL);
}
SSL_CTX_set_ecdh_auto(ctx, 1);
if (rpctls_ciphers != NULL) {
/*
* Set preferred ciphers, since KERN_TLS only supports a
* few of them.
*/
ret = SSL_CTX_set_cipher_list(ctx, rpctls_ciphers);
if (ret == 0) {
rpctls_verbose_out("rpctls_setupcl_ssl: "
"SSL_CTX_set_cipher_list failed: %s\n",
rpctls_ciphers);
SSL_CTX_free(ctx);
return (NULL);
}
}
/*
* If rpctls_cert is true, a certificate and key exists in
* rpctls_certdir, so that it can do mutual authentication.
*/
if (rpctls_cert) {
/* Get the cert.pem and certkey.pem files. */
len = strlcpy(path, rpctls_certdir, sizeof(path));
rlen = sizeof(path) - len;
if (strlcpy(&path[len], "cert.pem", rlen) != 8) {
SSL_CTX_free(ctx);
return (NULL);
}
ret = SSL_CTX_use_certificate_file(ctx, path,
SSL_FILETYPE_PEM);
if (ret != 1) {
rpctls_verbose_out("rpctls_setupcl_ssl: can't use "
"certificate file path=%s ret=%d\n", path, ret);
SSL_CTX_free(ctx);
return (NULL);
}
if (strlcpy(&path[len], "certkey.pem", rlen) != 11) {
SSL_CTX_free(ctx);
return (NULL);
}
ret = SSL_CTX_use_PrivateKey_file(ctx, path,
SSL_FILETYPE_PEM);
if (ret != 1) {
rpctls_verbose_out("rpctls_setupcl_ssl: Can't use "
"private key path=%s ret=%d\n", path, ret);
SSL_CTX_free(ctx);
return (NULL);
}
}
if (rpctls_verify_cafile != NULL || rpctls_verify_capath != NULL) {
if (rpctls_crlfile != NULL) {
ret = rpctls_loadcrlfile(ctx);
if (ret == 0) {
rpctls_verbose_out("rpctls_setupcl_ssl: "
"Load CRLfile failed\n");
SSL_CTX_free(ctx);
return (NULL);
}
}
#if OPENSSL_VERSION_NUMBER >= 0x30000000
ret = 1;
if (rpctls_verify_cafile != NULL)
ret = SSL_CTX_load_verify_file(ctx,
rpctls_verify_cafile);
if (ret != 0 && rpctls_verify_capath != NULL)
ret = SSL_CTX_load_verify_dir(ctx,
rpctls_verify_capath);
#else
ret = SSL_CTX_load_verify_locations(ctx,
rpctls_verify_cafile, rpctls_verify_capath);
#endif
if (ret == 0) {
rpctls_verbose_out("rpctls_setupcl_ssl: "
"Can't load verify locations\n");
SSL_CTX_free(ctx);
return (NULL);
}
/*
* The man page says that the
* SSL_CTX_set0_CA_list() call is not normally
* needed, but I believe it is harmless.
*/
if (rpctls_verify_cafile != NULL)
SSL_CTX_set0_CA_list(ctx,
SSL_load_client_CA_file(rpctls_verify_cafile));
}
/* RPC-over-TLS must use TLSv1.3, according to the IETF draft.*/
#ifdef notyet
flags = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1 |
SSL_OP_NO_TLSv1_1 | SSL_OP_NO_TLSv1_2;
#else
flags = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1_3;
#endif
SSL_CTX_set_options(ctx, flags);
SSL_CTX_clear_mode(ctx, SSL_MODE_NO_KTLS_TX | SSL_MODE_NO_KTLS_RX);
return (ctx);
}
static SSL *
rpctls_connect(SSL_CTX *ctx, int s, char *certname, u_int certlen, X509 **certp)
{
SSL *ssl;
X509 *cert;
struct sockaddr_storage ad;
struct sockaddr *sad;
char hostnam[NI_MAXHOST], path[PATH_MAX];
int gethostret, ret;
char *cp, *cp2;
size_t len, rlen;
long verfret;
*certp = NULL;
sad = (struct sockaddr *)&ad;
ssl = SSL_new(ctx);
if (ssl == NULL) {
rpctls_verbose_out("rpctls_connect: "
"SSL_new failed\n");
return (NULL);
}
if (SSL_set_fd(ssl, s) != 1) {
rpctls_verbose_out("rpctls_connect: "
"SSL_set_fd failed\n");
SSL_free(ssl);
return (NULL);
}
/*
* If rpctls_cert is true and certname is set, a alternate certificate
* and key exists in files named <certname>.pem and <certname>key.pem
* in rpctls_certdir that is to be used for mutual authentication.
*/
if (rpctls_cert && certlen > 0) {
len = strlcpy(path, rpctls_certdir, sizeof(path));
rlen = sizeof(path) - len;
if (rlen <= certlen) {
SSL_free(ssl);
return (NULL);
}
memcpy(&path[len], certname, certlen);
rlen -= certlen;
len += certlen;
path[len] = '\0';
if (strlcpy(&path[len], ".pem", rlen) != 4) {
SSL_free(ssl);
return (NULL);
}
ret = SSL_use_certificate_file(ssl, path, SSL_FILETYPE_PEM);
if (ret != 1) {
rpctls_verbose_out("rpctls_connect: can't use "
"certificate file path=%s ret=%d\n", path, ret);
SSL_free(ssl);
return (NULL);
}
if (strlcpy(&path[len], "key.pem", rlen) != 7) {
SSL_free(ssl);
return (NULL);
}
ret = SSL_use_PrivateKey_file(ssl, path, SSL_FILETYPE_PEM);
if (ret != 1) {
rpctls_verbose_out("rpctls_connect: Can't use "
"private key path=%s ret=%d\n", path, ret);
SSL_free(ssl);
return (NULL);
}
}
ret = SSL_connect(ssl);
if (ret != 1) {
rpctls_verbose_out("rpctls_connect: "
"SSL_connect failed %d\n",
ret);
SSL_free(ssl);
return (NULL);
}
cert = SSL_get_peer_certificate(ssl);
if (cert == NULL) {
rpctls_verbose_out("rpctls_connect: get peer"
" certificate failed\n");
SSL_free(ssl);
return (NULL);
}
gethostret = rpctls_gethost(s, sad, hostnam, sizeof(hostnam));
if (gethostret == 0)
hostnam[0] = '\0';
verfret = SSL_get_verify_result(ssl);
if (verfret == X509_V_OK && (rpctls_verify_cafile != NULL ||
rpctls_verify_capath != NULL) && (gethostret == 0 ||
rpctls_checkhost(sad, cert, X509_CHECK_FLAG_NO_WILDCARDS) != 1))
verfret = X509_V_ERR_HOSTNAME_MISMATCH;
if (verfret != X509_V_OK && (rpctls_verify_cafile != NULL ||
rpctls_verify_capath != NULL)) {
if (verfret != X509_V_OK) {
cp = X509_NAME_oneline(X509_get_issuer_name(cert),
NULL, 0);
cp2 = X509_NAME_oneline(X509_get_subject_name(cert),
NULL, 0);
if (rpctls_debug_level == 0)
syslog(LOG_INFO | LOG_DAEMON,
"rpctls_connect: client IP %s "
"issuerName=%s subjectName=%s verify "
"failed %s\n", hostnam, cp, cp2,
X509_verify_cert_error_string(verfret));
else
fprintf(stderr,
"rpctls_connect: client IP %s "
"issuerName=%s subjectName=%s verify "
"failed %s\n", hostnam, cp, cp2,
X509_verify_cert_error_string(verfret));
}
X509_free(cert);
SSL_free(ssl);
return (NULL);
}
/* Check to see if ktls is enabled on the connection. */
ret = BIO_get_ktls_send(SSL_get_wbio(ssl));
rpctls_verbose_out("rpctls_connect: BIO_get_ktls_send=%d\n", ret);
if (ret != 0) {
ret = BIO_get_ktls_recv(SSL_get_rbio(ssl));
rpctls_verbose_out("rpctls_connect: BIO_get_ktls_recv=%d\n",
ret);
}
if (ret == 0) {
if (rpctls_debug_level == 0)
syslog(LOG_ERR, "ktls not working\n");
else
fprintf(stderr, "ktls not working\n");
X509_free(cert);
SSL_free(ssl);
return (NULL);
}
if (ret == X509_V_OK && (rpctls_verify_cafile != NULL ||
rpctls_verify_capath != NULL) && rpctls_crlfile != NULL)
*certp = cert;
else
X509_free(cert);
return (ssl);
}
static void
rpctls_huphandler(int sig __unused)
{
rpctls_gothup = true;
}

View file

@ -0,0 +1,29 @@
# $FreeBSD$
.include <src.opts.mk>
PROG= rpc.tlsservd
MAN= rpc.tlsservd.8
SRCS= rpc.tlsservd.c rpc.tlscommon.c rpctlssd.h rpctlssd_svc.c rpctlssd_xdr.c
CFLAGS+= -I.
LIBADD= ssl crypto util
CLEANFILES= rpctlssd_svc.c rpctlssd_xdr.c rpctlssd.h
RPCSRC= ${SRCTOP}/sys/rpc/rpcsec_tls/rpctlssd.x
RPCGEN= RPCGEN_CPP=${CPP:Q} rpcgen -L -C -M
rpctlssd_svc.c: ${RPCSRC} rpctlssd.h
${RPCGEN} -m -o ${.TARGET} ${RPCSRC}
rpctlssd_xdr.c: ${RPCSRC} rpctlssd.h
${RPCGEN} -c -o ${.TARGET} ${RPCSRC}
rpctlssd.h: ${RPCSRC}
${RPCGEN} -h -o ${.TARGET} ${RPCSRC}
.PATH: ${SRCTOP}/sys/rpc/rpcsec_tls
.include <bsd.prog.mk>

View file

@ -0,0 +1,295 @@
/*-
* SPDX-License-Identifier: BSD-2-Clause-FreeBSD
*
* Copyright (c) 2021 Rick Macklem
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
*/
#include <sys/cdefs.h>
__FBSDID("$FreeBSD$");
#include <sys/queue.h>
#include <sys/syslog.h>
#include <sys/select.h>
#include <sys/time.h>
#include <netdb.h>
#include <signal.h>
#include <stdarg.h>
#include <stdbool.h>
#include <string.h>
#include <rpc/rpc.h>
#include <openssl/opensslconf.h>
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/x509v3.h>
#include "rpc.tlscommon.h"
/*
* How long to delay a reload of the CRL when there are RPC request(s)
* to process, in usec. Must be less than 1second.
*/
#define RELOADDELAY 250000
void
rpctls_svc_run(void)
{
int ret;
struct timeval tv;
fd_set readfds;
uint64_t curtime, nexttime;
struct timespec tp;
sigset_t sighup_mask;
/* Expand svc_run() here so that we can call rpctls_loadcrlfile(). */
curtime = nexttime = 0;
sigemptyset(&sighup_mask);
sigaddset(&sighup_mask, SIGHUP);
for (;;) {
clock_gettime(CLOCK_MONOTONIC, &tp);
curtime = tp.tv_sec;
curtime = curtime * 1000000 + tp.tv_nsec / 1000;
sigprocmask(SIG_BLOCK, &sighup_mask, NULL);
if (rpctls_gothup && curtime >= nexttime) {
rpctls_gothup = false;
sigprocmask(SIG_UNBLOCK, &sighup_mask, NULL);
ret = rpctls_loadcrlfile(rpctls_ctx);
if (ret != 0)
rpctls_checkcrl();
else
rpctls_verbose_out("rpc.tlsservd: Can't "
"reload CRLfile\n");
clock_gettime(CLOCK_MONOTONIC, &tp);
nexttime = tp.tv_sec;
nexttime = nexttime * 1000000 + tp.tv_nsec / 1000 +
RELOADDELAY;
} else
sigprocmask(SIG_UNBLOCK, &sighup_mask, NULL);
/*
* If a reload is pending, poll for received request(s),
* otherwise set a RELOADDELAY timeout, since a SIGHUP
* could be processed between the got_sighup test and
* the select() system call.
*/
tv.tv_sec = 0;
if (rpctls_gothup)
tv.tv_usec = 0;
else
tv.tv_usec = RELOADDELAY;
readfds = svc_fdset;
switch (select(svc_maxfd + 1, &readfds, NULL, NULL, &tv)) {
case -1:
if (errno == EINTR) {
/* Allow a reload now. */
nexttime = 0;
continue;
}
syslog(LOG_ERR, "rpc.tls daemon died: select: %m");
exit(1);
case 0:
/* Allow a reload now. */
nexttime = 0;
continue;
default:
svc_getreqset(&readfds);
}
}
}
/*
* (re)load the CRLfile into the certificate verification store.
*/
int
rpctls_loadcrlfile(SSL_CTX *ctx)
{
X509_STORE *certstore;
X509_LOOKUP *certlookup;
int ret;
if ((rpctls_verify_cafile != NULL ||
rpctls_verify_capath != NULL) &&
rpctls_crlfile != NULL) {
certstore = SSL_CTX_get_cert_store(ctx);
certlookup = X509_STORE_add_lookup(
certstore, X509_LOOKUP_file());
ret = 0;
if (certlookup != NULL)
ret = X509_load_crl_file(certlookup,
rpctls_crlfile, X509_FILETYPE_PEM);
if (ret != 0)
ret = X509_STORE_set_flags(certstore,
X509_V_FLAG_CRL_CHECK |
X509_V_FLAG_CRL_CHECK_ALL);
if (ret == 0) {
rpctls_verbose_out(
"rpctls_loadcrlfile: Can't"
" load CRLfile=%s\n",
rpctls_crlfile);
return (ret);
}
}
return (1);
}
/*
* Read the CRL file and check for any extant connections
* that might now be revoked.
*/
void
rpctls_checkcrl(void)
{
struct ssl_entry *slp;
BIO *infile;
X509_CRL *crl;
X509_REVOKED *revoked;
char *cp, *cp2, nullstr[1];
int ret;
if (rpctls_crlfile == NULL || (rpctls_verify_cafile == NULL &&
rpctls_verify_capath == NULL))
return;
infile = BIO_new(BIO_s_file());
if (infile == NULL) {
rpctls_verbose_out("rpctls_checkcrl: Cannot BIO_new\n");
return;
}
ret = BIO_read_filename(infile, rpctls_crlfile);
if (ret != 1) {
rpctls_verbose_out("rpctls_checkcrl: Cannot read CRL file\n");
BIO_free(infile);
return;
}
nullstr[0] = '\0';
for (crl = PEM_read_bio_X509_CRL(infile, NULL, NULL, nullstr);
crl != NULL; crl = PEM_read_bio_X509_CRL(infile, NULL, NULL,
nullstr)) {
LIST_FOREACH(slp, &rpctls_ssllist, next) {
if (slp->cert != NULL) {
ret = X509_CRL_get0_by_cert(crl, &revoked,
slp->cert);
/*
* Do a shutdown on the socket, so that it
* can no longer be used. The kernel RPC
* code will notice the socket is disabled
* and will do a disconnect upcall, which will
* close the socket.
*/
if (ret == 1) {
cp2 = X509_NAME_oneline(
X509_get_subject_name(slp->cert),
NULL, 0);
cp = X509_NAME_oneline(
X509_get_issuer_name(slp->cert),
NULL, 0);
if (rpctls_debug_level == 0)
syslog(LOG_INFO | LOG_DAEMON,
"rpctls_daemon: Certificate"
" Revoked "
"issuerName=%s "
"subjectName=%s: "
"TCP connection closed",
cp, cp2);
else
fprintf(stderr,
"rpctls_daemon: Certificate"
" Revoked "
"issuerName=%s "
"subjectName=%s: "
"TCP connection closed",
cp, cp2);
shutdown(slp->s, SHUT_WR);
slp->shutoff = true;
}
}
}
X509_CRL_free(crl);
}
BIO_free(infile);
}
void
rpctls_verbose_out(const char *fmt, ...)
{
va_list ap;
if (rpctls_verbose) {
va_start(ap, fmt);
if (rpctls_debug_level == 0)
vsyslog(LOG_INFO | LOG_DAEMON, fmt, ap);
else
vfprintf(stderr, fmt, ap);
va_end(ap);
}
}
/*
* Check a IP address against any host address in the
* certificate. Basically getnameinfo(3) and
* X509_check_host().
*/
int
rpctls_checkhost(struct sockaddr *sad, X509 *cert, unsigned int wildcard)
{
char hostnam[NI_MAXHOST];
int ret;
if (getnameinfo((const struct sockaddr *)sad,
sad->sa_len, hostnam, sizeof(hostnam),
NULL, 0, NI_NAMEREQD) != 0)
return (0);
rpctls_verbose_out("rpctls_checkhost: DNS %s\n",
hostnam);
ret = X509_check_host(cert, hostnam, strlen(hostnam),
wildcard, NULL);
return (ret);
}
/*
* Get the peer's IP address.
*/
int
rpctls_gethost(int s, struct sockaddr *sad, char *hostip, size_t hostlen)
{
socklen_t slen;
int ret;
slen = sizeof(struct sockaddr_storage);
if (getpeername(s, sad, &slen) < 0)
return (0);
ret = 0;
if (getnameinfo((const struct sockaddr *)sad,
sad->sa_len, hostip, hostlen,
NULL, 0, NI_NUMERICHOST) == 0) {
rpctls_verbose_out("rpctls_gethost: %s\n",
hostip);
ret = 1;
}
return (ret);
}

View file

@ -0,0 +1,68 @@
/*-
* SPDX-License-Identifier: BSD-2-Clause-FreeBSD
*
* Copyright (c) 2021 Rick Macklem
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*
* $FreeBSD$
*/
/*
* Functions in rpc.tlscommon.c used by both rpc.tlsservd.c and rpc.tlsclntd.c.
*/
int rpctls_gethost(int s, struct sockaddr *sad,
char *hostip, size_t hostlen);
int rpctls_checkhost(struct sockaddr *sad, X509 *cert,
unsigned int wildcard);
int rpctls_loadcrlfile(SSL_CTX *ctx);
void rpctls_checkcrl(void);
void rpctls_verbose_out(const char *fmt, ...);
void rpctls_svc_run(void);
/*
* A linked list of all current "SSL *"s and socket "fd"s
* for kernel RPC TLS connections is maintained.
* The "refno" field is a unique 64bit value used to
* identify which entry a kernel RPC upcall refers to.
*/
LIST_HEAD(ssl_list, ssl_entry);
struct ssl_entry {
LIST_ENTRY(ssl_entry) next;
uint64_t refno;
int s;
bool shutoff;
SSL *ssl;
X509 *cert;
};
/* Global variables shared between rpc.tlscommon.c and the daemons. */
extern int rpctls_debug_level;
extern bool rpctls_verbose;
extern SSL_CTX *rpctls_ctx;
extern const char *rpctls_verify_cafile;
extern const char *rpctls_verify_capath;
extern char *rpctls_crlfile;
extern bool rpctls_cert;
extern bool rpctls_gothup;
extern struct ssl_list rpctls_ssllist;

View file

@ -0,0 +1,348 @@
.\" Copyright (c) 2008 Isilon Inc http://www.isilon.com/
.\" Authors: Doug Rabson <dfr@rabson.org>
.\" Developed with Red Inc: Alfred Perlstein <alfred@FreeBSD.org>
.\"
.\" Redistribution and use in source and binary forms, with or without
.\" modification, are permitted provided that the following conditions
.\" are met:
.\" 1. Redistributions of source code must retain the above copyright
.\" notice, this list of conditions and the following disclaimer.
.\" 2. Redistributions in binary form must reproduce the above copyright
.\" notice, this list of conditions and the following disclaimer in the
.\" documentation and/or other materials provided with the distribution.
.\"
.\" THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
.\" ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
.\" $FreeBSD$
.\"
.\" Modified from gssd.8 for rpc.tlsservd.8 by Rick Macklem.
.Dd January 29, 2021
.Dt RPC.TLSSERVD 8
.Os
.Sh NAME
.Nm rpc.tlsservd
.Nd "Sun RPC over TLS Server Daemon"
.Sh SYNOPSIS
.Nm
.Op Fl D Ar certdir
.Op Fl d
.Op Fl h
.Op Fl l Ar CAfile
.Op Fl m
.Op Fl n Ar domain
.Op Fl p Ar CApath
.Op Fl r Ar CRLfile
.Op Fl u
.Op Fl v
.Op Fl W
.Op Fl w
.Sh DESCRIPTION
The
.Nm
program provides support for the server side of the kernel Sun RPC over TLS
implementation.
This daemon must be running to allow the kernel RPC to perform the TLS
handshake after a TCP client has sent the STARTTLS Null RPC request to
the server.
This daemon requires that the kernel be built with
.Dq options KERNEL_TLS
and be running on an architecture such as
.Dq amd64
that supports a direct map (not i386) with
.Xr ktls 4
enabled.
Note that the
.Fl tls
option in the
.Xr exports 5
file specifies that the client must use RPC over TLS.
The
.Fl tlscert
option in the
.Xr exports 5
file specifies that the client must provide a certificate
that verifies.
The
.Fl tlscertuser
option in the
.Xr exports 5
file specifies that the client must provide a certificate
that verifies and has a otherName:1.3.6.1.4.1.2238.1.1.1;UTF8: field of
subjectAltName of the form
.Dq user@domain
where
.Dq domain
matches the one for this server and
.Dq user
is a valid user name that maps to a <uid, gid_list>.
For the latter two cases, the
.Fl m
and either the
.Fl l
or
.Fl p
options must be specified.
The
.Fl tlscertuser
option also requires that the
.Fl u
option on this daemon be specified.
.Pp
Also, if the IP address used by the client cannot be trusted,
the rules in
.Xr exports 5
cannot be applied safely.
As such, the
.Fl h
option can be used along with
.Fl m
and either the
.Fl l
or
.Fl p
options to require that the client certificate have the correct
Fully Qualified Domain Name (FQDN) in it.
.Pp
A certificate and associated key must exist in /etc/rpc.tlsservd
(or the
.Dq certdir
specified by the
.Fl D
option)
in files named
.Dq cert.pem
and
.Dq certkey.pem .
.Pp
If a SIGHUP signal is sent to the daemon it will reload the
.Dq CRLfile
and will shut down any extant connections that presented certificates
during TLS handshake that have been revoked.
If the
.Fl r
option was not specified, the SIGHUP signal will be ignored.
.Pp
The daemon will log failed certificate verifications via
.Xr syslogd 8
using LOG_INFO | LOG_DAEMON when the
.Fl m
option has been specified.
.Pp
The options are as follows:
.Bl -tag -width indent
.It Fl D Ar certdir , Fl Fl certdir= Ns Ar certdir
Use
.Dq certdir
instead of /etc/rpc.tlsservd as the location for the
certificate in a file called
.Dq cert.pem
and associated key in
.Dq certkey.pem .
.It Fl d , Fl Fl debuglevel
Run in debug mode.
In this mode,
.Nm
will not fork when it starts.
.It Fl h , Fl Fl checkhost
This option specifies that the client must provide a certificate
that both verifies and has a FQDN that matches the reverse
DNS name for the IP address that
the client uses to connect to the server.
The FQDN should be
in the DNS field of the subjectAltName, but is also allowed
to be in the CN field of the
subjectName in the certificate.
By default, a wildcard "*" in the FQDN is not allowed.
With this option, a failure to verify the client certificate
or match the FQDN will result in the
server sending AUTH_REJECTEDCRED replies to all client RPCs.
This option requires the
.Fl m
and either the
.Fl l
or
.Fl p
options.
.It Fl l Ar CAfile , Fl Fl verifylocs= Ns Ar CAfile
This option specifies the path name of a CA certificate(s) file
in pem format, which is used to verify client certificates and to
set the list of CA(s) sent to the client so that it knows which
certificate to send to the server during the TLS handshake.
This path name is used in
.Dq SSL_CTX_load_verify_locations(ctx,CAfile,NULL)
and
.Dq SSL_CTX_set_client_CA_list(ctx,SSL_load_client_CA_file(CAfile))
openssl library calls.
Note that this is a path name for the file and is not assumed to be
in
.Dq certdir .
Either this option or the
.Fl p
option must be specified when the
.Fl m
option is specified so that the daemon can verify the client's
certificate.
.It Fl m , Fl Fl mutualverf
This option specifies that the server is to request a certificate
from the client during the TLS handshake.
It does not require that the client provide a certificate.
It should be specified unless no client doing RPC over TLS is
required to have a certificate.
For NFS, either the
.Xr exports 5
option
.Fl tlscert
or
.Fl tlscertuser
may be used to require a client to provide a certificate
that verifies.
See
.Xr exports 5 .
.It Fl n Ar domain , Fl Fl domain= Ns Ar domain
This option specifies what the
.Dq domain
is for use with the
.Fl u
option, overriding the domain taken from the
.Xr gethostname 2
of the server this daemon is running on.
If you have specified the
.Fl domain
command line option for
.Xr nfsuserd 8
then you should specify this option with the same
.Dq domain
that was specified for
.Xr nfsuserd 8 .
This option is only meaningful when used with the
.Fl u
option.
.It Fl p Ar CApath , Fl Fl verifydir= Ns Ar CApath
This option is similar to the
.Fl l
option, but specifies the path of a directory with CA
certificates in it.
When this option is used,
.Dq SSL_CTX_set_client_CA_list(ctx,SSL_load_client_CA_file())
is not called, so a list of CA names might not be passed
to the client during the TLS handshake.
.It Fl r Ar CRLfile , Fl Fl crl= Ns Ar CRLfile
This option specifies a Certificate Revocation List (CRL) file
that is to be loaded into the verify certificate store and
checked during verification.
This option is only meaningful when either the
.Fl l
or
.Fl p
have been specified.
.It Fl u , Fl Fl certuser
This option specifies that if the client provides a certificate
that both verifies and has a subjectAltName with an otherName
component of the form
.Dq otherName:1.3.6.1.4.1.2238.1.1.1;UTF8:user@domain
where
.Dq domain
matches the one for this server,
then the daemon will attempt to map
.Dq user
in the above
to a user credential <uid, gid_list>.
There should only be one of these otherName components for each
.Dq domain .
If
.Dq user
is a valid username in the password database,
then the <uid, gid_list> for
.Dq user
will be used for all
RPCs on the mount instead of the credentials in the RPC request
header.
This option requires the
.Fl m
and either the
.Fl l
or
.Fl p
options.
Use of this option might not conform to RFC-NNNN, which does
not allow certificates to be used for user authentication.
.It Fl v , Fl Fl verbose
Run in verbose mode.
In this mode,
.Nm
will log activity messages to
.Xr syslogd 8
using LOG_INFO | LOG_DAEMON or to
stderr, if the
.Fl d
option has also been specified.
.It Fl W , Fl Fl multiwild
This option is used with the
.Fl h
option to allow use of a wildcard
.Dq *
that matches multiple
components of the reverse DNS name for the client's IP
address.
For example, the FQDN
.Dq *.uoguelph.ca
would match both
.Dq laptop21.uoguelph.ca
and
.Dq laptop3.cis.uoguelph.ca .
.It Fl w , Fl Fl singlewild
Similar to
.Fl W
but allows the wildcard
.Dq *
to match a single component of the reverse DNS name.
For example, the FQDN
.Dq *.uoguelph.ca
would match
.Dq laptop21.uoguelph.ca
but not
.Dq laptop3.cis.uoguelph.ca .
Only one of the
.Fl W
and
.Fl w
options is allowed.
.El
.Sh EXIT STATUS
.Ex -std
.Sh SEE ALSO
.Xr openssl 1 ,
.Xr ktls 4 ,
.Xr exports 5 ,
.Xr mount_nfs 8 ,
.Xr nfsuserd 8 ,
.Xr rpc.tlsclntd 8 ,
.Xr syslogd 8
.Sh STANDARDS
The implementation is based on the specification in
.Rs
.%B "RFC NNNN"
.%T "Towards Remote Procedure Call Encryption By Default"
.Re
.Sh HISTORY
The
.Nm
manual page first appeared in
.Fx 13.0 .
.Sh BUGS
This daemon cannot be safely shut down and restarted if there are
any active RPC-over-TLS connections.
Doing so will orphan the KERNEL_TLS connections, so that they
can no longer do upcalls successfully, since the
.Dq SSL *
structures in userspace have been lost.

View file

@ -0,0 +1,886 @@
/*-
* SPDX-License-Identifier: BSD-2-Clause-FreeBSD
*
* Copyright (c) 2008 Isilon Inc http://www.isilon.com/
* Authors: Doug Rabson <dfr@rabson.org>
* Developed with Red Inc: Alfred Perlstein <alfred@freebsd.org>
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
* SUCH DAMAGE.
*/
/*
* Extensively modified from /usr/src/usr.sbin/gssd.c r344402 for
* the server side of kernel RPC-over-TLS by Rick Macklem.
*/
#include <sys/cdefs.h>
__FBSDID("$FreeBSD$");
#include <sys/param.h>
#include <sys/types.h>
#include <sys/linker.h>
#include <sys/module.h>
#include <sys/queue.h>
#include <sys/stat.h>
#include <sys/sysctl.h>
#include <sys/syslog.h>
#include <sys/time.h>
#include <err.h>
#include <getopt.h>
#include <libutil.h>
#include <netdb.h>
#include <pwd.h>
#include <signal.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <string.h>
#include <unistd.h>
#include <rpc/rpc.h>
#include <rpc/rpc_com.h>
#include <rpc/rpcsec_tls.h>
#include <openssl/opensslconf.h>
#include <openssl/bio.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#include <openssl/x509v3.h>
#include "rpctlssd.h"
#include "rpc.tlscommon.h"
#ifndef _PATH_RPCTLSSDSOCK
#define _PATH_RPCTLSSDSOCK "/var/run/rpc.tlsservd.sock"
#endif
#ifndef _PATH_CERTANDKEY
#define _PATH_CERTANDKEY "/etc/rpc.tlsservd/"
#endif
#ifndef _PATH_RPCTLSSDPID
#define _PATH_RPCTLSSDPID "/var/run/rpc.tlsservd.pid"
#endif
#ifndef _PREFERRED_CIPHERS
#define _PREFERRED_CIPHERS "AES128-GCM-SHA256"
#endif
/* Global variables also used by rpc.tlscommon.c. */
int rpctls_debug_level;
bool rpctls_verbose;
SSL_CTX *rpctls_ctx = NULL;
const char *rpctls_verify_cafile = NULL;
const char *rpctls_verify_capath = NULL;
char *rpctls_crlfile = NULL;
bool rpctls_gothup = false;
struct ssl_list rpctls_ssllist;
static struct pidfh *rpctls_pfh = NULL;
static bool rpctls_do_mutual = false;
static const char *rpctls_certdir = _PATH_CERTANDKEY;
static bool rpctls_comparehost = false;
static unsigned int rpctls_wildcard = X509_CHECK_FLAG_NO_WILDCARDS;
static uint64_t rpctls_ssl_refno = 0;
static uint64_t rpctls_ssl_sec = 0;
static uint64_t rpctls_ssl_usec = 0;
static bool rpctls_cnuser = false;
static char *rpctls_dnsname;
static const char *rpctls_cnuseroid = "1.3.6.1.4.1.2238.1.1.1";
static void rpctlssd_terminate(int);
static SSL_CTX *rpctls_setup_ssl(const char *certdir);
static SSL *rpctls_server(SSL_CTX *ctx, int s,
uint32_t *flags, uint32_t *uidp,
int *ngrps, uint32_t *gidp, X509 **certp);
static int rpctls_cnname(X509 *cert, uint32_t *uidp,
int *ngrps, uint32_t *gidp);
static char *rpctls_getdnsname(char *dnsname);
static void rpctls_huphandler(int sig __unused);
extern void rpctlssd_1(struct svc_req *rqstp, SVCXPRT *transp);
static struct option longopts[] = {
{ "certdir", required_argument, NULL, 'D' },
{ "debuglevel", no_argument, NULL, 'd' },
{ "checkhost", no_argument, NULL, 'h' },
{ "verifylocs", required_argument, NULL, 'l' },
{ "mutualverf", no_argument, NULL, 'm' },
{ "domain", required_argument, NULL, 'n' },
{ "verifydir", required_argument, NULL, 'p' },
{ "crl", required_argument, NULL, 'r' },
{ "certuser", no_argument, NULL, 'u' },
{ "verbose", no_argument, NULL, 'v' },
{ "multiwild", no_argument, NULL, 'W' },
{ "singlewild", no_argument, NULL, 'w' },
{ NULL, 0, NULL, 0 }
};
int
main(int argc, char **argv)
{
/*
* We provide an RPC service on a local-domain socket. The
* kernel rpctls code will upcall to this daemon to do the initial
* TLS handshake.
*/
struct sockaddr_un sun;
int ch, debug, fd, oldmask;
SVCXPRT *xprt;
struct timeval tm;
struct timezone tz;
char hostname[MAXHOSTNAMELEN + 2];
pid_t otherpid;
bool tls_enable;
size_t tls_enable_len;
/* Check that another rpctlssd isn't already running. */
rpctls_pfh = pidfile_open(_PATH_RPCTLSSDPID, 0600, &otherpid);
if (rpctls_pfh == NULL) {
if (errno == EEXIST)
errx(1, "rpctlssd already running, pid: %d.", otherpid);
warn("cannot open or create pidfile");
}
/* Check to see that the ktls is enabled. */
tls_enable_len = sizeof(tls_enable);
if (sysctlbyname("kern.ipc.tls.enable", &tls_enable, &tls_enable_len,
NULL, 0) != 0 || !tls_enable)
errx(1, "Kernel TLS not enabled");
/* Get the time when this daemon is started. */
gettimeofday(&tm, &tz);
rpctls_ssl_sec = tm.tv_sec;
rpctls_ssl_usec = tm.tv_usec;
/* Set the dns name for the server. */
rpctls_dnsname = rpctls_getdnsname(hostname);
if (rpctls_dnsname == NULL) {
strcpy(hostname, "@default.domain");
rpctls_dnsname = hostname;
}
debug = 0;
rpctls_verbose = false;
while ((ch = getopt_long(argc, argv, "D:dhl:n:mp:r:uvWw", longopts,
NULL)) != -1) {
switch (ch) {
case 'D':
rpctls_certdir = optarg;
break;
case 'd':
rpctls_debug_level++;
break;
case 'h':
rpctls_comparehost = true;
break;
case 'l':
rpctls_verify_cafile = optarg;
break;
case 'm':
rpctls_do_mutual = true;
break;
case 'n':
hostname[0] = '@';
strlcpy(&hostname[1], optarg, MAXHOSTNAMELEN + 1);
rpctls_dnsname = hostname;
break;
case 'p':
rpctls_verify_capath = optarg;
break;
case 'r':
rpctls_crlfile = optarg;
break;
case 'u':
rpctls_cnuser = true;
break;
case 'v':
rpctls_verbose = true;
break;
case 'W':
if (rpctls_wildcard != X509_CHECK_FLAG_NO_WILDCARDS)
errx(1, "options -w and -W are mutually "
"exclusive");
rpctls_wildcard = X509_CHECK_FLAG_MULTI_LABEL_WILDCARDS;
break;
case 'w':
if (rpctls_wildcard != X509_CHECK_FLAG_NO_WILDCARDS)
errx(1, "options -w and -W are mutually "
"exclusive");
rpctls_wildcard = 0;
break;
default:
fprintf(stderr, "usage: %s "
"[-D/--certdir certdir] [-d/--debuglevel] "
"[-h/--checkhost] "
"[-l/--verifylocs CAfile] [-m/--mutualverf] "
"[-n/--domain domain_name] "
"[-p/--verifydir CApath] [-r/--crl CRLfile] "
"[-u/--certuser] [-v/--verbose] [-W/--multiwild] "
"[-w/--singlewild]\n", argv[0]);
exit(1);
}
}
if (rpctls_do_mutual && rpctls_verify_cafile == NULL &&
rpctls_verify_capath == NULL)
errx(1, "-m requires the -l <CAfile> and/or "
"-p <CApath> options");
if (rpctls_comparehost && (!rpctls_do_mutual ||
(rpctls_verify_cafile == NULL && rpctls_verify_capath == NULL)))
errx(1, "-h requires the -m plus the "
"-l <CAfile> and/or -p <CApath> options");
if (!rpctls_comparehost && rpctls_wildcard !=
X509_CHECK_FLAG_NO_WILDCARDS)
errx(1, "The -w or -W options require the -h option");
if (rpctls_cnuser && (!rpctls_do_mutual ||
(rpctls_verify_cafile == NULL && rpctls_verify_capath == NULL)))
errx(1, "-u requires the -m plus the "
"-l <CAfile> and/or -p <CApath> options");
if (modfind("krpc") < 0) {
/* Not present in kernel, try loading it */
if (kldload("krpc") < 0 || modfind("krpc") < 0)
errx(1, "Kernel RPC is not available");
}
if (rpctls_debug_level == 0) {
if (daemon(0, 0) != 0)
err(1, "Can't daemonize");
signal(SIGINT, SIG_IGN);
signal(SIGQUIT, SIG_IGN);
signal(SIGHUP, SIG_IGN);
}
signal(SIGTERM, rpctlssd_terminate);
signal(SIGPIPE, SIG_IGN);
signal(SIGHUP, rpctls_huphandler);
pidfile_write(rpctls_pfh);
memset(&sun, 0, sizeof sun);
sun.sun_family = AF_LOCAL;
unlink(_PATH_RPCTLSSDSOCK);
strcpy(sun.sun_path, _PATH_RPCTLSSDSOCK);
sun.sun_len = SUN_LEN(&sun);
fd = socket(AF_LOCAL, SOCK_STREAM, 0);
if (fd < 0) {
if (rpctls_debug_level == 0) {
syslog(LOG_ERR, "Can't create local rpctlssd socket");
exit(1);
}
err(1, "Can't create local rpctlssd socket");
}
oldmask = umask(S_IXUSR|S_IRWXG|S_IRWXO);
if (bind(fd, (struct sockaddr *)&sun, sun.sun_len) < 0) {
if (rpctls_debug_level == 0) {
syslog(LOG_ERR, "Can't bind local rpctlssd socket");
exit(1);
}
err(1, "Can't bind local rpctlssd socket");
}
umask(oldmask);
if (listen(fd, SOMAXCONN) < 0) {
if (rpctls_debug_level == 0) {
syslog(LOG_ERR,
"Can't listen on local rpctlssd socket");
exit(1);
}
err(1, "Can't listen on local rpctlssd socket");
}
xprt = svc_vc_create(fd, RPC_MAXDATASIZE, RPC_MAXDATASIZE);
if (!xprt) {
if (rpctls_debug_level == 0) {
syslog(LOG_ERR,
"Can't create transport for local rpctlssd socket");
exit(1);
}
err(1, "Can't create transport for local rpctlssd socket");
}
if (!svc_reg(xprt, RPCTLSSD, RPCTLSSDVERS, rpctlssd_1, NULL)) {
if (rpctls_debug_level == 0) {
syslog(LOG_ERR,
"Can't register service for local rpctlssd socket");
exit(1);
}
err(1, "Can't register service for local rpctlssd socket");
}
rpctls_ctx = rpctls_setup_ssl(rpctls_certdir);
if (rpctls_ctx == NULL) {
if (rpctls_debug_level == 0) {
syslog(LOG_ERR, "Can't create SSL context");
exit(1);
}
err(1, "Can't create SSL context");
}
rpctls_gothup = false;
LIST_INIT(&rpctls_ssllist);
rpctls_syscall(RPCTLS_SYSC_SRVSETPATH, _PATH_RPCTLSSDSOCK);
rpctls_svc_run();
rpctls_syscall(RPCTLS_SYSC_SRVSHUTDOWN, "");
SSL_CTX_free(rpctls_ctx);
EVP_cleanup();
return (0);
}
bool_t
rpctlssd_null_1_svc(__unused void *argp, __unused void *result,
__unused struct svc_req *rqstp)
{
rpctls_verbose_out("rpctlssd_null_svc: done\n");
return (TRUE);
}
bool_t
rpctlssd_connect_1_svc(__unused void *argp,
struct rpctlssd_connect_res *result, __unused struct svc_req *rqstp)
{
int ngrps, s;
SSL *ssl;
uint32_t flags;
struct ssl_entry *newslp;
uint32_t uid;
uint32_t *gidp;
X509 *cert;
rpctls_verbose_out("rpctlsd_connect_svc: started\n");
memset(result, 0, sizeof(*result));
/* Get the socket fd from the kernel. */
s = rpctls_syscall(RPCTLS_SYSC_SRVSOCKET, "");
if (s < 0)
return (FALSE);
/* Do the server side of a TLS handshake. */
gidp = calloc(NGROUPS, sizeof(*gidp));
ssl = rpctls_server(rpctls_ctx, s, &flags, &uid, &ngrps, gidp, &cert);
if (ssl == NULL) {
free(gidp);
rpctls_verbose_out("rpctlssd_connect_svc: ssl "
"accept failed\n");
/*
* For RPC-over-TLS, this upcall is expected
* to close off the socket upon handshake failure.
*/
close(s);
return (FALSE);
} else {
rpctls_verbose_out("rpctlssd_connect_svc: "
"succeeded flags=0x%x\n", flags);
result->flags = flags;
result->sec = rpctls_ssl_sec;
result->usec = rpctls_ssl_usec;
result->ssl = ++rpctls_ssl_refno;
/* Hard to believe this could ever wrap around.. */
if (rpctls_ssl_refno == 0)
result->ssl = ++rpctls_ssl_refno;
if ((flags & RPCTLS_FLAGS_CERTUSER) != 0) {
result->uid = uid;
result->gid.gid_len = ngrps;
result->gid.gid_val = gidp;
} else {
result->uid = 0;
result->gid.gid_len = 0;
result->gid.gid_val = gidp;
}
}
/* Maintain list of all current SSL *'s */
newslp = malloc(sizeof(*newslp));
newslp->ssl = ssl;
newslp->s = s;
newslp->shutoff = false;
newslp->refno = rpctls_ssl_refno;
newslp->cert = cert;
LIST_INSERT_HEAD(&rpctls_ssllist, newslp, next);
return (TRUE);
}
bool_t
rpctlssd_handlerecord_1_svc(struct rpctlssd_handlerecord_arg *argp,
struct rpctlssd_handlerecord_res *result, __unused struct svc_req *rqstp)
{
struct ssl_entry *slp;
int ret;
char junk;
slp = NULL;
if (argp->sec == rpctls_ssl_sec && argp->usec ==
rpctls_ssl_usec) {
LIST_FOREACH(slp, &rpctls_ssllist, next) {
if (slp->refno == argp->ssl)
break;
}
}
if (slp != NULL) {
rpctls_verbose_out("rpctlssd_handlerecord fd=%d\n",
slp->s);
/*
* An SSL_read() of 0 bytes should fail, but it should
* handle the non-application data record before doing so.
*/
ret = SSL_read(slp->ssl, &junk, 0);
if (ret <= 0) {
/* Check to see if this was a close alert. */
ret = SSL_get_shutdown(slp->ssl);
if ((ret & (SSL_SENT_SHUTDOWN |
SSL_RECEIVED_SHUTDOWN)) == SSL_RECEIVED_SHUTDOWN)
SSL_shutdown(slp->ssl);
} else {
if (rpctls_debug_level == 0)
syslog(LOG_ERR, "SSL_read returned %d", ret);
else
fprintf(stderr, "SSL_read returned %d\n", ret);
}
result->reterr = RPCTLSERR_OK;
} else
result->reterr = RPCTLSERR_NOSSL;
return (TRUE);
}
bool_t
rpctlssd_disconnect_1_svc(struct rpctlssd_disconnect_arg *argp,
struct rpctlssd_disconnect_res *result, __unused struct svc_req *rqstp)
{
struct ssl_entry *slp;
int ret;
slp = NULL;
if (argp->sec == rpctls_ssl_sec && argp->usec ==
rpctls_ssl_usec) {
LIST_FOREACH(slp, &rpctls_ssllist, next) {
if (slp->refno == argp->ssl)
break;
}
}
if (slp != NULL) {
rpctls_verbose_out("rpctlssd_disconnect fd=%d closed\n",
slp->s);
LIST_REMOVE(slp, next);
if (!slp->shutoff) {
ret = SSL_get_shutdown(slp->ssl);
/*
* Do an SSL_shutdown() unless a close alert has
* already been sent.
*/
if ((ret & SSL_SENT_SHUTDOWN) == 0)
SSL_shutdown(slp->ssl);
}
SSL_free(slp->ssl);
if (slp->cert != NULL)
X509_free(slp->cert);
/*
* For RPC-over-TLS, this upcall is expected
* to close off the socket.
*/
if (!slp->shutoff)
shutdown(slp->s, SHUT_WR);
close(slp->s);
free(slp);
result->reterr = RPCTLSERR_OK;
} else
result->reterr = RPCTLSERR_NOCLOSE;
return (TRUE);
}
int
rpctlssd_1_freeresult(__unused SVCXPRT *transp, xdrproc_t xdr_result,
caddr_t result)
{
rpctlssd_connect_res *res;
if (xdr_result == (xdrproc_t)xdr_rpctlssd_connect_res) {
res = (rpctlssd_connect_res *)(void *)result;
free(res->gid.gid_val);
}
return (TRUE);
}
static void
rpctlssd_terminate(int sig __unused)
{
struct ssl_entry *slp;
rpctls_syscall(RPCTLS_SYSC_SRVSHUTDOWN, "");
pidfile_remove(rpctls_pfh);
LIST_FOREACH(slp, &rpctls_ssllist, next)
shutdown(slp->s, SHUT_RD);
exit(0);
}
/* Allow the handshake to proceed. */
static int
rpctls_verify_callback(__unused int preverify_ok,
__unused X509_STORE_CTX *x509_ctx)
{
return (1);
}
static SSL_CTX *
rpctls_setup_ssl(const char *certdir)
{
SSL_CTX *ctx;
char path[PATH_MAX];
size_t len, rlen;
int ret;
SSL_library_init();
SSL_load_error_strings();
OpenSSL_add_all_algorithms();
ctx = SSL_CTX_new(TLS_server_method());
if (ctx == NULL) {
rpctls_verbose_out("rpctls_setup_ssl: SSL_CTX_new failed\n");
return (NULL);
}
SSL_CTX_set_ecdh_auto(ctx, 1);
/*
* Set preferred ciphers, since KERN_TLS only supports a
* few of them.
*/
ret = SSL_CTX_set_cipher_list(ctx, _PREFERRED_CIPHERS);
if (ret == 0) {
rpctls_verbose_out("rpctls_setup_ssl: "
"SSL_CTX_set_cipher_list failed to set any ciphers\n");
SSL_CTX_free(ctx);
return (NULL);
}
/* Get the cert.pem and certkey.pem files from the directory certdir. */
len = strlcpy(path, certdir, sizeof(path));
rlen = sizeof(path) - len;
if (strlcpy(&path[len], "cert.pem", rlen) != 8) {
SSL_CTX_free(ctx);
return (NULL);
}
ret = SSL_CTX_use_certificate_file(ctx, path, SSL_FILETYPE_PEM);
if (ret != 1) {
rpctls_verbose_out("rpctls_setup_ssl: can't use certificate "
"file path=%s ret=%d\n", path, ret);
SSL_CTX_free(ctx);
return (NULL);
}
if (strlcpy(&path[len], "certkey.pem", rlen) != 11) {
SSL_CTX_free(ctx);
return (NULL);
}
ret = SSL_CTX_use_PrivateKey_file(ctx, path, SSL_FILETYPE_PEM);
if (ret != 1) {
rpctls_verbose_out("rpctls_setup_ssl: Can't use private "
"key path=%s ret=%d\n", path, ret);
SSL_CTX_free(ctx);
return (NULL);
}
/* Set Mutual authentication, as required. */
if (rpctls_do_mutual) {
if (rpctls_verify_cafile != NULL ||
rpctls_verify_capath != NULL) {
if (rpctls_crlfile != NULL) {
ret = rpctls_loadcrlfile(ctx);
if (ret == 0) {
rpctls_verbose_out("rpctls_setup_ssl:"
" Load CRLfile failed\n");
SSL_CTX_free(ctx);
return (NULL);
}
}
#if OPENSSL_VERSION_NUMBER >= 0x30000000
ret = 1;
if (rpctls_verify_cafile != NULL)
ret = SSL_CTX_load_verify_file(ctx,
rpctls_verify_cafile);
if (ret != 0 && rpctls_verify_capath != NULL)
ret = SSL_CTX_load_verify_dir(ctx,
rpctls_verify_capath);
#else
ret = SSL_CTX_load_verify_locations(ctx,
rpctls_verify_cafile, rpctls_verify_capath);
#endif
if (ret == 0) {
rpctls_verbose_out("rpctls_setup_ssl: "
"Can't load verify locations\n");
SSL_CTX_free(ctx);
return (NULL);
}
if (rpctls_verify_cafile != NULL)
SSL_CTX_set_client_CA_list(ctx,
SSL_load_client_CA_file(
rpctls_verify_cafile));
}
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER,
rpctls_verify_callback);
}
SSL_CTX_clear_mode(ctx, SSL_MODE_NO_KTLS_TX | SSL_MODE_NO_KTLS_RX);
return (ctx);
}
static SSL *
rpctls_server(SSL_CTX *ctx, int s, uint32_t *flags, uint32_t *uidp,
int *ngrps, uint32_t *gidp, X509 **certp)
{
SSL *ssl;
X509 *cert;
struct sockaddr *sad;
struct sockaddr_storage ad;
char hostnam[NI_MAXHOST];
int gethostret, ret;
char *cp, *cp2;
long verfret;
*flags = 0;
*certp = NULL;
sad = (struct sockaddr *)&ad;
ssl = SSL_new(ctx);
if (ssl == NULL) {
rpctls_verbose_out("rpctls_server: SSL_new failed\n");
return (NULL);
}
if (SSL_set_fd(ssl, s) != 1) {
rpctls_verbose_out("rpctls_server: SSL_set_fd failed\n");
SSL_free(ssl);
return (NULL);
}
ret = SSL_accept(ssl);
if (ret != 1) {
rpctls_verbose_out("rpctls_server: SSL_accept "
"failed ret=%d\n", ret);
SSL_free(ssl);
return (NULL);
}
*flags |= RPCTLS_FLAGS_HANDSHAKE;
if (rpctls_do_mutual) {
cert = SSL_get_peer_certificate(ssl);
if (cert != NULL) {
gethostret = rpctls_gethost(s, sad, hostnam,
sizeof(hostnam));
if (gethostret == 0)
hostnam[0] = '\0';
cp2 = X509_NAME_oneline(
X509_get_subject_name(cert), NULL, 0);
*flags |= RPCTLS_FLAGS_GOTCERT;
verfret = SSL_get_verify_result(ssl);
if (verfret != X509_V_OK) {
cp = X509_NAME_oneline(
X509_get_issuer_name(cert), NULL, 0);
if (rpctls_debug_level == 0)
syslog(LOG_INFO | LOG_DAEMON,
"rpctls_server: client IP %s "
"issuerName=%s subjectName=%s"
" verify failed %s\n", hostnam,
cp, cp2,
X509_verify_cert_error_string(
verfret));
else
fprintf(stderr,
"rpctls_server: client IP %s "
"issuerName=%s subjectName=%s"
" verify failed %s\n", hostnam,
cp, cp2,
X509_verify_cert_error_string(
verfret));
}
if (verfret ==
X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT ||
verfret == X509_V_ERR_SELF_SIGNED_CERT_IN_CHAIN)
*flags |= RPCTLS_FLAGS_SELFSIGNED;
else if (verfret == X509_V_OK) {
if (rpctls_comparehost) {
ret = 0;
if (gethostret != 0)
ret = rpctls_checkhost(sad,
cert, rpctls_wildcard);
if (ret != 1) {
*flags |=
RPCTLS_FLAGS_DISABLED;
rpctls_verbose_out(
"rpctls_server: "
"checkhost "
"failed\n");
}
}
if (rpctls_cnuser) {
ret = rpctls_cnname(cert, uidp,
ngrps, gidp);
if (ret != 0)
*flags |= RPCTLS_FLAGS_CERTUSER;
}
*flags |= RPCTLS_FLAGS_VERIFIED;
*certp = cert;
cert = NULL;
}
if (cert != NULL)
X509_free(cert);
} else
rpctls_verbose_out("rpctls_server: "
"No peer certificate\n");
}
/* Check to see that ktls is working for the connection. */
ret = BIO_get_ktls_send(SSL_get_wbio(ssl));
rpctls_verbose_out("rpctls_server: BIO_get_ktls_send=%d\n", ret);
if (ret != 0) {
ret = BIO_get_ktls_recv(SSL_get_rbio(ssl));
rpctls_verbose_out("rpctls_server: BIO_get_ktls_recv=%d\n",
ret);
}
if (ret == 0) {
if (rpctls_debug_level == 0)
syslog(LOG_ERR, "ktls not working");
else
fprintf(stderr, "ktls not working\n");
/*
* The handshake has completed, so all that can be
* done is disable the connection.
*/
*flags |= RPCTLS_FLAGS_DISABLED;
}
return (ssl);
}
/*
* Acquire the dnsname for this server.
*/
static char *
rpctls_getdnsname(char *hostname)
{
char *cp, *dnsname;
struct addrinfo *aip, hints;
int error;
dnsname = NULL;
if (gethostname(hostname, MAXHOSTNAMELEN) == 0) {
if ((cp = strchr(hostname, '.')) != NULL &&
*(cp + 1) != '\0') {
*cp = '@';
dnsname = cp;
} else {
memset((void *)&hints, 0, sizeof (hints));
hints.ai_flags = AI_CANONNAME;
error = getaddrinfo(hostname, NULL, &hints, &aip);
if (error == 0) {
if (aip->ai_canonname != NULL &&
(cp = strchr(aip->ai_canonname, '.')) !=
NULL && *(cp + 1) != '\0') {
hostname[0] = '@';
strlcpy(&hostname[1], cp + 1,
MAXHOSTNAMELEN + 1);
dnsname = hostname;
}
freeaddrinfo(aip);
}
}
}
return (dnsname);
}
/*
* Check for an otherName component of subjectAltName where the OID
* matches and the "domain" matches that of this server.
* If found, map "user" to a <uid, gidlist> for it.
*/
static int
rpctls_cnname(X509 *cert, uint32_t *uidp, int *ngrps, uint32_t *gidp)
{
char *cp, usern[1024 + 1];
struct passwd *pwd;
gid_t gids[NGROUPS];
int i, j;
GENERAL_NAMES *genlist;
GENERAL_NAME *genname;
OTHERNAME *val;
size_t slen;
/* First, find the otherName in the subjectAltName. */
genlist = X509_get_ext_d2i(cert, NID_subject_alt_name, NULL, NULL);
if (genlist == NULL)
return (0);
cp = NULL;
for (i = 0; i < sk_GENERAL_NAME_num(genlist); i++) {
genname = sk_GENERAL_NAME_value(genlist, i);
if (genname->type != GEN_OTHERNAME)
continue;
val = genname->d.otherName;
/* Check to see that it is the correct OID. */
slen = i2t_ASN1_OBJECT(usern, sizeof(usern), val->type_id);
if (slen != strlen(rpctls_cnuseroid) || memcmp(usern,
rpctls_cnuseroid, slen) != 0)
continue;
/* Sanity check the otherName. */
if (val->value->type != V_ASN1_UTF8STRING ||
val->value->value.utf8string->length < 3 ||
(size_t)val->value->value.utf8string->length > sizeof(usern)
- 1) {
rpctls_verbose_out("rpctls_cnname: invalid cnuser "
"type=%d\n", val->value->type);
continue;
}
/* Look for a "user" in the otherName */
memcpy(usern, val->value->value.utf8string->data,
val->value->value.utf8string->length);
usern[val->value->value.utf8string->length] = '\0';
/* Now, look for the @dnsname suffix in the commonName. */
cp = strcasestr(usern, rpctls_dnsname);
if (cp == NULL)
continue;
if (*(cp + strlen(rpctls_dnsname)) != '\0') {
cp = NULL;
continue;
}
*cp = '\0';
break;
}
if (cp == NULL)
return (0);
/* See if the "user" is in the passwd database. */
pwd = getpwnam(usern);
if (pwd == NULL)
return (0);
*uidp = pwd->pw_uid;
*ngrps = NGROUPS;
if (getgrouplist(pwd->pw_name, pwd->pw_gid, gids, ngrps) < 0)
return (0);
rpctls_verbose_out("mapped user=%s ngrps=%d uid=%d\n", pwd->pw_name,
*ngrps, pwd->pw_uid);
for (j = 0; j < *ngrps; j++)
gidp[j] = gids[j];
return (1);
}
static void
rpctls_huphandler(int sig __unused)
{
rpctls_gothup = true;
}