Skip to content

Release 4.5.post1: CVE-2020-25658 for the Python 2.7 line - #4

Merged
icanhasmath merged 7 commits into
version-4.5.xfrom
4.5.post1-release
Nov 10, 2026
Merged

icanhasmath merged 7 commits into
version-4.5.xfrom
4.5.post1-release

Conversation

@icanhasmath

@icanhasmath icanhasmath commented Nov 10, 2026 •

Copy link
Copy Markdown

rsa 4.5.post1: Python 2.7 security release

rsa 4.5 is the last release that supports Python 2.7. It is still exposed to CVE-2020-25658 (GHSA-xrx6-fmxq-rjj2, high), a Bleichenbacher-style timing oracle in PKCS#1 v1.5 decryption. Upstream fixed it only in 4.7, which needs Python 3. The fix was started on version-4.3.x in 2023 (#1, #2) but never released; that branch still says version 4.3.

This PR is based on the version-4.5 tag (new base branch version-4.5.x), so its history contains upstream's 4.5 rather than the 4.3 it was re-tagged from. On top of it are the two 2023 commits from version-4.3.x, cherry-picked, then this release's four. It replaces #3: same tree as #3's head up to 807eb28; 5207034 was added after self-review.

Commit What
6ba6f6e, cd3b88f The 2023 work from version-4.3.x (#1: upstream dae8ce0; #2: drop the typing import), cherry-picked.
03da125 CVE-2020-25658. Brings decrypt() to upstream 4.7's final form. #1 ported upstream dae8ce0 only; this adds 341e5c4 (the public length check raises at once) and f254895 (find() instead of index()/ValueError, so a missing separator takes the same path). Drops the unused import sys from #1. Adds regression tests that every malformed cleartext still raises DecryptionError.
579d820 Upstream sybrenstuvel#164 (f878c37): reject padding shorter than 8 bytes (RFC 8017 7.2.2 step 3). Upstream's test, without its Python 3 annotation.
5207034 Python < 2.7.7. hmac.compare_digest arrived in 2.7.7, but python_requires accepts any 2.7, so on 2.7.0 to 2.7.6 the backport made import rsa fail. rsa._compat now supplies a constant-time fallback when hmac lacks it; pkcs1 imports it from there.
6aacc00 Review follow-up: accurate DecryptionFailureTest docstring, drops a test that duplicated ExtraZeroesTest.test_prepend_zeroes, dates the 4.5.post1 changelog entry and __date__.
807eb28 Version 4.5.post1 in rsa/__init__.py and setup.py. The 4.3 & 4.5 changelog section, which #1 had rewritten, is restored, with a 4.5.post1 section on top.

Advisories against rsa

Advisory Range Status in 4.5.post1
CVE-2016-1494 / GHSA-8rjr-6qq5-pj9p < 3.3 Fixed upstream before the base
CVE-2020-13757 / GHSA-537h-rv9q-vvph < 4.1 Fixed in the 4.5 base (3283b12)
CVE-2020-25658 / GHSA-xrx6-fmxq-rjj2 >= 2.1, < 4.7 Patched here

OSV lists nothing against 4.7 through 4.9.1, so no later upstream fix needs carrying.

Verification (Python 2.7.18)

  • tests/: 99 passed (Python 2.7.18). Upstream 4.5 has 92; the 7 new tests account for the rest (4 in test_pkcs1.py, 3 for the compare_digest fallback).
  • Fallback exercised by deleting hmac.compare_digest before import rsa on 2.7.18: pkcs1 picks up _compare_digest and an encrypt/decrypt round trip works. Not run on a real 2.7.0-2.7.6 interpreter.
  • Mutations on a scratch copy: dropping the marker check fails test_bad_marker; sep_idx < 0 fails test_too_little_padding; dropping the separator check fails test_no_separator and test_too_little_padding; making the fallback always return True fails TestCompareDigest.
  • PaddingSizeTest fails without the pkcs1.py change and passes with it.
  • The CVE-2020-25658 tests check behaviour: a timing fix cannot be shown red-to-green in a unit test. They pin that the restructured checks still reject a bad marker, a missing separator and an over-long ciphertext, and that a valid block decrypts.
  • setup.py sdist bdist_wheel gives rsa-4.5.post1.tar.gz and rsa-4.5.post1-py2.py3-none-any.whl. rsa.__version__ is 4.5.post1.

After merge: tag version-4.5.post1 (the repo's tag style), a GitHub release (--latest=false), inventory rsa 4.5.0.1, and a CVE-scanner override row for CVE-2020-25658.

rickprice and others added 5 commits November 9, 2026 18:32
Fix sybrenstuvel#165: CVE-2020-25658 - Bleichenbacher-style timing oracle

Use as many constant-time comparisons as practical in the
`rsa.pkcs1.decrypt` function.

`cleartext.index(b'\x00', 2)` will still be non-constant-time. The
alternative would be to iterate over all the data byte by byte in
Python, which is several orders of magnitude slower. Given that a
perfect constant-time implementation is very hard or even impossible to
do in Python [1], I chose the more performant option here.

[1]: https://securitypitfalls.wordpress.com/2018/08/03/constant-time-compare-in-python/

(cherry picked from commit 81cda70)
The fork's earlier backport (PR #1) ported upstream dae8ce0 only. Upstream
4.7 refined it in two follow-ups, ported here:

- 341e5c4: the ciphertext length check raises at once. Length and block
  size are public, so they need no side-channel-free comparison.
- f254895: bytes.find() instead of bytes.index() with a ValueError
  handler, so a missing separator takes the same path as a present one.

Also drops the unused 'import sys' that PR #1 added, and adds regression
tests that every malformed cleartext (bad 00 02 marker, no separator,
over-long ciphertext) still raises DecryptionError through the combined
check. Python 2.7: hmac.compare_digest exists from 2.7.7.

(cherry picked from commit ca9f489)
… (upstream f878c37)

RFC 8017 section 7.2.2 step 3 requires at least 8 bytes of padding, so the
00 separator must sit at index 10 or later. decrypt() accepted any
separator after the marker. Part of upstream 4.7's decryption hardening
alongside the CVE-2020-25658 fix. Upstream's test, without its Python 3
type annotation.

(cherry picked from commit 4d9889e)
… 2.7 line

Sets the version to 4.5.post1 and restores the 4.3 & 4.5 changelog section
that the CVE-2020-25658 backport (cherry-picked from version-4.3.x) had
rewritten, with a 4.5.post1 section on top.

(cherry picked from commit 987fe91)

@ezequielp-activestate ezequielp-activestate left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve: 0 blockers.

🔴 Blockers

None.

Checked

  • The base 4834b903bb is the upstream version-4.5 tag. This PR's tree is byte-identical to #3's head (987fe91).
  • decrypt() in rsa/pkcs1.py now matches upstream 4.7–4.9 line for line:
  • hmac.compare_digest exists on Python 2.7.7+.
  • Upstream history from version-4.5 to version-4.9 for key.py, pkcs1.py, core.py and transform.py has no further security fixes to carry. The threading fix (88418f0) and the pickle fix (eb6ddb6) are for blinding-factor caching, which 4.7 introduced and 4.5 doesn't have.
  • OSV for rsa 4.5 lists only CVE-2020-25658 (GHSA-xrx6-fmxq-rjj2 / PYSEC-2020-100).
  • Python 2.7:
    • PR: 97 passed.
    • Base with this PR's test_pkcs1.py added: only PaddingSizeTest::test_too_little_padding fails (DecryptionError not raised).
  • setup.py sdist bdist_wheel gives rsa-4.5.post1.tar.gz and rsa-4.5.post1-py2.py3-none-any.whl, and rsa.__version__ is 4.5.post1.

⚪ Notes

  • Upstream 8ed5071 ("Choose blinding factor relatively prime to N") isn't included. It's a correctness fix for a very unlikely case, not a security fix, so it's optional.

Not assessed

  • I didn't measure decrypt timing. As the PR says, the CVE tests check behaviour, not timing.

hmac.compare_digest arrived in Python 2.7.7, but setup.py accepts any
2.7. On 2.7.0 to 2.7.6 the CVE-2020-25658 backport's import made
`import rsa` fail outright. rsa._compat now supplies a constant-time
fallback when hmac lacks it, and pkcs1 imports it from there.

@swagile1 swagile1 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 blocker: a test docstring that claims more than the test shows. The CVE fix itself is a faithful copy of upstream and holds up under testing. Reviewed at head 807eb28 against version-4.5.x.

Evidence

  • decrypt() matches upstream version-4.7 line for line, apart from type annotations. Upstream dae8ce0, f254895, 341e5c4 and f878c37 all exist and are contained in version-4.7.
  • Full suite: 97 passed on Python 2.7.18 and 97 on Python 3.8, both in Docker.
  • Mutation tests: I broke each new check in a scratch copy, and the right test failed each time:
    • padding check set back to < 0 → PaddingSizeTest fails
    • marker check set to False → test_bad_marker fails
    • early length check deleted → test_prepend_zeroes and test_crypto_longer_than_block fail
  • Each commit on its own passes on 2.7 (92 → 92 → 96 → 97 tests).
  • No check from the old code was dropped: the marker, separator and length checks all survive.

🔴 Blockers

  • tests/test_pkcs1.py:188 [importance 3 / effort 1]: the docstring says "every malformed cleartext still fails, through one combined check." test_crypto_longer_than_block contradicts it: since 341e5c4, that case raises at the early length check (rsa/pkcs1.py:241), not at the combined check. "Every" also overreaches: short padding is tested in PaddingSizeTest. Suggested wording: "CVE-2020-25658: each malformed block still raises DecryptionError after the checks were restructured."

🟡 Should fix (follow-up, not blocking)

  • No CI runs on this fork [2 / 2]: .travis.yml is the only CI config, and the branch reports no checks. The security tag will be cut on a local run. A small GitHub Actions job with a 2.7 container would cover this release and the next backport. This was true before this PR.

⚪ Nits

  • rsa/pkcs1.py:33 [1 / 1]: hmac.compare_digest arrived in Python 2.7.7, but setup.py:53 still allows >=2.7. On 2.7.0–2.7.6, import rsa would now fail outright. Raising the floor to >=2.7.7 would match the code.
  • tests/test_pkcs1.py:193 [1 / 1]:
    • encrypt_raw duplicates PaddingSizeTest.encrypt_with_short_padding.
    • test_crypto_longer_than_block overlaps ExtraZeroesTest.test_prepend_zeroes.
    • test_valid_padding_decrypts overlaps BinaryTest.test_enc_dec.
  • CHANGELOG.txt:4, rsa/__init__.py:31 [1 / 1]: the 4.5.post1 changelog heading has no release date, unlike every other entry, and __date__ still says 2020-06-12.
  • Commit sequence [1 / 1]:
    • 6ba6f6e imports typing. Bare Python 2.7 has no typing module, so import rsa fails at that commit.
    • The same commit adds a stray ## Version 4.4 & 4.6 heading to the changelog, which 807eb28 later repairs.
    • Squashing 6ba6f6e and cd3b88f together would make every commit work for git bisect. Keeping them separate for provenance is a reasonable choice too.

Worth knowing for the release

A consumer pinned to rsa==4.5 will not pick up 4.5.post1, because under Python's version rules ==4.5 excludes post-releases. Only >=/~= ranges, or builds from our inventory, get the fix.

Not assessed

  • The sdist/wheel build claim in the PR body.
  • Actual constant-time behavior: no unit test can measure it, so that claim rests on parity with upstream.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

An unresolved spelling nit remains in rsa/pkcs1.py:254: change “preceeds” to “precedes” in the marker explanation.

1 open issue
What changed in this PR

Prepares rsa 4.5.post1 as a Python 2.7 security release, completing the backport of upstream PKCS#1 v1.5 decryption fixes.

Changes:

  • Combines malformed-padding checks and rejects padding shorter than eight bytes.
  • Adds a compare_digest fallback for Python versions before 2.7.7 and regression tests.
  • Updates package versions and documents the security release.
File Description
tests/​test_pkcs1.py Adds valid-block and malformed-padding regression tests.
tests/​test_compat.py Tests the digest-comparison fallback.
setup.py Sets package version to 4.5.post1.
rsa/​pkcs1.py Backports decryption checks and minimum padding validation.
rsa/​_compat.py Provides a fallback when hmac.compare_digest is unavailable.
rsa/​__init__.py Updates the exposed library version.
CHANGELOG.txt Documents the security backports.

Review effort: Balanced

Comment thread rsa/pkcs1.py
# padding from the actual message. The padding should be at least 8 bytes
# long (see https://tools.ietf.org/html/rfc8017#section-7.2.2 step 3), which
# means the separator should be at least at index 10 (because of the
# `\x00\x02` marker that preceeds it).

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving this as is on purpose. This comment is copied verbatim from upstream f878c37, which still spells it "preceeds" in 4.7 to 4.9. Keeping decrypt() byte-identical to upstream lets anyone check the backport with a plain diff against version-4.7.

@swagile1

Copy link
Copy Markdown

Correction to my review above: I reviewed 807eb28, and 5207034 was pushed before the review went up. I've now read and run the new commit:

  • compare_digest nit: fixed. rsa._compat falls back to a constant-time pure-Python comparison when hmac has no compare_digest. I forced that branch by deleting hmac.compare_digest before import, and rsa._compat picked the fallback. On Python 2.7.18 the full suite passes: 100 tests.
  • The blocker still stands. The DecryptionFailureTest docstring at tests/test_pkcs1.py:187 is unchanged.
  • The other nits are unchanged: the duplicate test helper and overlapping tests, the undated changelog heading and __date__, and the commit-sequence notes.

…ate test, date the release

- The class docstring now says what its tests show: a bad marker or a
  missing separator fails through the combined check. A ciphertext longer
  than the key fails earlier, at the length check.
- Drop test_crypto_longer_than_block: ExtraZeroesTest.test_prepend_zeroes
  already covers it, and still fails when the length check is removed.
- Note why test_valid_padding_decrypts stays: it is the control for block().
- Date the 4.5.post1 changelog entry and __date__ (2026-11-10).
@icanhasmath

Copy link
Copy Markdown
Author

Thanks both. 6aacc00 addresses the review. Tests: 99 passed on Python 2.7.18.

@swagile1

  • 🔴 Docstring at tests/test_pkcs1.py:188: fixed.
    • The DecryptionFailureTest docstring now names only what its tests show: a bad marker or a missing separator fails through the combined check.
    • It points to where the other cases are covered: a ciphertext longer than the key is caught earlier, at the length check (ExtraZeroesTest), and short padding by PaddingSizeTest.
  • ⚪ Overlapping tests:
    • test_crypto_longer_than_block: dropped. It duplicated ExtraZeroesTest.test_prepend_zeroes. I re-ran the mutation check: with the length check at rsa/pkcs1.py:240 disabled, test_prepend_zeroes still fails.
    • test_valid_padding_decrypts: kept, with a comment. It is the control for block(). It proves that the hand-built block decrypts, so the failure tests fail because of the field they break, not because of the builder. BinaryTest.test_enc_dec goes through encrypt() and can't show that.
    • encrypt_raw vs encrypt_with_short_padding: kept. PaddingSizeTest is upstream f878c37 verbatim, and I'd rather not edit upstream code to share a helper.
  • ⚪ Changelog date and __date__: fixed. Both now say 2026-11-10. If the tag lands on a later day, I'll update them in the tag commit.
  • ⚪ compare_digest on 2.7.0–2.7.6: fixed in 5207034, as you confirmed. It's a fallback rather than a >=2.7.7 floor, so this release keeps the interpreter range that 4.5 accepted.
  • ⚪ Commit sequence: keeping 6ba6f6e and cd3b88f separate. They are the fork's 2023 commits (Fix for CVE-2020-25658 #1, Remove "Typing" because it breaks on Python2 #2), cherry-picked with -x for provenance. Every commit from 03da125 on imports and passes.
  • 🟡 No CI on this fork: agreed. This was true before this PR, so I'll handle it as a follow-up: a 2.7 container job in GitHub Actions to replace .travis.yml.
  • rsa==4.5 pins: good point. Our inventory and catalog build 4.5.post1 directly, so customers get the fix there. The release notes will say that an exact ==4.5 pin has to change to ==4.5.post1 or ~=4.5.

@ezequielp-activestate: on upstream 8ed5071 ("Choose blinding factor relatively prime to N"), the base already has it. It's 3bf7b2e, an ancestor of the version-4.5 tag, and its rsa/key.py change (_get_blinding_factor()) is the same as upstream's apart from the Python 3 return annotation. So nothing is missing. Could you re-approve? Your approval was on 807eb28, and the head has moved twice since.

@swagile1 swagile1 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approve: 0 blockers. Re-review at head 6aacc00; my last review saw 807eb28 (with a correction comment for 5207034). The only change since is 6aacc00, whose diff I read in full; I checked the earlier tree only through the test runs below.

🔴 Blockers

None.

Earlier findings, checked against the code

  • 🔴 DecryptionFailureTest docstring: fixed. tests/test_pkcs1.py:188 now claims only a bad marker and a missing separator. Both fail through anything_bad (rsa/pkcs1.py:257). A missing separator gives find() = -1, so sep_idx_bad catches it. The docstring sends the length case to ExtraZeroesTest and short padding to PaddingSizeTest, which is correct.
  • ⚪ Duplicate test_crypto_longer_than_block: dropped. I disabled the length check at rsa/pkcs1.py:240 on a scratch copy. ExtraZeroesTest::test_prepend_zeroes still fails (DecryptionError not raised, tests/test_pkcs1.py:93), so nothing lost coverage.
  • ⚪ test_valid_padding_decrypts and encrypt_raw: kept, and the reasons hold. The control test proves the hand-built block() decrypts. That is the premise of the two failure tests, and test_enc_dec cannot show it. PaddingSizeTest is upstream f878c37 verbatim.
  • ⚪ Changelog date and __date__: fixed (CHANGELOG.txt:4, rsa/__init__.py:31).
  • ⚪ compare_digest before 2.7.7: fixed in 5207034, as noted in my earlier comment.
  • ⚪ Commit sequence: keeping 6ba6f6e and cd3b88f separate for provenance is fine.
  • 🟡 No CI on the fork: now tracked as PROD-2446.
  • Copilot's "preceeds" thread: leaving it is the right call. version-4.7:rsa/pkcs1.py:269 spells it the same way. Apart from the type annotations on the signature, decrypt() diffs clean against version-4.7.

Evidence (Python 2.7.18, python:2.7-slim)

  • Full suite: 99 passed.
  • Mutations, one at a time, on tests/test_pkcs1.py. Each fails exactly one test:
    • length check off: test_prepend_zeroes fails;
    • sep_idx_bad = False: one test fails;
    • cleartext_marker_bad = False: one test fails.
  • Upstream 8ed5071 ("Choose blinding factor relatively prime to N") is 3bf7b2e, an ancestor of version-4.5, so the base already carries it.

⚠️ Not assessed

  • Decrypt timing was not measured; the tests pin behavior only.
  • The compare_digest fallback has not run on a real 2.7.0–2.7.6 interpreter.
  • The THR review tooling handles only TheHomeRepot PRs, so this review was done by hand against its checklist.

@icanhasmath
icanhasmath merged commit 52e2215 into version-4.5.x Nov 10, 2026
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.

5 participants