Hi Evgeny,
On Fri, Sep 18, 2026 at 11:57 PM Yegor Yefremov <[email protected]> wrote: > > From: Yegor Yefremov <[email protected]> > > GnuTLS takes a "success" answer from a gnutls_certificate_retrieve_function2 > or ...3 at face value: it dereferences the returned private key without > checking it. An application callback that returns zero while leaving the key > NULL therefore crashes the process inside gnutls_handshake(), on a stack in > which MHD does not appear -- the callback was installed directly into the > credentials, so nothing of ours sits between it and the crash. > > Install a wrapper around the application callback instead, and reject an > answer that is not internally consistent: a non-empty certificate list with a > NULL list pointer or without a private key, and for the ...3 variant a > non-empty OCSP list with a NULL list pointer. An answer with no certificate > at all is passed through untouched, as that is the documented way for the > callback to say it has nothing for this connection. The out-parameters are > cleared before the application is called, so a callback that reports success > without setting them is caught as well rather than validated against > whatever GnuTLS left there. > > The cost of a rejected answer is one failed handshake for that connection. > The callback is at fault either way, but the failure is worth catching here: > it happens inside a library the application cannot see into, and only for > those connections that actually reach the callback, which can be a small and > hard to reproduce subset of the traffic. > > Found by fuzz_tls with the SNI_NO_KEY callback behaviour. > > Assisted-by: Claude:claude-opus-5 > --- > src/microhttpd/daemon.c | 212 +++++++++++++++++++++++++++++++++++++++- > 1 file changed, 210 insertions(+), 2 deletions(-) > > diff --git a/src/microhttpd/daemon.c b/src/microhttpd/daemon.c > index b42d4ee1..788eac8e 100644 > --- a/src/microhttpd/daemon.c > +++ b/src/microhttpd/daemon.c > @@ -484,6 +484,214 @@ MHD_ip_limit_del (struct MHD_Daemon *daemon, > > > #ifdef HTTPS_SUPPORT > +#if GNUTLS_VERSION_MAJOR >= 3 > +/** > + * Check the answer of an application certificate retrieve callback for > + * internal consistency. > + * > + * GnuTLS takes a "success" answer at face value: it dereferences the > + * private key without checking it, so a callback that returns zero while > + * leaving the key NULL crashes the process inside gnutls_handshake(). > + * Such a callback is at fault, but it is worth catching here anyway: the > + * crash happens inside a library the application cannot see into, and > + * only for those connections that actually reach the callback, which can > + * be a small and hard to reproduce subset of the traffic. Refusing the > + * answer instead costs one failed handshake. > + * > + * An answer with no certificate at all is left alone: that is the > + * documented way for the callback to say "I have nothing for this > + * connection", and GnuTLS handles it. > + * > + * @param daemon the daemon to log the problem to > + * @param pcert_length the number of certificates reported by the callback > + * @param pcert the certificate list reported by the callback > + * @param pkey the private key reported by the callback > + * @return true if the answer may be handed to GnuTLS, > + * false if it must be rejected > + */ > +static bool > +cert_retrieve_answer_is_sane (struct MHD_Daemon *daemon, > + unsigned int pcert_length, > + const gnutls_pcert_st *pcert, > + gnutls_privkey_t pkey) > +{ > + if (0 == pcert_length) > + return true; /* No certificate offered, nothing to be inconsistent */ > + if (NULL == pcert) > + { > +#ifdef HAVE_MESSAGES > + MHD_DLOG (daemon, > + _ ("The application certificate callback reported success " \ > + "with a non-empty certificate list, but the list pointer " \ > + "is NULL. Failing the TLS handshake.\n")); > +#else /* ! HAVE_MESSAGES */ > + (void) daemon; /* Mute compiler warning */ > +#endif /* ! HAVE_MESSAGES */ > + return false; > + } > + if (NULL == pkey) > + { > +#ifdef HAVE_MESSAGES > + MHD_DLOG (daemon, > + _ ("The application certificate callback reported success " \ > + "with a certificate, but without a private key. " \ > + "Failing the TLS handshake.\n")); > +#else /* ! HAVE_MESSAGES */ > + (void) daemon; /* Mute compiler warning */ > +#endif /* ! HAVE_MESSAGES */ > + return false; > + } > + return true; > +} > + > + > +/** > + * Wrapper around the application callback set by > + * #MHD_OPTION_HTTPS_CERT_CALLBACK, see cert_retrieve_answer_is_sane(). > + * > + * @param session the session to get the certificate for > + * @param req_ca_dn the distinguished names of the acceptable CAs > + * @param nreqs the number of entries in @a req_ca_dn > + * @param pk_algos the public key algorithms the client supports > + * @param pk_algos_length the number of entries in @a pk_algos > + * @param[out] pcert the certificate list to use > + * @param[out] pcert_length the number of entries in @a pcert > + * @param[out] pkey the private key matching @a pcert > + * @return 0 on success, negative value on error > + */ > +static int > +cert_retrieve_wrapper (gnutls_session_t session, > + const gnutls_datum_t *req_ca_dn, > + int nreqs, > + const gnutls_pk_algorithm_t *pk_algos, > + int pk_algos_length, > + gnutls_pcert_st **pcert, > + unsigned int *pcert_length, > + gnutls_privkey_t *pkey) > +{ > + struct MHD_Connection *connection; > + struct MHD_Daemon *daemon; > + int ret; > + > + connection = gnutls_session_get_ptr (session); > + if (NULL == connection) > + return -1; > + daemon = connection->daemon; > + mhd_assert (NULL != daemon->cert_callback); > + if (NULL == daemon->cert_callback) > + return -1; /* Cannot happen: the wrapper is installed only > + when the application set the callback */ > + > + /* GnuTLS promises nothing about the initial content of these, so clear > + them: a callback that reports success without setting them must be > + caught below rather than read as garbage. */ > + *pcert = NULL; > + *pcert_length = 0; > + *pkey = NULL; > + > + ret = daemon->cert_callback (session, > + req_ca_dn, > + nreqs, > + pk_algos, > + pk_algos_length, > + pcert, > + pcert_length, > + pkey); > + if (0 != ret) > + return ret; > + if (! cert_retrieve_answer_is_sane (daemon, > + *pcert_length, > + *pcert, > + *pkey)) > + return -1; > + return 0; > +} > + > + > +#endif /* GNUTLS_VERSION_MAJOR >= 3 */ > +#if GNUTLS_VERSION_NUMBER >= 0x030603 > +/** > + * Wrapper around the application callback set by > + * #MHD_OPTION_HTTPS_CERT_CALLBACK2, see cert_retrieve_answer_is_sane(). > + * > + * Anything the callback allocated is left alone when the answer is > + * rejected, even if it asked for #GNUTLS_CERT_RETR_DEINIT_ALL: the answer > + * is by definition not in the shape the deinitialisation expects, so > + * leaking it is the safer of the two outcomes. > + * > + * @param session the session to get the certificate for > + * @param info the information about the request from GnuTLS > + * @param[out] certs the certificate list to use > + * @param[out] certs_length the number of entries in @a certs > + * @param[out] ocsp the OCSP responses to staple > + * @param[out] ocsp_length the number of entries in @a ocsp > + * @param[out] pkey the private key matching @a certs > + * @param[out] flags the #gnutls_certificate_flags to apply > + * @return 0 on success, negative value on error > + */ > +static int > +cert_retrieve2_wrapper (gnutls_session_t session, > + const struct gnutls_cert_retr_st *info, > + gnutls_pcert_st **certs, > + unsigned int *certs_length, > + gnutls_ocsp_data_st **ocsp, > + unsigned int *ocsp_length, > + gnutls_privkey_t *pkey, > + unsigned int *flags) > +{ > + struct MHD_Connection *connection; > + struct MHD_Daemon *daemon; > + int ret; > + > + connection = gnutls_session_get_ptr (session); > + if (NULL == connection) > + return -1; > + daemon = connection->daemon; > + mhd_assert (NULL != daemon->cert_callback2); > + if (NULL == daemon->cert_callback2) > + return -1; /* Cannot happen: the wrapper is installed only > + when the application set the callback */ > + > + /* See the same assignments in cert_retrieve_wrapper() */ > + *certs = NULL; > + *certs_length = 0; > + *ocsp = NULL; > + *ocsp_length = 0; > + *pkey = NULL; > + *flags = 0; > + > + ret = daemon->cert_callback2 (session, > + info, > + certs, > + certs_length, > + ocsp, > + ocsp_length, > + pkey, > + flags); > + if (0 != ret) > + return ret; > + if (! cert_retrieve_answer_is_sane (daemon, > + *certs_length, > + *certs, > + *pkey)) > + return -1; > + if ((0 != *ocsp_length) && (NULL == *ocsp)) > + { > +#ifdef HAVE_MESSAGES > + MHD_DLOG (daemon, > + _ ("The application certificate callback reported success " \ > + "with a non-empty OCSP response list, but the list " \ > + "pointer is NULL. Failing the TLS handshake.\n")); > +#endif /* HAVE_MESSAGES */ > + return -1; > + } > + return 0; > +} > + > + > +#endif /* GNUTLS_VERSION_NUMBER >= 0x030603 */ > + > + > /** > * Read and setup our certificate and key. > * > @@ -501,14 +709,14 @@ MHD_init_daemon_certificate (struct MHD_Daemon *daemon) > if (NULL != daemon->cert_callback) > { > gnutls_certificate_set_retrieve_function2 (daemon->x509_cred, > - daemon->cert_callback); > + &cert_retrieve_wrapper); > } > #endif > #if GNUTLS_VERSION_NUMBER >= 0x030603 > else if (NULL != daemon->cert_callback2) > { > gnutls_certificate_set_retrieve_function3 (daemon->x509_cred, > - daemon->cert_callback2); > + &cert_retrieve2_wrapper); > } > #endif Should I create a Mantis ticket for ths issue? Any plans for libmicrohttpd2 release? I'd like to use libmicrohttpd with the OpenSSL backend. As far as I understand, the 1.x version won't get support for OpenSSL? Regards, Yegor
