tools: reimplement virt-pki-validate in C

The virt-pki-validate tool is currently a shell script. We have a
general goal of eliminating use of shell in the project. By doing a
new implementation in C, we can also make use of our more thorough
sanity checking code to validate the certificate setup.

This new implementation the same output format as the host validation
tool for a more consistent user experiance.

It also eliminates the requirement to have certtool installed on
libvirt hosts, which has been an issue for Fedora flatpak packages
since certtool isn't in the default platform runtime.

Reviewed-by: Michal Privoznik <mprivozn@redhat.com>
Signed-off-by: Daniel P. Berrangé <berrange@redhat.com>
This commit is contained in:
Daniel P. Berrangé 2024-06-07 11:27:10 +01:00
parent 3faa78d98d
commit 8e97fd4181
6 changed files with 339 additions and 306 deletions

View File

@ -48,7 +48,7 @@ failure a non-zero status will be set.
AUTHOR AUTHOR
====== ======
Daniel Veillard Daniel Veillard, Daniel P. Berrangé
BUGS BUGS
@ -70,7 +70,7 @@ Alternatively, you may report bugs to your software distributor / vendor.
COPYRIGHT COPYRIGHT
========= =========
Copyright (C) 2006-2012 by Red Hat, Inc. Copyright (C) 2006-2024 by Red Hat, Inc.
LICENSE LICENSE

View File

@ -1032,8 +1032,6 @@ capabilities of VirtualBox
%package client %package client
Summary: Client side utilities of the libvirt library Summary: Client side utilities of the libvirt library
Requires: libvirt-libs = %{version}-%{release} Requires: libvirt-libs = %{version}-%{release}
# Needed by virt-pki-validate script.
Requires: gnutls-utils
# Ensure smooth upgrades # Ensure smooth upgrades
Obsoletes: libvirt-bash-completion < 7.3.0 Obsoletes: libvirt-bash-completion < 7.3.0
@ -2513,7 +2511,7 @@ exit 0
%{mingw32_bindir}/virt-admin.exe %{mingw32_bindir}/virt-admin.exe
%{mingw32_bindir}/virt-xml-validate %{mingw32_bindir}/virt-xml-validate
%{mingw32_bindir}/virt-pki-query-dn.exe %{mingw32_bindir}/virt-pki-query-dn.exe
%{mingw32_bindir}/virt-pki-validate %{mingw32_bindir}/virt-pki-validate.exe
%{mingw32_bindir}/libvirt-lxc-0.dll %{mingw32_bindir}/libvirt-lxc-0.dll
%{mingw32_bindir}/libvirt-qemu-0.dll %{mingw32_bindir}/libvirt-qemu-0.dll
%{mingw32_bindir}/libvirt-admin-0.dll %{mingw32_bindir}/libvirt-admin-0.dll
@ -2572,7 +2570,7 @@ exit 0
%{mingw64_bindir}/virt-admin.exe %{mingw64_bindir}/virt-admin.exe
%{mingw64_bindir}/virt-xml-validate %{mingw64_bindir}/virt-xml-validate
%{mingw64_bindir}/virt-pki-query-dn.exe %{mingw64_bindir}/virt-pki-query-dn.exe
%{mingw64_bindir}/virt-pki-validate %{mingw64_bindir}/virt-pki-validate.exe
%{mingw64_bindir}/libvirt-lxc-0.dll %{mingw64_bindir}/libvirt-lxc-0.dll
%{mingw64_bindir}/libvirt-qemu-0.dll %{mingw64_bindir}/libvirt-qemu-0.dll
%{mingw64_bindir}/libvirt-admin-0.dll %{mingw64_bindir}/libvirt-admin-0.dll

View File

@ -391,6 +391,7 @@ tools/virt-host-validate-qemu.c
tools/virt-host-validate.c tools/virt-host-validate.c
tools/virt-login-shell-helper.c tools/virt-login-shell-helper.c
tools/virt-pki-query-dn.c tools/virt-pki-query-dn.c
tools/virt-pki-validate.c
tools/virt-validate-common.c tools/virt-validate-common.c
tools/vsh-table.c tools/vsh-table.c
tools/vsh.c tools/vsh.c

View File

@ -255,13 +255,33 @@ configure_file(
install_mode: 'rwxr-xr-x', install_mode: 'rwxr-xr-x',
) )
configure_file( executable(
input: 'virt-pki-validate.in', 'virt-pki-validate',
output: '@BASENAME@', tlsconfig_sources + [
configuration: tools_conf, 'virt-validate-common.c',
'virt-pki-validate.c',
],
dependencies: [
glib_dep,
gnutls_dep,
],
include_directories: [
libvirt_inc,
src_inc_dir,
top_inc_dir,
util_inc_dir,
rpc_inc_dir,
],
link_args: (
libvirt_relro
+ libvirt_no_indirect
+ libvirt_no_undefined
),
link_with: [
libvirt_lib
],
install: true, install: true,
install_dir: bindir, install_dir: bindir,
install_mode: 'rwxr-xr-x',
) )
executable( executable(

309
tools/virt-pki-validate.c Normal file
View File

@ -0,0 +1,309 @@
/*
* SPDX-License-Identifier: GPL-2.0-or-later
*/
#include <config.h>
#include "internal.h"
#include <getopt.h>
#include <stdio.h>
#include <stdlib.h>
#include <gnutls/gnutls.h>
#include <gnutls/x509.h>
#include "virgettext.h"
#include "virfile.h"
#include "virnettlsconfig.h"
#include "virnettlscert.h"
#include "virutil.h"
#include "virt-validate-common.h"
static bool
virPKIValidateFile(const char *file,
uid_t owner,
gid_t group,
mode_t mode)
{
struct stat sb;
if (stat(file, &sb) < 0)
return false;
if (sb.st_uid != owner ||
sb.st_gid != group)
return false;
return (sb.st_mode & 0777) == mode;
}
#define FILE_REQUIRE_EXISTS(scope, path, message, hint, ...) \
do { \
virValidateCheck(scope, "%s", message); \
if (!virFileExists(path)) { \
virValidateFail(VIR_VALIDATE_FAIL, hint, __VA_ARGS__); \
ok = false; \
goto done; \
} else { \
virValidatePass(); \
} \
} while (0)
#define FILE_REQUIRE_ACCESS(scope, path, message, uid, gid, mode, hint, ...) \
do { \
virValidateCheck(scope, "%s", message); \
if (!virPKIValidateFile(path, uid, gid, mode)) { \
virValidateFail(VIR_VALIDATE_FAIL, hint, __VA_ARGS__); \
ok = false; \
} else { \
virValidatePass(); \
} \
} while (0)
static bool
virPKIValidateTrust(void)
{
g_autofree char *cacert = NULL, *cacrl = NULL;
bool ok = true;
virNetTLSConfigSystemTrust(&cacert,
&cacrl);
FILE_REQUIRE_EXISTS("TRUST",
LIBVIRT_PKI_DIR,
_("Checking if system PKI dir exists"),
_("The system PKI dir %1$s is usually installed as part of the base filesystem or openssl packages"),
LIBVIRT_PKI_DIR);
FILE_REQUIRE_ACCESS("TRUST",
LIBVIRT_PKI_DIR,
_("Checking system PKI dir access"),
0, 0, 0755,
_("The system PKI dir %1$s must be accessible to all users. As root, run: chown root.root; chmod 0755 %2$s"),
LIBVIRT_PKI_DIR, LIBVIRT_PKI_DIR);
FILE_REQUIRE_EXISTS("TRUST",
LIBVIRT_CACERT_DIR,
_("Checking if system CA dir exists"),
_("The system CA dir %1$s is usually installed as part of the base filesystem or openssl packages"),
LIBVIRT_CACERT_DIR);
FILE_REQUIRE_ACCESS("TRUST",
LIBVIRT_CACERT_DIR,
_("Checking system CA dir access"),
0, 0, 0755,
_("The system CA dir %1$s must be accessible to all users. As root, run: chown root.root; chmod 0755 %2$s"),
LIBVIRT_CACERT_DIR, LIBVIRT_CACERT_DIR);
FILE_REQUIRE_EXISTS("TRUST",
cacert,
_("Checking if CA cert exists"),
_("The machine cannot act as a client or server. See https://libvirt.org/kbase/tlscerts.html#setting-up-a-certificate-authority-ca on how to install %1$s"),
cacert);
FILE_REQUIRE_ACCESS("TRUST",
cacert,
_("Checking CA cert access"),
0, 0, 0644,
_("The CA certificate %1$s must be accessible to all users. As root run: chown root.root %2$s; chmod 0644 %3$s"),
cacert, cacert, cacert);
done:
return ok;
}
static bool
virPKIValidateIdentity(bool isServer)
{
g_autofree char *cacert = NULL, *cacrl = NULL;
g_autofree char *cert = NULL, *key = NULL;
bool ok = true;
const char *scope = isServer ? "SERVER" : "CLIENT";
virNetTLSConfigSystemTrust(&cacert,
&cacrl);
virNetTLSConfigSystemIdentity(isServer,
&cert,
&key);
FILE_REQUIRE_EXISTS(scope,
LIBVIRT_CERT_DIR,
_("Checking if system cert dir exists"),
_("The system cert dir %1$s is usually installed as part of the libvirt package"),
LIBVIRT_CERT_DIR);
FILE_REQUIRE_ACCESS(scope,
LIBVIRT_CERT_DIR,
_("Checking system cert dir access"),
0, 0, 0755,
_("The system cert dir %1$s must be accessible to all users. As root, run: chown root.root; chmod 0755 %2$s"),
LIBVIRT_PKI_DIR, LIBVIRT_PKI_DIR);
FILE_REQUIRE_EXISTS(scope,
LIBVIRT_KEY_DIR,
_("Checking if system key dir exists"),
_("The system key dir %1$s is usually installed as part of the libvirt package"),
LIBVIRT_KEY_DIR);
FILE_REQUIRE_ACCESS(scope,
LIBVIRT_KEY_DIR,
_("Checking system key dir access"),
0, 0, 0755,
_("The system key dir %1$s must be accessible to all users. As root, run: chown root.root; chmod 0755 %2$s"),
LIBVIRT_KEY_DIR, LIBVIRT_PKI_DIR);
FILE_REQUIRE_EXISTS(scope,
key,
_("Checking if key exists"),
isServer ?
_("The machine cannot act as a server. See https://libvirt.org/kbase/tlscerts.html#issuing-server-certificates on how to regenerate %1$s") :
_("The machine cannot act as a client. See https://libvirt.org/kbase/tlscerts.html#issuing-client-certificates on how to regenerate %1$s"),
key);
FILE_REQUIRE_ACCESS(scope,
key,
_("Checking key access"),
0, 0, isServer ? 0600 : 0644,
isServer ?
_("The server key %1$s must not be accessible to unprivileged users. As root run: chown root.root %2$s; chmod 0600 %3$s") :
_("The client key %1$s must be accessible to all users. As root run: chown root.root %2$s; chmod 0644 %3$s"),
key, key, key);
FILE_REQUIRE_EXISTS(scope,
cert,
_("Checking if cert exists"),
isServer ?
_("The machine cannot act as a server. See https://libvirt.org/kbase/tlscerts.html#issuing-server-certificates on how to regenerate %1$s") :
_("The machine cannot act as a client. See https://libvirt.org/kbase/tlscerts.html#issuing-client-certificates on how to regenerate %1$s"),
cert);
FILE_REQUIRE_ACCESS(scope,
cert,
_("Checking cert access"),
0, 0, 0644,
isServer ?
_("The server cert %1$s must be accessible to all users. As root run: chown root.root %2$s; chmod 0644 %3$s") :
_("The client cert %1$s must be accessible to all users. As root run: chown root.root %2$s; chmod 0644 %3$s"),
cert, cert, cert);
virValidateCheck(scope, "%s", _("Checking cert properties"));
if (virNetTLSCertSanityCheck(isServer,
cacert,
cert) < 0) {
virValidateFail(VIR_VALIDATE_FAIL, "%s",
virGetLastErrorMessage());
ok = false;
} else {
virValidatePass();
}
if (isServer) {
gnutls_x509_crt_t crt;
virValidateCheck(scope, "%s", _("Checking cert hostname match"));
if (!(crt = virNetTLSCertLoadFromFile(cert, true))) {
virValidateFail(VIR_VALIDATE_FAIL,
_("Unable to load %1$s: %2$s"),
cert, virGetLastErrorMessage());
} else {
g_autofree char *hostname = virGetHostname();
int ret = gnutls_x509_crt_check_hostname(crt, hostname);
gnutls_x509_crt_deinit(crt);
if (!ret) {
/* Only warning, since there can be valid reasons for mis-match */
virValidateFail(VIR_VALIDATE_WARN,
_("Certificate %1$s owner does not match the hostname %2$s"),
cert, hostname);
ok = false;
} else {
virValidatePass();
}
}
}
done:
return ok;
}
static void
print_usage(const char *progname,
FILE *out)
{
fprintf(out,
_("Usage:\n"
" %1$s { -v | -h } [TRUST|SERVER|CLIENT]\n"
"\n"
"Validate TLS certificate configuration\n"
"\n"
"options:\n"
" -h | --help display this help and exit\n"
" -v | --version output version information and exit\n"),
progname);
}
int main(int argc, char **argv)
{
const char *scope = NULL;
bool quiet = false;
int arg = 0;
bool ok = true;
const char *progname = argv[0];
struct option opt[] = {
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, 'v' },
{ NULL, 0, NULL, 0 },
};
if (virGettextInitialize() < 0)
return EXIT_FAILURE;
while ((arg = getopt_long(argc, argv, "hvsup:", opt, NULL)) != -1) {
switch (arg) {
case 'v':
printf("%s\n", PACKAGE_VERSION);
return EXIT_SUCCESS;
case 'h':
print_usage(progname, stdout);
return EXIT_SUCCESS;
case 'q':
quiet = true;
break;
case '?':
default:
print_usage(progname, stderr);
return EXIT_FAILURE;
}
}
if ((argc - optind) > 2) {
fprintf(stderr, _("%1$s: too many command line arguments\n"), argv[0]);
print_usage(progname, stderr);
return EXIT_FAILURE;
}
if (argc > 1)
scope = argv[optind];
virValidateSetQuiet(quiet);
if ((!scope || g_str_equal(scope, "trust")) &&
!virPKIValidateTrust())
ok = false;
if ((!scope || g_str_equal(scope, "server")) &&
!virPKIValidateIdentity(true))
ok = false;
if ((!scope || g_str_equal(scope, "client")) &&
!virPKIValidateIdentity(false))
ok = false;
if (!ok)
return EXIT_FAILURE;
return EXIT_SUCCESS;
}

View File

@ -1,295 +0,0 @@
#!/bin/sh
#
# This shell script checks the TLS certificates and options needed
# for the secure client/server support of libvirt as documented at
# https://libvirt.org/kbase/tlscerts.html
#
# Copyright (C) 2009-2013 Red Hat, Inc.
#
# This library is free software; you can redistribute it and/or
# modify it under the terms of the GNU Lesser General Public
# License as published by the Free Software Foundation; either
# version 2.1 of the License, or (at your option) any later version.
#
# This library is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
# Lesser General Public License for more details.
#
# You should have received a copy of the GNU Lesser General Public
# License along with this library. If not, see
# <http://www.gnu.org/licenses/>.
#
# Daniel Veillard <veillard@redhat.com>
#
case $1 in
-h | --h | --he | --hel | --help)
cat <<EOF
Usage:
$0 [OPTION]
Options:
-h | --help Display program help
-V | --version Display program version
EOF
exit ;;
-V | --v | --ve | --ver | --vers | --versi | --versio | --version)
cat <<EOF
$0 (libvirt) @VERSION@
EOF
exit ;;
--) shift ;;
-) # Not an option but an argument; it gets rejected later
;;
-*)
echo "$0: unrecognized option '$1'" >&2
exit 1 ;;
esac
if test $# != 0; then
echo "$0: unrecognized argument '$1'" >&2
exit 1
fi
USER=`who am i | awk '{ print $1 }'`
SERVER=1
CLIENT=1
PORT=16514
#
# First get certtool
#
CERTOOL=`which certtool 2>/dev/null`
if [ ! -x "$CERTOOL" ]
then
echo "Could not locate the certtool program"
echo "make sure the gnutls-utils (or gnutls-bin) package is installed"
exit 1
fi
echo Found "$CERTOOL"
#
# Check the directory structure
#
SYSCONFDIR="@sysconfdir@"
PKI="$SYSCONFDIR/pki"
if [ ! -d "$PKI" ]
then
echo the $PKI directory is missing, it is usually
echo installed as part of the filesystem or openssl packages
exit 1
fi
if [ ! -r "$PKI" ]
then
echo the $PKI directory is not readable by $USER
echo "as root do: chmod a+rx $PKI"
exit 1
fi
if [ ! -x "$PKI" ]
then
echo the $PKI directory is not listable by $USER
echo "as root do: chmod a+rx $PKI"
exit 1
fi
CA="$PKI/CA"
if [ ! -d "$CA" ]
then
echo the $CA directory is missing, it is usually
echo installed as part of the or openssl package
exit 1
fi
if [ ! -r "$CA" ]
then
echo the $CA directory is not readable by $USER
echo "as root do: chmod a+rx $CA"
exit 1
fi
if [ ! -x "$CA" ]
then
echo the $CA directory is not listable by $USER
echo "as root do: chmod a+rx $CA"
exit 1
fi
LIBVIRT="$PKI/libvirt"
if [ ! -d "$LIBVIRT" ]
then
echo the $LIBVIRT directory is missing, it is usually
echo installed by the libvirt package
echo "as root do: mkdir -m 755 $LIBVIRT ; chown root:root $LIBVIRT"
exit 1
fi
if [ ! -r "$LIBVIRT" ]
then
echo the $LIBVIRT directory is not readable by $USER
echo "as root do: chown root:root $LIBVIRT ; chmod 755 $LIBVIRT"
exit 1
fi
if [ ! -x "$LIBVIRT" ]
then
echo the $LIBVIRT directory is not listable by $USER
echo "as root do: chown root:root $LIBVIRT ; chmod 755 $LIBVIRT"
exit 1
fi
LIBVIRTP="$LIBVIRT/private"
if [ ! -d "$LIBVIRTP" ]
then
echo the $LIBVIRTP directory is missing, it is usually
echo installed by the libvirt package
echo "as root do: mkdir -m 755 $LIBVIRTP ; chown root:root $LIBVIRTP"
exit 1
fi
if [ ! -r "$LIBVIRTP" ]
then
echo the $LIBVIRTP directory is not readable by $USER
echo "as root do: chown root:root $LIBVIRTP ; chmod 755 $LIBVIRTP"
exit 1
fi
if [ ! -x "$LIBVIRTP" ]
then
echo the $LIBVIRTP directory is not listable by $USER
echo "as root do: chown root:root $LIBVIRTP ; chmod 755 $LIBVIRTP"
exit 1
fi
#
# Now check the certificates
# First the CA certificate
#
if [ ! -f "$CA/cacert.pem" ]
then
echo the CA certificate $CA/cacert.pem is missing while it
echo should be installed on both client and servers
echo "see https://libvirt.org/kbase/tlscerts.html#setting-up-a-certificate-authority-ca"
echo on how to install it
exit 1
fi
if [ ! -r "$CA/cacert.pem" ]
then
echo the CA certificate $CA/cacert.pem is not readable by $USER
echo "as root do: chmod 644 $CA/cacert.pem"
exit 1
fi
sed_get_org='/Issuer:/ {
s/.*Issuer:.*CN=//
s/,.*//
p
}'
ORG=`"$CERTOOL" -i --infile "$CA/cacert.pem" | sed -n "$sed_get_org"`
if [ "$ORG" = "" ]
then
echo the CA certificate $CA/cacert.pem does not define the organization
echo it should probably regenerated
echo "see https://libvirt.org/kbase/tlscerts.html#setting-up-a-certificate-authority-ca"
echo on how to regenerate it
exit 1
fi
echo Found CA certificate $CA/cacert.pem for $ORG
# Second the client certificates
if [ -f "$LIBVIRT/clientcert.pem" ]
then
if [ ! -r "$LIBVIRT/clientcert.pem" ]
then
echo Client certificate $LIBVIRT/clientcert.pem should be world readable
echo "as root do: chown root:root $LIBVIRT/clientcert.pem ; chmod 644 $LIBVIRT/clientcert.pem"
else
C_ORG=`"$CERTOOL" -i --infile "$LIBVIRT/clientcert.pem" | grep Subject: | sed 's+.*O=\([^,]*\).*+\1+'`
if [ "$ORG" != "$C_ORG" ]
then
echo The CA certificate and the client certificate do not match
echo CA organization: $ORG
echo Client organization: $C_ORG
fi
CLIENT=`"$CERTOOL" -i --infile "$LIBVIRT/clientcert.pem" | grep Subject: | sed 's+.*CN=\(.[^,]*\).*+\1+'`
echo Found client certificate $LIBVIRT/clientcert.pem for $CLIENT
if [ ! -e "$LIBVIRTP/clientkey.pem" ]
then
echo Missing client private key $LIBVIRTP/clientkey.pem
else
echo Found client private key $LIBVIRTP/clientkey.pem
OWN=`ls -l "$LIBVIRTP/clientkey.pem" | awk '{ print $3 }'`
# The substr($1, 1, 10) gets rid of acl and xattr markers
MOD=`ls -l "$LIBVIRTP/clientkey.pem" | awk '{ print substr($1, 1, 10) }'`
if [ "$OWN" != "root" ]
then
echo The client private key should be owned by root
echo "as root do: chown root $LIBVIRTP/clientkey.pem"
fi
if [ "$MOD" != "-rw-r--r--" ]
then
echo The client private key need to be read by client tools
echo "as root do: chmod 644 $LIBVIRTP/clientkey.pem"
fi
fi
fi
else
echo Did not find "$LIBVIRT/clientcert.pem" client certificate
echo The machine cannot act as a client
echo "see https://libvirt.org/kbase/tlscerts.html#issuing-client-certificates"
echo on how to regenerate it
CLIENT=0
fi
# Third the server certificates
if [ -f "$LIBVIRT/servercert.pem" ]
then
if [ ! -r "$LIBVIRT/servercert.pem" ]
then
echo Server certificate $LIBVIRT/servercert.pem should be world readable
echo "as root do: chown root:root $LIBVIRT/servercert.pem ; chmod 644 $LIBVIRT/servercert.pem"
else
S_ORG=`"$CERTOOL" -i --infile "$LIBVIRT/servercert.pem" | grep Subject: | sed 's+.*O=\([^,]*\).*+\1+'`
if [ "$ORG" != "$S_ORG" ]
then
echo The CA certificate and the server certificate do not match
echo CA organization: $ORG
echo Server organization: $S_ORG
fi
S_HOST=`"$CERTOOL" -i --infile "$LIBVIRT/servercert.pem" | grep Subject: | sed 's+.*CN=\([^,]*\).*+\1+'`
if test "$S_HOST" != "`hostname -s`" && test "$S_HOST" != "`hostname`"
then
echo The server certificate does not seem to match the host name
echo hostname: '"'`hostname`'"'
echo Server certificate CN: '"'$S_HOST'"'
fi
echo Found server certificate $LIBVIRT/servercert.pem for $S_HOST
if [ ! -e "$LIBVIRTP/serverkey.pem" ]
then
echo Missing server private key $LIBVIRTP/serverkey.pem
else
echo Found server private key $LIBVIRTP/serverkey.pem
OWN=`ls -l "$LIBVIRTP/serverkey.pem" | awk '{ print $3 }'`
# The substr($1, 1, 10) gets rid of acl and xattr markers
MOD=`ls -l "$LIBVIRTP/serverkey.pem" | awk '{ print substr($1, 1, 10) }'`
if [ "$OWN" != "root" ]
then
echo The server private key should be owned by root
echo "as root do: chown root $LIBVIRTP/serverkey.pem"
fi
if [ "$MOD" != "-rw-------" ]
then
echo The server private key need to be read only by root
echo "as root do: chmod 600 $LIBVIRTP/serverkey.pem"
fi
fi
fi
else
echo Did not find $LIBVIRT/servercert.pem server certificate
echo The machine cannot act as a server
echo "see https://libvirt.org/kbase/tlscerts.html#issuing-server-certificates"
echo on how to regenerate it
SERVER=0
fi
exit 0