diff --git a/CHANGELOG.md b/CHANGELOG.md index fad689a..1501385 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## v3.8.5 (2026-10-21) + +### Security + +- Added `certifi>=2026.10.19` as a declared dependency so the CA bundle used by `requests` for stackql binary downloads no longer trusts `emSign ECC Root CA - G3`, the eMudhra root whose eight intermediate CA private keys were found to be recoverable in October 2026 (Mozilla Bugzilla 2083449, certifi advisory GHSA-6q48-6c7w-9gw2). pystackql ships no CA material of its own; this floor ensures fresh installs and upgrades resolve a patched bundle. +- Added `tests/test_tls_trust.py` to assert the resolved certifi bundle excludes the compromised root (by certificate fingerprint) and that downloads never disable certificate verification. + ## v3.8.4 (2026-06-29) ### Updates diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7aab58f --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,29 @@ +**Note**: + +- All contributions to `pystackql`, through any means, are subject to the [license](/LICENSE). +- This document itself and the patterns specified herein, are heavily influenced by [redis](https://github.com/redis/redis). We profusely thank the maintainers of same, from which we draw inspiration. +- For useful information contributors **may** want to consider in developing `pystackql`, please see the "Building the package", "Testing Locally" and "Building the docs" sections of the [README](/README.rst), the [tests README](/tests/README.md), and the published documentation at [pystackql.readthedocs.io](https://pystackql.readthedocs.io). +- All contributions to `pystackql` and actions related, through any means, are subject to the StackQL [CODE OF CONDUCT](https://github.com/stackql/stackql/blob/main/CODE_OF_CONDUCT.md). + +# Contribution flow + +Before beginning implementation work, please raise an issue (using the bug report, feature request or question template as appropriate) and receive a consensus that work should proceed. + +Please raise pull requests (PRs) from a fork of this repository. The richer the detail / evidence supplied, the easier the review process. A usefully named feature branch for the PR source is appreciated. + +PRs against `main` run the test suite on Linux, macOS and Windows across Python 3.9 to 3.13. Please keep changes working on all of them. + +## Development notes + +- `pystackql` is a Python wrapper around the `stackql` engine. Changes to the engine itself belong in [stackql/stackql](https://github.com/stackql/stackql), and provider definitions belong in the [stackql-provider-registry](https://github.com/stackql/stackql-provider-registry). +- Run the non-server tests with `python run_tests.py`. Server-mode tests need a running `stackql srv` process (see `start-stackql-server.sh` or `start-stackql-server.ps1`), then `python run_server_tests.py`. +- Tests use the no-auth Homebrew provider and provider-agnostic literal queries. Please do not add tests that require cloud credentials. +- Runtime dependencies are declared in `pyproject.toml` as minimum versions rather than pins. Please keep it that way and avoid unrelated dependency bumps. +- `README.rst` is reStructuredText and doubles as the PyPI readme. The documentation in `docs/` is built with Sphinx from the docstrings, so update the `StackQL.__init__` docstring when changing constructor parameters. +- Releases bump `version` in `pyproject.toml` and add an entry to `CHANGELOG.md`. + +Finally, in respect of all your efforts; we thank you and ask that you please give us a star while you are at it. + +Many Thanks, + +The StackQL team. diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..732b900 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,42 @@ +# Security Policy + +The StackQL team takes the security of `pystackql` seriously. We appreciate everyone who reports vulnerabilities responsibly, and we will do our best to acknowledge and address valid reports promptly. + +## Reporting a Vulnerability + +Please do not open a public GitHub issue for security reports. A public issue discloses the problem before a fix is available and puts users at risk. + +Instead, report privately using GitHub private vulnerability reporting: + +1. Open the [new advisory form](https://github.com/stackql/pystackql/security/advisories/new) (Security tab -> Advisories -> "Report a vulnerability"). +2. Provide as much detail as you can: affected `pystackql` version(s), Python version and operating system, reproduction steps, impact, and any suggested remediation. +3. Submit. This opens a private advisory thread visible only to you and the maintainers. + +If you cannot use GitHub private vulnerability reporting, email us at [info@stackql.io](mailto:info@stackql.io). Please put "SECURITY" in the subject line and treat the contents as confidential. + +## Supported Versions + +Security fixes are applied to the latest released minor line, published to [PyPI](https://pypi.org/project/pystackql/). Older lines are supported on a best-effort basis only; we recommend upgrading to the latest release with `pip install --upgrade pystackql`. See the [changelog](/CHANGELOG.md) and the [PyPI release history](https://pypi.org/project/pystackql/#history) for the current version. + +| Version | Supported | +| ------- | ------------ | +| 3.8.x | Yes | +| < 3.8 | Best-effort | + +`pystackql` supports Python 3.9 to 3.13 on Linux, macOS and Windows. + +## Response Expectations + +These are targets, not contractual guarantees: + +- We aim to acknowledge a report within a few business days. +- We will keep you updated as we investigate and work on a fix. +- We will coordinate disclosure timing with you once a fix or mitigation is ready. + +## Disclosure Policy + +We follow coordinated (responsible) disclosure. Please give us a reasonable opportunity to release a fix before any public disclosure. We are happy to credit reporters in the advisory and release notes, unless you prefer to remain anonymous. + +## Scope + +This policy covers the `pystackql` Python package in this repository as published to PyPI, including its Jupyter magic extensions. `pystackql` downloads and runs the `stackql` engine binary in local mode and connects to a `stackql` server in server mode; the engine, its binaries and its Docker images are maintained in [stackql/stackql](https://github.com/stackql/stackql) and covered by that repository's [security policy](https://github.com/stackql/stackql/blob/main/SECURITY.md). Provider definitions are maintained separately in the [stackql-provider-registry](https://github.com/stackql/stackql-provider-registry). Please report issues specific to those in their respective repositories. diff --git a/pyproject.toml b/pyproject.toml index e4a0337..886e35a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "pystackql" -version = "3.8.4" +version = "3.8.5" description = "A Python interface for StackQL" readme = "README.rst" authors = [ @@ -25,6 +25,7 @@ classifiers = [ requires-python = ">=3.9" dependencies = [ "requests", + "certifi>=2026.10.19", "pandas", "IPython", "psycopg[binary]>=3.1.0", diff --git a/requirements.txt b/requirements.txt index 5270cf7..c69d776 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,6 +1,7 @@ # Core dependencies pandas>=1.3.0 requests>=2.25.0 +certifi>=2026.10.19 # CA bundle without the compromised emSign ECC Root CA - G3 (eMudhra incident, Oct 2026) IPython>=7.0.0 termcolor>=1.1.0 PyYAML>=5.4.0 diff --git a/run_tests.py b/run_tests.py index 9289308..e8f47ea 100644 --- a/run_tests.py +++ b/run_tests.py @@ -45,7 +45,8 @@ def main(): "tests/test_output_formats.py", "tests/test_magic.py", "tests/test_async.py", - "tests/test_download.py" + "tests/test_download.py", + "tests/test_tls_trust.py" ]) # Run pytest with the arguments diff --git a/tests/test_tls_trust.py b/tests/test_tls_trust.py new file mode 100644 index 0000000..5da7c0c --- /dev/null +++ b/tests/test_tls_trust.py @@ -0,0 +1,110 @@ +# tests/test_tls_trust.py + +""" +TLS trust store tests for PyStackQL. + +PyStackQL makes one outbound HTTPS call (downloading the stackql binary from +releases.stackql.io via `requests`), and `requests` validates server +certificates against the CA bundle shipped by `certifi`. These tests guard +against resolving a certifi bundle that still trusts a compromised root. + +Background: in October 2026 the private keys of eight eMudhra intermediate CAs +chaining to "emSign ECC Root CA - G3" were found to be trivially recoverable +(Mozilla Bugzilla 2083449). certifi removed that root in 2026.10.18 and +restored the unaffected emSign roots in 2026.10.19 (GHSA-6q48-6c7w-9gw2). +""" + +import hashlib +import os +import re +import ssl +import sys +from unittest.mock import patch, MagicMock + +import certifi +import requests + +# Add the parent directory to the path so we can import from pystackql +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from pystackql.utils import download + +# Minimum certifi release that distrusts the compromised root while keeping the +# unaffected emSign roots. Must match the floor declared in pyproject.toml. +MIN_CERTIFI_VERSION = (2026, 10, 19) + +# SHA-256 fingerprint of "emSign ECC Root CA - G3", the root that signed the +# compromised eMudhra intermediates. +DISTRUSTED_ROOT_SHA256 = ( + "86a1ecba089c4a8d3bbe2734c612ba341d813e043cf9e8a862cd5c57a36bbe6b" +) + +_PEM_BLOCK = re.compile( + r"-----BEGIN CERTIFICATE-----.*?-----END CERTIFICATE-----", re.S +) + + +def _bundle_fingerprints(path): + """Return the set of SHA-256 fingerprints (hex) of every cert in a PEM bundle.""" + with open(path, "r", encoding="utf-8") as f: + data = f.read() + fingerprints = set() + for block in _PEM_BLOCK.findall(data): + der = ssl.PEM_cert_to_DER_cert(block) + fingerprints.add(hashlib.sha256(der).hexdigest()) + return fingerprints + + +class TestCertifiTrustStore: + """The resolved certifi bundle must not trust the compromised root.""" + + def test_certifi_meets_minimum_version(self): + """certifi must be at least the first release that distrusts the root + while retaining the unaffected emSign roots.""" + installed = tuple(int(p) for p in certifi.__version__.split(".")) + assert installed >= MIN_CERTIFI_VERSION, ( + f"certifi {certifi.__version__} is older than " + f"{'.'.join(map(str, MIN_CERTIFI_VERSION))}; its CA bundle still " + "trusts emSign ECC Root CA - G3" + ) + + def test_bundle_is_non_empty_and_parseable(self): + """Sanity check so the fingerprint test cannot pass on an empty bundle.""" + fingerprints = _bundle_fingerprints(certifi.where()) + assert len(fingerprints) > 50 + + def test_bundle_excludes_compromised_emsign_root(self): + """emSign ECC Root CA - G3 must be absent from the bundle, checked by + certificate fingerprint rather than by the human-readable label.""" + fingerprints = _bundle_fingerprints(certifi.where()) + assert DISTRUSTED_ROOT_SHA256 not in fingerprints, ( + "certifi bundle still contains emSign ECC Root CA - G3 " + f"({certifi.where()})" + ) + + +class TestRequestsUsesCertifi: + """The HTTPS client used for downloads must validate against certifi.""" + + def test_requests_default_ca_bundle_is_certifi(self): + """requests must resolve its default CA bundle from certifi, so the + certifi floor is what governs the download's trust store.""" + assert os.path.realpath(requests.certs.where()) == \ + os.path.realpath(certifi.where()) + + def test_download_file_does_not_disable_verification(self, tmp_path): + """download_file must never opt out of certificate verification.""" + fake_response = MagicMock() + fake_response.headers = {'content-length': '4'} + fake_response.iter_content.return_value = [b'data'] + fake_response.raise_for_status.return_value = None + + dest = os.path.join(str(tmp_path), 'out.bin') + with patch('pystackql.utils.download.requests.get', + return_value=fake_response) as mock_get: + download.download_file( + 'https://releases.stackql.io/stackql/latest/x.zip', + dest, showprogress=False) + + assert mock_get.called + assert mock_get.call_args.kwargs.get('verify', True) is not False