Skip to content

Rewrite cert_expired to root_cert_expired for cross-sign recovery#863

Merged
benoitc merged 5 commits into
masterfrom
fix/cross-sign-cert-expired-recovery
Jun 3, 2026
Merged

Rewrite cert_expired to root_cert_expired for cross-sign recovery#863
benoitc merged 5 commits into
masterfrom
fix/cross-sign-cert-expired-recovery

Conversation

@benoitc
Copy link
Copy Markdown
Owner

@benoitc benoitc commented Jun 3, 2026

Rebase of #860 (by @mrnovalles) on master to get the ARM64 CI fix, with extra tests.

When the chain is anchored by an expired cross-signed root (Let's Encrypt ISRG Root X2 signed by the expired X1), OTP can recover by finding another valid root with the same key, but only when path validation reports root_cert_expired. hackney uses ssl_verify_hostname, which returns cert_expired and stops the handshake before the recovery runs.

The fix wraps the verify_fun to rewrite cert_expired into root_cert_expired. Other events go to ssl_verify_hostname as before.

Safe: if no valid alternative root exists, OTP fails again as cert_expired, so an expired leaf or intermediate still fails. Partial chains keep working: partial_chain stays, and valid, valid_peer and extension events pass through. Tests added for both.

Supersedes #860.

mrnovalles and others added 5 commits June 3, 2026 17:43
OTP's ssl_certificate:find_cross_sign_root_paths/4 recovers from an
expired cross-signed root by locating an alternative valid root with
the same public key in the trust store. It only triggers when path
validation reports root_cert_expired.

ssl_verify_hostname:verify_fun/3 returns {fail, {bad_cert, cert_expired}}
verbatim, which terminates the handshake before OTP's recovery can run.

Wrap the verify_fun in check_hostname_opts/1 to intercept cert_expired
and rewrite it to root_cert_expired. All other events are delegated to
ssl_verify_hostname unchanged, so hostname checking is unaffected.

Confirmed against rest.fra-01.braze.eu (Let's Encrypt chain containing
the ISRG Root X2 cross-signed by ISRG Root X1, expired 2025-09-15)
using hackney 1.25.0, certifi 2.15.0, OTP 27.
…_fun

Add regression tests that the verify_fun wrapper delegates valid and
extension events to ssl_verify_hostname unchanged, and that the
partial_chain option is preserved, so valid and partial certificate
chains still verify after the cert_expired rewrite.
@benoitc benoitc merged commit 52176e0 into master Jun 3, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants