Application Security
This page is the design document and hardening roadmap for Vishwakarma security. Like
undo-redo.md and login.md, prose sections are locked decisions; items marked OPEN
are still under debate. Nothing here is an implementation task list yet — it is the ordering,
the reasoning, and the acceptance criteria that a future implementation (human or AI) must meet.
The roadmap below is derived from a full-codebase review (audit date 2026-07-21) covering
the four subsystems: the C++ desktop application (code-core), the build / release tooling
(code-miscellaneous), the Django telemetry server (server), and the Hugo website
(website). Findings are ordered by risk × exposure, not by ease of fixing.
1. Threat Model
Security work is only meaningful against a stated adversary. Vishwakarma is a source-available desktop CAD/CAM application that runs with the logged-in user’s full privileges, parses files authored by third parties, and auto-updates itself over the internet. That shape dictates the threat model.
1.1 Assets worth protecting
- The update trust chain. If an attacker can make
Vishwakarma.exeaccept a malicious setup executable, they achieve code execution on every installation. This is the highest value target in the whole system. - The user’s machine. The application and its extension worker parse untrusted files
(
.std,.dxf,.mvdb, embedded images/fonts) with the user’s own token. A parser bug is a path to arbitrary code with full access to the user’s documents. - The telemetry / account identity. Each installation is an Ed25519 key pair
(
AccountManager.cpp, and the future account system inlogin.md). Theft or spoofing of that identity pollutes statistics and, once accounts ship, impersonates users. - Server availability. The telemetry/stats server (
mv-server.ramshanker.in) runs on a single Raspberry Pi behind Cloudflare. Availability, not confidentiality, is its scarce resource. - Website integrity.
mv.ramshanker.inis where users are told to download the software. Defacement or a poisoned download link is a distribution-channel attack.
1.2 Adversaries, most-likely first
- The malicious file author. Engineers routinely exchange CAD files. A booby-trapped
.dxfor.stdopened by a victim is the most realistic attack, and it needs no network access to the attacker. This is the primary threat. - The repository scraper. The repo is public. Anything secret that is committed — now or in history — is already compromised.
- The network attacker (MITM). Can tamper with update downloads and telemetry in transit. Largely defeated today (see §2) but must stay defeated.
- The telemetry flooder. Wants to exhaust the Pi’s CPU or fill its disk.
- The future web attacker. Targets the planned user-edits pipeline (
useredits.md) and login subsystem (login.md) once they exist.
1.3 Trust boundaries
UNTRUSTED INPUT TRUST BOUNDARY TRUSTED CORE
------------------ --------------- ------------
.dxf / .std file ───────► Extension worker (CPython) ──────► validated batch ──► host
.mvdb file ───────► DataStorage protobuf decode ──────► in-memory model
signed manifest ───────► Ed25519 verify + SHA-256 ──────► staged installer ──► exec
telemetry POST ───────► Ed25519 chain + rate limit ──────► SQLite row
embedded PNG/font ───────► libpng / freetype (C) ──────► GPU texture / atlas
Every arrow crossing into the trusted core is a place where validation must be complete. The roadmap is essentially a list of arrows that are not yet strong enough, ordered by blast radius.
2. Current Posture — What Is Already Right
A hardening roadmap that ignores existing defenses invites “fixing” things that are fine and regressing them. The review found the codebase already does the following well, and these must not be weakened:
- Signed, hash-verified updates.
SoftwareUpdate.cppdownloads a manifest, verifies its Ed25519 signature against a source-pinned public key (VerifyManifestSignature) before parsing it, then verifies the downloaded setup’s SHA-256 against the signed manifest, plus a rollback guard (manifestSequence), an expiry (validUntil), anappIdcheck, and a blocked-versions list. The naive JSON scanner only ever runs on already-authenticated bytes. - TLS is never downgraded. Both
SoftwareUpdate.cppandImprovementData.cppuse WinHTTP withWINHTTP_FLAG_SECURE; noSECURITY_FLAG_IGNORE_*or cert-bypass flags exist anywhere in the tree. A network attacker cannot inject an update even without pinning, because the payload is independently Ed25519-signed. - A network-stripped extension worker.
VishwakarmaExtension.exeis a frozen, statically linked CPython withsocket/sslnever compiled in,subprocess/multiprocessingnot frozen, and thentmodule scrubbed at import — solid defense-in-depth ahead of the sandbox (documented inextensions.md). - Host-side validation of all worker output.
ExtensionCommunications.cppenforces count caps (kMaxImportedNodes, etc.), finite-float checks, per-message size limits, and length bounds on every batch the worker returns. - A genuinely hardened server.
improvement_server/settings.pyrefuses to boot in production withoutMV_SECRET_KEY, pinsALLOWED_HOSTS, sets HSTS / secure cookies / nosniff /X-Frame-Options: DENY, ships a strict CSP viaapi/middleware.py, locks CORS to a single origin and a single URL regex, caps upload size, verifies the full Ed25519 signature chain on ingestion (api/crypto.py), rate-limits per installation key, and exposes no admin / auth / session surface. Thesystemdunit addsNoNewPrivileges,ProtectSystem=strict, a read-only filesystem,PrivateTmp, and kernel-protection knobs. - Signing keys are at least encrypted. The private keys committed to the repo are
ENCRYPTED PRIVATE KEYPKCS#8 PEMs, not plaintext (this is still a finding — see P0). SDLCheckis on for all four.vcxprojprojects.
The server subsystem in particular is the strongest part of the codebase and needs only minor edge-DoS hardening (P4).
3. Prioritized Hardening Roadmap
Each item states the finding, the risk, the evidence, a design direction (what to build — no code here), and the acceptance criteria that prove it is done.
Priority summary
| # | Priority | Item | Subsystem | Blast radius |
|---|---|---|---|---|
| P0 | Critical | Signing-key material lives in the public repo | code-miscellaneous |
All installations |
| P1 | High | Extension worker has no OS-level sandbox | code-core |
User’s machine |
| P2a | High | File parsers are unfuzzed / untested for memory safety | code-core |
User’s machine |
| P2b | Medium | Binary exploit-mitigation flags not fully enabled | code-core build |
User’s machine |
| P3a | Medium | Installation private key stored in plaintext at rest | code-core |
Telemetry identity |
| P3b | Medium | No Authenticode gate before executing staged installer | code-core |
Defense-in-depth |
| P3c | Medium | No dependency-CVE / SBOM process | all | Supply chain |
| P4a | Low | Server DoS relies on per-worker in-memory rate limit | server |
Availability |
| P4b | Low | No SECURITY.md / disclosure policy |
repo | Process |
| P4c | Low | Static-site security headers undocumented | website |
Distribution |
| P4d | Future | User-edits & login pipelines need their own threat model | server/website |
TBD |
P0 — Signing-key material is committed to a public repository
Finding. git ls-files lists MV-CodeSigner-01.key, ManifestSigner-01.key, and
RootCA-MV-01.key (plus the .crt public certs) under code-miscellaneous/. The .pfx is
.gitignored and arrives on CI as the RELEASE_SIGN_PFX_B64 secret, but the encrypted
private .key PEMs are tracked in the repository and its history.
Risk. These three keys are the update trust chain from §1.1(1):
ManifestSigner-01.keysigns the release manifest thatVerifyManifestSignaturetrusts. Whoever holds it can sign a manifest pointing at a malicious setup and every installation will accept it — remote code execution on the entire user base.MV-CodeSigner-01.keyis the Authenticode identity users see as the publisher.RootCA-MV-01.keyis the root that signs the code-signer; if that root is ever added to any trust store, its compromise forges arbitrary trusted certificates.
The only thing standing between a repo scraper and this capability is the passphrase on the
encrypted PEMs. The same passphrase (RELEASE_SIGN_PASSWORD) protects all of them and is reused
as the manifest-key password (VW_MANIFEST_PW in GenerateRelease.ps1). Encryption buys time,
not safety: the ciphertext is permanently public, so the keys are only as strong as one shared
password against an offline attack with unlimited attempts.
This also corrects a stale internal assumption that “the signing key is a CI secret, not in git.” The
.pfxis; the encrypted.keyPEMs are not.
Design direction.
- Treat all three keys as compromised and plan a rotation: generate new Root CA, code-signer, and manifest-signer key pairs offline; ship the new manifest public key in a source update; keep an overlap window where the client accepts either the old or the new manifest key so in-flight installs can migrate.
- Purge the
.keyfiles from the working tree and from git history (history rewrite; coordinate because it changes commit hashes). Publish only the.crt/.pubfiles. - Store the new private keys only offline (hardware token / HSM / an air-gapped machine) and as CI secrets, never as repo files. Encrypted-in-git is not an acceptable resting place for the root of the update chain.
- Give the manifest-signing key its own passphrase, distinct from the PFX password.
- Document the rotation runbook in
release.md.
Acceptance criteria. git log --all -- '**/*.key' shows the keys were removed and history
rewritten; a fresh clone contains no private key; a release built from CI secrets still verifies
end-to-end on a clean install; the client accepts manifests signed by the new key and rejects
the old one after the overlap window.
P1 — The extension worker runs without an OS sandbox
Finding. VishwakarmaExtension.exe parses untrusted .std/.dxf files but runs as a
plain child process with the user’s full token. ExtensionCommunications.cpp launches it with
CreateProcessW and CREATE_NO_WINDOW only — no AppContainer, no Job Object, no child-process
ban. The code and extensions.md both explicitly flag this as the acknowledged next step.
Risk. This is the arrow from the primary threat (§1.2). Today’s defenses are all
language-level (no sockets compiled in, nt scrubbed). A memory-corruption bug in the
CPython interpreter, in a C extension, or in the pure-Python parsers — or simply a scrubbing
gap — lets attacker-controlled file bytes run code that can read and write every file the user
can, spawn processes, and reach the network via any means the language ban missed. A single
malicious .dxf mailed to an engineer is the whole attack.
Design direction (defense-in-depth, each layer independently valuable):
- AppContainer / low-integrity token. Run the worker in an AppContainer with no capabilities, so even arbitrary native code is denied the filesystem, network, and clipboard by the OS regardless of what the language allows.
- Job Object. Assign the worker to a Job with
JOB_OBJECT_LIMIT_ACTIVE_PROCESS = 1andKILL_ON_JOB_CLOSE, plus memory and CPU-time ceilings, so it cannot spawn children or outlive the host and cannot be used as a fork bomb. - Explicit child-process ban via
PROC_THREAD_ATTRIBUTE_CHILD_PROCESS_POLICY. - File delivery, not path access. The host already streams file bytes over the pipe rather than handing the worker a path; keep that invariant so the AppContainer needs zero filesystem grants.
- Keep all existing host-side output validation — the sandbox complements it, never replaces it.
Acceptance criteria. The worker process token shows an AppContainer SID; a test extension
that attempts to open a file outside its grant, open a socket, or spawn a child fails at the OS
level; killing the host kills the worker via the Job; imports still succeed for the real
.std/.dxf corpus.
P2a — File parsers are untested for memory safety
Finding. Vishwakarma parses several untrusted binary/text formats in memory-unsafe contexts, with no fuzzing or sanitizer coverage:
- Native
.mvdbfiles via protobuf decode inDataStorage.cpp(ParseMessage, followed byreserve()on message-supplied counts andmemcpyinto fixed C-string fields inCopyStringToFixedCString). .dxfand.stdin the worker (memory-safe Python, but the host then trusts converted batches — already validated, keep it).- Embedded PNG via libpng, JPEG via libjpeg, fonts via freetype — mature C libraries with a long CVE history. These inputs are trusted resources today (icons, logo, bundled font), but the moment a user-supplied image or font can reach them, they become live attack surface.
Risk. A crafted file that trips a bounds bug in any native parser yields the same outcome as P1 — code execution or, at minimum, a denial-of-service crash. CAD files are shared constantly, so this is high-exposure.
Design direction.
- Stand up the sanitizer / Valgrind testing server described in §4 as the verification engine for this item.
- Build a small, portable, headless harness that feeds a corpus and fuzzer-generated inputs
through the platform-independent parsers (protobuf decode + geometry validation, the memory
manager, ID/encoding utilities) under ASan/UBSan and Valgrind memcheck on Linux, and under
MSVC
/fsanitize=address+ Application Verifier on Windows. - Keep a regression corpus of every crash ever found; every release must pass it clean.
- Treat libpng/libjpeg/freetype as untrusted the day user-supplied assets are allowed, and gate that feature on fuzzing those code paths first.
Acceptance criteria. A fuzzing target exists for each untrusted format; CI runs the regression corpus under sanitizers on every PR with zero findings; a documented seed corpus and crash-triage flow exist.
P2b — Binary exploit-mitigation flags are not fully enabled
Finding. SDLCheck is on, but the projects do not explicitly enable Control Flow Guard
(/guard:cf), CET shadow stack (/CETCOMPAT), or assert high-entropy ASLR / DEP. Some are
linker defaults, but they are not pinned, so a toolchain or setting change can silently drop them.
Risk. Without CFG/CET, a memory bug from P2a is far easier to turn into reliable code execution. These flags are close to free and raise exploitation cost substantially.
Design direction. Enable and pin /guard:cf, /CETCOMPAT, /DYNAMICBASE,
/HIGHENTROPYVA, /NXCOMPAT, and /DEPENDENTLOADFLAG across Vishwakarma,
VishwakarmaExtension, and VishwakarmaSetup. Add a release-gate check (e.g. dumpbin /headers / BinSkim) that fails if any mitigation is missing from a shipped binary.
Acceptance criteria. BinSkim (or equivalent) reports all mitigations present on every
shipped .exe; the check runs in CI.
P3a — Installation private key is stored in plaintext
Finding. AccountManager.cpp writes the installation Ed25519 private key to
%LOCALAPPDATA%\Mission Vishwakarma\Credentials\InstallationKey.pem via
PEM_write_bio_PKCS8PrivateKey(..., nullptr /*cipher*/, ...) — unencrypted. No DPAPI, no
CryptProtectData.
Risk. Any process or user with read access to the profile can copy the key and impersonate
the installation to the telemetry server (and, once login.md ships, to the account system).
Impact is currently limited to identity spoofing / stat pollution — no funds, no PII — hence
Medium, but it grows the day accounts carry authority.
Design direction. Wrap the private key at rest with DPAPI (CryptProtectData, user
scope) so it is bound to the Windows user profile, or store it in the platform credential store.
The public key stays as a convenience copy. Provide a one-time migration that re-wraps an
existing plaintext key on next launch.
Acceptance criteria. The on-disk key is unreadable when copied to another user/machine; telemetry signing still works; a legacy plaintext key is transparently migrated once.
P3b — No Authenticode gate before executing the staged installer
Finding. SoftwareUpdateOnAppLaunch() launches the staged setup after verifying its
SHA-256 against the signed manifest, but does not call WinVerifyTrust to confirm the
binary carries a valid Authenticode signature from the MV code-signer. No WinVerifyTrust call
exists in the tree.
Risk. Cryptographically the hash-from-signed-manifest chain is already sound provided the manifest key is safe — which is exactly the P0 dependency. An Authenticode check is defense-in-depth: it means an attacker needs to break both the manifest key and the code-signing key, and it catches a locally tampered staging file before it runs.
Design direction. Before LaunchProcess(setupPath, "--update"), call WinVerifyTrust and
require the signer chain to terminate at the pinned MV code-signing certificate. Refuse to
launch on failure and discard the staging area.
Acceptance criteria. A staged installer that is unsigned, self-signed, or signed by a different cert is rejected and deleted; a legitimately signed installer runs.
P3c — No dependency-CVE / SBOM process
Finding. The app statically links a substantial C/C++ supply chain (OpenSSL, protobuf,
libpng, libjpeg, freetype, CPython, and the rest of code-external). There is no documented
process for tracking CVEs in these, no SBOM, and no update cadence.
Risk. A published CVE in any linked library (OpenSSL and freetype especially) ships in Vishwakarma until someone notices by hand.
Design direction. Generate an SBOM (CycloneDX) at release; subscribe the pinned submodule
versions to a vulnerability feed / osv-scanner; define a patch SLA for critical CVEs; record
the policy in release.md.
Acceptance criteria. Each release ships an SBOM; a scanner runs in CI against the pinned dependency set; a documented owner and SLA exist.
P4 — Lower-priority and future items
- P4a — Server edge DoS. Rate limiting is per-gunicorn-worker
LocMemCache(acknowledged as coarse insettings.py), and every request pays two Ed25519 verifications before the limiter. An unauthenticated flood of bad-signature requests still burns CPU on the Pi. Direction: add a Cloudflare edge rate-limit rule and an IP-based pre-auth throttle so bad-signature traffic is dropped before signature math. Low priority — Cloudflare already fronts the origin and body size is capped at 1 MB. - P4b — No
SECURITY.md/ disclosure policy. A source-available app that parses untrusted files needs a stated way to report vulnerabilities. Direction: addSECURITY.mdwith a contact and a coordinated-disclosure window. - P4c — Static-site headers undocumented.
hugo.tomldoes not enable goldmarkunsafeHTML (good — raw HTML is escaped), but the security headers formv.ramshanker.in(CSP, HSTS,X-Content-Type-Options) are set at the Cloudflare/host layer and are not captured in the repo. Direction: document the required response headers alongside the deploy config so they survive a hosting change. - P4d — Future subsystems. The planned user-edits pipeline (
useredits.md: wiki[edit]→ Django/api/edits→ bot PRs) and the login subsystem (login.md) each introduce new untrusted-input boundaries (content injection, PR spam, OTP abuse, PII at rest). Each must carry its own threat-model section in its own design doc before implementation, and be linked back here.
4. Verification Infrastructure: the Sanitizer / Valgrind Testing Server
P2a is only credible if there is a machine that continuously proves the parsers are memory-safe. This section is the plan for that machine. It is a plan, not an implementation.
4.1 The reality check first
Vishwakarma is a Windows + DirectX 12 application, and Valgrind does not run on Windows and cannot instrument Direct3D. Any plan that claims to “run Vishwakarma under Valgrind” is wrong. The honest architecture splits the codebase by portability:
| Layer | Examples | Portable to Linux? | Tooling |
|---|---|---|---|
| Portable core | DataStorage protobuf decode & validation, MemoryManagerCPU, ID/CrockfordBase32, geometry math, extension-batch validators |
Yes — no Win32/DX12 | Valgrind + libFuzzer/AFL++ + ASan/UBSan on Linux |
| Windows platform | DX12 renderers, WinHTTP, Win32 UI, SoftwareUpdate process logic |
No | MSVC /fsanitize=address, Application Verifier, Dr. Memory on Windows |
| Server | Django app | N/A (already Linux) | pytest, bandit, pip-audit — not Valgrind |
So the “Valgrind server” is really a Linux memory-safety CI box that exercises the portable core and the file parsers, complemented by a Windows-side sanitizer job. The highest-value target — untrusted-file parsing — is almost entirely in the portable core, which is exactly what Valgrind and a fuzzer can reach. That is why this is worth building despite the platform split.
4.2 What the server does
- Builds a headless Linux harness. A small CMake target links the portable-core subset
(no Win32, no DX12) plus a
main()that reads one input file and drives it through a parser entry point. One target per format:.mvdbprotobuf decode, the.stdparser module, the.dxfparser module, and (once user assets are allowed) libpng/libjpeg/freetype wrappers. - Fuzzes continuously. Each harness is also compiled as a libFuzzer (or AFL++) target
with
-fsanitize=address,undefined,fuzzer. The fuzzer runs against a seed corpus of real engineering files and mutates toward crashes. - Runs deep Valgrind passes. On a slower schedule (nightly), the regression corpus runs
under
valgrind --tool=memcheck --leak-check=full --error-exitcode=1and--tool=helgrindfor the threaded memory manager. Valgrind catches uninitialized reads and leaks that ASan misses; ASan catches overflows Valgrind is slower on. They are complementary, so run both. - Triages and reports. Any crash/leak is minimized, added to the permanent regression corpus, and filed. A release is blocked until the regression corpus passes clean.
4.3 Suggested provisioning (design, not scripts)
- Host. A cheap always-on Linux box or VM (4+ cores, 8 GB+ RAM, SSD for corpus I/O). Ubuntu LTS. This can be a container on the same infrastructure family as the telemetry Pi, but should be a separate machine — fuzzing is CPU-hungry and must not starve the production server.
- Toolchain.
clang/llvm(libFuzzer, sanitizers),valgrind,cmake,afl++optional,git. No network egress needed except to pull the repo. - Isolation. The fuzzer executes attacker-shaped inputs, so run it as an unprivileged user
inside a container with no secrets mounted, read-only root, and a
tmpfsscratch — the same posture as the telemetrysystemdunit. A parser crash must never reach anything valuable. - Corpus store. A versioned directory (or small git-LFS repo) holding:
seeds/(real files, scrubbed of any confidential project data),crashes/(regressions), andminimized/. Seeds must be non-confidential — never commit a customer’s drawing. - CI wiring. A GitHub Actions job builds the sanitizer harnesses and runs the regression corpus on every PR (fast, minutes). The full mutation fuzzing and nightly Valgrind runs happen on the dedicated box on a timer, reporting new crashes back as issues.
- Windows counterpart. A parallel Windows CI job builds with MSVC
/fsanitize=addressand runs the same corpus through the real Windows parsers under Application Verifier, covering the platform code Valgrind cannot.
4.4 What “done” looks like
- A documented list of portable-core files that compile clean on Linux with no Win32/DX12.
- At least one fuzz target per untrusted format, each reaching the actual parser.
- A seed corpus and a growing regression corpus, both non-confidential.
- Nightly Valgrind memcheck + helgrind on the regression corpus, exit-code-gated.
- CI that fails a PR on any sanitizer or Valgrind finding.
- A one-page runbook (in
release.mdor atesting.md) for adding a new fuzz target and triaging a crash.
4.5 Sequencing
The server underpins P2a, so it is built alongside it, after P0 (never expose signing keys to a fuzzing box) and can proceed in parallel with P1. Order: rotate keys (P0) → stand up the Linux harness + seed corpus → wire regression-corpus CI → enable continuous fuzzing + nightly Valgrind → add the Windows sanitizer counterpart.
5. Non-Goals
- Not a penetration-test report or a compliance certification (SOC 2, etc.). This is an engineering roadmap.
- Not DRM or anti-tamper. The software is source-available by design.
- Not confidentiality of telemetry content — it is aggregate and non-PII by design
(
login.md,telemetry). - Not a rewrite in a memory-safe language. Mitigation here is sandboxing + fuzzing + exploit mitigations, not rewriting the DX12 engine.
6. Open Questions (OPEN)
- OPEN: Does the Root CA (
RootCA-MV-01) get installed into any user or OS trust store, or is it used purely as an internal signing anchor? The answer changes P0 from “rotate a signing key” to “rotate a trusted root,” which is far more urgent. - OPEN: Should the extension worker sandbox target AppContainer specifically, or the newer Win32 app-isolation / process-mitigation policies? Decide before P1 implementation.
- OPEN: Where does the fuzzing box live physically, and who owns crash triage?
- OPEN: Timeline for the P0 key rotation overlap window — how long must clients accept both the old and new manifest keys before the old one is retired?