Files
kaidi/scripts/verify-release.sh
T
Qiufeng 2c73ad7eb0
Release / release (push) Canceled after 0s
fix: support panel-managed deployment safely
2026-08-18 00:02:45 +08:00

176 lines
8.8 KiB
Bash
Executable File

#!/usr/bin/env bash
set -Eeuo pipefail
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)
RELEASE_DIR=${1:-$ROOT/dist/release}
TRUSTED_PUBLIC_KEY_SHA256=${KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256:-}
EXPECTED_SOURCE_REVISION=${KAIDI_EXPECTED_SOURCE_REVISION:-}
EXPECTED_SOURCE_REF=${KAIDI_EXPECTED_SOURCE_REF:-}
EXPECTED_SOURCE_DIRTY=${KAIDI_EXPECTED_SOURCE_DIRTY:-}
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT
sha256_file() {
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | awk '{print $1}'
else
shasum -a 256 "$1" | awk '{print $1}'
fi
}
cd "$RELEASE_DIR"
if command -v sha256sum >/dev/null 2>&1; then
sha256sum -c SHA256SUMS
else
shasum -a 256 -c SHA256SUMS
fi
openssl dgst -sha256 -verify release-public.pem \
-signature release-manifest.sig release-manifest.json
VERSION=$(jq -er '.version | strings | select(length > 0)' release-manifest.json)
"$ROOT/scripts/check-semver.sh" "$VERSION" \
|| { printf 'Release version is not valid SemVer\n' >&2; exit 1; }
ARTIFACT=$(jq -er '.artifact | strings | select(length > 0)' release-manifest.json)
[ -s "$ARTIFACT" ] || { printf 'Release artifact is missing\n' >&2; exit 1; }
[ "$ARTIFACT" = "kaidi-finance-$VERSION.tar.gz" ] \
|| { printf 'Release artifact name does not match the version\n' >&2; exit 1; }
printf '%s\n' \
SHA256SUMS \
backend-sbom.cdx.json \
bootstrap-checksums.txt \
frontend-sbom.cdx.json \
install.sh \
"$ARTIFACT" \
release-manifest.json \
release-manifest.sig \
release-public.pem | LC_ALL=C sort > "$WORK/expected-assets.txt"
for path in ./* ./.[!.]* ./..?*; do
[ -e "$path" ] || [ -L "$path" ] || continue
basename "$path"
done | LC_ALL=C sort > "$WORK/actual-assets.txt"
diff -u "$WORK/expected-assets.txt" "$WORK/actual-assets.txt" \
|| { printf 'Release directory must contain exactly the nine published assets\n' >&2; exit 1; }
EXPECTED_ARTIFACT_SHA256=$(jq -er \
'.sha256 | strings | ascii_downcase | select(test("^[0-9a-f]{64}$"))' \
release-manifest.json)
[ "$(sha256_file "$ARTIFACT")" = "$EXPECTED_ARTIFACT_SHA256" ] \
|| { printf 'Release artifact digest does not match the signed manifest\n' >&2; exit 1; }
[ "$(jq '.sboms | length' release-manifest.json)" -eq 2 ] \
|| { printf 'Release manifest must bind two SBOMs\n' >&2; exit 1; }
jq -e --arg version "$VERSION" \
'.source.revision | strings | test("^[0-9a-f]{40}$")' release-manifest.json >/dev/null \
|| { printf 'Release manifest source revision is invalid\n' >&2; exit 1; }
jq -e \
'.source.ref | strings | (length > 0 and length <= 128)' release-manifest.json >/dev/null \
|| { printf 'Release manifest source ref is invalid\n' >&2; exit 1; }
jq -e '(.source.dirty | type) == "boolean"' release-manifest.json >/dev/null \
|| { printf 'Release manifest source dirty flag is invalid\n' >&2; exit 1; }
SOURCE_REVISION=$(jq -er '.source.revision | ascii_downcase' release-manifest.json)
SOURCE_REF=$(jq -er '.source.ref' release-manifest.json)
SOURCE_DIRTY=$(jq -er '.source.dirty | tostring' release-manifest.json)
if [[ "$SOURCE_REF" == v* ]] && [ "$SOURCE_REF" != "v$VERSION" ]; then
printf 'Release manifest source tag does not match the version\n' >&2
exit 1
fi
if [ -n "$EXPECTED_SOURCE_REVISION" ]; then
NORMALIZED_EXPECTED_SOURCE_REVISION=$(printf '%s' "$EXPECTED_SOURCE_REVISION" | tr '[:upper:]' '[:lower:]')
[[ "$NORMALIZED_EXPECTED_SOURCE_REVISION" =~ ^[0-9a-f]{40}$ ]] \
|| { printf 'KAIDI_EXPECTED_SOURCE_REVISION must be a 40-character Git commit SHA\n' >&2; exit 1; }
[ "$SOURCE_REVISION" = "$NORMALIZED_EXPECTED_SOURCE_REVISION" ] \
|| { printf 'Release manifest source revision does not match the expected commit\n' >&2; exit 1; }
fi
if [ -n "$EXPECTED_SOURCE_REF" ] && [ "$SOURCE_REF" != "$EXPECTED_SOURCE_REF" ]; then
printf 'Release manifest source ref does not match the expected ref\n' >&2
exit 1
fi
if [ -n "$EXPECTED_SOURCE_DIRTY" ]; then
[[ "$EXPECTED_SOURCE_DIRTY" = true || "$EXPECTED_SOURCE_DIRTY" = false ]] \
|| { printf 'KAIDI_EXPECTED_SOURCE_DIRTY must be true or false\n' >&2; exit 1; }
[ "$SOURCE_DIRTY" = "$EXPECTED_SOURCE_DIRTY" ] \
|| { printf 'Release manifest source dirty flag does not match the expected state\n' >&2; exit 1; }
fi
jq -e --arg version "$VERSION" \
'.buildVersions.backend == $version and .buildVersions.frontend == $version' \
release-manifest.json >/dev/null \
|| { printf 'Release build versions do not match the manifest version\n' >&2; exit 1; }
for ecosystem in maven npm; do
file=$(jq -er --arg ecosystem "$ecosystem" \
'.sboms[] | select(.ecosystem == $ecosystem) | .artifact' release-manifest.json)
expected=$(jq -er --arg ecosystem "$ecosystem" \
'.sboms[] | select(.ecosystem == $ecosystem) | .sha256' release-manifest.json)
[ "$(sha256_file "$file")" = "$expected" ] \
|| { printf '%s SBOM digest does not match the manifest\n' "$ecosystem" >&2; exit 1; }
jq -e --arg version "$VERSION" \
'. as $document | .metadata.component.version == $version
and any(.dependencies[]; .ref == $document.metadata.component["bom-ref"])' \
"$file" >/dev/null
done
PUBLIC_KEY_SHA256=$(sha256_file release-public.pem)
INSTALLER_SHA256=$(sha256_file install.sh)
BOOTSTRAP_SHA256=$(sha256_file bootstrap-checksums.txt)
SIGNED_INSTALLER=$(jq -er '.bootstrap.installer' release-manifest.json)
SIGNED_PUBLIC_KEY=$(jq -er '.bootstrap.publicKey' release-manifest.json)
SIGNED_BOOTSTRAP=$(jq -er '.bootstrap.checksums' release-manifest.json)
[ "$SIGNED_INSTALLER" = install.sh ] && [ "$SIGNED_PUBLIC_KEY" = release-public.pem ] \
&& [ "$SIGNED_BOOTSTRAP" = bootstrap-checksums.txt ] \
|| { printf 'Release manifest bootstrap asset names are invalid\n' >&2; exit 1; }
jq -e --arg installer "$INSTALLER_SHA256" --arg publicKey "$PUBLIC_KEY_SHA256" \
--arg bootstrap "$BOOTSTRAP_SHA256" \
'.bootstrap.installerSha256 == $installer
and .bootstrap.publicKeySha256 == $publicKey
and .bootstrap.checksumsSha256 == $bootstrap' release-manifest.json >/dev/null \
|| { printf 'Bootstrap assets do not match the signed manifest\n' >&2; exit 1; }
NORMALIZED_TRUSTED_PUBLIC_KEY_SHA256=$(printf '%s' "$TRUSTED_PUBLIC_KEY_SHA256" | tr '[:upper:]' '[:lower:]')
[[ "$NORMALIZED_TRUSTED_PUBLIC_KEY_SHA256" =~ ^[0-9a-f]{64}$ ]] \
|| { printf 'KAIDI_TRUSTED_RELEASE_PUBLIC_KEY_SHA256 must be 64 hexadecimal characters\n' >&2; exit 1; }
[ "$PUBLIC_KEY_SHA256" = "$NORMALIZED_TRUSTED_PUBLIC_KEY_SHA256" ] \
|| { printf 'Release public key does not match the trusted fingerprint\n' >&2; exit 1; }
[ "$(sed -n 's/^KAIDI_RELEASE_PUBLIC_KEY_SHA256=//p' bootstrap-checksums.txt)" = "$PUBLIC_KEY_SHA256" ] \
|| { printf 'Bootstrap public-key fingerprint does not match\n' >&2; exit 1; }
[ "$(sed -n 's/^KAIDI_INSTALLER_SHA256=//p' bootstrap-checksums.txt)" = "$INSTALLER_SHA256" ] \
|| { printf 'Bootstrap installer digest does not match\n' >&2; exit 1; }
ARCHIVE_LIST=$(tar -tzf "$ARTIFACT")
if grep -Eq '(^/|(^|/)\.\.(/|$))' <<< "$ARCHIVE_LIST"; then
printf 'Release archive contains an unsafe path\n' >&2
exit 1
fi
if grep -Eq '(^|/)(signing-private|private-key|id_rsa)' <<< "$ARCHIVE_LIST"; then
printf 'Release archive contains private key material\n' >&2
exit 1
fi
[ "$(tar -xOf "$ARTIFACT" ./VERSION)" = "$VERSION" ] \
|| { printf 'Release archive version does not match the manifest\n' >&2; exit 1; }
tar -xOf "$ARTIFACT" ./app.jar > "$WORK/app.jar"
(cd "$WORK" && jar -xf app.jar \
META-INF/MANIFEST.MF META-INF/maven/com.kaidi/finance-system/pom.properties)
JAR_POM_VERSION=$(sed -n 's/^version=//p' \
"$WORK/META-INF/maven/com.kaidi/finance-system/pom.properties")
JAR_IMPLEMENTATION_VERSION=$(sed -n 's/^Implementation-Version: //p' \
"$WORK/META-INF/MANIFEST.MF" | tr -d '\r')
[ "$JAR_POM_VERSION" = "$VERSION" ] && [ "$JAR_IMPLEMENTATION_VERSION" = "$VERSION" ] \
|| { printf 'Release JAR metadata versions do not match the manifest\n' >&2; exit 1; }
compare_archive_file() {
archive_path=$1
source_path=$2
cmp <(tar -xOf "$ARTIFACT" "$archive_path") "$ROOT/$source_path"
}
compare_archive_file ./ops/update.sh deploy/update.sh
compare_archive_file ./ops/baota-start.sh deploy/baota-start.sh
compare_archive_file ./ops/baota.env.example deploy/baota.env.example
compare_archive_file ./ops/kaidi-update.path deploy/systemd/kaidi-update.path
compare_archive_file ./ops/kaidi-update.service deploy/systemd/kaidi-update.service
compare_archive_file ./ops/kaidi-finance.service deploy/systemd/kaidi-finance.service
compare_archive_file ./ops/kaidi-finance.conf deploy/nginx/kaidi-finance.conf
tar -tvzf "$ARTIFACT" ./ops/baota-start.sh | grep -Eq '^-.{2}x.{2}x.{2}x' \
|| { printf 'Baota Spring Boot launcher is not executable\n' >&2; exit 1; }
bash -n "$ROOT/deploy/baota-start.sh"
cmp install.sh "$ROOT/deploy/install.sh" \
|| { printf 'Release installer does not match deploy/install.sh\n' >&2; exit 1; }
printf 'Release %s verified; installer SHA-256: %s\n' "$VERSION" "$INSTALLER_SHA256"