CVE-2026-42789
Description
Improper Following of a Certificate's Chain of Trust vulnerability in Erlang OTP public_key (pubkey_cert module) allows a non-CA certificate to be accepted as an intermediate issuer, enabling certificate chain forgery.
In lib/public_key/src/pubkey_cert.erl, pubkey_cert:validate_extensions/7 contains two flaws that together allow a certificate with basicConstraints cA:false and no keyUsage extension to be used as an intermediate issuer in a chain passed to public_key:pkix_path_validation/3: the cA:false clause recurses into the remaining extensions without rejecting the certificate when it is in issuer position, and the keyUsage check only fires when the extension is present, so a certificate lacking keyUsage entirely bypasses the keyCertSign enforcement.
Any party holding an end-entity certificate with basicConstraints cA:false and no keyUsage extension, issued by any CA in the victim's trust store, can use that certificate's private key to sign forged leaf certificates for arbitrary identities. public_key:pkix_path_validation/3 accepts the resulting chain, and by extension every TLS or mTLS endpoint built on the OTP ssl application that relies on the default verifier is affected, including server identity verification on the client side and client certificate verification on mTLS servers.
This issue affects OTP from OTP 17.0 before OTP 26.2.5.21, 27.3.4.12, 28.5.0.1, and 29.0.1 corresponding to public_key from 0.22 before 1.15.1.7, 1.17.1.3, 1.20.3.1, and 1.21.1.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
A path-validation flaw in Erlang/OTP public_key allows a non-CA certificate with no keyUsage to be used as an intermediate issuer, enabling certificate chain forgery.
Vulnerability
A flaw in the pubkey_cert.erl module of the Erlang OTP public_key application (function pubkey_cert:validate_extensions/7) allows a certificate with basicConstraints cA:false and no keyUsage extension to be accepted as an intermediate certificate authority in chain validation performed by public_key:pkix_path_validation/3 [2][3][4]. The cA:false clause erroneously recurses into remaining extensions without rejecting the certificate in issuer position, and the keyCertSign check only fires when the keyUsage extension is present, so a certificate lacking that extension entirely bypasses enforcement [3][4]. Affected versions: OTP 17.0 up to (but not including) OTP 26.2.5.21, 27.3.4.12, 28.5.0.1, and 29.0.1, corresponding to public_key versions >=0.22 and <1.15.1.7, <1.17.1.3, <1.20.3.1, or <1.21.1 [1][3].
Exploitation
An attacker needs only an end-entity certificate (with basicConstraints cA:false and no keyUsage extension) issued by any CA present in the victim's trust store, along with the corresponding private key [3][4]. Such certificates are common in internal PKIs, IoT fleets, and minimal ACME profiles [4]. The attacker uses that private key to sign forged leaf certificates for arbitrary identities. Any application that relies on public_key:pkix_path_validation/3 directly, or indirectly via the Erlang ssl application (for TLS/mTLS server identity verification or client certificate verification), will accept the forged chain [3][4].
Impact
A successful attacker can forge leaf certificates for any identity, leading to compromise of TLS server or client authentication [3][4]. The impact is high: an attacker can impersonate any server to a victim client, or authenticate as any client to a victim mTLS server, effectively bypassing the trust chain entirely. No additional privileges beyond possession of the aforementioned end-entity certificate and key are required.
Mitigation
Fixed versions: OTP 26.2.5.21, 27.3.4.12, 28.5.0.1, and 29.0.1 (or later) [1][3]. Users should upgrade to a patched release. For systems that cannot immediately upgrade, a verify_fun callback can be used to enforce that intermediate certificates have basicConstraints cA:true [4]. No other workaround is provided by the vendor. The vulnerability is not listed as exploited in the wild (no KEV entry at the time of writing).
AI Insight generated on May 27, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
2- Range: < 1.15.1.7, < 1.17.1.3, < 1.20.3.1, < 1.21.1
Patches
259c8d824386bpublic_key: Update to stricter check as clearly defined in RFC 5280
2 files changed · +93 −16
lib/public_key/src/pubkey_cert.erl+22 −12 modified@@ -713,21 +713,30 @@ validate_extensions(Cert, [], ValidationState = true when SelfSigned -> {ValidationState, UserState0}; true -> - UserState = validate_ext_key_usage(Cert, UserState0, VerifyFun, endentity), - {ValidationState#path_validation_state{max_path_length = Len - 1}, - UserState}; - false -> - %% basic_constraint must appear in certs used for digital sign - %% see 4.2.1.10 in rfc 3280 + %% If the cA boolean is not asserted (basic_constraint) + %% then the keyCertSign bit in the key usage extension MUST NOT be + %% asserted. See 4.2.1.9 in RFC 5280 case is_digitally_sign_cert(Cert) of true -> - missing_basic_constraints(Cert, SelfSigned, + missing_basic_constraints(Cert, SelfSigned, ValidationState, VerifyFun, UserState0, Len); - false -> %% Example CRL signer only - {ValidationState, UserState0} - end + false -> + UserState = validate_ext_key_usage(Cert, UserState0, VerifyFun, endentity), + {ValidationState#path_validation_state{max_path_length = Len - 1}, + UserState} + end; + false -> + %% If the basic constraints extension is not present in a + %% version 3 certificate, or the extension is present but the cA boolean + %% is not asserted, then the certified public key MUST NOT be used to + %% verify certificate signature. + missing_basic_constraints(Cert, SelfSigned, + ValidationState, VerifyFun, + UserState0, Len) end; +%% The pathLenConstraint field is meaningful only if cA is set to +%% TRUE. validate_extensions(Cert, [#'Extension'{extnID = ?'id-ce-basicConstraints', extnValue = @@ -745,8 +754,9 @@ validate_extensions(Cert, Length}, basic_constraint, SelfSigned, UserState, VerifyFun); -%% The pathLenConstraint field is meaningful only if cA is set to -%% TRUE. + +%% cA:false — per RFC 5280 Section 6.1.4(k), treated identically to +%% absent basicConstraints. The base case clause rejects if intermediate. validate_extensions(Cert, [#'Extension'{extnID = ?'id-ce-basicConstraints', extnValue = #'BasicConstraints'{cA = false}} |
lib/public_key/test/public_key_SUITE.erl+71 −4 modified@@ -103,6 +103,8 @@ pkix_path_validation/1, pkix_path_validation_root_expired/0, pkix_path_validation_root_expired/1, + pkix_path_validation_forged_chain/0, + pkix_path_validation_forged_chain/1, pkix_ext_key_usage/0, pkix_ext_key_usage/1, pkix_ext_key_usage_any/0, @@ -173,6 +175,7 @@ all() -> pkix_decode_cert, pkix_path_validation, pkix_path_validation_root_expired, + pkix_path_validation_forged_chain, pkix_ext_key_usage, pkix_ext_key_usage_any, pkix_path_validation_bad_date, @@ -502,14 +505,14 @@ eddsa_pub() -> eddsa_pub(Config) when is_list(Config) -> Datadir = proplists:get_value(data_dir, Config), {ok, EDDSAPubPem} = file:read_file(filename:join(Datadir, "public_eddsa.pem")), - [{'SubjectPublicKeyInfo', _, not_encrypted} = Key] = PemEntry = + [{'SubjectPublicKeyInfo', _, not_encrypted}] = PemEntry = public_key:pem_decode(EDDSAPubPem), EDDSAPubKey = public_key:pem_entry_decode(PemEntry), true = check_entry_type(EDDSAPubKey, 'ECPoint'), {_, {namedCurve, ?'id-Ed25519'}} = EDDSAPubKey, - PrivEntry0 = public_key:pem_entry_encode('SubjectPublicKeyInfo', EDDSAPubKey), + EncPemEntry = public_key:pem_entry_encode('SubjectPublicKeyInfo', EDDSAPubKey), ECPemNoEndNewLines = strip_superfluous_newlines(EDDSAPubPem), - ECPemNoEndNewLines = strip_superfluous_newlines(public_key:pem_encode([PemEntry])). + ECPemNoEndNewLines = strip_superfluous_newlines(public_key:pem_encode([EncPemEntry])). eddsa_sign_verify_24_compat(_Config) -> Key = @@ -1023,7 +1026,71 @@ pkix_path_validation_root_expired(Config) when is_list(Config) -> true = public_key:pkix_is_self_signed(Root), Peer = proplists:get_value(cert, Conf), {error, {bad_cert, cert_expired}} = public_key:pkix_path_validation(Root, [ICA, Peer], []). - + +pkix_path_validation_forged_chain() -> + [{doc, "Test that end-entity can not be used as intermediate CA"}]. +pkix_path_validation_forged_chain(Config) when is_list(Config) -> + #{cert := Root} = SRootSpec = public_key:pkix_test_root_cert("OTP test server ROOT", []), + Exts = [#'Extension'{extnID = ?'id-ce-keyUsage', + extnValue = [keyCertSign, digitalSignature]}], + #{server_config := ServerOpts0} = + public_key:pkix_test_data(#{server_chain => + #{root => SRootSpec, + intermediates => [[]], + peer => [{extensions, Exts}]}, + client_chain => + #{root => [], + intermediates => [], + peer => []}} + ), + {ASN1, Key} = proplists:get_value(key, ServerOpts0), + ServerKey = public_key:der_decode(ASN1, Key), + ServerCert = public_key:pkix_decode_cert(proplists:get_value(cert, ServerOpts0), otp), + [ICA] = [I || I <- proplists:get_value(cacerts, ServerOpts0), + not public_key:pkix_is_self_signed(I)], + {_, Subject} = public_key:pkix_subject_id(ServerCert), + #'ECPrivateKey'{parameters = Params, + publicKey = PubKey} = public_key:generate_key({namedCurve, ?'secp256r1'}), + Algo = #'PublicKeyAlgorithm'{algorithm= ?'id-ecPublicKey', parameters = Params}, + SPKI = #'OTPSubjectPublicKeyInfo'{algorithm = Algo, + subjectPublicKey = #'ECPoint'{point = PubKey}}, + #'OTPCertificate'{tbsCertificate = ServerTBC} = ServerCert, + NewTBC = ServerTBC#'OTPTBSCertificate'{issuer = Subject, + subject = {rdnSequence, + [[{'AttributeTypeAndValue', + {2,5,4,3}, + {printableString,"forged server Peer cert"}}], + [{'AttributeTypeAndValue', + {2,5,4,7}, + {printableString,"Stockholm"}}], + [{'AttributeTypeAndValue',{2,5,4,6},"SE"}], + [{'AttributeTypeAndValue', + {2,5,4,10}, + {printableString,"erlang"}}], + [{'AttributeTypeAndValue', + {2,5,4,11}, + {printableString,"automated testing"}}]]}, + subjectPublicKeyInfo = SPKI, + extensions = []}, + ForgedCert = public_key:pkix_sign(NewTBC, ServerKey), + Fun = fun(_, _, {bad_cert, _} = R, _) -> + {fail, R}; + (_, _, {extension, _}, UserState) -> + {unknown, UserState}; + (_, _, valid, UserState) -> + {valid, UserState}; + (OTPCert, _, valid_peer, UserState) -> + case public_key:pkix_verify_hostname(OTPCert, + [{dns_id, net_adm:localhost()}], []) of + true -> + {valid, UserState}; + false -> + {fail, {bad_cert, hostname_check_failed}} + end + end, + {error, Err} = public_key:pkix_path_validation(Root, [ICA, ServerCert, ForgedCert], + [{verify_fun, {Fun, []}}]). + pkix_ext_key_usage() -> [{doc, "If extended key usage is a critical extension in a CA (usually not included) make sure it is compatible with keyUsage extension"}]. pkix_ext_key_usage(Config) when is_list(Config) ->
471cd2f66430public_key: Update to stricter check as clearly defined in RFC 5280
1 file changed · +18 −11
lib/public_key/src/pubkey_cert.erl+18 −11 modified@@ -698,20 +698,27 @@ validate_extensions(OtpCert, [], ValidationState = true when SelfSigned -> {ValidationState, UserState0}; true -> - UserState = validate_ext_key_usage(OtpCert, UserState0, VerifyFun, endentity), - {ValidationState#path_validation_state{max_path_length = Len - 1}, - UserState}; - false -> - %% basic_constraint must appear in certs used for digital sign - %% see 4.2.1.10 in rfc 3280 - case is_digitally_sign_cert(OtpCert) of + %% If the cA boolean is not asserted (basic_constraint) + %% then the keyCertSign bit in the key usage extension MUST NOT be + %% asserted. See 4.2.1.9 in RFC 5280 + case is_digitally_sign_cert(OtpCert) of true -> - missing_basic_constraints(OtpCert, SelfSigned, + missing_basic_constraints(OtpCert, SelfSigned, ValidationState, VerifyFun, UserState0, Len); - false -> %% Example CRL signer only - {ValidationState, UserState0} - end + false -> + UserState = validate_ext_key_usage(OtpCert, UserState0, VerifyFun, endentity), + {ValidationState#path_validation_state{max_path_length = Len - 1}, + UserState} + end; + false -> + %% If the basic constraints extension is not present in a + %% version 3 certificate, or the extension is present but the cA boolean + %% is not asserted, then the certified public key MUST NOT be used to + %% verify certificate signature. + missing_basic_constraints(OtpCert, SelfSigned, + ValidationState, VerifyFun, + UserState0, Len) end; validate_extensions(OtpCert, [#'Extension'{extnID = ?'id-ce-basicConstraints',
Vulnerability mechanics
Root cause
"Missing rejection of a certificate with basicConstraints cA:false when used as an intermediate issuer, combined with a keyUsage check that only fires when the extension is present, allows certificate chain forgery."
Attack vector
An attacker who holds an end-entity certificate with `basicConstraints cA:false` and no `keyUsage` extension, issued by any CA in the victim's trust store, can use that certificate's private key to sign forged leaf certificates for arbitrary identities [ref_id=1]. The attacker constructs a chain: trust anchor → legitimate intermediate CA → attacker's end-entity certificate (used as a bogus intermediate) → forged leaf. `public_key:pkix_path_validation/3` accepts this chain because the old code did not reject a `cA:false` certificate in issuer position when `keyUsage` was absent [patch_id=2662157]. Every TLS or mTLS endpoint built on the OTP `ssl` application that relies on the default verifier is affected, including server identity verification on the client side and client certificate verification on mTLS servers.
Affected code
The flaw resides in `lib/public_key/src/pubkey_cert.erl`, specifically in the `validate_extensions/7` function. The `cA:false` clause (matching `#'BasicConstraints'{cA = false}`) previously recursed into remaining extensions without rejecting the certificate when it appeared in issuer position, and the `keyUsage` check only fired when the extension was present, so a certificate lacking `keyUsage` entirely bypassed `keyCertSign` enforcement [patch_id=2662157][patch_id=2662156].
What the fix does
Both patches restructure the `validate_extensions/7` function so that when `cA` is `false` (or `basicConstraints` is absent), the code falls through to `missing_basic_constraints/6`, which rejects the certificate if it is used in issuer position [patch_id=2662157][patch_id=2662156]. The commit message explicitly cites RFC 5280 Section 6.1.4(k): if `basicConstraints` is absent or `cA` is not asserted, the certificate MUST NOT be used to verify other certificates [ref_id=1]. The new test `pkix_path_validation_forged_chain` confirms that a chain using an end-entity certificate as an intermediate now correctly fails validation [ref_id=1].
Preconditions
- inputAttacker must possess an end-entity certificate with basicConstraints cA:false and no keyUsage extension, issued by a CA in the victim's trust store.
- inputAttacker must have access to the corresponding private key of that end-entity certificate.
- configThe victim's application must use public_key:pkix_path_validation/3 (or the default verifier in the ssl application) to validate certificate chains.
Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
6- cna.erlef.org/cves/CVE-2026-42789.htmlnvd
- github.com/erlang/otp/commit/471cd2f664300a95353c467873800bbe706005dbnvd
- github.com/erlang/otp/commit/59c8d824386b2eb1614ff9340624843ef6aca0fdnvd
- github.com/erlang/otp/security/advisories/GHSA-c99q-jmpx-v8qqnvd
- osv.dev/vulnerability/EEF-CVE-2026-42789nvd
- www.erlang.org/doc/system/versions.htmlnvd
News mentions
0No linked articles in our index yet.