diff --git a/cmd/llm_fallback.txt b/cmd/llm_fallback.txt index 259e2f3..0d29277 100644 --- a/cmd/llm_fallback.txt +++ b/cmd/llm_fallback.txt @@ -192,10 +192,11 @@ Current limits (source values): Zip scan and signing (preview, CLI v0.2): - `vapour scan ` (also `--dir`, `--json`) checks a build locally; exit 1 on any error finding. It writes no attestation. - `vapour verify [--attestation ] [--json]` checks a zip against its attestation locally. -- `vapour publish` (including `--dry-run`) runs the scan, signs the zip and writes `.vapour/build.zip.attestation.json`. There is no flag to skip the scan. With `--experimental-chunks` the scan runs but no attestation is sent. +- `vapour publish` (including `--dry-run`) scans, signs and locally verifies the exact ZIP; it writes `.vapour/build.zip.attestation.json`. There is no flag to skip verification. `--experimental-chunks` supports dry runs but cannot upload without an attestation. - `vapour verify --remote --version ` also compares the server's recorded SHA-256 and status; exit 1 unless the status is `verified`. - `vapour verify --remote --version ` may omit the ZIP: it checks the signed attestation and server record, but not local ZIP bytes. `--dir` selects the project and `--json` emits a result object. -- Server verification is asynchronous: the upload is accepted right away and the server records `pending`, then `verified`, `rejected` or `unverified`. `unverified` means no attestation was sent (older CLI) and the server scan passed. +- The server first requires and independently verifies the signed attestation, registered developer key, ZIP SHA-256 and size before storing a version. Missing signatures return 400 `verification_required`; forged, mismatched or wrong-key signatures return 400 `verification_failed`. Malformed, duplicate or oversized attestations return 400 `invalid_upload`. +- The independent archive scan is asynchronous after accepted uploads: the server records `pending`, then `verified` or `rejected`. Legacy versions or queue failures can still appear as `unverified`; new unsigned ZIP uploads are not accepted. - Reject reasons include `bad_signature`, `hash_mismatch`, `size_mismatch`, `bad_key_id`, `bad_key`, `unsupported_version`, `unknown_key`, `bad_attestation`, or any scan code. A rejected version is taken offline and can never be approved; publish a new version. - Malformed requests (invalid attestation JSON, attestation over 64 KiB) fail immediately with HTTP 400 `invalid_upload`. - The first valid signing key a developer account uses is remembered; a zip signed by a different key is rejected with `unknown_key`. Losing the key requires a platform-team reset. diff --git a/cmd/publish.go b/cmd/publish.go index 6ea0f87..5232136 100644 --- a/cmd/publish.go +++ b/cmd/publish.go @@ -16,6 +16,7 @@ import ( "github.com/vapour-computer/vapour-cli/internal/project" "github.com/vapour-computer/vapour-cli/internal/storepage" "github.com/vapour-computer/vapour-cli/internal/zipdir" + "github.com/vapour-computer/vapour-cli/internal/zipsign" "github.com/vapour-computer/vapour-cli/pkg/delivery" ) @@ -67,6 +68,9 @@ Release notes are required with --notes or --notes-file.`, return err } if chunked { + if !dryRun { + return errors.New("--experimental-chunks cannot upload without a signed build attestation; use ZIP publishing") + } if err := scanFolder(out, buildPath); err != nil { return err } @@ -160,6 +164,15 @@ Release notes are required with --notes or --notes-file.`, if err != nil { return err } + // Use the same local verification as `vapour verify` before any + // network call. The API independently verifies the signed bytes. + att, err := zipsign.Parse(attestation) + if err != nil { + return fmt.Errorf("cannot upload an unverified build: %w", err) + } + if err := zipsign.Verify(zipPath, att); err != nil { + return fmt.Errorf("cannot upload an unverified build: %w", err) + } printBundle(out, bundle) meta, err := publishMetadata(m, dir) if err != nil { diff --git a/cmd/zipcheck_test.go b/cmd/zipcheck_test.go index 5dfc9b3..56dfa14 100644 --- a/cmd/zipcheck_test.go +++ b/cmd/zipcheck_test.go @@ -355,6 +355,18 @@ func TestPublishUploadsAttestationAndBuild(t *testing.T) { } } +func TestChunkUploadCannotBypassVerificationGate(t *testing.T) { + api := zipEnv(t) + dir := writeFolderProject(t) + out, err := run(t, "publish", "--dir", dir, "--version", "0.1.0", "--notes", "x", "--skip-store", "--experimental-chunks") + if err == nil || !strings.Contains(err.Error(), "cannot upload without a signed build attestation") { + t.Fatalf("chunk upload should be blocked: err=%v out=%q", err, out) + } + if api.count() != 0 { + t.Fatalf("chunk upload reached server: %d request(s)", api.count()) + } +} + func TestVerifyLocalZipAndTampering(t *testing.T) { t.Run("happy path", func(t *testing.T) { zipEnv(t) diff --git a/docs/llm/publish.md b/docs/llm/publish.md index 73168e2..ae08042 100644 --- a/docs/llm/publish.md +++ b/docs/llm/publish.md @@ -5,7 +5,7 @@ Flags: `--dir`, required `--version`, exactly one of `--notes` or `--notes-file` Order for default ZIP publishing: load + validate manifest → load store bundle (unless `--skip-store`) → resolve release notes (`--notes-file` or `--notes`, non-empty after trimming; ≤5000 chars) → pack the build → scan the ZIP → sign it and write -`.vapour/build.zip.attestation.json` → (stop here on `--dry-run`) → register project if no id → push store page → upload +`.vapour/build.zip.attestation.json` → verify the signed ZIP locally → (stop here on `--dry-run`) → register project if no id → push store page → upload build and attestation. Store goes first so rejected media doesn't use up a version. The server verifies the ZIP, then an admin reviews the version before it goes live. @@ -26,6 +26,10 @@ Unless suppressed by `--no-perf-hints`, command output also includes warnings fr A version can be published once: API error `version_exists` becomes "pass a new --version". Success prints the game URL. -`--experimental-chunks`: opt-in content-addressed chunk protocol (`pkg/delivery/README.md`); the deployed API doesn't -implement it yet. Default remains ZIP. The chunk path scans but does not send a signed ZIP attestation or release notes. -Upload chunk URLs must be HTTPS and on an allowlisted public host. +`--experimental-chunks`: its dry run still scans, but real chunk uploads are blocked until this protocol can provide +a signed attestation that the API can verify. Use ZIP publishing for uploads. The deployed API does not implement the +chunk protocol yet. Upload chunk URLs must be HTTPS and on an allowlisted public host. + +The API requires an attestation on every ZIP upload and verifies its Ed25519 signature, key identity, ZIP SHA-256 and +size before writing the build. Missing or forged attestations fail with `verification_required` or `verification_failed`. +The CLI's local check does not replace the server's check.