# C8 MAVLink 2.0 per-flight signing handshake (AP only) — D-C8-9 / R03

**Task**: AZ-395_c8_mavlink_signing
**Name**: C8 AP MAVLink 2.0 per-flight signing — handshake + key rotation + zeroisation (D-C8-9 = (d), R03)
**Description**: Extend `PymavlinkArdupilotAdapter` with MAVLink 2.0 per-flight signing per D-C8-9 = (d) (R03 risk; gated for production by IT-3 SITL pass per ADR-008). Generate a fresh ephemeral signing key at takeoff via `secrets.token_bytes(32)`; complete the pymavlink signing handshake during `open(port, signing_key)`; tighten `open(...)` to REJECT `signing_key=None` (replacing the placeholder accept from AZ-393 AP outbound task). Per-flight key rotation: a new key is generated on every `open(...)` call (one per flight). Key zeroisation on `close()`: overwrite the key buffer with `b"\x00" * 32` BEFORE the buffer is deallocated. Key NEVER written to disk; NEVER appears in any log line, FDR record, or stderr trace. Mid-flight signing failure (link drop / counter desync): emit ERROR log, FDR record `kind="c8.ap.signing_failure"`, STATUSTEXT to GCS — but DO NOT raise; the FC ignores unsigned messages and AC-5.2 fallback (AZ-388) takes over downstream. FDR signing-key rotation event ALWAYS emitted at `open(...)` with `kind="c8.ap.signing_key_rotated"` and `{flight_id, key_age_s: 0}` (NO key bytes).
**Complexity**: 5 points
**Dependencies**: AZ-393 (`PymavlinkArdupilotAdapter` skeleton); AZ-390 (`SigningHandshakeError` + `SigningKeyExpiredError`); AZ-273 (FDR), AZ-272, AZ-263, AZ-269, AZ-266
**Component**: c8_fc_adapter (epic AZ-261 / E-C8)
**Tracker**: AZ-395
**Epic**: AZ-261 (E-C8)

### Document Dependencies

- `_docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.md` — Invariants 2, 10.
- `_docs/02_document/components/10_c8_fc_adapter/description.md` — § 5 error handling (signing failure path), § 9 logging (signing key rotation ALWAYS to FDR).
- `_docs/02_document/architecture.md` — D-C8-9 = (d), R03, ADR-008 (IT-3 gate).

## Problem

Without this task, the AP wired MAVLink channel is unsigned — operator-deployed flights would expose the FC to spoofed MAVLink injection (R03). D-C8-9 mandates per-flight signing on the AP wired channel. This task is gated for production by the IT-3 SITL handshake test (C8-ST-01) per ADR-008.

## Outcome

- Extension of `pymavlink_ardupilot_adapter.py`:
  - `__init__` now also accepts the signing-key-source from `config.fc.signing_key_source` (always `"ephemeral_per_flight"` in production; `"static_dev"` in dev for repeatable tests — gated by `BUILD_DEV_STATIC_KEY=ON`).
  - `open(port, signing_key)`:
    - Generates a fresh `signing_key = secrets.token_bytes(32)` if `signing_key_source == "ephemeral_per_flight"`.
    - REJECTS `signing_key=None` (AC-1).
    - Calls `pymavlink.mavutil.mavlink_connection(...).setup_signing(signing_key, ...)`.
    - On handshake failure: raises `SigningHandshakeError("AP signing handshake failed; refusing takeoff")` + ERROR log + FDR record `kind="c8.ap.signing_handshake_failed"`.
    - On handshake success: INFO log + FDR record `kind="c8.ap.signing_key_rotated"` with `{flight_id, key_age_s: 0}` (NO key bytes).
  - `close()`:
    - Zeroes the signing-key buffer in place before deallocation.
    - INFO log `kind="c8.ap.signing_key_zeroised"`.
  - Mid-flight signing failure detection: pymavlink's `signing_failure_count` polled every emit; when it crosses a threshold (configurable; default 3), emit ERROR + FDR `kind="c8.ap.signing_failure"` + STATUSTEXT — DO NOT raise.
- Unit tests: ephemeral key generation, key rejection of None, handshake-failure raises, handshake-success logs, mid-flight signing-failure logs but does not raise, key zeroisation on close, key never appears in any log line / FDR record (regex-based assertion on captured logs).

## Scope

### Included
- Ephemeral per-flight signing-key generation.
- pymavlink signing handshake.
- `SigningHandshakeError` raise on handshake failure (refuse takeoff per § 5 error-handling).
- Mid-flight signing-failure path (log + STATUSTEXT, no raise).
- Key zeroisation on `close()`.
- Key-rotation FDR event.
- Tighten `open(...)` to reject `signing_key=None`.
- Unit tests including secret-leakage regex assertion.
- Integration with `BUILD_DEV_STATIC_KEY=ON` for repeatable dev tests.

### Excluded
- D-C8-2 source-set switch — owned by source-set task.
- iNav signing — explicitly NOT applicable per RESTRICT-COMM-2 (AZ-394 iNav task rejects signing-key).
- C8-ST-01 SITL handshake test — deferred to E-BBT (this task implements the surface; the IT-3 SITL gate exercises it).

## Acceptance Criteria

**AC-1: signing_key=None rejection** — `open(port, signing_key=None)` with `signing_key_source="ephemeral_per_flight"` → key is generated internally; `signing_key_source="explicit"` with None → `SigningHandshakeError("signing_key required for AP")`.

**AC-2: ephemeral key generation** — every `open(...)` call generates a fresh 32-byte key from `secrets.token_bytes`; two consecutive opens produce distinct keys (probabilistic AC; `key_a != key_b` with probability ≈ 1).

**AC-3: handshake failure raises** — SITL refuses the handshake → `SigningHandshakeError` raised + ERROR log + FDR `kind="c8.ap.signing_handshake_failed"`.

**AC-4: handshake success logs (no key bytes)** — successful handshake → FDR `kind="c8.ap.signing_key_rotated"` with `{flight_id, key_age_s: 0}`; assert NO byte sub-sequence of the key appears in the FDR record (regex / hex-substring scan).

**AC-5: Key never in any log** — capture all log lines + FDR records emitted during a 60-second flight; assert NO substring of the key appears in any. (Regex-based test against captured stdout + FDR.)

**AC-6: Mid-flight signing failure no-raise** — inject `signing_failure_count = 5` → ERROR log + FDR `kind="c8.ap.signing_failure"` + STATUSTEXT — `emit_external_position` does NOT raise; subsequent emits continue.

**AC-7: Key zeroisation on close** — instrument the test harness to read the key buffer post-close (best-effort via process memory inspection or test hook); assert the buffer contains all-zero bytes; INFO log `kind="c8.ap.signing_key_zeroised"`.

**AC-8: BUILD_DEV_STATIC_KEY=ON repeatability** — with the dev flag ON, two opens use the SAME static key (sourced from `config.fc.dev_static_signing_key`); production ignores this flag (rejected at config load if `BUILD_DEV_STATIC_KEY=OFF` AND `dev_static_signing_key` set).

**AC-9: STATUSTEXT severity ERROR for handshake failure** — handshake failure → STATUSTEXT severity = ERROR (mapped to MAVLink severity 3); mid-flight failure → STATUSTEXT severity = WARNING (4).

**AC-10: Tighten existing AC** — AZ-393 AP outbound task's AC-8 (placeholder accept of `signing_key=None`) is REPLACED by AC-1 of this task. Document the tightening in code-review hooks.

## Non-Functional Requirements

- pymavlink signing handshake p95 ≤ 1 s (sub-second per § 7).
- Per-emit signing overhead p95 ≤ 0.5 ms (additive on top of the 5 ms emit budget).

## Constraints

- pymavlink bundled unmodified per D-C8-3.
- Keys NEVER persisted to disk, NEVER logged, NEVER serialised.
- `BUILD_DEV_STATIC_KEY` is OFF in production builds; refuse the dev-static path at composition root if the build flag is OFF and the config tries to use it.
- The signing-failure-count threshold is configurable but must never be set so high that the system silently fails for an entire flight without operator notification.

## Risks & Mitigation

- **R03 (no operator-deployed precedent)** — *Mitigation*: gated by IT-3 SITL handshake pass; D-C8-2-FALLBACK options recorded if IT-3 escalates.
- **R09 (key compromise)** — *Mitigation*: per-flight ephemeral keys + zeroisation + never-on-disk + never-in-logs.
- **Risk: key zeroisation flake under Python's GC** — *Mitigation*: use `bytearray` for the key buffer (mutable; can be overwritten in place); explicit overwrite via `for i in range(len(buf)): buf[i] = 0` BEFORE letting GC reclaim it; harness test inspects buffer post-close.
- **Risk: log-leak via uncaught traceback** — *Mitigation*: AC-5 regex scan on captured logs; pymavlink calls wrapped in narrow try/except that logs the error class + message but never the locals.

## Runtime Completeness

- **Named capability**: AP MAVLink 2.0 per-flight signing handshake + zeroisation.
- **Production code**: real `secrets.token_bytes`; real pymavlink `setup_signing`; real zeroisation; real FDR rotation event.
- **Unacceptable substitutes**: a hardcoded global signing key (defeats R03 + R09); skipping zeroisation (defeats Invariant 10).

## Contract

Implements `_docs/02_document/contracts/c8_fc_adapter/fc_adapter_protocol.md` — Invariants 2, 10. Delivers C8-ST-01 (gated by IT-3) wire surface.
