feat(store): add Ed25519 manifest signing and verification for install-v2#1924
feat(store): add Ed25519 manifest signing and verification for install-v2#1924hognek wants to merge 0 commit into
Conversation
|
Warning Review limit reached
Next review available in: 8 minutes Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (6)
📝 WalkthroughWalkthroughStore manifests now use persistent Ed25519 signatures. Catalog loading stores signature data, app startup wires signing keys, and ChangesStore manifest signing
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant Client
participant install_v2
participant AppRegistry
participant store_signing
Client->>install_v2: POST /api/store/install-v2
install_v2->>AppRegistry: verify_manifest_signature(app_id, public_pem)
AppRegistry->>store_signing: verify_manifest_signature(manifest, signature, public_pem)
store_signing-->>AppRegistry: verification result
AppRegistry-->>install_v2: valid or invalid
install_v2-->>Client: install response or HTTP 403
Possibly related PRs
Suggested reviewers: 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
| pub = data["public_pem"].encode() | ||
| # Sanity-check: can we deserialize the key? | ||
| _load_private_key(priv) | ||
| return priv, pub |
There was a problem hiding this comment.
WARNING: Restrictive permissions (0o600) are only applied when the keypair is created (line 105); when an existing keyfile is loaded here, its permissions are never checked or re-enforced. If the file was written under a loose umask, copied, backed-up, or chmod'd by an admin/tool, the Ed25519 private key can end up world- or group-readable on disk. Since the security model is explicitly "local-tamper-detection" and the private key lives in <data_dir>, you should os.chmod(keyfile, 0o600) on the load path too (after confirming it is the owner), or at minimum assert the mode and warn/regenerate if it is too open.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| # here, blocking any install that would pull untrusted scripts or images. | ||
| _store_pub = getattr(request.app.state, "store_signing_pubkey", None) | ||
| if _store_pub is not None and registry is not None and hasattr(registry, "verify_manifest_signature"): | ||
| if not registry.verify_manifest_signature(manifest_id, _store_pub): |
There was a problem hiding this comment.
WARNING: The gate verifies the signature against self._manifest_dicts[app_id] and self._signatures[app_id], both of which are populated once at boot from the YAML (registry._load_catalog). It does not re-read the manifest from disk at install time. So while the server process is running, an attacker who edits manifest.yaml on disk does not change the in-memory copy, the re-verify passes, and the install proceeds using the (safe) in-memory manifest — the tampering is never detected. The "post-boot catalog tampering" protection only fires across a process restart, and on a restart an attacker with write access to the catalog can also replace store_signing_key.json (same data_dir), causing a fresh keypair to be minted and every manifest re-signed, so the gate passes again. Net: against a local attacker who can write the catalog dir, this gate provides weak assurance. Either (a) re-read and re-verify the on-disk manifest.yaml at install time, or (b) document precisely that this only defends against a narrow threat (e.g. read-only media / accidental drift) so reviewers don't over-trust it.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| """ | ||
| pub_pem = getattr(request.app.state, "store_signing_pubkey", None) | ||
| if pub_pem is None: | ||
| return JSONResponse({"error": "store signing key not configured"}, status_code=500) |
There was a problem hiding this comment.
SUGGESTION: Returning HTTP 500 for the expected, normal "key not configured" state is misleading. 500 signals a server-side fault to clients, monitoring, and alerting, while this is a legitimate configuration condition. It also forces the test test_returns_500_when_not_configured to assert a server-error code for a non-error situation. Prefer 404 Not Found (no public key published) or 503 Service Unavailable. The same concern applies to how store_signing_pubkey interacts with the install gate, which already treats a missing key as skip.
| return JSONResponse({"error": "store signing key not configured"}, status_code=500) | |
| return JSONResponse({"error": "store signing key not configured"}, status_code=404) |
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| sig = self._signatures.get(app_id) | ||
| if manifest_dict is None or sig is None: | ||
| # No signature stored (e.g. signing key not available at load time). | ||
| return False |
There was a problem hiding this comment.
SUGGESTION: When no signature is stored (manifest_dict is None or sig is None) this returns False, which the install gate (store_install.py:708) turns into a hard 403. But the PR description states "When no signing key is configured, the gate is skipped (graceful degradation)" — and the skip is actually gated on app.state.store_signing_pubkey is not None, not on whether signatures exist. A registry built without a signing_key (e.g. constructed elsewhere, or after a reload() with self._signing_key reset) will have empty _signatures/_manifest_dicts, so every install is rejected with 403 rather than degraded. Consider distinguishing the two cases: missing-signature ⇒ degrade/skip (or a distinct 4xx), and signature-present-but-invalid ⇒ 403. This prevents a misconfigured registry from blocking all installs.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
Code Review SummaryStatus: No New Issues (reconciled) | Recommendation: Merge OverviewThe PR (1924) has been merged and a follow-up security-hardening commit ( Resolved findings (verified against current HEAD)
Files Reviewed (reconciled — current merged tree)
Previous Review Summaries (3 snapshots, latest commit 147d6be)Current summary above is authoritative. Previous snapshots are kept for context only. Previous review (commit 147d6be)Status: No New Issues (incremental) | Recommendation: Merge Overview (current incremental changes)The only file changed since the previous review commit (
No new defects were introduced by the incremental change. The remaining open findings (permissions-on-load, in-memory verification scope, missing-key degradation, registry exception handling, boot coupling in Files Reviewed (incremental: 1 changed file)
Previous review (commit 18524fd)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)SUGGESTION
Resolved since previous review (incremental change to store_signing.py)
Files Reviewed (incremental: 1 changed file)
Fix these issues in Kilo Cloud Previous review (commit ac135b6)Status: 1 Issue Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Resolved since previous review
Files Reviewed (5 changed files)
Reviewed by hy3:free · Input: 74.9K · Output: 4.3K · Cached: 324.4K |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/test_routes_store_install.py`:
- Around line 437-494: Add an install-v2 test using a real AppRegistry instead
of mocking verify_manifest_signature. Load the manifest into the registry,
mutate the catalog entry afterward, configure the signing key and install
dependencies as needed, then assert the POST to /api/store/install-v2 returns
403 with the manifest-signature failure response. Keep the existing mocked tests
unchanged.
In `@tests/test_store_signing.py`:
- Around line 47-51: Update test_verify_tampered_signature so bad_sig always
differs from sig: replace the final signature byte with a value selected to
differ from its original value, rather than unconditionally appending "ff".
Preserve the existing verification assertion.
In `@tinyagentos/registry.py`:
- Around line 129-136: Replace local signing of untrusted catalog data in
tinyagentos/registry.py lines 129-136 with verification against a trusted
publisher signature or pinned catalog digest. In tinyagentos/registry.py lines
168-182, re-read and return the verified install-time manifest instead of
validating _manifest_dicts against its own stored signature. Update
tests/test_routes_store_install.py lines 437-494 to use a real AppRegistry and
mutate the catalog after loading, covering end-to-end tamper detection.
In `@tinyagentos/routes/store_install.py`:
- Around line 702-708: Centralize manifest retrieval and signature verification
in the install flow around the existing manifest gate, then use the verified
result for the requested app and every dependency resolved before execution.
Ensure each backend manifest, including those used by chained installs near
backend_manifest.install, is verified with store_signing_pubkey before any
install logic runs, and reject unsigned or invalid manifests.
In `@tinyagentos/store_signing.py`:
- Around line 83-108: Update the key persistence flow around the keyfile loading
and generation logic to create the temporary private-key file exclusively with
os.open using O_CREAT|O_EXCL and mode 0o600, handling FileExistsError by loading
the key written by the competing process. Derive the persisted public key from
the validated private key instead of trusting data["public_pem"], and preserve
atomic replacement and existing corruption-regeneration behavior.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro Plus
Run ID: 3070cb61-2e1c-4a64-9db3-55c6732e6fea
📒 Files selected for processing (6)
tests/test_routes_store_install.pytests/test_store_signing.pytinyagentos/app.pytinyagentos/registry.pytinyagentos/routes/store_install.pytinyagentos/store_signing.py
e663eb1 to
5ec192d
Compare
| catalog.append(AppManifest.from_dict(raw_dict, manifest_dir=app_dir)) | ||
| if self._signing_key is not None: | ||
| from tinyagentos.store_signing import sign_manifest | ||
|
|
There was a problem hiding this comment.
WARNING: sign_manifest() is called inside the catalog-load loop but the surrounding except only catches (yaml.YAMLError, KeyError). If self._signing_key is malformed PEM or _load_private_key raises (ValueError/TypeError), that exception is NOT caught — it propagates out of _load_catalog → _ensure_loaded → and every registry accessor (get, get_signature, verify_manifest_signature) raises. That means one bad signing key (or a transient crypto error) takes down the entire app store, not just the offending manifest. Add (ValueError, TypeError) (or a broad Exception) to the except, matching the KeyError/YAMLError skip-and-continue pattern used for invalid manifests.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
| registry = AppRegistry(catalog_dir=catalog_dir, installed_path=installed_path) | ||
|
|
||
| from tinyagentos.store_signing import load_or_create_signing_keypair | ||
|
|
There was a problem hiding this comment.
SUGGESTION: create_app now unconditionally calls load_or_create_signing_keypair(data_dir) during boot. This couples server startup to the ability to write a private key into data_dir (and to cryptography/json succeeding). On a first boot where data_dir is read-only, mounted read-only, or the store_signing_key.json.tmp write is denied, this raises and the whole app fails to start — even for deployments that don't need store signing (the gate already degrades gracefully when no key is configured). Consider wrapping this in try/except and proceeding with _store_pub = None on failure, or only creating the keypair lazily, so a non-writable keyfile doesn't block server boot.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
|
Held. The crypto primitives are sound and failure-closed, but the verification does not deliver its stated security property yet, so I do not want to merge it as-is:
Also fold the remaining Kilo nits (manifest-present-but-no-signature returning a hard 403 makes an app uninstallable when a key is configured; the |
| priv, pub = generate_signing_keypair() | ||
| # Fall through to writing below (retry the O_EXCL dance). | ||
| try: | ||
| fd = _os_module.open(tmp, _os_module.O_CREAT | _os_module.O_EXCL | _os_module.O_WRONLY, 0o600) |
There was a problem hiding this comment.
WARNING: This FileExistsError fallback path is broken and can raise an unhandled exception during boot.
When the first os.open(tmp, O_CREAT|O_EXCL, ...) at line 136 fails because a competing process already created tmp, and after the 0.1s sleep keyfile still does not exist (the winner hasn't run os.replace yet), this branch mints a fresh keypair and retries O_EXCL on the same tmp path (line 157). But tmp is still present (the competing process created it and has not yet promoted it), so this second O_EXCL almost always raises FileExistsError again, falling into the inner except at line 158. There, if keyfile still isn't ready, the bare raise propagates FileExistsError out of load_or_create_signing_keypair, which then bubbles up through create_app and can crash boot under startup contention.
Secondary problems in this path:
- The fresh keypair minted at line 154 is silently discarded whenever the code recurses, wasting an Ed25519 generation and adding confusion.
- The fixed
time.sleep(0.1)is a fragile timing assumption; on a slow/loaded host the winner may still not have completedos.replace, producing spurious regeneration or the crash above.
Consider a bounded retry/backoff loop that only ever loads the winner's key (never re-mints on the contended path), and remove the same-tmp re-open. Cleaning up a stale tmp should only happen if you can prove no other process owns it.
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
c9f6aca to
18524fd
Compare
| # the loser loads the winner's key instead of overwriting it. | ||
| # The file descriptor starts with mode 0o600 so there is never | ||
| # a world-readable window — no separate chmod call is needed. | ||
| import os as _os_module |
There was a problem hiding this comment.
SUGGESTION: os is already imported at module top (line 31), so re-importing it as _os_module here inside the function is redundant and confusing — especially since line 163 still uses the top-level os.replace. Use the already-imported os directly (and move import time to the top-level imports). The shadowing alias adds cognitive overhead with no functional benefit.
| import os as _os_module | |
| tmp = keyfile.with_suffix(keyfile.suffix + ".tmp") |
Reply with @kilocode-bot fix it to have Kilo Code address this issue.
|
Re-reviewed the folds against head (18524fd) - both CRITICALs are genuinely resolved, nice work:
One trivial Kilo nit still open before merge: the redundant |
|
Pushed Kilo fix (both findings from inline review on 18524fd): 1. WARNING (line 153 — broken FileExistsError fallback):
2. SUGGESTION (line 132 — redundant os import):
Tests: 13/13 store_signing tests pass. |
|
Security-sensitive - the narrow-except on sign_manifest is already folded at head, thanks. Genuinely open: store_signing.py:116 the 0o600 perms are enforced only on key creation, never re-checked in _load_private_key, so a key written under a loose umask stays world-readable - chmod/verify mode on load; registry.py:208 when no signature is stored verify_manifest_signature returns False which the gate turns into a hard 403, contradicting the PR description ('when no signature, allow') - decide fail-open-unsigned vs fail-closed and align gate + docs; store_install.py:708 the gate verifies the boot-snapshot manifest dicts, not the manifest actually executed later at L795-817 (TOCTOU/provenance gap) - verify the executed manifest; app.py:260 create_app unconditionally calls load_or_create_signing_keypair, coupling boot to write-access + cryptography/jcs availability so a read-only data_dir bricks startup - make it lazy/optional; store_install.py:920 returns 500 for the normal 'key not configured' state (should be a 4xx). Fold and I merge. |
|
Re-reviewed each item from your 18:25 comment against the current HEAD (147d6be). All five are already addressed — here's where: 1. store_signing.py:116 — 0o600 never re-checked on load ✅ Resolved
2. registry.py:208 — unsigned→hard 403 ✅ Resolved
3. store_install.py:708 — TOCTOU (verify boot-snapshot, not disk) ✅ Resolved
4. app.py:260 — bricks read-only startup ✅ Resolved
5. store_install.py:920 — 500 for unconfigured key ✅ Resolved
Targeted tests: 13/13 store_signing, 3/3 pubkey-related, all pass. Full gate running in the background. This PR should be ready to merge once CI is green. |
…2023) * feat(store): add Ed25519 manifest signing and verification for install-v2 Add code signing to the app store install flow (#647): - store_signing.py: Ed25519 keypair generation, persistence, sign/verify utilities that mirror the hub/identity.py pattern - registry.py: AppRegistry now accepts a signing key, signs every manifest at catalog load time, and exposes verify_manifest_signature() for re-verification at install time - store_install.py: signature verification gate in install-v2 before any installer or script runs; tampered manifests are rejected with 403 - GET /api/store/signing-pubkey: public key endpoint for clients and auditing tools - app.py: loads/creates the store signing keypair on boot Design: one Ed25519 keypair per taOS instance, generated on first boot, private key never leaves the node. Signatures are computed at catalog load time; install-v2 re-verifies against the stored signature to detect post-boot catalog tampering. Tests: 13 unique unit tests covering keypair lifecycle, sign/verify, tamper detection, deterministic signatures, _signature field stripping, and file permissions. * fix(store): address Kilo bot review (round 2) - registry.py: wrap sign_manifest() in try/except so a malformed signing key does not crash the entire catalog load; add logging import - app.py: wrap load_or_create_signing_keypair() in try/except OSError so a read-only data_dir does not prevent server startup - store_signing.py: enforce 0600 permissions on existing keyfile load (from round 1) - store_install.py: document threat model limitation; change pubkey endpoint 500→404 when unconfigured - tests: update pubkey endpoint test to expect 404 * fix(store): resolve Ed25519 signing review issues (Kilo CRITICAL + nits) - store_signing.py: atomic keyfile creation with O_CREAT|O_EXCL+0o600, derive public key from loaded private key instead of trusting data['public_pem'], handle FileExistsError race by loading winner - registry.py: verify_manifest_signature now re-reads manifest.yaml from disk at install time, so post-boot catalog tampering is actually detected (previously compared two in-memory copies loaded at boot) - store_install.py: add _verify_manifest_for_install helper that distinguishes 'no stored signature' (graceful skip) from 'bad signature' (hard 403); verify backend manifests in install chain before running their installer - test_store_signing.py: fix flaky sig[:-2]+'ff' to XOR last byte so it always differs from original (~1/256 failure fixed) - test_routes_store_install.py: add end-to-end real-registry tamper test that mutates on-disk YAML and asserts 403 Tests: 13/13 store_signing pass. Install route fixture has pre-existing aiosqlite hang (CI will cover). * fix(store): unique retry tmp path in signing keypair creation + remove redundant os import - FileExistsError fallback now uses a PID-unique tmp path on retry so it never collides with a still-running winner's tmp file. - Added an early keyfile.exists() check after the 3s wait (the competing process may have completed os.replace by then). - Removed local 'import os as _os_module' and 'import time as _time' — os was already a top-level import and time is now imported at module level. - Replaced all _os_module.* and _time.* references with os.* and time.* respectively. Fixes: Kilo WARNING (store_signing.py:153) + SUGGESTION (line 132) * fix(store): address 5 security findings from jaylfc review of #1924 1. store_signing.py: enforce 0o600 BEFORE reading keyfile (umask bypass) 2. registry.py: fail-open for unsigned manifests, document policy, add set_signing_key() 3. store_install.py: TOCTOU guard — re-verify manifest from disk at execution time 4. app.py: defer keypair loading to lifespan (read-only data_dir no longer bricks startup) 5. store_install.py: 500→422 for backend without installer mapping Also fix test_real_registry_detects_post_load_tampering empty installed_path init. * fix(store): resolve Kilo WARNING — remove tmp.unlink race in keygen recovery Skip the shared tmp name entirely after a FileExistsError contention timeout instead of unlinking it. Unlinking the shared tmp could race with a still-running winner process, causing a FileNotFoundError crash for the winner or inconsistent on-disk state. Jump directly to the unique PID-based tmp path, which is safe because every process gets its own name. * fix(security): address Kilo SUGGESTIONS — fail-closed verify_manifest_signature + second signature re-verify TOCTOU guard - registry.py: Change verify_manifest_signature from fail-open to fail-closed (returns False for unsigned manifests). The install gate (_verify_manifest_for_install) already short-circuits for unsigned manifests via get_signature() check, so the install path is unaffected. This prevents future callers from accidentally allowing unsigned manifests through. - store_install.py: Replace the TOCTOU field-comparison guard with a second signature re-verify against the re-read disk bytes. This is more robust: catches any change (not just the whitelisted fields), does not false-positive on legitimate catalog reloads, and aligns with the existing Ed25519 trust model. Fixes the 2 remaining Kilo SUGGESTIONS on #2023. * fix(store): assert 422 instead of 500 for unknown backend install Rename test_unknown_backend_returns_500(_not_exception) to _returns_422 in both test_store_install_v2 and test_routes_store_install, matching the new 422 response code jaylfc requested for unknown backends. * fix(security): address CodeRabbit findings — aliasing, permissions, race, snapshot, signing failures, 422 test 8 CodeRabbit findings fixed: store_signing.py: - Docstring: document detached in-memory signatures (not YAML) + post-load model - _enforce_permissions: fail-closed — raise PermissionError instead of swallowing OSError - Keypair creation race: use os.link for non-overwriting claim instead of os.replace app.py: - Import aliasing: alias store_signing.load_or_create_signing_keypair to prevent shadowing by agent_registry_store version - Move signing key init from create_app() into lifespan (before _startup_complete) registry.py: - _CatalogState: bundle catalog/signatures/manifest_dicts into one atomic snapshot, preventing TOCTOU across the 3 independent assignments - signing_failures: track manifests that failed sign_manifest so install gate can block them (not treat as merely unsigned) store_install.py: - _verify_manifest_for_install: check is_signing_failure() before allowing unsigned manifests through tests: - test_unknown_backend_returns_422: expect 422 (matches production code) * fix(store): add TOCTOU re-verify guard for backend manifest and guard keyfile.stat() OSErrors - Backend manifest TOCTOU: re-read backend manifest.yaml from disk and re-verify signature immediately before backend_installer.install(), matching the primary manifest guard at lines 783-829. - store_signing: wrap keyfile.stat() calls in _enforce_permissions with try/except so non-PermissionError OSErrors are raised as PermissionError instead of raw OSError. * fix(store): remove redundant secondary TOCTOU re-verify for backend manifest The primary gate at line 924 (_verify_manifest_for_install) already re-reads the manifest from disk and verifies its signature. No await/yield exists between the primary gate and the install call, so the coroutine cannot be preempted in that synchronous stretch. The secondary re-read + re-verify (~40 lines) adds no real attack-window reduction. Reported-by: Kilo Code bot * fix(security): address CodeRabbit findings round 3 — corrupt keyfile recursion, exception chaining, mkstemp fd leak, TOCTOU test coverage - store_signing.py: unlink corrupt keyfile before regeneration to prevent infinite recursion when os.link fails with FileExistsError on the stale corrupt file - store_signing.py: chain PermissionError with 'from exc' in _enforce_permissions for better traceback preservation - test_routes_store_install.py: close mkstemp file descriptor to prevent fd leak - test_routes_store_install.py: monkeypatch initial signing gate in tampering test so the install-time TOCTOU re-verification is independently exercised All 32 targeted tests pass (store_signing + registry + store_install signing tests) * fix(test): use tmp_path fixture instead of tempfile for auto-cleanup Replace tempfile.mkdtemp/mkstemp with pytest's tmp_path fixture in test_real_registry_detects_post_load_tampering, which gives automatic teardown and removes manual fd/os/tempfile bookkeeping. Addresses CodeRabbit nitpick (run f55a8949) in PR #2023. --------- Co-authored-by: Hogne <227774406+hognek@users.noreply.github.com>
…1924 1. store_signing.py: enforce 0o600 BEFORE reading keyfile (umask bypass) 2. registry.py: fail-open for unsigned manifests, document policy, add set_signing_key() 3. store_install.py: TOCTOU guard — re-verify manifest from disk at execution time 4. app.py: defer keypair loading to lifespan (read-only data_dir no longer bricks startup) 5. store_install.py: 500→422 for backend without installer mapping Also fix test_real_registry_detects_post_load_tampering empty installed_path init.
147d6be to
91af53e
Compare
|
Closed without a stated replacement. If #2050 (fail-closed sign_manifest) is the intended successor carrying the remaining perms item (0o600 on rotation, not just creation), say so there and confirm the item is in its scope; otherwise reopen. Tracking so the open finding does not fall through the gap. |
…, closure conventions (#2055) Codifies the working agreements the review gate already enforces: findings outrank new slices, CONFLICTING PRs get rebased promptly, a green computed before the base moved is stale, closures always link their successor, and one PR per slice. Grounded in this week's real cases (#2009/#1932 semantic conflict, the #1927/#1924 closure forensics, the store-signing fragmentation).
|
Landed via #2003. The store-signing 500→422 fix and its test update were consolidated into a single PR per the one-PR-per-slice rule. |
Summary
Add code signing to the app store install flow. Fixes #647.
What
store_signing.py: Ed25519 keypair generation, persistence, sign/verify — mirrors thehub/identity.pypatternregistry.py:AppRegistrynow accepts an optional signing key; signs every manifest at catalog load time; exposesverify_manifest_signature()andget_signature()store_install.py: Signature verification gate ininstall-v2— tampered manifests rejected with 403 before any installer or script runsGET /api/store/signing-pubkey: Public key endpoint for clients and auditing toolsapp.py: Loads/creates the store signing keypair on boot; attaches toapp.stateDesign decisions
Tests
store_signing.py: keypair generation, sign/verify, tamper detection, deterministic signatures,_signaturefield stripping, persistence, file permissionstest_routes_store_install.py: tampered manifest → 403, valid signature allows install, no-key skips gateGET /api/store/signing-pubkeyTask
Kanban: t_a6a7c0c2
Summary by CodeRabbit
New Features
Bug Fixes
Tests