Skip to content

Security: smprather/engineering-loadout

Security

docs/SECURITY.md

Security & Malware Assurance

This project deploys into chip-design environments where TSMC infosec mandates no project backups -- ransomware or a wiper inside a bundled tool would be unrecoverable. Assurance is therefore treated as a first-class deliverable, with a per-package framework combining supply-chain provenance (pin + hash + scan everything that ships) and behavioral analysis (canary/strace detonation in a network-isolated container). The source of truth is the machine-checkable ledger under assurance/.

Every claim below is reproducible by running the named command.

1. Content integrity: .content-manifest

.content-manifest records the SHA-256 of every shipped payload file, as stored on disk (per .part-NNN chunk). It is generated by build/gen-content-manifest and regenerated automatically by strip-all-elf-binaries after any payload change.

  • loadout install verifies each payload archive against the manifest before extracting it (default-on; --no-verify escapes). A byte mismatch, or a payload file not listed in the manifest, aborts the install.
  • loadout doctor --verify re-hashes every manifest entry.
  • tests/content-verify and the content-manifest in sync check (build/gen-content-manifest --check) run in tests/run-all Tier 1.

Trust chain: the manifest lives in git; the release tag is SSH-signed (see §6), so tampering with a shipped payload requires the signing key, not just a file drop.

2. Malware scan: scan-for-malware

Two engines; the scan fails if either detects:

  • YARA-Forge full ruleset (vendored yara + payload/yara/), one --scan-list invocation over all files.
  • ClamAV (clamscan) when present; degrades to YARA-only with a warning if absent. Refresh signatures with freshclam on the build box.

./release runs it as a mandatory Step 0 in parallel with the other pre-release gates. The final tag/release step waits and aborts on detection.

Coverage is the whole shipped tree -- ~69,000 files: bin/lib64, runtime archives, wheels, typelibs, treesitter parsers, fonts, the tldr cache, the Windows payload, the crate store, portable-python, and the Neovim + tmux plugin bundles. (Historically only bin/lib64/runtime were effectively scanned; the crate store scanned a dead path and portable-python was skipped entirely -- both fixed.)

./scan-for-malware              # full scan (YARA + ClamAV)
./scan-for-malware --fast       # bin/lib64 only (dev tier)
./scan-for-malware --no-clamav  # YARA only
./scan-for-malware --no-cache   # force a fresh scan and do not write cache
./scan-for-malware --clear-cache # delete cached clean scan results first
./scan-for-malware --path DIR   # scan an arbitrary staging tree (pre-pack)

Clean-result cache. Clean scan results are cached under ${XDG_CACHE_HOME:-~/.cache}/engineering-loadout/malware-scan-v1/. Only clean verdicts are cached; detections are never cached. The cache key hashes the source scan corpus (compressed payloads plus vendored plugin trees, or the explicit --path tree), scan-for-malware itself, vendored YARA binary/rules, the YARA release tag, scan mode flags, and the ClamAV engine/signature fingerprint from clamscan --version. Any payload byte change, rule update, allowlist/script edit, or ClamAV signature update misses the cache and performs a full scan. --verbose bypasses the cache so raw YARA output is available.

False-positive allowlist. _YARA_ALLOWLIST in scan-for-malware excuses a short, audited set of (rule, path) pairs, each with a written justification; any other rule on the same file, or that rule on any other file, still fails. Currently one entry: Firefox's omni.ja (its own resource zip trips a structural APPX/MSIX heuristic). The offensive-security tldr pages (mimikatz/certutil/msfvenom) that used to trip keyword rules are not allowlisted -- ./update tldr-data prunes them from the bundled cache entirely (_TLDR_OFFENSIVE_GLOBS).

3. Plugin pinning (Neovim)

The Neovim plugin bundle is the largest third-party attack surface. It is now commit-pinned in envs/nvim/lazy-lock.json (24 plugins), and env-nvim ships that lockfile so :Lazy restore reproduces the exact vetted commits.

  • Install-time online plugin sync is default-off. --allow-online-plugin-sync opts in and runs Lazy! restore against the committed lockfile (pinned commits), never a bare sync to upstream HEAD.
  • build/build-nvim-plugins rebuilds the bundle from the lockfile in a clean staging directory (never the maintainer's live ~/.local/share/nvim/lazy), scans it with scan-for-malware --path, and packs deterministically.

The tmux plugin bundle is likewise pinned via envs/tmux/vendor/plugins.lock (./update tmux-plugins clones + checks out the pinned commit and records it).

4. Behavioral analysis: tests/dynamic-analysis

A tool is "detonated" in a mock chip-design HOME (canary .v/.lib/.sdc files + docs + a binary blob), wrapped in strace, inside the clean AlmaLinux 8.10 container under docker run --network none. The tool FAILS if it behaves like ransomware/a wiper: any canary modified, an entropy spike (encryption), a write/unlink/rename to project data outside the profile's allowlist, a real AF_INET/AF_INET6 network attempt (local AF_UNIX IPC is ignored), or a mass-unlink. Profiles live in assurance/profiles/<pkg>.toml.

tests/prebuilt-binaries-almalinux8 --dynamic   # container, network isolated
tests/run-all --container                       # includes --full and --dynamic

nvim (the pilot) passes with zero out-of-allowlist mutations and no network.

5. Upstream hash verification: build/verify-binaries

For tools with an official upstream binary release, downloads it, reproduces the bundling transform (strip -> patchelf RPATH), and SHA-256 compares against the bundled binary.

build/verify-binaries            # all
build/verify-binaries rg uv      # spot-check

Outcomes: PASS (byte match), PASS (patchelf layout delta) (identical NEEDED libs + near-identical size), SKIP (EL8 source build / no upstream binary), FAIL (differing deps or size -- investigate). Most tools (bash, rg, bat, jq, eza, fd, tmux, vim, gnuplot, nvim, etc.) are intentionally source-built on EL8, so they SKIP here and are covered by the scan + content manifest + per-package assurance record instead.

6. Supply-chain provenance

  • ./update hashes every download (_download_file) and appends date url sha256 to assurance/downloads.log (TOFU provenance); expected_sha256 can pin a download and abort on mismatch.
  • The nodejs bundle is fetched via nvm, which verifies against nodejs.org SHASUMS256.txt.

Release signing (active)

./release runs the malware scan, binary smoke, version table, and checksum generation as independent parallel gates, then signs the tag with git tag -s and attaches sha256sums.txt plus the .content-manifest asset. The trust chain (manifest in git -> signed tag) is tamper-evident. Verify with git tag -v <tag>; GitHub shows the tag as Verified. The notes carry an on-page SHA-256 table of the integrity-critical files (§1) for at-a-glance checking without a download.

EL8 signer requirement. git SSH signing shells out to ssh-keygen -Y sign, added in OpenSSH 8.2. Stock EL8 ships 8.0p1, whose ssh-keygen has no -Y subcommand -- git tag -s there dies with unknown option -- Y. The bundled openssh package (OpenSSH 10.4p1, ./loadout install openssh) provides a signer that works; it is also how the maintainer's own build box signs. It intentionally does not install bare ssh, scp, or sftp, because mainline OpenSSH cannot parse some Red Hat crypto-policy directives that stock RHEL OpenSSH accepts. Normal ssh stays host-integrated; use ssh10 only when an explicit 10.x client is needed. Configure signing once:

git config gpg.format ssh
git config user.signingkey ~/.ssh/<key>.pub
git config gpg.ssh.program ~/.local/bin/ssh-keygen   # the bundled 10.4p1, not stock 8.0p1

For one-off GitHub transport with the bundled client, use GIT_SSH_COMMAND=ssh10 git fetch or git config core.sshCommand ssh10.

A passphrase-protected key must be unlocked in an ssh-agent first (ssh-add ~/.ssh/<key>) -- otherwise ssh-keygen -Y sign falls back to a GUI askpass and fails on a headless box. ./release inherits SSH_AUTH_SOCK from its environment.

Alternative signer sources (documented for reproducibility): build from the official openssh/openssh-portable tag with build/build-openssh.sh (provenance preferred), or a third-party EL8 OpenSSH RPM (github.com/Nugent1a/OpenSSH-rpms/releases -- unvetted; using an unvetted RPM to build the signer itself is a provenance hole, so the source build is primary).

7. Assurance ledger

assurance/records/<pkg>.toml records, per package: source/build provenance, artifact SHA-256s, scan result, dynamic-analysis result, and package-specific pins:

  • nvim -- per-plugin commit pins (envs/nvim/lazy-lock.json).
  • rust-crate-store -- the full name version cksum closure in assurance/crate-store.lock. build/verify-crate-store re-hashes every one of the 2101 .crate files against the crates.io SHA-256 embedded in the store's own registry index (run in Tier 2 as --check-lock), so a tampered crate fails against both the store index and the signed-tag'd lock.
  • treesitter-parsers -- per-grammar url + revision + shipped-.so sha256 for 318 grammars in assurance/treesitter-parser-locks.json (build/gen-parser-locks, checked in Tier 1). Grammars are already commit-pinned by the vendored parsers.lua; this records the pin -> shipped bytes mapping.

tests/assurance-check re-hashes each record's artifacts against disk and validates these pins, so a record cannot silently drift from reality. nvim, rust, rust-crate-store, and treesitter-parsers are status = verified; coverage rolls out package-by-package.

There aren't any published security advisories