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

OpenSSL legacy provider isn't always installed on Debian unstable #11450

Open
cjwatson opened this issue Aug 18, 2024 · 49 comments
Open

OpenSSL legacy provider isn't always installed on Debian unstable #11450

cjwatson opened this issue Aug 18, 2024 · 49 comments

Comments

@cjwatson
Copy link

This is an attempt to get a conversation started. I wasn't responsible for the original change here, but I've been cleaning up after it in Debian's Python team and I'm not convinced my current approach is really ideal.

The OpenSSL packages in Debian unstable recently changed to move the legacy provider into a separate package, openssl-provider-legacy. There's a Recommends relationship from the main library package (libssl3t64) which means that it's installed on typical user systems, but it's not a Depends so it's now possible for users not to have the legacy provider installed. In particular, Recommends are not normally installed in package builds and automatic test environments (deliberately, to ensure that declared dependencies are sufficient), and as a result we find ourselves having to apply workarounds to a fairly large number of Python packages in Debian to get their tests working again.

I'd like to find out what approach you'd recommend here. I can think of a bunch of possibilities:

  • Just set CRYPTOGRAPHY_OPENSSL_NO_LEGACY=1 in the Debian build rules for lots of Python packages (this is pretty much what I'm doing at the moment, but it's repetitive)
  • Make affected Python packages in Debian depend/build-depend on openssl-provider-legacy (also repetitive and feels heavy-handed)
  • Make Debian's python3-cryptography package depend on openssl-provider-legacy
  • Effectively revert the Debian OpenSSL packaging change (perhaps just by upgrading the Recommends to Depends)
  • Change the logic in cryptography/src/rust/src/lib.rs in some way
  • Something else I haven't thought of

Do you have any opinions on this? Ideally I wouldn't be passing messages back and forward, so I'll see if I can get Debian's OpenSSL maintainers to show up on this issue.

@alex
Copy link
Member

alex commented Aug 18, 2024

I guess as a starting point: What would your desired behavior be :-)

I'm guessing it's something like: if openssl-provider-legacy is installed, use it, if it's not, ignore it. Never bother users about this.

My suggestion would probably be to just change cryptography to silently allow the legacy provider to fail to load. This would look something like (untested):

diff --git a/src/rust/src/lib.rs b/src/rust/src/lib.rs
index cd7b99f15..dce24b100 100644
--- a/src/rust/src/lib.rs
+++ b/src/rust/src/lib.rs
@@ -60,9 +60,7 @@ fn _initialize_providers() -> CryptographyResult<LoadedProviders> {
         .map(|v| v.is_empty() || v == "0")
         .unwrap_or(true);
     let legacy = if load_legacy {
-        let legacy_result = provider::Provider::load(None, "legacy");
-        _legacy_provider_error(legacy_result.is_ok())?;
-        Some(legacy_result?)
+        provider::Provider::load(None, "legacy").ok()
     } else {
         None
     };

@alex
Copy link
Member

alex commented Aug 18, 2024

It occurs to me that this will also impact our non-wheel users. If distros are going to be switching legacy to not be installed by default, I guess we'll probably also need to handle legacy failing to load silently.

@stefanor
Copy link

Yes, graceful failure sounds like the best possible option here.

@cjwatson
Copy link
Author

I'm not completely sure what my desired behaviour would be! It does include not having cryptography be weirdly different on Debian than elsewhere, though I appreciate that's a bit vague.

Silently allowing the legacy provider to fail to load is probably OK, although it does mean that users of cryptography-based applications who don't have openssl-provider-legacy installed may have an interesting debugging experience. A thought: would it make sense for it to still raise a non-fatal warning of some kind, unless CRYPTOGRAPHY_OPENSSL_NO_LEGACY=1 is set? That may still have some downstream test suite fallout due to test suites being picky about what shows up on stderr and such, but the fallout would probably be less extensive and easier to deal with.

@stefanor
Copy link

I assume that the overwealming majority of cryptography's use will not need the legacy provider, and wouldn't want to be showing warnings. It's going to be things like exhaustive test suites that need it.

@alex
Copy link
Member

alex commented Aug 18, 2024

I would not assume that. AFAIK legacy provider is needed for several algorithms that are widely used in PEM and PKCS#12 encryption. (These algorithms are garbage are people shouldn't use them, but here we are.)

FWIW the original motivation for making it noisy for the legacy provider to fail to load was concern that users would basically bork their installs and it'd be impossible for them to debug.

Do you know if any other distros are planning to put the legacy provider in a separate package?

@cjwatson
Copy link
Author

Do you know if any other distros are planning to put the legacy provider in a separate package?

I'm afraid I have no idea. If any of the Debian OpenSSL folks show up here then maybe they will.

@reaperhulk
Copy link
Member

Without the legacy provider RC2 PKCS12 encryption (which is, sadly, incredibly common) will fail with an opaque error:

let parsed = p12
.parse2(password)
.map_err(|_| pyo3::exceptions::PyValueError::new_err("Invalid password or PKCS12 data"))?;

As Alex noted there are also other scenarios where this is likely to happen. I'm generally supportive of silently not loading the legacy provider, but I think we need to make the error messages where these failures can occur actionable. This probably requires error stack parsing though, and error stacks aren't guaranteed stable (nor are they always consistent across OpenSSL, LibreSSL, and BoringSSL, sigh).

I suppose one other approach would be to simply track whether we've loaded legacy or not and just add some alternate text to the error, e.g. " or possibly due to lack of legacy provider"

@cjwatson
Copy link
Author

Also, I wonder if "widely used in PEM and PKCS#12" may be a reason why Debian's OpenSSL team should reconsider their packaging structure? The original motivation in @xnox's bug report said "None of the algorithms it provides are useful, or needed at all", which is strongly at odds with what you're saying here. Perhaps they were unaware of how widespread the uses actually are?

@xnox
Copy link

xnox commented Aug 18, 2024

I have been hit by this in Wolfi OS / Chainguard too.

IMHO the variable as is doesn't make sense (opt into secure behaviour).

Standards compliance and certification require to block this. Thus I am literally shipping docker containers that set said variable, because with strict FIPS mode enabled it just crashes. Even if legacy provider is present.

I am trying to go even further than just splitting. IMHO all distros should start shipping opensslconf.h setting / indicating that none of these things are available anymore. Such that without breaking runtime ABI, we can make all runtime applications to stop using these things.

RC2 PKCS12 is museum cryptography. Upstream calls it pathetically weak.

Please convert your private keys to anything stronger or unencrypted and protected by other means.

@alex
Copy link
Member

alex commented Aug 18, 2024

@xnox You should re-consider your approach, both maintainers of this library had extremely negative reactions to your comment.

And the reason for that is that you've failed to understand how ecosystems works, and instead loudly demanded things from people who cannot give you them.

We support RC2 in order to parse things emitted by other projects. We don't ourselves encrypt anything with RC2, and haven't for a long time (if ever). We're not at all confused about how cryptographically garbage it is.

However, RC2 remains widely used. The macOS still uses RC2-40 for encrypting the certificate container in PKCS#12. It's also what OpenSSL<3.0 used, most examples for libraries like bouncycastle continue to show, Windows uses it in some circumstances, etc. Looking around at the current state of this ecosystem you'll find numerous examples using the -legacy flag on OpenSSL's CLI specifically to force the use of RC2.

Most people don't use PKCS#12 encryption as a security boundary, they use it as an interchange format.

So we're as interested in anyone else in getting rid of RC2, however we also need to manage our breakage budget (see https://alexgaynor.net/2021/oct/07/whats-in-a-version-number/), and breaking common workflows like "parse a PKCS#12 from macOS" for limited security gain.

If you want to get rid of PKCS#12, the place to start is with people who emit it, not people who parse it.

@xnox
Copy link

xnox commented Aug 18, 2024

Supporting converting RC2-40-CBC files to AES-256-CBC makes sense on Linux as a standalone tool.

Requiring runtime cryptographic libraries to support direct usage of RC2-40-CBC certificates without requiring to upgrade them first does not make sense.

The risk of having legacy provider available and loaded is too large causing to unintentionally use weak cryptography.

Ditto all secret scanning and security scanners should flag up weak private keys.

The common workflows must change to add a new step of converting private keys to strong encryption prior to usage.

We must protect user privacy, even if Mac OS chooses not to.

I will also email Apple to address their defaults.

@kroeckx
Copy link

kroeckx commented Aug 19, 2024

I would like to come to a situation where the legacy provider is only loaded in cases where the maintainer knows about it, and that it's needed, for instance the application that needs to do PKCS#12 with RC2. I do not want something RC2 to be available in all applications. So loading it when it's available is also not a preferred solution for me, I would like to push it application.

@alex
Copy link
Member

alex commented Aug 19, 2024

As a general purpose cryptography library, we don't really know what our users are going to use us for, which makes it more challenging to only load it when required. (If Python packaging had some sort of "features" indicator, ala Rust's Cargo, we could use it to opt-in, but it doesn't.)

@xnox
Copy link

xnox commented Aug 19, 2024

As a general purpose cryptography library, we don't really know what our users are going to use us for, which makes it more challenging to only load it when required. (If Python packaging had some sort of "features" indicator, ala Rust's Cargo, we could use it to opt-in, but it doesn't.)

Assume that OpenSSL on some distributions no longer provides RC2. Meaning try not to go via OpenSSL to gain RC2. Most of the time it should not be needed. And when that fails, fallback to a vendored in implementation of RC2 to on-the-fly convert to unencrypted or AES-256-CBC to then continue back via OpenSSL or anything else.

This is similar to how other pieces of software have adapted to continue using obsolete cryptography, as required to maintain interoperability with whatever you want to support. I.e. I have seen projects vendor in MD4, MD5, RC2, RC4 to continue supporting NTLM auth and similar things.

Crashing upon importing your library, because legacy provider is not available, is not optimal for all the users who are on modern linux distributions, and are not planning on using weak cryptography (which obviously is impossible to know ahead of time). As the current variable is "opt out of legacy algorithms support", instead of "opt into legacy algorithms support", and thus forcing systems to carry additional runtime dependencies which depending on use case will never be used (with a declining probability over time).

@stefanor
Copy link

Vendoring legacy cryptography functions in 2nd-level libraries to allow core cryptographic libraries to drop support for them, seems.... not particularly useful

@alex
Copy link
Member

alex commented Aug 19, 2024

It also simply wouldn't work. RC2 decryption is handled by OpenSSL as part of the PKCS#12 parse-and-decrypt process, there's not an entrypoint for us to only do that RC2 encryption.

@stefanor
Copy link

Debian has reverted this change for now, to figure out next steps without breaking everything.

@kroeckx
Copy link

kroeckx commented Aug 20, 2024

Would it be possible to only load the legacy provider if some function is called?

@xnox
Copy link

xnox commented Aug 20, 2024

About Mac OS X:
As per https://openradar.appspot.com/FB8988319 it seems that Mac OS Catalina 10.15.x appear to lack support for AES-256-CBC
In either macOS 11 Big Sur or macOS 12 Monterey AES-256-CBC support was added, with default message digest set to MD5. macOS 13 Ventura switch default message digest to SHA256. Based on this discussion https://discussions.apple.com/thread/254328280?sortBy=rank

Version history is available at https://en.wikipedia.org/wiki/MacOS_version_history

Note that Catalina release went EOL in 2022, but macOS releases can be very sticky even when hardware supports newer releases, as one may simply not have enough disk space to upgrade, nor a compelling reason to upgrade.

Supported releases of Mac OS do support AES-256-CBC.

Obviously keys generated in previous releases, are not converted automatically.

However, this incompatibility with Mac OS X should be diminishing over time, as people rotate keys / generate new keys on the supported Mac OS X releases.

@reaperhulk
Copy link
Member

I just exported a cert+key from Keychain Access on macOS 14.6.1 (latest release) and it used RC2-40 and 3DES. I also tested on a machine running the beta release of Sequoia that was released today -- RC2-40 and 3DES. So while they may support newer algorithms on import, they are still choosing to export legacy cryptography (with no UI options to change it).

@xnox
Copy link

xnox commented Aug 20, 2024

I just exported a cert+key from Keychain Access on macOS 14.6.1 (latest release) and it used RC2-40 and 3DES. I also tested on a machine running the beta release of Sequoia that was released today -- RC2-40 and 3DES. So while they may support newer algorithms on import, they are still choosing to export legacy cryptography (with no UI options to change it).

Absolutely fabulous. Probably likely due to appleTV and iPhone support - as I think those devices have longer support time-frames and thus more of them are behind the curve, requiring RC2-40.

How broken is RC-40 and 3DES and how quickly those keys can be bruteforce decrypted, to effectively render them plain text? Sort of pondering if a javascript web-app can do it.

@xnox
Copy link

xnox commented Aug 20, 2024

@reaperhulk if you can make a gist of a freshly generated & exported key, that would help with cracking it or providing instructions on how to convert it. As I think at the very least OpenSSL documentation should direct people at upgrading those.

@reaperhulk are you able to test if import of strong pkcs12 file works via gui in the keychain app? (one can generate one with mac os terminal).

@alex
Copy link
Member

alex commented Aug 20, 2024

Let's try to keep this thread on topic. For the purposes of this thread, PKCS#12 + RC2 is a fact of life, we can focus on changing life elsehwere.

@kroeckx to answer your question, I think theoretically we could lazily load it, but it'd be anytime PKCS#12 was used, not just when RC2 was used (and anything else that might require these algorithms, which is maybe all PEM loading?). We'd still have to decide what the behavior is (allow it to silently fail or not). And finally, I think the fact that OpenSSL allows lazy-loading of algorithms is a very poor design decision on their part, and the shared mutable state they have has led to tons of performance regressions, so I'm very reticent to rely on it.

@alex alex added the waiting-on-reporter Issue is waiting on a reply from the reporter. It will be automatically cloesd if there is no reply. label Sep 20, 2024
Copy link

This issue has been waiting for a reporter response for 3 days. It will be auto-closed if no activity occurs in the next 5 days.

@github-actions github-actions bot added the Stale label Sep 24, 2024
@scw
Copy link

scw commented Sep 24, 2024

Would it be useful to have a compile time flag that explicitly disabled the legacy provider? I work in an context where we don't have the legacy provider within our OpenSSL conda package, and currently use a tiny patch to disable legacy explicitly. For those who are in environments where they explicitly want legacy primitives to fail and are in a position to build from source, this would provide a mechanism to enforce that desire.

Thanks for all the work that goes into cryptography!

@alex
Copy link
Member

alex commented Sep 24, 2024 via email

@scw
Copy link

scw commented Sep 24, 2024

I admit I have limited knowledge of the modern pip install workflows, but possibly by setting an environment variable at build time, e.g. reuse the same environment variable with:

export CRYPTOGRAPHY_OPENSSL_NO_LEGACY=1 
python -m pip install . -vv

Some existing downstream builds already set OPENSSL_DIR in a similar fashion when building cryptography, which is bound to within the openssl-sys package.

@alex
Copy link
Member

alex commented Sep 24, 2024 via email

@github-actions github-actions bot removed the Stale label Sep 25, 2024
Copy link

This issue has been waiting for a reporter response for 3 days. It will be auto-closed if no activity occurs in the next 5 days.

@github-actions github-actions bot added the Stale label Sep 28, 2024
@cjwatson
Copy link
Author

This issue is labelled as waiting for a response from the reporter (me), but I'm not really sure what I can provide given that I was just trying to link up two other parties (cryptography upstream and the Debian OpenSSL team).

@alex alex removed waiting-on-reporter Issue is waiting on a reply from the reporter. It will be automatically cloesd if there is no reply. Stale labels Sep 29, 2024
@alex
Copy link
Member

alex commented Sep 29, 2024

I think we've accomplished that :-)

I'll hold it open to track adding a build-time env var.

@sebastianas
Copy link

build-time env var? How is it going to work?
I though something in the cryptography function that will ask for the legacy algorithm and if it gets none, it is just quiet and complains once the algorithm is actually used. Or is this not feasible?
If you remove support for legacy algorithms (which is my maybe wrong interpretation) then users won't be able to open certain certificates.

Sebastian

@alex
Copy link
Member

alex commented Oct 5, 2024 via email

@sebastianas
Copy link

sebastianas commented Oct 9, 2024 via email

@alex
Copy link
Member

alex commented Oct 9, 2024

The desire for building without legacy support is, as I understand it, orthagonal to the request from Debian.

We have a few choices available to us:

  1. Always eagerly load "legacy", and error loudly when it's not available. This is the current behavior.
  2. Always eagerly load "legacy", but do not error when it's not available. This would address Debian's concerns, at the risk of difficult to debug errors for users. (With potential middle-ground of emitting a warning.)
  3. Never load "legacy" against the user specifically requests it. This is a non-starter because too many workloads effectively require "legacy"
  4. Lazily load "legacy" when the user does an operation that may need the legacy provider (and either error or not). This is a non-starter because a) we don't believe in dynamic provider loading, b) so many operations potential require the legacy provide that it's not particularly different from (1)/(2).

I'm somewhat persuaded that the right direction is: eagerly load "legacy" and emit a warning if it fails, provide a run-time option to disable attempting to load legacy, as well as a compile-time option to disable loading it ever.

But I'd like to hear if a) @reaperhulk agrees, b) that sounds like it addresses various folks requests, and if so if anyone would be interested in submitting a pull-request for it.

@xnox
Copy link

xnox commented Oct 9, 2024

There is case 5 - on operating systems in FIPS approved mode obtaining legacy will be blocked. However, legacy algorithms maybe still available at runtime via fips module for approved legacy operations - for example decryption of a private key.

@alex
Copy link
Member

alex commented Oct 9, 2024 via email

@reaperhulk
Copy link
Member

I am okay with an eagerly load but emit warning if unavailable approach, although ideally we'd also augment our exceptions to include a bit more info in their repr so we can more easily detect failures due to a missing legacy provider.

@alex
Copy link
Member

alex commented Oct 9, 2024 via email

@MGlaus
Copy link

MGlaus commented Nov 20, 2024

FreeBSD also builds OpenSSL 3 without Legacy Provider.

I find it kinda weird that a program does not work, because something it does not need is not available.

Emitting a warning when Legacy Provider aren't available is ok. (In my opinion this would be still the wrong behavior, but i understand why it would be necessary)

@alex
Copy link
Member

alex commented Nov 20, 2024 via email

@MGlaus
Copy link

MGlaus commented Nov 20, 2024

Would you be interested in contributing a PR implementing the behavior described above?

Sorry, i don't have the necessary experience for it. I don't know any rust and i don't have much experience in python too.

@xnox
Copy link

xnox commented Nov 20, 2024

Is there more to this than just legacy.

  • .pem file is parsed with pycryptography that is not controlled by the user of pycryptography
  • that .pem uses an optional algorithm (i.e. one of the signatures, or cert-chain has SM signature, K-/B- ECC curves, GOST signature, signature that uses MD4, DSA, EdDDSA )
  • the optional algorithm may or may not be supported by the runtime openssl; it may be rectified by installing additional providers; it may have been compiled out of openssl
  • upon hitting that, implicit loading providers may work; or not
  • when things didn't "just" work, it is hard to debug it

Above will get more weird at runtime over time. I.e. Soon, SHA1 will be available, blocked to generate a digest, allowed to generate HMAC with long enough key; blocked to generate a signature; allowed to verify a signature.

Emitting a warning at that point may be helpful; or possibly raising a error. Cause human guidance should state, what happened, why, and what actions a user can take if any (i.e. install additional providers, use a different build of openssl, update cryptography). Existing openssl errors really should be bubbled up verbose (key size too short; digest not allowed; digest not know; curve not known; etc). Such that users of pycryptography APIs can handle them intelligently (fix their deployment, downgrade/rebuild openssl, or in their app tell their user what happened and what they should do).

I haven't looked, but this may require adding more error handling in the openssl bindings; and then also adding those warnings/errors in python cryptography; and emitting lots of warnings/errors (and allow to catch them, or suppress them). Because yes most of the time, most things, just work. But there are many many many combinations that do not and will not (i.e. when there is no legacy provider at all, or openssl was compiled with no-ec to completely remove ECC support, and so on).

@alex
Copy link
Member

alex commented Nov 20, 2024

You're conflating unrelated issues. The precise behavior of different APIs when algorithms are unavailable (either entirely or for specific operations) are specific to those APIs, but in general we almost certainly already raise a clear exception for all of those, and where we don't there are individual bugs to be filed.

This bug is specifically about our behavior of automatically attempting to load the legacy provider and erroring if it is unable.

I will also take this moment to register my firm belief that the providers API was a terrible design on OpenSSL's part, and some day in the future when I finally snap and propose we drop OpenSSL, providers will have played a substantial role.

@xnox
Copy link

xnox commented Nov 20, 2024

You're conflating unrelated issues.
...
This bug is specifically about our behavior of automatically attempting to load the legacy provider and erroring if it is unable.

Previously it was stated that hard loading legacy provider is done to ensure eventual availability of algorithms.

Loading legacy provider doesn't change any available C APIs - e.g. with or without legacy provider EVP_Digest APIs are available.

Reasons for force loading it are unclear to me, as it will be autoloaded by openssl when needed regardless. E.g. If one makes request for EVP_Digest of NID MD4, and no imolementstion is found, legacy provider will be attempted to be autoloaded, and if not available error out.

Furthermore, depending on default property query string, even with a loaded legacy provider, particular algorithm access may still be blocked.

I struggle to understand reasons for manually loading legacy provider at startup 1) when it will not be needed at all 2) may not actually do anything 3) as intended by system administrator / OS.

Why is legacy provider being loaded manually at all? Is it because runtime errors were not clear to the user and pycryptography was getting blamed? Is autoloading legacy peovider in openssl broken on some distributions?

@xnox
Copy link

xnox commented Nov 21, 2024

I am reading cipher_registry.rs and the implementation there assumes that build-time openssl configuration is the same as runtime.

However, that is not universally true. Instead of hard coding the list of things based on openssl build time config, one really should at init time query what algorithms are available and register those dynamically and expose them to the Python world. I.e. there shouldn't be any need for cfg settings in rust code, and the same build of rust code at runtime can link with any builds of openssl.

A good example is supporting Blake3 & GOST algorithms dynamically at runtime, even though neither are available in stock openssl nor stock providers. If one queries available algorithms at runtime, it should just work.

@MGlaus
Copy link

MGlaus commented Dec 4, 2024

Any updates on this issue?

@alex
Copy link
Member

alex commented Dec 4, 2024 via email

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

No branches or pull requests

9 participants