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

STOMP+SSL-related changes: security fixes + convenience enhancement (+ doc updates/improvements) #2414

Merged

Conversation

zuo
Copy link
Contributor

@zuo zuo commented Oct 7, 2023

The main purpose of this PR is to fix the following two problems -- both related to how the stomp.py library deals with SSL:

  • (1) [ad security] Certain versions of stomp.py, that IntelMQ needs to be compatible with, use the ssl module's tools in ways that suffers from certain security weaknesses. In particular, stomp.py < 4.1.12 [not supported anymore] uses a deprecated helper: ssl.wrap_socket() stomp.py >=8.0, <8.1 mistakenly creates an SSLContext instance with the check_hostname flag unset; an important negative result is that the hostname of the STOMP server is not checked during the TLS handshake (!). Also, there are weaknesses (caused either by stomp.py or by older Python versions) of using too old versions of TLS (namely: 1.0 and 1.1 -- today considered insecure).

  • (2) [ad admin convenience] No version of stomp.py makes it possible to load the system's default CA certificates -- condemning administrators to bother with manual updates of the CA certificate(s) file, even if the certificate of the STOMP server the bots connect to could be verified using some of the publicly available CA certificates which are part of nearly all mainstream operating system distributions... Note: this is the case with the upcoming new n6 Stream API server's certificate.

An important part of the changes to fix these problems is a non-public helper class, intelmq.lib.mixins.stomp._StompPyDedicatedSSLProxy, which implements a kind of transparent proxy object: it wraps the ssl attribute of the stomp.transport module (originaly set to the ssl module object) replacing some of the ssl module's tools with their patched variants. Note: the ssl module itself and all its members are left untouched -- only the ssl member of the stomp.transport module is replaced with an instance of that class.

For more details -- see the docstrings and comments in the code. [See the 4th commit...]

Apart from that, versions of stomp.py >=4.1.8, <4.1.12 are no longer supported + some details in debian/control have been fixed. [See the 3rd commit...]

Together with the aforementioned changes, relevant parts of the documentation (including also the feeds spec and the changelog) have been appropriately updated + also improved by including some extra details concerning the upcoming switch to the new variant of n6 Stream API (the one with username+password-based authentication). [Also, see the 3rd and 4th commit...]

Also, an additional minor fix has been applied to the imports part of the STOMP collector's module, concerning the stomp.py-imports-related logic. [See the 5th commit...]

Plus, apart from all that, several additional (also STOMP-or-n6-integration-related) doc updates/improvements as well as very minor/cosmetic code tweaks have been made. [See the 1st, 2nd and 6th commit...]

intelmq/lib/mixins/stomp.py Fixed Show resolved Hide resolved
@zuo zuo force-pushed the jk/ssl-improvements-related-to-stomp-and-n6 branch 3 times, most recently from cdda076 to 425944f Compare October 7, 2023 23:38
@zuo zuo force-pushed the jk/ssl-improvements-related-to-stomp-and-n6 branch 4 times, most recently from a32e17f to 4d758a7 Compare October 8, 2023 13:42
@sebix sebix added this to the 3.2.2 milestone Oct 9, 2023
@zuo zuo mentioned this pull request Oct 10, 2023
@@ -947,7 +947,7 @@ Install the `stomp.py` library from PyPI:
* **Feed parameters** (see above)
* `server`: STOMP server's hostname or IP, e.g. "n6stream.cert.pl" (which is default)
* `port`: STOMP server's port number (default: 61614)
* `exchange`: STOMP *destination* to subscribe to, e.g. "/exchange/my.example.org/*.*.*.*"
* `exchange`: STOMP *destination* to subscribe to, e.g. ``"/exchange/my.example.org/*.*.*.*"``
Copy link
Contributor Author

@zuo zuo Oct 10, 2023

Choose a reason for hiding this comment

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

Without the `` marks rendering of the *.*.*.* fragment was wrong.

Copy link
Member

Choose a reason for hiding this comment

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

the backtick markup is correct, to display it as code/preformatted text

Copy link
Contributor

@kamil-certat kamil-certat left a comment

Choose a reason for hiding this comment

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

I have started the review, but I after a while of analysing it, I'd like to start with challenging some architectural decisions you've made.

I love the improvements in the security, however please see that the solution is really crafted for a very specific conditions, and can easily break with any upstream changes, as well as duplicate a lot of checks. As we support a library mode, I can imagine creating two objects of bots connecting to two different hosts in a one process - and then the Proxy will keep the first hostname and raise an exception.

Consider we re-design the solution in following way:

  1. Do not check things that are already granted, like minimum version of Python, if it doesn't change the code flow.
  2. Create a subclass of SSLContext, and override related methods there, instead of patching them. Keep the default behaviour any time the call isn't the one marking a special behaviour, but just use the super().... for that.
  3. The proxy should override only the expected objects (SSLContext class, create_default_context etc.), and do not change the ssl behaviour in any other way.

If the conditions doesn't look like a call from expected stomp, do not raise - assume it was another legit use of the stomp etc., just not the one we want to secure.


# Used if `auth_by_ssl_client_certificate` is false (otherwise ignored):
username: str = 'guest' # (STOMP auth *login*)
password: str = 'guest' # (STOMP auth *passcode*)
Copy link
Contributor

Choose a reason for hiding this comment

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

More just a FYI, but basically, if parameters are set by StompMixIn, it shouldn't be necessary to repeat them in bot's class. As there is quite a long part with parameters' explanation, I'd put it in the Mixin class, and just leave a note in bots' classes where to look for them. On the other hand, here they are easier accessible. What do you think, @zuo & @sebix?

Copy link
Member

Choose a reason for hiding this comment

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

Yes, re-defining them is not necessary


def __patch_ssl_related_stomp_stuff(self,
host_and_ports: List[Tuple[str, int]]) -> None:
assert len(host_and_ports) == 1
Copy link
Contributor

Choose a reason for hiding this comment

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

Do not rely on assertions in non-test code, they are intended for the debug purposes only, and can be easily disabled with optimizations.

host_and_ports = [(self.server, int(self.port))]
self.__patch_ssl_related_stomp_stuff(host_and_ports)
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
self.__patch_ssl_related_stomp_stuff(host_and_ports)
self.__patch_ssl_in_stomp(host_and_ports)

Or so, to make it look better ;)

def __patch_ssl_related_stomp_stuff(self,
host_and_ports: List[Tuple[str, int]]) -> None:
assert len(host_and_ports) == 1
[(server_hostname, _)] = host_and_ports
Copy link
Contributor

Choose a reason for hiding this comment

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

Instead of pack (L99), call, assert, unpack, ignore one, call please just write in L100:

_StompPyDedicatedSSLProxy.patch_stomp_transport_ssl(self.server)

and remove this function.

Comment on lines 258 to 261
assert sys.version_info[:2] >= (3, 7)
assert stomp is not None
assert stomp.__version__ >= (4, 1, 8)
assert getattr(stomp, 'transport', None) is not None
Copy link
Contributor

Choose a reason for hiding this comment

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

Please remove. Those conditions are considered already guaranteed, so verifying them again with the debug-only asserts should be considered unnecessary - it would make sense in a mathematical proof, but not in a service. It introduces an additional place, where we possibly should maintenance the check - e.g., for the consistency, if we drop the Python 3.7 support, this place should also be updated.

@zuo
Copy link
Contributor Author

zuo commented Oct 11, 2023

I have started the review, but I after a while of analysing it, I'd like to start with challenging some architectural decisions you've made.

I love the improvements in the security, however please see that the solution is really crafted for a very specific conditions, and can easily break with any upstream changes, as well as duplicate a lot of checks. As we support a library mode, I can imagine creating two objects of bots connecting to two different hosts in a one process - and then the Proxy will keep the first hostname and raise an exception.
[...]

OK, I will reconsider the design...

One question: could we drop support for stomp.py < 4.1.12? The hostname-related problem you mentioned above would disappear (as versions >= 4.1.12 do not use ssl.wrap_socket() [at least not for Python >= 3.7], so the patched variant of it -- which is the only place where that early-stored hostname is used -- could be entirely removed).

@sebix
Copy link
Member

sebix commented Oct 12, 2023

One question: could we drop support for stomp.py < 4.1.12? The hostname-related problem you mentioned above would disappear (as versions >= 4.1.12 do not use ssl.wrap_socket() [at least not for Python >= 3.7], so the patched variant of it -- which is the only place where that early-stored hostname is used -- could be entirely removed).

Yes, absolutely.
By way of comparison: Debian 10 buster and Ubuntu 20.04 focal have python3-stomp == 4.1.19.

We can also consider dropping support for < 6.
We seek for compatibility and don't want to enforce that users strictly need the absolute latest version of a library (makes sense in very controlled and similar environments), but if the required effort is high, the reward is low (it affects only one single bot, not the core), dropping compatibility is reasonable.

@zuo zuo force-pushed the jk/ssl-improvements-related-to-stomp-and-n6 branch 3 times, most recently from 2e64fc1 to a5d0922 Compare October 19, 2023 17:05
@zuo zuo force-pushed the jk/ssl-improvements-related-to-stomp-and-n6 branch 2 times, most recently from 43a4b8c to 717bbbf Compare October 19, 2023 17:51
Copy link
Member

@sebix sebix left a comment

Choose a reason for hiding this comment

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

The more important review comes from @kamil-certat as he is a user/operator of the bot and knows more details, also from a user- and admin perspective.


# Used if `auth_by_ssl_client_certificate` is false (otherwise ignored):
username: str = 'guest' # (STOMP auth *login*)
password: str = 'guest' # (STOMP auth *passcode*)
Copy link
Member

Choose a reason for hiding this comment

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

Yes, re-defining them is not necessary

@zuo
Copy link
Contributor Author

zuo commented Oct 19, 2023

@sebix @kamil-certat

OK, this is a redesigned version + with a few additional minor improvements/fixes/updates -- in particular, minimum stomp.py version has been changed from 4.1.8 to 4.1.12, and a package name has been fixed in debian/control (see the relevant commit...).

Copy link
Contributor

@kamil-certat kamil-certat left a comment

Choose a reason for hiding this comment

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

I think it looks good. Thanks for the change!

@kamil-certat
Copy link
Contributor

@sebix @aaronkaplan Could you please merge this change? It's quite important for changes in n6 (using default CAs).

@kamil-certat
Copy link
Contributor

@zuo We have merged the new docs system, may you merge develop and copy your changes in documentation to docs/user/bots.md?

@zuo
Copy link
Contributor Author

zuo commented Nov 7, 2023

@zuo We have merged the new docs system, may you merge develop and copy your changes in documentation to docs/user/bots.md?

OK, I will.

…+ doc updates/improvements)

doc: a few fixes/improvements ad STOMP bots and *n6* feed

lib, bots, doc: only cosmetic/very minor tweaks and comments

lib, bots, pkg, doc: drop support for `stomp.py` older than 4.1.12

The affected bots are: *STOMP collector* (`StompCollectorBot` defined
in `intelmq.bots.collectors.stomp.collector`) and *STOMP output*
(`StompOutputBot` defined in `intelmq.bots.outputs.stomp.output`).

Also, in `debian/control`, the `python3-stomp` package name has been
fixed (by removing the `.py` suffix).

The changelog has been updated appropriately.

lib, bots, doc: STOMP/*n6*-related fixes/enhancements, also ad security

SSL-related changes -- regarding `intelmq.lib.mixins.StompMixin` and,
therefore, also the *STOMP collector* bot (`StompCollectorBot` defined
in `intelmq.bots.collectors.stomp.collector`) and the *STOMP output*
bot (`StompOutputBot` defined in `intelmq.bots.outputs.stomp.output`)
-- have been made:

* *Security*-focused: fixed certain security problems which were caused
  by the fact that certain versions of the `stomp.py` library we need
  to be compatible with use the `ssl` module's tools in such ways that
  suffer from certain *security weaknesses*. In particular, `stomp.py`
  in versions `>=8.0, <8.1` mistakenly creates an `SSLContext` instance
  with the `check_hostname` flag unset -- an important negative effect
  of that is that the hostname of the STOMP server is *not* checked
  during the TLS handshake (making all STOMP communication vulnerable
  to certain kinds of attacks...). Also, there are weaknesses (caused
  either by `stomp.py` or by older, yet still supported by IntelMQ,
  Python versions) of using too old versions of the TLS protocol
  (namely: 1.0 and 1.1 -- nowadays considered insecure).

* *Admin convenience*-focused: from now on, for each of the STOMP bots,
  you can set the `ssl_ca_certificate` config param to an empty string
  -- dictating that the SSL tools employed by the `stomp.py`'s machinery
  will attempt to load the system’s default CA certificates. Thanks to
  that, administrators of the given IntelMQ instance can be relieved of
  of the fuss with manual updates of the CA certificate(s) file -- *if*
  the certificate of the STOMP server can be verified using some of
  the publicly available CA certificates which are part of nearly all
  mainstream operating system distributions (this will be the case with
  the server certificate of the new variant of the *n6* Stream API, that
  is, the variant with STOMP-login-and-passcode-based authentication).

An important part of the implementation of the aforementioned changes is
a non-public class, `intelmq.lib.mixins.stomp._StompPyDedicatedSSLProxy`
-- which implements a kind of transparent proxy object that wraps the
`ssl` attribute of the `stomp.transport` module (originaly set to the
`ssl` module object), replacing some of the `ssl` module's tools with
their patched variants (note that the `ssl` module itself and all its
members are left untouched).

The parts of the IntelMQ's documentation related to those STOMP bots +
integration with *n6* (including the CERT.PL's "N6 Stomp Stream" feed
description) have been updated and improved; also, the changelog has
been updated.

bots: fix import logic in STOMP collector's module

The logic regarding importing of the `stomp.py`'s stuff has been fixed:
now the condition of the absence of the `stomp` module (and thus, of the
entire library) would not be confused with the condition of the absence
of only the `stomp.exception` module (which would mean the presence of a
version of that library lacking just the `exception` submodule).
@sebix sebix force-pushed the jk/ssl-improvements-related-to-stomp-and-n6 branch from 717bbbf to 735f415 Compare November 21, 2023 14:45
@sebix
Copy link
Member

sebix commented Nov 21, 2023

I rebased the PR on the branch 'develop' and moved all docs to the new files. As the documentation changes were spread over five commits, I squashed them all together, because keeping the changes apart in separate commits, and moving every single change manually to the new format would be a heck of a lot of effort.

As @kamil-certat approved the PR, I will merge it after the CI checks pass.

@sebix sebix merged commit d14611c into certtools:develop Nov 21, 2023
20 checks passed
@zuo
Copy link
Contributor Author

zuo commented Nov 23, 2023

@sebix @kamil-certat Oh, thank you for the rebase and merge! And I am sorry I haven't managed to do that promptly!

@sebix
Copy link
Member

sebix commented Nov 24, 2023

@zuo Thank you for improving, maintaining and contributing to IntelMQ!

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

Successfully merging this pull request may close these issues.

3 participants