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

Reply via email to