From nobody Mon Oct 27 13:50:42 2025 Delivered-To: importer@patchew.org Authentication-Results: mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass(p=quarantine dis=none) header.from=redhat.com ARC-Seal: i=1; a=rsa-sha256; t=1761312052; cv=none; d=zohomail.com; s=zohoarc; b=Li2e7IilK0zvqV3QdCtqgNhf3cylp0UXRho1rFoP5Rhh0xiNFMT2JCgAzOXlX2EdnQIM8MXed56K3VhBR8Q17tmWFDaKC8WWaG5sl6pH7jKwN6DRce/w/wDDYDG923JUuMheLz/rNilCAdLIdy8dP1l4uDYXNQfvtvVJwFWqpm8= ARC-Message-Signature: i=1; a=rsa-sha256; c=relaxed/relaxed; d=zohomail.com; s=zohoarc; t=1761312052; h=Content-Type:Content-Transfer-Encoding:Cc:Cc:Date:Date:From:From:In-Reply-To:List-Subscribe:List-Post:List-Id:List-Archive:List-Help:List-Unsubscribe:MIME-Version:Message-ID:References:Sender:Subject:Subject:To:To:Message-Id:Reply-To; bh=sRUaQrfk7K27n90oxEwxQVJ2+OTJlIxRwQMTmmkikRk=; b=ZGW6O7Av/sKnIlkya34vzpboFZmYnScwuNqtvUC9XxH3W3ZtIQ24eqzBtFXAopI+yqSWdGKR/7/juQ/VLS7HkNSR4rOdTa4zxVvARQE4gGyrldc06zhQKulbST04oJxOWugpNwyh7PM8Sk737qPbEhEfTTuOeo4vGLu5sOXi7ak= ARC-Authentication-Results: i=1; mx.zohomail.com; dkim=pass; spf=pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) smtp.mailfrom=qemu-devel-bounces+importer=patchew.org@nongnu.org; dmarc=pass header.from= (p=quarantine dis=none) Return-Path: Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) by mx.zohomail.com with SMTPS id 1761312052819650.6409801511384; Fri, 24 Oct 2025 06:20:52 -0700 (PDT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1vCHih-00064c-Gm; Fri, 24 Oct 2025 09:20:39 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1vCHiI-0005bk-6I for qemu-devel@nongnu.org; Fri, 24 Oct 2025 09:20:16 -0400 Received: from us-smtp-delivery-124.mimecast.com ([170.10.133.124]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1vCHiD-0004oW-AW for qemu-devel@nongnu.org; Fri, 24 Oct 2025 09:20:13 -0400 Received: from mx-prod-mc-04.mail-002.prod.us-west-2.aws.redhat.com (ec2-54-186-198-63.us-west-2.compute.amazonaws.com [54.186.198.63]) by relay.mimecast.com with ESMTP with STARTTLS (version=TLSv1.3, cipher=TLS_AES_256_GCM_SHA384) id us-mta-662-RYs57CHtMKe99OlsMzzbpA-1; Fri, 24 Oct 2025 09:20:07 -0400 Received: from mx-prod-int-05.mail-002.prod.us-west-2.aws.redhat.com (mx-prod-int-05.mail-002.prod.us-west-2.aws.redhat.com [10.30.177.17]) (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 mx-prod-mc-04.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTPS id EAA3B19541BF; Fri, 24 Oct 2025 13:20:05 +0000 (UTC) Received: from toolbx.redhat.com (unknown [10.42.28.2]) by mx-prod-int-05.mail-002.prod.us-west-2.aws.redhat.com (Postfix) with ESMTP id D802D19540EB; Fri, 24 Oct 2025 13:20:03 +0000 (UTC) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=redhat.com; s=mimecast20190719; t=1761312008; 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=sRUaQrfk7K27n90oxEwxQVJ2+OTJlIxRwQMTmmkikRk=; b=jBogYp3tT8+s7oF3uZFuoywlMnfYPqPy+F6TNe4Q7IZxsrcCBy33wbCk5PeGJ+f7QhaPQR cu8mlYZg24sucsmjoj/k3tFxihWLL72yA1sF3+X6IlHrW2GQdu93k9Lt7HVWm534mJjMgu rtO4kXwtOq9oymopHpKa+zKEn6v6cqU= X-MC-Unique: RYs57CHtMKe99OlsMzzbpA-1 X-Mimecast-MFC-AGG-ID: RYs57CHtMKe99OlsMzzbpA_1761312006 From: =?UTF-8?q?Daniel=20P=2E=20Berrang=C3=A9?= To: qemu-devel@nongnu.org Cc: =?UTF-8?q?Daniel=20P=2E=20Berrang=C3=A9?= , Eduardo Habkost , Paolo Bonzini , Eric Blake , matoro Subject: [PULL 11/13] crypto: allow client/server cert chains Date: Fri, 24 Oct 2025 14:19:35 +0100 Message-ID: <20251024131937.56673-12-berrange@redhat.com> In-Reply-To: <20251024131937.56673-1-berrange@redhat.com> References: <20251024131937.56673-1-berrange@redhat.com> MIME-Version: 1.0 Content-Type: text/plain; charset="utf-8" Content-Transfer-Encoding: quoted-printable X-Scanned-By: MIMEDefang 3.0 on 10.30.177.17 Received-SPF: pass (zohomail.com: domain of gnu.org designates 209.51.188.17 as permitted sender) client-ip=209.51.188.17; envelope-from=qemu-devel-bounces+importer=patchew.org@nongnu.org; helo=lists.gnu.org; Received-SPF: pass client-ip=170.10.133.124; envelope-from=berrange@redhat.com; helo=us-smtp-delivery-124.mimecast.com X-Spam_score_int: -20 X-Spam_score: -2.1 X-Spam_bar: -- X-Spam_report: (-2.1 / 5.0 requ) BAYES_00=-1.9, DKIMWL_WL_HIGH=-0.001, DKIM_SIGNED=0.1, DKIM_VALID=-0.1, DKIM_VALID_AU=-0.1, DKIM_VALID_EF=-0.1, RCVD_IN_DNSWL_NONE=-0.0001, RCVD_IN_MSPIKE_H3=0.001, RCVD_IN_MSPIKE_WL=0.001, RCVD_IN_VALIDITY_RPBL_BLOCKED=0.001, RCVD_IN_VALIDITY_SAFE_BLOCKED=0.001, SPF_HELO_PASS=-0.001, SPF_PASS=-0.001 autolearn=ham autolearn_force=no X-Spam_action: no action X-BeenThere: qemu-devel@nongnu.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: qemu-devel-bounces+importer=patchew.org@nongnu.org Sender: qemu-devel-bounces+importer=patchew.org@nongnu.org X-ZohoMail-DKIM: pass (identity @redhat.com) X-ZM-MESSAGEID: 1761312060269158500 From: matoro The existing implementation assumes that client/server certificates are single individual certificates. If using publicly-issued certificates, or internal CAs that use an intermediate issuer, this is unlikely to be the case, and they will instead be certificate chains. While this can be worked around by moving the intermediate certificates to the CA certificate, which DOES currently support multiple certificates, this instead allows the issued certificate chains to be used as-is, without requiring the overhead of shuffling certificates around. Corresponding libvirt change is available here: https://gitlab.com/libvirt/libvirt/-/merge_requests/222 Reviewed-by: Daniel P. Berrang=C3=A9 Reviewed-by: Eric Blake Signed-off-by: matoro [DB: adapted for code conflicts with multi-CA patch] Signed-off-by: Daniel P. Berrang=C3=A9 --- crypto/tlscredsx509.c | 157 ++++++++++++-------------- tests/unit/test-crypto-tlscredsx509.c | 77 +++++++++++++ 2 files changed, 147 insertions(+), 87 deletions(-) diff --git a/crypto/tlscredsx509.c b/crypto/tlscredsx509.c index e1f82f1cdc..e21d85fe16 100644 --- a/crypto/tlscredsx509.c +++ b/crypto/tlscredsx509.c @@ -317,7 +317,8 @@ qcrypto_tls_creds_check_cert(QCryptoTLSCredsX509 *creds, =20 static int qcrypto_tls_creds_check_authority_chain(QCryptoTLSCredsX509 *creds, - gnutls_x509_crt_t cert, + gnutls_x509_crt_t *certs, + unsigned int ncerts, gnutls_x509_crt_t *cacerts, unsigned int ncacerts, const char *cacertFile, @@ -325,9 +326,32 @@ qcrypto_tls_creds_check_authority_chain(QCryptoTLSCred= sX509 *creds, bool isCA, Error **errp) { - gnutls_x509_crt_t cert_to_check =3D cert; + gnutls_x509_crt_t cert_to_check =3D certs[ncerts - 1]; int retval =3D 0; - gnutls_datum_t dn =3D {}; + gnutls_datum_t dn =3D {}, dnissuer =3D {}; + + for (int i =3D 0; i < (ncerts - 1); i++) { + if (!gnutls_x509_crt_check_issuer(certs[i], certs[i + 1])) { + retval =3D gnutls_x509_crt_get_dn2(certs[i], &dn); + if (retval < 0) { + error_setg(errp, "Unable to fetch cert DN: %s", + gnutls_strerror(retval)); + return -1; + } + retval =3D gnutls_x509_crt_get_dn2(certs[i + 1], &dnissuer); + if (retval < 0) { + gnutls_free(dn.data); + error_setg(errp, "Unable to fetch cert DN: %s", + gnutls_strerror(retval)); + return -1; + } + error_setg(errp, "Cert '%s' does not match issuer of cert '%s'= ", + dnissuer.data, dn.data); + gnutls_free(dn.data); + gnutls_free(dnissuer.data); + return -1; + } + } =20 for (;;) { gnutls_x509_crt_t cert_issuer =3D NULL; @@ -373,7 +397,8 @@ qcrypto_tls_creds_check_authority_chain(QCryptoTLSCreds= X509 *creds, } =20 static int -qcrypto_tls_creds_check_cert_pair(gnutls_x509_crt_t cert, +qcrypto_tls_creds_check_cert_pair(gnutls_x509_crt_t *certs, + size_t ncerts, const char *certFile, gnutls_x509_crt_t *cacerts, size_t ncacerts, @@ -383,7 +408,7 @@ qcrypto_tls_creds_check_cert_pair(gnutls_x509_crt_t cer= t, { unsigned int status; =20 - if (gnutls_x509_crt_list_verify(&cert, 1, + if (gnutls_x509_crt_list_verify(certs, ncerts, cacerts, ncacerts, NULL, 0, 0, &status) < 0) { @@ -425,66 +450,14 @@ qcrypto_tls_creds_check_cert_pair(gnutls_x509_crt_t c= ert, } =20 =20 -static gnutls_x509_crt_t -qcrypto_tls_creds_load_cert(QCryptoTLSCredsX509 *creds, - const char *certFile, - bool isServer, - Error **errp) -{ - gnutls_datum_t data; - gnutls_x509_crt_t cert =3D NULL; - g_autofree char *buf =3D NULL; - gsize buflen; - GError *gerr =3D NULL; - int ret =3D -1; - int err; - - trace_qcrypto_tls_creds_x509_load_cert(creds, isServer, certFile); - - err =3D gnutls_x509_crt_init(&cert); - if (err < 0) { - error_setg(errp, "Unable to initialize certificate: %s", - gnutls_strerror(err)); - goto cleanup; - } - - if (!g_file_get_contents(certFile, &buf, &buflen, &gerr)) { - error_setg(errp, "Cannot load CA cert list %s: %s", - certFile, gerr->message); - g_error_free(gerr); - goto cleanup; - } - - data.data =3D (unsigned char *)buf; - data.size =3D strlen(buf); - - err =3D gnutls_x509_crt_import(cert, &data, GNUTLS_X509_FMT_PEM); - if (err < 0) { - error_setg(errp, isServer ? - "Unable to import server certificate %s: %s" : - "Unable to import client certificate %s: %s", - certFile, - gnutls_strerror(err)); - goto cleanup; - } - - ret =3D 0; - - cleanup: - if (ret !=3D 0) { - gnutls_x509_crt_deinit(cert); - cert =3D NULL; - } - return cert; -} - - static int -qcrypto_tls_creds_load_ca_cert_list(QCryptoTLSCredsX509 *creds, - const char *certFile, - gnutls_x509_crt_t **certs, - unsigned int *ncerts, - Error **errp) +qcrypto_tls_creds_load_cert_list(QCryptoTLSCredsX509 *creds, + const char *certFile, + gnutls_x509_crt_t **certs, + unsigned int *ncerts, + bool isServer, + bool isCA, + Error **errp) { gnutls_datum_t data; g_autofree char *buf =3D NULL; @@ -507,7 +480,9 @@ qcrypto_tls_creds_load_ca_cert_list(QCryptoTLSCredsX509= *creds, if (gnutls_x509_crt_list_import2(certs, ncerts, &data, GNUTLS_X509_FMT_PEM, 0) < 0) { error_setg(errp, - "Unable to import CA certificate list %s", + isCA ? "Unable to import CA certificate list %s" : + (isServer ? "Unable to import server certificate %s" : + "Unable to import client certificate %s"), certFile); return -1; } @@ -523,7 +498,8 @@ qcrypto_tls_creds_x509_sanity_check(QCryptoTLSCredsX509= *creds, const char *certFile, Error **errp) { - gnutls_x509_crt_t cert =3D NULL; + gnutls_x509_crt_t *certs =3D NULL; + unsigned int ncerts =3D 0; gnutls_x509_crt_t *cacerts =3D NULL; unsigned int ncacerts =3D 0; size_t i; @@ -531,41 +507,48 @@ qcrypto_tls_creds_x509_sanity_check(QCryptoTLSCredsX5= 09 *creds, =20 if (certFile && access(certFile, R_OK) =3D=3D 0) { - cert =3D qcrypto_tls_creds_load_cert(creds, - certFile, isServer, - errp); - if (!cert) { + if (qcrypto_tls_creds_load_cert_list(creds, + certFile, + &certs, + &ncerts, + isServer, + false, + errp) < 0) { goto cleanup; } } if (access(cacertFile, R_OK) =3D=3D 0) { - if (qcrypto_tls_creds_load_ca_cert_list(creds, - cacertFile, - &cacerts, - &ncacerts, - errp) < 0) { + if (qcrypto_tls_creds_load_cert_list(creds, + cacertFile, + &cacerts, + &ncacerts, + isServer, + true, + errp) < 0) { goto cleanup; } } =20 - if (cert && - qcrypto_tls_creds_check_cert(creds, - cert, certFile, isServer, - false, errp) < 0) { - goto cleanup; + for (i =3D 0; i < ncerts; i++) { + if (qcrypto_tls_creds_check_cert(creds, + certs[i], certFile, + isServer, i !=3D 0, errp) < 0) { + goto cleanup; + } } =20 - if (cert && - qcrypto_tls_creds_check_authority_chain(creds, cert, + if (ncerts && + qcrypto_tls_creds_check_authority_chain(creds, + certs, ncerts, cacerts, ncacerts, cacertFile, isServer, true, errp) < 0) { goto cleanup; } =20 - if (cert && ncacerts && - qcrypto_tls_creds_check_cert_pair(cert, certFile, cacerts, - ncacerts, cacertFile, + if (ncerts && ncacerts && + qcrypto_tls_creds_check_cert_pair(certs, ncerts, certFile, + cacerts, ncacerts, cacertFile, isServer, errp) < 0) { goto cleanup; } @@ -573,8 +556,8 @@ qcrypto_tls_creds_x509_sanity_check(QCryptoTLSCredsX509= *creds, ret =3D 0; =20 cleanup: - if (cert) { - gnutls_x509_crt_deinit(cert); + for (i =3D 0; i < ncerts; i++) { + gnutls_x509_crt_deinit(certs[i]); } for (i =3D 0; i < ncacerts; i++) { gnutls_x509_crt_deinit(cacerts[i]); diff --git a/tests/unit/test-crypto-tlscredsx509.c b/tests/unit/test-crypto= -tlscredsx509.c index 7c5df32bcc..96ad4e741b 100644 --- a/tests/unit/test-crypto-tlscredsx509.c +++ b/tests/unit/test-crypto-tlscredsx509.c @@ -580,6 +580,12 @@ int main(int argc, char **argv) true, true, GNUTLS_KEY_KEY_CERT_SIGN, false, false, NULL, NULL, 0, 0); + TLS_ROOT_REQ(someotherrootreq, + "UK", "some other random CA", NULL, NULL, NULL, NULL, + true, true, true, + true, true, GNUTLS_KEY_KEY_CERT_SIGN, + false, false, NULL, NULL, + 0, 0); TLS_CERT_REQ(cacertlevel1areq, cacertrootreq, "UK", "qemu level 1a", NULL, NULL, NULL, NULL, true, true, true, @@ -626,6 +632,32 @@ int main(int argc, char **argv) cacertlevel2areq.crt, }; =20 + gnutls_x509_crt_t cabundle[] =3D { + someotherrootreq.crt, + cacertrootreq.crt, + }; + + gnutls_x509_crt_t servercertchain[] =3D { + servercertlevel3areq.crt, + cacertlevel2areq.crt, + cacertlevel1areq.crt, + }; + + gnutls_x509_crt_t servercertchain_incomplete[] =3D { + servercertlevel3areq.crt, + cacertlevel2areq.crt, + }; + + gnutls_x509_crt_t servercertchain_unsorted[] =3D { + servercertlevel3areq.crt, + cacertlevel1areq.crt, + cacertlevel2areq.crt, + }; + + gnutls_x509_crt_t clientcertchain[] =3D { + clientcertlevel2breq.crt, + cacertlevel1breq.crt, + }; =20 test_tls_write_cert_chain(WORKDIR "cacertchain-ctx.pem", certchain, @@ -665,6 +697,46 @@ int main(int argc, char **argv) WORKDIR "cacertchain-incomplete-ctx.pem", servercertlevel3areq.filename, true); =20 + test_tls_write_cert_chain(WORKDIR "servercertchain-ctx.pem", + servercertchain, + G_N_ELEMENTS(servercertchain)); + + TLS_TEST_REG(serverchain, true, + cacertrootreq.filename, + WORKDIR "servercertchain-ctx.pem", false); + + test_tls_write_cert_chain(WORKDIR "cabundle-ctx.pem", + cabundle, + G_N_ELEMENTS(cabundle)); + + TLS_TEST_REG(multiplecaswithchain, true, + WORKDIR "cabundle-ctx.pem", + WORKDIR "servercertchain-ctx.pem", false); + + test_tls_write_cert_chain(WORKDIR "servercertchain_incomplete-ctx.pem", + servercertchain_incomplete, + G_N_ELEMENTS(servercertchain_incomplete)); + + TLS_TEST_REG(incompleteserverchain, true, + cacertrootreq.filename, + WORKDIR "servercertchain_incomplete-ctx.pem", true); + + test_tls_write_cert_chain(WORKDIR "servercertchain_unsorted-ctx.pem", + servercertchain_unsorted, + G_N_ELEMENTS(servercertchain_unsorted)); + + TLS_TEST_REG(unsortedserverchain, true, + cacertrootreq.filename, + WORKDIR "servercertchain_unsorted-ctx.pem", true); + + test_tls_write_cert_chain(WORKDIR "clientcertchain-ctx.pem", + clientcertchain, + G_N_ELEMENTS(clientcertchain)); + + TLS_TEST_REG(clientchain, false, + cacertrootreq.filename, + WORKDIR "clientcertchain-ctx.pem", false); + /* Some missing certs - first two are fatal, the last * is ok */ @@ -734,8 +806,13 @@ int main(int argc, char **argv) test_tls_discard_cert(&cacertlevel2areq); test_tls_discard_cert(&servercertlevel3areq); test_tls_discard_cert(&clientcertlevel2breq); + test_tls_discard_cert(&someotherrootreq); unlink(WORKDIR "cacertchain-ctx.pem"); unlink(WORKDIR "cacertchain-with-invalid-ctx.pem"); + unlink(WORKDIR "servercertchain-ctx.pem"); + unlink(WORKDIR "servercertchain_incomplete-ctx.pem"); + unlink(WORKDIR "servercertchain_unsorted-ctx.pem"); + unlink(WORKDIR "clientcertchain-ctx.pem"); =20 test_tls_cleanup(KEYFILE); rmdir(WORKDIR); --=20 2.50.1