Skip to content

fix(cluster-manager): authenticate pairing signals and limit completion attempts - #105

Closed
woodsonl wants to merge 1 commit into
NVIDIA:developfrom
woodsonl:fix/cluster-manager-pairing-signals
Closed

woodsonl wants to merge 1 commit into
NVIDIA:developfrom
woodsonl:fix/cluster-manager-pairing-signals

Conversation

@woodsonl

@woodsonl woodsonl commented Sep 22, 2026 •

Copy link
Copy Markdown

Description

All four changes are on the plaintext pre-commit HTTP path between the inviter
and the joiner.

Terminal signals carry an HMAC. The cancel, decline, expire, and
fail phases of the pairing endpoint were accepted from any caller that named
an inviteId. Each signal now carries a signalTag: an HMAC keyed by the
session's ephemeral Key Exchange secret, which both parties derive during the
Initial Exchange. handlePairing verifies the tag before dispatching the
phase. eap-noob exposes that secret as Server/Peer.EphemeralKey.

The Completion Exchange is rate-limited per invite. The PIN is six digits,
so each attempt reduces the number of guesses left. The handler counts attempts
per invite, allows a few for typos, and past that tears down the invite and its
EAP session.

The PIN Noob is derived with PBKDF2. It was a single unsalted hash. It is
now PBKDF2 with a fresh per-invite salt, carried inside the MAC-covered
ServerInfo transcript so the salt cannot be swapped. An empty salt keeps the
previous derivation so an older peer still pairs.

A mutex is no longer double-unlocked. handleCancelInvite unlocked
sess.mu on a path that still held it, when the session had no Key Exchange
secret.

Release intent

Changelog title

Pairing signals are authenticated and PIN attempts are limited

Changelog body

Pairing lifecycle signals between an inviter and a joiner are now
authenticated, so a third party who saw an invite identifier cannot tear the
pairing down. Repeated wrong-PIN attempts against one invite are capped, and
the PIN-derived value used during pairing is stretched with a per-invite salt.

Bumps

  • services: patch
  • nvpair-cluster-manager: patch
  • nvpair-engine-manager: none
  • nvpair-errors: none
  • nvpair-job-scheduler: none
  • nvpair-manual-nodes: none
  • nvpair-node-info: none
  • nvpair-node-scanner: none
  • nvpair-node-settings: none
  • nvpair-proxy: none
  • nvpair-tui: none
  • nvpair-ui-broker: none
  • nvpair-workload-manager: none

Scope

Included: the signal authentication, the per-invite completion-attempt cap, the
PBKDF2 PIN Noob, the handleCancelInvite mutex fix, and the EphemeralKey
export in eap-noob that the signal MAC needs.

Excluded: the other pairing behavior is unchanged. The wire format gains an
optional signalTag and pinSalt; a peer that does not send them is handled
by the existing unknown-invite paths.

Validation

  • go build ./... and go test ./... in services/eap-noob and
    services/nvpair-cluster-manager
  • services/nvpair-cluster-manager/pairing_signal_gate_test.go covers signal
    rejection without a valid tag and acceptance with one.
  • services/nvpair-cluster-manager/cm_unit_test.go covers the salted PIN Noob
    round-trip, salt independence, and the empty-salt legacy path.

Risk

Pairing wire format. A signalTag is now required for cancel, decline,
expire, and the plain-HTTP fail path; both sides of a pairing run this
code after the change, so a mixed-version pair is the only concern. The salt
field is optional and empty means the previous derivation, so an older peer
still completes pairing.

Checklist

  • I have read the Contributing Guidelines.
  • Every commit is signed off (git commit -s), certifying the Developer Certificate of Origin.
  • New or existing tests cover the change.
  • Relevant documentation is updated.
  • I checked the diff, changed filenames, and commit messages for credentials, private data, internal URLs, internal issue identifiers, and generated artifacts.
  • I recorded the validation commands and results above.
  • I declared version bumps in the release-intent block above. services/versions.json is written by automation — do not edit it by hand.

Five changes to the pairing flow, all on the plaintext pre-commit HTTP
path:

- Terminal pairing signals (cancel/decline/expire/fail) are now
  authenticated. A bystander who only observed an inviteId on the wire
  could previously drive the inviter or joiner to tear down a pending
  invite. Each signal now carries an HMAC (signalTag) keyed by the
  session's ephemeral Key Exchange secret, which only the two parties
  that ran the Initial Exchange hold; handlePairing verifies it before
  dispatching the phase. eap-noob exposes that secret via
  Server/Peer.EphemeralKey.

- A repeated Key Exchange on a session that already holds its ephemeral
  secret is refused. The EAP peer's onKeyExchangeRequest has no state
  guard, so without this an attacker who learned the inviteId could POST
  a fresh Type-3, overwrite the session key, and then forge the signal
  MAC the change above relies on.

- The Completion Exchange is rate-limited per invite. The PIN is six
  digits, so each attempt materially reduces the guesses needed; N
  attempts tolerate honest typos, and beyond that the invite and its EAP
  session are torn down. The kickoff POST is not counted. The endpoint is
  unauthenticated and the inviteId is on the wire, so an on-path attacker
  can still burn the budget — that is the documented active-MITM boundary,
  noted in the README.

- The PIN Noob is derived through PBKDF2 with a fresh per-invite salt
  (carried inside the MAC-covered ServerInfo transcript, so it cannot be
  swapped) instead of a single unsalted hash. This raises the cost of
  checking PIN candidates against a captured NoobId from milliseconds to
  ~10^6 x iteration cost. Empty salt keeps the legacy derivation so an
  older peer still pairs.

- handleCancelInvite no longer unlocks sess.mu on a path that still held
  it, fixing a double-unlock/leak when the session had no Key Exchange
  secret.

Signed-off-by: woodsonl <65194841+woodsonl@users.noreply.github.com>
@woodsonl woodsonl closed this Sep 24, 2026
@woodsonl
woodsonl deleted the fix/cluster-manager-pairing-signals branch September 24, 2026 05:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant