VYPR
Medium severityNVD Advisory· Published May 27, 2026

CVE-2026-42791

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
  • Erlang/Otpllm-fuzzy
    Range: >=27.0, <27.3.4.12; >=28.0, <28.5.0.1; >=29.0, <29.0.1

Patches

2
b3870e02405c

public_key: Validate OCSP responder certificate validity period

https://github.com/erlang/otpJakub WitczakApr 20, 2026via body-scan
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}.
    
7995f1fdaee3

public_key: Validate OCSP responder certificate validity period

https://github.com/erlang/otpJakub WitczakApr 20, 2026via body-scan
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

News mentions

0

No linked articles in our index yet.