Built-in TLS: CA-bundle certificate chain accepted without any signature verification
Severity: HIGH
Location
- File:
src/tls_builtin.c
- Line: 1890
Description
When a client is configured with a multi-certificate PEM CA bundle (mg_tls_opts.ca containing more than one CERTIFICATE block), mg_tls_init() stores it in tls->ca_bundle_der and leaves tls->ca_der.len == 0. In mg_tls_recv_cert(), the intra-chain loop only verifies that cert[i-1] was signed by cert[i]; the last (topmost) certificate of the presented chain is never verified against a trust anchor. The bundle fallback block at lines 1887-1897 only calls tls_bundle_find(tls, &certs[certnum-1].issuer, &ca), which merely searches the bundle for a certificate whose subject Common Name string equals the issuer Common Name of the last chain certificate (mg_strcasecmp on CN only), and then does nothing with the result. The signature check that exists for the single-CA case (mg_tls_verify_cert_signature(&certs[certnum-1], &ca) at lines 1898-1906) is guarded by tls->ca_der.len > 0, which is always false in bundle mode. Consequently, trust-anchor validation degenerates into a string comparison of a CN.
Attack Scenario
A network attacker (DNS/ARP spoofing, hostile Wi-Fi, BGP hijack, or any MITM position) intercepts a Mongoose HTTPS/MQTTS/WSS client connection that was configured with a CA bundle. The attacker generates a self-signed certificate whose subject CN/SAN matches the target hostname (satisfying mg_tls_verify_cert_san()/mg_tls_verify_cert_cn()) and whose issuer Common Name is set to the CN of any root present in the victim's bundle (e.g. "ISRG Root X1" or "DigiCert Global Root CA"). It presents this single certificate in the TLS Certificate message and signs the CertificateVerify with its own private key. mg_tls_recv_cert() finds a bundle entry with a matching subject CN, reports success, and mg_tls_recv_cert_verify() validates the CertificateVerify against the attacker's own public key taken from that same forged certificate.
Impact
Complete TLS server-authentication bypass for built-in-TLS clients using a CA bundle: full man-in-the-middle interception and modification of all traffic, disclosure of credentials/tokens/telemetry, and injection of attacker-controlled responses (including, on devices that use HTTP OTA, attacker-controlled firmware).
Recommendation
In the !found_ca && tls->ca_bundle_len > 0 branch, after tls_bundle_find() locates a candidate anchor, cryptographically verify the last chain certificate against it, e.g. if (!mg_tls_verify_cert_signature(&certs[certnum - 1], &ca)) { mg_error(c, "failed to verify CA"); return -1; }, and additionally require that the anchor is a CA (ca.is_ca). Matching on issuer/subject Common Name must never by itself be treated as chain validation; prefer matching by full issuer DN plus Subject Key Identifier and always finish with a signature check.
Evidence
mg_tls_init(): if (cert_count > 1 && c->is_client) { tls->ca_bundle_len = cert_count; tls->ca_bundle_der = all_certs; } leaves ca_der.len == 0. mg_tls_recv_cert(): if (!found_ca && certnum > 0 && tls->ca_bundle_len > 0) { r = tls_bundle_find(tls, &certs[certnum-1].issuer, &ca); if (r <= 0) { mg_error(...); return -1; } MG_VERBOSE(("CA serial: ...")); } — no mg_tls_verify_cert_signature() call. tls_bundle_find() returns 1 purely on mg_strcasecmp(subj, tgt) == 0 where subj/tgt are OID 2.5.4.3 (CN) values.
Built-in TLS: CA-bundle certificate chain accepted without any signature verification
Severity: HIGH
Location
src/tls_builtin.cDescription
When a client is configured with a multi-certificate PEM CA bundle (mg_tls_opts.ca containing more than one CERTIFICATE block), mg_tls_init() stores it in tls->ca_bundle_der and leaves tls->ca_der.len == 0. In mg_tls_recv_cert(), the intra-chain loop only verifies that cert[i-1] was signed by cert[i]; the last (topmost) certificate of the presented chain is never verified against a trust anchor. The bundle fallback block at lines 1887-1897 only calls tls_bundle_find(tls, &certs[certnum-1].issuer, &ca), which merely searches the bundle for a certificate whose subject Common Name string equals the issuer Common Name of the last chain certificate (mg_strcasecmp on CN only), and then does nothing with the result. The signature check that exists for the single-CA case (mg_tls_verify_cert_signature(&certs[certnum-1], &ca) at lines 1898-1906) is guarded by
tls->ca_der.len > 0, which is always false in bundle mode. Consequently, trust-anchor validation degenerates into a string comparison of a CN.Attack Scenario
A network attacker (DNS/ARP spoofing, hostile Wi-Fi, BGP hijack, or any MITM position) intercepts a Mongoose HTTPS/MQTTS/WSS client connection that was configured with a CA bundle. The attacker generates a self-signed certificate whose subject CN/SAN matches the target hostname (satisfying mg_tls_verify_cert_san()/mg_tls_verify_cert_cn()) and whose issuer Common Name is set to the CN of any root present in the victim's bundle (e.g. "ISRG Root X1" or "DigiCert Global Root CA"). It presents this single certificate in the TLS Certificate message and signs the CertificateVerify with its own private key. mg_tls_recv_cert() finds a bundle entry with a matching subject CN, reports success, and mg_tls_recv_cert_verify() validates the CertificateVerify against the attacker's own public key taken from that same forged certificate.
Impact
Complete TLS server-authentication bypass for built-in-TLS clients using a CA bundle: full man-in-the-middle interception and modification of all traffic, disclosure of credentials/tokens/telemetry, and injection of attacker-controlled responses (including, on devices that use HTTP OTA, attacker-controlled firmware).
Recommendation
In the
!found_ca && tls->ca_bundle_len > 0branch, after tls_bundle_find() locates a candidate anchor, cryptographically verify the last chain certificate against it, e.g.if (!mg_tls_verify_cert_signature(&certs[certnum - 1], &ca)) { mg_error(c, "failed to verify CA"); return -1; }, and additionally require that the anchor is a CA (ca.is_ca). Matching on issuer/subject Common Name must never by itself be treated as chain validation; prefer matching by full issuer DN plus Subject Key Identifier and always finish with a signature check.Evidence
mg_tls_init():
if (cert_count > 1 && c->is_client) { tls->ca_bundle_len = cert_count; tls->ca_bundle_der = all_certs; }leaves ca_der.len == 0. mg_tls_recv_cert():if (!found_ca && certnum > 0 && tls->ca_bundle_len > 0) { r = tls_bundle_find(tls, &certs[certnum-1].issuer, &ca); if (r <= 0) { mg_error(...); return -1; } MG_VERBOSE(("CA serial: ...")); }— no mg_tls_verify_cert_signature() call. tls_bundle_find() returns 1 purely onmg_strcasecmp(subj, tgt) == 0where subj/tgt are OID 2.5.4.3 (CN) values.