The accepts for #9 (PIN-window burn) and #13 (knock-queue flood) rested on a circular premise — each cited the other as the safe fallback — and a re-review showed one LAN attacker could defeat BOTH, denying all onboarding. Close them: - #13 per-source-IP cap on the pending-knock queue (MAX_PENDING_PER_IP) so one host can't fill/evict the 32-slot queue (QUIC validates the source address); and eviction now NEVER drops a live *parked* knock (a held-open connection awaiting operator approval), so a cert-rotating flood can't evict the genuine device being onboarded. This makes the delegated-approval path genuinely flood-resistant — restoring the validity of #9's "use delegated approval on hostile LANs" fallback. - #9 fingerprint-bindable PIN window: `NativePairing::arm_for(ttl, Some(fp))` binds the window to one operator-selected device; `pin_for_attempt` returns `BoundToOther` for any other fingerprint, which the QUIC pair path rejects WITHOUT consuming the window — so an unpaired peer can neither pair nor BURN a window armed for a specific device (it can't forge the bound fingerprint). The mgmt `POST /native/pair/arm` gains an optional `fingerprint` (from a pending knock); unbound arming keeps the legacy any-device behavior (trusted-LAN). (Web-console "pair this pending device with a PIN" UX is a follow-up; the flood-resistant knock path above is the immediate hostile-LAN onboarding path.) + regression tests (armed_pin_is_fingerprint_bindable, pending_per_ip_cap_and_parked_protection); api/openapi.json regenerated. 110 host tests + clippy + fmt green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -532,10 +532,25 @@ async fn serve_session(
|
||||
.await
|
||||
.map_err(|_| anyhow!("first message timeout"))??;
|
||||
if let Ok(req) = PairRequest::decode(&first) {
|
||||
// Read the live arming PIN per attempt, so a window that lapsed no longer pairs.
|
||||
let pin = np
|
||||
.current_pin()
|
||||
.context("pairing not armed (arm it in the console, or start with --allow-pairing)")?;
|
||||
// The client fingerprint (cert possession is proven by the QUIC handshake) is needed to honor
|
||||
// a fingerprint-bound PIN window (#9): a window the operator armed for a SPECIFIC device must
|
||||
// not be consumable — or burnable — by any other fingerprint.
|
||||
let client_fp = endpoint::peer_fingerprint(&conn)
|
||||
.ok_or_else(|| anyhow!("pairing requires the client to present a certificate"))?;
|
||||
let client_fp_hex = fingerprint_hex(&client_fp);
|
||||
// Resolve the live arming PIN per attempt (so a lapsed window no longer pairs), honoring any
|
||||
// fingerprint binding.
|
||||
let pin = match np.pin_for_attempt(&client_fp_hex) {
|
||||
crate::native_pairing::PinAttempt::Pin(pin) => pin,
|
||||
crate::native_pairing::PinAttempt::Disarmed => anyhow::bail!(
|
||||
"pairing not armed (arm it in the console, or start with --allow-pairing)"
|
||||
),
|
||||
// Armed for a DIFFERENT device — reject without running the ceremony, so this attempt does
|
||||
// NOT consume (burn) the operator's window for the device they actually selected (#9).
|
||||
crate::native_pairing::PinAttempt::BoundToOther => anyhow::bail!(
|
||||
"pairing is armed for a different device — this attempt does not consume the window"
|
||||
),
|
||||
};
|
||||
{
|
||||
let mut last = last_pairing.lock().unwrap();
|
||||
if let Some(t) = *last {
|
||||
@@ -589,7 +604,9 @@ async fn serve_session(
|
||||
);
|
||||
tracing::info!(name = %label, fingerprint = %fp_hex,
|
||||
"unpaired device knocked — parking connection for delegated approval in the console");
|
||||
np.note_pending(&label, &fp_hex);
|
||||
// Record the QUIC-validated source IP so the pending queue's per-source cap can stop one
|
||||
// host from flooding/evicting genuine knocks (#13).
|
||||
np.note_pending(&label, &fp_hex, Some(peer.ip()));
|
||||
// Free the session slot while a human decides — a parked knock must not hold an NVENC
|
||||
// permit (a handful of parked knocks would otherwise block every real session).
|
||||
drop(permit);
|
||||
|
||||
Reference in New Issue
Block a user