diff --git a/.github/workflows/tls-trust.yml b/.github/workflows/tls-trust.yml new file mode 100644 index 0000000..dae3636 --- /dev/null +++ b/.github/workflows/tls-trust.yml @@ -0,0 +1,18 @@ +name: TLS trust regression +on: [push, pull_request, workflow_dispatch] +permissions: + contents: read +jobs: + trust: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: sudo apt-get update && sudo apt-get install -y ca-certificates + - name: Verify the bootstrap OS trust store without bundled packages + run: python -S scripts/check_tls_trust.py + - name: Refresh installers and independently bundled certificates + run: python -m pip --cert /etc/ssl/certs/ca-certificates.crt install --upgrade 'pip>=26.2.2' 'certifi>=2026.10.19' + - run: python scripts/check_tls_trust.py diff --git a/Dockerfile b/Dockerfile index 61402d2..329b382 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,9 @@ # The first start prints the line that adds a machine (docker logs savetokens): # curl -fsSL http://:8787/join | sh -s (the code is in the same logs) FROM python:3.12-slim +RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates \ + && rm -rf /var/lib/apt/lists/* \ + && python -m pip --cert /etc/ssl/certs/ca-certificates.crt install --no-cache-dir --upgrade "pip>=26.2.2" "certifi>=2026.10.19" ENV PYTHONUNBUFFERED=1 \ PIP_NO_CACHE_DIR=1 \ diff --git a/scripts/check_tls_trust.py b/scripts/check_tls_trust.py new file mode 100644 index 0000000..b23f151 --- /dev/null +++ b/scripts/check_tls_trust.py @@ -0,0 +1,38 @@ +"""Fail a build if Python's default or installed certifi stores retain affected roots.""" +from __future__ import annotations + +import hashlib +import importlib +import json +import ssl +import sys + +AFFECTED_ROOTS = { + '7DD78D5F4F13459A83DFF9ABBBA62EDBAF6F2D102BF257FD712F4D9F2746ED8D', + 'F2EDAAFF3D9B701FB4D14C9F285F8C0BAD90FC3BCC1FA07D47F4718EDABA34A5', + 'D7A7A0FB5D7E2731D771E9484EBCDEF71D5F0C3E0A2948782BC83EE0EA699EF4', + '86A1ECBA089C4A8D3BBE2734C612BA341D813E043CF9E8A862CD5C57A36BBE6B', +} + + +def main(): + contexts = {'system': ssl.create_default_context()} + for module_name in ['certifi', 'pip._vendor.certifi']: + try: + module = importlib.import_module(module_name) + except ImportError: + continue + contexts[module_name] = ssl.create_default_context(cafile=module.where()) + results = [] + for name, context in contexts.items(): + found = sorted(AFFECTED_ROOTS & { + hashlib.sha256(cert).hexdigest().upper() + for cert in context.get_ca_certs(binary_form=True) + }) + results.append({'store': name, 'affected_roots': found}) + print(json.dumps({'python': sys.version.split()[0], 'stores': results})) + return int(any(row['affected_roots'] for row in results)) + + +if __name__ == '__main__': + raise SystemExit(main())