Skip to content

feat(sip): M6 CP4 — per-endpoint auth selection (unauth trunk + authed bridge coexist) - #152

Merged
ryanmurf merged 2 commits into
masterfrom
feat/m6-cp4-per-endpoint-auth-selection
Jul 17, 2026
Merged

feat(sip): M6 CP4 — per-endpoint auth selection (unauth trunk + authed bridge coexist)#152
ryanmurf merged 2 commits into
masterfrom
feat/m6-cp4-per-endpoint-auth-selection

Conversation

@ryanmurf

Copy link
Copy Markdown
Owner

M6 CP4 — per-endpoint auth selection (unauth trunk + authed bridge coexist)

FINDING. rustisk challenged an inbound request against the UNION of every configured
credential — so if ANY endpoint had auth, EVERY request was challenged. That breaks the topology
where an UNAUTHENTICATED trunk (a carrier, matched by source IP, no digest) and an AUTHENTICATED
bridge (matched by source IP, digest required) share one transport.

FIX. Per-endpoint auth SELECTION: the inbound request is matched to an endpoint (via
type=identify), and a digest challenge is issued ONLY if the MATCHED endpoint has an auth section.

  • matched a specific endpoint → challenge only if THAT endpoint has auth; no auth section → no
    challenge, proceed.
  • no identify match (no type=identify configured) → fall back to the union of authed endpoints'
    credentials, preserving the pre-CP4 default and still challenging a digest endpoint.

This is strictly NARROWER than the old union (which accepted any configured credential), so an
authed endpoint is never weakened — it still requires a valid digest against its OWN credential.

RECEIVER-SIDE PROOF (e2e_per_endpoint_auth.rs). Two endpoints on one transport identified by
distinct source IPs — trunk (127.0.0.2, no auth) and bridge (127.0.0.3, auth). Asserting on the
actual response datagram + accept/reject outcome:

  • (a) trunk INVITE → accepted, NO 401 datagram;
  • (b) bridge INVITE without creds → 401 datagram, rejected;
  • (c) bridge INVITE with a valid digest (computed from the 401 challenge) → accepted.

CAPTURED RED. Revert to the all-credentials union (challenge whenever any endpoint has auth):

assertion failed: the unauth trunk endpoint must be accepted without a challenge
  left: None   right: Some("cp4-trunk")

(the trunk gets challenged → returns None → coexistence broken).

Scope note: this is authz/RFC correctness with NO secret-in-log and NO brute-force control — kept
that way deliberately (per the brief, so the review can go to codex).

GATES. cargo test --workspace --exclude pjsip-shim green; cargo clippy --workspace --exclude pjsip-shim -- -D warnings clean. Rust 1.97.0.

Repin Agent added 2 commits July 17, 2026 04:01
Replace the all-credentials UNION challenge with per-endpoint auth SELECTION:
match the inbound request to an endpoint (via type=identify) and issue a digest
challenge ONLY if the MATCHED endpoint has an auth section. An unauthenticated
trunk (matched, no auth) and an authenticated bridge (matched, auth) now coexist
on one transport. Falls back to the union only when no identify match exists
(no type=identify configured), preserving the pre-CP4 default. Selection is
strictly narrower than the old union (which accepted ANY configured credential),
so an authed endpoint is never weakened.

Receiver-side proof (e2e_per_endpoint_auth.rs): a trunk request (source
127.0.0.2, no auth) is accepted with NO 401 datagram; a bridge request (source
127.0.0.3, auth) WITHOUT creds gets a 401 datagram and is rejected, and WITH a
valid digest is accepted. RED: revert to the union -> the trunk is challenged
(returns None) -> scenario (a) goes RED.
…ial review)

The per-endpoint selection used unwrap_or_default() on the matched endpoint's
auth lookup, which collapsed two misconfigurations to an EMPTY credential set
and then SKIPPED the challenge — accepting a credential-less caller (a fail-open
regression vs the old union, which still challenged via other endpoints' creds):
  (a) the matched endpoint has auth=<name> but no auth section named <name>
      resolves (typo / removed section, dangling reference);
  (b) an identify's endpoint=<name> names a non-existent endpoint.
Both now fail CLOSED with 403 Forbidden. A matched endpoint with NO auth section
still correctly proceeds unchallenged (the unauthenticated trunk).

Adds fail-closed scenarios to e2e_per_endpoint_auth (folded into the single test
since the binary shares process-global config). RED: revert to unwrap_or_default
-> the dangling-auth endpoint accepts a credential-less INVITE (Some(call_id)).
@ryanmurf

Copy link
Copy Markdown
Owner Author

Adversarial review disposition (codex refused the diff — OpenAI safety filter flagged auth code as 'cybersecurity'; fell back to an independent adversarial Claude reviewer per review-routing)

The reviewer confirmed the selection logic is strictly narrower than the old union (a hardening) and the test is a genuine receiver-side digest round-trip. One MAJOR finding, FIXED (f9f8a64):

  • MAJOR (fail-OPEN) — FIXED: the Some(name) branch used unwrap_or_default(), collapsing an UNRESOLVABLE matched-endpoint auth to an empty credential set → challenge SKIPPED → credential-less caller ACCEPTED. Two reachable misconfigs: (a) endpoint auth=<x> but no auth section <x>; (b) an identify's endpoint= names a non-existent endpoint. Both now fail CLOSED (403). A matched endpoint with NO auth still proceeds unchallenged (the trunk). New fail-closed test scenarios added; RED: revert to unwrap_or_default() → the dangling-auth endpoint accepts a credential-less INVITE.
  • MINOR (pre-existing, not a security hole) — noted: match_header-only identify sections aren't consulted by identify_endpoint_by_ip, so a header-identified trunk falls to the union path and gets over-challenged (fail-closed). Pre-existing; the coexistence feature requires IP match= rules. Doc-note for follow-up.

Confirmed non-weakening: verify matches a cred by username against the single matched endpoint's cred; no cross-endpoint credential acceptance; the None/union fallback is only reachable when NO identify rules exist at all (else source_acl 403s first); no cross-context escalation.

CI green; workspace test + clippy green locally.

@ryanmurf
ryanmurf merged commit e607869 into master Jul 17, 2026
3 checks passed
@ryanmurf
ryanmurf deleted the feat/m6-cp4-per-endpoint-auth-selection branch July 17, 2026 10:35
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