CVE-2026-42791
Description
Improper Certificate Validation vulnerability in Erlang OTP public_key (pubkey_ocsp module) allows forged OCSP responses signed with an expired responder certificate to be accepted as valid.
OCSP response verification in pubkey_ocsp:verify_response/5 and pubkey_ocsp:is_authorized_responder/3 in lib/public_key/src/pubkey_ocsp.erl does not check the validity period (notBefore/notAfter) of the OCSP responder certificate. An attacker who has obtained the private key of an expired CA-designated OCSP responder certificate can forge OCSP responses that Erlang/OTP accepts as valid.
This affects TLS clients using OCSP stapling via the ssl application: a malicious or compromised server can present a revoked TLS certificate together with a forged OCSP response signed by an expired responder key, and the client will accept the revoked certificate as valid. It also affects applications calling public_key:pkix_ocsp_validate/5 directly, where the impact depends on the use case — server-side client certificate validation using this API may allow authentication bypass with a revoked client certificate.
This issue affects OTP from OTP 27.0 before OTP 27.3.4.12, 28.5.0.1, and 29.0.1 corresponding to public_key from 1.16 before 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.
Erlang/OTP public_key OCSP validator accepts forged responses signed with an expired responder certificate, enabling revocation bypass for TLS clients.
Vulnerability
The public_key application's OCSP response verification in pubkey_ocsp:verify_response/5 and pubkey_ocsp:is_authorized_responder/3 does not check the validity period (notBefore/notAfter) of the responder certificate. This affects OTP versions 27.0 up to (but not including) 27.3.4.12, 28.5.0.1, and 29.0.1, corresponding to public_key versions 1.16 to before 1.17.1.3, 1.20.3.1, and 1.21.1 [2].
Exploitation
An attacker who has obtained the private key of an expired OCSP responder certificate—one that was previously designated by the CA—can forge OCSP responses. When a TLS server presents a revoked certificate along with such a forged OCSP staple, or when an application calls public_key:pkix_ocsp_validate/5 directly, Erlang/OTP accepts the response as valid because it never validates the responder certificate's temporal validity [2]. The attacker must be in a position to serve the forged OCSP response (e.g., as a malicious TLS server or via a man-in-the-middle attack).
Impact
Successful exploitation allows a revocated certificate to be accepted as valid by the verifying party. For TLS clients using OCSP stapling via the ssl application, this means a compromised or malicious server can present a revoked TLS certificate together with a forged OCSP response, and the client will not detect the revocation. For applications directly calling the OCSP API, server-side client certificate validation may be bypassed, enabling authentication with a revoked client certificate [2].
Mitigation
Fix versions: OTP 27.3.4.12, 28.5.0.1, and 29.0.1, which include commits b3870e02405c709a872b01ba6086065620cdfe76 and 7995f1fdaee3da569bb810358ce0f546471d169b that add validity period checks [3][4]. Users should upgrade to these or later versions. No workaround is documented; affected systems should apply the patch as soon as possible.
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.16, <1.17.1.3; >=1.20, <1.20.3.1; >=1.21, <1.21.1
Patches
2b3870e02405cpublic_key: Validate OCSP responder certificate validity period
3 files changed · +59 −2
lib/public_key/src/pubkey_cert.erl+2 −1 modified@@ -47,7 +47,8 @@ match_name/3, extensions_list/1, cert_auth_key_id/1, - time_str_2_gregorian_sec/1 + time_str_2_gregorian_sec/1, + parse_and_check_validity_dates/1 ]). %% Generate test data
lib/public_key/src/pubkey_ocsp.erl+11 −0 modified@@ -218,6 +218,17 @@ is_responder_cert({byKey, Key}, #cert{otp = Cert}) -> is_authorized_responder(CombinedResponderCert = #cert{otp = ResponderCert}, IssuerCert, IsTrustedResponderFun) -> + case pubkey_cert:parse_and_check_validity_dates(ResponderCert) of + ok -> + check_responder_authorization(CombinedResponderCert, + ResponderCert, IssuerCert, + IsTrustedResponderFun); + _ExpiredOrError -> + not_authorized_responder + end. + +check_responder_authorization(CombinedResponderCert, ResponderCert, + IssuerCert, IsTrustedResponderFun) -> Case1 = %% the CA who issued the certificate in question signed the %% response
lib/public_key/test/pubkey_ocsp_SUITE.erl+46 −1 modified@@ -107,7 +107,8 @@ %% Common Test interface functions ----------------------------------- %%-------------------------------------------------------------------- all() -> - [ocsp_test, designated_responder]. + [ocsp_test, designated_responder, + ocsp_responder_cert_expired, ocsp_responder_cert_not_yet_valid]. groups() -> []. @@ -262,6 +263,44 @@ designated_responder(Config) when is_list(Config) -> NonceExt, CACert, IsNotTrustedFun), ok. +%%-------------------------------------------------------------------- +ocsp_responder_cert_expired() -> + [{doc, "Verify that an expired responder certificate is rejected. " + "RFC 5280 Section 4.1.2.5 requires validity period checking."}]. +ocsp_responder_cert_expired(Config) when is_list(Config) -> + {ok, OcspResponse} = + pubkey_ocsp:decode_response(?OCSP_RESPONSE_DER), + ExpiredCert = set_cert_validity(?ISSUER_CERT, + {generalTime, "20180101000000Z"}, + {generalTime, "20200101000000Z"}), + IsTrustedFun = fun(_) -> true end, + {error, ocsp_responder_cert_not_found} = + pubkey_ocsp:verify_response(OcspResponse, + [#cert{otp = ExpiredCert}], + ?NONCE, + ?ISSUER_CERT, + IsTrustedFun), + ok. + +%%-------------------------------------------------------------------- +ocsp_responder_cert_not_yet_valid() -> + [{doc, "Verify that a not-yet-valid responder certificate is rejected. " + "RFC 5280 Section 4.1.2.5 requires validity period checking."}]. +ocsp_responder_cert_not_yet_valid(Config) when is_list(Config) -> + {ok, OcspResponse} = + pubkey_ocsp:decode_response(?OCSP_RESPONSE_DER), + FutureCert = set_cert_validity(?ISSUER_CERT, + {generalTime, "21000101000000Z"}, + {generalTime, "21100101000000Z"}), + IsTrustedFun = fun(_) -> true end, + {error, ocsp_responder_cert_not_found} = + pubkey_ocsp:verify_response(OcspResponse, + [#cert{otp = FutureCert}], + ?NONCE, + ?ISSUER_CERT, + IsTrustedFun), + ok. + %%-------------------------------------------------------------------- %% Helpers ----------------------------------------------------------- %%-------------------------------------------------------------------- @@ -357,3 +396,9 @@ build_ocsp_response(IssuerName, IssuerKey, NonceExt, SignKey) -> parameters = asn1_NOVALUE}, signature = Signature, certs = asn1_NOVALUE}. + +set_cert_validity(OtpCert, NotBefore, NotAfter) -> + TBS = OtpCert#'OTPCertificate'.tbsCertificate, + NewValidity = #'Validity'{notBefore = NotBefore, notAfter = NotAfter}, + NewTBS = TBS#'OTPTBSCertificate'{validity = NewValidity}, + OtpCert#'OTPCertificate'{tbsCertificate = NewTBS}.
7995f1fdaee3public_key: Validate OCSP responder certificate validity period
3 files changed · +59 −2
lib/public_key/src/pubkey_cert.erl+2 −1 modified@@ -47,7 +47,8 @@ match_name/3, extensions_list/1, cert_auth_key_id/1, - time_str_2_gregorian_sec/1 + time_str_2_gregorian_sec/1, + parse_and_check_validity_dates/1 ]). %% Generate test data
lib/public_key/src/pubkey_ocsp.erl+11 −0 modified@@ -216,6 +216,17 @@ is_responder_cert({byKey, Key}, #cert{otp = Cert}) -> is_authorized_responder(CombinedResponderCert = #cert{otp = ResponderCert}, IssuerCert, IsTrustedResponderFun) -> + case pubkey_cert:parse_and_check_validity_dates(ResponderCert) of + ok -> + check_responder_authorization(CombinedResponderCert, + ResponderCert, IssuerCert, + IsTrustedResponderFun); + _ExpiredOrError -> + not_authorized_responder + end. + +check_responder_authorization(CombinedResponderCert, ResponderCert, + IssuerCert, IsTrustedResponderFun) -> Case1 = %% the CA who issued the certificate in question signed the %% response
lib/public_key/test/pubkey_ocsp_SUITE.erl+46 −1 modified@@ -105,7 +105,8 @@ %% Common Test interface functions ----------------------------------- %%-------------------------------------------------------------------- all() -> - [ocsp_test, designated_responder]. + [ocsp_test, designated_responder, + ocsp_responder_cert_expired, ocsp_responder_cert_not_yet_valid]. groups() -> []. @@ -260,6 +261,44 @@ designated_responder(Config) when is_list(Config) -> NonceExt, CACert, IsNotTrustedFun), ok. +%%-------------------------------------------------------------------- +ocsp_responder_cert_expired() -> + [{doc, "Verify that an expired responder certificate is rejected. " + "RFC 5280 Section 4.1.2.5 requires validity period checking."}]. +ocsp_responder_cert_expired(Config) when is_list(Config) -> + {ok, OcspResponse} = + pubkey_ocsp:decode_response(?OCSP_RESPONSE_DER), + ExpiredCert = set_cert_validity(?ISSUER_CERT, + {generalTime, "20180101000000Z"}, + {generalTime, "20200101000000Z"}), + IsTrustedFun = fun(_) -> true end, + {error, ocsp_responder_cert_not_found} = + pubkey_ocsp:verify_response(OcspResponse, + [#cert{otp = ExpiredCert}], + ?NONCE, + ?ISSUER_CERT, + IsTrustedFun), + ok. + +%%-------------------------------------------------------------------- +ocsp_responder_cert_not_yet_valid() -> + [{doc, "Verify that a not-yet-valid responder certificate is rejected. " + "RFC 5280 Section 4.1.2.5 requires validity period checking."}]. +ocsp_responder_cert_not_yet_valid(Config) when is_list(Config) -> + {ok, OcspResponse} = + pubkey_ocsp:decode_response(?OCSP_RESPONSE_DER), + FutureCert = set_cert_validity(?ISSUER_CERT, + {generalTime, "21000101000000Z"}, + {generalTime, "21100101000000Z"}), + IsTrustedFun = fun(_) -> true end, + {error, ocsp_responder_cert_not_found} = + pubkey_ocsp:verify_response(OcspResponse, + [#cert{otp = FutureCert}], + ?NONCE, + ?ISSUER_CERT, + IsTrustedFun), + ok. + %%-------------------------------------------------------------------- %% Helpers ----------------------------------------------------------- %%-------------------------------------------------------------------- @@ -356,3 +395,9 @@ build_ocsp_response(IssuerName, IssuerKey, NonceExt, SignKey) -> parameters = asn1_NOVALUE}, signature = Signature, certs = asn1_NOVALUE}. + +set_cert_validity(OtpCert, NotBefore, NotAfter) -> + TBS = OtpCert#'OTPCertificate'.tbsCertificate, + NewValidity = #'Validity'{notBefore = NotBefore, notAfter = NotAfter}, + NewTBS = TBS#'OTPTBSCertificate'{validity = NewValidity}, + OtpCert#'OTPCertificate'{tbsCertificate = NewTBS}.
Vulnerability mechanics
Root cause
"Missing validity period (notBefore/notAfter) check on the OCSP responder certificate in is_authorized_responder/3 allows expired or not-yet-valid certificates to be accepted as authorized responders."
Attack vector
An attacker who obtains the private key of an expired CA-designated OCSP responder certificate can forge OCSP responses. The functions pubkey_ocsp:verify_response/5 and pubkey_ocsp:is_authorized_responder/3 in lib/public_key/src/pubkey_ocsp.erl do not validate the responder certificate's validity period [patch_id=2662153][patch_id=2662152]. For TLS clients using OCSP stapling, a malicious server can present a revoked TLS certificate together with a forged OCSP response signed by an expired responder key, and the client will accept the revoked certificate as valid. Applications calling public_key:pkix_ocsp_validate/5 directly are also affected; server-side client certificate validation using this API may allow authentication bypass with a revoked client certificate [ref_id=1][ref_id=2].
Affected code
The vulnerability is in lib/public_key/src/pubkey_ocsp.erl, specifically in the functions pubkey_ocsp:verify_response/5 and pubkey_ocsp:is_authorized_responder/3 [patch_id=2662153][patch_id=2662152]. The patch also adds the exported function parse_and_check_validity_dates/1 to lib/public_key/src/pubkey_cert.erl.
What the fix does
The patch adds a call to pubkey_cert:parse_and_check_validity_dates(ResponderCert) at the start of is_authorized_responder/3 [patch_id=2662153][patch_id=2662152]. If the validity check fails (expired or not-yet-valid), the function returns not_authorized_responder instead of proceeding to evaluate responder authorization. The existing authorization logic was refactored into a new helper, check_responder_authorization/4, which is only reached when the validity check passes. This enforces RFC 5280 Section 4.1.2.5 and RFC 6960 Section 4.2.2.2 requirements [ref_id=1][ref_id=2].
Preconditions
- inputAttacker must possess the private key of an expired CA-designated OCSP responder certificate.
- networkFor TLS stapling attack: attacker controls a malicious or compromised TLS server.
- configTarget must use OCSP stapling via the ssl application or call public_key:pkix_ocsp_validate/5 directly.
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-42791.htmlnvd
- github.com/erlang/otp/commit/7995f1fdaee3da569bb810358ce0f546471d169bnvd
- github.com/erlang/otp/commit/b3870e02405c709a872b01ba6086065620cdfe76nvd
- github.com/erlang/otp/security/advisories/GHSA-cjxj-wj6x-3fffnvd
- osv.dev/vulnerability/EEF-CVE-2026-42791nvd
- www.erlang.org/doc/system/versions.htmlnvd
News mentions
0No linked articles in our index yet.