diff --git a/.github/workflows/test-pr.yaml b/.github/workflows/test-pr.yaml
index 1c86a2265..b2f7d4e0b 100644
--- a/.github/workflows/test-pr.yaml
+++ b/.github/workflows/test-pr.yaml
@@ -41,6 +41,16 @@ jobs:
terraform: ./**/terraform/**
nix: ./**/nix/**
+ python-sigstore:
+ needs: [detect-changes]
+ if: contains(fromJSON(needs.detect-changes.outputs.features), 'python')
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v7
+
+ - name: "Test Python Sigstore verification"
+ run: bash test/python/sigstore_verification_test.sh
+
test:
needs: [detect-changes]
runs-on: ubuntu-latest
diff --git a/src/python/NOTES.md b/src/python/NOTES.md
index c1d3ca0b2..9e8d3a77c 100644
--- a/src/python/NOTES.md
+++ b/src/python/NOTES.md
@@ -9,3 +9,7 @@ This Feature should work on recent versions of Debian/Ubuntu, RedHat Enterprise
## Additional Python versions
`additionalVersions` accepts a comma-separated list. Comma splitting is scoped to reading this list so package-manager commands keep normal shell word splitting during each installation.
+
+## Source Verification
+
+The feature verifies the authenticity and integrity of downloaded Python source archives. Python 3.14 and later use Sigstore (via Cosign) instead of GPG. For Python 3.14 and later, verification follows a fail-open policy: unsuccessful or unavailable verification is reported as a warning and does not block installation.
\ No newline at end of file
diff --git a/src/python/devcontainer-feature.json b/src/python/devcontainer-feature.json
index 8012bbc8e..218977bb3 100644
--- a/src/python/devcontainer-feature.json
+++ b/src/python/devcontainer-feature.json
@@ -1,6 +1,6 @@
{
"id": "python",
- "version": "1.8.1",
+ "version": "1.9.0",
"name": "Python",
"documentationURL": "https://github.com/devcontainers/features/tree/main/src/python",
"description": "Installs the provided version of Python, as well as PIPX, and other common Python utilities. JupyterLab is conditionally installed with the python feature. Note: May require source code compilation.",
diff --git a/src/python/install.sh b/src/python/install.sh
index 3eb028b80..94a907edf 100755
--- a/src/python/install.sh
+++ b/src/python/install.sh
@@ -16,7 +16,8 @@ PYTHON_INSTALL_PATH="${INSTALLPATH:-"/usr/local/python"}"
OVERRIDE_DEFAULT_VERSION="${OVERRIDEDEFAULTVERSION:-"true"}"
PYTHON_LAST_KNOWN_VERSION="3.14.7"
OPENSSL3_LAST_KNOWN_VERSION="3.0.22"
-COSIGN_LAST_KNOWN_VERSION="3.1.3"
+COSIGN_PINNED_VERSION="3.1.4"
+COSIGN_LAST_KNOWN_VERSION="3.1.4"
export PIPX_HOME=${PIPX_HOME:-"/usr/local/py-utils"}
@@ -40,6 +41,22 @@ KEYSERVER_PROXY="${HTTPPROXY:-"${HTTP_PROXY:-""}"}"
set -e
+# Keep Cosign binaries and downloaded Sigstore bundles available across Python installs.
+COSIGN_CACHE_DIR=""
+COSIGN_LATEST_VERSION=""
+COSIGN_LATEST_READY="false"
+COSIGN_PINNED_BINARY=""
+COSIGN_LATEST_BINARY=""
+COSIGN_ACTIVE_BINARY=""
+
+cleanup_cosign_cache() {
+ if [ -n "${COSIGN_CACHE_DIR}" ] && [ -d "${COSIGN_CACHE_DIR}" ]; then
+ rm -rf -- "${COSIGN_CACHE_DIR}" || echo "(!) WARNING: Failed to remove temporary Cosign cache." >&2
+ fi
+ return 0
+}
+trap cleanup_cosign_cache EXIT
+
if [ "$(id -u)" -ne 0 ]; then
echo -e 'Script must be run as root. Use sudo, su, or add "USER root" to your Dockerfile before running this script.'
exit 1
@@ -517,101 +534,267 @@ get_architecture() {
echo ${architecture}
}
-# Install cosign with multi-distro support
-install_cosign() {
+# Fetch Python.org's signer table and return the identity and issuer for a series.
+get_sigstore_signer_policy() {
+ local series="$1"
+ local policy_file="$2"
+ local policy_attempt=1
- COSIGN_VERSION="latest"
- local cosign_url='https://github.com/sigstore/cosign'
- local architecture=$(get_architecture)
+ if [ ! -s "${policy_file}" ]; then
+ while [ "${policy_attempt}" -le 3 ]; do
+ if curl -fsSL --connect-timeout 20 --max-time 60 -o "${policy_file}" "https://www.python.org/downloads/metadata/sigstore/"; then
+ break
+ fi
+ echo "(!) Signer policy download attempt ${policy_attempt}/3 failed" >&2
+ rm -f "${policy_file}"
+ policy_attempt=$((policy_attempt + 1))
+ if [ "${policy_attempt}" -le 3 ]; then
+ sleep 2
+ fi
+ done
+ fi
- find_version_from_git_tags COSIGN_VERSION "${cosign_url}" "tags/v" "." "false" "" "${COSIGN_LAST_KNOWN_VERSION}"
+ if [ ! -s "${policy_file}" ]; then
+ echo "(!) Failed to download Python.org's Sigstore signer policy" >&2
+ return 1
+ fi
- # Remove 'v' prefix if present for download URL
- local version_for_url="${COSIGN_VERSION#v}"
+ local policy
+ if ! policy="$(awk -v wanted_series="${series}" '
+function trim(value) {
+ sub(/^[[:space:]]+/, "", value)
+ sub(/[[:space:]]+$/, "", value)
+ return value
+}
+function process_row(value, cells, count, field_number) {
+ sub(/^.*
]*>/, "", value)
+ sub(/<\/tr>.*/, "", value)
+ gsub(/<\/t[dh][^>]*>/, "\034", value)
+ gsub(/<[^>]*>/, "", value)
+ gsub(/ | /, " ", value)
+ value = trim(value)
+ sub(/\034$/, "", value)
+ count = split(value, cells, "\034")
+ for (field_number = 1; field_number <= count; field_number++) {
+ cells[field_number] = trim(cells[field_number])
+ }
+ if (count == 4 && cells[1] == "Release" && cells[2] == "PEP" && cells[3] == "Release manager" && cells[4] == "OIDC Issuer") {
+ in_policy_table = 1
+ } else if (in_policy_table && count == 4 && cells[1] == wanted_series) {
+ match_count++
+ expected_identity = cells[3]
+ expected_issuer = cells[4]
+ }
+}
+/])/ { in_table = 1; in_policy_table = 0 }
+{
+ if (in_table) {
+ if (row == "" && /])/) {
+ row = $0
+ } else if (row != "") {
+ row = row " " $0
+ }
+ if (row != "" && /<\/tr>/) {
+ process_row(row)
+ row = ""
+ }
+ }
+ if (/<\/table>/) {
+ in_table = 0
+ in_policy_table = 0
+ }
+}
+END {
+ if (match_count != 1 || expected_identity !~ /^[^[:space:]<>@]+@[^[:space:]<>@]+$/ || expected_issuer !~ /^https:\/\/[^[:space:]<>]+$/) {
+ exit 1
+ }
+ printf "%s\t%s\n", expected_identity, expected_issuer
+}
+' "${policy_file}")"; then
+ echo "(!) Could not parse Python.org's Sigstore signer policy for Python ${series}" >&2
+ return 1
+ fi
+ printf '%s\n' "${policy}"
+}
- local cosign_filename="/tmp/cosign_${version_for_url}_${architecture}.deb"
- local cosign_url="https://github.com/sigstore/cosign/releases/download/v${version_for_url}/cosign_${version_for_url}_${architecture}.deb"
-
- echo "Downloading cosign from: ${cosign_url}"
-
- if curl -L -f --fail-with-body "${cosign_url}" -o "$cosign_filename" 2>/dev/null; then
- echo "(*) Successfully downloaded cosign v${COSIGN_VERSION}"
- else
- echo -e "\n(!) Failed to fetch cosign v${COSIGN_VERSION}..."
- # Try previous version
- find_prev_version_from_git_tags COSIGN_VERSION "https://github.com/sigstore/cosign"
- echo -e "\nAttempting to install previous cosign ${COSIGN_VERSION} version as fallback mechanism"
-
- version_for_url="${COSIGN_VERSION#v}"
- cosign_filename="/tmp/cosign_${version_for_url}_${architecture}.deb"
- cosign_url="https://github.com/sigstore/cosign/releases/download/v${version_for_url}/cosign_${version_for_url}_${architecture}.deb"
-
- if ! curl -L -f --fail-with-body "${cosign_url}" -o "$cosign_filename" 2>/dev/null; then
- echo "(!) Failed to download cosign v${COSIGN_VERSION} as fallback"
+# Prepare a trusted pinned Cosign binary and, when available, a verified latest v3 binary.
+prepare_cosign_binaries() {
+ local cosign_architecture="$1"
+ local checksum="$2"
+ local cosign_repo="https://github.com/sigstore/cosign"
+ local asset_name="cosign-linux-${cosign_architecture}"
+ local pinned_tmp latest_tmp latest_bundle version_result
+
+ if [ -z "${COSIGN_CACHE_DIR}" ]; then
+ COSIGN_CACHE_DIR="$(mktemp -d)" || {
+ echo "(!) Failed to create temporary Cosign cache directory." >&2
+ return 1
+ }
+ chmod 700 "${COSIGN_CACHE_DIR}"
+ fi
+
+ COSIGN_PINNED_BINARY="${COSIGN_CACHE_DIR}/cosign-${COSIGN_PINNED_VERSION}-${cosign_architecture}"
+
+ # Bootstrap trust: the pinned binary is verified against a checksum reviewed in this feature.
+ if [ ! -x "${COSIGN_PINNED_BINARY}" ]; then
+ pinned_tmp="${COSIGN_PINNED_BINARY}.download"
+ if ! curl -fsSL --connect-timeout 20 --max-time 300 --retry 2 --retry-delay 2 \
+ -o "${pinned_tmp}" "https://github.com/sigstore/cosign/releases/download/v${COSIGN_PINNED_VERSION}/${asset_name}"; then
+ echo "(!) Failed to download pinned Cosign v${COSIGN_PINNED_VERSION}." >&2
+ rm -f "${pinned_tmp}"
+ return 1
+ fi
+ if ! echo "${checksum} ${pinned_tmp}" | sha256sum -c -; then
+ echo "(!) Pinned Cosign v${COSIGN_PINNED_VERSION} checksum verification failed." >&2
+ rm -f "${pinned_tmp}"
+ return 1
+ fi
+ if ! chmod 0755 "${pinned_tmp}" || ! mv "${pinned_tmp}" "${COSIGN_PINNED_BINARY}"; then
+ echo "(!) Failed to prepare pinned Cosign v${COSIGN_PINNED_VERSION}." >&2
+ rm -f "${pinned_tmp}"
return 1
fi
fi
-
- # Install the package
- if [ -f "$cosign_filename" ]; then
- dpkg -i "$cosign_filename"
- rm "$cosign_filename"
- echo "Installation of cosign succeeded with ${COSIGN_VERSION}."
- else
- echo "(!) Failed to install cosign package"
- return 1
+
+ if [ "${COSIGN_LATEST_READY}" != "true" ]; then
+ # Keep automatic updates on the v3 line so this feature's verify-blob CLI remains compatible.
+ # Run the existing resolver in a subshell so a lookup failure does not terminate the installer.
+ if version_result="$( ( COSIGN_VERSION="3"; find_version_from_git_tags COSIGN_VERSION "${cosign_repo}" "tags/v" "." "false" "" "${COSIGN_LAST_KNOWN_VERSION}" ) 2>/dev/null )"; then
+ COSIGN_LATEST_VERSION="${version_result#COSIGN_VERSION=}"
+ else
+ COSIGN_LATEST_VERSION="${COSIGN_PINNED_VERSION}"
+ echo "(!) Could not resolve the latest Cosign v3 release; using pinned v${COSIGN_PINNED_VERSION}." >&2
+ fi
+ if [[ ! "${COSIGN_LATEST_VERSION}" =~ ^3\.[0-9]+\.[0-9]+$ ]]; then
+ COSIGN_LATEST_VERSION="${COSIGN_PINNED_VERSION}"
+ echo "(!) Cosign version lookup returned an invalid v3 tag; using pinned v${COSIGN_PINNED_VERSION}." >&2
+ fi
+ COSIGN_LATEST_READY="true"
+
+ if [ "${COSIGN_LATEST_VERSION}" = "${COSIGN_PINNED_VERSION}" ]; then
+ # The pinned binary is already the latest; do not download it twice.
+ COSIGN_LATEST_BINARY="${COSIGN_PINNED_BINARY}"
+ else
+ COSIGN_LATEST_BINARY="${COSIGN_CACHE_DIR}/cosign-${COSIGN_LATEST_VERSION}-${cosign_architecture}"
+ latest_tmp="${COSIGN_LATEST_BINARY}.download"
+ latest_bundle="${COSIGN_CACHE_DIR}/${asset_name}.sigstore.json"
+
+ if [ ! -x "${COSIGN_LATEST_BINARY}" ]; then
+ if curl -fsSL --connect-timeout 20 --max-time 300 --retry 2 --retry-delay 2 \
+ -o "${latest_tmp}" "https://github.com/sigstore/cosign/releases/download/v${COSIGN_LATEST_VERSION}/${asset_name}" \
+ && curl -fsSL --connect-timeout 20 --max-time 60 --retry 2 --retry-delay 2 \
+ -o "${latest_bundle}" "https://github.com/sigstore/cosign/releases/download/v${COSIGN_LATEST_VERSION}/${asset_name}.sigstore.json"; then
+ if ! chmod 0755 "${latest_tmp}"; then
+ echo "(!) Failed to prepare downloaded Cosign v${COSIGN_LATEST_VERSION}; retaining pinned Cosign." >&2
+ rm -f "${latest_tmp}" "${latest_bundle}"
+ COSIGN_LATEST_BINARY="${COSIGN_PINNED_BINARY}"
+ else
+ echo "(*) Verifying downloaded Cosign v${COSIGN_LATEST_VERSION} with pinned Cosign v${COSIGN_PINNED_VERSION}..."
+ if "${COSIGN_PINNED_BINARY}" verify-blob "${latest_tmp}" \
+ --bundle "${latest_bundle}" \
+ --certificate-identity "keyless@projectsigstore.iam.gserviceaccount.com" \
+ --certificate-oidc-issuer "https://accounts.google.com" \
+ && mv "${latest_tmp}" "${COSIGN_LATEST_BINARY}"; then
+ echo "(*) Cosign v${COSIGN_LATEST_VERSION} signature verification successful."
+ else
+ echo "(!) Latest Cosign signature verification failed; retaining pinned Cosign." >&2
+ rm -f "${latest_tmp}" "${COSIGN_LATEST_BINARY}"
+ COSIGN_LATEST_BINARY="${COSIGN_PINNED_BINARY}"
+ fi
+ fi
+ else
+ echo "(!) Failed to download latest Cosign v${COSIGN_LATEST_VERSION} or its Sigstore bundle; retaining pinned Cosign." >&2
+ rm -f "${latest_tmp}" "${latest_bundle}"
+ COSIGN_LATEST_BINARY="${COSIGN_PINNED_BINARY}"
+ fi
+ fi
+ rm -f "${latest_bundle}"
+ fi
fi
+ COSIGN_ACTIVE_BINARY="${COSIGN_LATEST_BINARY:-${COSIGN_PINNED_BINARY}}"
+ return 0
}
-# COSIGN signature verification for python versions >= 3.14
+# COSIGN signature verification for Python versions >= 3.14
cosign_verification() {
local VERSION="$1"
+ local series="${VERSION%.*}"
+ local identity issuer signer_policy
+ local architecture cosign_architecture checksum
+ local bundle_file bundle_attempt=1
+
+ architecture="$(get_architecture)" || return 1
+ case "${architecture}" in
+ amd64) cosign_architecture="amd64"; checksum="424e2c97d0b45fd7a6b608a87f090f35633f4c23ece5f092d48bba11aaf4bbfa" ;;
+ arm64) cosign_architecture="arm64"; checksum="ade83b7aac62e131cacfe96d958948b2150cc9efec46a570a2649b8e27dedad2" ;;
+ armhf) cosign_architecture="arm"; checksum="09e77a313910cf5c4774508d85d017d9c60983f41bf657f83ecbd3892f7952cb" ;;
+ *) echo "(!) No checksum-verified pinned Cosign binary available for architecture ${architecture}." >&2; return 1 ;;
+ esac
- # Ensure cosign is installed
- if ! type cosign > /dev/null 2>&1; then
- echo "(*) cosign not found, installing..."
- if ! install_cosign; then
- echo "(!) Failed to install cosign"
- return 1
- fi
- else
- echo "(*) cosign is already available on the system"
+ if [ -z "${COSIGN_CACHE_DIR}" ]; then
+ COSIGN_CACHE_DIR="$(mktemp -d)" || return 1
+ chmod 700 "${COSIGN_CACHE_DIR}"
fi
-
- echo "(*) Attempting COSIGN verification for Python ${VERSION}..."
-
- # Check if COSIGN signature files exist (these don't exist yet for Python releases)
- local cosign_sig_url="${cpython_tgz_url}.sig"
- local cosign_cert_url="${cpython_tgz_url}.pem"
-
- # Download COSIGN signature and certificate files with proper error handling
- echo "(*) Checking for cosign signature files..."
- if ! curl -sSL -f --fail-with-body -o "/tmp/python-src/${cpython_tgz_filename}.sig" "${cosign_sig_url}" 2>/dev/null; then
- echo "(!) COSIGN signature file not available for Python ${VERSION}"
- echo " Signature URL: ${cosign_sig_url}"
+
+ # Reuse Python.org's signer table from the same cache across additional Python versions.
+ if ! signer_policy="$(get_sigstore_signer_policy "${series}" "${COSIGN_CACHE_DIR}/python-sigstore-policy.html")"; then
+ echo "(!) Could not resolve a Sigstore signer policy for Python ${series}." >&2
return 1
fi
-
- if ! curl -sSL -f --fail-with-body -o "/tmp/python-src/${cpython_tgz_filename}.pem" "${cosign_cert_url}" 2>/dev/null; then
- echo "(!) COSIGN certificate file not available for Python ${VERSION}"
- echo " Certificate URL: ${cosign_cert_url}"
+ IFS=$'\t' read -r identity issuer <<< "${signer_policy}"
+
+ if ! prepare_cosign_binaries "${cosign_architecture}" "${checksum}"; then
return 1
fi
-
- # Perform COSIGN verification
- if cosign verify-blob \
- --certificate "/tmp/python-src/${cpython_tgz_filename}.pem" \
- --signature "/tmp/python-src/${cpython_tgz_filename}.sig" \
- --certificate-identity-regexp=".*" \
- --certificate-oidc-issuer-regexp=".*" \
+
+ bundle_file="${COSIGN_CACHE_DIR}/python-${VERSION}.sigstore"
+ while [ "${bundle_attempt}" -le 3 ]; do
+ if curl -fsSL --connect-timeout 20 --max-time 60 --retry 1 --retry-delay 2 \
+ -o "${bundle_file}" "${cpython_tgz_url}.sigstore"; then
+ break
+ fi
+ echo "(!) Sigstore bundle download attempt ${bundle_attempt}/3 failed" >&2
+ rm -f "${bundle_file}"
+ bundle_attempt=$((bundle_attempt + 1))
+ if [ "${bundle_attempt}" -le 3 ]; then
+ sleep 2
+ fi
+ done
+ if [ "${bundle_attempt}" -gt 3 ]; then
+ echo "(!) Failed to download the Sigstore bundle for Python ${VERSION}." >&2
+ rm -f "${bundle_file}"
+ return 1
+ fi
+
+ echo "(*) Verifying Python ${VERSION} with Sigstore signer ${identity}..."
+ if "${COSIGN_ACTIVE_BINARY}" verify-blob \
+ --bundle "${bundle_file}" \
+ --certificate-identity "${identity}" \
+ --certificate-oidc-issuer "${issuer}" \
"/tmp/python-src/${cpython_tgz_filename}"; then
- echo "(*) COSIGN signature verification successful"
+ rm -f "${bundle_file}"
+ echo "(*) Python ${VERSION} Sigstore verification successful."
return 0
- else
- echo "(!) COSIGN signature verification failed"
- return 1
fi
+
+ # A verified latest Cosign release can still have a regression; retry with the pinned verifier.
+ if [ "${COSIGN_ACTIVE_BINARY}" != "${COSIGN_PINNED_BINARY}" ]; then
+ echo "(!) Latest Cosign failed to verify Python ${VERSION}; retrying with pinned Cosign v${COSIGN_PINNED_VERSION}." >&2
+ if "${COSIGN_PINNED_BINARY}" verify-blob \
+ --bundle "${bundle_file}" \
+ --certificate-identity "${identity}" \
+ --certificate-oidc-issuer "${issuer}" \
+ "/tmp/python-src/${cpython_tgz_filename}"; then
+ rm -f "${bundle_file}"
+ echo "(*) Python ${VERSION} Sigstore verification successful with pinned Cosign."
+ return 0
+ fi
+ fi
+
+ rm -f "${bundle_file}"
+ echo "(!) Sigstore verification failed with both available Cosign verifiers." >&2
+ return 1
}
# GPG verification for python versions < 3.14
@@ -684,10 +867,9 @@ install_from_source() {
if cosign_verification "$VERSION"; then
echo "(*) COSIGN verification successful."
else
- echo "(*) COSIGN verification failed or not available for Python ${VERSION}"
- echo "(*) WARNING: Installing Python ${VERSION} without signature verification"
- echo "(*) This is expected for newly released versions where cosign signatures are not yet available"
- echo "(*) Python 3.14+ discontinued PGP signatures in favor of cosign, but cosign signatures may take time to be published"
+ echo "(!) WARNING: Sigstore verification failed or could not be completed for Python ${VERSION}." >&2
+ echo "(!) WARNING: Python ${VERSION} source authenticity and integrity have NOT been verified." >&2
+ echo "(!) Continuing installation under the existing fail-open policy." >&2
fi
else
echo "(*) Python < 3.14 detected. Using GPG signature verification..."
diff --git a/test/python/sigstore_verification_test.sh b/test/python/sigstore_verification_test.sh
new file mode 100644
index 000000000..427899214
--- /dev/null
+++ b/test/python/sigstore_verification_test.sh
@@ -0,0 +1,250 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+repository_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
+install_script="${INSTALL_SH:-${repository_root}/src/python/install.sh}"
+work_dir="$(mktemp -d)"
+trap 'rm -rf "${work_dir}"' EXIT
+mkdir -p "${work_dir}/tmp"
+export TMPDIR="${work_dir}/tmp"
+export MOCK_LOG="${work_dir}/cosign-args"
+export MOCK_CALLS="${work_dir}/cosign-calls"
+export MOCK_REJECT="false"
+export MOCK_REJECT_LATEST="false"
+export MOCK_REJECT_RELEASE="false"
+mock_architecture="x86_64"
+mock_download_failure=""
+mock_checksum_failure="false"
+mock_resolved_version="3.1.4"
+mock_resolver_failure="false"
+download_log="${work_dir}/downloads"
+checksum_log="${work_dir}/checksum"
+COSIGN_PINNED_VERSION="3.1.4"
+COSIGN_LAST_KNOWN_VERSION="${COSIGN_PINNED_VERSION}"
+COSIGN_CACHE_DIR=""
+COSIGN_LATEST_VERSION=""
+COSIGN_LATEST_READY="false"
+COSIGN_PINNED_BINARY=""
+COSIGN_LATEST_BINARY=""
+COSIGN_ACTIVE_BINARY=""
+
+cat > "${work_dir}/cosign" <<'COSIGN'
+#!/usr/bin/env bash
+printf '%s\n' "$@" > "${MOCK_LOG}"
+printf '%s|%s\n' "$(basename "$0")" "$*" >> "${MOCK_CALLS}"
+if [[ "$*" == *'keyless@projectsigstore.iam.gserviceaccount.com'* ]]; then
+ [[ "${MOCK_REJECT_RELEASE}" != "true" ]]
+elif [[ "$(basename "$0")" == cosign-3.2.0-* ]]; then
+ [[ "${MOCK_REJECT_LATEST}" != "true" && "${MOCK_REJECT}" != "true" ]]
+else
+ [[ "${MOCK_REJECT}" != "true" ]]
+fi
+COSIGN
+chmod 0755 "${work_dir}/cosign"
+
+uname() { printf '%s\n' "${mock_architecture}"; }
+sleep() { :; }
+
+find_version_from_git_tags() {
+ local variable_name="$1"
+ [[ "${7:-}" == "${COSIGN_LAST_KNOWN_VERSION}" ]] || return 1
+ [[ "${mock_resolver_failure}" != "true" ]] || return 1
+ printf '%s=%s\n' "${variable_name}" "${mock_resolved_version}"
+}
+
+curl() {
+ local destination="" url=""
+ while [[ $# -gt 0 ]]; do
+ if [[ "$1" == "-o" ]]; then
+ destination="$2"
+ shift 2
+ else
+ url="$1"
+ shift
+ fi
+ done
+ printf '%s\n' "${url}" >> "${download_log}"
+ if [[ -n "${mock_download_failure}" && "${url}" == *"${mock_download_failure}"* ]]; then
+ return 22
+ fi
+ if [[ "${url}" == */downloads/metadata/sigstore/ ]]; then
+ cat > "${destination}" <<'POLICY'
+
+| Release | PEP | Release manager | OIDC Issuer |
+
+| 3.14 | PEP 745 | hugo@python.org | https://github.com/login/oauth |
+| 3.15 | PEP 790 | hugo@python.org | https://github.com/login/oauth |
+| 3.16 | PEP 826 | savannah@python.org | https://github.com/login/oauth |
+
+
+POLICY
+ elif [[ "${url}" == *.sigstore || "${url}" == *.sigstore.json ]]; then
+ printf 'bundle\n' > "${destination}"
+ else
+ cp "${work_dir}/cosign" "${destination}"
+ fi
+}
+
+sha256sum() {
+ cat >> "${checksum_log}"
+ [[ "${mock_checksum_failure}" != "true" ]]
+}
+
+# Stop at the GPG function itself, independent of the preceding comment's case.
+helper_functions="$(awk '
+ /^get_architecture\(\)[[:space:]]*\{[[:space:]]*$/ { copying = 1 }
+ copying && /^gpg_verification\(\)[[:space:]]*\{[[:space:]]*$/ { exit }
+ copying { print }
+' "${install_script}")"
+
+[[ "${helper_functions}" == *'cosign_verification() {'* ]] || {
+ echo "Failed to extract Sigstore helpers from ${install_script}" >&2
+ exit 1
+}
+
+source /dev/stdin <<< "${helper_functions}"
+
+cpython_tgz_filename="Python-3.14.10.tgz"
+cpython_tgz_url="https://www.python.org/ftp/python/3.14.10/${cpython_tgz_filename}"
+policy_url="https://www.python.org/downloads/metadata/sigstore/"
+
+reset_cache() {
+ if [[ -n "${COSIGN_CACHE_DIR}" && -d "${COSIGN_CACHE_DIR}" ]]; then
+ rm -rf -- "${COSIGN_CACHE_DIR}"
+ fi
+ COSIGN_CACHE_DIR=""
+ COSIGN_LATEST_VERSION=""
+ COSIGN_LATEST_READY="false"
+ COSIGN_PINNED_BINARY=""
+ COSIGN_LATEST_BINARY=""
+ COSIGN_ACTIVE_BINARY=""
+ [[ -z "$(find "${TMPDIR}" -mindepth 1 -print -quit)" ]]
+}
+assert_downloaded() { grep -Fqx -- "$1" "${download_log}"; }
+assert_called() { grep -Fq -- "$1" "${MOCK_CALLS}"; }
+
+# Pinned verifier: signer selection, arguments, checksum, and cache reuse.
+cosign_verification 3.14.10
+assert_downloaded "${policy_url}"
+assert_downloaded 'https://github.com/sigstore/cosign/releases/download/v3.1.4/cosign-linux-amd64'
+assert_downloaded "${cpython_tgz_url}.sigstore"
+grep -Fq -- '424e2c97d0b45fd7a6b608a87f090f35633f4c23ece5f092d48bba11aaf4bbfa' "${checksum_log}"
+grep -Fxq -- 'hugo@python.org' "${MOCK_LOG}"
+grep -Fxq -- 'https://github.com/login/oauth' "${MOCK_LOG}"
+cosign_verification 3.15.0
+grep -Fxq -- 'hugo@python.org' "${MOCK_LOG}"
+cosign_verification 3.16.0
+grep -Fxq -- 'savannah@python.org' "${MOCK_LOG}"
+[[ "$(grep -Fc -- "${policy_url}" "${download_log}")" -eq 1 ]]
+[[ "$(grep -Fc -- '/v3.1.4/cosign-linux-amd64' "${download_log}")" -eq 1 ]]
+reset_cache
+
+mock_architecture="armv7l"
+cosign_verification 3.14.10
+assert_downloaded 'https://github.com/sigstore/cosign/releases/download/v3.1.4/cosign-linux-arm'
+grep -Fq -- '09e77a313910cf5c4774508d85d017d9c60983f41bf657f83ecbd3892f7952cb' "${checksum_log}"
+reset_cache
+mock_architecture="x86_64"
+
+export MOCK_REJECT="true"
+if cosign_verification 3.14.10; then exit 1; fi
+export MOCK_REJECT="false"
+reset_cache
+
+mock_checksum_failure="true"
+if cosign_verification 3.14.10; then exit 1; fi
+mock_checksum_failure="false"
+reset_cache
+
+mock_download_failure="/downloads/metadata/sigstore/"
+if cosign_verification 3.14.10; then exit 1; fi
+reset_cache
+mock_download_failure=".sigstore"
+if cosign_verification 3.14.10; then exit 1; fi
+reset_cache
+mock_download_failure="cosign-linux-amd64"
+if cosign_verification 3.14.10; then exit 1; fi
+mock_download_failure=""
+reset_cache
+
+: > "${download_log}"
+if cosign_verification 3.99.0; then exit 1; fi
+assert_downloaded "${policy_url}"
+if grep -Eq 'cosign-linux-|\.sigstore$' "${download_log}"; then exit 1; fi
+reset_cache
+
+: > "${download_log}"
+mock_architecture="unsupported"
+if cosign_verification 3.14.10; then exit 1; fi
+[[ ! -s "${download_log}" ]]
+reset_cache
+mock_architecture="x86_64"
+
+# Promote a newer Cosign only after pinned Cosign verifies its Sigstore bundle.
+mock_resolved_version="3.2.0"
+: > "${MOCK_CALLS}"
+: > "${download_log}"
+cosign_verification 3.14.10
+assert_downloaded 'https://github.com/sigstore/cosign/releases/download/v3.2.0/cosign-linux-amd64'
+assert_downloaded 'https://github.com/sigstore/cosign/releases/download/v3.2.0/cosign-linux-amd64.sigstore.json'
+assert_called 'cosign-3.1.4-amd64|verify-blob'
+assert_called 'keyless@projectsigstore.iam.gserviceaccount.com'
+assert_called 'https://accounts.google.com'
+assert_called 'cosign-3.2.0-amd64|verify-blob'
+[[ "${COSIGN_ACTIVE_BINARY}" == *'/cosign-3.2.0-amd64' ]]
+reset_cache
+
+# Reject an unauthenticated newer Cosign binary; continue with the pinned one.
+export MOCK_REJECT_RELEASE="true"
+: > "${MOCK_CALLS}"
+cosign_verification 3.14.10
+[[ "${COSIGN_ACTIVE_BINARY}" == "${COSIGN_PINNED_BINARY}" ]]
+assert_called 'keyless@projectsigstore.iam.gserviceaccount.com'
+if grep -Fq 'cosign-3.2.0-amd64|verify-blob' "${MOCK_CALLS}"; then exit 1; fi
+export MOCK_REJECT_RELEASE="false"
+reset_cache
+
+# Newer verifier rejects Python; pinned verifier retries successfully.
+export MOCK_REJECT_LATEST="true"
+: > "${MOCK_CALLS}"
+cosign_verification 3.14.10
+assert_called 'cosign-3.2.0-amd64|verify-blob'
+assert_called 'cosign-3.1.4-amd64|verify-blob'
+export MOCK_REJECT_LATEST="false"
+reset_cache
+
+# Both verifiers reject Python; verification must fail.
+export MOCK_REJECT="true"
+if cosign_verification 3.14.10; then exit 1; fi
+export MOCK_REJECT="false"
+reset_cache
+
+# Newer release download or bundle download fails; pinned remains usable.
+mock_download_failure="/v3.2.0/cosign-linux-amd64.sigstore.json"
+cosign_verification 3.14.10
+[[ "${COSIGN_ACTIVE_BINARY}" == "${COSIGN_PINNED_BINARY}" ]]
+mock_download_failure=""
+reset_cache
+
+# Newer executable download fails; pinned remains usable.
+mock_download_failure="/v3.2.0/cosign-linux-amd64"
+cosign_verification 3.14.10
+[[ "${COSIGN_ACTIVE_BINARY}" == "${COSIGN_PINNED_BINARY}" ]]
+mock_download_failure=""
+reset_cache
+
+# Invalid major-version resolution must not promote an unsupported verifier.
+mock_resolved_version="4.0.0"
+cosign_verification 3.14.10
+[[ "${COSIGN_ACTIVE_BINARY}" == "${COSIGN_PINNED_BINARY}" ]]
+mock_resolved_version="3.2.0"
+reset_cache
+
+# Tag lookup failure must retain the pinned trusted verifier.
+mock_resolver_failure="true"
+cosign_verification 3.14.10
+[[ "${COSIGN_ACTIVE_BINARY}" == "${COSIGN_PINNED_BINARY}" ]]
+mock_resolver_failure="false"
+reset_cache
+
+printf 'Sigstore verification regression checks passed\n'
\ No newline at end of file