From b6f266abb037c44ebd82b78be0700a590c83ef2b Mon Sep 17 00:00:00 2001 From: Nick Hildebrant <234180643+nick-hildebrant-etas@users.noreply.github.com> Date: Fri, 23 Oct 2026 11:40:17 +0200 Subject: [PATCH] emudhra ca certificate update --- .github/renovate.json5 | 15 ++++++++++ .../.devcontainer/Dockerfile | 11 ++++++++ .../.devcontainer/bazel-feature/install.sh | 14 ++++++++-- .../.devcontainer/s-core-local/install.sh | 14 ++++++++-- .../s-core-local/tests/test_default.sh | 28 +++++++++++++++++++ .../.devcontainer/with-proxy-vars.Dockerfile | 11 ++++++++ tools/README.md | 6 ++-- tools/internal/refresh_lockfile_digests.py | 17 ++++++++--- tools/lockfiles/python_tools.bzl | 2 +- tools/lockfiles/uv.lock.json | 28 +++++++++---------- tools/tests/python_tool_runner_test.sh | 2 +- 11 files changed, 121 insertions(+), 27 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 650bff2..ddf79c6 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -64,6 +64,21 @@ "depNameTemplate": "anomalyco/opencode", "versioningTemplate": "semver" }, + { + // uv ships the TLS stack used by every Bazel and DevContainer Python + // tool, so it must track upstream security releases rather than being + // bumped by hand. Both the `uv` and `uvx` entries share one version. + "customType": "regex", + "fileMatch": [ + "^tools/lockfiles/uv\\.lock\\.json$" + ], + "matchStrings": [ + "\"version\":\\s*\"(?[^\"]+)\"" + ], + "datasourceTemplate": "github-releases", + "depNameTemplate": "astral-sh/uv", + "versioningTemplate": "semver" + }, { "customType": "regex", "fileMatch": [ diff --git a/src/s-core-devcontainer/.devcontainer/Dockerfile b/src/s-core-devcontainer/.devcontainer/Dockerfile index 2ba2494..ea50cde 100644 --- a/src/s-core-devcontainer/.devcontainer/Dockerfile +++ b/src/s-core-devcontainer/.devcontainer/Dockerfile @@ -16,6 +16,17 @@ FROM buildpack-deps:noble-curl LABEL dev.containers.features="common" +# Refresh the system trust store before anything else downloads over TLS. The +# base image carries the CA bundle from its own build date, and the +# common-utils feature deliberately runs with `upgradePackages: false`, so a +# distrusted CA would otherwise stay trusted for the life of the image. +# Deliberately unpinned: a pinned trust store is a stale trust store. +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates \ + && update-ca-certificates --fresh \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + COPY tools /usr/local/share/score-tools RUN userdel -f -r ubuntu diff --git a/src/s-core-devcontainer/.devcontainer/bazel-feature/install.sh b/src/s-core-devcontainer/.devcontainer/bazel-feature/install.sh index ea34c32..71cc7fc 100755 --- a/src/s-core-devcontainer/.devcontainer/bazel-feature/install.sh +++ b/src/s-core-devcontainer/.devcontainer/bazel-feature/install.sh @@ -28,13 +28,23 @@ rm -f "${COPY_TARGET}/devcontainer-features.env" "${COPY_TARGET}/devcontainer-fe # used by apt-get only inside this script DEBIAN_FRONTEND=noninteractive +apt-get update + +# Refresh the system trust store before the first TLS download. This must come +# before load_feature_versions.sh, which fetches yq over HTTPS, so every +# download in this script validates against a current set of roots. The feature +# is also usable standalone on a base image this repository does not control, +# so it cannot rely on the Dockerfile having done this already. +# Deliberately not pinned in versions.yaml: a pinned trust store is a stale +# trust store, and it has to be current before versions.yaml can even be read. +apt-get install -y --no-install-recommends ca-certificates +update-ca-certificates --fresh + # Read tool versions + metadata into environment variables . /usr/local/share/score-tools/internal/devcontainer/load_feature_versions.sh /devcontainer/features/bazel/versions.yaml ARCHITECTURE=$(dpkg --print-architecture) -apt-get update - # INSTALL CONTAINER BUILD DEPENDENCIES # Container build dependencies are not pinned, since they are removed anyway after container creation. apt-get install apt-transport-https -y diff --git a/src/s-core-devcontainer/.devcontainer/s-core-local/install.sh b/src/s-core-devcontainer/.devcontainer/s-core-local/install.sh index 8e3ded8..0258883 100755 --- a/src/s-core-devcontainer/.devcontainer/s-core-local/install.sh +++ b/src/s-core-devcontainer/.devcontainer/s-core-local/install.sh @@ -30,14 +30,24 @@ rm -f "${COPY_TARGET}/devcontainer-features.env" "${COPY_TARGET}/devcontainer-fe # used by apt-get only inside this script DEBIAN_FRONTEND=noninteractive +apt-get update + +# Refresh the system trust store before the first TLS download. This must come +# before load_feature_versions.sh, which fetches yq over HTTPS, so every +# download in this script validates against a current set of roots. The base +# image carries the CA bundle from its own build date and this feature runs +# with `upgradePackages: false`, so a distrusted CA would otherwise persist. +# Deliberately not pinned in versions.yaml: a pinned trust store is a stale +# trust store, and it has to be current before versions.yaml can even be read. +apt-get install -y --no-install-recommends ca-certificates +update-ca-certificates --fresh + # Read tool versions + metadata into environment variables . /usr/local/share/score-tools/internal/devcontainer/load_feature_versions.sh /devcontainer/features/s-core-local/versions.yaml ARCHITECTURE=$(dpkg --print-architecture) KERNEL=$(uname -s) -apt-get update - # Unminimize the image to include standard packages like man pages bash -c "yes || true" | unminimize apt-get install -y man-db manpages manpages-dev manpages-posix manpages-posix-dev diff --git a/src/s-core-devcontainer/.devcontainer/s-core-local/tests/test_default.sh b/src/s-core-devcontainer/.devcontainer/s-core-local/tests/test_default.sh index f83eb4a..146e8f0 100755 --- a/src/s-core-devcontainer/.devcontainer/s-core-local/tests/test_default.sh +++ b/src/s-core-devcontainer/.devcontainer/s-core-local/tests/test_default.sh @@ -21,6 +21,30 @@ KERNEL=$(uname -s) # Read tool versions + metadata into environment variables . /usr/local/share/score-tools/internal/devcontainer/load_feature_versions.sh /devcontainer/features/s-core-local/versions.yaml +# System trust store +# +# The October 2026 eMudhra incident compromised intermediate CA private keys +# chaining to "emSign ECC Root CA - G3"; Ubuntu distrusts that root as of +# ca-certificates 20260816ubuntu0.24.04.1. The install step upgrades the +# package, but image builds run with `--cache-from`, so a layer assembled +# before that upgrade could still be reused. Asserting the outcome here means +# a stale trust store fails CI instead of being published. +distrusted_root_cn="emSign ECC Root CA - G3" +distrusted_root_sha256="86:A1:EC:BA:08:9C:4A:8D:3B:BE:27:34:C6:12:BA:34:1D:81:3E:04:3C:F9:E8:A8:62:CD:5C:57:A3:6B:BE:6B" + +# Prove the bundle is readable first; otherwise the absence check below would +# pass simply because nothing could be parsed out of it. +check "validate the system CA bundle is present and parsable" bash -c \ + "[ \"\$(openssl crl2pkcs7 -nocrl -certfile /etc/ssl/certs/ca-certificates.crt | openssl pkcs7 -print_certs -noout | grep -c '^subject=')\" -gt 50 ]" +check "validate the compromised ${distrusted_root_cn} is not in the system CA bundle" bash -c \ + "! openssl crl2pkcs7 -nocrl -certfile /etc/ssl/certs/ca-certificates.crt | openssl pkcs7 -print_certs -noout | grep -qF '${distrusted_root_cn}'" + +# Bazel runs its JVM against this keystore (see the bazel feature's +# /etc/bazel.bazelrc), so it needs the same guarantee as the OpenSSL bundle. +# Matched on fingerprint because the keytool alias is derived from a filename. +check "validate the compromised ${distrusted_root_cn} is not in the Java keystore" bash -c \ + "! keytool -list -keystore /etc/ssl/certs/java/cacerts -storepass changeit | grep -qiF '${distrusted_root_sha256}'" + shellcheck_lockfile_version="$(/usr/local/share/score-tools/internal/devcontainer/install.py version shellcheck)" ruff_lockfile_version="$(/usr/local/share/score-tools/internal/devcontainer/install.py version ruff)" actionlint_lockfile_version="$(/usr/local/share/score-tools/internal/devcontainer/install.py version actionlint)" @@ -87,6 +111,10 @@ check "validate opencode is working and has the correct version" bash -c "openco # uv check "validate uv is working and has the correct version" bash -c "uv --version | grep '${uv_lockfile_version}'" check "validate uvx is working and has the correct version" bash -c "uvx --version | grep '${uvx_lockfile_version}'" +# Both ship from one upstream release and one archive. Renovate matches the two +# entries in uv.lock.json separately, so a partial bump would otherwise leave +# them silently pinned to different releases. +check "validate uv and uvx are pinned to the same release" bash -c "[ '${uv_lockfile_version}' = '${uvx_lockfile_version}' ]" # pycairo build from source (requires pkg-config and libcairo2-dev) check "validate pycairo can be built and installed from source" bash -c "pip install --break-system-packages pycairo==1.29.0" diff --git a/src/s-core-devcontainer/.devcontainer/with-proxy-vars.Dockerfile b/src/s-core-devcontainer/.devcontainer/with-proxy-vars.Dockerfile index ab546e9..430371e 100644 --- a/src/s-core-devcontainer/.devcontainer/with-proxy-vars.Dockerfile +++ b/src/s-core-devcontainer/.devcontainer/with-proxy-vars.Dockerfile @@ -32,6 +32,17 @@ ENV no_proxy=${no_proxy} LABEL dev.containers.features="common" +# Refresh the system trust store before anything else downloads over TLS. The +# base image carries the CA bundle from its own build date, and the +# common-utils feature deliberately runs with `upgradePackages: false`, so a +# distrusted CA would otherwise stay trusted for the life of the image. +# Deliberately unpinned: a pinned trust store is a stale trust store. +RUN apt-get update \ + && apt-get install -y --no-install-recommends ca-certificates \ + && update-ca-certificates --fresh \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* + # Unset proxy variables for all login shells COPY src/s-core-devcontainer/.devcontainer/unset-proxy.sh /etc/bash_completion.d/unset-proxy.sh COPY tools /usr/local/share/score-tools diff --git a/tools/README.md b/tools/README.md index c294ec7..78025c1 100644 --- a/tools/README.md +++ b/tools/README.md @@ -44,12 +44,12 @@ host-side Bazel invocation may require network access to download the binary. | `bazelisk` | `1.27.0` | Run the Bazel version selected by a repository | | `buildifier` | `8.2.1` | Format and lint Bazel files | | `opencode` | `1.18.31` | Run the OpenCode CLI | -| `pre-commit` | `4.5.1` | Run repository pre-commit hooks | +| `pre-commit` | `4.7.0` | Run repository pre-commit hooks | | `ruff` | `0.11.13` | Check and format Python code | | `shellcheck` | `0.10.0` | Check shell scripts | | `starpls` | `0.1.22` | Provide language-server support for Starlark | -| `uv` | `0.10.4` | Manage Python projects and packages | -| `uvx` | `0.10.4` | Run Python tools in isolated environments | +| `uv` | `0.13.5` | Manage Python projects and packages | +| `uvx` | `0.13.5` | Run Python tools in isolated environments | | `yamlfmt` | `0.17.0` | Format YAML files | diff --git a/tools/internal/refresh_lockfile_digests.py b/tools/internal/refresh_lockfile_digests.py index 8eb9004..d79de5e 100755 --- a/tools/internal/refresh_lockfile_digests.py +++ b/tools/internal/refresh_lockfile_digests.py @@ -68,10 +68,10 @@ def refresh_lockfile(path, token=None): continue # Derive the GitHub repo from the first binary URL: - # https://github.com///releases/download/v/ + # https://github.com///releases/download// first_url = tool["binaries"][0].get("url", "") parts = first_url.split("/") - if len(parts) < 8 or parts[2] != "github.com" or parts[5] != "releases": + if len(parts) < 9 or parts[2] != "github.com" or parts[5] != "releases": print( f" ! {tool_name}: cannot derive repo from {first_url!r}", file=sys.stderr, @@ -79,8 +79,15 @@ def refresh_lockfile(path, token=None): continue owner, repo = parts[3], parts[4] + # Release tags are not uniformly prefixed: most projects publish + # `v1.27.0`, while the astral-sh ones (uv, ruff) publish a bare + # `0.13.5`. Reuse the prefix the pinned URL already encodes, because + # Renovate rewrites the version inside that URL but never the prefix. + # Assuming `v` here silently rewrote those tools to a nonexistent tag. + tag = ("v" if parts[7].startswith("v") else "") + version + release = _api_get( - f"{GITHUB_API}/repos/{owner}/{repo}/releases/tags/v{version}", token + f"{GITHUB_API}/repos/{owner}/{repo}/releases/tags/{tag}", token ) digests = {a["name"]: a.get("digest", "") for a in release.get("assets", [])} @@ -96,7 +103,9 @@ def refresh_lockfile(path, token=None): sha = digest.split(":", 1)[1] if digest.startswith("sha256:") else digest # Rebuild the URL from the version so the file is consistent # whether or not Renovate already updated the URL. - new_url = f"https://github.com/{owner}/{repo}/releases/download/v{version}/{asset}" + new_url = ( + f"https://github.com/{owner}/{repo}/releases/download/{tag}/{asset}" + ) if binary.get("sha256") != sha or binary.get("url") != new_url: binary["url"] = new_url binary["sha256"] = sha diff --git a/tools/lockfiles/python_tools.bzl b/tools/lockfiles/python_tools.bzl index 216a52b..262d40b 100644 --- a/tools/lockfiles/python_tools.bzl +++ b/tools/lockfiles/python_tools.bzl @@ -21,7 +21,7 @@ PYTHON_TOOLS = { # users. They are separate because Python distributions may name them # differently. "package": "pre-commit", - "version": "4.5.1", + "version": "4.7.0", "entrypoint": "pre-commit", # Documentation is generated from the catalog so version and purpose # are reviewed together. diff --git a/tools/lockfiles/uv.lock.json b/tools/lockfiles/uv.lock.json index 5ab2f8f..b43edbd 100644 --- a/tools/lockfiles/uv.lock.json +++ b/tools/lockfiles/uv.lock.json @@ -1,14 +1,14 @@ { "$schema": "https://raw.githubusercontent.com/bazel-contrib/rules_multitool/main/lockfile.schema.json", "uv": { - "version": "0.10.4", + "version": "0.13.5", "description": "Manage Python projects and packages", "binaries": [ { "kind": "archive", "file": "uv-aarch64-apple-darwin/uv", - "url": "https://github.com/astral-sh/uv/releases/download/0.10.4/uv-aarch64-apple-darwin.tar.gz", - "sha256": "a6852e4dc565c8fedcf5adcdf09fca7caf5347739bed512bd95b15dada36db51", + "url": "https://github.com/astral-sh/uv/releases/download/0.13.5/uv-aarch64-apple-darwin.tar.gz", + "sha256": "743f85035a1996ecb350659e01cd9faea9709829be09b1b4b03c73eaf3c9cd73", "type": "tar.gz", "os": "macos", "cpu": "arm64" @@ -16,8 +16,8 @@ { "kind": "archive", "file": "uv-x86_64-unknown-linux-gnu/uv", - "url": "https://github.com/astral-sh/uv/releases/download/0.10.4/uv-x86_64-unknown-linux-gnu.tar.gz", - "sha256": "6b52a47358deea1c5e173278bf46b2b489747a59ae31f2a4362ed5c6c1c269f7", + "url": "https://github.com/astral-sh/uv/releases/download/0.13.5/uv-x86_64-unknown-linux-gnu.tar.gz", + "sha256": "f68d291c75783122dad58ab77def0e13f3233f358731db5d6d7e8715603b928f", "type": "tar.gz", "os": "linux", "cpu": "x86_64" @@ -25,8 +25,8 @@ { "kind": "archive", "file": "uv-aarch64-unknown-linux-gnu/uv", - "url": "https://github.com/astral-sh/uv/releases/download/0.10.4/uv-aarch64-unknown-linux-gnu.tar.gz", - "sha256": "c84a6e6405715caa6e2f5ef8e5f29a5d0bc558a954e9f1b5c082b9d4708c222e", + "url": "https://github.com/astral-sh/uv/releases/download/0.13.5/uv-aarch64-unknown-linux-gnu.tar.gz", + "sha256": "4bb69cf95ed13f23713bb7bc09f241896ea0c75b7c557e615a20604b82f9c403", "type": "tar.gz", "os": "linux", "cpu": "arm64" @@ -34,14 +34,14 @@ ] }, "uvx": { - "version": "0.10.4", + "version": "0.13.5", "description": "Run Python tools in isolated environments", "binaries": [ { "kind": "archive", "file": "uv-aarch64-apple-darwin/uvx", - "url": "https://github.com/astral-sh/uv/releases/download/0.10.4/uv-aarch64-apple-darwin.tar.gz", - "sha256": "a6852e4dc565c8fedcf5adcdf09fca7caf5347739bed512bd95b15dada36db51", + "url": "https://github.com/astral-sh/uv/releases/download/0.13.5/uv-aarch64-apple-darwin.tar.gz", + "sha256": "743f85035a1996ecb350659e01cd9faea9709829be09b1b4b03c73eaf3c9cd73", "type": "tar.gz", "os": "macos", "cpu": "arm64" @@ -49,8 +49,8 @@ { "kind": "archive", "file": "uv-x86_64-unknown-linux-gnu/uvx", - "url": "https://github.com/astral-sh/uv/releases/download/0.10.4/uv-x86_64-unknown-linux-gnu.tar.gz", - "sha256": "6b52a47358deea1c5e173278bf46b2b489747a59ae31f2a4362ed5c6c1c269f7", + "url": "https://github.com/astral-sh/uv/releases/download/0.13.5/uv-x86_64-unknown-linux-gnu.tar.gz", + "sha256": "f68d291c75783122dad58ab77def0e13f3233f358731db5d6d7e8715603b928f", "type": "tar.gz", "os": "linux", "cpu": "x86_64" @@ -58,8 +58,8 @@ { "kind": "archive", "file": "uv-aarch64-unknown-linux-gnu/uvx", - "url": "https://github.com/astral-sh/uv/releases/download/0.10.4/uv-aarch64-unknown-linux-gnu.tar.gz", - "sha256": "c84a6e6405715caa6e2f5ef8e5f29a5d0bc558a954e9f1b5c082b9d4708c222e", + "url": "https://github.com/astral-sh/uv/releases/download/0.13.5/uv-aarch64-unknown-linux-gnu.tar.gz", + "sha256": "4bb69cf95ed13f23713bb7bc09f241896ea0c75b7c557e615a20604b82f9c403", "type": "tar.gz", "os": "linux", "cpu": "arm64" diff --git a/tools/tests/python_tool_runner_test.sh b/tools/tests/python_tool_runner_test.sh index 91fae20..74f1f91 100755 --- a/tools/tests/python_tool_runner_test.sh +++ b/tools/tests/python_tool_runner_test.sh @@ -53,7 +53,7 @@ chmod +x "${fake_uvx}" # The explicit assertion verifies the requested release. Subsequent checks use # the parsed value to keep every expected uv argument aligned with the catalog. catalog_version="$("${installer}" version pre-commit)" -[[ "${catalog_version}" = "4.5.1" ]] +[[ "${catalog_version}" = "4.7.0" ]] # The Bazel launcher receives an execroot-relative uvx path, then changes to the # caller's workspace. This catches regressions where that path breaks after cd.