From 9d2aec123033097842305befef279fd60d9e099a Mon Sep 17 00:00:00 2001 From: randypanding Date: Sat, 29 Aug 2026 15:22:03 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20W4-R3=20=E7=AD=BE=E5=90=8D=E8=AF=81?= =?UTF-8?q?=E6=8D=AE=E5=8C=85=E7=BB=91=E5=AE=9A=E9=9D=A2+=E5=9B=9E?= =?UTF-8?q?=E6=BA=AF=E5=BC=95=E6=93=8E+=E6=BC=94=E7=BB=83=E5=B7=A5?= =?UTF-8?q?=E4=BD=9C=E6=B5=81=EF=BC=88IR-0006=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - governance/attest-trace.sh:部署可回溯引擎(AC-8e)——账本反查 action=attestation-pack 事件→payload.bundle_ref→证据包互证(payload↔ attestation digest/commit 双锚)→git archive 重建→内容级验证(verify-cmd 注入 CIW attest_verify --content-only) - .github/workflows/attest-drill.yml:演练全链——pack(ATTEST_SIGNING_KEY secret 签名)→首发机械验证→archive evidence/ 绑定(write_evidence 唯一 写入器+checkpoint,PR 面)→回溯实测;产物本体不入 git(INV-06 摘要纪律) - governance/tests/test-attest-bind.sh:5 断言入 gate(回溯全链绿+三负向 漂移/伪造/缺包+事件契约)——零 CIW/archive 依赖自足 fixture --- .github/workflows/attest-drill.yml | 108 ++++++++++++++++++ governance/attest-trace.sh | 67 +++++++++++ governance/tests/test-attest-bind.sh | 163 +++++++++++++++++++++++++++ 3 files changed, 338 insertions(+) create mode 100644 .github/workflows/attest-drill.yml create mode 100644 governance/attest-trace.sh create mode 100644 governance/tests/test-attest-bind.sh diff --git a/.github/workflows/attest-drill.yml b/.github/workflows/attest-drill.yml new file mode 100644 index 0000000..abe9d33 --- /dev/null +++ b/.github/workflows/attest-drill.yml @@ -0,0 +1,108 @@ +name: attest-drill +# 签名证据包演练面(IR-0006 W4-R3 / 卡 #420 / SC-4 / ADR-0103) +# +# 全链(AC-8e 部署可回溯): +# .github@SHA 产物(git archive tar.gz) +# → CIW attest_pack(SBOM+attestation,RS256——私钥 ATTEST_SIGNING_KEY secret) +# → attest_verify 机械验证(AC-8f:digest 复算+验签,INV-01 零 LLM) +# → 绑定 archive evidence/(write_evidence 判定记录+证据包文件,PR 面) +# → attest-trace 回溯实测(账本反查→包→commit→git archive 重建→内容级验证) +# +# 产物本体不入 git(INV-06 摘要纪律):git 侧只存 SBOM/attestation/事件摘要; +# 产物可由 materials.git_commit 经 git archive 复得。 +on: + workflow_dispatch: {} + # 骨架期手动触发;真实部署产物管线接入后转周期(release/tag 钩子) + +permissions: {} + +jobs: + drill: + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + - name: 拉取 CIW 证据包工具(sparse:pipeline/attestation) + env: + GH_TOKEN: ${{ secrets.GOVERNANCE_TOKEN }} + run: | + set -euo pipefail + git clone --depth 1 --filter=blob:none --sparse \ + "https://x-access-token:${GH_TOKEN}@github.com/Cloudbird-Software/CI-Workflows.git" ciw + git -C ciw sparse-checkout set pipeline/attestation + - name: 产物 + 证据包(pack)+ 机械验证(AC-8f) + env: + ATTEST_SIGNING_KEY: ${{ secrets.ATTEST_SIGNING_KEY }} + CARD: Cloudbird-Software/.github#420 + run: | + set -euo pipefail + SHA=$(git rev-parse HEAD) + git archive --format=tar.gz -o artifact.tar.gz "$SHA" + python3 ciw/pipeline/attestation/attest_pack.py \ + --artifact artifact.tar.gz --repo Cloudbird-Software/.github \ + --commit "$SHA" --card "$CARD" --tenant cloudbird-internal --out bundle + # 首发验证:字节级 digest + SBOM 重算 + RS256 验签 + commit/card 锚 + python3 ciw/pipeline/attestation/attest_verify.py \ + --bundle bundle --artifact artifact.tar.gz \ + --pubkey ciw/pipeline/attestation/keys/attest-pub.pub \ + --expect-commit "$SHA" --expect-card "$CARD" + echo "SHA=$SHA" >> "$GITHUB_ENV" + - name: 绑定 archive evidence/ 判定记录(write_evidence 唯一写入器,PR 面) + env: + GH_TOKEN: ${{ secrets.GOVERNANCE_TOKEN }} + CARD: Cloudbird-Software/.github#420 + RUN_ID: ${{ github.run_id }} + run: | + set -euo pipefail + ART_D=$(python3 -c "import json;print(json.load(open('bundle/attestation.json'))['subject']['digest']['sha256'])") + SBOM_D=$(python3 -c "import json;print(json.load(open('bundle/attestation.json'))['materials']['sbom_sha256'])") + BID="evidence/attestations/$SHA-${ART_D:0:12}" + git clone --depth 5 "https://x-access-token:${GH_TOKEN}@github.com/Cloudbird-Software/archive.git" arch + mkdir -p "arch/$BID" + cp bundle/sbom.json bundle/attestation.json "arch/$BID/" + cat > ev.json <> "$GITHUB_ENV" + - name: 回溯实测(AC-8e:从判定记录反查产物全链) + env: + CARD: Cloudbird-Software/.github#420 + run: | + set -euo pipefail + # 从 PR 分支账本反查(合并前即可回溯——绑定即生效) + BR="attest-$RUN_ID" + git -C arch fetch origin "$BR" 2>/dev/null || true + bash governance/attest-trace.sh \ + --ledger arch/evidence/ledger.jsonl \ + --bundle-root arch \ + --git-repo . \ + --verify-cmd "python3 ciw/pipeline/attestation/attest_verify.py --bundle {BUNDLE} --artifact {ARTIFACT} --pubkey ciw/pipeline/attestation/keys/attest-pub.pub --content-only --expect-commit $SHA --expect-card $CARD" + echo "OK 回溯全链绿(账本→证据包→commit→git archive 重建→内容级验证)——archive PR #$ARCH_PR 待 verify.yml 绿后合并" diff --git a/governance/attest-trace.sh b/governance/attest-trace.sh new file mode 100644 index 0000000..9a62ed9 --- /dev/null +++ b/governance/attest-trace.sh @@ -0,0 +1,67 @@ +#!/usr/bin/env bash +# attest-trace.sh —— 部署可回溯:从 evidence/ 判定记录反查产物全链(IR-0006 W4-R3 / AC-8e) +# +# 回溯链(机械,零 LLM): +# 产物 digest ──→ archive evidence/ledger.jsonl(action=attestation-pack 事件: +# payload.bundle_ref + artifact_digest)──→ 证据包(attestation.json+sbom.json) +# ──→ materials.git_commit ──→ git archive 重建产物 ──→ 内容级验证(verify-cmd) +# +# 事件 payload 契约(write_evidence 写入,4KB 内联 INV-06): +# {"bundle_ref": "evidence/attestations/", "artifact_digest": "", +# "sbom_sha256": "", "verify": "signed+verified"} +# +# 用法: +# attest-trace.sh --ledger --bundle-root \ +# --git-repo <产物源仓> --verify-cmd "<模板:{BUNDLE} {ARTIFACT} 代入>" +# 退出码:0=回溯全链绿 | 1=链断(事件/包/digest/验证任一不符)| 2=infra +set -uo pipefail + +die2() { echo "FATAL attest-trace: $*" >&2; exit 2; } +die1() { echo "REJECT attest-trace: $*" >&2; exit 1; } + +while [[ $# -gt 0 ]]; do + case "$1" in + --ledger) LEDGER="$2"; shift 2 ;; + --bundle-root) ROOT="$2"; shift 2 ;; + --git-repo) REPO="$2"; shift 2 ;; + --verify-cmd) VCMD="$2"; shift 2 ;; + *) die2 "未知参数: $1" ;; + esac +done +[[ -n "${LEDGER:-}" && -n "${ROOT:-}" && -n "${REPO:-}" && -n "${VCMD:-}" ]] \ + || { echo "用法: attest-trace.sh --ledger --bundle-root --git-repo --verify-cmd " >&2; exit 2; } +[[ -f "$LEDGER" ]] || die2 "账本不存在: $LEDGER" +[[ -d "$REPO/.git" || -d "$REPO" ]] || die2 "源仓不存在: $REPO" + +# ---- 1. 账本反查:最新 attestation-pack 事件 ---- +EV=$(grep '"action":"attestation-pack"' "$LEDGER" | tail -1) +[[ -n "$EV" ]] || die1 "账本无 attestation-pack 事件(绑定缺失——AC-8e 链断)" +PAYLOAD=$(jq -r '.payload' <<<"$EV") +[[ -n "$PAYLOAD" && "$PAYLOAD" != "null" ]] || die1 "事件 payload 缺失" +BUNDLE_REF=$(jq -r '.bundle_ref' <<<"$PAYLOAD") +ART_DIGEST=$(jq -r '.artifact_digest' <<<"$PAYLOAD") +COMMIT=$(jq -r '.subject.commit // empty' <<<"$EV") +CARD=$(jq -r '.subject.card' <<<"$EV") +[[ -n "$BUNDLE_REF" && -n "$ART_DIGEST" ]] || die1 "payload 契约字段缺失(bundle_ref/artifact_digest)" +[[ -n "$COMMIT" ]] || die1 "subject.commit 缺失(回溯链断)" +echo "OK 账本反查命中:$CARD @ $COMMIT → $BUNDLE_REF" + +# ---- 2. 证据包在位 + 记录一致性(payload ↔ attestation 互证) ---- +BUNDLE="$ROOT/$BUNDLE_REF" +[[ -f "$BUNDLE/attestation.json" && -f "$BUNDLE/sbom.json" ]] \ + || die1 "证据包文件缺失: $BUNDLE" +SUBJ_DIGEST=$(jq -r '.subject.digest.sha256' "$BUNDLE/attestation.json") +SBOM_SHA=$(jq -r '.materials.sbom_sha256' "$BUNDLE/attestation.json") +ATTEST_COMMIT=$(jq -r '.materials.git_commit' "$BUNDLE/attestation.json") +[[ "$SUBJ_DIGEST" == "$ART_DIGEST" ]] || die1 "payload.artifact_digest ≠ attestation.subject.digest(两套记录漂移)" +[[ "$ATTEST_COMMIT" == "$COMMIT" ]] || die1 "事件 commit ≠ attestation.materials.git_commit(锚点漂移)" +echo "OK 记录一致性绿(payload↔attestation 互证,commit 锚一致)" + +# ---- 3. 产物重建(git archive)+ 内容级验证 ---- +TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT +git -C "$REPO" archive --format=tar.gz -o "$TMP/artifact.tar.gz" "$COMMIT" 2>/dev/null \ + || die1 "git archive 失败(commit=$COMMIT 不在源仓?)" +CMD=${VCMD//\{BUNDLE\}/$BUNDLE} +CMD=${CMD//\{ARTIFACT\}/$TMP/artifact.tar.gz} +bash -c "$CMD" || die1 "内容级验证红(产物与 SBOM 漂移或验签失败)" +echo "OK 回溯全链绿:$CARD ← 事件 ← 证据包 ← commit $COMMIT ← 产物重建内容一致" diff --git a/governance/tests/test-attest-bind.sh b/governance/tests/test-attest-bind.sh new file mode 100644 index 0000000..ca0cfee --- /dev/null +++ b/governance/tests/test-attest-bind.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +# test-attest-bind.sh —— W4-R3(#420)AC-8e 部署可回溯入 gate +# +# 离线自足(CIW/archive 仓均不依赖): +# fixture=throwaway RSA 密钥 + 临时 git 仓(git archive 产物)+ 内联生成 +# 证据包(sbom+attestation,与 CIW attest_pack.py 同契约)+ 模拟账本事件 +# (write_evidence 链字段由本测试按同款算法计算——trace 只消费契约字段)。 +# 断言:attest-trace.sh 全链绿;三负向(payload/attestation 漂移、commit 伪造、 +# 包缺失)必红。 +set -uo pipefail +DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +PASS=0; FAIL=0 +ok() { PASS=$((PASS+1)); echo "PASS $1"; } +bad() { FAIL=$((FAIL+1)); echo "FAIL $1"; } + +TMP=$(mktemp -d); trap 'rm -rf "$TMP"' EXIT + +# ---- fixture:源仓 + 产物 + 密钥 ---- +git init -q "$TMP/repo" && git -C "$TMP/repo" config user.name t && git -C "$TMP/repo" config user.email t@t +mkdir -p "$TMP/repo/gov" && echo "engine-v1" > "$TMP/repo/gov/engine.sh" && echo "policy-v1" > "$TMP/repo/policy.yaml" +git -C "$TMP/repo" add -A && git -C "$TMP/repo" commit -qm v1 +COMMIT=$(git -C "$TMP/repo" rev-parse HEAD) +echo "$COMMIT" > "$TMP/commit" +git -C "$TMP/repo" archive --format=tar.gz -o "$TMP/artifact.tar.gz" HEAD +openssl genrsa -out "$TMP/sk.pem" 2048 2>/dev/null +openssl rsa -in "$TMP/sk.pem" -pubout -out "$TMP/pub.pem" 2>/dev/null + +# ---- 证据包(内联生成,与 CIW attest_pack.py 同契约) ---- +python3 - "$TMP" <<'PY' +import base64, hashlib, json, os, subprocess, sys, tarfile, tempfile, datetime +tmp = sys.argv[1] +def sha(b): return hashlib.sha256(b).hexdigest() +art = f"{tmp}/artifact.tar.gz" +blob = open(art, "rb").read() +files = [] +with tarfile.open(art) as tf: + for m in tf.getmembers(): + if m.isfile(): + files.append({"path": m.name, "sha256": sha(tf.extractfile(m).read()), "size": m.size}) +files.sort(key=lambda x: x["path"]) +sbom = {"sbom_version": "0", "format": "file-manifest", + "artifact": {"name": "artifact.tar.gz", "sha256": sha(blob), "size": len(blob)}, + "source": {"repo": "Cloudbird-Software/.github", "commit": open(f"{tmp}/commit").read().strip()}, + "files": files, + "generated_at": "2026-08-29T00:00:00Z"} +canon = json.dumps(sbom, ensure_ascii=False, sort_keys=True, separators=(",", ":")) +statement = {"_type": "attest-pack/v0", + "subject": {"name": "artifact.tar.gz", "digest": {"sha256": sha(blob)}}, + "materials": {"sbom_sha256": sha(canon.encode()), "repo": "Cloudbird-Software/.github", + "git_commit": sbom["source"]["commit"]}, + "predicate": {"card": "Cloudbird-Software/.github#420", "tenant": "cl", + "tool": "fixture", "generated_at": "2026-08-29T00:00:00Z"}} +msg = json.dumps(statement, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode() +with tempfile.NamedTemporaryFile(delete=False) as mf: + mf.write(msg) +sig = subprocess.run(["openssl", "dgst", "-sha256", "-sign", f"{tmp}/sk.pem", mf.name], + capture_output=True).stdout +os.unlink(mf.name) +att = dict(statement); att["signature"] = {"alg": "RS256", "sig": base64.b64encode(sig).decode()} +bdir = f"{tmp}/root/evidence/attestations/fixture-001" +os.makedirs(bdir, exist_ok=True) +json.dump(sbom, open(f"{bdir}/sbom.json", "w"), ensure_ascii=False, indent=1) +json.dump(att, open(f"{bdir}/attestation.json", "w"), ensure_ascii=False, indent=1) +PY + +# ---- 账本事件(模拟 write_evidence 输出——链字段同款算法) ---- +make_event() { python3 - "$TMP" < "$TMP/verify_fixture.py" <<'PY' +import base64, hashlib, json, subprocess, sys, tarfile, tempfile +bundle, artifact, pub = sys.argv[1], sys.argv[2], sys.argv[3] +att = json.load(open(f"{bundle}/attestation.json")) +sb = json.load(open(f"{bundle}/sbom.json")) +files = [] +with tarfile.open(artifact) as tf: + for m in tf.getmembers(): + if m.isfile(): + files.append({"path": m.name, "sha256": hashlib.sha256(tf.extractfile(m).read()).hexdigest(), + "size": m.size}) +files.sort(key=lambda x: x["path"]) +assert files == sb["files"], "SBOM 漂移" +msg = json.dumps({k: v for k, v in att.items() if k != "signature"}, + ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode() +with tempfile.NamedTemporaryFile(delete=False) as mf, tempfile.NamedTemporaryFile(delete=False) as sf: + mf.write(msg) + sf.write(base64.b64decode(att["signature"]["sig"])) +# 先 close 再验签(NamedTemporaryFile 缓冲未 flush 时 openssl 读到空文件——实测教训) +r = subprocess.run(["openssl", "dgst", "-sha256", "-verify", pub, "-signature", sf.name, mf.name], + capture_output=True) +assert r.returncode == 0, "RS256 验签失败" +PY +VCMD="python3 '$TMP/verify_fixture.py' {BUNDLE} {ARTIFACT} '$TMP/pub.pem'" + +# ---- 正向:回溯全链 ---- +bash "$DIR/governance/attest-trace.sh" --ledger "$TMP/ledger.jsonl" --bundle-root "$TMP/root" \ + --git-repo "$TMP/repo" --verify-cmd "$VCMD" >/dev/null 2>&1 +[[ $? -eq 0 ]] && ok "AC-8e 回溯全链绿(账本→包→commit→重建→内容+验签)" || bad "回溯全链红" + +# ---- 负向 1:payload artifact_digest 漂移 ---- +python3 - "$TMP" <<'PY' +import json, sys +tmp = sys.argv[1] +lines = open(f"{tmp}/ledger.jsonl").read().strip().split("\n") +ev = json.loads(lines[0]) +p = json.loads(ev["payload"]) +d = list(p["artifact_digest"]); d[0] = "0" if d[0] != "0" else "1" +p["artifact_digest"] = "".join(d) +ev["payload"] = json.dumps(p, ensure_ascii=False, separators=(",", ":")) +open(f"{tmp}/ledger-bad1.jsonl", "w").write(json.dumps(ev, ensure_ascii=False, separators=(",", ":")) + "\n") +PY +bash "$DIR/governance/attest-trace.sh" --ledger "$TMP/ledger-bad1.jsonl" --bundle-root "$TMP/root" \ + --git-repo "$TMP/repo" --verify-cmd "$VCMD" >/dev/null 2>&1 +[[ $? -eq 1 ]] && ok "payload↔attestation digest 漂移 → 红(互证锚执法)" || bad "负向 1 漏检" + +# ---- 负向 2:commit 伪造(不在源仓) ---- +python3 - "$TMP" <<'PY' +import json, sys +tmp = sys.argv[1] +ev = json.loads(open(f"{tmp}/ledger.jsonl").read().strip()) +ev["subject"]["commit"] = "1234567890" * 4 +open(f"{tmp}/ledger-bad2.jsonl", "w").write(json.dumps(ev, ensure_ascii=False, separators=(",", ":")) + "\n") +PY +bash "$DIR/governance/attest-trace.sh" --ledger "$TMP/ledger-bad2.jsonl" --bundle-root "$TMP/root" \ + --git-repo "$TMP/repo" --verify-cmd "$VCMD" >/dev/null 2>&1 +[[ $? -eq 1 ]] && ok "commit 伪造 → 红(git archive 锚执法)" || bad "负向 2 漏检" + +# ---- 负向 3:包缺失 ---- +mv "$TMP/root/evidence/attestations/fixture-001" "$TMP/hidden" +bash "$DIR/governance/attest-trace.sh" --ledger "$TMP/ledger.jsonl" --bundle-root "$TMP/root" \ + --git-repo "$TMP/repo" --verify-cmd "$VCMD" >/dev/null 2>&1 +[[ $? -eq 1 ]] && ok "证据包缺失 → 红(bundle_ref 锚执法)" || bad "负向 3 漏检" +mv "$TMP/hidden" "$TMP/root/evidence/attestations/fixture-001" + +# ---- 事件 schema 契约(write_evidence 执法面:card/tenant/inputs_digest) ---- +jq -e '.subject.card == "Cloudbird-Software/.github#420" and .subject.tenant == "cl" and (.inputs_digest | startswith("sha256:"))' \ + "$TMP/ledger.jsonl" >/dev/null && ok "事件契约字段齐(join key+tenant+inputs_digest provenance 锚)" || bad "事件契约字段缺" + +echo "----------------------------------------" +echo "test-attest-bind: $([[ $FAIL -eq 0 ]] && echo PASS || echo "FAIL($FAIL)")" +exit $([[ $FAIL -eq 0 ]] && echo 0 || echo 1)