diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3aa8101..1eef70a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -230,6 +230,23 @@ jobs: exit 1 fi + - name: Install Cosign + uses: sigstore/cosign-installer@v4.1.0 + + # Sign the in-bundle checksums.txt and ship the signature inside the + # bundle so the release-mode installer can verify authenticity offline + # (issue #15). checksums.txt already vouches for every bundled file, so one + # signature covers the extracted payload. + - name: Sign bundle checksums for offline verification + env: + ARCHIVE_BASE: ${{ steps.meta.outputs.archive_base }} + run: | + set -euo pipefail + bundle="dist/${ARCHIVE_BASE}" + cosign sign-blob --yes \ + --bundle "${bundle}/checksums.txt.cosign.bundle" \ + "${bundle}/checksums.txt" + - name: Create archives and checksums env: ARCHIVE_BASE: ${{ steps.meta.outputs.archive_base }} @@ -270,9 +287,6 @@ jobs: (cd "${bundle}" && sha256sum --check checksums.txt) done - - name: Install Cosign - uses: sigstore/cosign-installer@v4.1.0 - # Keyless detached signatures for the release archives and checksums # (issue #15). Each .cosign.bundle is portable and verifiable offline with # `cosign verify-blob` against the workflow identity and OIDC issuer. diff --git a/install.sh b/install.sh index 5b37762..89969eb 100644 --- a/install.sh +++ b/install.sh @@ -169,6 +169,37 @@ verify_release_bundle() { die "checksum verification requires sha256sum or shasum on PATH" fi log "Release bundle checksums verified." + + verify_release_signature +} + +# Verify the bundle's authenticity signature when possible (issue #15). The +# release pipeline ships a keyless cosign signature of checksums.txt inside the +# bundle; because checksums.txt already vouches for every bundled file, +# verifying it authenticates the whole payload. Gated: a host without cosign, or +# a bundle without a signature, still installs on checksums alone. When cosign +# and the signature are both present, a bad signature is fatal. +verify_release_signature() { + _sig="${ROOT_DIR}/checksums.txt.cosign.bundle" + if [ ! -f "${_sig}" ]; then + log "No bundle signature present; skipping signature check (checksums verified)." + return 0 + fi + if ! command -v cosign >/dev/null 2>&1; then + log "cosign not found; skipping signature check (checksums verified)." + log "Install cosign to cryptographically verify bundle authenticity." + return 0 + fi + _identity="${UBEROS_SIGN_IDENTITY_REGEXP:-https://github.com/jmservera/UbeROS/.github/workflows/release.yml@.*}" + _issuer="${UBEROS_SIGN_OIDC_ISSUER:-https://token.actions.githubusercontent.com}" + log "Verifying release bundle signature with cosign..." + cosign verify-blob \ + --bundle "${_sig}" \ + --certificate-identity-regexp "${_identity}" \ + --certificate-oidc-issuer "${_issuer}" \ + "${ROOT_DIR}/checksums.txt" >/dev/null 2>&1 \ + || die "release bundle signature verification failed; the bundle is not authentic or was modified" + log "Release bundle signature verified." } # True when the installer may prompt: not forced off and stdin is a terminal. diff --git a/scripts/validate-wave3.sh b/scripts/validate-wave3.sh index acd878c..d6a7024 100644 --- a/scripts/validate-wave3.sh +++ b/scripts/validate-wave3.sh @@ -350,6 +350,60 @@ expect_failure 'checksum verification failed' env PATH="${FAKE_BIN}:${PATH}" \ [ ! -s "${DOCKER_LOG}" ] || fail "tampered bundle reached Docker before rejection" pass "tampered release bundle is rejected before Docker runs" +# --- Release signature verification (issue #15) ----------------------------- +printf '\n== release signature verification (issue #15) ==\n' +SIG_ROOT="${SANDBOX}/release-sig" +SIG_WS="${SANDBOX}/release-sig-ws" +mkdir -p "${SIG_ROOT}" +cp install.sh .env.template compose.override.gpu.yaml \ + compose.override.intel.yaml compose.override.wsl.yaml "${SIG_ROOT}/" +printf 'services: {}\n' > "${SIG_ROOT}/compose.release.yaml" +printf '0.4.0-beta\n' > "${SIG_ROOT}/VERSION" +( + cd "${SIG_ROOT}" + find . -type f ! -name checksums.txt -print0 \ + | sort -z \ + | xargs -0 sha256sum > checksums.txt +) +# Ship a signature file so the installer attempts verification. +printf 'stub-signature\n' > "${SIG_ROOT}/checksums.txt.cosign.bundle" + +# A cosign that succeeds: the signed bundle installs and reaches Docker. +cat > "${FAKE_BIN}/cosign" < "${DOCKER_LOG}" +PATH="${FAKE_BIN}:${PATH}" sh "${SIG_ROOT}/install.sh" -y \ + --workspace "${SIG_WS}" --no-migrate >/dev/null \ + || fail "signed release bundle failed to install with a valid signature" +grep -q 'compose .* pull' "${DOCKER_LOG}" \ + || fail "signed release bundle did not reach the pull step" +pass "signed release bundle with a valid signature installs" + +# A cosign that fails: verification is fail-closed before Docker. +cat > "${FAKE_BIN}/cosign" < "${DOCKER_LOG}" +expect_failure 'signature verification failed' env PATH="${FAKE_BIN}:${PATH}" \ + sh "${SIG_ROOT}/install.sh" -y --workspace "${SIG_WS}" --no-migrate +[ ! -s "${DOCKER_LOG}" ] || fail "bad-signature bundle reached Docker before rejection" +pass "release bundle with an invalid signature is rejected before Docker runs" + +# Without cosign the signed bundle still installs on verified checksums alone. +rm -f "${FAKE_BIN}/cosign" +: > "${DOCKER_LOG}" +PATH="${FAKE_BIN}:${PATH}" sh "${SIG_ROOT}/install.sh" -y \ + --workspace "${SIG_WS}" --no-migrate >/dev/null \ + || fail "signed bundle failed to install when cosign is absent" +grep -q 'compose .* pull' "${DOCKER_LOG}" \ + || fail "cosign-absent install did not reach the pull step" +pass "signed bundle installs on checksums alone when cosign is absent" + # --- Learning-package selection (PR-14) ------------------------------------- printf '\n== learning-package selection (FR-H1, FR-H4, FR-H5) ==\n' PACKAGE_ROOT="${SANDBOX}/packages"