Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CVE-2020-25658 - Bleichenbacher-style timing oracle in PKCS#1 v1.5 decryption code #165

Closed
tomato42 opened this issue Oct 26, 2020 · 32 comments

Comments

@tomato42
Copy link

tomato42 commented Oct 26, 2020

Current PKCS#1 v1.5 decryption code:

python-rsa/rsa/pkcs1.py

Lines 246 to 267 in 6f59ff0

blocksize = common.byte_size(priv_key.n)
encrypted = transform.bytes2int(crypto)
decrypted = priv_key.blinded_decrypt(encrypted)
cleartext = transform.int2bytes(decrypted, blocksize)
# Detect leading zeroes in the crypto. These are not reflected in the
# encrypted value (as leading zeroes do not influence the value of an
# integer). This fixes CVE-2020-13757.
if len(crypto) > blocksize:
raise DecryptionError('Decryption failed')
# If we can't find the cleartext marker, decryption failed.
if cleartext[0:2] != b'\x00\x02':
raise DecryptionError('Decryption failed')
# Find the 00 separator between the padding and the message
try:
sep_idx = cleartext.index(b'\x00', 2)
except ValueError:
raise DecryptionError('Decryption failed')
return cleartext[sep_idx + 1:]

performs the checks on the decrypted value in turn, aborting as soon as first error is found, it also raises an exception in case of errors. This likely provides enough of a timing side channel to mount a Bleichenbacher style attack.

While it's unlikely that a completely side-channel free implementation is possible (see https://securitypitfalls.wordpress.com/2018/08/03/constant-time-compare-in-python/ ), it should be possible to minimise the side-channel by making at least the execution path the same irrespective of previous checks and by providing an API that returns a randomly generated secret in case of error (instead of leaking the timing side-channel by rising an exception) for uses that decrypted value directly as an input to a hash or use it as a symmetric key.

@tomato42
Copy link
Author

The code is basically unchanged for at least 10 years:

python-rsa/rsa/pkcs1.py

Lines 120 to 135 in 3f8c551

blocksize = common.byte_size(priv_key['n'])
encrypted = transform.bytes2int(crypto)
decrypted = core.decrypt_int(encrypted, priv_key['d'], priv_key['n'])
cleartext = transform.int2bytes(decrypted, blocksize)
# If we can't find the cleartext marker, decryption failed.
if cleartext[0:2] != '\x00\x02':
raise ValueError('Decryption failed')
# Find the 00 separator between the padding and the message
try:
sep_idx = cleartext.index('\x00', 2)
except ValueError:
raise ValueError('Decryption failed')
return cleartext[sep_idx+1:]

so we can be reasonably sure that all released versions since 2.1 (the first version that included RSA decryption API) are vulnerable

@ran-isenberg
Copy link

Hey guys, this seems like a critical security issue. We've been working with python-jose which depends on this repo.
Our security scanner, Snyk, tagged this repo with a high level risk security issue.
Is somebody working on this?
Thanks in advance

@tomato42
Copy link
Author

  1. it's not critical, it's medium severity, though we would need to calculate CVSS to be sure. That being said, I haven't seen any Bleichenbacher CVEs scored high, let alone critical
  2. python-jose depends on python-rsa, but it will not use it if better libraries are available, you should use python-jose with pyca/cryptography, then python-rsa code will be unused and unexploitable
  3. nobody is working on this; we've decided with Sybren to make it public specifically so that somebody else could start the work on this

@ran-isenberg
Copy link

Thanks for the quick reply.

@avishayil
Copy link

Hi @tomato42

  1. it's not critical, it's medium severity, though we would need to calculate CVSS to be sure. That being said, I haven't seen any Bleichenbacher CVEs scored high, let alone critical
  2. python-jose depends on python-rsa, but it will not use it if better libraries are available, you should use python-jose with pyca/cryptography, then python-rsa code will be unused and unexploitable
  3. nobody is working on this; we've decided with Sybren to make it public specifically so that somebody else could start the work on this

Snyk apparently calculated the score here, see here: https://app.snyk.io/vuln/SNYK-PYTHON-RSA-1038401

@tomato42
Copy link
Author

For one, as it's timing based, so it's harder to exploit than issues like https://nvd.nist.gov/vuln/detail/CVE-2017-13098 (https://snyk.io/vuln/SNYK-JAVA-ORGBOUNCYCASTLE-32031)
or any of the other issues listed on https://robotattack.org/ page. It also allows exactly the same kind of attack as ROBOT, so impact to confidentiality or integrity should be similar as those issues.

So I don't think that rating is consistent with other similar issues.

@sybrenstuvel
Copy link
Owner

sybrenstuvel commented Nov 11, 2020

Thanks for the report & the interest, people, it's much appreciated.

How about this approach, would that be a proper fix for this issue?

def decrypt(crypto: bytes, priv_key: key.PrivateKey) -> bytes:
    blocksize = common.byte_size(priv_key.n)
    encrypted = transform.bytes2int(crypto)
    decrypted = priv_key.blinded_decrypt(encrypted)
    cleartext = transform.int2bytes(decrypted, blocksize)

    # Detect leading zeroes in the crypto. These are not reflected in the
    # encrypted value (as leading zeroes do not influence the value of an
    # integer). This fixes CVE-2020-13757.
    crypto_len_bad = len(crypto) > blocksize

    # If we can't find the cleartext marker, decryption failed.
    cleartext_marker_bad = not compare_digest(cleartext[:2], b'\x00\x02')

    # Find the 00 separator between the padding and the message
    try:
        sep_idx = cleartext.index(b'\x00', 2)
    except ValueError:
        sep_idx = -1
    sep_idx_bad = sep_idx < 0

    anything_bad = crypto_len_bad | cleartext_marker_bad | sep_idx_bad
    if anything_bad:
        raise DecryptionError('Decryption failed')

    return cleartext[sep_idx + 1:]

The weakest spot here I think is the call to cleartext.index(b'\x00', 2). I'm open to any suggestions as to how to get rid of it and replace it with something constant-time.

@tomato42
Copy link
Author

Thanks for the report & the interest, people, it's much appreciated.

How about this approach, would that be a proper fix for this issue?

def decrypt(crypto: bytes, priv_key: key.PrivateKey) -> bytes:
    blocksize = common.byte_size(priv_key.n)
    encrypted = transform.bytes2int(crypto)
    decrypted = priv_key.blinded_decrypt(encrypted)
    cleartext = transform.int2bytes(decrypted, blocksize)

    # Detect leading zeroes in the crypto. These are not reflected in the
    # encrypted value (as leading zeroes do not influence the value of an
    # integer). This fixes CVE-2020-13757.
    crypto_len_bad = len(crypto) > blocksize

    # If we can't find the cleartext marker, decryption failed.
    cleartext_marker_bad = not compare_digest(cleartext[:2], b'\x00\x02')

    # Find the 00 separator between the padding and the message
    try:
        sep_idx = cleartext.index(b'\x00', 2)
    except ValueError:
        sep_idx = -1
    sep_idx_bad = sep_idx < 0

    anything_bad = crypto_len_bad | cleartext_marker_bad | sep_idx_bad
    if anything_bad:
        # raise DecryptionError('Decryption failed')
        return cleartext[sep_idx + 1:]

    return cleartext[sep_idx + 1:]

The weakest spot here I think is the call to cleartext.index(b'\x00', 2). I'm open to any suggestions as to how to get rid of it and replace it with something constant-time.

the fact that cleartext.index() raises an exception will likely leak enough information to still mount an attack, using cleartext.find() would be better, but realistically, we need to traverse the whole cleartext reading each byte, as I'm quite sure cleartext.find() will exit the faster the earlier it finds the null byte

second, in case of failure we definitely can't return part of cleartext

@sybrenstuvel
Copy link
Owner

sybrenstuvel commented Nov 11, 2020

How about using before, separator, after = cleartext[:2].partition(b'\x00')? We can then inspect len(before) to test for correctness, and if correct, return after instead of cleartext[sep_idx+1:]. This should cause a copy of each byte, either into before or after, and the following correctness test is just a single integer comparison.

second, in case of failure we definitely can't return part of cleartext

Oh geesh, that shouldn't have been in here. The exception was just disabled to make it possible to do a timing test, and certainly won't be in the final code ;-)

@tomato42
Copy link
Author

How about using before, separator, after = cleartext[:2].partition(b'\x00')? We can then inspect len(before) to test for correctness, and if correct, return after instead of cleartext[sep_idx+1:]. This should cause a copy of each byte, either into before or after, and the following correctness test is just a single integer comparison.

yes, that does make for nice and clean code, but again, I don't expect it to have any better timing characteristic than cleartext.find()

for example of code that attempts side-channel free behaviour see here: https://github.com/openssl/openssl/blob/d8701e25239dc3d0c9d871e53873f592420f71d0/crypto/rsa/rsa_pk1.c#L170-L278

@sybrenstuvel
Copy link
Owner

Yeah, I made something similar, but looping over each byte and doing some operations there is much slower than what we have now.

@tomato42
Copy link
Author

from my previous work on this, I've noticed that subscripting strings and arrays is very expensive (like cleartext[i]), it's much faster to do for i, b in enumerate(cleartext); but yes, it won't be near as fast as native code

@tomato42
Copy link
Author

I'm quite sure that the code in dae8ce0 does not fix it.

@piyushpyoaknorth
Copy link

Hi,
We have the same issue as highlighted by Snyk. What should be the right solution for it?

@tomato42
Copy link
Author

Hi,
We have the same issue as highlighted by Snyk. What should be the right solution for it?

  1. audit your code for use of rsa decryption—if you don't use it you're not vulnerable; it's a false positive
  2. propose fixes to this library to fix it—though that's unlikely to be successful in the end, as I wrote above
  3. modify code you depend on so that it uses libraries that do provide side-channel free behaviour for RSA decryption

@piyushpyoaknorth
Copy link

Thanks @tomato42

  1. I checked our code, and we don't use the rsa library directly. But it is used indirectly from tls and jwt libraries. We use Django
  2. I am not too experienced in python, to fix/monkey patch the library. But will give it a shot :)
  3. I think this is what I can try and do.

How did you end up resolving this issue for you?

@tomato42
Copy link
Author

Thanks @tomato42

  1. I checked our code, and we don't use the rsa library directly. But it is used indirectly from tls and jwt libraries. We use Django

tls? You mean this https://pypi.org/project/tls/#description ??

I don't see jwt using python-rsa, unless we're not talking about https://github.com/GehirnInc/python-jwt

  1. I am not too experienced in python, to fix/monkey patch the library. But will give it a shot :)

I would advise against monkey-patching this code, writing side-channel secure cryptographic code is hard, you really should have any changes around it audited by a cryptographer

How did you end up resolving this issue for you?

I'm not using python-rsa, I just noticed that it's vulnerable and widely used

@piyushpyoaknorth
Copy link

piyushpyoaknorth commented Nov 17, 2020

Thanks @tomato42

  1. I checked our code, and we don't use the rsa library directly. But it is used indirectly from tls and jwt libraries. We use Django

tls? You mean this https://pypi.org/project/tls/#description ??

Yes, by https in Django for the servers

I don't see jwt using python-rsa, unless we're not talking about https://github.com/GehirnInc/python-jwt

Let me check once again.

  1. I am not too experienced in python, to fix/monkey patch the library. But will give it a shot :)

I would advise against monkey-patching this code, writing side-channel secure cryptographic code is hard, you really should have any changes around it audited by a cryptographer

Okay. Will do that for sure.

How did you end up resolving this issue for you?

I'm not using python-rsa, I just noticed that it's vulnerable and widely used

Ah. I see.

@attila123
Copy link

@sybrenstuvel Thanks for the fix! Could you please release a new version?
For me it was also picked up by a vulnerability scan (Anchore). Many people would benefit/save time by not having to deal with this vulnerability (even to research what it is). So it would be quite appreciated, I think. :) Thanks in advance :)

@tomato42
Copy link
Author

@attila123 #165 (comment)

@attila123
Copy link

@tomato42 thanks. Then it is quite misleading to have this issue closed.

@NicoleG25
Copy link

Hi,
Could someone please clarify then if this issue was addressed ? :)

Thanks in advance !

@attila123
Copy link

attila123 commented Dec 10, 2020

@NicoleG25 This is definitely not addressed. Why: the supposed fix dae8ce0 was committed on Nov 15, 2020, but the latest release of this library is 4.6 released at Jun 12, 2020 (see https://pypi.org/project/rsa/#history).

I am not a security expert, but as @tomato42 's #165 (comment) above: "I'm quite sure that the code in dae8ce0 does not fix it."

Good news is that at least google-auth does not use this problematic decrypt method (see above), so it can be considered false-positive in my use-case.

@sybrenstuvel
Copy link
Owner

Version 4.7 has just been released to pypi.

@dalesit
Copy link

dalesit commented Jan 15, 2021

Issue still being flagged in scanning for version 4.7

@sybrenstuvel
Copy link
Owner

What does that mean @dalesit ?

@batisteo
Copy link

batisteo commented Mar 4, 2021

I can’t say if @dalesit mean the same thing, but here’s what I’m getting from Gitlab Dependency Scanning (gemnasium-python)

dependency-scanning-python_rsa

@vavkamil
Copy link

vavkamil commented Jul 28, 2021

Info about the fixed version was never added into the gemnasium vulnerability database https://gitlab.com/gitlab-org/security-products/gemnasium-db/-/blob/master/pypi/rsa/CVE-2020-25658.yml#L11

mtremer pushed a commit to ipfire/ipfire-2.x that referenced this issue Feb 14, 2022
- Update from 4.0 to 4.8
- Update of rootfile
- Changelog
- Switch to [Poetry](https://python-poetry.org/) for dependency and release management.
- Compatibility with Python 3.10.
- Chain exceptions using `raise new_exception from old_exception`
  ([#157](sybrenstuvel/python-rsa#157))
- Added marker file for PEP 561. This will allow type checking tools in dependent projects
  to use type annotations from Python-RSA
  ([#136](sybrenstuvel/python-rsa#136)).
- Use the Chinese Remainder Theorem when decrypting with a private key. This
  makes decryption 2-4x faster
  ([#163](sybrenstuvel/python-rsa#163)).
- Fix picking/unpickling issue introduced in 4.7
  ([#173](sybrenstuvel/python-rsa#173))
- Fix threading issue introduced in 4.7
  ([#173](sybrenstuvel/python-rsa#173))
- Fix [#165](sybrenstuvel/python-rsa#165):
  CVE-2020-25658 - Bleichenbacher-style timing oracle in PKCS#1 v1.5 decryption
  code
- Add padding length check as described by PKCS#1 v1.5 (Fixes
  [#164](sybrenstuvel/python-rsa#164))
- Reuse of blinding factors to speed up blinding operations.
  Fixes [#162](sybrenstuvel/python-rsa#162).
- Declare & test support for Python 3.9
Version 4.4 and 4.6 are almost a re-tagged release of version 4.2. It requires
Python 3.5+. To avoid older Python installations from trying to upgrade to RSA
4.4, this is now made explicit in the `python_requires` argument in `setup.py`.
There was a mistake releasing 4.4 as "3.5+ only", which made it necessary to
retag 4.4 as 4.6 as well.
No functional changes compared to version 4.2.
Version 4.3 and 4.5 are almost a re-tagged release of version 4.0. It is the
last to support Python 2.7. This is now made explicit in the `python_requires`
argument in `setup.py`. Python 3.4 is not supported by this release. There was a
mistake releasing 4.4 as "3.5+ only", which made it necessary to retag 4.3 as
4.5 as well.
Two security fixes have also been backported, so 4.3 = 4.0 + these two fixes.
- Choose blinding factor relatively prime to N. Thanks Christian Heimes for pointing this out.
- Reject cyphertexts (when decrypting) and signatures (when verifying) that have
  been modified by prepending zero bytes. This resolves CVE-2020-13757. Thanks
  Carnil for pointing this out.
- Rolled back the switch to Poetry, and reverted back to using Pipenv + setup.py
  for dependency management. There apparently is an issue no-binary installs of
  packages build with Poetry. This fixes
  [#148](sybrenstuvel/python-rsa#148)
- Limited SHA3 support to those Python versions (3.6+) that support it natively.
  The third-party library that adds support for this to Python 3.5 is a binary
  package, and thus breaks the pure-Python nature of Python-RSA.
  This should fix [#147](sybrenstuvel/python-rsa#147).
- Added support for Python 3.8.
- Dropped support for Python 2 and 3.4.
- Added type annotations to the source code. This will make Python-RSA easier to use in
  your IDE, and allows better type checking.
- Added static type checking via [MyPy](http://mypy-lang.org/).
- Fix [#129](sybrenstuvel/python-rsa#129) Installing from source
  gives UnicodeDecodeError.
- Switched to using [Poetry](https://poetry.eustace.io/) for package
  management.
- Added support for SHA3 hashing: SHA3-256, SHA3-384, SHA3-512. This
  is natively supported by Python 3.6+ and supported via a third-party
  library on Python 3.5.
- Choose blinding factor relatively prime to N. Thanks Christian Heimes for pointing this out.
- Reject cyphertexts (when decrypting) and signatures (when verifying) that have
  been modified by prepending zero bytes. This resolves CVE-2020-13757. Thanks
  Adelapie for pointing this out.

Signed-off-by: Adolf Belka <adolf.belka@ipfire.org>
Reviewed-by: Peter Müller <peter.mueller@ipfire.org>
@davidhay1969
Copy link

Morning, just to note that the potential vuln has raised it's head again, with rsa:4.9 being reported as being vulnerable

CVE-2020-25658 has been updated with: -

This vulnerability has been modified since it was last analyzed by the NVD. It is awaiting reanalysis which may result in further changes to the information provided.

Not sure if there's anything that can be done, apart from to be aware ?

@tomato42
Copy link
Author

@davidhay1969

There are two solutions:

  1. Don't use RSA PKCS#1 v1.5 padding for encryption and decryption (though even if you do implement OAEP I'm afraid that python-rsa is likely vulnerable to Manger's attack just by virtue of using python's big integer arithmetic)
  2. don't use python-rsa for RSA decryption (though note CVE-2020-25657 and CVE-2020-25659, and that proper fix for both of them needs Make RSA decryption API safe to use with PKCS#1 v1.5 padding openssl/openssl#13817, which at the moment is master only, targetting openssl-3.2.0; other libraries may have similar issues, I haven't looked at other python libraries)

@davidhay1969
Copy link

Thanks @tomato42 appreciate the assist - I think python-rsa is being pulled in by the Kubernetes project, specifically their Python client, so I've raised CVE-2020-25658 and python-rsa #2053 there ....

Put it this way, it's the inclusion of kubernetes-client/python` from PyPi which is then throwing up the CVE, Sonatype scan etc.

Thanks again, my friend, you rule 🙇

@myheroyuki
Copy link
Contributor

There are two solutions:

This is correct! We have a couple of open PRs with PKCS#1 v2.0+ functionality, including OAEP, but they haven't seen much activity lately. If you're interested in bringing python-rsa up-to-date and increasing it's security, it would be great if you could take a look!

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

No branches or pull requests