diff --git a/.gitignore b/.gitignore index d73bc0c..db47641 100644 --- a/.gitignore +++ b/.gitignore @@ -28,3 +28,4 @@ go.work.sum .DS_Store dist/ +tmp/ diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ae76c9..c891ccc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,23 @@ Date format: `YYYY-MM-DD` ### Fixed ### Security +--- + +## [1.11.0] - 2025-11-20 + +### Added +- **risk:** Added `signature-verify` make target to verify latest release's digital signatures for the current GOOS and GOARCH combination. + +### Changed +- **debt:** Upgraded dependencies to their latest stable versions. + +### Deprecated +### Removed +### Fixed +- **defect:** Fixed `README.md` instructions for verifying module checksums. + +### Security + --- ## [1.10.3] - 2025-11-07 @@ -180,7 +197,8 @@ Date format: `YYYY-MM-DD` ### Fixed ### Security -[Unreleased]: https://github.com/sixafter/semver/compare/v1.10.3...HEAD +[Unreleased]: https://github.com/sixafter/semver/compare/v1.11.0...HEAD +[1.11.0]: https://github.com/sixafter/semver/compare/v1.10.3...v1.11.0 [1.10.3]: https://github.com/sixafter/semver/compare/v1.10.1...v1.10.3 [1.10.1]: https://github.com/sixafter/semver/compare/v1.10.0...v1.10.1 [1.10.0]: https://github.com/sixafter/semver/compare/v1.9.0...v1.10.0 diff --git a/Makefile b/Makefile index b091471..6f275a8 100644 --- a/Makefile +++ b/Makefile @@ -81,8 +81,15 @@ vuln: ## Check for vulnerabilities .PHONY: release-verify release-verify: ## Verify the release - rm -fr dist - goreleaser --config .goreleaser.yaml release --snapshot + @scripts/verify-release.sh + +.PHONY: module-verify +mod-verify: ## Verify Go module integrity + @scripts/verify-mod.sh + +.PHONY: signature-verify +signature-verify: ## Verify latest release's digital signatures + @scripts/verify-sig.sh .PHONY: help help: ## Display this help screen diff --git a/README.md b/README.md index df83c55..24af543 100644 --- a/README.md +++ b/README.md @@ -33,27 +33,45 @@ A Semantic Versioning 2.0.0 compliant parser and utility library written in Go. To verify the integrity of the release, you can use Cosign to check the signature and checksums. Follow these steps: ```sh -# Fetch the latest release tag from GitHub API (e.g., "v1.8.0") +# Fetch the latest release tag from GitHub API (e.g., "v1.11.0") TAG=$(curl -s https://api.github.com/repos/sixafter/semver/releases/latest | jq -r .tag_name) -# Remove leading "v" for filenames (e.g., "v1.8.0" -> "1.8.0") +# Remove leading "v" for filenames (e.g., "v1.11.0" -> "1.11.0") VERSION=${TAG#v} -# Verify the release tarball +# --------------------------------------------------------------------- +# Verify the source tarball using Sigstore bundle +# --------------------------------------------------------------------- + +# Download the release tarball and its Sigstore bundle +curl -LO "https://github.com/sixafter/semver/releases/download/${TAG}/semver-${VERSION}.tar.gz" +curl -LO "https://github.com/sixafter/semver/releases/download/${TAG}/semver-${VERSION}.tar.gz.sigstore.json" + +# Verify the tarball with Cosign cosign verify-blob \ - --key https://raw.githubusercontent.com/sixafter/semver/main/cosign.pub \ - --signature semver-${VERSION}.tar.gz.sig \ - semver-${VERSION}.tar.gz + --key "https://raw.githubusercontent.com/sixafter/semver/main/cosign.pub" \ + --bundle "semver-${VERSION}.tar.gz.sigstore.json" \ + "semver-${VERSION}.tar.gz" + +# --------------------------------------------------------------------- +# Verify checksums.txt using Sigstore bundle +# --------------------------------------------------------------------- -# Download checksums.txt and its signature from the latest release assets -curl -LO https://github.com/sixafter/semver/releases/download/${TAG}/checksums.txt -curl -LO https://github.com/sixafter/semver/releases/download/${TAG}/checksums.txt.sig +# Download checksums.txt and its bundle +curl -LO "https://github.com/sixafter/semver/releases/download/${TAG}/checksums.txt" +curl -LO "https://github.com/sixafter/semver/releases/download/${TAG}/checksums.txt.sigstore.json" -# Verify checksums.txt with cosign +# Verify the checksums.txt signature cosign verify-blob \ - --key https://raw.githubusercontent.com/sixafter/semver/main/cosign.pub \ - --signature checksums.txt.sig \ + --key "https://raw.githubusercontent.com/sixafter/semver/main/cosign.pub" \ + --bundle "checksums.txt.sigstore.json" \ checksums.txt + +# --------------------------------------------------------------------- +# Validate file integrity +# --------------------------------------------------------------------- + +shasum -a 256 -c checksums.txt ``` If valid, Cosign will output: diff --git a/scripts/verify-mod.sh b/scripts/verify-mod.sh new file mode 100755 index 0000000..829d4ea --- /dev/null +++ b/scripts/verify-mod.sh @@ -0,0 +1,95 @@ +#!/bin/bash +# Copyright (c) 2024-2025 Six After, Inc. +# +# This source code is licensed under the Apache 2.0 License found in the +# LICENSE file in the root directory of this source tree. +set -euo pipefail + +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${__dir}"/os-type.sh + +# Windows +if is_windows; then + echo "[ERROR] Windows is not currently supported." >&2 + exit 1 +fi + +# Ensure tmp directory exists +mkdir -p tmp +rm tmp/*.zip 2>/dev/null || true + +# ------------------------------------------------------------ +# Detect latest release (README method) +# ------------------------------------------------------------ +REPO_OWNER="sixafter" +REPO_NAME="semver" +MODULE="github.com/${REPO_OWNER}/${REPO_NAME}" + +TAG=$(curl -s "https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest" | jq -r .tag_name) +VERSION=${TAG#v} + +echo "Latest release: $TAG (version: $VERSION)" + +# ------------------------------------------------------------ +# Portable SHA-256 function (macOS + Linux) +# ------------------------------------------------------------ +if command -v sha256sum >/dev/null 2>&1; then + SHA256="sha256sum" +else + SHA256="shasum -a 256" +fi + +# ------------------------------------------------------------ +# 1. GitHub Tag ZIP +# ------------------------------------------------------------ +echo "Downloading GitHub tag archive..." +curl -sSfL -o tmp/github.zip \ + "https://github.com/${REPO_OWNER}/${REPO_NAME}/archive/refs/tags/${TAG}.zip" + +GITHUB_SHA=$($SHA256 tmp/github.zip | awk '{print $1}') +echo "GitHub ZIP SHA256: $GITHUB_SHA" + +# ------------------------------------------------------------ +# 2. Direct go mod ZIP +# ------------------------------------------------------------ +echo "Downloading go mod ZIP using direct mode..." + +MOD_JSON=$(GOPROXY=direct go mod download -json "${MODULE}@${TAG}") +MOD_ZIP_PATH=$(echo "$MOD_JSON" | jq -r '.Zip') + +if [ ! -f "$MOD_ZIP_PATH" ]; then + echo "ERROR: The go mod ZIP path does not exist:" + echo "$MOD_ZIP_PATH" + exit 1 +fi + +cp "$MOD_ZIP_PATH" tmp/gomod.zip +GOMOD_SHA=$($SHA256 tmp/gomod.zip | awk '{print $1}') +echo "go mod ZIP SHA256: $GOMOD_SHA" + +# ------------------------------------------------------------ +# 3. Go Proxy ZIP +# ------------------------------------------------------------ +echo "Downloading Go module proxy ZIP..." +curl -sSfL -o tmp/proxy.zip \ + "https://proxy.golang.org/${MODULE}/@v/${TAG}.zip" + +PROXY_SHA=$($SHA256 tmp/proxy.zip | awk '{print $1}') +echo "Proxy ZIP SHA256: $PROXY_SHA" + +# ------------------------------------------------------------ +# Comparison +# ------------------------------------------------------------ +echo +echo "Comparing checksums..." +echo "GitHub : $GITHUB_SHA" +echo "go mod : $GOMOD_SHA" +echo "Proxy : $PROXY_SHA" +echo + +if [ "$GITHUB_SHA" != "$GOMOD_SHA" ] || [ "$GITHUB_SHA" != "$PROXY_SHA" ]; then + echo "ERROR: CHECKSUM MISMATCH DETECTED!" + exit 1 +fi + +echo "Go module archive is fully reproducible across GitHub, direct, and proxy." diff --git a/scripts/verify-release.sh b/scripts/verify-release.sh new file mode 100755 index 0000000..fc9a0bc --- /dev/null +++ b/scripts/verify-release.sh @@ -0,0 +1,22 @@ +#!/bin/bash +# Copyright (c) 2024-2025 Six After, Inc. +# +# This source code is licensed under the Apache 2.0 License found in the +# LICENSE file in the root directory of this source tree. + +# Verify the integrity of the latest release using Cosign + checksums +# Works on macOS and Linux + +set -euo pipefail + +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${__dir}"/os-type.sh + +# Windows +if is_windows; then + echo "[ERROR] Windows is not currently supported." >&2 + exit 1 +fi + +rm -fr dist +goreleaser --config .goreleaser.yaml release --snapshot diff --git a/scripts/verify-sig.sh b/scripts/verify-sig.sh new file mode 100755 index 0000000..8423f86 --- /dev/null +++ b/scripts/verify-sig.sh @@ -0,0 +1,152 @@ +#!/usr/bin/env bash +# Copyright (c) 2024-2025 Six After, Inc. +# +# This source code is licensed under the Apache 2.0 License found in the +# LICENSE file in the root directory of this source tree. + +set -euo pipefail + +__dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +source "${__dir}/os-type.sh" + +# Windows +if is_windows; then + echo "[ERROR] Windows is not currently supported." >&2 + exit 1 +fi + +curl_retry() { + local url="$1" + local out="$2" + local attempt=1 + local max=5 + local delay=2 + + while true; do + # -f: fail on HTTP error codes + # -s: silent + # -S: show errors + # -L: follow redirects + if curl -fSsSL "${url}" -o "${out}"; then + return 0 + fi + + if (( attempt >= max )); then + echo "[ERROR] curl failed after ${attempt} attempts: ${url}" >&2 + return 1 + fi + + echo "[WARN] curl failed (attempt ${attempt}/${max}). Retrying in ${delay}s..." + sleep $delay + attempt=$(( attempt + 1 )) + delay=$(( delay * 2 )) # exponential backoff + done +} + +# ------------------------------------------------------------ +# Project / repository name (portable) +# ------------------------------------------------------------ +PROJECT="semver" +REPO="sixafter/${PROJECT}" +MODULE="github.com/${REPO}" + +# tmp directory for artifacts +TMP="${__dir}/tmp" +mkdir -p "${TMP}" + +echo "Project: ${PROJECT}" +echo "Repository: ${REPO}" +echo "Module path: ${MODULE}" +echo "Artifact directory: ${TMP}" +echo + +# ------------------------------------------------------------ +# Detect latest release +# ------------------------------------------------------------ +TAG=$(curl -s "https://api.github.com/repos/${REPO}/releases/latest" | jq -r .tag_name) +VERSION=${TAG#v} + +echo "Latest release: ${TAG} (version: ${VERSION})" + +# ------------------------------------------------------------ +# Determine SHA-256 tool +# ------------------------------------------------------------ +if command -v sha256sum >/dev/null 2>&1; then + SHA256="sha256sum" +else + SHA256="shasum -a 256" +fi + +# ------------------------------------------------------------ +# Download release artifacts → tmp/ +# ------------------------------------------------------------ +echo +echo "Downloading release artifacts into ${TMP}..." + +# Core tarball +curl_retry \ + "https://github.com/${REPO}/releases/download/${TAG}/${PROJECT}-${VERSION}.tar.gz" \ + "${TMP}/${PROJECT}-${VERSION}.tar.gz" + +# Tarball signature +curl_retry \ + "https://github.com/${REPO}/releases/download/${TAG}/${PROJECT}-${VERSION}.tar.gz.sigstore.json" \ + "${TMP}/${PROJECT}-${VERSION}.tar.gz.sigstore.json" + +# SBOM +curl_retry \ + "https://github.com/${REPO}/releases/download/${TAG}/${PROJECT}-${VERSION}.tar.gz.sbom.json" \ + "${TMP}/${PROJECT}-${VERSION}.tar.gz.sbom.json" + +# checksums.txt +curl_retry \ + "https://github.com/${REPO}/releases/download/${TAG}/checksums.txt" \ + "${TMP}/checksums.txt" + +# checksums.txt signature +curl_retry \ + "https://github.com/${REPO}/releases/download/${TAG}/checksums.txt.sigstore.json" \ + "${TMP}/checksums.txt.sigstore.json" + +# ------------------------------------------------------------ +# Verify tarball with Cosign +# ------------------------------------------------------------ +echo +echo "Verifying tarball signature..." + +cosign verify-blob \ + --key "${__dir}/../cosign.pub" \ + --bundle "${TMP}/${PROJECT}-${VERSION}.tar.gz.sigstore.json" \ + "${TMP}/${PROJECT}-${VERSION}.tar.gz" + +echo "Tarball signature OK." + +# ------------------------------------------------------------ +# Verify checksums manifest signature +# ------------------------------------------------------------ +echo +echo "Verifying checksums.txt signature..." + +cosign verify-blob \ + --key "${__dir}/../cosign.pub" \ + --bundle "${TMP}/checksums.txt.sigstore.json" \ + "${TMP}/checksums.txt" + +echo "Checksums signature OK." + +# ------------------------------------------------------------ +# Validate local artifact integrity +# ------------------------------------------------------------ +echo +echo "Verifying file checksums locally..." +( + cd "${TMP}" + $SHA256 -c checksums.txt +) || { + echo + echo "❌ Release verification FAILED." + exit 1 +} + +echo +echo "✔ Release verification succeeded." diff --git a/vendor/modules.txt b/vendor/modules.txt index 191e66d..966d5a7 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -4,8 +4,6 @@ github.com/davecgh/go-spew/spew # github.com/pmezard/go-difflib v1.0.0 ## explicit github.com/pmezard/go-difflib/difflib -# github.com/stretchr/objx v0.5.2 -## explicit; go 1.20 # github.com/stretchr/testify v1.11.1 ## explicit; go 1.17 github.com/stretchr/testify/assert