This file covers all four TinkerNorth repositories that ship the wireless-gamepad product end-to-end:
satellite: server (Windows / Linux / macOS)dish-android: Android clientdish-linux: Linux client (Qt6 / SDL2)dish-mac: macOS client (SwiftUI)
Each repo has its own CONTRIBUTING.md#security section with
ecosystem-specific local commands; this file is the single source of
truth for (1) reporting a vulnerability, (2) what we do with the
report, and (3) how a downstream consumer verifies a release
artifact.
Do not file a public issue for a suspected vulnerability.
Use one of:
- GitHub private vulnerability reporting: open the repo, click Security → Report a vulnerability. This is preferred because it creates a tracked advisory and a private discussion thread.
- Email:
security@tinkernorth.com(PGP key on request). Include the repo, version (commit SHA or release tag), reproduction steps, and impact.
Please do not test exploits against infrastructure you don't own. The on-LAN threat model already covers an attacker with packet-injection ability on the local network; that's the documented design boundary, not a bug.
| Severity | Triage acknowledgement | Initial assessment | Fix target |
|---|---|---|---|
| Critical (CVSS >= 9.0) | 1 business day | 3 business days | 14 days, coordinated disclosure |
| High (CVSS 7.0-8.9) | 2 business days | 5 business days | 30 days |
| Medium / Low | 5 business days | 10 business days | next minor release |
If we miss the SLA, you may publish 90 days after the original report date regardless. We'd rather know than not know.
In scope:
- All four repos in this directory.
- Release artifacts attached to GitHub Releases for any of the four repos.
- The wire protocol (protocol 1; the single source of truth is
docs/contract.md): the HTTPS:9443control plane (TOFU-pinned self-signed TLS, HMAC proof-of-key headers, declarative session PUT) and the UDP:9876data plane (ChaCha20-Poly1305-IETF under per-session HKDF keys, per-direction counters, replay guard). - The pairing flow (PIN paths A/B on
:9443) + the loopback-only admin HTTP/SSE web UI exposed bysatellite.
Out of scope:
- Anything that requires the attacker to already have local privileges
on the user's PC (root, Administrator, ability to drop binaries in
%APPDATA%, etc.). - The vendored ViGEmBus driver itself; file with nefarius/ViGEmBus.
- DoS via raw network flooding. UDP without rate-limit is a known trade-off for hot-path latency; mitigations belong in the network fabric, not the protocol.
| Repo | Supported | Notes |
|---|---|---|
satellite |
latest minor on main; previous minor for 90 days |
Windows is the canonical target; Linux is supported; macOS virtual pads require the virtual-HID entitlement (unentitled builds fall back to an inert backend) |
dish-android |
latest minor on main; previous minor for 90 days |
minSdk 24 |
dish-linux |
latest minor on main; previous minor for 90 days |
tracks the oldest LTS the release CI builds against |
dish-mac |
latest minor on main; previous minor for 90 days |
macOS 13+ |
Patch releases (X.Y.Z+1) are issued on demand for the latest minor;
the previous minor only receives backports for high/critical fixes.
The satellite server exposes two HTTP surfaces with deliberately
different trust models:
-
Admin API / web UI: plain HTTP, bound to
127.0.0.1. There is no authentication because loopback is the trust boundary. Two browser-borne attacks can still reach a loopback port, so an origin guard runs before every route:- DNS rebinding: a page on
evil.comwhose A record is flipped to127.0.0.1can script requests at us, but the browser still sendsHost: evil.com. We reject any request whoseHostis present and not loopback. - CSRF: a page can fire a no-cors cross-site
POSTstraight athttp://127.0.0.1:<port>(loopbackHost, so the check above passes). The browser attachesOrigin: http://evil.comto such a write, so we reject state-changing methods whoseOriginis present and non-loopback. Same-origin dashboard requests carry a loopback (or absent)Originand pass untouched.
- DNS rebinding: a page on
-
Client API: HTTPS (self-signed; clients TOFU-pin the certificate), bound to
0.0.0.0and therefore LAN-reachable. Authenticated routes require a paireddeviceIdplus a per-request HMAC proof of the pairing key (X-Device-Id/X-Hmac-Proofheaders); pairing itself is PIN-gated.
PINs gate the LAN-facing pairing flow and are hardened accordingly:
- PINs and identity tokens are drawn from libsodium's CSPRNG, never a
deterministic PRNG such as
std::mt19937. - PIN comparison is constant-time (
sodium_memcmp) so a wrong guess cannot leak, via timing, how many leading digits matched. - PINs rotate every 5 minutes (the previous PIN stays valid for one extra period), are consumed on a successful pair, and both are burned after 5 failed guesses, so the 4-digit space is not online-brute-forceable.
- The rotating PINs are echoed only on the loopback-bound admin surface (dashboard SSE / status endpoint), never on the LAN-facing client API.
Each repo runs the same shape of gates:
On every PR (blocking):
- Action-pin lint: every
uses:line must reference a 40-char SHA. - Allowlist expiry:
.security/allowlist.yamlentries must be unexpired. - Dependency review: GitHub advisory DB (PR-only).
- OSV-Scanner: vendored components + manifest deps; ecosystem-specific
scope (see each repo's
security.yml). - Gitleaks: secret scanning over the worktree.
- CodeQL:
cppforsatellite/dish-linux,swiftfordish-mac,java-kotlin+cppfordish-android.
On every tagged release (also blocking):
- Re-run of every PR-time gate against the tagged commit.
- Required-secrets gate: reports which platform signing secrets are
missing for a tag (Windows Authenticode, Apple Developer ID +
notarization, Android keystore). It is advisory by default —
satellitecurrently ships without those certificates, so releases degrade to-unsignedartifacts instead of failing, and authenticity rests on cosign + SLSA provenance rather than an OS-vendor certificate. Setting the repository variableENFORCE_RELEASE_SIGNING=truerestores the hard failure for stable tags once the certificates exist. - Artifact-level vulnerability scan: Anchore Grype, fails on CRITICAL/HIGH.
- SBOM generation: Syft, both SPDX-JSON and CycloneDX-JSON.
SHA256SUMSover every artifact + its signatures + the SBOMs.- Cosign keyless signing: every artifact and
SHA256SUMSget a.sig.crt, anchored in the Sigstore transparency log.
- SLSA L3 build provenance:
slsa-framework/slsa-github-generatoremits<repo>.intoto.jsonl.
The result: a known-vulnerable dep, a missing signature, or a tampered binary all fail the release before any artifact lands on the GitHub Release page.
This recipe works the same way for every release, repo, and platform; only the artifact filenames change.
# cosign 2.x
brew install cosign # macOS
go install github.com/sigstore/cosign/v2/cmd/cosign@latest
# slsa-verifier (for the SLSA provenance step)
go install github.com/slsa-framework/slsa-verifier/v2/cli/slsa-verifier@latestFor tag X.Y.Z of <repo> (one of satellite, dish-android,
dish-linux, dish-mac; releases before satellite 1.0.0 used vX.Y.Z
tags):
gh release download X.Y.Z -R TinkerNorth/<repo> -D ./release
cd release
lsYou should see (filenames vary per repo):
satellite-... # platform binary / installer / .app / .apk / etc.
satellite-....sig # cosign signature
satellite-....crt # cosign certificate
SHA256SUMS
SHA256SUMS.sig
SHA256SUMS.crt
satellite.sbom.spdx.json
satellite.sbom.cdx.json
satellite.intoto.jsonl # SLSA L3 provenance
sha256sum -c SHA256SUMS # Linux / Windows
shasum -a 256 -c SHA256SUMS # macOSEvery line must say OK. A failure here means the artifact was
modified after release.
cosign verify-blob \
--certificate SHA256SUMS.crt \
--signature SHA256SUMS.sig \
--certificate-identity-regexp '^https://github\.com/TinkerNorth/<repo>/\.github/workflows/release\.yml@refs/tags/v?[0-9].*$' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
SHA256SUMSOutput ends with Verified OK. The --certificate-identity-regexp
binds the signature to a specific workflow path on TinkerNorth/<repo>;
substitute the actual GitHub organisation. A match means the signature
came from a tagged-release run of release.yml on the public commit
that produced these artifacts; the Sigstore transparency log
(https://search.sigstore.dev/) carries the same record.
To verify each artifact individually (not just SHA256SUMS):
for f in *.exe *.zip *.deb *.AppImage *.apk *.aab; do
[ -f "$f" ] || continue
cosign verify-blob \
--certificate "$f.crt" \
--signature "$f.sig" \
--certificate-identity-regexp '^https://github\.com/TinkerNorth/<repo>/\.github/workflows/release\.yml@refs/tags/v?[0-9].*$' \
--certificate-oidc-issuer 'https://token.actions.githubusercontent.com' \
"$f"
doneslsa-verifier verify-artifact \
--provenance-path <repo>.intoto.jsonl \
--source-uri github.com/TinkerNorth/<repo> \
--source-tag X.Y.Z \
<artifact-filename>This proves the artifact was produced by a tagged run of release.yml
on the named source repo. Output ends with
PASSED: SLSA verification passed.
# Top-level summary
syft attestation --output spdx-json release/<repo>.sbom.spdx.json
# Or just diff against last release
diff <(jq -S . prev-release/<repo>.sbom.spdx.json) \
<(jq -S . release/<repo>.sbom.spdx.json) \
| less- Branch protection on
main. Three of the four repos run on a free org plan that does not expose required-status-check enforcement for private repositories. Direct pushes tomainare blocked by convention only; the per-repo CI workflows are the de-facto gate. See the matchingREADME.mdin each repo for the full text. - Vendored-header scanners.
satellite/lib/andsatellite/vigem/include/are not understood by ecosystem scanners. We feed OSV-Scanner a syntheticosv-scanner.tomlderived fromlib/VENDORED.md, and thevendored-freshnessCI job fails if anyLast-vendored:date is more than 90 days old. This is best-effort, not exhaustive; file an advisory if you spot a vendored component that's missing fromVENDORED.md. - macOS virtual pads are entitlement-gated. The macOS backend
synthesizes virtual DualShock 4 controllers via
IOHIDUserDevice, which requires thecom.apple.developer.hid.virtual.deviceentitlement. Production (signed) builds carry it; CI artifacts and local unentitled builds detect the missing entitlement at runtime and fall back to the historical inert backend: the protocol stack runs, but every controller descriptor applies asbackendUnavailable. The CI artifact name (satellite-macos-stub-...) reflects that fallback. Don't open a vulnerability report for the absence of virtual-gamepad creation in an unentitled build; it's the documented, probe-enforced fallback.