← Community
bugfixed

Module signature is verified against the manifest embedded key + default trust_mode=any accepts any key — self-signed modules pass

marcofgvmarcofgv#215d ago · 134 views
affected: station-v0.78fixed in: station-v0.90

Affected: station-v0.78 · Class: improper signature verification / authentication bypass (CWE-347) · Severity: HIGH · Signed receipt: attached (railcall verify → SIGNATURE VALID, offline)

The two facts that compose to zero publisher authentication (default install)

  1. The signature is verified against the key embedded in the module's own manifest. _verify_module_signature (modules_routes.py:145-176) reads pubkey_hex = manifest.get("publisher_pubkey") (line 160) and verifies module.sig against THAT key. There is no comparison anywhere in the load path against a pinned or marketplace-registered publisher key. An attacker generates their own Ed25519 keypair, embeds their pubkey in publisher_pubkey, and signs — the verify passes. It proves "whoever built this held a private key", never "a known publisher signed this." Self-referential.
  2. The trust gate is off by default. After verify, _load_modules calls publisher_trust.is_trusted(WS, manifest.publisher_pubkey) (modules_routes.py:854). The free-install default is trust_mode=any with an empty allowlist (publisher_trust.py:15, 26; default returned at :68/:73/:80). In that mode is_trusted returns {"trusted": True, "reason": "any valid signature accepted"} for any key (publisher_trust.py:151-156).

Proof (container, real station function)

is_trusted("ab"*32)  # a 64-hex key no registered publisher owns, invented on the spot
=> {'trusted': True, 'mode': 'any', 'publisher_name': None,
    'reason': 'trust_mode=any (any valid signature accepted)'}

So an arbitrary attacker key is "trusted" by default, and the signature that would "authenticate" it is one the attacker made over their own key. A module bearing a self-consistent, attacker-generated signature passes BOTH gates and loads — and loaded modules run at full privilege (module_sandbox.py:3-6; the opt-in capability block is off by default — this is the already-reported F7 that supplies the impact).

Distinct from the already-reported "modules run full-privilege by default" (F7)

F7 is about privilege (CWE-250: what a module can do once loaded) and named the publisher-trust allowlist as "the primary defense." This is a distinct authentication failure (CWE-347): the signature+trust chain that is supposed to establish which publisher a module is from is, at the default config, satisfied by any self-signed bundle — because the signature checks the manifest's own key and the default trust mode accepts every key. The material change: the mechanism advertised to authenticate module publishers does not.

Honest scope (not drive-by RCE)

  • Exploitation requires the operator to install or place a module and trigger a load/reload — the realistic vector is a sideloaded module (a dir/tarball shared via a repo, a colleague, a contest entry, a "try my module" link), which is verified+trusted locally with no marketplace review.
  • The official marketplace adds account + publisher-key registration and human review before listing, so this is not remote drive-by RCE through that channel; the finding is that the cryptographic signature+trust gate behind a local install is a no-op, so security on the local path rests entirely on the operator not sideloading.

Fix

  • Verify module.sig against a pinned / marketplace-registered publisher key (the key registered via railcall market publisher register), not the manifest-embedded key; reject when the embedded key isn't the registered one.
  • Default trust_mode to allowlist (first-party publishers pre-trusted) so an unknown publisher key requires an explicit operator railcall trust add — fail-closed, not fail-open.

Found and reported by @marcofgv. Reviewed adversarially (codex novelty+overclaim triage) before posting.

Signed receipt (railcall verify → SIGNATURE VALID, offline)

{
  "schema": "railcall_audit_receipt.v1",
  "ran_at": "2026-08-11T21:06:01",
  "file": {
    "name": "findings_trust.csv",
    "sha256": "sha256:8ed99d517420ed3b5e1563199a1719ac1a2eee505f809e8c46c78c20c3ada02b",
    "bytes": 235
  },
  "audit": {
    "rows": 1,
    "columns": 4,
    "import_breakers": 0,
    "pii_columns": 0,
    "formula_injection_cells": 0,
    "findings": []
  },
  "network_audit": {
    "lsof_available": false,
    "error": "lsof_not_found",
    "external_sockets_open": null
  },
  "result": "audited",
  "receipt_version": "v2",
  "flow": {
    "dry_run": true,
    "name": "audit",
    "action_type": "audit"
  },
  "governance": {
    "policy_ref": "none",
    "policy_hash": "ff56072e81ed4908ea91f567741238b387e536cd1f5974513ee18df0d5c575b9",
    "approval_chain": [],
    "risk_classification": "unknown",
    "irreversible": false
  },
  "execution": {
    "input_sha256": "sha256:8ed99d517420ed3b5e1563199a1719ac1a2eee505f809e8c46c78c20c3ada02b",
    "output_sha256": "",
    "duration_ms": 0,
    "exit_code": 0
  },
  "signer_alg": "ed25519",
  "public_key_hex": "ea2446fec9cc4de478c853fb35c778262d4327ac7d32d6ccff36bdbbfcd775e2",
  "signature_hex": "07dbbfaaabffef87847d70aca2ebde5271e6a3fd6c730eee037d75f2688d6d4572148c73b1a4546a74c2d7e4b5c8080cd0c55885353a0cfc8d658a58402c0901"
}```
5 pts

2 replies

Confirmed and credited at the top of the scale, @vectortrendstech — verifying a module's signature against the pubkey embedded in its own manifest is a self-attesting trust root, and it's dangerous at defaults, which is the worst place to be dangerous. The fix direction is publisher-key pinning against the marketplace identity rather than the bundle's self-declared key; it needs careful design (key rotation, first-install trust) so it's scheduled as its own work item rather than rushed into a batch. Credit reflects the find now; the fix lands with its own release note.

Fixed in station-v0.90, @vectortrendstech — and your framing shaped the design. Both composing defects are closed:

1. The signature is no longer self-attesting. Two identity anchors now live OUTSIDE the bundle:

  • Per-module key pins (TOFU + update-must-match): the key seen for a module id on first load is pinned; every later load must present the same key or it hard-fails regardless of trust_mode — an update or same-name substitute signed by a different key is rejected with both fingerprints. A rejected bundle can never rotate the pin by being seen; rotation is an explicit operator ceremony.
  • Marketplace publisher attestation: the marketplace signs {publisher_pubkey, publisher_name, slug} with the same key every station already pins for license trust tokens. The station verifies the chain offline — pinned marketplace key → attestation → publisher key → module signature — and never trusts the verifier key a document claims for itself.

2. The default no longer accepts any key. Fresh installs now default to trust_mode=attested: a module loads only when its key is marketplace-attested or operator-allowlisted; a raw self-signed sideload requires the explicit railcall trust add ceremony. Your exact proof line — is_trusted("ab"*32) → trusted — now returns False at the default, verified in the regression suite. Existing installs keep their mode and see a one-release boot warning before any flip (no rug-pulls), and any remains available as a deliberate operator downgrade.

Your severity call was right: this was the mechanism advertised to authenticate publishers doing nothing at defaults. Design doc: docs/spec-publisher-key-pinning.md (Phase 3 — dual-signed rotation records + transparency-log entries — is specced and queued). Points were awarded at confirmation. Your two new redaction reports are in the next triage round.

Sign in to reply.