From nobody Thu Sep 19 00:57:23 2024 Delivered-To: importer@patchew.org Received-SPF: pass (zohomail.com: domain of lists.libvirt.org designates 8.43.85.245 as permitted sender) client-ip=8.43.85.245; envelope-from=devel-bounces@lists.libvirt.org; helo=lists.libvirt.org; Authentication-Results: mx.zohomail.com; dkim=fail; spf=pass (zohomail.com: domain of lists.libvirt.org designates 8.43.85.245 as permitted sender) smtp.mailfrom=devel-bounces@lists.libvirt.org; dmarc=fail(p=none dis=none) header.from=redhat.com Return-Path: Received: from lists.libvirt.org (lists.libvirt.org [8.43.85.245]) by mx.zohomail.com with SMTPS id 1717770569998856.3828236383986; Fri, 7 Jun 2024 07:29:29 -0700 (PDT) Received: by lists.libvirt.org (Postfix, from userid 996) id D33612385; Fri, 7 Jun 2024 10:29:24 -0400 (EDT) Received: from lists.libvirt.org (localhost [IPv6:::1]) by lists.libvirt.org (Postfix) with ESMTP id 36ED32341; Fri, 7 Jun 2024 10:26:32 -0400 (EDT) Received: by lists.libvirt.org (Postfix, from userid 996) id 0851C2378; Fri, 7 Jun 2024 10:26:29 -0400 (EDT) Received: from us-smtp-delivery-124.mimecast.com (us-smtp-delivery-124.mimecast.com [170.10.133.124]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (No client certificate requested) by lists.libvirt.org (Postfix) with ESMTPS id C59C922F5 for ; Fri, 7 Jun 2024 10:26:23 -0400 (EDT) Received: from mimecast-mx02.redhat.com (mx-ext.redhat.com [66.187.233.73]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-646-Ra5h2QBNPZmMZ3jOLfNDuw-1; Fri, 07 Jun 2024 10:26:20 -0400 Received: from smtp.corp.redhat.com (int-mx09.intmail.prod.int.rdu2.redhat.com [10.11.54.9]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange X25519 server-signature RSA-PSS (2048 bits) server-digest SHA256) (No client certificate requested) by mimecast-mx02.redhat.com (Postfix) with ESMTPS id 4887F1C05129 for ; Fri, 7 Jun 2024 14:26:20 +0000 (UTC) Received: from toolbox.redhat.com (unknown [10.39.193.232]) by smtp.corp.redhat.com (Postfix) with ESMTP id 820A9492BC6; Fri, 7 Jun 2024 14:26:19 +0000 (UTC) X-Spam-Checker-Version: SpamAssassin 3.4.4 (2020-01-24) on lists.libvirt.org X-Spam-Level: X-Spam-Status: No, score=-0.6 required=5.0 tests=DKIM_INVALID,DKIM_SIGNED, HEADER_FROM_DIFFERENT_DOMAINS,MAILING_LIST_MULTI,RCVD_IN_DNSWL_NONE, RCVD_IN_MSPIKE_H4,RCVD_IN_MSPIKE_WL,SPF_HELO_NONE,T_SCC_BODY_TEXT_LINE autolearn=unavailable autolearn_force=no version=3.4.4 DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1717770382; h=from:from:reply-to:subject:subject:date:date:message-id:message-id: to:to:cc:cc:mime-version:mime-version:content-type:content-type: content-transfer-encoding:content-transfer-encoding: in-reply-to:in-reply-to:references:references; bh=AC2xbQ1VhLU2TE7NIblnO5b7PMrtxQZ491I5fl8C6bc=; b=I6EX3An6I8F/yugDCk9x/QD6pEDGzkQJ8fBKc/uyaQd/jM+GukX3i5GKlH8cKwOONkcGP3 BlI4bgUbOSBLOxHWuYQE5TVdmItmZTZ33UV5B5Bq1I28KEJSwcNiLOxeE0igWkTOlVYnyj SPER5/6QoLyWZ4zp4LzA07b0CPSFbps= X-MC-Unique: Ra5h2QBNPZmMZ3jOLfNDuw-1 From: =?UTF-8?q?Daniel=20P=2E=20Berrang=C3=A9?= To: devel@lists.libvirt.org Cc: =?UTF-8?q?Daniel=20P=2E=20Berrang=C3=A9?= Subject: [PATCH 3/9] rpc: split TLS cert validation into separate file Date: Fri, 7 Jun 2024 15:26:10 +0100 Message-ID: <20240607142616.749339-4-berrange@redhat.com> In-Reply-To: <20240607142616.749339-1-berrange@redhat.com> References: <20240607142616.749339-1-berrange@redhat.com> MIME-Version: 1.0 X-Scanned-By: MIMEDefang 3.4.1 on 10.11.54.9 X-Mimecast-Spam-Score: 0 X-Mimecast-Originator: redhat.com Content-Transfer-Encoding: quoted-printable Message-ID-Hash: D5ALL3WUXMIEKOMGR4EJMIVJZF3HXZLP X-Message-ID-Hash: D5ALL3WUXMIEKOMGR4EJMIVJZF3HXZLP X-MailFrom: berrange@redhat.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; emergency; loop; banned-address; member-moderation; header-match-config-1; header-match-config-2; header-match-config-3; header-match-devel.lists.libvirt.org-0; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; suspicious-header X-Mailman-Version: 3.2.2 Precedence: list List-Id: Development discussions about the libvirt library & tools Archived-At: List-Archive: List-Help: List-Post: List-Subscribe: List-Unsubscribe: X-ZohoMail-DKIM: fail (Header signature does not verify) X-ZM-MESSAGEID: 1717770572436100001 Content-Type: text/plain; charset="utf-8" The TLS cert validation logic will be reused for the new impl of the virt-pki-validate tool. Signed-off-by: Daniel P. Berrang=C3=A9 --- po/POTFILES | 1 + src/rpc/meson.build | 1 + src/rpc/virnettlscert.c | 553 +++++++++++++++++++++++++++++++++++++ src/rpc/virnettlscert.h | 42 +++ src/rpc/virnettlscontext.c | 535 +---------------------------------- 5 files changed, 606 insertions(+), 526 deletions(-) create mode 100644 src/rpc/virnettlscert.c create mode 100644 src/rpc/virnettlscert.h diff --git a/po/POTFILES b/po/POTFILES index 4ad7e19e08..0f68c652eb 100644 --- a/po/POTFILES +++ b/po/POTFILES @@ -219,6 +219,7 @@ src/rpc/virnetserverprogram.c src/rpc/virnetserverservice.c src/rpc/virnetsocket.c src/rpc/virnetsshsession.c +src/rpc/virnettlscert.c src/rpc/virnettlscontext.c src/secret/secret_driver.c src/security/security_apparmor.c diff --git a/src/rpc/meson.build b/src/rpc/meson.build index d11d532d0f..8bdbf5c88f 100644 --- a/src/rpc/meson.build +++ b/src/rpc/meson.build @@ -2,6 +2,7 @@ gendispatch_prog =3D find_program('gendispatch.pl') =20 tlsconfig_sources =3D [ files('virnettlsconfig.c'), + files('virnettlscert.c'), ] =20 socket_sources =3D tlsconfig_sources + [ diff --git a/src/rpc/virnettlscert.c b/src/rpc/virnettlscert.c new file mode 100644 index 0000000000..2e1e4c56d5 --- /dev/null +++ b/src/rpc/virnettlscert.c @@ -0,0 +1,553 @@ +/* + * virnettlscert.c: TLS x509 certificate helpers + * + * Copyright (C) 2010-2024 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 + * . + */ + +#include + +#include "virnettlscert.h" + +#include "viralloc.h" +#include "virfile.h" +#include "virlog.h" +#include "virerror.h" + +#define VIR_FROM_THIS VIR_FROM_NONE + +VIR_LOG_INIT("rpc.nettlscert"); + +static int virNetTLSCertCheckTimes(gnutls_x509_crt_t cert, + const char *certFile, + bool isServer, + bool isCA) +{ + time_t now; + + if ((now =3D time(NULL)) =3D=3D ((time_t)-1)) { + virReportSystemError(errno, "%s", + _("cannot get current time")); + return -1; + } + + if (gnutls_x509_crt_get_expiration_time(cert) < now) { + virReportError(VIR_ERR_SYSTEM_ERROR, + (isCA ? + _("The CA certificate %1$s has expired") : + (isServer ? + _("The server certificate %1$s has expired") : + _("The client certificate %1$s has expired"))), + certFile); + return -1; + } + + if (gnutls_x509_crt_get_activation_time(cert) > now) { + virReportError(VIR_ERR_SYSTEM_ERROR, + (isCA ? + _("The CA certificate %1$s is not yet active") : + (isServer ? + _("The server certificate %1$s is not yet active"= ) : + _("The client certificate %1$s is not yet active"= ))), + certFile); + return -1; + } + + return 0; +} + + +static int virNetTLSCertCheckBasicConstraints(gnutls_x509_crt_t cert, + const char *certFile, + bool isServer, + bool isCA) +{ + int status; + + status =3D gnutls_x509_crt_get_basic_constraints(cert, NULL, NULL, NUL= L); + VIR_DEBUG("Cert %s basic constraints %d", certFile, status); + + if (status > 0) { /* It is a CA cert */ + if (!isCA) { + virReportError(VIR_ERR_SYSTEM_ERROR, isServer ? + _("The certificate %1$s basic constraints show = a CA, but we need one for a server") : + _("The certificate %1$s basic constraints show = a CA, but we need one for a client"), + certFile); + return -1; + } + } else if (status =3D=3D 0) { /* It is not a CA cert */ + if (isCA) { + virReportError(VIR_ERR_SYSTEM_ERROR, + _("The certificate %1$s basic constraints do no= t show a CA"), + certFile); + return -1; + } + } else if (status =3D=3D GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) { /* M= issing basicConstraints */ + if (isCA) { + virReportError(VIR_ERR_SYSTEM_ERROR, + _("The certificate %1$s is missing basic constr= aints for a CA"), + certFile); + return -1; + } + } else { /* General error */ + virReportError(VIR_ERR_SYSTEM_ERROR, + _("Unable to query certificate %1$s basic constrain= ts %2$s"), + certFile, gnutls_strerror(status)); + return -1; + } + + return 0; +} + + +static int virNetTLSCertCheckKeyUsage(gnutls_x509_crt_t cert, + const char *certFile, + bool isCA) +{ + int status; + unsigned int usage =3D 0; + unsigned int critical =3D 0; + + status =3D gnutls_x509_crt_get_key_usage(cert, &usage, &critical); + + VIR_DEBUG("Cert %s key usage status %d usage %d critical %u", certFile= , status, usage, critical); + if (status < 0) { + if (status =3D=3D GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) { + usage =3D isCA ? GNUTLS_KEY_KEY_CERT_SIGN : + GNUTLS_KEY_DIGITAL_SIGNATURE|GNUTLS_KEY_KEY_ENCIPHERMENT; + } else { + virReportError(VIR_ERR_SYSTEM_ERROR, + _("Unable to query certificate %1$s key usage %= 2$s"), + certFile, gnutls_strerror(status)); + return -1; + } + } + + if (isCA) { + if (!(usage & GNUTLS_KEY_KEY_CERT_SIGN)) { + if (critical) { + virReportError(VIR_ERR_SYSTEM_ERROR, + _("Certificate %1$s usage does not permit c= ertificate signing"), + certFile); + return -1; + } else { + VIR_WARN("Certificate %s usage does not permit certificate= signing", + certFile); + } + } + } else { + if (!(usage & GNUTLS_KEY_DIGITAL_SIGNATURE)) { + if (critical) { + virReportError(VIR_ERR_SYSTEM_ERROR, + _("Certificate %1$s usage does not permit d= igital signature"), + certFile); + return -1; + } else { + VIR_WARN("Certificate %s usage does not permit digital sig= nature", + certFile); + } + } + if (!(usage & GNUTLS_KEY_KEY_ENCIPHERMENT)) { + if (critical) { + virReportError(VIR_ERR_SYSTEM_ERROR, + _("Certificate %1$s usage does not permit k= ey encipherment"), + certFile); + return -1; + } else { + VIR_WARN("Certificate %s usage does not permit key enciphe= rment", + certFile); + } + } + } + + return 0; +} + + +static int virNetTLSCertCheckKeyPurpose(gnutls_x509_crt_t cert, + const char *certFile, + bool isServer) +{ + int status; + size_t i; + unsigned int purposeCritical; + unsigned int critical; + char *buffer =3D NULL; + size_t size; + bool allowClient =3D false, allowServer =3D false; + + critical =3D 0; + for (i =3D 0; ; i++) { + size =3D 0; + status =3D gnutls_x509_crt_get_key_purpose_oid(cert, i, buffer, &s= ize, NULL); + + if (status =3D=3D GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) { + VIR_DEBUG("No key purpose data available at slot %zu", i); + + /* If there is no data at all, then we must allow client/serve= r to pass */ + if (i =3D=3D 0) + allowServer =3D allowClient =3D true; + break; + } + if (status !=3D GNUTLS_E_SHORT_MEMORY_BUFFER) { + virReportError(VIR_ERR_SYSTEM_ERROR, + _("Unable to query certificate %1$s key purpose= %2$s"), + certFile, gnutls_strerror(status)); + return -1; + } + + buffer =3D g_new0(char, size); + status =3D gnutls_x509_crt_get_key_purpose_oid(cert, i, buffer, &s= ize, &purposeCritical); + if (status < 0) { + VIR_FREE(buffer); + virReportError(VIR_ERR_SYSTEM_ERROR, + _("Unable to query certificate %1$s key purpose= %2$s"), + certFile, gnutls_strerror(status)); + return -1; + } + if (purposeCritical) + critical =3D true; + + VIR_DEBUG("Key purpose %d %s critical %u", status, buffer, purpose= Critical); + if (STREQ(buffer, GNUTLS_KP_TLS_WWW_SERVER)) { + allowServer =3D true; + } else if (STREQ(buffer, GNUTLS_KP_TLS_WWW_CLIENT)) { + allowClient =3D true; + } else if (STRNEQ(buffer, GNUTLS_KP_ANY)) { + allowServer =3D allowClient =3D true; + } + + VIR_FREE(buffer); + } + + if (isServer) { + if (!allowServer) { + if (critical) { + virReportError(VIR_ERR_SYSTEM_ERROR, + _("Certificate %1$s purpose does not allow = use for with a TLS server"), + certFile); + return -1; + } else { + VIR_WARN("Certificate %s purpose does not allow use for wi= th a TLS server", + certFile); + } + } + } else { + if (!allowClient) { + if (critical) { + virReportError(VIR_ERR_SYSTEM_ERROR, + _("Certificate %1$s purpose does not allow = use for with a TLS client"), + certFile); + return -1; + } else { + VIR_WARN("Certificate %s purpose does not allow use for wi= th a TLS client", + certFile); + } + } + } + + return 0; +} + +/* Check DN is on tls_allowed_dn_list. */ +static int +virNetTLSCertCheckDNACL(const char *dname, + const char *const *wildcards) +{ + while (*wildcards) { + if (g_pattern_match_simple(*wildcards, dname)) + return 1; + + wildcards++; + } + + /* Log the client's DN for debugging */ + VIR_DEBUG("Failed ACL check for client DN '%s'", dname); + + /* This is the most common error: make it informative. */ + virReportError(VIR_ERR_SYSTEM_ERROR, "%s", + _("Client's Distinguished Name is not on the list of al= lowed clients (tls_allowed_dn_list). Use 'virt-pki-query-dn clientcert.pem= ' to view the Distinguished Name field in the client certificate, or run th= is daemon with --verbose option.")); + return 0; +} + + +static int +virNetTLSCertCheckDN(gnutls_x509_crt_t cert, + const char *certFile, + const char *hostname, + const char *dname, + const char *const *acl) +{ + if (acl && dname && + virNetTLSCertCheckDNACL(dname, acl) <=3D 0) + return -1; + + if (hostname && + !gnutls_x509_crt_check_hostname(cert, hostname)) { + virReportError(VIR_ERR_RPC, + _("Certificate %1$s owner does not match the hostna= me %2$s"), + certFile, hostname); + return -1; + } + + return 0; +} + + +static int virNetTLSCertCheck(gnutls_x509_crt_t cert, + const char *certFile, + bool isServer, + bool isCA) +{ + if (virNetTLSCertCheckTimes(cert, certFile, isServer, isCA) < 0) + return -1; + + if (virNetTLSCertCheckBasicConstraints(cert, certFile, isServer, isCA)= < 0) + return -1; + + if (virNetTLSCertCheckKeyUsage(cert, certFile, isCA) < 0) + return -1; + + if (!isCA && + virNetTLSCertCheckKeyPurpose(cert, certFile, isServer) < 0) + return -1; + + return 0; +} + + +static int virNetTLSCertCheckPair(gnutls_x509_crt_t cert, + const char *certFile, + gnutls_x509_crt_t *cacerts, + size_t ncacerts, + const char *cacertFile, + bool isServer) +{ + unsigned int status; + + if (gnutls_x509_crt_list_verify(&cert, 1, + cacerts, ncacerts, + NULL, 0, + 0, &status) < 0) { + virReportError(VIR_ERR_SYSTEM_ERROR, isServer ? + _("Unable to verify server certificate %1$s against= CA certificate %2$s") : + _("Unable to verify client certificate %1$s against= CA certificate %2$s"), + certFile, cacertFile); + return -1; + } + + if (status !=3D 0) { + const char *reason =3D _("Invalid certificate"); + + if (status & GNUTLS_CERT_INVALID) + reason =3D _("The certificate is not trusted."); + + if (status & GNUTLS_CERT_SIGNER_NOT_FOUND) + reason =3D _("The certificate hasn't got a known issuer."); + + if (status & GNUTLS_CERT_REVOKED) + reason =3D _("The certificate has been revoked."); + + if (status & GNUTLS_CERT_INSECURE_ALGORITHM) + reason =3D _("The certificate uses an insecure algorithm"); + + virReportError(VIR_ERR_SYSTEM_ERROR, + _("Our own certificate %1$s failed validation again= st %2$s: %3$s"), + certFile, cacertFile, reason); + return -1; + } + + return 0; +} + + +gnutls_x509_crt_t virNetTLSCertLoadFromFile(const char *certFile, + bool isServer) +{ + gnutls_datum_t data; + gnutls_x509_crt_t cert =3D NULL; + g_autofree char *buf =3D NULL; + int ret =3D -1; + + VIR_DEBUG("isServer %d certFile %s", + isServer, certFile); + + if (gnutls_x509_crt_init(&cert) < 0) { + virReportError(VIR_ERR_SYSTEM_ERROR, "%s", + _("Unable to initialize certificate")); + goto cleanup; + } + + if (virFileReadAll(certFile, (1<<16), &buf) < 0) + goto cleanup; + + data.data =3D (unsigned char *)buf; + data.size =3D strlen(buf); + + if (gnutls_x509_crt_import(cert, &data, GNUTLS_X509_FMT_PEM) < 0) { + virReportError(VIR_ERR_SYSTEM_ERROR, isServer ? + _("Unable to import server certificate %1$s") : + _("Unable to import client certificate %1$s"), + certFile); + goto cleanup; + } + + ret =3D 0; + + cleanup: + if (ret !=3D 0) { + g_clear_pointer(&cert, gnutls_x509_crt_deinit); + } + return cert; +} + + +static int virNetTLSCertLoadCAListFromFile(const char *certFile, + gnutls_x509_crt_t *certs, + unsigned int certMax, + size_t *ncerts) +{ + gnutls_datum_t data; + g_autofree char *buf =3D NULL; + + *ncerts =3D 0; + VIR_DEBUG("certFile %s", certFile); + + if (virFileReadAll(certFile, (1<<16), &buf) < 0) + return -1; + + data.data =3D (unsigned char *)buf; + data.size =3D strlen(buf); + + if (gnutls_x509_crt_list_import(certs, &certMax, &data, GNUTLS_X509_FM= T_PEM, 0) < 0) { + virReportError(VIR_ERR_SYSTEM_ERROR, + _("Unable to import CA certificate list %1$s"), + certFile); + return -1; + } + *ncerts =3D certMax; + + return 0; +} + + +#define MAX_CERTS 16 +int virNetTLSCertSanityCheck(bool isServer, + const char *cacertFile, + const char *certFile) +{ + gnutls_x509_crt_t cert =3D NULL; + gnutls_x509_crt_t cacerts[MAX_CERTS] =3D { 0 }; + size_t ncacerts =3D 0; + size_t i; + int ret =3D -1; + + if ((access(certFile, R_OK) =3D=3D 0) && + !(cert =3D virNetTLSCertLoadFromFile(certFile, isServer))) + goto cleanup; + if ((access(cacertFile, R_OK) =3D=3D 0) && + virNetTLSCertLoadCAListFromFile(cacertFile, cacerts, + MAX_CERTS, &ncacerts) < 0) + goto cleanup; + + if (cert && + virNetTLSCertCheck(cert, certFile, isServer, false) < 0) + goto cleanup; + + for (i =3D 0; i < ncacerts; i++) { + if (virNetTLSCertCheck(cacerts[i], cacertFile, isServer, true) < 0) + goto cleanup; + } + + if (cert && ncacerts && + virNetTLSCertCheckPair(cert, certFile, cacerts, ncacerts, cacertFi= le, isServer) < 0) + goto cleanup; + + ret =3D 0; + + cleanup: + if (cert) + gnutls_x509_crt_deinit(cert); + for (i =3D 0; i < ncacerts; i++) + gnutls_x509_crt_deinit(cacerts[i]); + return ret; +} + +int virNetTLSCertValidateCA(gnutls_x509_crt_t cert, + bool isServer) +{ + if (virNetTLSCertCheckTimes(cert, "[session]", + isServer, true) < 0) { + return -1; + } + return 0; +} + +char *virNetTLSCertValidate(gnutls_x509_crt_t cert, + bool isServer, + const char *hostname, + const char *const *x509dnACL) +{ + size_t dnamesize =3D 256; + g_autofree char *dname =3D g_new0(char, dnamesize); + int ret; + + if (virNetTLSCertCheckTimes(cert, "[session]", + isServer, false) < 0) { + return NULL; + } + + ret =3D gnutls_x509_crt_get_dn(cert, dname, &dnamesize); + if (ret =3D=3D GNUTLS_E_SHORT_MEMORY_BUFFER) { + VIR_DEBUG("Reallocating dname to fit %zu bytes", dnamesize); + dname =3D g_realloc(dname, dnamesize); + ret =3D gnutls_x509_crt_get_dn(cert, dname, &dnamesize); + } + if (ret !=3D 0) { + virReportError(VIR_ERR_SYSTEM_ERROR, + _("Failed to get certificate %1$s distinguished nam= e: %2$s"), + "[session]", gnutls_strerror(ret)); + return NULL; + } + + VIR_DEBUG("Peer DN is %s", dname); + + if (virNetTLSCertCheckDN(cert, "[session]", hostname, + dname, x509dnACL) < 0) { + return NULL; + } + + /* !isServer, since on the client, we're validating the + * server's cert, and on the server, the client's cert + */ + if (virNetTLSCertCheckBasicConstraints(cert, "[session]", + !isServer, false) < 0) { + return NULL; + } + + if (virNetTLSCertCheckKeyUsage(cert, "[session]", + false) < 0) { + return NULL; + } + + /* !isServer - as above */ + if (virNetTLSCertCheckKeyPurpose(cert, "[session]", + !isServer) < 0) { + return NULL; + } + + return g_steal_pointer(&dname); +} diff --git a/src/rpc/virnettlscert.h b/src/rpc/virnettlscert.h new file mode 100644 index 0000000000..0ac511a141 --- /dev/null +++ b/src/rpc/virnettlscert.h @@ -0,0 +1,42 @@ +/* + * virnettlscert.h: TLS x509 certificate helpers + * + * Copyright (C) 2010-2024 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 + * . + */ + +#pragma once + +#include +#include +#include + +#include "internal.h" + +int virNetTLSCertSanityCheck(bool isServer, + const char *cacertFile, + const char *certFile); + +int virNetTLSCertValidateCA(gnutls_x509_crt_t cert, + bool isServer); + +char *virNetTLSCertValidate(gnutls_x509_crt_t cert, + bool isServer, + const char *hostname, + const char *const *x509dnACL); + +gnutls_x509_crt_t virNetTLSCertLoadFromFile(const char *certFile, + bool isServer); diff --git a/src/rpc/virnettlscontext.c b/src/rpc/virnettlscontext.c index a56abee009..e8023133b4 100644 --- a/src/rpc/virnettlscontext.c +++ b/src/rpc/virnettlscontext.c @@ -28,6 +28,7 @@ =20 #include "virnettlscontext.h" #include "virnettlsconfig.h" +#include "virnettlscert.h" #include "virstring.h" =20 #include "viralloc.h" @@ -110,466 +111,6 @@ static void virNetTLSLog(int level G_GNUC_UNUSED, } =20 =20 -static int virNetTLSContextCheckCertTimes(gnutls_x509_crt_t cert, - const char *certFile, - bool isServer, - bool isCA) -{ - time_t now; - - if ((now =3D time(NULL)) =3D=3D ((time_t)-1)) { - virReportSystemError(errno, "%s", - _("cannot get current time")); - return -1; - } - - if (gnutls_x509_crt_get_expiration_time(cert) < now) { - virReportError(VIR_ERR_SYSTEM_ERROR, - (isCA ? - _("The CA certificate %1$s has expired") : - (isServer ? - _("The server certificate %1$s has expired") : - _("The client certificate %1$s has expired"))), - certFile); - return -1; - } - - if (gnutls_x509_crt_get_activation_time(cert) > now) { - virReportError(VIR_ERR_SYSTEM_ERROR, - (isCA ? - _("The CA certificate %1$s is not yet active") : - (isServer ? - _("The server certificate %1$s is not yet active"= ) : - _("The client certificate %1$s is not yet active"= ))), - certFile); - return -1; - } - - return 0; -} - - -static int virNetTLSContextCheckCertBasicConstraints(gnutls_x509_crt_t cer= t, - const char *certFile, - bool isServer, - bool isCA) -{ - int status; - - status =3D gnutls_x509_crt_get_basic_constraints(cert, NULL, NULL, NUL= L); - VIR_DEBUG("Cert %s basic constraints %d", certFile, status); - - if (status > 0) { /* It is a CA cert */ - if (!isCA) { - virReportError(VIR_ERR_SYSTEM_ERROR, isServer ? - _("The certificate %1$s basic constraints show = a CA, but we need one for a server") : - _("The certificate %1$s basic constraints show = a CA, but we need one for a client"), - certFile); - return -1; - } - } else if (status =3D=3D 0) { /* It is not a CA cert */ - if (isCA) { - virReportError(VIR_ERR_SYSTEM_ERROR, - _("The certificate %1$s basic constraints do no= t show a CA"), - certFile); - return -1; - } - } else if (status =3D=3D GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) { /* M= issing basicConstraints */ - if (isCA) { - virReportError(VIR_ERR_SYSTEM_ERROR, - _("The certificate %1$s is missing basic constr= aints for a CA"), - certFile); - return -1; - } - } else { /* General error */ - virReportError(VIR_ERR_SYSTEM_ERROR, - _("Unable to query certificate %1$s basic constrain= ts %2$s"), - certFile, gnutls_strerror(status)); - return -1; - } - - return 0; -} - - -static int virNetTLSContextCheckCertKeyUsage(gnutls_x509_crt_t cert, - const char *certFile, - bool isCA) -{ - int status; - unsigned int usage =3D 0; - unsigned int critical =3D 0; - - status =3D gnutls_x509_crt_get_key_usage(cert, &usage, &critical); - - VIR_DEBUG("Cert %s key usage status %d usage %d critical %u", certFile= , status, usage, critical); - if (status < 0) { - if (status =3D=3D GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) { - usage =3D isCA ? GNUTLS_KEY_KEY_CERT_SIGN : - GNUTLS_KEY_DIGITAL_SIGNATURE|GNUTLS_KEY_KEY_ENCIPHERMENT; - } else { - virReportError(VIR_ERR_SYSTEM_ERROR, - _("Unable to query certificate %1$s key usage %= 2$s"), - certFile, gnutls_strerror(status)); - return -1; - } - } - - if (isCA) { - if (!(usage & GNUTLS_KEY_KEY_CERT_SIGN)) { - if (critical) { - virReportError(VIR_ERR_SYSTEM_ERROR, - _("Certificate %1$s usage does not permit c= ertificate signing"), - certFile); - return -1; - } else { - VIR_WARN("Certificate %s usage does not permit certificate= signing", - certFile); - } - } - } else { - if (!(usage & GNUTLS_KEY_DIGITAL_SIGNATURE)) { - if (critical) { - virReportError(VIR_ERR_SYSTEM_ERROR, - _("Certificate %1$s usage does not permit d= igital signature"), - certFile); - return -1; - } else { - VIR_WARN("Certificate %s usage does not permit digital sig= nature", - certFile); - } - } - if (!(usage & GNUTLS_KEY_KEY_ENCIPHERMENT)) { - if (critical) { - virReportError(VIR_ERR_SYSTEM_ERROR, - _("Certificate %1$s usage does not permit k= ey encipherment"), - certFile); - return -1; - } else { - VIR_WARN("Certificate %s usage does not permit key enciphe= rment", - certFile); - } - } - } - - return 0; -} - - -static int virNetTLSContextCheckCertKeyPurpose(gnutls_x509_crt_t cert, - const char *certFile, - bool isServer) -{ - int status; - size_t i; - unsigned int purposeCritical; - unsigned int critical; - char *buffer =3D NULL; - size_t size; - bool allowClient =3D false, allowServer =3D false; - - critical =3D 0; - for (i =3D 0; ; i++) { - size =3D 0; - status =3D gnutls_x509_crt_get_key_purpose_oid(cert, i, buffer, &s= ize, NULL); - - if (status =3D=3D GNUTLS_E_REQUESTED_DATA_NOT_AVAILABLE) { - VIR_DEBUG("No key purpose data available at slot %zu", i); - - /* If there is no data at all, then we must allow client/serve= r to pass */ - if (i =3D=3D 0) - allowServer =3D allowClient =3D true; - break; - } - if (status !=3D GNUTLS_E_SHORT_MEMORY_BUFFER) { - virReportError(VIR_ERR_SYSTEM_ERROR, - _("Unable to query certificate %1$s key purpose= %2$s"), - certFile, gnutls_strerror(status)); - return -1; - } - - buffer =3D g_new0(char, size); - status =3D gnutls_x509_crt_get_key_purpose_oid(cert, i, buffer, &s= ize, &purposeCritical); - if (status < 0) { - VIR_FREE(buffer); - virReportError(VIR_ERR_SYSTEM_ERROR, - _("Unable to query certificate %1$s key purpose= %2$s"), - certFile, gnutls_strerror(status)); - return -1; - } - if (purposeCritical) - critical =3D true; - - VIR_DEBUG("Key purpose %d %s critical %u", status, buffer, purpose= Critical); - if (STREQ(buffer, GNUTLS_KP_TLS_WWW_SERVER)) { - allowServer =3D true; - } else if (STREQ(buffer, GNUTLS_KP_TLS_WWW_CLIENT)) { - allowClient =3D true; - } else if (STRNEQ(buffer, GNUTLS_KP_ANY)) { - allowServer =3D allowClient =3D true; - } - - VIR_FREE(buffer); - } - - if (isServer) { - if (!allowServer) { - if (critical) { - virReportError(VIR_ERR_SYSTEM_ERROR, - _("Certificate %1$s purpose does not allow = use for with a TLS server"), - certFile); - return -1; - } else { - VIR_WARN("Certificate %s purpose does not allow use for wi= th a TLS server", - certFile); - } - } - } else { - if (!allowClient) { - if (critical) { - virReportError(VIR_ERR_SYSTEM_ERROR, - _("Certificate %1$s purpose does not allow = use for with a TLS client"), - certFile); - return -1; - } else { - VIR_WARN("Certificate %s purpose does not allow use for wi= th a TLS client", - certFile); - } - } - } - - return 0; -} - -/* Check DN is on tls_allowed_dn_list. */ -static int -virNetTLSContextCheckCertDNACL(const char *dname, - const char *const *wildcards) -{ - while (*wildcards) { - if (g_pattern_match_simple(*wildcards, dname)) - return 1; - - wildcards++; - } - - /* Log the client's DN for debugging */ - VIR_DEBUG("Failed ACL check for client DN '%s'", dname); - - /* This is the most common error: make it informative. */ - virReportError(VIR_ERR_SYSTEM_ERROR, "%s", - _("Client's Distinguished Name is not on the list of al= lowed clients (tls_allowed_dn_list). Use 'virt-pki-query-dn clientcert.pem= ' to view the Distinguished Name field in the client certificate, or run th= is daemon with --verbose option.")); - return 0; -} - - -static int -virNetTLSContextCheckCertDN(gnutls_x509_crt_t cert, - const char *certFile, - const char *hostname, - const char *dname, - const char *const *acl) -{ - if (acl && dname && - virNetTLSContextCheckCertDNACL(dname, acl) <=3D 0) - return -1; - - if (hostname && - !gnutls_x509_crt_check_hostname(cert, hostname)) { - virReportError(VIR_ERR_RPC, - _("Certificate %1$s owner does not match the hostna= me %2$s"), - certFile, hostname); - return -1; - } - - return 0; -} - - -static int virNetTLSContextCheckCert(gnutls_x509_crt_t cert, - const char *certFile, - bool isServer, - bool isCA) -{ - if (virNetTLSContextCheckCertTimes(cert, certFile, - isServer, isCA) < 0) - return -1; - - if (virNetTLSContextCheckCertBasicConstraints(cert, certFile, - isServer, isCA) < 0) - return -1; - - if (virNetTLSContextCheckCertKeyUsage(cert, certFile, - isCA) < 0) - return -1; - - if (!isCA && - virNetTLSContextCheckCertKeyPurpose(cert, certFile, - isServer) < 0) - return -1; - - return 0; -} - - -static int virNetTLSContextCheckCertPair(gnutls_x509_crt_t cert, - const char *certFile, - gnutls_x509_crt_t *cacerts, - size_t ncacerts, - const char *cacertFile, - bool isServer) -{ - unsigned int status; - - if (gnutls_x509_crt_list_verify(&cert, 1, - cacerts, ncacerts, - NULL, 0, - 0, &status) < 0) { - virReportError(VIR_ERR_SYSTEM_ERROR, isServer ? - _("Unable to verify server certificate %1$s against= CA certificate %2$s") : - _("Unable to verify client certificate %1$s against= CA certificate %2$s"), - certFile, cacertFile); - return -1; - } - - if (status !=3D 0) { - const char *reason =3D _("Invalid certificate"); - - if (status & GNUTLS_CERT_INVALID) - reason =3D _("The certificate is not trusted."); - - if (status & GNUTLS_CERT_SIGNER_NOT_FOUND) - reason =3D _("The certificate hasn't got a known issuer."); - - if (status & GNUTLS_CERT_REVOKED) - reason =3D _("The certificate has been revoked."); - - if (status & GNUTLS_CERT_INSECURE_ALGORITHM) - reason =3D _("The certificate uses an insecure algorithm"); - - virReportError(VIR_ERR_SYSTEM_ERROR, - _("Our own certificate %1$s failed validation again= st %2$s: %3$s"), - certFile, cacertFile, reason); - return -1; - } - - return 0; -} - - -static gnutls_x509_crt_t virNetTLSContextLoadCertFromFile(const char *cert= File, - bool isServer) -{ - gnutls_datum_t data; - gnutls_x509_crt_t cert =3D NULL; - g_autofree char *buf =3D NULL; - int ret =3D -1; - - VIR_DEBUG("isServer %d certFile %s", - isServer, certFile); - - if (gnutls_x509_crt_init(&cert) < 0) { - virReportError(VIR_ERR_SYSTEM_ERROR, "%s", - _("Unable to initialize certificate")); - goto cleanup; - } - - if (virFileReadAll(certFile, (1<<16), &buf) < 0) - goto cleanup; - - data.data =3D (unsigned char *)buf; - data.size =3D strlen(buf); - - if (gnutls_x509_crt_import(cert, &data, GNUTLS_X509_FMT_PEM) < 0) { - virReportError(VIR_ERR_SYSTEM_ERROR, isServer ? - _("Unable to import server certificate %1$s") : - _("Unable to import client certificate %1$s"), - certFile); - goto cleanup; - } - - ret =3D 0; - - cleanup: - if (ret !=3D 0) { - g_clear_pointer(&cert, gnutls_x509_crt_deinit); - } - return cert; -} - - -static int virNetTLSContextLoadCACertListFromFile(const char *certFile, - gnutls_x509_crt_t *certs, - unsigned int certMax, - size_t *ncerts) -{ - gnutls_datum_t data; - g_autofree char *buf =3D NULL; - - *ncerts =3D 0; - VIR_DEBUG("certFile %s", certFile); - - if (virFileReadAll(certFile, (1<<16), &buf) < 0) - return -1; - - data.data =3D (unsigned char *)buf; - data.size =3D strlen(buf); - - if (gnutls_x509_crt_list_import(certs, &certMax, &data, GNUTLS_X509_FM= T_PEM, 0) < 0) { - virReportError(VIR_ERR_SYSTEM_ERROR, - _("Unable to import CA certificate list %1$s"), - certFile); - return -1; - } - *ncerts =3D certMax; - - return 0; -} - - -#define MAX_CERTS 16 -static int virNetTLSContextSanityCheckCredentials(bool isServer, - const char *cacertFile, - const char *certFile) -{ - gnutls_x509_crt_t cert =3D NULL; - gnutls_x509_crt_t cacerts[MAX_CERTS] =3D { 0 }; - size_t ncacerts =3D 0; - size_t i; - int ret =3D -1; - - if ((access(certFile, R_OK) =3D=3D 0) && - !(cert =3D virNetTLSContextLoadCertFromFile(certFile, isServer))) - goto cleanup; - if ((access(cacertFile, R_OK) =3D=3D 0) && - virNetTLSContextLoadCACertListFromFile(cacertFile, cacerts, - MAX_CERTS, &ncacerts) < 0) - goto cleanup; - - if (cert && - virNetTLSContextCheckCert(cert, certFile, isServer, false) < 0) - goto cleanup; - - for (i =3D 0; i < ncacerts; i++) { - if (virNetTLSContextCheckCert(cacerts[i], cacertFile, isServer, tr= ue) < 0) - goto cleanup; - } - - if (cert && ncacerts && - virNetTLSContextCheckCertPair(cert, certFile, cacerts, ncacerts, c= acertFile, isServer) < 0) - goto cleanup; - - ret =3D 0; - - cleanup: - if (cert) - gnutls_x509_crt_deinit(cert); - for (i =3D 0; i < ncacerts; i++) - gnutls_x509_crt_deinit(cacerts[i]); - return ret; -} - - static int virNetTLSContextLoadCredentials(virNetTLSContext *ctxt, bool isServer, const char *cacert, @@ -683,7 +224,7 @@ static virNetTLSContext *virNetTLSContextNew(const char= *cacert, } =20 if (sanityCheckCert && - virNetTLSContextSanityCheckCredentials(isServer, cacert, cert) < 0) + virNetTLSCertSanityCheck(isServer, cacert, cert) < 0) goto error; =20 if (virNetTLSContextLoadCredentials(ctxt, isServer, cacert, cacrl, cer= t, key) < 0) @@ -846,7 +387,7 @@ int virNetTLSContextReloadForServer(virNetTLSContext *c= txt, goto error; } =20 - if (virNetTLSContextSanityCheckCredentials(true, cacert, cert)) + if (virNetTLSCertSanityCheck(true, cacert, cert)) goto error; =20 if (virNetTLSContextLoadCredentials(ctxt, true, cacert, cacrl, cert, k= ey)) @@ -876,65 +417,6 @@ virNetTLSContext *virNetTLSContextNewClient(const char= *cacert, sanityCheckCert, requireValidCert, false); } =20 -static int virNetTLSContextCertValidateCA(gnutls_x509_crt_t cert, - bool isServer) -{ - if (virNetTLSContextCheckCertTimes(cert, "[session]", isServer, true) = < 0) - return -1; - - return 0; -} - -static char *virNetTLSContextCertValidate(gnutls_x509_crt_t cert, - bool isServer, - const char *hostname, - const char *const *x509dnACL) -{ - size_t dnamesize =3D 256; - g_autofree char *dname =3D g_new0(char, dnamesize); - int ret; - - if (virNetTLSContextCheckCertTimes(cert, "[session]", - isServer, false) < 0) - return NULL; - - ret =3D gnutls_x509_crt_get_dn(cert, dname, &dnamesize); - if (ret =3D=3D GNUTLS_E_SHORT_MEMORY_BUFFER) { - VIR_DEBUG("Reallocating dname to fit %zu bytes", dnamesize); - dname =3D g_realloc(dname, dnamesize); - ret =3D gnutls_x509_crt_get_dn(cert, dname, &dnamesize); - } - if (ret !=3D 0) { - virReportError(VIR_ERR_SYSTEM_ERROR, - _("Failed to get certificate %1$s distinguished nam= e: %2$s"), - "[session]", gnutls_strerror(ret)); - return NULL; - } - - VIR_DEBUG("Peer DN is %s", dname); - - if (virNetTLSContextCheckCertDN(cert, "[session]", hostname, - dname, x509dnACL) < 0) - return NULL; - - /* !isServer, since on the client, we're validating the - * server's cert, and on the server, the client's cert - */ - if (virNetTLSContextCheckCertBasicConstraints(cert, "[session]", - !isServer, false) < 0) - return NULL; - - if (virNetTLSContextCheckCertKeyUsage(cert, "[session]", - false) < 0) - return NULL; - - /* !isServer - as above */ - if (virNetTLSContextCheckCertKeyPurpose(cert, "[session]", - !isServer) < 0) - return NULL; - - return g_steal_pointer(&dname); -} =20 static int virNetTLSContextValidCertificate(virNetTLSContext *ctxt, virNetTLSSession *sess) @@ -1005,15 +487,16 @@ static int virNetTLSContextValidCertificate(virNetTL= SContext *ctxt, } =20 if (i =3D=3D 0) { - if (!(sess->x509dname =3D virNetTLSContextCertValidate(cert, - sess->isS= erver, - sess->hos= tname, - ctxt->x50= 9dnACL))) { + if (!(sess->x509dname =3D virNetTLSCertValidate(cert, + sess->isServer, + sess->hostname, + ctxt->x509dnACL)= )) { gnutls_x509_crt_deinit(cert); goto authdeny; } } else { - if (virNetTLSContextCertValidateCA(cert, sess->isServer) < 0) { + if (virNetTLSCertValidateCA(cert, + sess->isServer) < 0) { gnutls_x509_crt_deinit(cert); goto authdeny; } --=20 2.43.0