Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/threats.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ follow-up issue.
| **Velocity-mismatch teleport** — GPS reports velocity inconsistent with IMU integration | Two-of-two persistence: `|v_gps − v_imu| > 15 m/s` sustained ≥2 consecutive fixes | `src/detect/jump.rs:46-55, 41` | `vmismatch_requires_persistence` | Attacker who can keep `|Δv|` below 15 m/s wins this single test, falls through to CUSUM and hard-residual layers |
| **Slow drift (naive)** — GPS walks off course at sub-jump speed, reported Doppler left honest | Per-axis two-sided CUSUM with `k=1.0 m`, `h=25 m`. Sums accumulate any per-fix residual above noise floor until threshold | `src/detect/drift.rs:79-98` | `persistent_north_drift_fires`, `tests/scenario_drift.rs` | If attacker pins `|r|` to *exactly* `k=1.0 m`, per-axis accumulators stagnate (B-01 risk); magnitude CUSUM is the backup |
| **Consistent-velocity walk-off ("smart" / EKF-laundered)** — GPS position ramps off course AND the reported Doppler is faked to match, so the complementary velocity blend tracks it and the position + velocity-mismatch lanes are driven to ~0 (the RQ-170 class; also the param-mode SITL case where ArduPilot's EKF has fused a slow ramp). Found by the SITL Phase-2 characterization: evaded the detector entirely below 2 m/s | Velocity-aiding CUSUM over the FREE-INERTIAL velocity residual `mag_vel_free = \|v_gps − v_free_inertial\|`, where `v_free_inertial = v_blended − Σ(blend corrections)` is reconstructed GPS-velocity-INDEPENDENT (so it retains the masked bias ≈ the spoof rate). Adaptive floor learned only from clearly-quiet (`< base k`) non-maneuvering fixes; base `k=0.55 m/s`, `h=8`. SUSPENDED while maneuvering (gyro-gated) — a coordinated turn makes the free-inertial velocity diverge ~2 m/s of legitimate attitude/centripetal error | `src/detect/drift.rs` (`s_vel_aiding`, `vel_aiding_*`), `src/nav/mod.rs` (`aiding_vel`, `is_maneuvering`) | `velocity_aiding_fires_on_sustained_masked_bias`, `velocity_aiding_suspended_while_maneuvering`, `velocity_aiding_no_false_alarm_on_realistic_doppler_noise`, `tests/scenario_consistent_drift.rs` (caught ≥1 m/s; clean 600 s + sustained turn no false-latch) | Bounds (all 🟡, by design — each trades detection of a vanishingly-slow attack for zero false alarms on honest dynamic flight): (1) a walk-off below ~0.5 m/s sits at/under the free-inertial velocity-bias floor — undetected; (2) a walk-off confined to turns is unobservable (lane suspended while maneuvering); (3) a spoofer who ramps the bias on over many minutes can be tracked by the adaptive floor |
| **Receiver takeover / constellation capture** — the moment a spoofer out-powers the live signals and hands the receiver a SIMULATED constellation. Every other lane in this table is residual-based and therefore structurally blind here: a competent spoofer captures ALIGNED (first spoofed fix ≈ truth) and only then walks off, so at takeover the residual is ~0 and there is genuinely nothing for them to see. This is the window the velocity-aiding row's bounds (1) and (3) live in | Constellation-quality discontinuity lane: EWMA baselines of satellite count and HDOP track "the sky we have been flying under"; a step of ≥4 sats or ≥0.8 HDOP away from that baseline, persisting ≥2 fixes, fires `ConstellationShift` and registers an external anomaly with the FSM. An abrupt HDOP **improvement** counts as much as a degradation — synthesized geometry is cleaner than a real sky. The baseline learner FREEZES while a deviation is in progress (same idiom as the drift lane's noise floor) so it cannot chase the step and erase the signal | `src/detect/quality.rs`, `src/fusion.rs` (call site) | `satellite_count_step_is_reported_once_then_the_lane_goes_quiet`, `suspiciously_perfect_geometry_is_reported`, `simultaneous_steps_report_as_both`, `slow_natural_drift_is_absorbed_not_reported`, `single_fix_glitch_is_ignored`, `oscillating_metadata_cannot_sustain_the_dwell`, `warmup_suppresses_reports_while_the_baseline_is_forming`, `absent_quality_data_is_not_a_discontinuity`, `steady_constellation_never_reports`, `disabled_lane_is_silent`; end-to-end `tests/scenario_constellation_shift.rs` | 🟡 **Evadable by construction, and must never be described as coverage.** These are fields the attacker's own signal generator produces, so an adversary who holds sat count and HDOP steady across the handover is invisible to this lane. What it buys is cost: a clean takeover now requires modelling the victim's *current* constellation well enough to fake continuity into it, not merely out-powering the sky. It is **corroboration, not proof** — and it is built so it *cannot* sever GPS alone: it re-baselines onto the new level the instant it reports (one-shot per step) and is rate-limited by a 30 s refractory, so it can raise Suspicious but can never sustain a firing across the spoofed dwell. Severing requires a residual lane to confirm. Also blind to **meaconing** (rebroadcast preserves the real constellation) — covered instead by the boot anchor and the rebroadcast-delay position jump |
| **Circular / spiral drift** — per-axis sums oscillate around zero so signed CUSUM misses it | Magnitude CUSUM: one-sided sum over `|r|`, with an ADAPTIVE reference — the effective `k_mag` is `max(base, learned_noise_floor × 1.5)`. The noise floor is an online running-mean (warmup) then quiescence-gated EWMA of `|r|`, so the detector adapts to the actual GPS noise level instead of a fixed constant | `src/detect/drift.rs` (`mag_noise_ewma`, warmup logic) | `circular_drift_caught_by_magnitude_cusum_only`, `magnitude_cusum_no_false_alarm_on_realistic_gps_noise`, `real_attack_still_fires_after_noise_floor_learned_on_noisy_gps` | None for the false-alarm case (B-02 fixed: verified 0 fires over 600 s of σ=2.5 m noise while a 6 m/fix drift still fires). Two residuals (both 🟢, audit U-01): (1) a constant-radius circular attack present from boot with NO clean baseline reads as the noise floor — indistinguishable from noisy GPS by construction; (2) a circular spoof sustained through the 20-fix warmup window (begins at preflight-Ready) can inflate the learned floor, desensitizing ONLY the magnitude lane — the per-axis CUSUM and jump detector are unaffected during warmup, so a linear/teleport component is still caught |
| **Replay attack** — attacker re-injects an old GPS_RAW_INT with fresh timestamp, GPS lat/lon identical across fixes | Frozen-fix detector: if last 3+ fixes are within `FROZEN_FIX_RADIUS_M = 0.5 m` AND IMU reports `|v| > 1 m/s` motion, fire `FrozenGps` event and register external anomaly with FSM | `src/fusion.rs:198-205, 665-763` | `tests/scenario_frozen_gps.rs` | Attacker who alternates replay with one drift-step per cycle (B-04) keeps streak below threshold; circular-replay also defeats unless covered by magnitude CUSUM |
| **Vertical-only spoof (sudden)** — altitude teleported but lat/lon unchanged | GPS-only vertical-rate sanity check: if reported altitude changes faster than `max_vertical_rate_mps` (default 30 m/s, ~2× the fastest real multirotor descent) between consecutive fixes, fire a `Jump` event with reason `VerticalRate` and register an external anomaly with the FSM. Deliberately does NOT use the unreliable vertical dead-reckoning | `src/fusion.rs:847-883`, `src/detect/mod.rs:46-58` | `altitude_teleport_fires_vertical_rate_jump`, `clean_flight_emits_no_vertical_rate_jump` | Two gaps (both 🟢): (1) **gradual** altitude drift below the 30 m/s rate bound — GPS altitude is inherently 2-3× noisier than horizontal with no reliable inertial vertical reference; (2) an altitude teleport that lands exactly on a GPS-dropout boundary (gap > freeze threshold) is not assessed — the rate check is skipped across dropouts (audit U-02) to avoid false-firing on a legitimate sustained descent during an outage. Sudden teleports during normal operation are caught (B-42) |
Expand Down
15 changes: 15 additions & 0 deletions src/detect/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@

pub mod drift;
pub mod jump;
/// Constellation-quality discontinuity — the only lane that watches the GPS
/// METADATA rather than the residual, so it can fire at the takeover moment,
/// before any drift has accumulated. Corroboration, not coverage: a spoofer
/// controls these fields and can forge continuity. See the module docs.
pub mod quality;
pub mod residual;
pub mod state_machine;

Expand Down Expand Up @@ -81,6 +86,15 @@ pub struct DetectConfig {
/// see docs/threats.md.
pub vel_aiding_cusum_k_mps: f32,
pub vel_aiding_cusum_h: f32,
/// Constellation-quality discontinuity lane (see [`quality`]). Watches the
/// satellite count and HDOP for a STEP away from the sky we have been
/// flying under — the signature of a receiver being captured by a
/// simulated constellation. Orthogonal to every other lane: it fires at the
/// takeover, where the residual is still ~0 and nothing else can see
/// anything. Evadable by a spoofer that forges metadata continuity, so it
/// escalates to Suspicious and is deliberately built so it can never
/// sustain a firing and sever GPS by itself.
pub quality: quality::QualityConfig,
}

impl Default for DetectConfig {
Expand Down Expand Up @@ -122,6 +136,7 @@ impl Default for DetectConfig {
// realistic-noise + consistent-drift sims.
vel_aiding_cusum_k_mps: 0.55,
vel_aiding_cusum_h: 8.0,
quality: quality::QualityConfig::default(),
}
}
}
Loading
Loading