# Security Tests > Black-box security scenarios at the public interfaces. Code-level vulnerability scanning is out of scope here (handled by Phase 4 security audit / `security/SKILL.md`). --- ### NFT-SEC-01: MAVLink2 signing — invalid signature rejected (S-T1) **Summary**: A GPS_INPUT or other companion-bound MAVLink frame with invalid signing tag is rejected by the FC; SUT and FC both log the rejection. **Traces to**: M-7, R10, restrictions §Sensors (MAVLink2 signing mandatory), S-T1, F-T9. Tier: T3 (`deferred-sitl`). **Steps**: | Step | Consumer Action | Expected Response | |------|----------------|-------------------| | 1 | Runner injects a GPS_INPUT with valid schema but signing tag computed against a wrong key | FC discards frame; STATUSTEXT WARN visible at GCS | | 2 | Inspect FC log | rejection event recorded | | 3 | Subsequent valid GPS_INPUT | accepted normally | **Pass criteria**: invalid frame discarded; FC continues on prior valid source; valid frames still accepted. --- ### NFT-SEC-02: HTTPS unauthenticated requests are rejected **Summary**: All HTTPS API endpoints require valid JWT. **Traces to**: results_report row 33, restriction "JWT auth on the HTTP boundary". Tier: T1. **Steps**: | Step | Endpoint | Auth | Expected Response | |------|---------|------|-------------------| | 1 | `POST /sessions` | none | HTTP 401 | | 2 | `POST /objects/locate` | none | HTTP 401 | | 3 | `GET /sessions/{id}/stream` | none | HTTP 401 | | 4 | `GET /health` | none | HTTP 200 (health is intentionally unauthenticated for liveness probes — confirm via S-T2) OR 401 if it requires auth | **Pass criteria**: 1–3 return 401; 4's behaviour matches the documented contract (test asserts whichever the contract states). If `/health` is unauthenticated, body still must NOT leak sensitive state (no flight data, no key fingerprints). --- ### NFT-SEC-03: HTTPS — malformed / expired / wrong-issuer JWT **Summary**: JWTs that fail validation are rejected. **Traces to**: derived from results_report row 33. Tier: T1. **Steps**: | Step | Token | Expected Response | |------|-------|-------------------| | 1 | malformed (`.foo.bar`) | HTTP 401 | | 2 | expired (`exp` in the past) | HTTP 401 | | 3 | wrong issuer | HTTP 401 | | 4 | wrong signing algorithm (`none` algorithm) | HTTP 401 | | 5 | missing required claim (e.g., `sub`) | HTTP 401 | **Pass criteria**: all return 401 with no leaked state in the body. --- ### NFT-SEC-04: TLS — minimum version + downgrade rejection **Summary**: TLS ≥1.2; weaker / downgrade attempts rejected. **Traces to**: S-T2, derived from restriction "telemetry plumbing uses MAVSDK + HTTPS API". Tier: T1. **Steps**: | Step | Consumer Action | Expected Response | |------|----------------|-------------------| | 1 | Connect with TLSv1.0 / TLSv1.1 | refused | | 2 | Connect with cipher suite from a known weak set (e.g., RC4) | refused | | 3 | Valid TLSv1.2+ + modern cipher | accepted | **Pass criteria**: all weak attempts refused; modern accepted. --- ### NFT-SEC-05: Tile-cache write attempt by unauthorized API path **Summary**: SUT does not expose any HTTP path that allows external clients to write to the tile cache. **Traces to**: AC-8.5 (storage policy), AC-NEW-7 (cache integrity), restriction §Satellite. Tier: T1. **Steps**: | Step | Consumer Action | Expected Response | |------|----------------|-------------------| | 1 | `POST /tiles` (or any guess) with valid JWT | 404 or 405 (no such endpoint) | | 2 | Try `PUT /var/lib/gpsdenied/tiles/...` via any exposed API | 404 / 405 | | 3 | Inspect the documented OpenAPI contract | no tile-write endpoints | **Pass criteria**: no successful tile-write paths exist via HTTP; only the post-flight uploader (out-bound to `service-stub`) writes outside the SUT. --- ### NFT-SEC-06: Spoofed sysid / sysid collision (M-31) **Summary**: A second device claiming sysid 11 (the SUT's sysid) — FC handles per ArduPilot routing rules. **Traces to**: M-31, F-T9. Tier: T3. **Steps**: | Step | Consumer Action | Expected Response | |------|----------------|-------------------| | 1 | Runner publishes a fake GPS_INPUT from a sysid-collision sender | FC routing handles per documented behaviour (latest-talker wins or rejects) | | 2 | Confirm FC parameter audit prints the actual sysid configured | matches deployment runbook (M-31 sysid collision-check) | **Pass criteria**: behaviour matches documented FC routing rule; STATUSTEXT WARN observable; test verifies the deploy runbook's collision-check (M-31) catches this in pre-flight. --- ### NFT-SEC-07: Operator-hint injection — only signed STATUSTEXT consumed **Summary**: Unsigned operator hints (or hints from a non-allowed sender) are not consumed. **Traces to**: AC-6.2, M-7. Tier: T3. **Steps**: | Step | Consumer Action | Expected Response | |------|----------------|-------------------| | 1 | Send `RELOC_HINT` STATUSTEXT with invalid MAVLink2 signing | SUT discards; emits WARN | | 2 | Send from a sysid not on the allowed-list | SUT discards | | 3 | Send signed by allowed sender | SUT consumes (NFT-RES-05 covers happy path) | **Pass criteria**: only authenticated, allowed-sender hints are consumed. --- ### NFT-SEC-08: GPS_RAW_INT spoofing chain — SUT promotion is the safety boundary **Summary**: A spoofed `GPS_RAW_INT` cannot influence SUT's GPS_INPUT directly; SUT only uses GPS_RAW_INT for source-promotion logic, not for fusing. **Traces to**: AC-NEW-2, restriction §Failsafe. Tier: T3. **Steps**: | Step | Consumer Action | Expected Response | |------|----------------|-------------------| | 1 | Inject GPS_RAW_INT with high-quality false fix | SUT does NOT use it as a position seed; only uses it for the "real-GPS health" rolling average | | 2 | After scripted spoofing-pattern, SUT promotes its own estimate per AC-NEW-2 | promotion event observable | **Pass criteria**: SUT GPS_INPUT positions never influenced by spoofed GPS_RAW_INT lat/lon (compare SUT GPS_INPUT vs ground truth from `coordinates.csv` during the spoof window). --- ### NFT-SEC-09: USB bypass surface — bench-only **Summary**: USB bypasses MAVLink2 signing per restriction; this must be **disabled in production** runtime config. **Traces to**: M-7, restrictions §Onboard Hardware. Tier: T1 (config audit). **Steps**: | Step | Consumer Action | Expected Response | |------|----------------|-------------------| | 1 | At SUT boot, inspect runtime config | USB MAVLink endpoint disabled in production profile (env var `MAVLINK_USB_ALLOWED=false` or absent) | | 2 | Attempt to connect via USB | refused | **Pass criteria**: production config refuses USB MAVLink; bench config (env var explicitly enabled) accepts. --- ### NFT-SEC-10: FDR — no sensitive-data leak **Summary**: FDR contains the documented payload classes only — no private keys, no plaintext JWTs, no MAVLink2 signing keys, no raw frames (AC-8.5). **Traces to**: AC-8.5, AC-NEW-3, S-T3 (data-at-rest). Tier: T1. **Steps**: | Step | Consumer Action | Expected Response | |------|----------------|-------------------| | 1 | After a 30 min replay, scan FDR for known-sensitive byte patterns (test-only signing key bytes; test JWT) | none found | | 2 | Scan for raw JPEG headers in non-thumbnail-log payload classes | none | | 3 | Verify failure-thumbnail log is ≤ 0.1 Hz and within FDR cap | as spec'd | **Pass criteria**: no leaks; raw-frame storage policy enforced. --- ### NFT-SEC-11: External-host network policy **Summary**: SUT does not call external commercial satellite providers at runtime. **Traces to**: AC-8.1, restrictions §Satellite. Tier: T1. **Steps**: | Step | Consumer Action | Expected Response | |------|----------------|-------------------| | 1 | Run a 5-min replay with `iptables` / Docker network policy capturing all out-bound connections | none of the captured destinations resolves to Maxar / Airbus / Planet / Sentinel-2 / Esri / etc. | | 2 | The only allowed out-bound is to `service-stub` (the Suite Satellite Service candidate-pool endpoint, post-flight) | matches | **Pass criteria**: no out-bound to commercial / public ortho providers at runtime. --- ### NFT-SEC-12: HTTPS — payload size + path-traversal hardening **Summary**: Pathological HTTP requests do not crash the SUT or leak filesystem content. **Traces to**: AC-3.x (resilience), restrictions (security defaults). Tier: T1. **Steps**: | Step | Consumer Action | Expected Response | |------|----------------|-------------------| | 1 | `POST /objects/locate` with a 100 MB body | HTTP 413 (payload too large) | | 2 | Path-traversal `GET /sessions/../../etc/passwd` | HTTP 404 / 400; no filesystem leak | | 3 | Header-injection (`X-Forwarded-For: \r\nSet-Cookie: …`) | sanitised; no echo back | **Pass criteria**: as above; SUT alive; no leak. --- ### NFT-SEC-13: AC-NEW-7 over-confidence injection — gate rejects **Summary**: Synthetic over-confidence injection (1.5×–3× covariance deflation) does not let bad tiles into the trusted basemap. **Traces to**: AC-NEW-7. Tier: T2 (`deferred-corpus`). **Steps**: | Step | Consumer Action | Expected Response | |------|----------------|-------------------| | 1 | Replay AerialVL + Mavic + AerialExtreMatch with synthetic deflation | per-tile geo-misalignment computed | | 2 | At the σ_xy boundary (3 m, 5 m, 10 m), assert hard-gate behaviour | tiles outside σ_xy ≤ 5 m never written; tiles in (3, 5] m marked `trust_level=soft`; tiles ≤ 3 m `trust_level=candidate` | **Pass criteria**: P(misalign > 30 m) < 1 %, P(misalign > 100 m) < 0.1 %; voting layer prevents single-flight promotion in non-active sectors.