From ed9eeea65b9fce21a1b8d8329df0c1bd72820583 Mon Sep 17 00:00:00 2001 From: Widthdom Date: Sun, 31 May 2026 23:31:35 +0900 Subject: [PATCH] Verify installer release attestations (#1678) --- USER_GUIDE.md | 10 +++- changelog.d/unreleased/1678.security.md | 17 ++++++ install.sh | 47 ++++++++++++++++ tests/CodeIndex.Tests/InstallScriptTests.cs | 60 +++++++++++++++++++++ 4 files changed, 133 insertions(+), 1 deletion(-) create mode 100644 changelog.d/unreleased/1678.security.md diff --git a/USER_GUIDE.md b/USER_GUIDE.md index 9b2d28ff39..f9bd2e8ca4 100644 --- a/USER_GUIDE.md +++ b/USER_GUIDE.md @@ -467,7 +467,10 @@ gh attestation verify CodeIndex-linux-x64.tar.gz -R Widthdom/CodeIndex ``` The GitHub attestation verifies that the artifact was generated by the -repository workflow identity. +repository workflow identity. The installer runs this verification +automatically when the `gh` command is available and the public GitHub release +host is used. Set `CDIDX_REQUIRE_ATTESTATION=1` to make the installer fail +closed when provenance verification cannot be completed. ### Option A: One-liner install (no .NET required) @@ -2482,6 +2485,11 @@ signature に対する GitHub build provenance attestation も出力します。 gh attestation verify CodeIndex-linux-x64.tar.gz -R Widthdom/CodeIndex ``` +`gh` command が利用可能で public GitHub release host を使っている場合、 +installer はこの provenance verification を自動実行します。 +`CDIDX_REQUIRE_ATTESTATION=1` を設定すると、provenance verification を完了 +できない場合に installer は fail closed します。 + GitHub attestation は、その artifact が repository workflow identity により 生成されたことを検証します。 diff --git a/changelog.d/unreleased/1678.security.md b/changelog.d/unreleased/1678.security.md new file mode 100644 index 0000000000..35c0484365 --- /dev/null +++ b/changelog.d/unreleased/1678.security.md @@ -0,0 +1,17 @@ +--- +category: security +issues: + - 1678 +affected: + - install.sh + - USER_GUIDE.md + - tests/CodeIndex.Tests/InstallScriptTests.cs +--- + +## English + +- **Installer downloads can now verify GitHub provenance attestations (#1678)** - `install.sh` now verifies release archives and `sha256sums.txt` through GitHub provenance attestations when `gh` is available, and `CDIDX_REQUIRE_ATTESTATION=1` makes installs fail closed if that second-channel verification cannot complete. + +## 日本語 + +- **installer download が GitHub provenance attestation を検証できるようになりました (#1678)** - `install.sh` は `gh` が利用可能な場合に release archive と `sha256sums.txt` を GitHub provenance attestation で検証し、`CDIDX_REQUIRE_ATTESTATION=1` を設定すると second-channel verification を完了できない場合に install を fail closed します。 diff --git a/install.sh b/install.sh index f377b4b3f4..d20f22f45e 100755 --- a/install.sh +++ b/install.sh @@ -18,6 +18,7 @@ # Optional env vars / 任意環境変数: # CDIDX_GITHUB_BASE_URL Release download base URL override # CDIDX_GITHUB_API_BASE_URL API base URL override for latest-release lookup +# CDIDX_REQUIRE_ATTESTATION=1 Require GitHub provenance verification via gh # CDIDX_LOCAL_MIRROR_PORT Local self-test HTTP server port (default: 18765) # HTTPS_PROXY / HTTP_PROXY Proxy used by curl for release and API probes # NO_PROXY Hosts that should bypass the proxy @@ -76,6 +77,7 @@ BINARY_NAME="cdidx" MANIFEST_REQUIRED_VERSION="1.24.6" GITHUB_BASE_URL="${CDIDX_GITHUB_BASE_URL:-https://github.com}" GITHUB_API_BASE_URL="${CDIDX_GITHUB_API_BASE_URL:-https://api.github.com}" +REQUIRE_ATTESTATION="${CDIDX_REQUIRE_ATTESTATION:-0}" # Normalize optional base URL overrides by removing a trailing slash. # 末尾スラッシュ付きでも URL 連結が壊れないようにする。 GITHUB_BASE_URL="${GITHUB_BASE_URL%/}" @@ -182,6 +184,49 @@ need_cmd() { fi } +has_cmd() { + command -v "$1" > /dev/null 2>&1 +} + +release_attestation_supported() { + if [ "${CDIDX_INSTALL_SH_LIB_ONLY:-0}" = "1" ] && [ "${CDIDX_TEST_ENABLE_ATTESTATION:-0}" != "1" ]; then + return 1 + fi + + [ "$GITHUB_BASE_URL" = "https://github.com" ] && [ "${SELF_TEST_LOCAL_MIRROR:-0}" != "1" ] +} + +verify_release_attestation() { + local artifact_path="$1" + local artifact_name="$2" + + if ! release_attestation_supported; then + if [ "$REQUIRE_ATTESTATION" = "1" ]; then + error "GitHub provenance attestation verification is required, but the release host is not github.com. Unset CDIDX_REQUIRE_ATTESTATION or install from the public GitHub release." + fi + return 0 + fi + + if ! has_cmd gh; then + if [ "$REQUIRE_ATTESTATION" = "1" ]; then + error "GitHub provenance attestation verification is required, but the 'gh' command was not found. Install GitHub CLI or unset CDIDX_REQUIRE_ATTESTATION." + fi + warn "Skipping GitHub provenance attestation for ${artifact_name}: 'gh' command not found. Set CDIDX_REQUIRE_ATTESTATION=1 to require this verification." + return 0 + fi + + info "Verifying GitHub provenance attestation for ${artifact_name}..." + if gh attestation verify "$artifact_path" -R "$REPO" > /dev/null; then + return 0 + fi + + if [ "$REQUIRE_ATTESTATION" = "1" ]; then + error "GitHub provenance attestation verification failed for ${artifact_name}." + fi + + warn "GitHub provenance attestation verification failed for ${artifact_name}; continuing with checksum verification. Set CDIDX_REQUIRE_ATTESTATION=1 to fail closed." +} + temp_root() { printf '%s' "${TMPDIR:-/tmp}" } @@ -989,9 +1034,11 @@ download_and_install() { info "Downloading ${archive_name}..." download_release_file "$archive_url" "${tmpdir}/${archive_name}" "${archive_name}" + verify_release_attestation "${tmpdir}/${archive_name}" "$archive_name" info "Downloading checksums..." download_release_file "$checksums_url" "${tmpdir}/sha256sums.txt" "sha256sums.txt" + verify_release_attestation "${tmpdir}/sha256sums.txt" "sha256sums.txt" # Verify checksum / チェックサム検証 info "Verifying checksum..." diff --git a/tests/CodeIndex.Tests/InstallScriptTests.cs b/tests/CodeIndex.Tests/InstallScriptTests.cs index 8e25358aad..5f564224fc 100644 --- a/tests/CodeIndex.Tests/InstallScriptTests.cs +++ b/tests/CodeIndex.Tests/InstallScriptTests.cs @@ -607,6 +607,66 @@ shift 2 Assert.Contains("allow-list at least one artifact host path", stderr); } + [Fact] + public void VerifyReleaseAttestation_GhAvailable_VerifiesArtifactWithRepository() + { + if (OperatingSystem.IsWindows()) + return; + + var logPath = Path.Combine(_tempRoot, "gh_attestation.log"); + var artifactPath = Path.Combine(_tempRoot, "CodeIndex-linux-x64.tar.gz"); + File.WriteAllText(artifactPath, "archive"); + + var (exitCode, stdout, stderr) = RunInstallerSnippet( + $$""" + gh() { + printf '%s\n' "$*" >> "{{logPath}}" + return 0 + } + + verify_release_attestation "{{artifactPath}}" "CodeIndex-linux-x64.tar.gz" + """, + new Dictionary + { + ["CDIDX_TEST_ENABLE_ATTESTATION"] = "1", + }); + + Assert.Equal(0, exitCode); + Assert.Empty(stderr); + Assert.Contains("Verifying GitHub provenance attestation for CodeIndex-linux-x64.tar.gz", stdout); + Assert.Equal($"attestation verify {artifactPath} -R Widthdom/CodeIndex{Environment.NewLine}", File.ReadAllText(logPath)); + } + + [Fact] + public void VerifyReleaseAttestation_RequiredAndGhFails_Aborts() + { + if (OperatingSystem.IsWindows()) + return; + + var artifactPath = Path.Combine(_tempRoot, "sha256sums.txt"); + File.WriteAllText(artifactPath, "checksums"); + + var (exitCode, stdout, stderr) = RunInstallerSnippet( + $$""" + gh() { + return 1 + } + + verify_release_attestation "{{artifactPath}}" "sha256sums.txt" + echo "UNREACHABLE" + """, + new Dictionary + { + ["CDIDX_REQUIRE_ATTESTATION"] = "1", + ["CDIDX_TEST_ENABLE_ATTESTATION"] = "1", + }); + + Assert.Equal(1, exitCode); + Assert.Contains("Verifying GitHub provenance attestation for sha256sums.txt", stdout); + Assert.DoesNotContain("UNREACHABLE", stdout); + Assert.Contains("GitHub provenance attestation verification failed for sha256sums.txt", stderr); + } + [Theory] [InlineData("curl: (56) CONNECT tunnel failed, response 403")] [InlineData("curl: (56) Received HTTP code 403 from proxy after CONNECT")]