192 lines
9.6 KiB
Bash
Executable File
192 lines
9.6 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 \
|
|
purge.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 ten 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)
|
|
PURGER_SHA256=$(sha256_file purge.sh)
|
|
BOOTSTRAP_SHA256=$(sha256_file bootstrap-checksums.txt)
|
|
SIGNED_INSTALLER=$(jq -er '.bootstrap.installer' release-manifest.json)
|
|
SIGNED_PURGER=$(jq -er '.bootstrap.purger' 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_PURGER" = purge.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 purger "$PURGER_SHA256" \
|
|
--arg publicKey "$PUBLIC_KEY_SHA256" \
|
|
--arg bootstrap "$BOOTSTRAP_SHA256" \
|
|
'.bootstrap.installerSha256 == $installer
|
|
and .bootstrap.purgerSha256 == $purger
|
|
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; }
|
|
[ "$(sed -n 's/^KAIDI_PURGER_SHA256=//p' bootstrap-checksums.txt)" = "$PURGER_SHA256" ] \
|
|
|| { printf 'Bootstrap purger 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-init.sh deploy/baota-init.sh
|
|
compare_archive_file ./ops/release-public.pem deploy/release-public.pem
|
|
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; }
|
|
tar -tvzf "$ARTIFACT" ./ops/baota-init.sh | grep -Eq '^-.{2}x.{2}x.{2}x' \
|
|
|| { printf 'Baota initializer is not executable\n' >&2; exit 1; }
|
|
bash -n "$ROOT/deploy/baota-start.sh"
|
|
bash -n "$ROOT/deploy/baota-init.sh"
|
|
cmp install.sh "$ROOT/deploy/install.sh" \
|
|
|| { printf 'Release installer does not match deploy/install.sh\n' >&2; exit 1; }
|
|
bash -n "$ROOT/deploy/purge.sh"
|
|
cmp purge.sh "$ROOT/deploy/purge.sh" \
|
|
|| { printf 'Release purger does not match deploy/purge.sh\n' >&2; exit 1; }
|
|
|
|
printf 'Release %s verified; installer SHA-256: %s\n' "$VERSION" "$INSTALLER_SHA256"
|