Skip to content

fix(security): address 8 CodeRabbit findings — aliasing, permissions, race, snapshot, signing-failure gate, 422 test#2026

Closed
hognek wants to merge 10 commits into
jaylfc:devfrom
hognek:fix/cr-bot-2023
Closed

fix(security): address 8 CodeRabbit findings — aliasing, permissions, race, snapshot, signing-failure gate, 422 test#2026
hognek wants to merge 10 commits into
jaylfc:devfrom
hognek:fix/cr-bot-2023

Conversation

@hognek

@hognek hognek commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

fix(security): address 8 CodeRabbit findings on PR #2023 store security

What changed

store_signing.py (3 fixes)

  • Docstring correction: Document detached in-memory signatures (stored in AppRegistry._signatures, not YAML). Clarify the post-load tamper-detection model.
  • _enforce_permissions fail-closed: Raise PermissionError instead of swallowing OSError, so a keyfile with unenforceable permissions is not read.
  • Race fix in keypair creation: Use os.link() for non-overwriting atomic claim instead of os.replace(). A late contender that loses the race loads the winner's keypair.

app.py (2 fixes)

  • Import aliasing: Alias \ as \ to prevent shadowing by the agent_registry_store import of the same name. The catalog-signing path now correctly uses the store-signing key (store_signing_key.json).
  • Move signing key init into lifespan: Previously ran synchronously during create_app() (triggering registry.reload()). Now runs in lifespan() before , so tests bypassing the lifespan get store_signing_pubkey=None.

registry.py (2 fixes)

  • _CatalogState snapshot: Bundle catalog, signatures, manifest_dicts, and signing_failures into one immutable dataclass snapshot that is atomically replaced. Prevents TOCTOU where a reader could see a new manifest from _catalog paired with an old _signatures dict.
  • Signing failure tracking: Track manifests that failed sign_manifest() in . Expose via \ so the install gate can block them (rather than treating them as merely unsigned, which would bypass the gate).

store_install.py (1 fix)

  • Check is_signing_failure() in : When get_signature() returns None, additionally check if the manifest failed to sign during load. If so, return 403 — a signing failure is not the same as "never signed" and must not bypass the gate.

tests (1 fix)

  • test_unknown_backend_returns_422: Updated from expecting 500 to 422, matching the production code at store_install.py:975.

Test results

  • store_signing: 13/13 pass
  • store_install (key tests): 5/5 pass — unknown_backend_422, tampered_403, valid_signature, real_registry_tampering, no_signing_key_skips

Fixes: CodeRabbit 7 inline + 1 outside-diff findings on #2023
Related: #1924

hognek added 8 commits July 17, 2026 21:25
…l-v2

Add code signing to the app store install flow (jaylfc#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.
- 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
- 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).
…e 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)
…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.
…ecovery

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.
…_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 jaylfc#2023.
…ace, 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)
@hognek
hognek marked this pull request as ready for review July 18, 2026 20:15
@qodo-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

@coderabbitai

coderabbitai Bot commented Jul 18, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@hognek, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 5 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 04bf3e86-6549-4d8a-8658-6760b8025d0f

📥 Commits

Reviewing files that changed from the base of the PR and between 0d76733 and ad81361.

📒 Files selected for processing (6)
  • tests/test_routes_store_install.py
  • tests/test_store_signing.py
  • tinyagentos/app.py
  • tinyagentos/registry.py
  • tinyagentos/routes/store_install.py
  • tinyagentos/store_signing.py
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@gitar-bot

gitar-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown

Gitar is working

Gitar

# Verify the backend manifest's signature before running its installer.
# A backend manifest that was tampered with after catalog load would
# run an untrusted script/image — reject it here.
be_verified, be_verify_err = _verify_manifest_for_install(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

WARNING: Backend manifest has no TOCTOU re-verify guard before its installer runs.

The primary manifest gets a disk re-read + signature re-verify at lines 790-829 right before install, closing the window between the gate check (line 764) and execution. The backend manifest here is verified once at the gate (this _verify_manifest_for_install call, which does re-read disk via registry.verify_manifest_signature), but then backend_installer.install(...) runs an untrusted script/image at lines 940-944 with no equivalent re-verify immediately before execution. An attacker who swaps the backend manifest.yaml on disk between this gate check and the actual install (which can be a non-trivial async operation — e.g. model download/resolution) would execute the tampered backend installer, defeating the very protection this PR adds for the primary manifest.

Apply the same disk re-verify guard (re-read the backend manifest from disk and re-check _verify_manifest_for_install / signature) immediately before backend_installer.install(...).


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

Comment thread tinyagentos/store_signing.py Outdated
as 0600, allowing the caller to disable signing rather than read a
potentially group/world-readable private key.
"""
if (keyfile.stat().st_mode & 0o777) != 0o600:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: keyfile.stat() is now unguarded, so any OSError from stat() (e.g. transient EIO, a filesystem that doesn't report permission bits, or a symlink edge case) propagates as a raw OSError, while the failure-mode you intend to surface is PermissionError.

This is fail-closed by design and the lifespan (app.py:1362) does catch (OSError, PermissionError), so signing is only harmlessly disabled — but the raised exception type is misleading and a non-permission OSError is indistinguishable from a true permission violation when logged. Consider wrapping the stat() calls so that non-PermissionError OSErrors are either re-raised as PermissionError deliberately or explicitly allowed to bubble with a clear message.

Suggested change
if (keyfile.stat().st_mode & 0o777) != 0o600:
try:
mode = keyfile.stat().st_mode & 0o777
except OSError as exc:
raise PermissionError(f"cannot stat keyfile {keyfile}: {exc}")
if mode != 0o600:
os.chmod(keyfile, 0o600)
if (keyfile.stat().st_mode & 0o777) != 0o600:
raise PermissionError(f"cannot enforce 0600 permissions on {keyfile}")

Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found (incremental) | Recommendation: Merge

Overview

Incremental review of commit ad81361c vs 73462615. The only change is the removal of the redundant secondary backend-manifest TOCTOU re-verify block in tinyagentos/routes/store_install.py (42 lines deleted). No new code was added; no new issues introduced.

Resolved Findings (verified against current HEAD)

File Line Previous Status
tinyagentos/routes/store_install.py 964 SUGGESTION: secondary TOCTOU re-verify redundant RESOLVED — block deleted in this commit
tinyagentos/routes/store_install.py 924 WARNING: backend manifest has no TOCTOU re-verify RESOLVED — primary gate at line 924 (_verify_manifest_for_install) re-reads the manifest from disk and verifies via registry.verify_manifest_signature; the removed block only duplicated it
tinyagentos/store_signing.py 96 SUGGESTION: keyfile.stat() unguarded RESOLVED — both stat() calls now wrapped in try/except OSError raising PermissionError (lines 96-105)
Files Reviewed (1 file changed in increment)
  • tinyagentos/routes/store_install.py - 42 deletions, 0 additions — removal of redundant secondary TOCTOU guard; no new issues
Previous Review Summaries (2 snapshots, latest commit 7346261)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 7346261)

Status: 1 Issue Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 1
Issue Details (click to expand)

SUGGESTION

File Line Issue
tinyagentos/routes/store_install.py 964 Secondary TOCTOU re-verify duplicates the primary gate's own disk re-read and sits in a synchronous block with no await before install, so it does not close a real race window.
Files Reviewed (2 files changed since last review)
  • tinyagentos/routes/store_install.py - 1 issue (new SUGGESTION) + 1 resolved WARNING
  • tinyagentos/store_signing.py - 1 resolved SUGGESTION
Previously reported findings — status
  • tinyagentos/routes/store_install.py:924 WARNING (backend manifest TOCTOU guard missing): RESOLVED — the new guard at lines 940-981 adds the disk re-read + re-verify.
  • tinyagentos/store_signing.py:96 SUGGESTION (keyfile.stat() unguarded): RESOLVED — both stat() calls are now wrapped in try/except OSError raising PermissionError.

Previous review (commit dc3cbc2)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 1
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
tinyagentos/routes/store_install.py 924 Backend manifest lacks the TOCTOU disk re-verify guard that the primary manifest received; the backend installer runs an untrusted script/image (lines 940-944) with no re-verification immediately before execution.

SUGGESTION

File Line Issue
tinyagentos/store_signing.py 96 keyfile.stat() is now unguarded; non-permission OSErrors propagate as raw OSError rather than the intended PermissionError, making the failure mode misleading (harmlessly caught by the lifespan, which disables signing).
Files Reviewed (6 files)
  • tinyagentos/store_signing.py - 1 issue
  • tinyagentos/registry.py - 0 issues
  • tinyagentos/app.py - 0 issues (verified signing-key init moved into lifespan; aliasing correct)
  • tinyagentos/routes/store_install.py - 1 issue
  • tests/test_routes_store_install.py - 0 issues
  • tests/test_store_signing.py - 0 issues (not present in diff/working tree)

Fix these issues in Kilo Cloud


Reviewed by hy3:free · Input: 38.6K · Output: 2.3K · Cached: 93.6K

… 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.
Comment thread tinyagentos/routes/store_install.py Outdated
be_stored_sig = registry.get_signature(result.backend_id)
if be_stored_sig is not None:
from tinyagentos.store_signing import verify_manifest_signature as _verify_sig
if not _verify_sig(on_disk_be, be_stored_sig, _store_pub):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION]: Secondary TOCTOU re-verify is redundant with the primary gate and does not close a real race.

The initial gate at line 924 (_verify_manifest_for_install) already re-reads the backend manifest from disk and verifies it via registry.verify_manifest_signature (see its docstring: "Re-reads the manifest from disk at verify time"). So this second disk re-read + re-verify duplicates the first check's coverage.

More importantly, the window this guard claims to defend ("modified between signature verification and install") is the span from line 924 to line 983, and there is no await/yield anywhere in between (the next await is backend_installer.install(...) at line 983). The coroutine cannot be preempted in that synchronous stretch, so an attacker cannot swap manifest.yaml on disk mid-block. The real TOCTOU surface is between catalog-load and the gate, which the primary gate already re-reads from disk for. This block therefore adds ~40 lines of logic (and two more disk reads) without reducing an actual attack window.

If you want a meaningful post-gate guard, the check should be moved to sit immediately before the await backend_installer.install(...) after any other awaits, or the primary gate's already-on-disk verification is sufficient and this block can be removed. At minimum, consider collapsing this into a single verify call rather than reading get_signature separately and re-reading the file a second time.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

…anifest

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
@hognek

hognek commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

Folded into #2023

@hognek

hognek commented Jul 19, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #2023. The 500→422 test assertion fix was consolidated into #2023 per jaylfc's fold instruction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant