A module signed on any machine with ordinary LF line endings in
handlers/handler.py (the normal case for a non-Windows publisher, or any
publisher whose repo doesn't specifically special-case this one file)
fails Ed25519 signature verification once it's installed via either
standard install path on Windows -- the CLI's railcall market install
and Studio's own "Install" button both hit this.
Every module-install write site that touches handlers/handler.py opens it
in TEXT mode with no newline="":
- railcall-cli/railcall_cli.py _install_write_module(), v1
(single-file) path
- the same function's v2 (tree) path, which force-overwrites
handlers/handler.py from the wire payload string as a "belt-and-
braces" step even after a correct, binary-safe tarball extraction
- the station's own Studio marketplace-install route,
routes/marketplace.py marketplace_install_listing()
On Windows, Python's default text-mode write (newline=None) translates
every '\n' the caller writes to the platform line separator ('\r\n'). The
wire payload string for handler.py naturally contains '\n' (it's Python
source code), so every one of these writes corrupts LF to CRLF on disk.
Unlike module.json -- which gets re-parsed with json.loads() and
re-serialized into a canonical, whitespace-insensitive form before being
signed or verified, so its own line-ending style never affects the signed
bytes -- handlers/handler.py's bytes are hashed/signed RAW, with no
canonicalization step. The signature genuinely does depend on its exact
on-disk bytes, so the CRLF corruption is fatal here.
Reproduction steps:
- Extract a clean station-v0.73 tarball, sys.path.insert(0, "workbench").
- Build a manifest + an ordinary LF handler.py
("def ping(args):\n return {'ok': True}\n"), sign it with a fresh
Ed25519 keypair using the same canonicalization
routes/modules.py._module_canonical() implements (sorted-key, no-
whitespace JSON dump of the manifest, concatenated with the RAW
handler.py bytes).
- Confirm routes.modules._verify_module_signature(manifest,
handler_bytes, sig, module_dir=None) returns True against the
original bytes.
- Rewrite handlers/handler.py the same way the install-time write sites
do: open(path, "w", encoding="utf-8", newline="\r\n") and write the
same source string -- this reproduces the exact translation Python's
text-mode write performs natively on Windows, deterministically on any
host OS.
- Read the file back in BINARY mode (exactly like the real read path)
and call _verify_module_signature() again with those bytes.
Expected: a module's signature verifies identically regardless of which
platform installed it -- installing a module must never change whether it
verifies.
Actual: verification returns True before the simulated install and False
after -- the exact same signature, now rejected, because the only thing
that changed is line-ending bytes in a file that was never supposed to be
rewritten by an install at all.
Root cause: the "always overwrite the three canonical files from payload
strings" step (present in both the CLI and the station) treats
handlers/handler.py the same way it treats module.json -- a plain text
write of a wire string -- without accounting for the fact that, unlike
module.json, handler.py's bytes are NOT canonicalized before signing, so
a platform-specific newline translation directly changes the signed
payload.
Suggested fix: open handlers/handler.py (and any other non-JSON file ever
written from a wire payload string during install) with
newline="" and encode to bytes explicitly before writing, or write it as
raw bytes (open(path, "wb"); f.write(payload_bundle["handler_py"].encode
("utf-8"))) the same way the v2 tarball-extraction path already correctly
does for every OTHER file in the tree. module.json can keep its current
text-mode write since canonicalization already makes it immune to this
class of corruption.