Choose Your Backend: Three TLS Bugs in GoAhead

Sébastien Féry

TLDR

Three 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:

  • OpenSSL-compatible backend - Critical, CVSS 9.1 - CRL loaded but not enforced, revoked client certificates are silently accepted. GOAHEAD-SA-2026-0006.

  • 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.

  • 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.

TLS build

GoAhead versions

Required configuration

Fix or mitigation

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.

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.

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.

Background

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.

Each finding below affects a specific subset of those deployments, depending on the build and configuration.

GoAhead supports two TLS backends: OpenSSL, the default, and MbedTLS. The OpenSSL backend also works with compatible libraries such as LibreSSL.

GoAhead takes a compile-time approach regarding certain configurations:

- ME_GOAHEAD_SSL_VERIFY_PEER=1 requires a client certificate.

- ME_GOAHEAD_SSL_AUTHORITY points at a PEM bundle of trusted CAs.

- ME_GOAHEAD_SSL_VERIFY_ISSUER=1 requires the certificate chain to establish issuer trust against that bundle.

- ME_GOAHEAD_SSL_REVOKE points at a PEM-encoded Certificate Revocation List.

- The OpenSSL backend sets a chain-depth limit of 10 (VERIFY_DEPTH).

OpenSSL-compatible builds

GoAhead'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.

The CRL is loaded in sslOpen():

// src/openssl.c
if (ME_GOAHEAD_SSL_REVOKE && *ME_GOAHEAD_SSL_REVOKE) {
    store = SSL_CTX_get_cert_store(sslctx);
    if (!X509_STORE_load_locations(store, ME_GOAHEAD_SSL_REVOKE, 0)) {
        error("Cannot load certificate revoke list: %s", ME_GOAHEAD_SSL_REVOKE);
        sslClose();
        return -1;
    }
}
// src/openssl.c
if (ME_GOAHEAD_SSL_REVOKE && *ME_GOAHEAD_SSL_REVOKE) {
    store = SSL_CTX_get_cert_store(sslctx);
    if (!X509_STORE_load_locations(store, ME_GOAHEAD_SSL_REVOKE, 0)) {
        error("Cannot load certificate revoke list: %s", ME_GOAHEAD_SSL_REVOKE);
        sslClose();
        return -1;
    }
}
// src/openssl.c
if (ME_GOAHEAD_SSL_REVOKE && *ME_GOAHEAD_SSL_REVOKE) {
    store = SSL_CTX_get_cert_store(sslctx);
    if (!X509_STORE_load_locations(store, ME_GOAHEAD_SSL_REVOKE, 0)) {
        error("Cannot load certificate revoke list: %s", ME_GOAHEAD_SSL_REVOKE);
        sslClose();
        return -1;
    }
}

OpenSSL 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:

// OpenSSL crypto/x509/x509_vfy.c
int crl_check_enabled =
    (ctx->param->flags &
     (X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL)) != 0;
...
if (!crl_check_enabled && !ocsp_check_enabled)
    return 1;
// OpenSSL crypto/x509/x509_vfy.c
int crl_check_enabled =
    (ctx->param->flags &
     (X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL)) != 0;
...
if (!crl_check_enabled && !ocsp_check_enabled)
    return 1;
// OpenSSL crypto/x509/x509_vfy.c
int crl_check_enabled =
    (ctx->param->flags &
     (X509_V_FLAG_CRL_CHECK | X509_V_FLAG_CRL_CHECK_ALL)) != 0;
...
if (!crl_check_enabled && !ocsp_check_enabled)
    return 1;

GoAhead 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.

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:

VERIFY_CB: preverify_ok=1 error=0(ok) depth=1
VERIFY_CB: preverify_ok=1 error=0(ok) depth=0
VERIFY_CB: preverify_ok=1 error=0(ok) depth=1
VERIFY_CB: preverify_ok=1 error=0(ok) depth=0
VERIFY_CB: preverify_ok=1 error=0(ok) depth=1
VERIFY_CB: preverify_ok=1 error=0(ok) depth=0

X509_STORE_load_locations() succeeds, so startup produces no warning that CRL enforcement is inactive.

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.

Fix: 6.1.1 now checks after loading the CRL:

+ X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK);
+ X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK);
+ X509_STORE_set_flags(store, X509_V_FLAG_CRL_CHECK);

Mbed TLS builds

GoAhead'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:

// src/mbedtls.c
cuchar dhm_p[] = MBEDTLS_DHM_RFC3526_MODP_2048_P_BIN; // 256 bytes
cuchar dhm_g[] = MBEDTLS_DHM_RFC3526_MODP_2048_G_BIN; // 1 byte: 0x02

mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_g), dhm_g, sizeof(dhm_g));
// src/mbedtls.c
cuchar dhm_p[] = MBEDTLS_DHM_RFC3526_MODP_2048_P_BIN; // 256 bytes
cuchar dhm_g[] = MBEDTLS_DHM_RFC3526_MODP_2048_G_BIN; // 1 byte: 0x02

mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_g), dhm_g, sizeof(dhm_g));
// src/mbedtls.c
cuchar dhm_p[] = MBEDTLS_DHM_RFC3526_MODP_2048_P_BIN; // 256 bytes
cuchar dhm_g[] = MBEDTLS_DHM_RFC3526_MODP_2048_G_BIN; // 1 byte: 0x02

mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_g), dhm_g, sizeof(dhm_g));

The 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.

As 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:

0c 00 01 0d 00 01 ff 00 01 02 00 01 08
0c 00 01 0d 00 01 ff 00 01 02 00 01 08
0c 00 01 0d 00 01 ff 00 01 02 00 01 08

In ServerKeyExchange, 00 01 ff identifies a one-byte DH parameter with the value 255. It should contain the full 256-byte value.

The broken parameters leave just eight possible shared-secret values. An observer can try each candidate and identify the correct one by checking whether it decrypts the captured traffic successfully.

In 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.

A passive observer can decrypt captured sessions using the recorded handshake and encrypted application records. No MITM position, server private key, or modification of traffic is required.

Exploitation 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.

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.

If you see any of these suites in the JA4S logs of your environment, you might be impacted (eg. t120100_009f_1ca028f07214)

0014  TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA
0015  TLS_DHE_RSA_WITH_DES_CBC_SHA
0016  TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA
0033  TLS_DHE_RSA_WITH_AES_128_CBC_SHA
0039  TLS_DHE_RSA_WITH_AES_256_CBC_SHA
0045  TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA
0067  TLS_DHE_RSA_WITH_AES_128_CBC_SHA256
006B  TLS_DHE_RSA_WITH_AES_256_CBC_SHA256
0088  TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA
009A  TLS_DHE_RSA_WITH_SEED_CBC_SHA
009E  TLS_DHE_RSA_WITH_AES_128_GCM_SHA256
009F  TLS_DHE_RSA_WITH_AES_256_GCM_SHA384
00BE  TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256
00C4  TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256
C044  TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256
C045  TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384
C052  TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256
C053  TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384
0C07C  TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256
C07D  TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384
C09E  TLS_DHE_RSA_WITH_AES_128_CCM
C09F  TLS_DHE_RSA_WITH_AES_256_CCM
C0A2  TLS_DHE_RSA_WITH_AES_128_CCM_8
C0A3  TLS_DHE_RSA_WITH_AES_256_CCM_8
CCAA  TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256
0014  TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA
0015  TLS_DHE_RSA_WITH_DES_CBC_SHA
0016  TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA
0033  TLS_DHE_RSA_WITH_AES_128_CBC_SHA
0039  TLS_DHE_RSA_WITH_AES_256_CBC_SHA
0045  TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA
0067  TLS_DHE_RSA_WITH_AES_128_CBC_SHA256
006B  TLS_DHE_RSA_WITH_AES_256_CBC_SHA256
0088  TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA
009A  TLS_DHE_RSA_WITH_SEED_CBC_SHA
009E  TLS_DHE_RSA_WITH_AES_128_GCM_SHA256
009F  TLS_DHE_RSA_WITH_AES_256_GCM_SHA384
00BE  TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256
00C4  TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256
C044  TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256
C045  TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384
C052  TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256
C053  TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384
0C07C  TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256
C07D  TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384
C09E  TLS_DHE_RSA_WITH_AES_128_CCM
C09F  TLS_DHE_RSA_WITH_AES_256_CCM
C0A2  TLS_DHE_RSA_WITH_AES_128_CCM_8
C0A3  TLS_DHE_RSA_WITH_AES_256_CCM_8
CCAA  TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256
0014  TLS_DHE_RSA_EXPORT_WITH_DES40_CBC_SHA
0015  TLS_DHE_RSA_WITH_DES_CBC_SHA
0016  TLS_DHE_RSA_WITH_3DES_EDE_CBC_SHA
0033  TLS_DHE_RSA_WITH_AES_128_CBC_SHA
0039  TLS_DHE_RSA_WITH_AES_256_CBC_SHA
0045  TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA
0067  TLS_DHE_RSA_WITH_AES_128_CBC_SHA256
006B  TLS_DHE_RSA_WITH_AES_256_CBC_SHA256
0088  TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA
009A  TLS_DHE_RSA_WITH_SEED_CBC_SHA
009E  TLS_DHE_RSA_WITH_AES_128_GCM_SHA256
009F  TLS_DHE_RSA_WITH_AES_256_GCM_SHA384
00BE  TLS_DHE_RSA_WITH_CAMELLIA_128_CBC_SHA256
00C4  TLS_DHE_RSA_WITH_CAMELLIA_256_CBC_SHA256
C044  TLS_DHE_RSA_WITH_ARIA_128_CBC_SHA256
C045  TLS_DHE_RSA_WITH_ARIA_256_CBC_SHA384
C052  TLS_DHE_RSA_WITH_ARIA_128_GCM_SHA256
C053  TLS_DHE_RSA_WITH_ARIA_256_GCM_SHA384
0C07C  TLS_DHE_RSA_WITH_CAMELLIA_128_GCM_SHA256
C07D  TLS_DHE_RSA_WITH_CAMELLIA_256_GCM_SHA384
C09E  TLS_DHE_RSA_WITH_AES_128_CCM
C09F  TLS_DHE_RSA_WITH_AES_256_CCM
C0A2  TLS_DHE_RSA_WITH_AES_128_CCM_8
C0A3  TLS_DHE_RSA_WITH_AES_256_CCM_8
CCAA  TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256

A sample query you might want to run (in Splunk or other) would look like:

DestIP = (GoAhead Device)
AND
JA4S = *_0014_

DestIP = (GoAhead Device)
AND
JA4S = *_0014_

DestIP = (GoAhead Device)
AND
JA4S = *_0014_

replacing 0014 with the curves listed above.

Fix: 6.1.1 fixed the typo

- mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_g), dhm_g, sizeof(dhm_g))
+ mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_p), dhm_g, sizeof(dhm_g))
- mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_g), dhm_g, sizeof(dhm_g))
+ mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_p), dhm_g, sizeof(dhm_g))
- mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_g), dhm_g, sizeof(dhm_g))
+ mbedtls_ssl_conf_dh_param_bin(conf, dhm_p, sizeof(dhm_p), dhm_g, sizeof(dhm_g))

LibreSSL builds

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.

GoAhead 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:

// src/openssl.c
if (ok && VERIFY_DEPTH < depth) {
    if (error == 0) {
        error = X509_V_ERR_CERT_CHAIN_TOO_LONG;
    }
}
// src/openssl.c
if (ok && VERIFY_DEPTH < depth) {
    if (error == 0) {
        error = X509_V_ERR_CERT_CHAIN_TOO_LONG;
    }
}
// src/openssl.c
if (ok && VERIFY_DEPTH < depth) {
    if (error == 0) {
        error = X509_V_ERR_CERT_CHAIN_TOO_LONG;
    }
}

That 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:

case X509_V_ERR_CERT_UNTRUSTED:
case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:
    if (ME_GOAHEAD_SSL_VERIFY_ISSUER) {
        ok = 0;
    }
    break;

case X509_V_ERR_CERT_CHAIN_TOO_LONG:
default:
    ok = 0;
    break;
case X509_V_ERR_CERT_UNTRUSTED:
case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:
    if (ME_GOAHEAD_SSL_VERIFY_ISSUER) {
        ok = 0;
    }
    break;

case X509_V_ERR_CERT_CHAIN_TOO_LONG:
default:
    ok = 0;
    break;
case X509_V_ERR_CERT_UNTRUSTED:
case X509_V_ERR_UNABLE_TO_GET_ISSUER_CERT_LOCALLY:
    if (ME_GOAHEAD_SSL_VERIFY_ISSUER) {
        ok = 0;
    }
    break;

case X509_V_ERR_CERT_CHAIN_TOO_LONG:
default:
    ok = 0;
    break;

Error 20, UNABLE_TO_GET_ISSUER_CERT_LOCALLY, is tolerated when issuer checking is disabled. Error 22, CERT_CHAIN_TOO_LONG, is rejected.

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):

// LibreSSL src/lib/libcrypto/x509/x509_vfy.c
for (;;) {
    /* FIXME: If this happens, we should take
     * note of it and, if appropriate, use the
     * X509_V_ERR_CERT_CHAIN_TOO_LONG error code
     * later.
     */
    if (depth < num)
        break;
// LibreSSL src/lib/libcrypto/x509/x509_vfy.c
for (;;) {
    /* FIXME: If this happens, we should take
     * note of it and, if appropriate, use the
     * X509_V_ERR_CERT_CHAIN_TOO_LONG error code
     * later.
     */
    if (depth < num)
        break;
// LibreSSL src/lib/libcrypto/x509/x509_vfy.c
for (;;) {
    /* FIXME: If this happens, we should take
     * note of it and, if appropriate, use the
     * X509_V_ERR_CERT_CHAIN_TOO_LONG error code
     * later.
     */
    if (depth < num)
        break;

The 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.

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:

VERIFY_CB_ENTRY: preverify_ok=0 error=20(unable to get local issuer certificate) depth=10
VERIFY_CB_ENTRY: preverify_ok=1 error=20(unable to get local issuer certificate) depth=9
VERIFY_CB_ENTRY: preverify_ok=1 error=20(unable to get local issuer certificate) depth=8

VERIFY_CB_ENTRY: preverify_ok=0 error=20(unable to get local issuer certificate) depth=10
VERIFY_CB_ENTRY: preverify_ok=1 error=20(unable to get local issuer certificate) depth=9
VERIFY_CB_ENTRY: preverify_ok=1 error=20(unable to get local issuer certificate) depth=8

VERIFY_CB_ENTRY: preverify_ok=0 error=20(unable to get local issuer certificate) depth=10
VERIFY_CB_ENTRY: preverify_ok=1 error=20(unable to get local issuer certificate) depth=9
VERIFY_CB_ENTRY: preverify_ok=1 error=20(unable to get local issuer certificate) depth=8

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.

With 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.

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.

Conclusion

All 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.

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'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.

To find impacted publicly facing servers in your fleet, here are the JA4Scan-TLS fingerprints that map to the different backends:

Server (GoAhead 6.1.x)

JA4Scan-TLS

OpenSSL 3.6.2

03100h0s00sn_1fe27f6025fb_d06dc4648d2f_sr00ss537461

LibreSSL 3.3.6

03100h0s00s0_9d52559f55bc_c64486648d2f_sr00sc24603c

LibreSSL 4.3.1 (depth bypass unaffected)

03100h0r00s0_9d52559f55bc_c64486206461_sr00scc98ba1

mbedTLS 3.6.6

23100h0s01s0_d7d3025021f4_0475c5cede0d_sr0ss02265cf

Read the full blog post about JA4Scan here:

https://foxio.io/blog/introducing-ja4scan-active-server-fingerprinting-for-tls-and-quic

POC

Reproduction code is available at https://github.com/FoxIO-LLC/blog_poc/tree/main/chooseyourbackend

Timeline

- April 22 2026 - LibreSSL report

- April 27 2026 - MbedTLS report

- April 30 2026 - OpenSSL report

- Aug 2 2026 - GoAhead 6.1.1 released

- Oct 12 2026 - Advisories published

- Oct 26 2026 - Blog published

References

GOAHEAD-SA-2026-0004 / CVE-2026-115884: mTLS chain-depth bypass

GOAHEAD-SA-2026-0005: MbedTLS DH parameter truncation

GOAHEAD-SA-2026-0006 / CVE-2026-115881: OpenSSL CRL checking disabled

GoAhead SSL documentation

OpenSSL verification flags

OpenSSL X509_STORE_set_flags(3)

RFC 3526 - MODP Diffie-Hellman groups