Skip to content
3 changes: 2 additions & 1 deletion src/python/devcontainer-feature.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"id": "python",
"version": "1.7.1",
"version": "1.8.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.",
Expand All @@ -9,6 +9,7 @@
"type": "string",
"proposals": [
"latest",
"3.14",
"os-provided",
"none",
"3.12",
Expand Down
236 changes: 225 additions & 11 deletions src/python/install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -77,11 +77,11 @@ if [ ${ADJUSTED_ID} = "rhel" ] && [ ${ID} != "rhel" ]; then
INSTALL_CMD_ADDL_REPOS="--enablerepo crb"
fi
fi

# Setup INSTALL_CMD & PKG_MGR_CMD
if type apt-get > /dev/null 2>&1; then
PKG_MGR_CMD=apt-get
INSTALL_CMD="${PKG_MGR_CMD} -y install --no-install-recommends"
TIME_PIECE_PKG="libtime-piece-perl"
elif type microdnf > /dev/null 2>&1; then
PKG_MGR_CMD=microdnf
INSTALL_CMD="${PKG_MGR_CMD} ${INSTALL_CMD_ADDL_REPOS} -y install --refresh --best --nodocs --noplugins --setopt=install_weak_deps=0"
Expand All @@ -92,6 +92,57 @@ else
PKG_MGR_CMD=yum
INSTALL_CMD="${PKG_MGR_CMD} ${INSTALL_CMD_ADDL_REPOS} -y install --noplugins --setopt=install_weak_deps=0"
fi
# Set TIME_PIECE_PKG for RHEL-based systems (all except apt-get)
if [ "${PKG_MGR_CMD}" != "apt-get" ]; then
TIME_PIECE_PKG="perl-Time-Piece"
fi
# Install Time::Piece Perl module required by OpenSSL 3.0.18+ build system
install_time_piece() {
echo "(*) Ensuring Time::Piece Perl module is available..."

# Check if Time::Piece is already available (it's usually in Perl core)
if perl -MTime::Piece -e 'exit 0' 2>/dev/null; then
echo "(*) Time::Piece already available"
return 0
fi

echo "(*) Time::Piece not found, attempting installation..."

case ${ADJUSTED_ID} in
debian)
# Update package cache
pkg_mgr_update

# Try different package combinations
if ${INSTALL_CMD} perl-modules-5.36 2>/dev/null; then
echo "(*) Installed perl-modules-5.36"
elif ${INSTALL_CMD} perl-modules-5.34 2>/dev/null; then
echo "(*) Installed perl-modules-5.34"
elif ${INSTALL_CMD} perl-modules 2>/dev/null; then
echo "(*) Installed perl-modules"
elif ${INSTALL_CMD} perl-base perl-modules-5.* 2>/dev/null; then
echo "(*) Installed perl-base and modules"
else
echo "(*) Warning: Could not install Time::Piece via packages"
echo "(*) Time::Piece is usually built into Perl core, continuing..."
fi
;;
rhel)
${INSTALL_CMD} ${TIME_PIECE_PKG} || {
echo "(*) Warning: Could not install ${TIME_PIECE_PKG}"
}
;;
esac

# Final check
if perl -MTime::Piece -e 'exit 0' 2>/dev/null; then
echo "(*) Time::Piece is now available"
else
echo "(*) Warning: Time::Piece may not be available"
echo "(*) This could cause issues with OpenSSL 3.0.18+ builds"
fi
}


# Clean up
clean_up() {
Expand Down Expand Up @@ -478,6 +529,175 @@ install_cpython() {
curl -sSL -o "/tmp/python-src/${cpython_tgz_filename}" "${cpython_tgz_url}"
fi
}
# Get system architecture for downloads
get_architecture() {
local architecture=""
case $(uname -m) in
x86_64) architecture="amd64" ;;
aarch64 | armv8*) architecture="arm64" ;;
aarch32 | armv7* | armvhf*) architecture="armhf" ;;
i?86) architecture="386" ;;
*) echo "(!) Architecture $(uname -m) unsupported"; exit 1 ;;
esac
echo ${architecture}
}

# cosign installation
install_cosign() {
local COSIGN_VERSION="$1"
local architecture=$(get_architecture)

# Remove 'v' prefix if present for download URL
local version_for_url="${COSIGN_VERSION#v}"

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}"
curl -L "${cosign_url}" -o "$cosign_filename"

# Check if download was successful
if [ ! -f "$cosign_filename" ] || grep -q "Not Found\|404" "$cosign_filename"; then
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 ${COSIGN_VERSION}"

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"
curl -L "${cosign_url}" -o "$cosign_filename"
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 download cosign package"
return 1
fi
}

# Install 'cosign' for validating signatures from 3.14 onwards
ensure_cosign() {
check_packages curl ca-certificates gnupg2

if ! type cosign > /dev/null 2>&1; then
echo "Installing cosign..."
COSIGN_VERSION="latest"
cosign_url='https://github.com/sigstore/cosign'
find_version_from_git_tags COSIGN_VERSION "${cosign_url}"
install_cosign "${COSIGN_VERSION}"
fi
if ! type cosign > /dev/null 2>&1; then
echo "(!) Failed to install cosign."
return 1
fi
cosign version
return 0
}

# Updated signature verification logic with proper version-specific handling
verify_python_signature() {
local VERSION="$1"
local major_version=$(echo "$VERSION" | cut -d. -f1)
local minor_version=$(echo "$VERSION" | cut -d. -f2)

# Version-specific signature verification
if [ "$major_version" -eq 3 ] && [ "$minor_version" -ge 14 ]; then
echo "(*) Python 3.14+ detected. Attempting cosign verification..."

# Try to install and use cosign for 3.14+
if ensure_cosign; then
echo "Using cosign to verify Python ${VERSION} signature..."

# Attempt actual COSIGN verification
if perform_cosign_verification "${VERSION}"; then
echo "(*) COSIGN verification successful"
return 0
else
echo "(!) COSIGN verification failed"
fi
else
echo "(!) Failed to install cosign for Python 3.14+"
echo "(*) Skipping signature verification for Python ${VERSION}"
return 0
fi
else
# Direct GPG verification for Python < 3.14
echo "(*) Python < 3.14 detected. Using GPG signature verification..."
perform_gpg_verification "${VERSION}"
fi
}

# Extracted GPG verification logic to avoid duplication
perform_gpg_verification() {
local VERSION="$1"

echo "(*) Using GPG signature verification..."
if [[ ${VERSION_CODENAME} = "centos7" ]] || [[ ${VERSION_CODENAME} = "rhel7" ]]; then
receive_gpg_keys_centos7 PYTHON_SOURCE_GPG_KEYS
else
receive_gpg_keys PYTHON_SOURCE_GPG_KEYS
fi

echo "Downloading ${cpython_tgz_filename}.asc..."
if ! curl -sSL -o "/tmp/python-src/${cpython_tgz_filename}.asc" "${cpython_tgz_url}.asc"; then
echo "(!) Failed to download signature file"
echo "(*) Skipping signature verification for Python ${VERSION}"
return 0
fi

# Verify the signature
if ! gpg --verify "${cpython_tgz_filename}.asc" "${cpython_tgz_filename}"; then
echo "(!) GPG signature verification failed"
echo "(*) This may be normal for pre-release versions"
echo "(*) Continuing with installation..."
return 0
fi

echo "(*) GPG signature verification successful"
return 0
}

# COSIGN signature verification logic
perform_cosign_verification() {
local VERSION="$1"

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
if ! curl -sSL -o "/tmp/python-src/${cpython_tgz_filename}.sig" "${cosign_sig_url}"; then
echo "(!) COSIGN signature file not available for Python ${VERSION}"
return 1
fi

if ! curl -sSL -o "/tmp/python-src/${cpython_tgz_filename}.pem" "${cosign_cert_url}"; then
echo "(!) COSIGN certificate file not available for Python ${VERSION}"
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=".*" \
"/tmp/python-src/${cpython_tgz_filename}"; then
echo "(*) COSIGN signature verification successful"
return 0
else
echo "(!) COSIGN signature verification failed"
return 1
fi
}


install_from_source() {
VERSION=$1
Expand All @@ -496,6 +716,8 @@ install_from_source() {
case ${VERSION_CODENAME} in
centos7|rhel7)
check_packages perl-IPC-Cmd
# Call the installation function install_time_piece
install_time_piece
install_openssl3
ADDL_CONFIG_ARGS="--with-openssl=${SSL_INSTALL_PATH} --with-openssl-rpath=${SSL_INSTALL_PATH}/lib"
;;
Expand All @@ -506,16 +728,9 @@ install_from_source() {
if grep -q "404 Not Found" "/tmp/python-src/${cpython_tgz_filename}"; then
install_prev_vers_cpython "${VERSION}"
fi
fi;
# Verify signature
if [[ ${VERSION_CODENAME} = "centos7" ]] || [[ ${VERSION_CODENAME} = "rhel7" ]]; then
receive_gpg_keys_centos7 PYTHON_SOURCE_GPG_KEYS
else
receive_gpg_keys PYTHON_SOURCE_GPG_KEYS
fi
echo "Downloading ${cpython_tgz_filename}.asc..."
curl -sSL -o "/tmp/python-src/${cpython_tgz_filename}.asc" "${cpython_tgz_url}.asc"
gpg --verify "${cpython_tgz_filename}.asc"
# Verify signature
verify_python_signature "${VERSION}"

# Update min protocol for testing only - https://bugs.python.org/issue41561
if [ -f /etc/pki/tls/openssl.cnf ]; then
Expand Down Expand Up @@ -555,7 +770,6 @@ install_from_source() {
ln -s "${INSTALL_PATH}/bin/python3-config" "${INSTALL_PATH}/bin/python-config"

add_symlink

}

install_using_oryx() {
Expand Down
99 changes: 99 additions & 0 deletions test/python/python_sig_veri_older_versions.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/bin/bash

set -e
source dev-container-features-test-lib

echo "=== Python Signature Verification Check ==="

# Find all Python versions
PRIMARY_VERSION=$(python3 --version | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
echo "Primary: $PRIMARY_VERSION"

declare -A VERSIONS
VERSIONS["$PRIMARY_VERSION"]="python3"

# Look for additional Python versions
for py in /usr/local/python/*/bin/python3; do
if [ -x "$py" ] && [[ "$py" != *"/current/"* ]]; then
ver=$($py --version 2>&1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')
if [ -n "$ver" ] && [ "$ver" != "$PRIMARY_VERSION" ]; then
VERSIONS["$ver"]="$py"
echo "Found: $ver"
fi
fi
done

echo "Total Python versions: ${#VERSIONS[@]}"

# Test each version works
for version in $(printf '%s\n' "${!VERSIONS[@]}" | sort -V); do
py_cmd="${VERSIONS[$version]}"
major=$(echo "$version" | cut -d. -f1)
minor=$(echo "$version" | cut -d. -f2)

# Basic functionality test
check "python $version works" $py_cmd -c "print('OK')"

# Version classification test
if [ "$major" -eq 3 ] && [ "$minor" -ge 14 ]; then
check "python $version identified as 3.14+" test "$major" -eq 3 -a "$minor" -ge 14
echo " Python $version: COSIGN→GPG fallback path"
else
check "python $version identified as <3.14" test "$major" -eq 3 -a "$minor" -lt 14
echo " Python $version: GPG-only path"
fi
done

# Essential tool checks
check "GPG available" command -v gpg
check "curl available" command -v curl

# COSIGN availability check
has_python_314_plus=false
for version in $(printf '%s\n' "${!VERSIONS[@]}"); do
major=$(echo "$version" | cut -d. -f1)
minor=$(echo "$version" | cut -d. -f2)
if [ "$major" -eq 3 ] && [ "$minor" -ge 14 ]; then
has_python_314_plus=true
break
fi
done

if [ "$has_python_314_plus" = true ]; then
if command -v cosign >/dev/null 2>&1; then
echo "✅ COSIGN installed (required for Python 3.14+)"
check "COSIGN available for Python 3.14+" command -v cosign
else
echo "❌ COSIGN missing but required for Python 3.14+"
fi
else
echo "ℹ️ No Python 3.14+ versions - COSIGN not required"
fi

# Final validation: count working versions (but don't fail if some don't work)
echo "Checking Python version functionality..."
working_versions=0
total_versions=${#VERSIONS[@]}

for version in $(printf '%s\n' "${!VERSIONS[@]}"); do
py_cmd="${VERSIONS[$version]}"
if $py_cmd -c "print('Test')" >/dev/null 2>&1; then
working_versions=$((working_versions + 1))
echo " ✅ Python $version working"
else
echo " ⚠️ Python $version not responding"
fi
done

# Use a more lenient check - as long as we have some working versions
if [ "$working_versions" -gt 0 ]; then
check "At least one Python version functional" test "$working_versions" -gt 0
echo "✅ $working_versions/$total_versions Python versions working"
else
check "At least one Python version functional" false
fi

echo "✅ Test completed!"
echo "Summary: $total_versions Python versions found, $working_versions working"

reportResults
Loading