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.
124 lines
6.5 KiB
Bash
124 lines
6.5 KiB
Bash
#!/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" <<EOF
|
||
DATA_DIR=$T/data
|
||
IMAGE_REPO=$REPO
|
||
UNIT=x
|
||
PUBKEY=$T/a.pub
|
||
ENV_FILE=$T/env
|
||
STATE_DIR=$T/state
|
||
POLICY=manual
|
||
ENGINE=podman
|
||
EOF
|
||
DIGEST="sha256:$(printf 'a%.0s' $(seq 64))"
|
||
|
||
# publish <签名私钥> <seq> [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/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
|
||
expect "already installed: still up to date" "up_to_date|" "$(check)"
|
||
|
||
exit "$fail"
|