Skip to content

Security: TinkerNorth/satellite

Security

SECURITY.md

Security policy: TinkerNorth

This file covers all four TinkerNorth repositories that ship the wireless-gamepad product end-to-end:

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.


Reporting a vulnerability

Do not file a public issue for a suspected vulnerability.

Use one of:

  1. 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.
  2. 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.

Response SLA

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.

Scope

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 :9443 control plane (TOFU-pinned self-signed TLS, HMAC proof-of-key headers, declarative session PUT) and the UDP :9876 data 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 by satellite.

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.

Supported versions

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.


Satellite local-surface hardening

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.com whose A record is flipped to 127.0.0.1 can script requests at us, but the browser still sends Host: evil.com. We reject any request whose Host is present and not loopback.
    • CSRF: a page can fire a no-cors cross-site POST straight at http://127.0.0.1:<port> (loopback Host, so the check above passes). The browser attaches Origin: http://evil.com to such a write, so we reject state-changing methods whose Origin is present and non-loopback. Same-origin dashboard requests carry a loopback (or absent) Origin and pass untouched.
  • Client API: HTTPS (self-signed; clients TOFU-pin the certificate), bound to 0.0.0.0 and therefore LAN-reachable. Authenticated routes require a paired deviceId plus a per-request HMAC proof of the pairing key (X-Device-Id / X-Hmac-Proof headers); pairing itself is PIN-gated.

PIN pairing

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.

How CI prevents vulnerable code from shipping

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.yaml entries 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: cpp for satellite / dish-linux, swift for dish-mac, java-kotlin + cpp for dish-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 defaultsatellite currently ships without those certificates, so releases degrade to -unsigned artifacts instead of failing, and authenticity rests on cosign + SLSA provenance rather than an OS-vendor certificate. Setting the repository variable ENFORCE_RELEASE_SIGNING=true restores 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.
  • SHA256SUMS over every artifact + its signatures + the SBOMs.
  • Cosign keyless signing: every artifact and SHA256SUMS get a .sig
    • .crt, anchored in the Sigstore transparency log.
  • SLSA L3 build provenance: slsa-framework/slsa-github-generator emits <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.


Verifying a release artifact (consumer recipe)

This recipe works the same way for every release, repo, and platform; only the artifact filenames change.

0) Pre-requisites

# 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@latest

1) Download every file from the Release

For 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
ls

You 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

2) Verify the checksum bundle

sha256sum -c SHA256SUMS         # Linux / Windows
shasum -a 256 -c SHA256SUMS     # macOS

Every line must say OK. A failure here means the artifact was modified after release.

3) Verify cosign signed SHA256SUMS from the right workflow

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' \
  SHA256SUMS

Output 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"
done

4) Verify the SLSA L3 provenance

slsa-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.

5) (Optional) Inspect the SBOM

# 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

Known gaps

  • 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 to main are blocked by convention only; the per-repo CI workflows are the de-facto gate. See the matching README.md in each repo for the full text.
  • Vendored-header scanners. satellite/lib/ and satellite/vigem/include/ are not understood by ecosystem scanners. We feed OSV-Scanner a synthetic osv-scanner.toml derived from lib/VENDORED.md, and the vendored-freshness CI job fails if any Last-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 from VENDORED.md.
  • macOS virtual pads are entitlement-gated. The macOS backend synthesizes virtual DualShock 4 controllers via IOHIDUserDevice, which requires the com.apple.developer.hid.virtual.device entitlement. 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 as backendUnavailable. 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.

There aren't any published security advisories