fix: harden provider and promotion evidence
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user