#!/usr/bin/env bash # 更新器从镜像仓库取签名清单的测试:用一个假的 podman(只实现 pull 和 image inspect)和真的 openssl, # 不需要容器引擎、网络或 root。运行:bash packaging/update/tests/feed-test.sh # 覆盖:正常取到、仓库里的清单被篡改、用别的密钥签的、旧的 seq、换钥(next_key)、仓库不可用时回退到信箱、FEED=off。 set -eu HERE="$(cd "$(dirname "$0")" && pwd)" ROOT="$(cd "$HERE/../../.." && pwd)" T="$(mktemp -d)" trap 'rm -rf "$T"' EXIT mkdir -p "$T/bin" "$T/feed" "$T/data/update" "$T/state" "$T/conf" fail=0 # 假的 flock 和 podman:feed 目录下有 <引用>/labels 文件("manifest_b64|signature")就算拉得到 printf '#!/bin/sh\nexit 0\n' > "$T/bin/flock" cat > "$T/bin/podman" <<'STUB' #!/bin/sh dir="$FAKE_FEED" case "$1" in pull) for last; do :; done; key="$(printf '%s' "$last" | tr '/:@' '___')"; [ -f "$dir/$key" ] ;; image) for last; do :; done; key="$(printf '%s' "$last" | tr '/:@' '___')"; cat "$dir/$key" ;; *) exit 1 ;; esac STUB chmod +x "$T/bin/flock" "$T/bin/podman" export PATH="$T/bin:$PATH" FAKE_FEED="$T/feed" MOSHIRO_UPDATE_CONF_DIR="$T/conf" for k in a b c; do openssl ecparam -name prime256v1 -genkey -noout -out "$T/$k.pem" 2>/dev/null openssl ec -in "$T/$k.pem" -pubout -out "$T/$k.pub" 2>/dev/null done REPO=registry.example/moshiro-dev/moshiro-agent REF_KEY="$(printf '%s' "$REPO-manifest:dev" | tr '/:@' '___')" cat > "$T/conf/agent.conf" < [next 公钥]:写出仓库里的清单标签,和 CI 的 release-manifest.sh + publish-manifest.sh 产出同样的格式 publish() { local next="" if [ -n "${3:-}" ]; then next=",\"next_key\":\"$(openssl pkey -pubin -in "$3" -outform DER | base64 -w0)\""; fi printf '{"schema":1,"component":"agent","channel":"dev","image":"%s","digest":"%s","tag":"dev-%s","version":"0.2.0","commit":"x","built_at":"t","seq":%s%s}' "$REPO" "$DIGEST" "$2" "$2" "$next" > "$T/m.json" openssl dgst -sha256 -sign "$1" -out "$T/s.bin" "$T/m.json" printf '%s|%s' "$(base64 -w0 "$T/m.json")" "$(base64 -w0 "$T/s.bin")" > "$T/feed/$REF_KEY" } status() { sed -n 's/.*"state":"\([^"]*\)".*"error":"\([^"]*\)".*/\1|\2/p' "$T/data/update-status/status.json"; } check() { sh "$ROOT/packaging/update/moshiro-update.sh" check agent >/dev/null 2>&1 || true; status; } expect() { # 名称 期望的状态前缀 实际 if [[ "$3" == "$2"* ]]; then echo "ok $1 ($3)"; else echo "FAIL $1: expected $2*, got '$3'"; fail=1; fi } publish "$T/a.pem" 10 expect "valid manifest from the registry" "available|" "$(check)" publish "$T/a.pem" 10; sed -i 's/^\(.\{40\}\)./\1X/' "$T/feed/$REF_KEY" expect "tampered manifest is rejected, not ignored" "invalid|" "$(check)" publish "$T/c.pem" 11 expect "manifest signed by another key" "invalid|signature check failed" "$(check)" printf '%s' "$DIGEST" > "$T/state/agent.installed.tmp"; printf '99999\n%s\n' "$DIGEST" > "$T/state/agent.installed" publish "$T/a.pem" 12 expect "stale seq is just up to date" "up_to_date|" "$(check)" rm -f "$T/state/agent.installed" publish "$T/a.pem" 20 "$T/b.pub" expect "manifest carrying next_key" "available|" "$(check)" publish "$T/a.pem" 21 expect "old key no longer accepted after rotation" "invalid|signature check failed" "$(check)" publish "$T/b.pem" 22 expect "new key accepted" "available|" "$(check)" rm -f "$T/feed/$REF_KEY" expect "registry unavailable and empty mailbox" "up_to_date|" "$(check)" # 信箱回退:仓库不可用,面板放进信箱的清单照样能用(用新钥签) printf '{"schema":1,"component":"agent","channel":"dev","image":"%s","digest":"%s","tag":"dev-30","version":"0.2.0","commit":"x","built_at":"t","seq":30}' "$REPO" "$DIGEST" > "$T/data/update/offer.manifest" openssl dgst -sha256 -sign "$T/b.pem" -out "$T/s.bin" "$T/data/update/offer.manifest" base64 -w0 "$T/s.bin" > "$T/data/update/offer.sig" expect "mailbox still works without the registry" "available|" "$(check)" # FEED=off:仓库里有更新的清单也不看 publish "$T/b.pem" 40 echo "FEED=off" >> "$T/conf/agent.conf" out="$(check)" if grep -q '"seq":40' "$T/data/update-status/status.json"; then echo "FAIL FEED=off still read the registry"; fail=1; else echo "ok FEED=off ignores the registry ($out)"; fi # --- deploy-node.sh 的首次安装:从仓库取清单、用随脚本带的发布公钥验签,再按清单里的 digest 固定镜像 mkdir -p "$T/pkg/update" cp "$T/b.pub" "$T/pkg/update/release-key.pub.pem" sed -n '/^pull_quiet() {/,/^if \[ -z "\$IMAGE" \]; then/p' "$ROOT/packaging/container/deploy-node.sh" | sed '$d' > "$T/resolve.sh" resolve() { # 签名私钥 组件 镜像仓库 rm -f "$T/feed"/* printf '{"schema":1,"component":"%s","channel":"dev","image":"%s","digest":"%s","tag":"dev-1","version":"0.2.0","commit":"x","built_at":"t","seq":1}' "$2" "$3" "$DIGEST" > "$T/m.json" openssl dgst -sha256 -sign "$1" -out "$T/s.bin" "$T/m.json" printf '%s|%s' "$(base64 -w0 "$T/m.json")" "$(base64 -w0 "$T/s.bin")" > "$T/feed/$(printf '%s' "$REPO-manifest:dev" | tr '/:@' '___')" : > "$T/feed/$(printf '%s' "$REPO@$DIGEST" | tr '/:@' '___')" ( ENGINE=podman AUTHFILE="" REPO="$REPO" CHANNEL="" ROLE=agent PACKAGING="$T/pkg" IMAGE="" . "$T/resolve.sh" resolve_verified >/dev/null 2>&1 && printf '%s' "$IMAGE" || printf 'REFUSED' ) } expect "first install: signed manifest pins the digest" "$REPO@$DIGEST" "$(resolve "$T/b.pem" agent "$REPO")" expect "first install: wrong signing key is refused" "REFUSED" "$(resolve "$T/c.pem" agent "$REPO")" expect "first install: manifest for another component is refused" "REFUSED" "$(resolve "$T/b.pem" probe "$REPO")" expect "first install: manifest naming another repository is refused" "REFUSED" "$(resolve "$T/b.pem" agent "registry.evil/moshiro-dev/moshiro-agent")" # --- 第一次部署:正在跑的镜像就是清单里的这个 digest → 记成已安装(不当作有更新),并把验过的清单交给节点握手时带上 sed -i '/^FEED=off/d' "$T/conf/agent.conf" rm -f "$T/state/agent.installed" "$T/state/agent.installed.manifest" "$T/state/agent.installed.sig" "$T/data/update-status/installed."* "$T/data/update/offer."* printf 'IMAGE=%s@%s\n' "$REPO" "$DIGEST" > "$T/env" publish "$T/b.pem" 50 expect "first deploy: the running image is already up to date" "up_to_date|" "$(check)" if [ "$(sed -n 2p "$T/state/agent.installed" 2>/dev/null)" = "$DIGEST" ]; then echo "ok first deploy: install record seeded from the running image"; else echo "FAIL first deploy: no install record"; fail=1; fi label="$(cat "$T/feed/$REF_KEY")" if [ "$(cat "$T/data/update-status/installed.manifest" 2>/dev/null | base64 -w0)" = "${label%%|*}" ] && [ "$(cat "$T/data/update-status/installed.sig" 2>/dev/null)" = "${label#*|}" ]; then echo "ok the verified manifest is left for the node to present at its handshake" else echo "FAIL installed.manifest / installed.sig missing or different"; fail=1 fi # 老版本的更新器没写过:同一份(seq 相同)下次检查补上 rm -f "$T/data/update-status/installed."* "$T/state/agent.installed.manifest" "$T/state/agent.installed.sig" expect "already installed: still up to date" "up_to_date|" "$(check)" if [ -s "$T/data/update-status/installed.manifest" ] && [ -s "$T/data/update-status/installed.sig" ]; then echo "ok a missing copy is written again for the node"; else echo "FAIL the copy was not written again"; fail=1; fi exit "$fail"