Files
Shadowbroker/.gitignore
T
Shadowbroker 729ea78cb2 Fix #258: AIS proxy SPKI pinning fallback for expired upstream cert (#262)
External report from @jmleclercq: AISStream's Let's Encrypt cert
expired on 2026-05-20 (verified — their renewal pipeline failed), so
the AIS WebSocket connection dies with CERT_HAS_EXPIRED and the
maritime layer empties out. The reporter worked around it locally by
passing { rejectUnauthorized: false } to the WebSocket constructor and
asked whether we should add an env var for that.

That fix is the wrong fix. Disabling TLS validation entirely lets any
network attacker MITM the WebSocket and inject fake ship positions —
same class as the GDELT plaintext-HTTP MITM we just closed in #199.
Adding an env var for it would be an attractive nuisance: operators
set it once during a bad cert week and then forget, leaving themselves
open to MITM forever.

Right fix: SPKI pinning, same pattern as the Tor bundle digest pinning
in #201. The insight is that Let's Encrypt renewals keep the SAME
public key by default, so the SPKI hash survives normal cert rotation.
We can relax the date check while keeping the identity check.

Mechanics:

  backend/data/aisstream_spki_pins.json (new)
    Pinned SHA-256 hashes of the DER-encoded SPKI bytes for
    stream.aisstream.io. Captured 2026-05-20 from the live cert.
    Format is base64(sha256(pubkey_der)), matching the canonical
    openssl pipeline. Whitelisted in .gitignore alongside the other
    static reference data files (KiwiSDR directory, Tor bundle
    digests).

  backend/ais_proxy.js
    Path A (99.9% of the time): normal TLS validation. Untouched.
    Path B (on CERT_HAS_EXPIRED only): re-handshake with
    rejectUnauthorized=false JUST to read the leaf cert, compute its
    SPKI hash, compare against the pinned list. If match → upstream
    is still the genuine AISStream → re-open the WebSocket with
    rejectUnauthorized=false and log DEGRADED MODE. If no match →
    refuse the connection, log loudly: this would be a real MITM.

    Pin file is looked up in three locations so the same code works
    in the Docker backend, the Tauri desktop runtime, and any
    operator-relocated layout (SHADOWBROKER_AIS_PINS env var).
    Embedded fallback list inside the JS so portable installs that
    haven't shipped the JSON still work.

  backend/services/ais_stream.py
    Captures the proxy's status markers from stdout
    ({"__ais_proxy_status": {"degraded_tls": true}}) into a module-
    level snapshot. Exposes ais_proxy_status() for the health
    endpoint. Doesn't touch the data plane — degraded mode keeps
    receiving vessel data, just with weaker MITM protection.

  backend/routers/health.py + backend/services/schemas.py
    /api/health now includes an ais_proxy block with degraded_tls.
    Top-level status escalates ok -> degraded when AIS is in
    degraded TLS mode (but won't downgrade a worse SLO status).
    Operators get a visible signal that they're in degraded mode
    without needing to grep logs.

Tests: backend/tests/test_ais_spki_pinning.py (7 tests)
  - Pin file structure validation (JSON, host entry, base64 SHA-256)
  - ais_proxy_status() snapshot semantics (starts empty, defensive copy)
  - /api/health surfaces ais_proxy.degraded_tls when set
  - /api/health returns empty ais_proxy when proxy hasn't reported

Node.js syntax check passes (node --check) on both backend/ais_proxy.js
and the Tauri runtime mirror.

When AISStream renews their cert (likely within hours-to-days), the
normal-TLS path succeeds on next reconnect and degraded_tls clears
automatically. No operator action needed. If they instead rotate their
server key, the SPKI check will fail and we'll need to add the new
hash to backend/data/aisstream_spki_pins.json before removing the old
one.

Credit: @jmleclercq for the clear report and the careful workaround
verification (Node version, ws version, manual probe).

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-20 20:31:56 -06:00

256 lines
5.2 KiB
Plaintext

# shadowbroker .gitignore
# ----------------------
# Dependencies
node_modules/
venv/
env/
.venv/
backend/.venv-dir
backend/venv-repair*/
backend/.venv-repair*/
# Environment Variables & Secrets
.env
.envrc
.env.local
.env.development.local
.env.test.local
.env.production.local
.npmrc
.pypirc
.netrc
*.pem
*.key
*.crt
*.csr
*.p12
*.pfx
id_rsa
id_rsa.*
id_ed25519
id_ed25519.*
known_hosts
authorized_keys
# Python caches & compiled files
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
.ruff_cache/
.pytest_cache/
.mypy_cache/
.hypothesis/
.tox/
# Next.js build output
.next/
out/
build/
*.tsbuildinfo
# Deprecated standalone Infonet Terminal skeleton (migrated into frontend/src/components/InfonetTerminal/)
frontend/infonet-terminal/
# Rust build artifacts (privacy-core)
target/
target-test/
# ========================
# LOCAL-ONLY: extra/ folder
# ========================
# All internal docs, planning files, raw data, backups, and dev scratch
# live here. NEVER commit this folder.
extra/
# ========================
# Application caches & runtime DBs (regenerate on startup)
# ========================
backend/ais_cache.json
backend/carrier_cache.json
backend/cctv.db
cctv.db
*.db
*.sqlite
*.sqlite3
# ========================
# backend/data/ — blanket ignore, whitelist static reference files
# ========================
# Everything in data/ is runtime-generated state (encrypted keys,
# MLS bindings, relay spools, caches) and MUST NOT be committed.
# Only static reference datasets that ship with the repo are whitelisted.
backend/data/*
!backend/data/datacenters.json
!backend/data/datacenters_geocoded.json
!backend/data/military_bases.json
!backend/data/plan_ccg_vessels.json
!backend/data/plane_alert_db.json
!backend/data/power_plants.json
!backend/data/tracked_names.json
!backend/data/yacht_alert_db.json
# Issue #206: bundled KiwiSDR receiver directory used as last-resort
# fallback when rx.linkfanel.net (HTTP-only upstream) is unreachable
# or returns content that fails our integrity validation.
!backend/data/kiwisdr_directory.json
# Issue #201: pinned SHA-256 digests for known Tor Expert Bundle URLs.
# Used as a second verification source when upstream .sha256sum fails.
!backend/data/tor_bundle_digests.json
# Issue #258: SPKI pins for stream.aisstream.io so we can survive upstream
# Let's Encrypt renewal failures without disabling TLS validation entirely.
!backend/data/aisstream_spki_pins.json
# OS generated files
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# IDEs and Editors
.vscode/
.idea/
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
# Vercel / Deployment
.vercel
# ========================
# Temp / scratch / debug files
# ========================
tmp/
*.log
*.tmp
*.bak
*.swp
*.swo
out.txt
out_sys.txt
rss_output.txt
merged.txt
tmp_fast.json
diff.txt
local_diff.txt
map_diff.txt
TERMINAL
# Debug dumps & release artifacts
backend/dump.json
backend/debug_fast.json
backend/nyc_sample.json
backend/nyc_full.json
backend/liveua_test.html
backend/out_liveua.json
backend/out.json
backend/temp.json
backend/seattle_sample.json
backend/sgp_sample.json
backend/wsdot_sample.json
backend/xlsx_analysis.txt
frontend/server_logs*.txt
frontend/cctv.db
frontend/eslint-report.json
*.zip
*.tar.gz
*.xlsx
# Old backups & repo clones
.git_backup/
local-artifacts/
release-secrets/
shadowbroker_repo/
frontend/src/components.bak/
frontend/src/components/map/icons/backups/
# Coverage
coverage/
.coverage
.coverage.*
dist/
# Test scratch files (not in tests/ folder)
backend/test_*.py
backend/services/test_*.py
# Local analysis & dev tools
backend/analyze_xlsx.py
backend/services/ais_cache.json
graphify/
graphify-out/
# ========================
# Internal docs & brainstorming (never commit)
# ========================
docs/*
!docs/mesh/
docs/mesh/*
!docs/mesh/threat-model.md
!docs/mesh/claims-reconciliation.md
!docs/mesh/mesh-canonical-fixtures.json
!docs/mesh/mesh-merkle-fixtures.json
!docs/mesh/wormhole-dm-root-operations-runbook.md
.local-docs/
infonet-economy/
updatestuff.md
ROADMAP.md
UPDATEPROTOCOL.md
CLAUDE.md
DOCKER_SECRETS.md
# Misc dev artifacts
clean_zip.py
zip_repo.py
refactor_cesium.py
jobs.json
# Claude / AI
.claude
.mise.local.toml
.codex-tmp/
prototype/
.runtime/
# ========================
# Runtime state & operator-local data (never commit)
# ========================
# TimeMachine snapshot cache — regenerated at runtime, can be 100 MB+
backend/timemachine/
# Operator witness keys, identity material, transparency ledgers (machine-local)
ops/
# Runtime DM relay state
dm_relay.json
# Dev scratch notes
improvements.txt
# ========================
# Custody verification temp dirs (runtime test artifacts with private keys!)
# ========================
backend/sb-custody-verify-*/
# Python egg-info (build artifact, regenerated by pip install -e)
*.egg-info/
# Privacy-core debug build (Windows DLL, 3.6 MB, not shipped)
privacy-core/debug/
# Desktop-shell export stash dirs (empty temp dirs from Tauri build)
frontend/.desktop-export-stash-*/
# Wormhole logs (can be 30 MB+ each, runtime-generated)
backend/data/wormhole_stderr.log
backend/data/wormhole_stdout.log
# Runtime caches that already slip through the backend/data/* blanket
# (these are caught by the wildcard but listing for clarity)
# Compressed snapshot archives (can be 100 MB+)
*.json.gz