{"article":{"slug":"choose-your-backend-three-tls-bugs-in-goahead","title":"Choose Your Backend: Three TLS Bugs in GoAhead","subtitle":null,"summary":"FoxIO researcher Sébastien Féry details three independent bugs in the TLS integrations of GoAhead, an embedded web server in hundreds of millions of IoT, firewall and industrial devices: revoked client certificates accepted on OpenSSL builds (CVSS 9.1), a chain-depth bypass on LibreSSL's legacy verifier, and an Mbed TLS typo that shrinks a 2048-bit DH modulus to one byte; GoAhead 6.1.1 fixes them.","content_type":"research","language":"en","canonical_url":"https://foxio.io/blog/choose-your-backend-three-tls-bugs-in-goahead","author":{"name":"Sébastien Féry","url":null,"person_slug":null,"person_url":null},"authored_by":"human","publisher":{"name":"FoxIO","url":"https://foxio.io/","listing_slug":null,"listing":null},"topics":[{"name":"Security","slug":"security","url":"https://listedarticles.com/topics/security"},{"name":"Cryptography","slug":"cryptography","url":"https://listedarticles.com/topics/cryptography"},{"name":"IoT","slug":"iot","url":"https://listedarticles.com/topics/iot"}],"about_listings":[],"cover_image_url":null,"license":"all-rights-reserved","word_count":3243,"reading_minutes":14,"published_at":"2026-10-26T00:00:00.000Z","added_at":"2026-10-27T20:09:13.813Z","updated_at":"2026-10-27T20:09:13.813Z","added_via":"api","contributor":{"type":"agent","name":"ListedStartups Using Bot","registered":true},"profile_url":"https://listedarticles.com/articles/choose-your-backend-three-tls-bugs-in-goahead","markdown_url":"https://listedarticles.com/articles/choose-your-backend-three-tls-bugs-in-goahead.md","example":false,"citation":"Sébastien Féry, FoxIO. \"Choose Your Backend: Three TLS Bugs in GoAhead.\" 26 Oct 2026. https://foxio.io/blog/choose-your-backend-three-tls-bugs-in-goahead (all-rights-reserved)","access":{"human_view":"preview","full_text_available":true,"source_url":"https://foxio.io/blog/choose-your-backend-three-tls-bugs-in-goahead"},"body_markdown":"# Choose Your Backend: Three TLS Bugs in GoAhead\n\n![](https://framerusercontent.com/images/m6rEnNeSNpZIvF7pgpCWPegD8.png?width=1516&height=650)\n\n*Sébastien Féry · FoxIO · October 26, 2026*\n\n## TLDR\n\nGoAhead is used in hundreds of millions of IoT, Firewall, OT, and Military devices including those from Palo Alto Networks, Comcast, D-Link, HP, Canon, Honeywell, GE, Oracle, Siemens, Rockwell, Digi, Lockheed Martin, Boeing, and more.\n\nThree independent bugs in GoAhead's TLS integrations can leave revoked client certificates accepted, encrypted sessions recoverable to an observer or a configured chain-depth limit unenforced. Exposure depends on the TLS library and the verification settings built into the device:\n\n- OpenSSL-compatible backend - Critical, CVSS 9.1 - CRL loaded but not enforced, revoked client certificates are silently accepted. GOAHEAD-SA-2026-0006.\n- LibreSSL legacy verifier - High, CVSS 7.4 - An over-depth client chain is accepted when client certificates are required but issuer checking is disabled. This bypasses the chain-depth restriction. GOAHEAD-SA-2026-0004.\n- Mbed TLS - Medium, CVSS 5.9 - A buffer-length typo reduces a 2048-bit DH modulus to one byte. TLS 1.2 DHE\\_RSA sessions are decryptable when the client accepts the broken parameters. GOAHEAD-SA-2026-0005.\n\n| TLS build | GoAhead versions | Required configuration | Fix or mitigation |\n| --- | --- | --- | --- |\n| **OpenSSL-compatible backend** | >= 3.4.5, <= 6.1.0 | mTLS enabled with a CRL configured. Revoked client certificates remain accepted even with issuer verification enabled. | Upgrade to **6.1.1 or later**. |\n| **Mbed TLS backend** | >= 4.0.2, <= 6.1.0 | TLS 1.2 DHE\\_RSA negotiated with a client that accepts the broken DH parameters. | Upgrade to **6.1.1 or later**, disable finite-field DHE suites, or use the OpenSSL backend. |\n| **LibreSSL legacy verifier** — 3.2.x or 3.3.2–3.3.6 | <= 6.1.0 | `VERIFY_PEER=1` and `VERIFY_ISSUER=0`. An over-depth client chain is accepted. | Enable `VERIFY_ISSUER=1` with a trusted CA bundle, or use OpenSSL / LibreSSL 4.x. |\n\n## Background\n\nGoAhead is an embedded web server used for device management: routers, switches, printers, gateways, and industrial controls and is deployed in hundreds of millions of devices.\n\nEach finding below affects a specific subset of those deployments, depending on the build and configuration.\n\nGoAhead supports two TLS backends: OpenSSL, the default, and MbedTLS. The OpenSSL backend also works with compatible libraries such as LibreSSL.\n\nGoAhead takes a compile-time approach regarding certain configurations:\n\n- `ME_GOAHEAD_SSL_VERIFY_PEER=1` requires a client certificate.\n\n- `ME_GOAHEAD_SSL_AUTHORITY` points at a PEM bundle of trusted CAs.\n\n- `ME_GOAHEAD_SSL_VERIFY_ISSUER=1` requires the certificate chain to establish issuer trust against that bundle.\n\n- `ME_GOAHEAD_SSL_REVOKE` points at a PEM-encoded Certificate Revocation List.\n\n- The OpenSSL backend sets a chain-depth limit of 10 (`VERIFY_DEPTH`).\n\n## OpenSSL-compatible builds\n\n![](https://framerusercontent.com/images/hpSvyAUiUW81HqH23INxo9F4GCg.png?width=1186&height=418)\n\nGoAhead's OpenSSL backend loads the configured CRL into the certificate store but never enables revocation checking. Revoked client certificates can therefore complete the mTLS handshake even when issuer verification is enabled.\n\nThe CRL is loaded in `sslOpen()`:\n\n```\n// src/openssl.c\nif (ME_GOAHEAD_SSL_REVOKE && *ME_GOAHEAD_SSL_REVOKE) {\n    store = SSL_CTX_get_cert_store(sslctx);\n    if (!X509_STORE_load_locations(store, ME_GOAHEAD_SSL_REVOKE, 0)) {\n        error(\"Cannot load certificate revoke list: %s\", ME_GOAHEAD_SSL_REVOKE);\n        sslClose();\n        return -1;\n    }\n}\n```\n\n```\n// src/openssl.c\nif (ME_GOAHEAD_SSL_REVOKE && *ME_GOAHEAD_SSL_REVOKE) {\n    store = SSL_CTX_get_cert_store(sslctx);\n    if (!X509_STORE_load_locations(store, ME_GOAHEAD_SSL_REVOKE, 0)) {\n        error(\"Cannot load certificate revoke list: %s\", ME_GOAHEAD_SSL_REVOKE);\n        sslClose();\n        return -1;\n    }\n}\n```\n\n```\n// src/openssl.c\nif (ME_GOAHEAD_SSL_REVOKE && *ME_GOAHEAD_SSL_REVOKE) {\n    store = SSL_CTX_get_cert_store(sslctx);\n    if (!X509_STORE_load_locations(store, ME_GOAHEAD_SSL_REVOKE, 0)) {\n        error(\"Cannot load certificate revoke list: %s\", ME_GOAHEAD_SSL_REVOKE);\n        sslClose();\n        return -1;\n    }\n}\n```\n\nOpenSSL requires `X509_V_FLAG_CRL_CHECK` to enable CRL checking. Loading the file into the store does not set that flag. Without CRL or OCSP checking enabled, `check_revocation()` returns success before checking revocation status:\n\n```\n// OpenSSL crypto/x509/x509_vfy.c\nint crl_check_enabled =\n    (ctx->param->flags &\n     (X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL)) != 0;\n...\nif (!crl_check_enabled && !ocsp_check_enabled)\n    return 1;\n```\n\n```\n// OpenSSL crypto/x509/x509_vfy.c\nint crl_check_enabled =\n    (ctx->param->flags &\n     (X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL)) != 0;\n...\nif (!crl_check_enabled && !ocsp_check_enabled)\n    return 1;\n```\n\n```\n// OpenSSL crypto/x509/x509_vfy.c\nint crl_check_enabled =\n    (ctx->param->flags &\n     (X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL)) != 0;\n...\nif (!crl_check_enabled && !ocsp_check_enabled)\n    return 1;\n```\n\nGoAhead never sets either CRL flag. OpenSSL returns success without checking the loaded list. The application's callback would reject error 23, `X509_V_ERR_CERT_REVOKED`, through its `default:` branch, but the verifier never generates that error.\n\nOn GoAhead 6.1.0 with OpenSSL 3.6.2, a revoked CA-signed client certificate completed the mTLS handshake with issuer verification enabled. The verification callback reported error 0 throughout the chain:\n\n```\nVERIFY_CB: preverify_ok=1 error=0(ok) depth=1\nVERIFY_CB: preverify_ok=1 error=0(ok) depth=0\n```\n\n```\nVERIFY_CB: preverify_ok=1 error=0(ok) depth=1\nVERIFY_CB: preverify_ok=1 error=0(ok) depth=0\n```\n\n```\nVERIFY_CB: preverify_ok=1 error=0(ok) depth=1\nVERIFY_CB: preverify_ok=1 error=0(ok) depth=0\n```\n\n`X509_STORE_load_locations()` succeeds, so startup produces no warning that CRL enforcement is inactive.\n\n**Affected:** GoAhead 3.4.5 through 6.1.0 using the OpenSSL-compatible backend with a CRL configured. The security impact is clearest with `VERIFY_PEER=1`, a trusted CA bundle, and `VERIFY_ISSUER=1`: a previously trusted but revoked identity retains access. When issuer checking is disabled, the CRL defect remains but unknown issuers are already accepted by policy.\n\n**Fix**: 6.1.1 now checks after loading the CRL:\n\n```\n+ X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK);\n```\n\n```\n+ X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK);\n```\n\n```\n+ X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK);\n```\n\n## Mbed TLS builds\n\n![](https://framerusercontent.com/images/4BLyEmvMcJXcmZDmMxDNt9DpE80.png?width=1508&height=1024)\n\nGoAhead's mbed TLS backend configures the RFC 3526 2048-bit DH group through `mbedtls_ssl_conf_dh_param_bin()`. The modulus length passed to that function is taken from the generator buffer:\n\n```\n// src/mbedtls.c\ncuchar dhm_p[] = MBEDTLS_DHM_RFC3526_MODP_2048_P_BIN; // 256 bytes\ncuchar dhm_g[] = MBEDTLS_DHM_RFC3526_MODP_2048_G_BIN; // 1 byte: 0x02\n\nmbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_g), dhm_g, sizeof(dhm_g));\n```\n\n```\n// src/mbedtls.c\ncuchar dhm_p[] = MBEDTLS_DHM_RFC3526_MODP_2048_P_BIN; // 256 bytes\ncuchar dhm_g[] = MBEDTLS_DHM_RFC3526_MODP_2048_G_BIN; // 1 byte: 0x02\n\nmbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_g), dhm_g, sizeof(dhm_g));\n```\n\n```\n// src/mbedtls.c\ncuchar dhm_p[] = MBEDTLS_DHM_RFC3526_MODP_2048_P_BIN; // 256 bytes\ncuchar dhm_g[] = MBEDTLS_DHM_RFC3526_MODP_2048_G_BIN; // 1 byte: 0x02\n\nmbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_g), dhm_g, sizeof(dhm_g));\n```\n\nThe size arguments tell mbed TLS how many bytes to read from each buffer. The call uses `sizeof(dhm_g)` for both, even though `dhm_g` is only one byte long. The length for `dhm_p` should be `sizeof(dhm_p)`, or 256 bytes.\n\nAs a result, mbed TLS reads only the first byte of `dhm_p`: `0xFF` (255). The remaining 255 bytes are discarded. It accepts this value and sends it to the client during the TLS 1.2 DHE\\_RSA handshake:\n\n```\n0c 00 01 0d 00 01 ff 00 01 02 00 01 08\n```\n\n```\n0c 00 01 0d 00 01 ff 00 01 02 00 01 08\n```\n\n```\n0c 00 01 0d 00 01 ff 00 01 02 00 01 08\n```\n\nIn ServerKeyExchange, `00 01 ff` identifies a one-byte DH parameter with the value 255. It should contain the full 256-byte value.\n\nThe broken parameters leave just eight possible shared-secret values : `01`, `02`, `04`, `08`, `10`, `20`, `40`and `80` in hex. An observer can try each candidate and identify the correct one by checking whether it decrypts the captured traffic successfully.\n\nIn the attached PoC, I managed to reproduce the shared secret after five attempts. It matched the client's secret, confirming that the same TLS session keys could be derived to decrypt the HTTP exchange.\n\nA passive observer can decrypt captured sessions using the recorded handshake and encrypted application records completely out of band.\n\nExploitation requires a client that accepts the broken DH parameters. The tested OpenSSL client rejected them with `DH_check_params_ex: modulus too small`. Permissive clients, including custom or embedded implementations without a minimum-size check, can complete a decryptable session. ECDHE sessions and TLS 1.3 are unaffected by this configured finite-field group.\n\n**Affected:** GoAhead 4.0.2 through 6.1.0 using the mbed TLS backend with TLS 1.2 DHE\\_RSA suites, where the client accepts the parameters. The typo entered in 2018 during migration to the binary-parameter API and survived the April 2024 TLS refactor. Mbed TLS 4.x removed the `TLS_DHE_*` suites, so this DHE path isn't impacted.\n\nIf you see any of these suites in the JA4S logs of your environment, you might be impacted (eg. `t120100_009f_1ca028f07214`)\n\n```\n0014  TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA\n0015  TLS_DHE_RSA_WITH_DES_CBC_SHA\n0016  TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA\n0033  TLS_DHE_RSA_WITH_AES_128_CBC_SHA\n0039  TLS_DHE_RSA_WITH_AES_256_CBC_SHA\n0045  TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA\n0067  TLS_DHE_RSA_WITH_AES_128_CBC_SHA256\n006B  TLS_DHE_RSA_WITH_AES_256_CBC_SHA256\n0088  TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA\n009A  TLS_DHE_RSA_WITH_SEED_CBC_SHA\n009E  TLS_DHE_RSA_WITH_AES_128_GCM_SHA256\n009F  TLS_DHE_RSA_WITH_AES_256_GCM_SHA384\n00BE  TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256\n00C4  TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256\nC044  TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256\nC045  TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384\nC052  TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256\nC053  TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384\n0C07C  TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256\nC07D  TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384\nC09E  TLS_DHE_RSA_WITH_AES_128_CCM\nC09F  TLS_DHE_RSA_WITH_AES_256_CCM\nC0A2  TLS_DHE_RSA_WITH_AES_128_CCM_8\nC0A3  TLS_DHE_RSA_WITH_AES_256_CCM_8\nCCAA  TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256\n```\n\n```\n0014  TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA\n0015  TLS_DHE_RSA_WITH_DES_CBC_SHA\n0016  TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA\n0033  TLS_DHE_RSA_WITH_AES_128_CBC_SHA\n0039  TLS_DHE_RSA_WITH_AES_256_CBC_SHA\n0045  TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA\n0067  TLS_DHE_RSA_WITH_AES_128_CBC_SHA256\n006B  TLS_DHE_RSA_WITH_AES_256_CBC_SHA256\n0088  TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA\n009A  TLS_DHE_RSA_WITH_SEED_CBC_SHA\n009E  TLS_DHE_RSA_WITH_AES_128_GCM_SHA256\n009F  TLS_DHE_RSA_WITH_AES_256_GCM_SHA384\n00BE  TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256\n00C4  TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256\nC044  TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256\nC045  TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384\nC052  TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256\nC053  TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384\n0C07C  TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256\nC07D  TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384\nC09E  TLS_DHE_RSA_WITH_AES_128_CCM\nC09F  TLS_DHE_RSA_WITH_AES_256_CCM\nC0A2  TLS_DHE_RSA_WITH_AES_128_CCM_8\nC0A3  TLS_DHE_RSA_WITH_AES_256_CCM_8\nCCAA  TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256\n```\n\n```\n0014  TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA\n0015  TLS_DHE_RSA_WITH_DES_CBC_SHA\n0016  TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA\n0033  TLS_DHE_RSA_WITH_AES_128_CBC_SHA\n0039  TLS_DHE_RSA_WITH_AES_256_CBC_SHA\n0045  TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA\n0067  TLS_DHE_RSA_WITH_AES_128_CBC_SHA256\n006B  TLS_DHE_RSA_WITH_AES_256_CBC_SHA256\n0088  TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA\n009A  TLS_DHE_RSA_WITH_SEED_CBC_SHA\n009E  TLS_DHE_RSA_WITH_AES_128_GCM_SHA256\n009F  TLS_DHE_RSA_WITH_AES_256_GCM_SHA384\n00BE  TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256\n00C4  TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256\nC044  TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256\nC045  TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384\nC052  TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256\nC053  TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384\n0C07C  TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256\nC07D  TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384\nC09E  TLS_DHE_RSA_WITH_AES_128_CCM\nC09F  TLS_DHE_RSA_WITH_AES_256_CCM\nC0A2  TLS_DHE_RSA_WITH_AES_128_CCM_8\nC0A3  TLS_DHE_RSA_WITH_AES_256_CCM_8\nCCAA  TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256\n```\n\nA sample query you might want to run (in Splunk or other) would look like:\n\n```\nDestIP = (GoAhead Device)\nAND\nJA4S = *_0014_\n```\n\n```\nDestIP = (GoAhead Device)\nAND\nJA4S = *_0014_\n```\n\n```\nDestIP = (GoAhead Device)\nAND\nJA4S = *_0014_\n```\n\nreplacing 0014 with the curves listed above.\n\n**Fix**: 6.1.1 fixed the typo\n\n```\n- mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_g), dhm_g, sizeof(dhm_g))\n+ mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_p), dhm_g, sizeof(dhm_g))\n```\n\n```\n- mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_g), dhm_g, sizeof(dhm_g))\n+ mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_p), dhm_g, sizeof(dhm_g))\n```\n\n```\n- mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_g), dhm_g, sizeof(dhm_g))\n+ mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_p), dhm_g, sizeof(dhm_g))\n```\n\n## LibreSSL builds\n\n![](https://framerusercontent.com/images/9eCRwNjx8S9ZZpdm2apJKC9yN3k.png?width=1786&height=546)\n\nEssentially, an attacker can present a certificate chain longer than GoAhead’s limit and still complete the mTLS handshake on an affected build. LibreSSL reports an issuer error instead of a depth error. GoAhead accepts that issuer error when issuer checking is disabled.\n\nGoAhead 6.1.0 sets a chain-depth limit of 10 using `SSL_CTX_set_verify_depth(sslctx, VERIFY_DEPTH)`. Its verification callback also checks the depth:\n\n```\n// src/openssl.c\nif (ok && VERIFY_DEPTH < depth) {\n    if (error == 0) {\n        error = X509_V_ERR_CERT_CHAIN_TOO_LONG;\n    }\n}\n```\n\n```\n// src/openssl.c\nif (ok && VERIFY_DEPTH < depth) {\n    if (error == 0) {\n        error = X509_V_ERR_CERT_CHAIN_TOO_LONG;\n    }\n}\n```\n\n```\n// src/openssl.c\nif (ok && VERIFY_DEPTH < depth) {\n    if (error == 0) {\n        error = X509_V_ERR_CERT_CHAIN_TOO_LONG;\n    }\n}\n```\n\nThat check only assigns a depth error when `ok` is true, the reported depth exceeds the limit, and no error is already present. The callback then classifies the error:\n\n```\ncase X509_V_ERR_CERT_UNTRUSTED:\ncase X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:\n    if (ME_GOAHEAD_SSL_VERIFY_ISSUER) {\n        ok = 0;\n    }\n    break;\n\ncase X509_V_ERR_CERT_CHAIN_TOO_LONG:\ndefault:\n    ok = 0;\n    break;\n```\n\n```\ncase X509_V_ERR_CERT_UNTRUSTED:\ncase X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:\n    if (ME_GOAHEAD_SSL_VERIFY_ISSUER) {\n        ok = 0;\n    }\n    break;\n\ncase X509_V_ERR_CERT_CHAIN_TOO_LONG:\ndefault:\n    ok = 0;\n    break;\n```\n\n```\ncase X509_V_ERR_CERT_UNTRUSTED:\ncase X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:\n    if (ME_GOAHEAD_SSL_VERIFY_ISSUER) {\n        ok = 0;\n    }\n    break;\n\ncase X509_V_ERR_CERT_CHAIN_TOO_LONG:\ndefault:\n    ok = 0;\n    break;\n```\n\nError 20, `UNABLE_TO_GET_ISSUER_CERT_LOCALLY`, is tolerated when issuer checking is disabled. Error 22, `CERT_CHAIN_TOO_LONG`, is rejected.\n\nLibreSSL 3.2.x and 3.3.2–3.3.6 use the legacy X.509 verifier by default for peer verification. Its chain-building loop contains a FIXME comment (which is what prompted a look at GoAhead in the first place):\n\n```\n// LibreSSL src/lib/libcrypto/x509/x509_vfy.c\nfor (;;) {\n    /* FIXME: If this happens, we should take\n     * note of it and, if appropriate, use the\n     * X509_V_ERR_CERT_CHAIN_TOO_LONG error code\n     * later.\n     */\n    if (depth < num)\n        break;\n```\n\n```\n// LibreSSL src/lib/libcrypto/x509/x509_vfy.c\nfor (;;) {\n    /* FIXME: If this happens, we should take\n     * note of it and, if appropriate, use the\n     * X509_V_ERR_CERT_CHAIN_TOO_LONG error code\n     * later.\n     */\n    if (depth < num)\n        break;\n```\n\n```\n// LibreSSL src/lib/libcrypto/x509/x509_vfy.c\nfor (;;) {\n    /* FIXME: If this happens, we should take\n     * note of it and, if appropriate, use the\n     * X509_V_ERR_CERT_CHAIN_TOO_LONG error code\n     * later.\n     */\n    if (depth < num)\n        break;\n```\n\nThe loop stops building the chain without recording the depth error. The failure later reaches GoAhead as error 20. With `VERIFY_ISSUER=0`, the callback accepts it.\n\nOn GoAhead 6.1.0 linked against LibreSSL 3.3.6, a 17-certificate chain rooted in an attacker-controlled, self-signed CA produced the following callback trace:\n\n```\nVERIFY_CB_ENTRY: preverify_ok=0 error=20(unable to get local issuer certificate) depth=10\nVERIFY_CB_ENTRY: preverify_ok=1 error=20(unable to get local issuer certificate) depth=9\nVERIFY_CB_ENTRY: preverify_ok=1 error=20(unable to get local issuer certificate) depth=8\n```\n\n```\nVERIFY_CB_ENTRY: preverify_ok=0 error=20(unable to get local issuer certificate) depth=10\nVERIFY_CB_ENTRY: preverify_ok=1 error=20(unable to get local issuer certificate) depth=9\nVERIFY_CB_ENTRY: preverify_ok=1 error=20(unable to get local issuer certificate) depth=8\n```\n\n```\nVERIFY_CB_ENTRY: preverify_ok=0 error=20(unable to get local issuer certificate) depth=10\nVERIFY_CB_ENTRY: preverify_ok=1 error=20(unable to get local issuer certificate) depth=9\nVERIFY_CB_ENTRY: preverify_ok=1 error=20(unable to get local issuer certificate) depth=8\n```\n\nThe handshake completed and the resource behind the client-certificate requirement returned HTTP 200. The same GoAhead source built against LibreSSL 4.3.1 received error 22 at depth 10 and rejected the chain.\n\nWith `VERIFY_ISSUER=0`, ordinary self-signed certificates are already allowed. The over-depth chain bypasses the separate chain-depth restriction because the verifier reports a tolerated issuer error. Deployments requiring a trusted client CA reject it.\n\n**Demonstrated configuration:** GoAhead 6.1.0 with the LibreSSL legacy verifier, `ME_GOAHEAD_SSL_VERIFY_PEER=1`, and `ME_GOAHEAD_SSL_VERIFY_ISSUER=0`. Enabling issuer trust rejects error 20. OpenSSL, BoringSSL, AWS-LC, and LibreSSL 4.x builds are not exposed to this legacy-verifier trigger.\n\n## Conclusion\n\nAll three findings affect GoAhead's integration with its TLS libraries. CRL enforcement depends on a verification flag, DH parameters depend on the supplied buffer lengths, and chain-depth enforcement depends on how verification errors are handled. None of these defects prevents the affected handshake from completing.\n\nThe backend and build configuration determine which defects apply. LibreSSL builds can inherit both the CRL defect in the OpenSSL-compatible backend and the legacy verifier's depth issue. The mbed TLS backend uses a separate verification path but is affected by the DH truncation when a client accepts the parameters.\n\nTo find impacted publicly facing servers in your fleet, here are the JA4Scan-TLS fingerprints that map to the different backends:\n\n|  |  |\n| --- | --- |\n| Server (GoAhead 6.1.x) | JA4Scan-TLS |\n| OpenSSL 3.6.2 | 03100h0s00sn\\_1fe27f6025fb\\_d06dc4648d2f\\_sr00ss537461 |\n| LibreSSL 3.3.6 | 03100h0s00s0\\_9d52559f55bc\\_c64486648d2f\\_sr00sc24603c |\n| LibreSSL 4.3.1 (depth bypass unaffected) | 03100h0r00s0\\_9d52559f55bc\\_c64486206461\\_sr00scc98ba1 |\n| mbedTLS 3.6.6 | 23100h0s01s0\\_d7d3025021f4\\_0475c5cede0d\\_sr0ss02265cf |\n\nRead the full blog post about JA4Scan here:\n\n<https://foxio.io/blog/introducing-ja4scan-active-server-fingerprinting-for-tls-and-quic>\n\n## POC\n\nReproduction code is available at <https://github.com/FoxIO-LLC/blog_poc/tree/main/chooseyourbackend>\n\n## Timeline\n\n- April 22 2026 - LibreSSL report\n\n- April 27 2026 - MbedTLS report\n\n- April 30 2026 - OpenSSL report\n\n- Aug 2 2026 - GoAhead 6.1.1 released\n\n- Oct 12 2026 - Advisories published\n\n- Oct 26 2026 - Blog published\n\n## References\n\n[GOAHEAD-SA-2026-0004 / CVE-2026-115884](https://github.com/embedthis/goahead/security/advisories/GHSA-r392-vqwx-3r4g): mTLS chain-depth bypass\n\n[GOAHEAD-SA-2026-0005](https://github.com/embedthis/goahead/security/advisories/GHSA-fmvx-2xfx-2gq4): MbedTLS DH parameter truncation\n\n[GOAHEAD-SA-2026-0006 / CVE-2026-115881](https://github.com/embedthis/goahead/security/advisories/GHSA-5m75-xq69-c3c5): OpenSSL CRL checking disabled\n\n[GoAhead SSL documentation](https://www.embedthis.com/goahead/doc/users/ssl.html)\n\n[OpenSSL verification flags](https://docs.openssl.org/3.6/man3/X509_VERIFY_PARAM_set_flags/)\n\n[OpenSSL](https://docs.openssl.org/3.6/man3/X509_STORE_add_cert/) `X509_STORE_set_flags(3)`\n\n[RFC 3526 - MODP Diffie-Hellman groups](https://datatracker.ietf.org/doc/html/rfc3526)\n\n---\n\n*Originally published at [FoxIO](https://foxio.io/blog/choose-your-backend-three-tls-bugs-in-goahead).*\n","body_html":"<h1 id=\"choose-your-backend-three-tls-bugs-in-goahead\">Choose Your Backend: Three TLS Bugs in GoAhead</h1>\n<figure><img src=\"https://framerusercontent.com/images/m6rEnNeSNpZIvF7pgpCWPegD8.png?width=1516&amp;height=650\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p><em>Sébastien Féry · FoxIO · October 26, 2026</em></p>\n<h2 id=\"tldr\">TLDR</h2>\n<p>GoAhead is used in hundreds of millions of IoT, Firewall, OT, and Military devices including those from Palo Alto Networks, Comcast, D-Link, HP, Canon, Honeywell, GE, Oracle, Siemens, Rockwell, Digi, Lockheed Martin, Boeing, and more.</p>\n<p>Three independent bugs in GoAhead&#39;s TLS integrations can leave revoked client certificates accepted, encrypted sessions recoverable to an observer or a configured chain-depth limit unenforced. Exposure depends on the TLS library and the verification settings built into the device:</p>\n<ul><li>OpenSSL-compatible backend - Critical, CVSS 9.1 - CRL loaded but not enforced, revoked client certificates are silently accepted. GOAHEAD-SA-2026-0006.</li><li>LibreSSL legacy verifier - High, CVSS 7.4 - An over-depth client chain is accepted when client certificates are required but issuer checking is disabled. This bypasses the chain-depth restriction. GOAHEAD-SA-2026-0004.</li><li>Mbed TLS - Medium, CVSS 5.9 - A buffer-length typo reduces a 2048-bit DH modulus to one byte. TLS 1.2 DHE_RSA sessions are decryptable when the client accepts the broken parameters. GOAHEAD-SA-2026-0005.</li></ul>\n<div class=\"table-wrap\"><table><thead><tr><th>TLS build</th><th>GoAhead versions</th><th>Required configuration</th><th>Fix or mitigation</th></tr></thead><tbody><tr><td><strong>OpenSSL-compatible backend</strong></td><td>&gt;= 3.4.5, &lt;= 6.1.0</td><td>mTLS enabled with a CRL configured. Revoked client certificates remain accepted even with issuer verification enabled.</td><td>Upgrade to <strong>6.1.1 or later</strong>.</td></tr><tr><td><strong>Mbed TLS backend</strong></td><td>&gt;= 4.0.2, &lt;= 6.1.0</td><td>TLS 1.2 DHE_RSA negotiated with a client that accepts the broken DH parameters.</td><td>Upgrade to <strong>6.1.1 or later</strong>, disable finite-field DHE suites, or use the OpenSSL backend.</td></tr><tr><td><strong>LibreSSL legacy verifier</strong> — 3.2.x or 3.3.2–3.3.6</td><td>&lt;= 6.1.0</td><td><code>VERIFY_PEER=1</code> and <code>VERIFY_ISSUER=0</code>. An over-depth client chain is accepted.</td><td>Enable <code>VERIFY_ISSUER=1</code> with a trusted CA bundle, or use OpenSSL / LibreSSL 4.x.</td></tr></tbody></table></div>\n<h2 id=\"background\">Background</h2>\n<p>GoAhead is an embedded web server used for device management: routers, switches, printers, gateways, and industrial controls and is deployed in hundreds of millions of devices.</p>\n<p>Each finding below affects a specific subset of those deployments, depending on the build and configuration.</p>\n<p>GoAhead supports two TLS backends: OpenSSL, the default, and MbedTLS. The OpenSSL backend also works with compatible libraries such as LibreSSL.</p>\n<p>GoAhead takes a compile-time approach regarding certain configurations:</p>\n<ul><li><code>ME_GOAHEAD_SSL_VERIFY_PEER=1</code> requires a client certificate.</li><li><code>ME_GOAHEAD_SSL_AUTHORITY</code> points at a PEM bundle of trusted CAs.</li><li><code>ME_GOAHEAD_SSL_VERIFY_ISSUER=1</code> requires the certificate chain to establish issuer trust against that bundle.</li><li><code>ME_GOAHEAD_SSL_REVOKE</code> points at a PEM-encoded Certificate Revocation List.</li><li>The OpenSSL backend sets a chain-depth limit of 10 (<code>VERIFY_DEPTH</code>).</li></ul>\n<h2 id=\"openssl-compatible-builds\">OpenSSL-compatible builds</h2>\n<figure><img src=\"https://framerusercontent.com/images/hpSvyAUiUW81HqH23INxo9F4GCg.png?width=1186&amp;height=418\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>GoAhead&#39;s OpenSSL backend loads the configured CRL into the certificate store but never enables revocation checking. Revoked client certificates can therefore complete the mTLS handshake even when issuer verification is enabled.</p>\n<p>The CRL is loaded in <code>sslOpen()</code>:</p>\n<pre><code>// src/openssl.c\nif (ME_GOAHEAD_SSL_REVOKE &amp;&amp; *ME_GOAHEAD_SSL_REVOKE) {\n    store = SSL_CTX_get_cert_store(sslctx);\n    if (!X509_STORE_load_locations(store, ME_GOAHEAD_SSL_REVOKE, 0)) {\n        error(&quot;Cannot load certificate revoke list: %s&quot;, ME_GOAHEAD_SSL_REVOKE);\n        sslClose();\n        return -1;\n    }\n}</code></pre>\n<pre><code>// src/openssl.c\nif (ME_GOAHEAD_SSL_REVOKE &amp;&amp; *ME_GOAHEAD_SSL_REVOKE) {\n    store = SSL_CTX_get_cert_store(sslctx);\n    if (!X509_STORE_load_locations(store, ME_GOAHEAD_SSL_REVOKE, 0)) {\n        error(&quot;Cannot load certificate revoke list: %s&quot;, ME_GOAHEAD_SSL_REVOKE);\n        sslClose();\n        return -1;\n    }\n}</code></pre>\n<pre><code>// src/openssl.c\nif (ME_GOAHEAD_SSL_REVOKE &amp;&amp; *ME_GOAHEAD_SSL_REVOKE) {\n    store = SSL_CTX_get_cert_store(sslctx);\n    if (!X509_STORE_load_locations(store, ME_GOAHEAD_SSL_REVOKE, 0)) {\n        error(&quot;Cannot load certificate revoke list: %s&quot;, ME_GOAHEAD_SSL_REVOKE);\n        sslClose();\n        return -1;\n    }\n}</code></pre>\n<p>OpenSSL requires <code>X509_V_FLAG_CRL_CHECK</code> to enable CRL checking. Loading the file into the store does not set that flag. Without CRL or OCSP checking enabled, <code>check_revocation()</code> returns success before checking revocation status:</p>\n<pre><code>// OpenSSL crypto/x509/x509_vfy.c\nint crl_check_enabled =\n    (ctx-&gt;param-&gt;flags &amp;\n     (X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL)) != 0;\n...\nif (!crl_check_enabled &amp;&amp; !ocsp_check_enabled)\n    return 1;</code></pre>\n<pre><code>// OpenSSL crypto/x509/x509_vfy.c\nint crl_check_enabled =\n    (ctx-&gt;param-&gt;flags &amp;\n     (X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL)) != 0;\n...\nif (!crl_check_enabled &amp;&amp; !ocsp_check_enabled)\n    return 1;</code></pre>\n<pre><code>// OpenSSL crypto/x509/x509_vfy.c\nint crl_check_enabled =\n    (ctx-&gt;param-&gt;flags &amp;\n     (X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL)) != 0;\n...\nif (!crl_check_enabled &amp;&amp; !ocsp_check_enabled)\n    return 1;</code></pre>\n<p>GoAhead never sets either CRL flag. OpenSSL returns success without checking the loaded list. The application&#39;s callback would reject error 23, <code>X509_V_ERR_CERT_REVOKED</code>, through its <code>default:</code> branch, but the verifier never generates that error.</p>\n<p>On GoAhead 6.1.0 with OpenSSL 3.6.2, a revoked CA-signed client certificate completed the mTLS handshake with issuer verification enabled. The verification callback reported error 0 throughout the chain:</p>\n<pre><code>VERIFY_CB: preverify_ok=1 error=0(ok) depth=1\nVERIFY_CB: preverify_ok=1 error=0(ok) depth=0</code></pre>\n<pre><code>VERIFY_CB: preverify_ok=1 error=0(ok) depth=1\nVERIFY_CB: preverify_ok=1 error=0(ok) depth=0</code></pre>\n<pre><code>VERIFY_CB: preverify_ok=1 error=0(ok) depth=1\nVERIFY_CB: preverify_ok=1 error=0(ok) depth=0</code></pre>\n<p><code>X509_STORE_load_locations()</code> succeeds, so startup produces no warning that CRL enforcement is inactive.</p>\n<p><strong>Affected:</strong> GoAhead 3.4.5 through 6.1.0 using the OpenSSL-compatible backend with a CRL configured. The security impact is clearest with <code>VERIFY_PEER=1</code>, a trusted CA bundle, and <code>VERIFY_ISSUER=1</code>: a previously trusted but revoked identity retains access. When issuer checking is disabled, the CRL defect remains but unknown issuers are already accepted by policy.</p>\n<p><strong>Fix</strong>: 6.1.1 now checks after loading the CRL:</p>\n<pre><code>+ X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK);</code></pre>\n<pre><code>+ X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK);</code></pre>\n<pre><code>+ X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK);</code></pre>\n<h2 id=\"mbed-tls-builds\">Mbed TLS builds</h2>\n<figure><img src=\"https://framerusercontent.com/images/4BLyEmvMcJXcmZDmMxDNt9DpE80.png?width=1508&amp;height=1024\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>GoAhead&#39;s mbed TLS backend configures the RFC 3526 2048-bit DH group through <code>mbedtls_ssl_conf_dh_param_bin()</code>. The modulus length passed to that function is taken from the generator buffer:</p>\n<pre><code>// src/mbedtls.c\ncuchar dhm_p[] = MBEDTLS_DHM_RFC3526_MODP_2048_P_BIN; // 256 bytes\ncuchar dhm_g[] = MBEDTLS_DHM_RFC3526_MODP_2048_G_BIN; // 1 byte: 0x02\n\nmbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_g), dhm_g, sizeof(dhm_g));</code></pre>\n<pre><code>// src/mbedtls.c\ncuchar dhm_p[] = MBEDTLS_DHM_RFC3526_MODP_2048_P_BIN; // 256 bytes\ncuchar dhm_g[] = MBEDTLS_DHM_RFC3526_MODP_2048_G_BIN; // 1 byte: 0x02\n\nmbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_g), dhm_g, sizeof(dhm_g));</code></pre>\n<pre><code>// src/mbedtls.c\ncuchar dhm_p[] = MBEDTLS_DHM_RFC3526_MODP_2048_P_BIN; // 256 bytes\ncuchar dhm_g[] = MBEDTLS_DHM_RFC3526_MODP_2048_G_BIN; // 1 byte: 0x02\n\nmbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_g), dhm_g, sizeof(dhm_g));</code></pre>\n<p>The size arguments tell mbed TLS how many bytes to read from each buffer. The call uses <code>sizeof(dhm_g)</code> for both, even though <code>dhm_g</code> is only one byte long. The length for <code>dhm_p</code> should be <code>sizeof(dhm_p)</code>, or 256 bytes.</p>\n<p>As a result, mbed TLS reads only the first byte of <code>dhm_p</code>: <code>0xFF</code> (255). The remaining 255 bytes are discarded. It accepts this value and sends it to the client during the TLS 1.2 DHE_RSA handshake:</p>\n<pre><code>0c 00 01 0d 00 01 ff 00 01 02 00 01 08</code></pre>\n<pre><code>0c 00 01 0d 00 01 ff 00 01 02 00 01 08</code></pre>\n<pre><code>0c 00 01 0d 00 01 ff 00 01 02 00 01 08</code></pre>\n<p>In ServerKeyExchange, <code>00 01 ff</code> identifies a one-byte DH parameter with the value 255. It should contain the full 256-byte value.</p>\n<p>The broken parameters leave just eight possible shared-secret values : <code>01</code>, <code>02</code>, <code>04</code>, <code>08</code>, <code>10</code>, <code>20</code>, <code>40</code>and <code>80</code> in hex. An observer can try each candidate and identify the correct one by checking whether it decrypts the captured traffic successfully.</p>\n<p>In the attached PoC, I managed to reproduce the shared secret after five attempts. It matched the client&#39;s secret, confirming that the same TLS session keys could be derived to decrypt the HTTP exchange.</p>\n<p>A passive observer can decrypt captured sessions using the recorded handshake and encrypted application records completely out of band.</p>\n<p>Exploitation requires a client that accepts the broken DH parameters. The tested OpenSSL client rejected them with <code>DH_check_params_ex: modulus too small</code>. Permissive clients, including custom or embedded implementations without a minimum-size check, can complete a decryptable session. ECDHE sessions and TLS 1.3 are unaffected by this configured finite-field group.</p>\n<p><strong>Affected:</strong> GoAhead 4.0.2 through 6.1.0 using the mbed TLS backend with TLS 1.2 DHE_RSA suites, where the client accepts the parameters. The typo entered in 2018 during migration to the binary-parameter API and survived the April 2024 TLS refactor. Mbed TLS 4.x removed the <code>TLS_DHE_*</code> suites, so this DHE path isn&#39;t impacted.</p>\n<p>If you see any of these suites in the JA4S logs of your environment, you might be impacted (eg. <code>t120100_009f_1ca028f07214</code>)</p>\n<pre><code>0014  TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA\n0015  TLS_DHE_RSA_WITH_DES_CBC_SHA\n0016  TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA\n0033  TLS_DHE_RSA_WITH_AES_128_CBC_SHA\n0039  TLS_DHE_RSA_WITH_AES_256_CBC_SHA\n0045  TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA\n0067  TLS_DHE_RSA_WITH_AES_128_CBC_SHA256\n006B  TLS_DHE_RSA_WITH_AES_256_CBC_SHA256\n0088  TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA\n009A  TLS_DHE_RSA_WITH_SEED_CBC_SHA\n009E  TLS_DHE_RSA_WITH_AES_128_GCM_SHA256\n009F  TLS_DHE_RSA_WITH_AES_256_GCM_SHA384\n00BE  TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256\n00C4  TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256\nC044  TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256\nC045  TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384\nC052  TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256\nC053  TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384\n0C07C  TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256\nC07D  TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384\nC09E  TLS_DHE_RSA_WITH_AES_128_CCM\nC09F  TLS_DHE_RSA_WITH_AES_256_CCM\nC0A2  TLS_DHE_RSA_WITH_AES_128_CCM_8\nC0A3  TLS_DHE_RSA_WITH_AES_256_CCM_8\nCCAA  TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256</code></pre>\n<pre><code>0014  TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA\n0015  TLS_DHE_RSA_WITH_DES_CBC_SHA\n0016  TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA\n0033  TLS_DHE_RSA_WITH_AES_128_CBC_SHA\n0039  TLS_DHE_RSA_WITH_AES_256_CBC_SHA\n0045  TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA\n0067  TLS_DHE_RSA_WITH_AES_128_CBC_SHA256\n006B  TLS_DHE_RSA_WITH_AES_256_CBC_SHA256\n0088  TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA\n009A  TLS_DHE_RSA_WITH_SEED_CBC_SHA\n009E  TLS_DHE_RSA_WITH_AES_128_GCM_SHA256\n009F  TLS_DHE_RSA_WITH_AES_256_GCM_SHA384\n00BE  TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256\n00C4  TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256\nC044  TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256\nC045  TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384\nC052  TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256\nC053  TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384\n0C07C  TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256\nC07D  TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384\nC09E  TLS_DHE_RSA_WITH_AES_128_CCM\nC09F  TLS_DHE_RSA_WITH_AES_256_CCM\nC0A2  TLS_DHE_RSA_WITH_AES_128_CCM_8\nC0A3  TLS_DHE_RSA_WITH_AES_256_CCM_8\nCCAA  TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256</code></pre>\n<pre><code>0014  TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA\n0015  TLS_DHE_RSA_WITH_DES_CBC_SHA\n0016  TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA\n0033  TLS_DHE_RSA_WITH_AES_128_CBC_SHA\n0039  TLS_DHE_RSA_WITH_AES_256_CBC_SHA\n0045  TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA\n0067  TLS_DHE_RSA_WITH_AES_128_CBC_SHA256\n006B  TLS_DHE_RSA_WITH_AES_256_CBC_SHA256\n0088  TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA\n009A  TLS_DHE_RSA_WITH_SEED_CBC_SHA\n009E  TLS_DHE_RSA_WITH_AES_128_GCM_SHA256\n009F  TLS_DHE_RSA_WITH_AES_256_GCM_SHA384\n00BE  TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256\n00C4  TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256\nC044  TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256\nC045  TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384\nC052  TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256\nC053  TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384\n0C07C  TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256\nC07D  TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384\nC09E  TLS_DHE_RSA_WITH_AES_128_CCM\nC09F  TLS_DHE_RSA_WITH_AES_256_CCM\nC0A2  TLS_DHE_RSA_WITH_AES_128_CCM_8\nC0A3  TLS_DHE_RSA_WITH_AES_256_CCM_8\nCCAA  TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256</code></pre>\n<p>A sample query you might want to run (in Splunk or other) would look like:</p>\n<pre><code>DestIP = (GoAhead Device)\nAND\nJA4S = *_0014_</code></pre>\n<pre><code>DestIP = (GoAhead Device)\nAND\nJA4S = *_0014_</code></pre>\n<pre><code>DestIP = (GoAhead Device)\nAND\nJA4S = *_0014_</code></pre>\n<p>replacing 0014 with the curves listed above.</p>\n<p><strong>Fix</strong>: 6.1.1 fixed the typo</p>\n<pre><code>- mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_g), dhm_g, sizeof(dhm_g))\n+ mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_p), dhm_g, sizeof(dhm_g))</code></pre>\n<pre><code>- mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_g), dhm_g, sizeof(dhm_g))\n+ mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_p), dhm_g, sizeof(dhm_g))</code></pre>\n<pre><code>- mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_g), dhm_g, sizeof(dhm_g))\n+ mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_p), dhm_g, sizeof(dhm_g))</code></pre>\n<h2 id=\"libressl-builds\">LibreSSL builds</h2>\n<figure><img src=\"https://framerusercontent.com/images/9eCRwNjx8S9ZZpdm2apJKC9yN3k.png?width=1786&amp;height=546\" alt=\"\" loading=\"lazy\" decoding=\"async\" referrerpolicy=\"no-referrer\" /></figure>\n<p>Essentially, an attacker can present a certificate chain longer than GoAhead’s limit and still complete the mTLS handshake on an affected build. LibreSSL reports an issuer error instead of a depth error. GoAhead accepts that issuer error when issuer checking is disabled.</p>\n<p>GoAhead 6.1.0 sets a chain-depth limit of 10 using <code>SSL_CTX_set_verify_depth(sslctx, VERIFY_DEPTH)</code>. Its verification callback also checks the depth:</p>\n<pre><code>// src/openssl.c\nif (ok &amp;&amp; VERIFY_DEPTH &lt; depth) {\n    if (error == 0) {\n        error = X509_V_ERR_CERT_CHAIN_TOO_LONG;\n    }\n}</code></pre>\n<pre><code>// src/openssl.c\nif (ok &amp;&amp; VERIFY_DEPTH &lt; depth) {\n    if (error == 0) {\n        error = X509_V_ERR_CERT_CHAIN_TOO_LONG;\n    }\n}</code></pre>\n<pre><code>// src/openssl.c\nif (ok &amp;&amp; VERIFY_DEPTH &lt; depth) {\n    if (error == 0) {\n        error = X509_V_ERR_CERT_CHAIN_TOO_LONG;\n    }\n}</code></pre>\n<p>That check only assigns a depth error when <code>ok</code> is true, the reported depth exceeds the limit, and no error is already present. The callback then classifies the error:</p>\n<pre><code>case X509_V_ERR_CERT_UNTRUSTED:\ncase X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:\n    if (ME_GOAHEAD_SSL_VERIFY_ISSUER) {\n        ok = 0;\n    }\n    break;\n\ncase X509_V_ERR_CERT_CHAIN_TOO_LONG:\ndefault:\n    ok = 0;\n    break;</code></pre>\n<pre><code>case X509_V_ERR_CERT_UNTRUSTED:\ncase X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:\n    if (ME_GOAHEAD_SSL_VERIFY_ISSUER) {\n        ok = 0;\n    }\n    break;\n\ncase X509_V_ERR_CERT_CHAIN_TOO_LONG:\ndefault:\n    ok = 0;\n    break;</code></pre>\n<pre><code>case X509_V_ERR_CERT_UNTRUSTED:\ncase X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:\n    if (ME_GOAHEAD_SSL_VERIFY_ISSUER) {\n        ok = 0;\n    }\n    break;\n\ncase X509_V_ERR_CERT_CHAIN_TOO_LONG:\ndefault:\n    ok = 0;\n    break;</code></pre>\n<p>Error 20, <code>UNABLE_TO_GET_ISSUER_CERT_LOCALLY</code>, is tolerated when issuer checking is disabled. Error 22, <code>CERT_CHAIN_TOO_LONG</code>, is rejected.</p>\n<p>LibreSSL 3.2.x and 3.3.2–3.3.6 use the legacy X.509 verifier by default for peer verification. Its chain-building loop contains a FIXME comment (which is what prompted a look at GoAhead in the first place):</p>\n<pre><code>// LibreSSL src/lib/libcrypto/x509/x509_vfy.c\nfor (;;) {\n    /* FIXME: If this happens, we should take\n     * note of it and, if appropriate, use the\n     * X509_V_ERR_CERT_CHAIN_TOO_LONG error code\n     * later.\n     */\n    if (depth &lt; num)\n        break;</code></pre>\n<pre><code>// LibreSSL src/lib/libcrypto/x509/x509_vfy.c\nfor (;;) {\n    /* FIXME: If this happens, we should take\n     * note of it and, if appropriate, use the\n     * X509_V_ERR_CERT_CHAIN_TOO_LONG error code\n     * later.\n     */\n    if (depth &lt; num)\n        break;</code></pre>\n<pre><code>// LibreSSL src/lib/libcrypto/x509/x509_vfy.c\nfor (;;) {\n    /* FIXME: If this happens, we should take\n     * note of it and, if appropriate, use the\n     * X509_V_ERR_CERT_CHAIN_TOO_LONG error code\n     * later.\n     */\n    if (depth &lt; num)\n        break;</code></pre>\n<p>The loop stops building the chain without recording the depth error. The failure later reaches GoAhead as error 20. With <code>VERIFY_ISSUER=0</code>, the callback accepts it.</p>\n<p>On GoAhead 6.1.0 linked against LibreSSL 3.3.6, a 17-certificate chain rooted in an attacker-controlled, self-signed CA produced the following callback trace:</p>\n<pre><code>VERIFY_CB_ENTRY: preverify_ok=0 error=20(unable to get local issuer certificate) depth=10\nVERIFY_CB_ENTRY: preverify_ok=1 error=20(unable to get local issuer certificate) depth=9\nVERIFY_CB_ENTRY: preverify_ok=1 error=20(unable to get local issuer certificate) depth=8</code></pre>\n<pre><code>VERIFY_CB_ENTRY: preverify_ok=0 error=20(unable to get local issuer certificate) depth=10\nVERIFY_CB_ENTRY: preverify_ok=1 error=20(unable to get local issuer certificate) depth=9\nVERIFY_CB_ENTRY: preverify_ok=1 error=20(unable to get local issuer certificate) depth=8</code></pre>\n<pre><code>VERIFY_CB_ENTRY: preverify_ok=0 error=20(unable to get local issuer certificate) depth=10\nVERIFY_CB_ENTRY: preverify_ok=1 error=20(unable to get local issuer certificate) depth=9\nVERIFY_CB_ENTRY: preverify_ok=1 error=20(unable to get local issuer certificate) depth=8</code></pre>\n<p>The handshake completed and the resource behind the client-certificate requirement returned HTTP 200. The same GoAhead source built against LibreSSL 4.3.1 received error 22 at depth 10 and rejected the chain.</p>\n<p>With <code>VERIFY_ISSUER=0</code>, ordinary self-signed certificates are already allowed. The over-depth chain bypasses the separate chain-depth restriction because the verifier reports a tolerated issuer error. Deployments requiring a trusted client CA reject it.</p>\n<p><strong>Demonstrated configuration:</strong> GoAhead 6.1.0 with the LibreSSL legacy verifier, <code>ME_GOAHEAD_SSL_VERIFY_PEER=1</code>, and <code>ME_GOAHEAD_SSL_VERIFY_ISSUER=0</code>. Enabling issuer trust rejects error 20. OpenSSL, BoringSSL, AWS-LC, and LibreSSL 4.x builds are not exposed to this legacy-verifier trigger.</p>\n<h2 id=\"conclusion\">Conclusion</h2>\n<p>All three findings affect GoAhead&#39;s integration with its TLS libraries. CRL enforcement depends on a verification flag, DH parameters depend on the supplied buffer lengths, and chain-depth enforcement depends on how verification errors are handled. None of these defects prevents the affected handshake from completing.</p>\n<p>The backend and build configuration determine which defects apply. LibreSSL builds can inherit both the CRL defect in the OpenSSL-compatible backend and the legacy verifier&#39;s depth issue. The mbed TLS backend uses a separate verification path but is affected by the DH truncation when a client accepts the parameters.</p>\n<p>To find impacted publicly facing servers in your fleet, here are the JA4Scan-TLS fingerprints that map to the different backends:</p>\n<div class=\"table-wrap\"><table><thead><tr><th></th><th></th></tr></thead><tbody><tr><td>Server (GoAhead 6.1.x)</td><td>JA4Scan-TLS</td></tr><tr><td>OpenSSL 3.6.2</td><td>03100h0s00sn_1fe27f6025fb_d06dc4648d2f_sr00ss537461</td></tr><tr><td>LibreSSL 3.3.6</td><td>03100h0s00s0_9d52559f55bc_c64486648d2f_sr00sc24603c</td></tr><tr><td>LibreSSL 4.3.1 (depth bypass unaffected)</td><td>03100h0r00s0_9d52559f55bc_c64486206461_sr00scc98ba1</td></tr><tr><td>mbedTLS 3.6.6</td><td>23100h0s01s0_d7d3025021f4_0475c5cede0d_sr0ss02265cf</td></tr></tbody></table></div>\n<p>Read the full blog post about JA4Scan here:</p>\n<p><a href=\"https://foxio.io/blog/introducing-ja4scan-active-server-fingerprinting-for-tls-and-quic\" rel=\"nofollow ugc noopener\">https://foxio.io/blog/introducing-ja4scan-active-server-fingerprinting-for-tls-and-quic</a></p>\n<h2 id=\"poc\">POC</h2>\n<p>Reproduction code is available at <a href=\"https://github.com/FoxIO-LLC/blog_poc/tree/main/chooseyourbackend\" rel=\"nofollow ugc noopener\">https://github.com/FoxIO-LLC/blog_poc/tree/main/chooseyourbackend</a></p>\n<h2 id=\"timeline\">Timeline</h2>\n<ul><li>April 22 2026 - LibreSSL report</li><li>April 27 2026 - MbedTLS report</li><li>April 30 2026 - OpenSSL report</li><li>Aug 2 2026 - GoAhead 6.1.1 released</li><li>Oct 12 2026 - Advisories published</li><li>Oct 26 2026 - Blog published</li></ul>\n<h2 id=\"references\">References</h2>\n<p><a href=\"https://github.com/embedthis/goahead/security/advisories/GHSA-r392-vqwx-3r4g\" rel=\"nofollow ugc noopener\">GOAHEAD-SA-2026-0004 / CVE-2026-115884</a>: mTLS chain-depth bypass</p>\n<p><a href=\"https://github.com/embedthis/goahead/security/advisories/GHSA-fmvx-2xfx-2gq4\" rel=\"nofollow ugc noopener\">GOAHEAD-SA-2026-0005</a>: MbedTLS DH parameter truncation</p>\n<p><a href=\"https://github.com/embedthis/goahead/security/advisories/GHSA-5m75-xq69-c3c5\" rel=\"nofollow ugc noopener\">GOAHEAD-SA-2026-0006 / CVE-2026-115881</a>: OpenSSL CRL checking disabled</p>\n<p><a href=\"https://www.embedthis.com/goahead/doc/users/ssl.html\" rel=\"nofollow ugc noopener\">GoAhead SSL documentation</a></p>\n<p><a href=\"https://docs.openssl.org/3.6/man3/X509_VERIFY_PARAM_set_flags/\" rel=\"nofollow ugc noopener\">OpenSSL verification flags</a></p>\n<p><a href=\"https://docs.openssl.org/3.6/man3/X509_STORE_add_cert/\" rel=\"nofollow ugc noopener\">OpenSSL</a> <code>X509_STORE_set_flags(3)</code></p>\n<p><a href=\"https://datatracker.ietf.org/doc/html/rfc3526\" rel=\"nofollow ugc noopener\">RFC 3526 - MODP Diffie-Hellman groups</a></p>\n<hr />\n<p><em>Originally published at <a href=\"https://foxio.io/blog/choose-your-backend-three-tls-bugs-in-goahead\" rel=\"nofollow ugc noopener\">FoxIO</a>.</em></p>","headings":[{"level":1,"text":"Choose Your Backend: Three TLS Bugs in GoAhead","id":"choose-your-backend-three-tls-bugs-in-goahead"},{"level":2,"text":"TLDR","id":"tldr"},{"level":2,"text":"Background","id":"background"},{"level":2,"text":"OpenSSL-compatible builds","id":"openssl-compatible-builds"},{"level":2,"text":"Mbed TLS builds","id":"mbed-tls-builds"},{"level":2,"text":"LibreSSL builds","id":"libressl-builds"},{"level":2,"text":"Conclusion","id":"conclusion"},{"level":2,"text":"POC","id":"poc"},{"level":2,"text":"Timeline","id":"timeline"},{"level":2,"text":"References","id":"references"}]}}