security: scaffold jpkg projects with strict defaults
Jaime Fournier <jaimef@linbsd.org>
e16b2a4b3b0b9b6a5e8672b14b83884b5201c7e9
--- a/docs/jpkg-guide.md +++ b/docs/jpkg-guide.md @@ -95,6 +95,7 @@ Everything local works with zero infrastructure: ```sh mkdir demo && cd demo jpkg init @me/demo # writes a minimal jpkg.sexp +jpkg verify --strict # validates manifest + committed empty lock jpkg pack # → me-demo-0.1.0.jpkg (reproducible) jpkg verify me-demo-0.1.0.jpkg jpkg verify --reproduce # prints the deterministic artifact digest @@ -125,10 +126,11 @@ always use the explicit form `jpkg add @lisp/jerboa-ssh`. Suppress the built-in default with `JPKG_NO_DEFAULT_REGISTRY=1` (e.g. to enforce that only your own registries are visible). -Scaffold a fresh package with source and docs: +Scaffold a fresh package with source, docs, lockfile, strict policy, and a +CI template that runs `jpkg verify --strict`, `jpkg audit`, and `jpkg build`: ```sh -jpkg new @me/hello # creates hello/{jpkg.sexp,src/main.ss,README.md} +jpkg new @me/hello cd hello && jpkg build # sandboxed build + deterministic attestation ``` @@ -203,8 +205,8 @@ Global: `jpkg --help`, `jpkg --version`. Every command also works as ### Project lifecycle | Command | What it does | |---|---| -| `jpkg init [NAME]` | Create `jpkg.sexp` (NAME defaults to `@local/<dir>`). | -| `jpkg new NAME` | Scaffold a new package directory (manifest + `src/main.ss` + README). | +| `jpkg init [NAME]` | Create `jpkg.sexp`, `jpkg.policy.sexp`, and an empty `jpkg.lock` (NAME defaults to `@local/<dir>`). | +| `jpkg new NAME` | Scaffold a new package directory with manifest, strict policy, lockfile, `src/main.ss`, README, and `.build.yml`. | | `jpkg add PKG[@VERSION]` | Add a dependency (range), resolve, write lock, install. | | `jpkg remove PKG` | Drop a dependency, re-resolve, prune the environment. | | `jpkg install` | Install **exactly** `jpkg.lock` (no resolution). | --- a/docs/kimi3-security-recommmendations.md +++ b/docs/kimi3-security-recommmendations.md @@ -846,6 +846,13 @@ The jpkg hardening exists but is opt-in (`JPKG_REQUIRE_SIGNATURES`, lockfile committed, and `jpkg audit` in their CI template. Update `jpkg-guide.md` quickstart to the secure default path. +- **Status:** complete for generated jpkg projects. `jpkg init` now creates + `jpkg.sexp`, strict `jpkg.policy.sexp` requiring signatures and provenance, + and an empty `jpkg.lock` in the current directory. `jpkg new` scaffolds the + same policy/lock defaults plus `.build.yml` with `jpkg verify --strict`, + `jpkg audit`, and `jpkg build`. `docs/jpkg-guide.md` documents these + defaults in the quickstart and command table. + ### K3-P2-02 — Dependency-audit parity and freshness **Serves:** G5. **Effort:** 3 days. --- a/lib/std/pkg/commands.ss +++ b/lib/std/pkg/commands.ss @@ -26,6 +26,7 @@ artifact-info-digest artifact-info-size) (only (std pkg tarball) tar-entry-dir?) (only (std pkg lock) + lock-write-file locked-package-name locked-package-version locked-package-registry locked-package-link) (only (std pkg project) @@ -98,10 +99,15 @@ (jpkg-error "invalid package name ~s (want @scope/name, lowercase)" name)) (when (file-exists? "jpkg.sexp") (jpkg-error "jpkg.sexp already exists (refusing to overwrite)")) + (when (file-exists? "jpkg.policy.sexp") + (jpkg-error "jpkg.policy.sexp already exists (refusing to overwrite)")) + (when (file-exists? "jpkg.lock") + (jpkg-error "jpkg.lock already exists (refusing to overwrite)")) (write-file-bytevector "jpkg.sexp" (string->utf8 (manifest-template name))) + (write-secure-project-defaults! ".") ;; template must always re-parse (parse-manifest-file "jpkg.sexp") - (say "created jpkg.sexp for ~a" name) + (say "created jpkg.sexp, jpkg.policy.sexp, and jpkg.lock for ~a" name) 0)) ;; ── new ──────────────────────────────────────────────────────────────── @@ -116,6 +122,28 @@ "\n" "(main)\n")) + (def strict-policy-template + (string-append + ";; jpkg.policy.sexp — generated by jpkg; keep with jpkg.lock.\n" + "(policy\n" + " (mode strict)\n" + " (require (signatures) (provenance)))\n")) + + (def ci-template + (string-append + "image: alpine/latest\n" + "tasks:\n" + " - verify: |\n" + " jpkg verify --strict\n" + " jpkg audit\n" + " - build: |\n" + " jpkg build\n")) + + (def (write-secure-project-defaults! dir) + (write-file-bytevector (path-concat dir "jpkg.policy.sexp") + (string->utf8 strict-policy-template)) + (lock-write-file (path-concat dir "jpkg.lock") '())) + (def (cmd-new args) (unless (and (pair? args) (null? (cdr args))) (jpkg-error "usage: jpkg new NAME (NAME like @scope/name)")) @@ -131,16 +159,22 @@ (mkdir-p (path-concat dir "src")) (write-file-bytevector (path-concat dir "jpkg.sexp") (string->utf8 (manifest-template name))) + (write-secure-project-defaults! dir) (write-file-bytevector (path-concat dir "src/main.ss") (string->utf8 main-ss-template)) (write-file-bytevector (path-concat dir "README.md") (string->utf8 (string-append "# " name "\n"))) + (write-file-bytevector (path-concat dir ".build.yml") + (string->utf8 ci-template)) (parse-manifest-file (path-concat dir "jpkg.sexp")) (say "created package ~a in ~a/" name dir) (say " ~a/jpkg.sexp" dir) + (say " ~a/jpkg.policy.sexp" dir) + (say " ~a/jpkg.lock" dir) (say " ~a/src/main.ss" dir) (say " ~a/README.md" dir) + (say " ~a/.build.yml" dir) 0))) ;; ── pack ─────────────────────────────────────────────────────────────── --- a/tests/test-jpkg-commands.ss +++ b/tests/test-jpkg-commands.ss @@ -44,9 +44,14 @@ ;; ── init ──────────────────────────────────────────────────────────────── -(check "init-creates-manifest" +(check "init-creates-secure-project-files" (let ([r (run-jpkg '("init" "@test/alpha"))]) - (and (= (car r) 0) (file-exists? "jpkg.sexp")))) + (and (= (car r) 0) + (file-exists? "jpkg.sexp") + (file-exists? "jpkg.policy.sexp") + (file-exists? "jpkg.lock") + (s-contains? (utf8->string (read-file-bytevector "jpkg.policy.sexp")) + "(require (signatures) (provenance))")))) (check "init-refuses-overwrite" (let ([r (run-jpkg '("init" "@test/alpha"))]) @@ -70,12 +75,17 @@ ;; ── new ───────────────────────────────────────────────────────────────── -(check "new-scaffolds" +(check "new-scaffolds-secure-defaults" (let ([r (run-jpkg '("new" "@test/beta"))]) (and (= (car r) 0) (file-exists? "beta/jpkg.sexp") + (file-exists? "beta/jpkg.policy.sexp") + (file-exists? "beta/jpkg.lock") (file-exists? "beta/src/main.ss") - (file-exists? "beta/README.md")))) + (file-exists? "beta/README.md") + (file-exists? "beta/.build.yml") + (s-contains? (utf8->string (read-file-bytevector "beta/.build.yml")) + "jpkg audit")))) (check "new-refuses-existing-dir" (= (car (run-jpkg '("new" "@test/beta"))) 1))