libmicrohttpd2

HTTP server C library (MHD 2.x, alpha)
Log | Files | Refs | README | LICENSE

commit cca48439b82f6d21c9b82a6b85d8e5a0b70335e6
parent abb56215377d20c1d6e9a46db634dcb94b90a4ab
Author: Evgeny Grin (Karlson2k) <k2k@drgrin.dev>
Date:   Mon, 31 Aug 2026 20:46:18 +0200

Added complex test for ACME ALPN

Diffstat:
Mconfigure.ac | 1+
Msrc/tests/Makefile.am | 6++++++
Asrc/tests/acme/Makefile.am | 38++++++++++++++++++++++++++++++++++++++
Asrc/tests/acme/acme_test_certs.h | 312+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/tests/acme/test_acme_alpn.c | 1674+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
5 files changed, 2031 insertions(+), 0 deletions(-)

diff --git a/configure.ac b/configure.ac @@ -9985,6 +9985,7 @@ src/tests/unit/Makefile src/tests/basic/Makefile src/tests/upgrade/Makefile src/tests/raw/Makefile +src/tests/acme/Makefile src/tests/client_server/Makefile src/examples2/Makefile src/examples2/acme/Makefile diff --git a/src/tests/Makefile.am b/src/tests/Makefile.am @@ -2,6 +2,12 @@ SUBDIRS = unit basic raw +if MHD_SUPPORT_THREADS +if MHD_SUPPORT_ACME +SUBDIRS += acme +endif +endif + if HAVE_POSIX_THREADS if MHD_SUPPORT_UPGRADE SUBDIRS += upgrade diff --git a/src/tests/acme/Makefile.am b/src/tests/acme/Makefile.am @@ -0,0 +1,38 @@ +# This Makefile.am is in the public domain +AM_CPPFLAGS = \ + -I$(top_srcdir)/src/include \ + -I$(top_srcdir)/src/incl_priv \ + -I$(top_srcdir)/src/mhd2 \ + -DMHD_CPU_COUNT=$(CPU_COUNT) \ + $(CPPFLAGS_ac) $(GNUTLS_CPPFLAGS) + +AM_CFLAGS = $(CFLAGS_ac) $(PTHREAD_CFLAGS) + +AM_LDFLAGS = $(LDFLAGS_ac) $(GNUTLS_LDFLAGS) + +LIBS = @LIBS@ $(PTHREAD_LIBS) + +AM_TESTS_ENVIRONMENT = $(TESTS_ENVIRONMENT_ac) + +if USE_COVERAGE + AM_CFLAGS += -fprofile-arcs -ftest-coverage +endif + +LDADD = \ + $(top_builddir)/src/mhd2/libmicrohttpd2.la $(GNUTLS_LIBS) + +$(top_builddir)/src/mhd2/libmicrohttpd2.la: $(top_builddir)/src/mhd2/Makefile + @echo ' cd $(top_builddir)/src/mhd2 && $(MAKE) $(AM_MAKEFLAGS) libmicrohttpd2.la'; \ + $(am__cd) $(top_builddir)/src/mhd2 && $(MAKE) $(AM_MAKEFLAGS) libmicrohttpd2.la + +# The test client is implemented with GnuTLS. The daemon side is tested with +# every TLS backend built in. +if MHD_SUPPORT_GNUTLS +check_PROGRAMS = \ + test_acme_alpn + +test_acme_alpn_SOURCES = \ + test_acme_alpn.c acme_test_certs.h +endif + +TESTS = $(check_PROGRAMS) diff --git a/src/tests/acme/acme_test_certs.h b/src/tests/acme/acme_test_certs.h @@ -0,0 +1,312 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later OR (GPL-2.0-or-later WITH eCos-exception-2.0) */ +/* + This file is part of GNU libmicrohttpd. + Copyright (C) 2026 Evgeny Grin (Karlson2k) + + GNU libmicrohttpd 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. + + GNU libmicrohttpd 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. + + Alternatively, you can redistribute GNU libmicrohttpd and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License, or (at your option) any later version, together + with the eCos exception, as follows: + + As a special exception, if other files instantiate templates or + use macros or inline functions from this file, or you compile this + file and link it with other works to produce a work based on this + file, this file does not by itself cause the resulting work to be + covered by the GNU General Public License. However the source code + for this file must still be made available in accordance with + section (3) of the GNU General Public License v2. + + This exception does not invalidate any other reasons why a work + based on this file might be covered by the GNU General Public + License. + + You should have received copies of the GNU Lesser General Public + License and the GNU General Public License along with this library; + if not, see <https://www.gnu.org/licenses/>. +*/ + +/** + * @file src/tests/acme/acme_test_certs.h + * @brief Certificates and keys for the ACME TLS-ALPN-01 challenge tests + * @author Karlson2k (Evgeny Grin) + * + * All certificates are self-signed, valid for one hundred years and share + * the same subject prefix "C=CH, ST=Geneva, L=Geneva, O=test-libmicrohttpd". + * + * The "server" certificate is the reply for any non-ACME connection. + * The two "challenge" certificates are the replies for the ACME TLS-ALPN-01 + * challenge on two different domains. Challenge certificate "B" repeats + * the common name of the "server" certificate and uses a completely + * different type of the key, so that the served certificate cannot be + * identified by the name or by the key type alone. + * + * The challenge certificates are formed as required by RFC 8737, section 3: + * the subjectAltName extension has a single dNSName entry and the critical + * acmeIdentifier extension (OID 1.3.6.1.5.5.7.1.31) holds the DER encoded + * OCTET STRING with the SHA-256 digest of the key authorization. The key + * authorization is the challenge token joined by a dot with the account + * thumbprint "nRbYuckWQyMDAm6D2H4p7ZiK9yT6R8UUD0vZNWqv8ps" (synthetic). + * The tokens are the examples from RFC 8555: + * "LoqXcYV8q5ONbJQxbmR7SCTNo3tiAXDfowyjxAjEuX0" for "A" and + * "DGyRejmCefe7v4NfDGDKfA" for "B". + */ + +#ifndef MHD_ACME_TEST_CERTS_H +#define MHD_ACME_TEST_CERTS_H 1 + +/** + * The domain validated by the challenge certificate "A" + */ +#define mhdt_ACME_CHLNG_A_DOMAIN "acme-test1.example" + +/** + * The domain validated by the challenge certificate "B" + */ +#define mhdt_ACME_CHLNG_B_DOMAIN "acme-test2.example" + + +/* *INDENT-OFF* */ +/** + * The certificate for all non-ACME connections. + * RSA 4096, CN=test-mhd-server, SAN: DNS:localhost, IP:127.0.0.1, IP:[::1] + */ +#define mhdt_ACME_SRV_CERT_PEM \ + "-----BEGIN CERTIFICATE-----\n" \ + "MIIGATCCA+mgAwIBAgIUZ9L0ZZRAMrVCCQFFzX/Xp5LwJJ8wDQYJKoZIhvcNAQEL\n" \ + "BQAwZjELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2Vu\n" \ + "ZXZhMRswGQYDVQQKDBJ0ZXN0LWxpYm1pY3JvaHR0cGQxGDAWBgNVBAMMD3Rlc3Qt\n" \ + "bWhkLXNlcnZlcjAgFw0yNjA4MzExNDM3MjBaGA8yMTI2MDkwMTE0MzcyMFowZjEL\n" \ + "MAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2VuZXZhMRsw\n" \ + "GQYDVQQKDBJ0ZXN0LWxpYm1pY3JvaHR0cGQxGDAWBgNVBAMMD3Rlc3QtbWhkLXNl\n" \ + "cnZlcjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANNdODgKFQX1QYKO\n" \ + "iH8J1j/iUXds+qXpm5LKOW1lPsYOTPEUj9DmzBmjjJa6OE+5W3W7BHWEfNF7AOJb\n" \ + "ZN9WyOtdcJh4ctBdxHi0IVGkDME2Q7HSF8GS7chXLolGUZ2/vZvgZXDYa1pPH3v2\n" \ + "pmFS8g/D8puRpV6yuPaIF5N2yK653hDll7wJQtsOIllWa9pS93MkdOvhezzxOpOh\n" \ + "hRlG+UMuizlAvZDBaF/ScztLoykiz5YbC4oqe8Yc71+q2uLcjab//Tg7bBLIszrn\n" \ + "ftXqWNzCz/NuO7g3//E2KAn/CQ+AOUXrr31yn9WjL2VVnIlsoSPOQzHg4uAJCQiU\n" \ + "PDb429E6p2r8GF/EIsa9i4Si0xBeaS5q1p0zCPUFwn5OQp+2LbkjkYarxz9/1xO7\n" \ + "uNj/MuEGnz+ikbD1G38NopFheVZPhSNMt5w9pEMbOMwGa0HzCqodt3P3QRbRXJyt\n" \ + "Lvc9bnRRtmCoH5deZGey2e0bMD/hO3D+Mgwm0X0eYUTi8yHIzNGcOLuNcE1a1apT\n" \ + "rRfoPzaBetbsihcbolRbuVVezeapU75ttjH9xozfXQhU1IyoNn4ZwaefOCOoaIY5\n" \ + "FqPc7AbaUQnUGPgfc+FGABzvHqpZ060zJjOKMkdBkNaovIXZdLcrObtbxFZO93gq\n" \ + "o5sVgOxQpgtsZQumORH4KEogHvjdAgMBAAGjgaQwgaEwHwYDVR0jBBgwFoAULOqQ\n" \ + "ne2Tsakom6X4bKgDgb1gDeQwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMCBaAw\n" \ + "EwYDVR0lBAwwCgYIKwYBBQUHAwEwLAYDVR0RBCUwI4IJbG9jYWxob3N0hwR/AAAB\n" \ + "hxAAAAAAAAAAAAAAAAAAAAABMB0GA1UdDgQWBBQs6pCd7ZOxqSibpfhsqAOBvWAN\n" \ + "5DANBgkqhkiG9w0BAQsFAAOCAgEAsLeSrXQC6qkXADn+KmW8F5CgNdQ1hpzHBXZv\n" \ + "Y8cWq9t6sHF33H2YRYZSRLmREjxiXe+/+988AzaQ60ALpl2YkvJ8OVGvU3Ou5Ya+\n" \ + "WKMm9ieXaTv+TvfFo/Kb0b0SnPcQ1aJoxeQxwIb09BZDapqxMHLo8ZOtoPUZ441m\n" \ + "5qQWGVXQ9wyLL+EsaeAOet9GrmNuRnkvIHfmc5rR23uzZwoz/FLoQqM0CXQ4STrA\n" \ + "PRu0DMEqafNp5VU391UegYFMXspQiUUbDngHzw1cxF0EP3WHzT81ZBcVdTM2Be+x\n" \ + "HAE9XfjQ7klquspNXK37S/zh15L2iXaMP2oRtzPcNkCCsQFkvkNvVOJlv/kTI755\n" \ + "Rs29BcTFT0L7ylN7JaS4euMUjTRkChTeYfSNlAqreKUuSGLqNxOYufK7ZZP/+8OY\n" \ + "/LUs9LHKSCyjjXPb/CKjPkK/b9WiusX3+30+lLe7JgG4aeOInm53ZuIggx8LK4iE\n" \ + "6B17zIlG1cZmvnFh0lsZR63+QtvwwQ+nAC4Q3EupB40MQJR1i3CZwGsHhiwajteW\n" \ + "SVKMtx9pPQv/iXfLCH5Boy2rH4Xv2sD3NM4WuzQlwIxJJKcBtlPxvaeiQlemrABf\n" \ + "FL1lu601jcVdGZTtYRdLVfPh6w5ID1zDzjKHkg4/9FSgqxurC7mdxubsfbMSQtLk\n" \ + "5tKZN+0=\n" \ + "-----END CERTIFICATE-----\n" + +/** + * The private key for #mhdt_ACME_SRV_CERT_PEM + */ +#define mhdt_ACME_SRV_KEY_PEM \ + "-----BEGIN PRIVATE KEY-----\n" \ + "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\n" \ + "EJ+F6dRWHnvqXcwIWHjHPPwcsaad4A65\n" \ + "-----END PRIVATE KEY-----\n" + +/** + * The ACME challenge certificate for #mhdt_ACME_CHLNG_A_DOMAIN. + * RSA 4096, CN=test-acme-challenge + */ +#define mhdt_ACME_CHLNG_A_CERT_PEM \ + "-----BEGIN CERTIFICATE-----\n" \ + "MIIGGDCCBACgAwIBAgIUWdg2aI2BGpzJpHF3q/3WC3L3p3EwDQYJKoZIhvcNAQEL\n" \ + "BQAwajELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2Vu\n" \ + "ZXZhMRswGQYDVQQKDBJ0ZXN0LWxpYm1pY3JvaHR0cGQxHDAaBgNVBAMME3Rlc3Qt\n" \ + "YWNtZS1jaGFsbGVuZ2UwIBcNMjYwODMxMTQzNzIwWhgPMjEyNjA5MDExNDM3MjBa\n" \ + "MGoxCzAJBgNVBAYTAkNIMQ8wDQYDVQQIDAZHZW5ldmExDzANBgNVBAcMBkdlbmV2\n" \ + "YTEbMBkGA1UECgwSdGVzdC1saWJtaWNyb2h0dHBkMRwwGgYDVQQDDBN0ZXN0LWFj\n" \ + "bWUtY2hhbGxlbmdlMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA+wAF\n" \ + "ZGAP0bAc3a6V5BCbddleMgPl7XnCaw8LAvzsnXlReO5gasEV0J4qBkx2b7P8qiJE\n" \ + "zW/AIAmxat2HlViWPQDcR+lgM9+dCYtlrAE+6/wE8FdSmP5poe0CjqlaC72/1Jbr\n" \ + "WzkXdC87NbGZEQatGc1Dm59J0T8L60NLSpmEA4MnyxQcyA5bMgDBMkuE2IpIiiY5\n" \ + "CaSMDvwj0v1o2YjVFni9gpjW557YMJtf5nc3ZHceEHGjF6j5YusBvtqhJ1l9DUN1\n" \ + "eC3xCMEjuWJP9/z8T6+4oG1mn9cx9rcd/hVvM16BFBZYNj5lAW6D06pLo/NwEbBw\n" \ + "mlyF5i7tHZejPg7djPjOqely6tlhooMMpSnSZ5Md/v5iIXgNUrM1Rw6fPrIy5Ioo\n" \ + "GBZ6F+WUb/KsdABy6wXhZIRxb8xXPy3OjI3GRJFHkuZrv7JkV5BrtCx4IbqtaBem\n" \ + "b6PntExzvtZwnHCm/i+mrpf9COtJwlZI5WXOzZ582OivrA1G9HFdsPH9vYTyzpWH\n" \ + "9F9Vo7pSKm/pJ6e5Rfxff2zktxwL3nsN3oGhSyelqVnQBSXvOxABMEPVxbbgaKA0\n" \ + "7eNeHBXwzajeBl1M4gLQlVEDC9li3IpbEfBdzZieSJaNbnd06Jnk+Et7wm646GCV\n" \ + "DJ2U3Mss2iU18vipCE0JUaT2ecs9gvt9e05X5BcCAwEAAaOBszCBsDAfBgNVHSME\n" \ + "GDAWgBTpYKJHLroAze4Ii3deRWoldmMm+jAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB\n" \ + "/wQEAwIHgDAdBgNVHREEFjAUghJhY21lLXRlc3QxLmV4YW1wbGUwHQYDVR0OBBYE\n" \ + "FOlgokcuugDN7giLd15FaiV2Yyb6MDEGCCsGAQUFBwEfAQH/BCIEIPLGBRPQeB8f\n" \ + "rpo0yvkswvcjBZOCCa4cGnjVp1qMp575MA0GCSqGSIb3DQEBCwUAA4ICAQDkHv8I\n" \ + "ra7wwYEg/wpjDyZnBAZvZslGyiX1rd58Gwp77htjx6C8ccjwEh05BOwgRFhYDvAs\n" \ + "Jpi0TXDMN4VI1353UplQQgBWaPbnXuBs8ppPMDRnH5TB+HAvEUJo/xMCwsjzHiub\n" \ + "Q3uU3xsmY0v01YJT93pWjigmJ3JcsFHBMzRubslsYB2hTu2nSU66KqstLIAKUQ1c\n" \ + "ih9XtJ+iWE0DwKnlUvc0yiW/G5hmp8LNSOJg6SjTBQWEatSclTvuxS+1Wh1EOXHd\n" \ + "fIn7Q3YEjbwy3sbwRMYIsRkZ9GFT91FTMV6i5uJ0y2cGrfkZIfvuI53iXbtZh3bt\n" \ + "EcjVMgHogI9VRwW2zR2UQXDW7Ae6BsriwGsk+gCOfpMyz4qVZ1EN79R6BhxskOHE\n" \ + "bqkql1G7niIAvX0WnU+4dFrm3Mtw5GY+qv9F0SYZsi7EGB/bhdc2d1vI5Gy2j9v3\n" \ + "0zPPsEODrbpiMViD3a/9Zg8sGN//iX4IdKXyOuT7wljz40d5RcItgjUwxIIXxnWz\n" \ + "flx/IaOlezHQygLyp6a4h9Tw+OGv9l+rWEK3OoE5kI2fNXpcCKKMf6C340F0xGUg\n" \ + "fk/te/o/mxjgNX1pqTehoRuvn7gpMGbaqqPsoJpSdeKolQkOBW5qoZm6i2EXSkVb\n" \ + "xoi1Eb4j0brypkqwYJqa3n+3Sz2CAS1JEk4eZA==\n" \ + "-----END CERTIFICATE-----\n" + +/** + * The private key for #mhdt_ACME_CHLNG_A_CERT_PEM + */ +#define mhdt_ACME_CHLNG_A_KEY_PEM \ + "-----BEGIN PRIVATE KEY-----\n" \ + "MIIJQwIBADANBgkqhkiG9w0BAQEFAASCCS0wggkpAgEAAoICAQD7AAVkYA/RsBzd\n" \ + "rpXkEJt12V4yA+XtecJrDwsC/OydeVF47mBqwRXQnioGTHZvs/yqIkTNb8AgCbFq\n" \ + "3YeVWJY9ANxH6WAz350Ji2WsAT7r/ATwV1KY/mmh7QKOqVoLvb/UlutbORd0Lzs1\n" \ + "sZkRBq0ZzUObn0nRPwvrQ0tKmYQDgyfLFBzIDlsyAMEyS4TYikiKJjkJpIwO/CPS\n" \ + "/WjZiNUWeL2CmNbnntgwm1/mdzdkdx4QcaMXqPli6wG+2qEnWX0NQ3V4LfEIwSO5\n" \ + "Yk/3/PxPr7igbWaf1zH2tx3+FW8zXoEUFlg2PmUBboPTqkuj83ARsHCaXIXmLu0d\n" \ + "l6M+Dt2M+M6p6XLq2WGigwylKdJnkx3+/mIheA1SszVHDp8+sjLkiigYFnoX5ZRv\n" \ + "8qx0AHLrBeFkhHFvzFc/Lc6MjcZEkUeS5mu/smRXkGu0LHghuq1oF6Zvo+e0THO+\n" \ + "1nCccKb+L6aul/0I60nCVkjlZc7NnnzY6K+sDUb0cV2w8f29hPLOlYf0X1WjulIq\n" \ + "b+knp7lF/F9/bOS3HAveew3egaFLJ6WpWdAFJe87EAEwQ9XFtuBooDTt414cFfDN\n" \ + "qN4GXUziAtCVUQML2WLcilsR8F3NmJ5Ilo1ud3TomeT4S3vCbrjoYJUMnZTcyyza\n" \ + "JTXy+KkITQlRpPZ5yz2C+317TlfkFwIDAQABAoICAAqUV1CCGrNBk2Xl9vFpoHPY\n" \ + "2UyNt4JJReA8qRUo01Gr4brm0++L+z8Te5KlDxTpkYvGQqokvK9dA+UM41XF1HH2\n" \ + "eFJw9sTDocXbjJ8xtOo+HCMukGUHIYKuumiIS4BVMMxldBQglZQobXwiOTZrgUj7\n" \ + "dY8N/rmFc8YodvhKUjMnsw9Z5gCqAj6wTh8FlIWKsUC0GGax+ylNMraw8bDBlgxg\n" \ + "GHTJu70q3SfnVKUif0DfzceZp+srtx9BPTiYfym+bt6WlPv0y8UNBv7IXbXb1vKw\n" \ + "5bz1VNmwjgLnzal+U8qRgCqi6nhBgau2xx2uft5b42xgyHjpJMetUKyU+KTOpu3g\n" \ + "gbUbmQVCiu7iXPQQLv/mRmImzhVCzxzCRZV1qb26JCuTPVA9zUq9Qd4XLfM1Z6oO\n" \ + "ZC7X7EKNVifzHRfRVxGfyFVDMyrZHfRXE8TZT5V5ehhZcQGzf2zMkcUl1nwlIru8\n" \ + "2Q7auFMw50/kB5Qxq4z5Wjfqdw8VU/4T7qtAxRPifaT20vu/gMHzQrF+hbbm7E2T\n" \ + "YWglwwC/TrQQpxy3qj/k5o4VUCqzUNX0mR5c3pB4IxHgsVmPkXW9KfBmBXdIRAhS\n" \ + "tEjY7FtbW/CveGPwHNYdDBbwxfQOjj98XZIEFLHEa2sluz2SiJ1qXb3ZL0Cm+Muo\n" \ + "iYdUI9Zt4Qo2UVa4jOWZAoIBAQD+LptWc9zPR/fhu6Q5oulR9yr+jIjSqOG6YL1c\n" \ + "3BiIh+VWf0LRiYGhVyYOhj5gbaH8bWqbX+Oq2ywfACKnKdM49zrpdtiYcH+xCLrc\n" \ + "/LulfXOOVyl18ka8dVeGpo93pqp7c0UVRMFYx56rmXmWgroAtEsmE/9cEt2VLgoE\n" \ + "lV/LtBk+G6s4jOV9raVy7eLpx4LKgqqu9laOPWjb1sT2YAsTQtv8XMWPo4vj1e/k\n" \ + "SYwmxoWxLxa7Xq9KljV6c+odQ8RjZcmcWcKKlQe9NxQHZLmk1nQwOri/hrU+c25I\n" \ + "cTMiGi6eNkM/YMD+iANVIdYPawn8JhHymlUoc8L0/gUkACSdAoIBAQD8y5aX3YUD\n" \ + "YPymtOYkctRa83JNWdZ4cO5DpjG5ANWrdCjOUGBVUySwPhcpnv29aRWwjiaR8bc7\n" \ + "NrIoApC49q5O5VSaN7eYKeRxTyXxEU4yotuWf5Nct5YH/IO05rBdCkFE+oGYiy+U\n" \ + "QOJUdCZnXrzB+ID4LT9cfcJotI/nOsVf40nTXZcL3Iiba7ONkQX4/Yw8nVSWWEnp\n" \ + "kzSXuRbA4tL8pnzPweXFWxrh/bCSvf2xAz3O76L81w1H8HXFhMXPGNjKFYuOHs20\n" \ + "kJmDTWE+AgvmenJsevpAQikbJw37n4itiogaMMK3XuX82fP7QYIPkXdhfXkQAiIw\n" \ + "2MFKKVzYLNtDAoIBAQDOfR5ERSuEWHrPLphjGi1hIOIUmCYuvHNj7WLoJQTpJTzx\n" \ + "38BwaLyeTSXldrkFrQqC/oA0aNmS2yokpP+BvUCSboW1U47xh7K0Jttm2wm1jwxo\n" \ + "/fX4kzFTuB+6i2TRVtv4HdRpSsmnTcNgs/VKiDiCifTioiw4qDrKDfSldJO18EDx\n" \ + "a5jU/+EPEBnwG8DTGfjueugd6pC2TLz6mqa3X56frxj3EoaMAdiK5ilT00u/1FaX\n" \ + "ciGNZYYRmaUoLTJrQ7ou5Wm7Uk/wNNFsrWnf97u4ZV9OLuYW8WAbs7pLr2pSseJd\n" \ + "NMe8nOUr4WKTrkEvlU0vQd4ir62uhxxB2npinJshAoIBAA5bWaAiWo2IkpC9MVzc\n" \ + "kKbhsJooAgAxGbcJ10UUgQ4NuMuO0QnHgQvpWtNoTizVrP+r8xDS038vagrPqnx2\n" \ + "tguRBq8Dfw/LQUgWIKeCPhiuGx5+/FWFr6Wgd7Jsb5O5I1/Rgk6xiPA4hyIYvp2c\n" \ + "ScgSXvC5USHlmobrykhtlAwQEn9Ij22DBtcc1MOzes1bSP2gSc+IEBqN26zKPDqU\n" \ + "JztmESyTbIM621n+WGrdE21cK8R246DMbdqqE2Y2Vi2e0aEYfCaEJVad57oHSQCG\n" \ + "iTp7tcfnbBL+Lut12ltyAgcpGPWz0zsqzpD/p9WbMNHFhywoPhnkpSB1DvUxSC6W\n" \ + "zZUCggEBAIAigxHheP0nEyAVvNzxJK4POkC+vICuGLuE9ojGVOzEFE2E+B+X6FhA\n" \ + "ICGMAlW+ILKQy1b+MFSQxb3QAQtkgZnPESA16CO5G4xUONXG64aIOtgJD/dbvURU\n" \ + "YEuWNPxFGlBZrcR6oA56ueMsihGuPGnDUD0URBDoMEPTOZxV/erB6LFKYBERWMIy\n" \ + "62wELBRaClAJode93G2NAuVvTjOX44HvUs0GA+vTuEP8x30aH3dOpHsODTi4j1AO\n" \ + "KPpCCkdJBGftNicErE9nZGujo6M2dJA1SqfTCy3klNOlCt+E6ROfqoDdof1waawu\n" \ + "aOxJkk3KdEnf10Zoq7FH9yN/2qxqkXo=\n" \ + "-----END PRIVATE KEY-----\n" + +/** + * The ACME challenge certificate for #mhdt_ACME_CHLNG_B_DOMAIN. + * ECDSA P-384, CN=test-mhd-server (the same name as the "server" one) + */ +#define mhdt_ACME_CHLNG_B_CERT_PEM \ + "-----BEGIN CERTIFICATE-----\n" \ + "MIICwTCCAkegAwIBAgIUGvDcAsQcCKB24Bkh8T/9jT1RthgwCgYIKoZIzj0EAwMw\n" \ + "ZjELMAkGA1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2VuZXZh\n" \ + "MRswGQYDVQQKDBJ0ZXN0LWxpYm1pY3JvaHR0cGQxGDAWBgNVBAMMD3Rlc3QtbWhk\n" \ + "LXNlcnZlcjAgFw0yNjA4MzExNDM3MjBaGA8yMTI2MDkwMTE0MzcyMFowZjELMAkG\n" \ + "A1UEBhMCQ0gxDzANBgNVBAgMBkdlbmV2YTEPMA0GA1UEBwwGR2VuZXZhMRswGQYD\n" \ + "VQQKDBJ0ZXN0LWxpYm1pY3JvaHR0cGQxGDAWBgNVBAMMD3Rlc3QtbWhkLXNlcnZl\n" \ + "cjB2MBAGByqGSM49AgEGBSuBBAAiA2IABOAPQQq1PtnjUgXrgNI8TAwWne0hHb20\n" \ + "9W7iopT9VnoKJdIXl071kjXjxo3YOPXgn3BC+jQpG/FiJwCCjFtd0WQA2vRAmSpH\n" \ + "iEXlVx9ZwnxQXfOPyfWOEqglByvQSDKdi6OBszCBsDAfBgNVHSMEGDAWgBTKY8Ch\n" \ + "2pGmjuS0FmSXU8DbmCYrRzAMBgNVHRMBAf8EAjAAMA4GA1UdDwEB/wQEAwIHgDAd\n" \ + "BgNVHREEFjAUghJhY21lLXRlc3QyLmV4YW1wbGUwHQYDVR0OBBYEFMpjwKHakaaO\n" \ + "5LQWZJdTwNuYJitHMDEGCCsGAQUFBwEfAQH/BCIEIIYAoxWrhIsBZAFvbd7j6s/3\n" \ + "2/DdvCio76/5ndU1HeNZMAoGCCqGSM49BAMDA2gAMGUCMCcJqSeM9Vh47njeifnv\n" \ + "U97CIsCB7T/yjv9RdhQWer0GS8+yjIk8hkILLFteviY5owIxANeSQclgdhXG/SGP\n" \ + "dkz2xtjfjvsZMSwJhgA0PQhJ2Ug0WxQcm2vL4w9i9ww21yZIIg==\n" \ + "-----END CERTIFICATE-----\n" + +/** + * The private key for #mhdt_ACME_CHLNG_B_CERT_PEM + */ +#define mhdt_ACME_CHLNG_B_KEY_PEM \ + "-----BEGIN PRIVATE KEY-----\n" \ + "MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDCH3vJW/8METKqnNfLR\n" \ + "S9H2QtUKOFiRShg0QnlSIYMky++m9m/4YHnKC4O7r3a0xpehZANiAATgD0EKtT7Z\n" \ + "41IF64DSPEwMFp3tIR29tPVu4qKU/VZ6CiXSF5dO9ZI148aN2Dj14J9wQvo0KRvx\n" \ + "YicAgoxbXdFkANr0QJkqR4hF5VcfWcJ8UF3zj8n1jhKoJQcr0EgynYs=\n" \ + "-----END PRIVATE KEY-----\n" + +/* *INDENT-ON* */ + +#endif /* ! MHD_ACME_TEST_CERTS_H */ diff --git a/src/tests/acme/test_acme_alpn.c b/src/tests/acme/test_acme_alpn.c @@ -0,0 +1,1674 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later OR (GPL-2.0-or-later WITH eCos-exception-2.0) */ +/* + This file is part of GNU libmicrohttpd. + Copyright (C) 2026 Evgeny Grin (Karlson2k) + + GNU libmicrohttpd 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. + + GNU libmicrohttpd 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. + + Alternatively, you can redistribute GNU libmicrohttpd and/or + modify it under the terms of the GNU General Public License as + published by the Free Software Foundation; either version 2 of + the License, or (at your option) any later version, together + with the eCos exception, as follows: + + As a special exception, if other files instantiate templates or + use macros or inline functions from this file, or you compile this + file and link it with other works to produce a work based on this + file, this file does not by itself cause the resulting work to be + covered by the GNU General Public License. However the source code + for this file must still be made available in accordance with + section (3) of the GNU General Public License v2. + + This exception does not invalidate any other reasons why a work + based on this file might be covered by the GNU General Public + License. + + You should have received copies of the GNU Lesser General Public + License and the GNU General Public License along with this library; + if not, see <https://www.gnu.org/licenses/>. +*/ + +/** + * @file src/tests/acme/test_acme_alpn.c + * @brief Test for the ACME TLS-ALPN-01 challenge support + * @author Karlson2k (Evgeny Grin) + * + * The test starts the daemon with two ACME challenge certificates and + * connects to it with a simple GnuTLS client. For each tested combination of + * the SNI and the offered ALPN protocols the test checks which certificate has + * been served and which application protocol has been negotiated. + */ + +#include "mhd_sys_options.h" + +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <errno.h> + +#include "sys_bool_type.h" + +#include "sys_sockets_headers.h" +#include "sys_sockets_types.h" +#include "sys_ip_headers.h" +#include "mhd_sockets_macros.h" +#include <fcntl.h> + +#include <gnutls/gnutls.h> +#include <gnutls/x509.h> + +#include "microhttpd2.h" + +#include "acme_test_certs.h" + + +/* The ACME TLS-ALPN-01 challenge requires ALPN, which is supported by + GnuTLS 3.2.0 and later. The test client uses the same library. */ +#if GNUTLS_VERSION_NUMBER >= 0x030200 + +# ifndef mhd_SSTR_LEN +/** + * Determine the length of the static string at compile time + */ +# define mhd_SSTR_LEN(sstr) (sizeof(sstr) / sizeof(char) - 1) +# endif /* ! mhd_SSTR_LEN */ + + +# if defined(MHD_SOCKETS_KIND_POSIX) +# if defined(ENETUNREACH) +# define mhdt_SCKT_HARD_ERR ENETUNREACH +# elif defined(ENOTCONN) +# define mhdt_SCKT_HARD_ERR ENOTCONN +# elif defined(ECONNRESET) +# define mhdt_SCKT_HARD_ERR ECONNRESET +# elif defined(EPIPE) +# define mhdt_SCKT_HARD_ERR EPIPE +# elif defined(EBADF) +# define mhdt_SCKT_HARD_ERR EBADF +# else +# define mhdt_SCKT_HARD_ERR 99 /* Fallback, never used in practice */ +# endif +# else /* MHD_SOCKETS_KIND_WINSOCK */ +# define mhdt_SCKT_HARD_ERR WSAENETRESET +# endif + + +MHD_NORETURN_ static void +_externalErrorExit_func (const char *errDesc, + const char *funcName, + int lineNum) +{ + int last_errno = errno; + + fflush (stdout); + if ((NULL != errDesc) && (0 != errDesc[0])) + fprintf (stderr, "!!! %s", errDesc); + else + fprintf (stderr, "!!! System or external library call failed"); + if ((NULL != funcName) && (0 != funcName[0])) + fprintf (stderr, " in %s", funcName); + if (0 < lineNum) + fprintf (stderr, " at line %d", lineNum); + + fprintf (stderr, ".\nLast errno value: %d (%s)\n", last_errno, + strerror (last_errno)); +# ifdef MHD_SOCKETS_KIND_WINSOCK + fprintf (stderr, "WSAGetLastError() value: %d\n", (int)WSAGetLastError ()); +# endif /* MHD_SOCKETS_KIND_WINSOCK */ + fflush (stderr); + exit (99); +} + + +MHD_NORETURN_ static void +_mhdErrorExit_func (const char *errDesc, + const char *funcName, + int lineNum) +{ + int last_errno = errno; + + fflush (stdout); + if ((NULL != errDesc) && (0 != errDesc[0])) + fprintf (stderr, "!!! %s", errDesc); + else + fprintf (stderr, "!!! MHD unexpected error"); + if ((NULL != funcName) && (0 != funcName[0])) + fprintf (stderr, " in %s", funcName); + if (0 < lineNum) + fprintf (stderr, " at line %d", lineNum); + + fprintf (stderr, ".\nLast errno value: %d (%s)\n", last_errno, + strerror (last_errno)); + fflush (stderr); + exit (8); +} + + +static void +_testErrorLog_func (const char *errDesc, + const char *funcName, + int lineNum) +{ + int last_errno = errno; + + fflush (stdout); + if ((NULL != errDesc) && (0 != errDesc[0])) + fprintf (stderr, "!!! %s", errDesc); + else + fprintf (stderr, "!!! System or external library call resulted in error"); + if ((NULL != funcName) && (0 != funcName[0])) + fprintf (stderr, " in %s", funcName); + if (0 < lineNum) + fprintf (stderr, " at line %d", lineNum); + + fprintf (stderr, ".\nLast errno value: %d (%s)\n", last_errno, + strerror (last_errno)); +# ifdef MHD_SOCKETS_KIND_WINSOCK + fprintf (stderr, "WSAGetLastError() value: %d\n", (int)WSAGetLastError ()); +# endif /* MHD_SOCKETS_KIND_WINSOCK */ + fflush (stderr); +} + + +# ifdef MHD_HAVE_MHD_FUNC_ +# define externalErrorExitDesc(errDesc) \ + _externalErrorExit_func (errDesc, MHD_FUNC_, __LINE__) +# define mhdErrorExitDesc(errDesc) \ + _mhdErrorExit_func (errDesc, MHD_FUNC_, __LINE__) +# define testErrorLogDesc(errDesc) \ + _testErrorLog_func (errDesc, MHD_FUNC_, __LINE__) +# else /* ! MHD_HAVE_MHD_FUNC_ */ +# define externalErrorExitDesc(errDesc) \ + _externalErrorExit_func (errDesc, NULL, __LINE__) +# define mhdErrorExitDesc(errDesc) \ + _mhdErrorExit_func (errDesc, NULL, __LINE__) +# define testErrorLogDesc(errDesc) \ + _testErrorLog_func (errDesc, NULL, __LINE__) +# endif /* ! MHD_HAVE_MHD_FUNC_ */ + + +/* Could be increased to facilitate debugging */ +static int test_timeout = 5; + + +/** + * The string with the length known at compile time + */ +struct TestStr +{ + /** + * The pointer to the string, NULL if the string is not used + */ + const char *str; + /** + * The length of the @a str, not including the terminating null character + */ + size_t len; +}; + +/** + * Static initialiser for #TestStr from the static string + */ +# define TSTR(s) { (s), mhd_SSTR_LEN (s) } + +/** + * Static initialiser for the unused #TestStr + */ +# define TSTR_NONE { NULL, 0 } + + +/** + * The ALPN protocol name for the ACME TLS-ALPN-01 challenge + */ +# define ALPN_ACME "acme-tls/1" + +/** + * The ALPN protocol name for HTTP/1.1 + */ +# define ALPN_HTTP_1_1 "http/1.1" + +/** + * The domain without any ACME challenge certificate + */ +# define DOMAIN_NO_ACME "acme-test3.example" + +/** + * The first ACME domain spelled in the different letters case + */ +# define DOMAIN_A_MIXED_CASE "ACME-Test1.Example" + +/** + * The domain used only for the calls that must be rejected + */ +# define DOMAIN_BAD_CERT "bad-cert.example" + +/** + * The data that looks like a certificate, but is not one + */ +# define BAD_CERT_PEM \ + "-----BEGIN CERTIFICATE-----\n" \ + "this is not a certificate\n" \ + "-----END CERTIFICATE-----\n" + +/** + * The maximum number of the ALPN protocols offered by the test client + */ +# define MAX_ALPN_PROTS 2 + + +/** + * The certificate expected from the daemon + */ +enum ExpectedCert +{ + /** + * The certificate served for all non-ACME connections + */ + CERT_SERVER = 0 + , + /** + * The ACME challenge certificate for #mhdt_ACME_CHLNG_A_DOMAIN + */ + CERT_CHLNG_A + , + /** + * The ACME challenge certificate for #mhdt_ACME_CHLNG_B_DOMAIN + */ + CERT_CHLNG_B +}; + +/** + * Set to 'true' if the daemon has received any request. + * + * The test client never sends a request, so this must not happen. + */ +static volatile bool unexpected_request; + + +/** + * The certificates in PEM format, indexed by #ExpectedCert + */ +static const struct TestStr certs_pem[] = { + TSTR (mhdt_ACME_SRV_CERT_PEM), + TSTR (mhdt_ACME_CHLNG_A_CERT_PEM), + TSTR (mhdt_ACME_CHLNG_B_CERT_PEM) +}; + +/** + * The names of the certificates for the log, indexed by #ExpectedCert + */ +static const char *const certs_name[] = { + "the server certificate", + "the challenge certificate \"A\"", + "the challenge certificate \"B\"" +}; + + +/** + * The parameters and the expected results for a single connection + */ +struct TestCase +{ + /** + * The name of the test case + */ + const char *label; + /** + * The domain name sent as SNI, unused string to send no SNI + */ + struct TestStr sni; + /** + * The ALPN protocols offered by the client, terminated by the unused string + */ + struct TestStr alpn[MAX_ALPN_PROTS + 1]; + /** + * The protocol expected to be negotiated, unused string if none must be + * selected + */ + struct TestStr expect_alpn; + /** + * The certificate expected to be served + */ + enum ExpectedCert expect_cert; +}; + + +/** + * The test cases performed for every TLS backend + */ +static const struct TestCase test_cases[] = { + { + "the ACME challenge for the first domain", + TSTR (mhdt_ACME_CHLNG_A_DOMAIN), + { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE }, + TSTR (ALPN_ACME), + CERT_CHLNG_A + }, + { + "the ACME challenge for the second domain", + TSTR (mhdt_ACME_CHLNG_B_DOMAIN), + { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE }, + TSTR (ALPN_ACME), + CERT_CHLNG_B + }, + { + "the ACME domain in the different letters case", + TSTR (DOMAIN_A_MIXED_CASE), + { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE }, + TSTR (ALPN_ACME), + CERT_CHLNG_A + }, + { + "the ACME protocol together with the HTTP protocol", + TSTR (mhdt_ACME_CHLNG_A_DOMAIN), + { TSTR (ALPN_ACME), TSTR (ALPN_HTTP_1_1), TSTR_NONE }, + TSTR (ALPN_HTTP_1_1), + CERT_SERVER + }, + { + "the ACME protocol for the domain without the challenge", + TSTR (DOMAIN_NO_ACME), + { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE }, + TSTR_NONE, + CERT_SERVER + }, + { + "the ACME protocol without the domain name", + TSTR_NONE, + { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE }, + TSTR_NONE, + CERT_SERVER + }, + { + "the HTTP protocol for the ACME domain", + TSTR (mhdt_ACME_CHLNG_A_DOMAIN), + { TSTR (ALPN_HTTP_1_1), TSTR_NONE, TSTR_NONE }, + TSTR (ALPN_HTTP_1_1), + CERT_SERVER + }, + { + "the ACME domain without the ALPN extension", + TSTR (mhdt_ACME_CHLNG_A_DOMAIN), + { TSTR_NONE, TSTR_NONE, TSTR_NONE }, + TSTR_NONE, + CERT_SERVER + }, + { + "the HTTP protocol offered before the ACME protocol", + TSTR (mhdt_ACME_CHLNG_A_DOMAIN), + { TSTR (ALPN_HTTP_1_1), TSTR (ALPN_ACME), TSTR_NONE }, + TSTR (ALPN_HTTP_1_1), + CERT_SERVER + }, + { + "the ACME protocol offered twice", + TSTR (mhdt_ACME_CHLNG_A_DOMAIN), + { TSTR (ALPN_ACME), TSTR (ALPN_ACME), TSTR_NONE }, + TSTR_NONE, + CERT_SERVER + } +}; + +/** + * The test case performed after the removal of the challenge certificate + * for the first domain + */ +static const struct TestCase removed_case = { + "the certificate removed by the differently spelled domain is not served", + TSTR (mhdt_ACME_CHLNG_A_DOMAIN), + { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE }, + TSTR_NONE, + CERT_SERVER +}; + +/** + * The check for the fallback certificate: the domain without a certificate + * of its own is served with the certificate registered for any domain + */ +static const struct TestCase fallback_case = { + "the fallback certificate is served for the unknown domain", + TSTR (DOMAIN_NO_ACME), + { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE }, + TSTR (ALPN_ACME), + CERT_CHLNG_A +}; + +/** + * The check that the fallback certificate is not used without the SNI + */ +static const struct TestCase fallback_no_sni_case = { + "the fallback certificate is not served without the domain name", + TSTR_NONE, + { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE }, + TSTR_NONE, + CERT_SERVER +}; + +/** + * The check that the certificate of the domain takes precedence over + * the fallback certificate + */ +static const struct TestCase fallback_specific_case = { + "the domain certificate takes precedence over the fallback certificate", + TSTR (mhdt_ACME_CHLNG_B_DOMAIN), + { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE }, + TSTR (ALPN_ACME), + CERT_CHLNG_B +}; + +/** + * The check performed after the challenge certificate for the first domain + * has been replaced by another certificate + */ +static const struct TestCase replaced_case = { + "the replaced challenge certificate is served", + TSTR (mhdt_ACME_CHLNG_A_DOMAIN), + { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE }, + TSTR (ALPN_ACME), + CERT_CHLNG_B +}; + +/** + * The check performed after the removal of all the challenge certificates + * by the single call. The second domain is used: its certificate has never + * been removed individually. + */ +static const struct TestCase cleared_case = { + "no challenge certificate is served after the removal of all of them", + TSTR (mhdt_ACME_CHLNG_B_DOMAIN), + { TSTR (ALPN_ACME), TSTR_NONE, TSTR_NONE }, + TSTR_NONE, + CERT_SERVER +}; + + +/** + * Set the socket to the non-blocking mode + * @param fd the socket to set the mode for + */ +static void +make_nonblocking (MHD_Socket fd) +{ +# if defined(MHD_SOCKETS_KIND_POSIX) + int flags; + + flags = fcntl (fd, F_GETFL); + if (-1 == flags) + externalErrorExitDesc ("fcntl() failed"); + if (O_NONBLOCK != (flags & O_NONBLOCK)) + if (-1 == fcntl (fd, F_SETFL, flags | O_NONBLOCK)) + externalErrorExitDesc ("fcntl() failed"); +# elif defined(MHD_SOCKETS_KIND_WINSOCK) + unsigned long flags = 1; + + if (0 != ioctlsocket (fd, (int)FIONBIO, &flags)) + externalErrorExitDesc ("ioctlsocket() failed"); +# endif /* MHD_SOCKETS_KIND_WINSOCK */ +} + + +/** + * Disable Nagle's algorithm on the socket, the failure is not fatal + * @param fd the socket to set the option for + */ +static void +make_nodelay (MHD_Socket fd) +{ +# ifdef TCP_NODELAY + const mhd_SCKT_OPT_BOOL on_val = 1; + + if (0 == setsockopt (fd, + IPPROTO_TCP, + TCP_NODELAY, + (const void *)&on_val, + sizeof (on_val))) + return; /* Success exit point */ + +# ifndef MHD_SOCKETS_KIND_WINSOCK + fprintf (stderr, "Failed to enable TCP_NODELAY on socket (ignored). " + "errno: %d (%s)\n", (int)errno, strerror (errno)); +# else /* MHD_SOCKETS_KIND_WINSOCK */ + fprintf (stderr, "Failed to enable TCP_NODELAY on socket (ignored). " + "WSAGetLastError() value: %d\n", (int)WSAGetLastError ()); +# endif /* MHD_SOCKETS_KIND_WINSOCK */ + fflush (stderr); +# else /* ! TCP_NODELAY */ + (void)fd; /* Unused */ +# endif /* ! TCP_NODELAY */ +} + + +/** + * The client socket with the TLS session on it + */ +struct wr_socket +{ + /** + * The socket + */ + MHD_Socket fd; + /** + * 'true' if the socket is in the non-blocking mode + */ + bool is_nonblocking; + /** + * The TLS credentials, without any trusted CA + */ + gnutls_certificate_credentials_t tls_crd; + /** + * The TLS session + */ + gnutls_session_t tls_s; + /** + * 'true' if the TLS handshake has been completed + */ + bool tls_connected; +}; + + +/** + * Create the socket with the TLS session, without connecting it. + * + * The server certificate is not verified: the daemon uses self-signed + * certificates. + * + * @return the created socket, + * NULL on failure + */ +static struct wr_socket * +wr_create_tls_sckt (void) +{ + struct wr_socket *s; + + s = (struct wr_socket *)malloc (sizeof(struct wr_socket)); + if (NULL == s) + { + testErrorLogDesc ("malloc() failed"); + return NULL; + } + s->is_nonblocking = false; + s->tls_connected = false; + s->fd = socket (AF_INET, SOCK_STREAM, IPPROTO_TCP); + if (MHD_INVALID_SOCKET != s->fd) + { + make_nodelay (s->fd); + if (GNUTLS_E_SUCCESS == gnutls_init (&(s->tls_s), GNUTLS_CLIENT)) + { + if (GNUTLS_E_SUCCESS == gnutls_set_default_priority (s->tls_s)) + { + if (GNUTLS_E_SUCCESS == + gnutls_certificate_allocate_credentials (&(s->tls_crd))) + { + if (GNUTLS_E_SUCCESS == + gnutls_credentials_set (s->tls_s, + GNUTLS_CRD_CERTIFICATE, + s->tls_crd)) + { +# ifndef _WIN64 + gnutls_transport_set_int (s->tls_s, (int)(s->fd)); +# else /* Win x64 */ + /* The socket does not fit into "int" */ + gnutls_transport_set_ptr (s->tls_s, + (gnutls_transport_ptr_t) \ + (intptr_t)(s->fd)); +# endif /* Win x64 */ + return s; /* Success exit point */ + } + else + testErrorLogDesc ("gnutls_credentials_set() failed"); + + /* Below is a clean-up path */ + gnutls_certificate_free_credentials (s->tls_crd); + } + else + testErrorLogDesc ("gnutls_certificate_allocate_credentials() failed"); + } + else + testErrorLogDesc ("gnutls_set_default_priority() failed"); + gnutls_deinit (s->tls_s); + } + else + testErrorLogDesc ("gnutls_init() failed"); + (void)mhd_socket_close (s->fd); + } + else + testErrorLogDesc ("socket() failed"); + free (s); + return NULL; /* Failure exit point */ +} + + +/** + * Close the socket and release the TLS session + * @param s the socket to close + */ +static void +wr_close (struct wr_socket *s) +{ + (void)mhd_socket_close (s->fd); + gnutls_deinit (s->tls_s); + gnutls_certificate_free_credentials (s->tls_crd); + free (s); +} + + +/** + * Set the socket to the non-blocking mode if it is not set already + * @param s the socket to set the mode for + */ +static void +wr_make_nonblocking (struct wr_socket *s) +{ + if (!s->is_nonblocking) + make_nonblocking (s->fd); + s->is_nonblocking = true; +} + + +/** + * The direction to wait for + */ +enum wr_wait_for_type +{ + WR_WAIT_FOR_RECV = 0 + , + WR_WAIT_FOR_SEND = 1 +}; + + +/** + * Wait for the socket to become ready for the specified operation. + * + * @param s the socket to wait for + * @param timeout_ms the maximum wait time in milliseconds, + * no limit if negative value is used + * @param wait_for the direction to wait for + * @return 'true' if the socket is ready, + * 'false' on timeout or error + */ +static bool +wr_wait_socket_ready_noabort_ (struct wr_socket *s, + int timeout_ms, + enum wr_wait_for_type wait_for) +{ + fd_set fds; + int sel_res; + struct timeval tmo; + struct timeval *tmo_ptr; + +# ifndef MHD_SOCKETS_KIND_WINSOCK + if (FD_SETSIZE <= s->fd) + externalErrorExitDesc ("Too large FD value"); +# endif /* ! MHD_SOCKETS_KIND_WINSOCK */ + FD_ZERO (&fds); + FD_SET (s->fd, &fds); + if (0 <= timeout_ms) + { +# if !defined(_WIN32) || defined(__CYGWIN__) + tmo.tv_sec = (time_t)(timeout_ms / 1000); +# else /* Native W32 */ + tmo.tv_sec = (long)(timeout_ms / 1000); +# endif /* Native W32 */ + tmo.tv_usec = ((long)(timeout_ms % 1000)) * 1000; + tmo_ptr = &tmo; + } + else + tmo_ptr = NULL; /* No timeout */ + + do + { + if (WR_WAIT_FOR_RECV == wait_for) + sel_res = select (1 + (int)s->fd, &fds, NULL, NULL, tmo_ptr); + else + sel_res = select (1 + (int)s->fd, NULL, &fds, NULL, tmo_ptr); + } while ((0 > sel_res) && mhd_SCKT_ERR_IS_EINTR (mhd_SCKT_GET_LERR ())); + + if (1 == sel_res) + return true; + + if (0 == sel_res) + fprintf (stderr, "Timeout"); + else + { +# ifndef MHD_SOCKETS_KIND_WINSOCK + fprintf (stderr, "Error %d (%s)", (int)errno, strerror (errno)); +# else /* MHD_SOCKETS_KIND_WINSOCK */ + fprintf (stderr, "Error (WSAGetLastError code: %d)", + (int)WSAGetLastError ()); +# endif /* MHD_SOCKETS_KIND_WINSOCK */ + } + fprintf (stderr, " waiting for socket to be available for %s.\n", + (WR_WAIT_FOR_RECV == wait_for) ? "receiving" : "sending"); + return false; +} + + +/** + * Wait for the socket to become ready, abort the test on failure + * @param s the socket to wait for + * @param timeout_ms the maximum wait time in milliseconds + * @param wait_for the direction to wait for + */ +static void +wr_wait_socket_ready_ (struct wr_socket *s, + int timeout_ms, + enum wr_wait_for_type wait_for) +{ + if (wr_wait_socket_ready_noabort_ (s, timeout_ms, wait_for)) + return; + + if (WR_WAIT_FOR_RECV == wait_for) + mhdErrorExitDesc ("The daemon did not send the data in time"); + else + mhdErrorExitDesc ("The daemon did not accept the data in time"); +} + + +/** + * Connect the socket to the specified address. + * + * @param s the socket to connect + * @param addr the address to connect to + * @param length the size of the structure pointed by @a addr + * @param timeout_ms the maximum wait time in milliseconds + * @return 'true' on success, + * 'false' otherwise + */ +static bool +wr_connect_tmo (struct wr_socket *s, + const struct sockaddr *addr, + unsigned int length, + int timeout_ms) +{ + if (0 != connect (s->fd, addr, (socklen_t)length)) + { + int err; + bool connect_completed = false; + + err = mhd_SCKT_GET_LERR (); +# if defined(MHD_SOCKETS_KIND_POSIX) + while ((!connect_completed) && (EINTR == err)) + { + connect_completed = (0 == connect (s->fd, addr, (socklen_t)length)); + if (!connect_completed) + { + err = errno; + if (EALREADY == err) + err = EINPROGRESS; + else if (EISCONN == err) + connect_completed = true; + } + } +# endif /* MHD_SOCKETS_KIND_POSIX */ + /* No modern system uses EAGAIN here, except W32 */ + if ((!connect_completed) + && (mhd_SCKT_ERR_IS_INPROGRESS (err) + || mhd_SCKT_ERR_IS_EAGAIN (err))) + connect_completed = + wr_wait_socket_ready_noabort_ (s, timeout_ms, WR_WAIT_FOR_SEND); + if (!connect_completed) + { + testErrorLogDesc ("connect() failed"); + return false; + } + } + return true; +} + + +/** + * Connect the socket to the daemon on the loopback interface + * @param s the socket to connect + * @param port the port of the daemon + * @return 'true' on success, + * 'false' otherwise + */ +static bool +wr_connect_to_daemon (struct wr_socket *s, + uint16_t port) +{ + struct sockaddr_in sa; + + memset (&sa, 0, sizeof(sa)); + sa.sin_family = AF_INET; + sa.sin_port = htons (port); + if (1 != inet_pton (AF_INET, + "127.0.0.1", + &(sa.sin_addr))) + { + testErrorLogDesc ("inet_pton() failed"); + return false; + } + return wr_connect_tmo (s, + (const struct sockaddr *)&sa, + (unsigned int)sizeof(sa), + test_timeout * 1000); +} + + +/** + * The result of the TLS handshake performed by the test client + */ +enum HandshakeResult +{ + /** + * The handshake has been completed + */ + HSK_COMPLETED = 0 + , + /** + * The daemon aborted the handshake as no offered ALPN protocol matched + */ + HSK_NO_ALPN_ALERT + , + /** + * The handshake failed + */ + HSK_FAILED +}; + + +/** + * Perform the TLS handshake. + * + * Unlike the same function in the "upgrade" test, the rejection by the + * "no_application_protocol" alert is reported separately: it is the expected + * result for some of the test cases. + * + * @param s the socket to perform the handshake on + * @param timeout_ms the maximum wait time in milliseconds + * @return the result of the handshake + */ +static enum HandshakeResult +wr_handshake_tmo_ (struct wr_socket *s, + int timeout_ms) +{ + int res; + + res = gnutls_handshake (s->tls_s); + while ((GNUTLS_E_AGAIN == res) || (GNUTLS_E_INTERRUPTED == res)) + { + wr_wait_socket_ready_ (s, timeout_ms, + gnutls_record_get_direction (s->tls_s) ? + WR_WAIT_FOR_SEND : WR_WAIT_FOR_RECV); + res = gnutls_handshake (s->tls_s); + } + if (GNUTLS_E_SUCCESS == res) + { + s->tls_connected = true; + return HSK_COMPLETED; + } + if ((GNUTLS_E_FATAL_ALERT_RECEIVED == res) + && (GNUTLS_A_NO_APPLICATION_PROTOCOL == gnutls_alert_get (s->tls_s))) + return HSK_NO_ALPN_ALERT; + + fprintf (stderr, "The error returned by gnutls_handshake() is " + "'%s' ", gnutls_strerror (res)); + fprintf (stderr, "(%s)\n", gnutls_strerror_name (res)); + testErrorLogDesc ("gnutls_handshake() failed with hard error"); + mhd_SCKT_SET_LERR (mhdt_SCKT_HARD_ERR); /* hard error */ + return HSK_FAILED; +} + + +/** + * Perform the TLS handshake with the default timeout + * @param s the socket to perform the handshake on + * @return the result of the handshake + */ +static enum HandshakeResult +wr_handshake (struct wr_socket *s) +{ + return wr_handshake_tmo_ (s, test_timeout * 1000); +} + + +/** + * Send the TLS "close notify" alert, the failure is not fatal. + * + * The daemon should not be left with the abruptly terminated connection. + * + * @param s the socket to close the session on + * @param timeout_ms the maximum wait time in milliseconds + */ +static void +wr_close_notify_tmo_ (struct wr_socket *s, + int timeout_ms) +{ + int res; + + if (!s->tls_connected) + return; + + res = gnutls_bye (s->tls_s, GNUTLS_SHUT_WR); + while ((GNUTLS_E_AGAIN == res) || (GNUTLS_E_INTERRUPTED == res)) + { + if (!wr_wait_socket_ready_noabort_ (s, timeout_ms, + gnutls_record_get_direction (s->tls_s) ? + WR_WAIT_FOR_SEND : WR_WAIT_FOR_RECV)) + return; + res = gnutls_bye (s->tls_s, GNUTLS_SHUT_WR); + } +} + + +/** + * Set the SNI and the ALPN protocols requested by the test case. + * + * @param s the socket to set the extensions for + * @param tc the test case being performed + * @return 'true' on success, + * 'false' otherwise + */ +static bool +client_set_extensions (struct wr_socket *s, + const struct TestCase *tc) +{ + gnutls_datum_t prots[MAX_ALPN_PROTS]; + unsigned int num_prots; + + if (NULL != tc->sni.str) + { + if (GNUTLS_E_SUCCESS != + gnutls_server_name_set (s->tls_s, + GNUTLS_NAME_DNS, + tc->sni.str, + tc->sni.len)) + { + testErrorLogDesc ("gnutls_server_name_set() failed"); + return false; + } + } + for (num_prots = 0; NULL != tc->alpn[num_prots].str; ++num_prots) + { + prots[num_prots].data = + (unsigned char *)mhd_DROP_CONST (tc->alpn[num_prots].str); + prots[num_prots].size = (unsigned int)tc->alpn[num_prots].len; + } + if (0 == num_prots) + return true; /* Send no ALPN extension at all */ + + if (GNUTLS_E_SUCCESS != + gnutls_alpn_set_protocols (s->tls_s, + prots, + num_prots, + 0)) + { + testErrorLogDesc ("gnutls_alpn_set_protocols() failed"); + return false; + } + return true; +} + + +/** + * Check that the daemon has served the expected certificate. + * + * The certificates are compared in the DER form: the challenge certificate + * "B" repeats the subject of the server certificate, so the names alone + * cannot tell them apart. + * + * @param sess the completed session + * @param tc the test case being performed + * @return 'true' if the expected certificate has been served, + * 'false' otherwise + */ +static bool +check_served_cert (gnutls_session_t sess, + const struct TestCase *tc) +{ + const gnutls_datum_t *peers; + unsigned int num_peers; + gnutls_x509_crt_t crt; + gnutls_datum_t pem; + gnutls_datum_t der; + bool res; + + peers = gnutls_certificate_get_peers (sess, + &num_peers); + if ((NULL == peers) || (0 == num_peers)) + { + fprintf (stderr, + "The daemon has not sent any certificate.\n"); + return false; + } + if (GNUTLS_E_SUCCESS != gnutls_x509_crt_init (&crt)) + { + fprintf (stderr, + "gnutls_x509_crt_init() failed.\n"); + return false; + } + pem.data = + (unsigned char *)mhd_DROP_CONST (certs_pem[tc->expect_cert].str); + pem.size = (unsigned int)certs_pem[tc->expect_cert].len; + res = false; + if (GNUTLS_E_SUCCESS == gnutls_x509_crt_import (crt, + &pem, + GNUTLS_X509_FMT_PEM)) + { + if (GNUTLS_E_SUCCESS == gnutls_x509_crt_export2 (crt, + GNUTLS_X509_FMT_DER, + &der)) + { + res = ((der.size == peers[0].size) + && (0 == memcmp (der.data, + peers[0].data, + (size_t)der.size))); + gnutls_free (der.data); + } + else + fprintf (stderr, + "gnutls_x509_crt_export2() failed.\n"); + } + else + fprintf (stderr, + "gnutls_x509_crt_import() failed.\n"); + gnutls_x509_crt_deinit (crt); + if (!res) + fprintf (stderr, + "The daemon has not served %s.\n", + certs_name[tc->expect_cert]); + return res; +} + + +/** + * Check that the expected application protocol has been negotiated. + * + * @param sess the completed session + * @param tc the test case being performed + * @return 'true' if the result matches the expectation, + * 'false' otherwise + */ +static bool +check_selected_alpn (gnutls_session_t sess, + const struct TestCase *tc) +{ + gnutls_datum_t alpn; + + if (GNUTLS_E_SUCCESS != gnutls_alpn_get_selected_protocol (sess, + &alpn)) + { + if (NULL == tc->expect_alpn.str) + return true; + fprintf (stderr, + "No application protocol has been negotiated, \"%s\" expected.\n", + tc->expect_alpn.str); + return false; + } + if ((NULL != tc->expect_alpn.str) + && (tc->expect_alpn.len == (size_t)alpn.size) + && (0 == memcmp (tc->expect_alpn.str, + alpn.data, + (size_t)alpn.size))) + return true; + + fprintf (stderr, + "The negotiated application protocol is \"%.*s\", %s expected.\n", + (int)alpn.size, + (const char *)alpn.data, + (NULL != tc->expect_alpn.str) ? tc->expect_alpn.str : "none"); + return false; +} + + +/** + * Perform a single test case. + * + * @param port the port of the daemon + * @param tc the test case to perform + * @return 'true' if the results are as expected, + * 'false' otherwise + */ +static bool +run_test_case (uint16_t port, + const struct TestCase *tc) +{ + struct wr_socket *s; + bool res; + + s = wr_create_tls_sckt (); + if (NULL == s) + return false; + + if (client_set_extensions (s, + tc)) + { + wr_make_nonblocking (s); + if (wr_connect_to_daemon (s, + port)) + { + switch (wr_handshake (s)) + { + case HSK_COMPLETED: + res = check_selected_alpn (s->tls_s, + tc); + if (!check_served_cert (s->tls_s, + tc)) + res = false; + break; + case HSK_NO_ALPN_ALERT: + /* The daemon supports none of the offered protocols and aborts the + handshake, as required by RFC 7301, section 3.2. The certificate + cannot be checked, the test case is passed if no protocol was + expected to be negotiated. */ + res = (NULL == tc->expect_alpn.str); + if (!res) + fprintf (stderr, + "The daemon rejected the offered application protocols.\n"); + break; + case HSK_FAILED: + default: + res = false; + break; + } + wr_close_notify_tmo_ (s, + test_timeout * 1000); + } + else + res = false; + } + else + res = false; + wr_close (s); + return res; +} + + +/** + * Check the status code returned by the rejected call + * + * @param sc the returned status code + * @param expected the expected status code + * @param descr the description of the checked call + * @return 'true' if the code is the expected one, + * 'false' otherwise + */ +static bool +check_status (enum MHD_StatusCode sc, + enum MHD_StatusCode expected, + const char *descr) +{ + if (expected == sc) + return true; + + fprintf (stderr, + "The status code for %s is %u, %u expected.\n", + descr, + (unsigned int)sc, + (unsigned int)expected); + return false; +} + + +/** + * The daemon request callback. + * + * The test client never sends any request, the callback is required by + * #MHD_daemon_create(). + */ +static const struct MHD_Action * +server_req_cb (void *cls, + struct MHD_Request *MHD_RESTRICT request, + const struct MHD_String *MHD_RESTRICT path, + enum MHD_HTTP_Method method, + uint_fast64_t upload_size) +{ + (void)cls; /* Unused */ + (void)path; /* Unused */ + (void)method; /* Unused */ + (void)upload_size; /* Unused */ + + unexpected_request = true; + fprintf (stderr, + "Unexpected request received.\n"); + return MHD_action_from_response ( + request, + MHD_response_from_empty (MHD_HTTP_STATUS_NO_CONTENT)); +} + + +/** + * Configure the started daemon and add the ACME challenge certificates. + * + * Everything set here is owned by the daemon, so nothing has to be released + * by this function on failure. + * + * @param d the created daemon + * @param be the TLS backend to use + * @param[out] p_port the port bound by the daemon, set on success only + * @return 0 on success, + * 77 if the ACME ALPN challenge is not supported, + * 1 on failure + */ +static int +daemon_setup (struct MHD_Daemon *d, + enum MHD_TlsBackend be, + uint16_t *p_port) +{ + union MHD_DaemonInfoFixedData info; + enum MHD_StatusCode sc; + + if (MHD_SC_OK != + MHD_DAEMON_SET_OPTIONS ( + d, + MHD_D_OPTION_WM_WORKER_THREADS (1), + MHD_D_OPTION_DEFAULT_TIMEOUT_MILSEC (2000), + MHD_D_OPTION_BIND_PORT (MHD_AF_AUTO, + 0), + MHD_D_OPTION_TLS (be), + /* The certificate parameter is not declared "const" */ + MHD_D_OPTION_TLS_CERT_KEY ((char *)mhd_DROP_CONST ( + mhdt_ACME_SRV_CERT_PEM), + mhdt_ACME_SRV_KEY_PEM, + NULL))) + { + fprintf (stderr, + "Failed to configure the daemon.\n"); + return 1; + } + + sc = MHD_daemon_acme_alpn_cert_add (d, + mhdt_ACME_CHLNG_A_DOMAIN, + mhdt_ACME_CHLNG_A_CERT_PEM, + mhdt_ACME_CHLNG_A_KEY_PEM, + NULL); + if (!check_status (sc, + MHD_SC_TOO_EARLY, + "adding the certificate before the daemon start")) + return 1; + + sc = MHD_daemon_acme_alpn_cert_del (d, + mhdt_ACME_CHLNG_A_DOMAIN); + if (!check_status (sc, + MHD_SC_TOO_EARLY, + "removing the certificate before the daemon start")) + return 1; + + sc = MHD_daemon_start (d); + if (MHD_SC_OK != sc) + { + fprintf (stderr, + "Failed to start the daemon: %u\n", + (unsigned int)sc); + return 1; + } + + sc = MHD_daemon_acme_alpn_cert_add (d, + mhdt_ACME_CHLNG_A_DOMAIN, + mhdt_ACME_CHLNG_A_CERT_PEM, + mhdt_ACME_CHLNG_A_KEY_PEM, + NULL); + if (MHD_SC_TLS_BACKEND_OPERATION_UNSUPPORTED == sc) + { + fprintf (stderr, + "The ACME ALPN challenge is not supported, skipping.\n"); + return 77; + } + if (MHD_SC_OK == sc) + sc = MHD_daemon_acme_alpn_cert_add (d, + mhdt_ACME_CHLNG_B_DOMAIN, + mhdt_ACME_CHLNG_B_CERT_PEM, + mhdt_ACME_CHLNG_B_KEY_PEM, + NULL); + if (MHD_SC_OK != sc) + { + fprintf (stderr, + "MHD_daemon_acme_alpn_cert_add() failed: %u\n", + (unsigned int)sc); + return 1; + } + + if (MHD_SC_OK != + MHD_daemon_get_info_fixed (d, + MHD_DAEMON_INFO_FIXED_BIND_PORT, + &info)) + { + fprintf (stderr, + "Failed to get the port of the daemon.\n"); + return 1; + } + *p_port = info.v_bind_port_uint16; + return 0; +} + + +/** + * Create and start the daemon with the ACME challenge certificates. + * + * @param be the TLS backend to use + * @param[out] p_d the started daemon, set on success only + * @param[out] p_port the port bound by the daemon, set on success only + * @return 0 on success, + * 77 if the ACME ALPN challenge is not supported, + * 1 on failure + */ +static int +start_daemon (enum MHD_TlsBackend be, + struct MHD_Daemon **p_d, + uint16_t *p_port) +{ + struct MHD_Daemon *d; + int res; + + *p_d = NULL; + d = MHD_daemon_create (&server_req_cb, + NULL); + if (NULL == d) + { + fprintf (stderr, + "MHD_daemon_create() failed.\n"); + return 1; + } + + res = daemon_setup (d, + be, + p_port); + if (0 == res) + { + *p_d = d; + return 0; /* Success exit point */ + } + + /* Below is a clean-up path */ + MHD_daemon_destroy (d); + return res; /* Failure exit point */ +} + + +/** + * Check that the certificate management functions reject the wrong use. + * + * @param d the started daemon + * @return 'true' if all the calls are rejected as expected, + * 'false' otherwise + */ +static bool +check_cert_api_misuse (struct MHD_Daemon *d) +{ + enum MHD_StatusCode sc; + bool res; + + res = true; + + sc = MHD_daemon_acme_alpn_cert_add (d, + "", + mhdt_ACME_CHLNG_A_CERT_PEM, + mhdt_ACME_CHLNG_A_KEY_PEM, + NULL); + if (!check_status (sc, + MHD_SC_PARAM_EMPTY, + "adding the certificate for the empty domain")) + res = false; + + sc = MHD_daemon_acme_alpn_cert_del (d, + ""); + if (!check_status (sc, + MHD_SC_PARAM_EMPTY, + "removing the certificate of the empty domain")) + res = false; + + sc = MHD_daemon_acme_alpn_cert_del (d, + DOMAIN_NO_ACME); + if (!check_status (sc, + MHD_SC_ITEM_NOT_FOUND, + "removing the certificate of the unknown domain")) + res = false; + + sc = MHD_daemon_acme_alpn_cert_add (d, + DOMAIN_BAD_CERT, + "", + mhdt_ACME_CHLNG_A_KEY_PEM, + NULL); + if (!check_status (sc, + MHD_SC_TLS_CONF_BAD_CERT, + "adding the empty certificate")) + res = false; + + sc = MHD_daemon_acme_alpn_cert_add (d, + DOMAIN_BAD_CERT, + BAD_CERT_PEM, + mhdt_ACME_CHLNG_A_KEY_PEM, + NULL); + if (!check_status (sc, + MHD_SC_TLS_CONF_BAD_CERT, + "adding the malformed certificate")) + res = false; + + sc = MHD_daemon_acme_alpn_cert_add (d, + DOMAIN_BAD_CERT, + mhdt_ACME_CHLNG_A_CERT_PEM, + mhdt_ACME_CHLNG_B_KEY_PEM, + NULL); + if (!check_status (sc, + MHD_SC_TLS_CONF_BAD_CERT, + "adding the certificate with the key of another one")) + res = false; + + return res; +} + + +/** + * Perform a single test case and report the failure + * + * @param port the port of the daemon + * @param tc the test case to perform + * @return 'true' if the results are as expected, + * 'false' otherwise + */ +static bool +run_case (uint16_t port, + const struct TestCase *tc) +{ + fprintf (stderr, + "Checking %s.\n", + tc->label); + if (run_test_case (port, + tc)) + return true; + + fprintf (stderr, + "FAILED: %s\n", + tc->label); + return false; +} + + +/** + * Perform all the test cases with the specified TLS backend. + * + * @param be the TLS backend to use + * @param be_name the name of the backend for the log + * @return 0 on success, + * 77 if the ACME ALPN challenge is not supported, + * 1 if any test case failed + */ +static int +run_backend (enum MHD_TlsBackend be, + const char *be_name) +{ + struct MHD_Daemon *d; + uint16_t port; + unsigned int i; + int res; + + fprintf (stderr, + "Testing with the TLS backend \"%s\".\n", + be_name); + res = start_daemon (be, + &d, + &port); + if (0 != res) + return res; + + if (!check_cert_api_misuse (d)) + res = 1; + + for (i = 0; i < (sizeof(test_cases) / sizeof(test_cases[0])); ++i) + { + if (!run_case (port, + test_cases + i)) + res = 1; + } + + /* The certificate for the first domain is removed, the domain name is + spelled in the different letters case */ + if (MHD_SC_OK != + MHD_daemon_acme_alpn_cert_del (d, + DOMAIN_A_MIXED_CASE)) + { + fprintf (stderr, + "MHD_daemon_acme_alpn_cert_del() failed.\n"); + res = 1; + } + else if (!run_case (port, + &removed_case)) + res = 1; + + /* The same certificate is registered for any domain */ + if (MHD_SC_OK != + MHD_daemon_acme_alpn_cert_add (d, + NULL, + mhdt_ACME_CHLNG_A_CERT_PEM, + mhdt_ACME_CHLNG_A_KEY_PEM, + NULL)) + { + fprintf (stderr, + "Failed to add the fallback certificate.\n"); + res = 1; + } + else + { + if (!run_case (port, + &fallback_case)) + res = 1; + if (!run_case (port, + &fallback_no_sni_case)) + res = 1; + if (!run_case (port, + &fallback_specific_case)) + res = 1; + } + + /* The certificate of the first domain is set to another certificate */ + if (MHD_SC_OK != + MHD_daemon_acme_alpn_cert_add (d, + mhdt_ACME_CHLNG_A_DOMAIN, + mhdt_ACME_CHLNG_B_CERT_PEM, + mhdt_ACME_CHLNG_B_KEY_PEM, + NULL)) + { + fprintf (stderr, + "Failed to replace the challenge certificate.\n"); + res = 1; + } + else if (!run_case (port, + &replaced_case)) + res = 1; + + /* All the challenge certificates are removed by the single call */ + if (MHD_SC_OK != + MHD_daemon_acme_alpn_cert_del (d, + NULL)) + { + fprintf (stderr, + "Failed to remove all the challenge certificates.\n"); + res = 1; + } + else if (!run_case (port, + &cleared_case)) + res = 1; + + /* Unlike the removal by the domain name, the removal of all the + certificates is not an error when nothing is left to remove */ + if (MHD_SC_OK != + MHD_daemon_acme_alpn_cert_del (d, + NULL)) + { + fprintf (stderr, + "The repeated removal of all the certificates failed.\n"); + res = 1; + } + + MHD_daemon_destroy (d); + + if (unexpected_request) + { + fprintf (stderr, + "FAILED: the daemon has received a request.\n"); + unexpected_request = false; + res = 1; + } + return res; +} + + +int +main (int argc, + char *argv[]) +{ + bool tested_one_or_more; + bool failed; + int res; + + (void)argc; /* Unused */ + (void)argv; /* Unused */ + + if (GNUTLS_E_SUCCESS != gnutls_global_init ()) + { + fprintf (stderr, + "gnutls_global_init() failed.\n"); + return 99; + } + tested_one_or_more = false; + failed = false; + + res = run_backend (MHD_TLS_BACKEND_GNUTLS, + "GnuTLS"); + if (77 != res) + { + tested_one_or_more = true; + if (0 != res) + failed = true; + } +# ifdef MHD_SUPPORT_OPENSSL + res = run_backend (MHD_TLS_BACKEND_OPENSSL, + "OpenSSL"); + if (77 != res) + { + tested_one_or_more = true; + if (0 != res) + failed = true; + } +# endif /* MHD_SUPPORT_OPENSSL */ + + gnutls_global_deinit (); + + if (!tested_one_or_more) + return 77; + + return failed ? 1 : 0; +} + + +#else /* GnuTLS before 3.2.0 */ + +int +main (void) +{ + fprintf (stderr, + "GnuTLS has no ALPN support, skipping.\n"); + return 77; +} + + +#endif /* GnuTLS before 3.2.0 */