scripts/release-manifest-check.sh
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bash
# The signed manifest's contract, end to end, without a registry: release/build-manifest.sh writes it, minisign signs it
# (a throwaway key), and then
# - quickstart.sh's own verification functions (the "release verification" block, run as they are) accept it, and
# refuse it tampered with or signed by another key;
# - they read the version, the three image digests and each downloaded file's SHA-256 from it;
# - supervisor/lib.js imagePins (what apply.sh is handed) reads the same three refs;
# - the fields older supervisors and the installers read (components.serverImage/webImage/apk) are unchanged;
# - quickstart.sh's copy of the release key and its file list match release/minisign.pub and build-manifest.sh.
# Needs bash, python3, node, openssl and minisign. Run from anywhere: scripts/release-manifest-check.sh
set -euo pipefail
ROOT=$(cd "$(dirname "$0")/.." && pwd)
T=$(mktemp -d)
cleanup() {
rm -f -- "$T/manifest.json" "$T/manifest.json.minisig" "$T/k.pub" "$T/k.key" "$T/x.pub" "$T/x.key" \
"$T/agent.apk" "$T/tampered.json" "$T/qs.sh"
rmdir -- "$T" 2>/dev/null || true
}
trap cleanup EXIT
fail() { echo "release-manifest-check: $*" >&2; exit 1; }
O=example-owner
D1=sha256:$(printf '1%.0s' $(seq 64)); D2=sha256:$(printf '2%.0s' $(seq 64)); D3=sha256:$(printf '3%.0s' $(seq 64))
head -c 4096 /dev/urandom > "$T/agent.apk"
( cd "$T" && "$ROOT/release/build-manifest.sh" 9.8.7 stable "ghcr.io/$O/mdmesh-server:9.8.7" "ghcr.io/$O/mdmesh-web:9.8.7" \
agent.apk 90807 CKSUM 1.5 "ghcr.io/$O/mdmesh-server@$D1" "ghcr.io/$O/mdmesh-web@$D2" "ghcr.io/$O/mdmesh-supervisor@$D3" ) >/dev/null
# A ref outside the image's repository, or not a digest, is refused at build time.
( cd "$T" && ! "$ROOT/release/build-manifest.sh" 9.8.7 stable "ghcr.io/$O/mdmesh-server:9.8.7" "ghcr.io/$O/mdmesh-web:9.8.7" \
agent.apk 90807 CKSUM 1.5 "ghcr.io/other/mdmesh-server@$D1" "ghcr.io/$O/mdmesh-web@$D2" "ghcr.io/$O/mdmesh-supervisor@$D3" ) 2>/dev/null \
|| fail "build-manifest.sh accepted a server digest from another repository"
( cd "$T" && ! "$ROOT/release/build-manifest.sh" 9.8.7 stable "ghcr.io/$O/mdmesh-server:9.8.7" "ghcr.io/$O/mdmesh-web:9.8.7" \
agent.apk 90807 CKSUM 1.5 "ghcr.io/$O/mdmesh-server:9.8.7" "ghcr.io/$O/mdmesh-web@$D2" "ghcr.io/$O/mdmesh-supervisor@$D3" ) 2>/dev/null \
|| fail "build-manifest.sh accepted a tag where a digest belongs"
minisign -G -W -p "$T/k.pub" -s "$T/k.key" >/dev/null
minisign -S -s "$T/k.key" -m "$T/manifest.json" >/dev/null
minisign -G -W -p "$T/x.pub" -s "$T/x.key" >/dev/null
# quickstart.sh's verification functions, exactly as the script defines them.
sed -n '/^# >>> release verification >>>$/,/^# <<< release verification <<<$/p' "$ROOT/quickstart.sh" > "$T/qs.sh"
[ -s "$T/qs.sh" ] || fail "quickstart.sh has no release verification block"
# shellcheck source=/dev/null
. "$T/qs.sh"
[ "$MDM_RELEASE_PUBKEY_DEFAULT" = "$(sed -n 2p "$ROOT/release/minisign.pub")" ] \
|| fail "quickstart.sh's MDM_RELEASE_PUBKEY_DEFAULT is not the key line of release/minisign.pub: copy it over"
want_files=$(python3 -c 'import re,sys; s=open(sys.argv[1]).read(); print(" ".join(re.search(r"QUICKSTART_FILES = \[(.*?)\]", s, re.S).group(1).replace("\"","").replace(",","").split()))' "$ROOT/release/build-manifest.sh")
[ "$want_files" = "$MDM_QUICKSTART_FILES" ] \
|| fail "quickstart.sh's MDM_QUICKSTART_FILES ($MDM_QUICKSTART_FILES) differ from build-manifest.sh's QUICKSTART_FILES ($want_files)"
PK=$(sed -n 2p "$T/k.pub")
mdm_minisign_verify "$PK" "$T/manifest.json" "$T/manifest.json.minisig" || fail "a good signature was refused"
sed 's/9\.8\.7/9.8.8/' "$T/manifest.json" > "$T/tampered.json"
! mdm_minisign_verify "$PK" "$T/tampered.json" "$T/manifest.json.minisig" 2>/dev/null || fail "a tampered manifest verified"
! mdm_minisign_verify "$(sed -n 2p "$T/x.pub")" "$T/manifest.json" "$T/manifest.json.minisig" 2>/dev/null \
|| fail "a manifest signed by another key verified"
# The same through mdm_verify_manifest: openssl when it can (3.0+), else minisign, else rc 2 (neither tool; nothing checked).
mdm_verify_manifest "$PK" "$T/manifest.json" "$T/manifest.json.minisig" || fail "mdm_verify_manifest refused a good signature"
(
# shellcheck disable=SC2317 # called by mdm_verify_manifest
mdm_openssl_can_verify() { return 1; } # an OpenSSL 1.1.1 host
mdm_verify_manifest "$PK" "$T/manifest.json" "$T/manifest.json.minisig" || fail "the minisign fallback refused a good signature"
rc=0; mdm_verify_manifest "$PK" "$T/tampered.json" "$T/manifest.json.minisig" 2>/dev/null || rc=$?
[ "$rc" = 1 ] || fail "the minisign fallback accepted a tampered manifest (rc $rc)"
rc=0; PATH=/nonexistent mdm_verify_manifest "$PK" "$T/manifest.json" "$T/manifest.json.minisig" || rc=$?
[ "$rc" = 2 ] || fail "with neither openssl 3 nor minisign, mdm_verify_manifest must return 2 (got $rc)"
)
[ "$(mdm_manifest_field "$T/manifest.json" version '[0-9A-Za-z.+-]+')" = 9.8.7 ] || fail "version not read"
[ "$(mdm_manifest_field "$T/manifest.json" server "$MDM_IMAGE_REF_ERE")" = "ghcr.io/$O/mdmesh-server@$D1" ] || fail "server ref not read"
[ "$(mdm_manifest_field "$T/manifest.json" web "$MDM_IMAGE_REF_ERE")" = "ghcr.io/$O/mdmesh-web@$D2" ] || fail "web ref not read"
[ "$(mdm_manifest_field "$T/manifest.json" supervisor "$MDM_IMAGE_REF_ERE")" = "ghcr.io/$O/mdmesh-supervisor@$D3" ] || fail "supervisor ref not read"
grep -q '^ "files": {$' "$T/manifest.json" || fail "quickstart.sh would not find the files block"
for f in $MDM_QUICKSTART_FILES; do
[ "$(mdm_manifest_field "$T/manifest.json" "$f" "$MDM_SHA256_ERE")" = "$(mdm_sha256 "$ROOT/$f")" ] || fail "sha256 of $f not read or wrong"
done
# shellcheck disable=SC2016 # the JavaScript is single-quoted on purpose
node -e '
const { imagePins, imageTags, apkAsset } = require(process.argv[1] + "/supervisor/lib.js");
const m = JSON.parse(require("fs").readFileSync(process.argv[2], "utf8"));
const o = process.argv[3], want = { server: `ghcr.io/${o}/mdmesh-server@${process.argv[4]}`,
web: `ghcr.io/${o}/mdmesh-web@${process.argv[5]}`, supervisor: `ghcr.io/${o}/mdmesh-supervisor@${process.argv[6]}` };
const got = imagePins(m, o);
if (JSON.stringify(got) !== JSON.stringify(want)) { console.error("imagePins:", got); process.exit(1); }
// What a supervisor before 0.5.0 reads (imageTags, apkAsset) is still there, in the same shape.
const t = imageTags(m);
if (t.version !== "9.8.7" || t.serverImage !== `ghcr.io/${o}/mdmesh-server:9.8.7` || t.webImage !== `ghcr.io/${o}/mdmesh-web:9.8.7`) process.exit(2);
if (!apkAsset({ assets: [{ name: "agent.apk", browser_download_url: "https://x/agent.apk" }] }, m)) process.exit(3);
' "$ROOT" "$T/manifest.json" "$O" "$D1" "$D2" "$D3" || fail "supervisor/lib.js does not read the manifest as expected"
# What install/lib/release.sh reads (setup.sh and the native installer).
python3 -c 'import json,sys; m=json.load(open(sys.argv[1])); a=m["components"]["apk"]; assert m["version"]=="9.8.7" and a["sha256"] and a["signatureChecksum"]=="CKSUM"' "$T/manifest.json" \
|| fail "the installers' fields changed"
echo "release-manifest-check: ok"