From 0c6830f400ca8569429575c74300ea4a452984d0 Mon Sep 17 00:00:00 2001 From: Vitaly Legchilkin Date: Tue, 25 Nov 2025 16:01:53 +0100 Subject: [PATCH] PY-85756 use sigstore to verify python distributive signatures instead of GPG GitOrigin-RevId: 8131263a42bd93e6830d74b5adaf72de3d32887a --- .../build/pycharm/sdks/PythonUpdater.kt | 60 +++++++++++++++---- 1 file changed, 48 insertions(+), 12 deletions(-) diff --git a/python/build/src/org/jetbrains/intellij/build/pycharm/sdks/PythonUpdater.kt b/python/build/src/org/jetbrains/intellij/build/pycharm/sdks/PythonUpdater.kt index 8653cc965ba6..abb581fac412 100644 --- a/python/build/src/org/jetbrains/intellij/build/pycharm/sdks/PythonUpdater.kt +++ b/python/build/src/org/jetbrains/intellij/build/pycharm/sdks/PythonUpdater.kt @@ -17,7 +17,9 @@ import org.jetbrains.intellij.build.pycharm.runCommand import org.jetbrains.jps.util.JpsChecksumUtil import org.jsoup.Jsoup import org.jsoup.nodes.Document +import java.net.HttpURLConnection import java.nio.file.Paths +import kotlin.io.path.exists import kotlin.io.path.fileSize @@ -64,23 +66,45 @@ class PythonUpdater { * Tries to get from the cache first. If it is a new resources then it downloads the resource file and the GPG signature of it. * Generates SHA256 and fileSize fields right after the GPG signature check. */ - private fun buildResource(version: String, trackedResource: TrackedResource): Resource { + private fun buildResource(languageLevel: LanguageLevel, version: String, trackedResource: TrackedResource): Resource { val resUrl = Urls.parseEncoded("${CPYTHON_BASE_URL}/$version/${trackedResource.filename(version)}")!! RESOURCE_CACHE[resUrl]?.let { return it } println("New resource was found ${resUrl}") val ascUrl = Urls.parseEncoded("${resUrl}.asc")!! + val sigstoreUrl = Urls.parseEncoded("${resUrl}.sigstore")!! val tempPath = PathManager.getTempPath() - val files = listOf(resUrl, ascUrl).associateWith { url -> + val files = listOf(resUrl, ascUrl, sigstoreUrl).associateWith { url -> val fileName = PathUtilRt.getFileName(url.getPath()) Paths.get(tempPath, "${System.nanoTime()}-${fileName}") } try { for ((url, path) in files) { - HttpRequests.request(url).saveToFile(path.toFile(), null) + try { + HttpRequests.request(url).saveToFile(path, null) + } + catch (ex: HttpRequests.HttpStatusException) { + if (ex.statusCode != HttpURLConnection.HTTP_NOT_FOUND) throw ex + } + } + val (resFilePath, ascFilePath, sigstoreFilePath) = files.values.map { it.toAbsolutePath() }.toTypedArray() + if (sigstoreFilePath.exists()) { + val manager = requireNotNull(PYTHON_RELEASE_MANAGERS[languageLevel]) { + "there is no manager defined for the $languageLevel, please specify manager in the PYTHON_RELEASE_MANAGERS\n" + + "check here: https://www.python.org/downloads/metadata/sigstore/" + } + runCommand( + "cosign", "verify-blob", + "--new-bundle-format", + "--certificate-identity=${manager.identity}", + "--certificate-oidc-issuer=${manager.oidcIssuer}", + "--bundle", sigstoreFilePath.toString(), + resFilePath.toString() + ) + } + else { + runCommand("gpg", "--verify", ascFilePath.toString(), resFilePath.toString()) } - val (resFilePath, ascFilePath) = files.values.map { it.toAbsolutePath().toString() }.toTypedArray() - runCommand("gpg", "--verify", ascFilePath, resFilePath) val file = files[resUrl]!! val sha256 = JpsChecksumUtil.getSha256Checksum(file) return Resource(resUrl, file.fileSize(), sha256) @@ -90,13 +114,13 @@ class PythonUpdater { } } - private fun buildBinary(version: String, trackedResource: TrackedResource): Binary { - return Binary(trackedResource.os!!, trackedResource.cpuArch, listOf(buildResource(version, trackedResource))) + private fun buildBinary(languageLevel: LanguageLevel, version: String, trackedResource: TrackedResource): Binary { + return Binary(trackedResource.os!!, trackedResource.cpuArch, listOf(buildResource(languageLevel, version, trackedResource))) } - private fun buildRelease(version: String, resources: List, product: Product = Product.CPython): Release { - val sources = resources.filter { it.source }.map { buildResource(version, it) }.takeIf { it.isNotEmpty() } - val binaries = resources.filter { !it.source }.map { buildBinary(version, it) }.takeIf { it.isNotEmpty() } + private fun buildRelease(languageLevel: LanguageLevel, version: String, resources: List, product: Product = Product.CPython): Release { + val sources = resources.filter { it.source }.map { buildResource(languageLevel, version, it) }.takeIf { it.isNotEmpty() } + val binaries = resources.filter { !it.source }.map { buildBinary(languageLevel, version, it) }.takeIf { it.isNotEmpty() } return Release( version = version, product = product, @@ -131,7 +155,7 @@ class PythonUpdater { println("Update local versions for ${languageLevel.toPythonVersion()}") val langLevelReleases = getLangLevelReleases(languageLevel) langLevelReleases.forEach { (version, resources) -> - releases.add(buildRelease(version, resources.sorted())) + releases.add(buildRelease(languageLevel, version, resources.sorted())) } } @@ -139,7 +163,7 @@ class PythonUpdater { } companion object { - const val CPYTHON_BASE_URL = "https://www.python.org/ftp/python/" + const val CPYTHON_BASE_URL = "https://www.python.org/ftp/python" /** * 'OpenPGP Public Keys' section from https://www.python.org/downloads/ @@ -153,7 +177,19 @@ class PythonUpdater { "3A5CA953F73C700D", // Larry Hastings (3.5.x source files and tags) "04C367C218ADD4FF", // Benjamin Peterson (2.7.z source files and tags) ) + private val PYTHON_RELEASE_MANAGERS = mapOf( + LanguageLevel.PYTHON37 to PythonReleaseManagers("nad@python.org", "https://github.com/login/oauth"), + LanguageLevel.PYTHON38 to PythonReleaseManagers("lukasz@langa.pl", "https://github.com/login/oauth"), + LanguageLevel.PYTHON39 to PythonReleaseManagers("lukasz@langa.pl", "https://github.com/login/oauth"), + LanguageLevel.PYTHON310 to PythonReleaseManagers("pablogsal@python.org", "https://accounts.google.com"), + LanguageLevel.PYTHON311 to PythonReleaseManagers("pablogsal@python.org", "https://accounts.google.com"), + LanguageLevel.PYTHON312 to PythonReleaseManagers("thomas@python.org", "https://accounts.google.com"), + LanguageLevel.PYTHON313 to PythonReleaseManagers("thomas@python.org", "https://accounts.google.com"), + LanguageLevel.PYTHON314 to PythonReleaseManagers("hugo@python.org", "https://github.com/login/oauth"), + ) } } +private data class PythonReleaseManagers(val identity: String, val oidcIssuer: String) +