refactor: remove the runtime binary binding; every release registration path verifies the CI signature [CI]
ci/woodpecker/push/main-amd64-build Pipeline was successful
ci/woodpecker/push/main-amd64-build Pipeline was successful
The hash a node reports about itself cannot be trusted (a swapped binary can copy the old hash), so the runtime binding only caught files that were replaced by something that did not lie. Provenance stays where it can be verified: the signed release manifest (digest) and the signed SHA256SUMS. - Remove BinaryBindingService, the node_bindings table (schema v15 drops it), BinaryPolicy, the handshake hash/manifest fields and the admin binary column. - Drop the per-platform "binaries" list from the release manifest, the hash scripts and the updater's installed.manifest copy. - ReleaseService.PublishAsync now verifies the release signature, so the admin POST /releases route is no longer an unsigned path; tests sign with a test key. - Docs: section 16 explains why the runtime check is gone.
This commit is contained in:
1 parent
a4f3d1892a
commit
904ea2b0ae
49 files changed
+115
-1143
No files matched your search
@@ -1,20 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# 镜像里某个可执行文件的 SHA-256:ci/binary-hash.sh <镜像> <镜像里的路径>
|
||||
# 节点握手时上报的就是它正在运行的那个文件的哈希,CI 把这个哈希写进签名的清单(binaries),Backend 据此认「官方二进制」。
|
||||
# 先试在镜像里运行 sha256sum(同架构),不行再挂载镜像文件系统直接读。
|
||||
set -euo pipefail
|
||||
image="${1:?usage: binary-hash.sh <image> <path>}"; path="${2:?path in the image}"
|
||||
# 镜像是本地刚构建的,不需要仓库认证;CI 里 REGISTRY_AUTH_FILE 指向的文件在登录之前还不存在,buildah 会因此报错
|
||||
unset REGISTRY_AUTH_FILE
|
||||
container="$(buildah from "$image")"
|
||||
trap 'buildah rm "$container" >/dev/null 2>&1 || true' EXIT
|
||||
if out="$(buildah run "$container" -- sha256sum "$path" 2>/dev/null)" && [[ "$out" =~ ^([0-9a-f]{64})[[:space:]] ]]; then
|
||||
echo "${BASH_REMATCH[1]}"
|
||||
exit 0
|
||||
fi
|
||||
buildah unshare bash -c '
|
||||
set -euo pipefail
|
||||
mount="$(buildah mount "$1")"
|
||||
sha256sum "$mount$2" | cut -d" " -f1
|
||||
buildah umount "$1" >/dev/null
|
||||
' _ "$container" "$path"
|
||||
@@ -54,11 +54,6 @@ buildah bud --layers --network host --tls-verify=true --arch amd64 \
|
||||
-f "$context/Containerfile" \
|
||||
-t "$local_image" "$context"
|
||||
|
||||
# 镜像里节点二进制的哈希:写进签名的清单(binaries),Backend 凭它认「官方二进制」,节点握手时上报的就是它正在运行的这个文件的哈希
|
||||
binary_sha256="$(bash "$root/ci/binary-hash.sh" "$local_image" "/usr/local/bin/moshiro-$component")"
|
||||
[[ "$binary_sha256" =~ ^[0-9a-f]{64}$ ]] || { echo "could not hash the $component binary in the image" >&2; exit 1; }
|
||||
echo "$component binary sha256: $binary_sha256"
|
||||
|
||||
logged_in=false
|
||||
for attempt in 1 2 3 4; do
|
||||
if buildah login --tls-verify=true --username "$ZOT_USERNAME" --password-stdin "$registry" <<< "$ZOT_SECRET" >/dev/null; then
|
||||
@@ -91,5 +86,5 @@ done
|
||||
# 发布清单:给镜像的 digest 签名(私钥只在 CI 密钥里),放进镜像仓库(<仓库>-manifest:dev),面板在线时再登记一份加速。
|
||||
# 没有配置签名密钥(例如 fork)就只构建镜像、不发清单。
|
||||
if [[ -n "${RELEASE_SIGNING_KEY:-}" ]]; then
|
||||
RELEASE_BINARIES="$binary_sha256" bash "$root/ci/publish-release-offer.sh" "$component" "$repository" "$(cat "$work/$component.digest")" "$dev_tag" dev "$work/$component-release"
|
||||
bash "$root/ci/publish-release-offer.sh" "$component" "$repository" "$(cat "$work/$component.digest")" "$dev_tag" dev "$work/$component-release"
|
||||
fi
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# 一个组件在所有平台、所有架构的发布压缩包里的可执行文件的 SHA-256(逗号分隔,输出到标准输出)。
|
||||
# ci/release-binary-hashes.sh <agent|probe> <版本,不带 v> 环境变量:GITEA_BASE、NETRC(curl 的 netrc 文件)
|
||||
# 签名的发布清单里的 binaries 就是它:节点握手时上报正在运行的可执行文件的哈希,Backend 凭 CI 签名的清单认「官方二进制」——
|
||||
# 容器、Linux glibc/musl 压缩包、macOS、Windows 的 x64/arm64 都一样,不只是容器镜像里的那两个。
|
||||
set -euo pipefail
|
||||
component="${1:?usage: release-binary-hashes.sh <agent|probe> <version>}"; version="${2:?version}"
|
||||
[[ "$component" =~ ^(agent|probe)$ ]] || { echo "component must be agent or probe" >&2; exit 2; }
|
||||
gitea_base="${GITEA_BASE:?GITEA_BASE is required}"; gitea_base="${gitea_base%/}"
|
||||
netrc=(); [[ -n "${NETRC:-}" ]] && netrc=(--netrc-file "$NETRC")
|
||||
work="$(mktemp -d)"
|
||||
trap 'rm -rf "$work"' EXIT
|
||||
|
||||
# 和 ci/publish-gitea-release.sh 里发布的那 8 种一样(每个组件)
|
||||
targets=(linux-amd64 linux-arm64 linux-musl-amd64 linux-musl-arm64 macos-x64 macos-arm64 windows-amd64 windows-arm64)
|
||||
hashes=()
|
||||
for target in "${targets[@]}"; do
|
||||
if [[ "$target" == windows-* ]]; then archive="moshiro-$component-v$version-$target.zip"; member="moshiro-$component.exe"
|
||||
else archive="moshiro-$component-v$version-$target.tar.gz"; member="moshiro-$component"; fi
|
||||
curl --fail --silent --show-error --retry 3 ${netrc[@]+"${netrc[@]}"} "$gitea_base/api/packages/justme/generic/moshiro-node/$version/$archive" -o "$work/$archive"
|
||||
if [[ "$archive" == *.zip ]]; then
|
||||
hash="$(cd "$work" && python3 -c 'import hashlib,sys,zipfile; print(hashlib.sha256(zipfile.ZipFile(sys.argv[1]).read(sys.argv[2])).hexdigest())' "$archive" "$member")"
|
||||
else
|
||||
hash="$(tar -xzOf "$work/$archive" "$member" | sha256sum | cut -d' ' -f1)"
|
||||
fi
|
||||
[[ "$hash" =~ ^[0-9a-f]{64}$ ]] || { echo "could not hash $member in $archive" >&2; exit 1; }
|
||||
hashes+=("$hash")
|
||||
rm -f "$work/$archive"
|
||||
done
|
||||
# 不同平台的二进制哈希不会相同;去重只是防御
|
||||
printf '%s\n' "${hashes[@]}" | sort -u | paste -sd,
|
||||
@@ -28,16 +28,9 @@ if [[ -n "${RELEASE_NEXT_KEY:-}" ]]; then
|
||||
[[ -n "$next_key" ]] || { echo "RELEASE_NEXT_KEY is not a public key" >&2; exit 2; }
|
||||
fi
|
||||
|
||||
# 官方二进制的哈希(逗号分隔的小写十六进制):节点握手时上报正在运行的二进制的哈希,Backend 用这份签名的清单认「官方」
|
||||
binaries=""
|
||||
if [[ -n "${RELEASE_BINARIES:-}" ]]; then
|
||||
[[ "$RELEASE_BINARIES" =~ ^[0-9a-f]{64}(,[0-9a-f]{64})*$ ]] || { echo "RELEASE_BINARIES must be comma-separated sha256 hashes" >&2; exit 2; }
|
||||
binaries=",\"binaries\":\"$RELEASE_BINARIES\""
|
||||
fi
|
||||
|
||||
mkdir -p "$outdir"
|
||||
printf '{"schema":1,"component":"%s","channel":"%s","image":"%s","digest":"%s","tag":"%s","version":"%s","commit":"%s","built_at":"%s","seq":%s,"install_sh":"%s","install_ps1":"%s"%s%s}' \
|
||||
"$component" "$channel" "$repository" "$digest" "$tag" "$version" "$commit" "$built_at" "$seq" "$install_sh" "$install_ps1" "${next_key:+,\"next_key\":\"$next_key\"}" "$binaries" > "$outdir/manifest.json"
|
||||
printf '{"schema":1,"component":"%s","channel":"%s","image":"%s","digest":"%s","tag":"%s","version":"%s","commit":"%s","built_at":"%s","seq":%s,"install_sh":"%s","install_ps1":"%s"%s}' \
|
||||
"$component" "$channel" "$repository" "$digest" "$tag" "$version" "$commit" "$built_at" "$seq" "$install_sh" "$install_ps1" "${next_key:+,\"next_key\":\"$next_key\"}" > "$outdir/manifest.json"
|
||||
|
||||
key="$(mktemp)"
|
||||
trap 'rm -f "$key" "$outdir/manifest.sig.bin"' EXIT
|
||||
|
||||
@@ -117,8 +117,6 @@ for component in agent probe; do
|
||||
# 没有配置签名密钥(例如 fork)就只发镜像、不发清单。
|
||||
if [[ -n "${RELEASE_SIGNING_KEY:-}" ]]; then
|
||||
digest="$(cat "$work/$component.digest")"
|
||||
# 清单里的 binaries:所有平台、所有架构的发布压缩包里的可执行文件(不只是镜像里的 musl 那两个),节点握手时上报的哈希对得上其中任何一个
|
||||
binaries="$(NETRC="$netrc" GITEA_BASE="$gitea_base" bash "$root/ci/release-binary-hashes.sh" "$component" "$version")"
|
||||
RELEASE_BINARIES="$binaries" bash "$root/ci/publish-release-offer.sh" "$component" "$image_root/moshiro-$component" "$digest" "$tag" release "$work/$component-release"
|
||||
bash "$root/ci/publish-release-offer.sh" "$component" "$image_root/moshiro-$component" "$digest" "$tag" release "$work/$component-release"
|
||||
fi
|
||||
done
|
||||
@@ -1,43 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# release-binary-hashes.sh 的测试:用本地的假 Gitea 包仓库(python http.server),造 8 个平台的压缩包(tar.gz 和 zip),
|
||||
# 核对脚本算出来的哈希就是压缩包里可执行文件的 SHA-256。运行:bash ci/tests/binary-hashes-test.sh
|
||||
set -eu
|
||||
ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
|
||||
T="$(mktemp -d)"
|
||||
SERVER=
|
||||
trap '[ -z "$SERVER" ] || kill "$SERVER" 2>/dev/null || true; rm -rf "$T" 2>/dev/null || true' EXIT
|
||||
V=9.9.9
|
||||
PKG="$T/srv/api/packages/justme/generic/moshiro-node/$V"
|
||||
mkdir -p "$PKG"
|
||||
fail=0
|
||||
expected=()
|
||||
for component in agent probe; do
|
||||
for target in linux-amd64 linux-arm64 linux-musl-amd64 linux-musl-arm64 macos-x64 macos-arm64 windows-amd64 windows-arm64; do
|
||||
stage="$T/stage-$component-$target"; mkdir -p "$stage"
|
||||
if [[ "$target" == windows-* ]]; then member="moshiro-$component.exe"; else member="moshiro-$component"; fi
|
||||
printf 'binary of %s for %s\n' "$component" "$target" > "$stage/$member"
|
||||
printf 'noise' > "$stage/fleet-enroll"; printf 'readme' > "$stage/README.md"
|
||||
if [[ "$target" == windows-* ]]; then
|
||||
(cd "$stage" && python3 -c "import zipfile,sys; z=zipfile.ZipFile('out.zip','w'); [z.write(n) for n in sys.argv[1:]]" "$member" fleet-enroll README.md && mv out.zip "$PKG/moshiro-$component-v$V-$target.zip")
|
||||
else
|
||||
tar -czf "$PKG/moshiro-$component-v$V-$target.tar.gz" -C "$stage" "$member" fleet-enroll README.md
|
||||
fi
|
||||
[[ "$component" == agent ]] && expected+=("$(sha256sum "$stage/$member" | cut -d' ' -f1)")
|
||||
done
|
||||
done
|
||||
want="$(printf '%s\n' "${expected[@]}" | sort -u | paste -sd,)"
|
||||
|
||||
(cd "$T/srv" && python3 -u -m http.server 0 --bind 127.0.0.1 >"$T/server.log" 2>&1 & echo $! > "$T/pid")
|
||||
SERVER="$(cat "$T/pid")"
|
||||
for _ in $(seq 1 50); do port="$(grep -o 'port [0-9]*' "$T/server.log" | head -1 | cut -d' ' -f2 || true)"; [[ -n "$port" ]] && break; sleep 0.2; done
|
||||
[[ -n "${port:-}" ]] || { echo "FAIL the fake package server did not start"; cat "$T/server.log"; exit 1; }
|
||||
|
||||
got="$(GITEA_BASE="http://127.0.0.1:$port" bash "$ROOT/ci/release-binary-hashes.sh" agent "$V")"
|
||||
if [[ "$got" == "$want" ]]; then echo "ok agent: all 8 platform binaries hashed ($(echo "$got" | tr ',' '\n' | wc -l) distinct)"; else echo "FAIL agent hashes differ"; echo " want $want"; echo " got $got"; fail=1; fi
|
||||
[[ "$(echo "$got" | tr ',' '\n' | wc -l)" -eq 8 ]] && echo "ok eight different binaries, none dropped" || { echo "FAIL expected 8 hashes"; fail=1; }
|
||||
|
||||
# 缺一个平台的压缩包:整个脚本失败,不能悄悄少签一个
|
||||
rm "$PKG/moshiro-probe-v$V-macos-arm64.tar.gz"
|
||||
if GITEA_BASE="http://127.0.0.1:$port" bash "$ROOT/ci/release-binary-hashes.sh" probe "$V" >/dev/null 2>&1; then echo "FAIL a missing archive was ignored"; fail=1; else echo "ok a missing archive fails the script instead of signing fewer binaries"; fi
|
||||
if bash "$ROOT/ci/release-binary-hashes.sh" other "$V" >/dev/null 2>&1; then echo "FAIL unknown component accepted"; fail=1; else echo "ok unknown component is refused"; fi
|
||||
exit "$fail"
|
||||
Reference in new issue
Block a user