fix: harden provider and promotion evidence

This commit is contained in:
DongHyeonka
2026-08-02 16:28:24 +09:00
parent 42ffb79997
commit 30ceac23c1
29 changed files with 3961 additions and 1076 deletions
+22
View File
@@ -196,6 +196,14 @@ If any external provider command, report, trust path, or key ID is absent,
promotion remains unavailable with `FAIL_UNVERIFIED`; there is no local
generator/restore fallback.
Each provider command must atomically emit strict provider evidence v2 from
the supervisor bindings for evidence type, issued/expires timestamps, run
ID/attempt, `PROVIDER_INVOCATION_NONCE`, source identity, candidate digests,
key ID, and DER-SPKI key fingerprint. Promotion receives the two supervisor
job outputs as `VULNERABILITY_INVOCATION_NONCE` and
`PROVENANCE_INVOCATION_NONCE`; do not replace them with constants or values
parsed from provider reports.
Promotion verification/staging step과 promoted-release upload action 사이에는
어떤 step도 둘 수 없고 upload에는 `if: always()`를 사용할 수 없다. 이 인접성은
실패한 검증의 publication을 막지만 staging path를 upload action에
@@ -207,6 +215,20 @@ consumer도 artifact service나 transfer action을 신뢰 경계 밖으로 보
manifest와 signed provider evidence에 바인딩된 digest를 다운로드 후 다시
검증해야 한다. 현재 producer-side adjacency 자체는 consumer-side digest
revalidation을 대신하지 않는다.
Finalizer output은 `RUNNER_TEMP` 아래 random private directory이며 exact-five
upload는 `${{ steps.finalize.outputs.staging_root }}`만 사용한다. 바로 다음
`always()` cleanup은 finalizer의 token과 runner-temp device/inode를 모두
요구한다. stable `.release/promoted-staging` directory를 만들거나 재사용하지
않는다. exact five는 captured archive/report 두 개와 process 안에서 생성한
provider/promotion verification v3 두 개이며 promotion record는 provider record,
local assessment, report hashes와 run/source/candidate/nonces/key identities/
trust-policy hash를 함께 bind한다. 이 descriptor-relative 정리는 ancestor 교체와 symlink leaf를
fail-closed로 처리하지만 upload action의 same-UID pathname reopen 또는 atomic
`renameat2` handoff를 보장하지 않는다. staging Gitea smoke/native adapter 확인
전에는 그 경계를 닫았다고 보고하지 않는다. 실제 smoke는 exact-five
upload-download와 success, validation failure, upload failure, cancellation 각각의
cleanup을 관찰해야 한다. 현재 repository에는 native uploader나 `renameat2`
보장이 없다.
Branch protection must mark each `FE-GATE-* / <name>` check required for its
declared tier. This repository cannot configure server-side protection by
+28
View File
@@ -106,6 +106,27 @@ or direct access to the sealed evidence path. Missing sandbox support, stale or
misplaced outputs, command failure/timeout, and post-command candidate drift
all stop publication.
Provider documents are strict schema v2. Their Ed25519 signature covers the
supervisor-supplied evidence type, validity window, run ID/attempt, independent
32-byte invocation nonce, archived source identity, and all four candidate
digests. Each provider job exposes its supervisor-generated nonce as a job
output; promotion treats those outputs as the independent expected values and
never lets a report define its own expected nonce. A report from another
attempt, source, archive, nonce, or key fingerprint is fail-closed even when it
has been correctly re-signed.
The immutable archive contains a strict producer-local assessment. Promotion
revalidates it from captured archive members without reopening checkout policy
or source paths. The finalizer captures the archive, both reports, and both
public keys once, generates both verification v3 records in memory, and writes
exactly five mode-`0400` files beneath a random mode-`0700` directory in
`RUNNER_TEMP`. The exact five are the captured archive, captured vulnerability
report, captured provenance attestation, generated provider-verification v3,
and generated promotion-verification v3. The promotion record binds the exact
provider-record hash, local-assessment hash, both report hashes, run/source/
candidate identities, both nonces, both key IDs/fingerprints, and canonical
trust-policy hash. It never creates or reuses `.release/promoted-staging`.
The final promotion verification/staging step must be immediately adjacent to
the promoted-release upload, and that upload must not use `always()`. This
reduces the post-verification mutation window but does not seal a pathname
@@ -116,6 +137,13 @@ service and transfer actions also remain outside the candidate's cryptographic
identity: every downstream consumer must revalidate the downloaded archive,
manifest member digests and signed provider evidence. Producer-side adjacency
does not provide consumer-side digest revalidation.
The immediately following upload action still reopens pathnames. The
descriptor-relative staging and cleanup code does not claim an atomic
`renameat2` handoff or close a malicious same-UID Gitea upload adapter; the
staging Gitea smoke/native platform adapter remains the required closure for
that boundary. That smoke must exercise exact-five upload and download plus
cleanup on success, validation failure, upload failure, and cancellation. No
native uploader or `renameat2` guarantee exists in this repository today.
Approved vulnerability exceptions require vulnerability/package identity,
owner, a different reviewer, reason and expiry. Expired or self-approved
@@ -0,0 +1,82 @@
# Security Finalizer Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Finalize a captured immutable candidate into one private random exact-five staging directory with strict v3 verification records and deterministic cleanup.
**Architecture:** `finalizeVerifiedPromotion` captures the archive, provider reports, and public keys before validation, evaluates only those captured bytes against archived local evidence, generates both v3 records in memory, and publishes five read-only files under a descriptor-relative runner-temp directory. The generated workflow consumes the returned staging path immediately and always invokes the token-bound cleanup CLI.
**Tech Stack:** Node.js 24, TypeScript, Zod, Vitest, bubblewrap-independent filesystem primitives, generated Gitea Actions YAML.
## Global Constraints
- Never accept pre-existing provider- or promotion-verification JSON as an input.
- Stage exactly archive, vulnerability report, provenance attestation, provider verification v3, and promotion verification v3.
- Use injected time and randomness for deterministic tests.
- Use a random runner-temp directory at mode `0700`, files at `0400`, and `O_EXCL | O_NOFOLLOW` creation.
- Do not claim that TypeScript closes the Gitea upload action pathname-reopen issue or guarantees `renameat2` semantics.
---
### Task 1: Exact-five finalizer contract
**Files:**
- Modify: `tests/unit/ci-artifact-contract.test.ts`
- Modify: `scripts/lib/promotion-stager.ts`
- Modify: `scripts/contracts/promotion-artifacts.ts`
**Interfaces:**
- Consumes: `finalizeVerifiedPromotion(input, dependencies)` with captured archive/report/key inputs.
- Produces: `{ stagingRoot, cleanupToken, files }` where `files` is the canonical exact-five name/digest list.
- [ ] **Step 1: Write failing tests** for no pre-existing records, strict distinct v3 roles, exact provider-record and local-assessment hashes, full run/source/candidate/nonces/key/trust bindings, key rotation, captured-source mutation, and no output on failures.
- [ ] **Step 2: Run RED:** `corepack pnpm exec vitest run tests/unit/ci-artifact-contract.test.ts -t "verified promotion finalizer" --maxWorkers=1` and retain the first contract failure.
- [ ] **Step 3: Implement minimal finalizer changes** so all validation and record generation consume captured bytes and both PASS records are created only after local/provider PASS.
- [ ] **Step 4: Run GREEN:** rerun the focused Vitest command and require zero failures.
### Task 2: Private staging and cleanup
**Files:**
- Modify: `tests/unit/ci-artifact-contract.test.ts`
- Modify: `scripts/lib/promotion-stager.ts`
- Modify: `scripts/cleanup-verified-promotion.ts`
**Interfaces:**
- Consumes: injected `randomBytes`, runner-temp root, cleanup token.
- Produces: descriptor-relative random staging at `0700`, exact files at `0400`, and token-bound cleanup.
- [ ] **Step 1: Write failing tests** for deterministic naming, modes, stable-path absence, exclusive no-follow creation, parent/leaf substitution, success cleanup, and failure cleanup.
- [ ] **Step 2: Run RED:** use the Task 1 focused Vitest command and retain the first filesystem-boundary failure.
- [ ] **Step 3: Implement minimal private publication and cleanup changes** using `/proc/self/fd` where available, bounded writes, identity rechecks, and removal of owned partial roots.
- [ ] **Step 4: Run GREEN:** rerun the focused Vitest command and require zero failures.
### Task 3: Workflow handoff
**Files:**
- Modify: `config/ci/gates.json`
- Modify: `scripts/contracts/ci-gates.ts`
- Modify: `scripts/stage-verified-promotion.ts`
- Modify: `.gitea/workflows/quality-gates.yml`
- Modify: `tests/unit/ci-workflow-generation.test.ts`
**Interfaces:**
- Consumes: finalizer step outputs `staging_root` and `cleanup_token`.
- Produces: setup, three downloads, finalizer, immediate non-`always()` exact-five upload, and `always()` cleanup ordering.
- [ ] **Step 1: Write/update failing workflow assertions** that reject standalone extraction, stable staging paths, missing `--ignore-scripts`, upload indirection, or cleanup ordering drift.
- [ ] **Step 2: Run RED:** `node scripts/generate-ci-workflow.ts --check` and the workflow snapshot test.
- [ ] **Step 3: Update the CI contract/config and regenerate YAML** with the finalizer output path and cleanup environment.
- [ ] **Step 4: Run GREEN:** require workflow byte check and snapshot test PASS.
### Task 4: Full verification and durable report
**Files:**
- Modify: `.superpowers/sdd/2026-08-01-quality-architecture-remediation/task-3-report.md`
**Interfaces:**
- Consumes: focused finalizer, provider, workflow, type, and lint evidence.
- Produces: durable RED/GREEN evidence and a commit-ready report without overclaiming platform handoff guarantees.
- [ ] **Step 1: Run verification:** focused finalizer/provider tests, `check:supply-chain:provider-fixtures`, workflow `--check`, `check:types`, and `lint`.
- [ ] **Step 2: Append exact RED/GREEN commands and outcomes** to the task report, including the remaining Gitea upload and `renameat2` limitations.
- [ ] **Step 3: Inspect diff/status** and report completion before committing.