Skip to content

SIP digest auth: nonce is never validated as server-issued or single-use — REGISTER/INVITE Authorization replay #128

Description

@ryanmurf

Summary

Inbound SIP digest verification recomputes the expected response using the client-supplied nonce and never checks that (a) the nonce was one the server actually issued, (b) it has not been used before, or (c) it is fresh (no stale/expiry, no nonce-count tracking). A captured valid Authorization header is therefore replayable, and a client may present an arbitrary self-chosen nonce.

Nonce entropy is fine (128 bits, rand::thread_rng → 16 bytes hex, authenticator.rs:480-485). The problem is purely the lack of server-side nonce state.

Location

crates/asterisk-sip/src/authenticator.rs

  • verify() builds the challenge to recompute against from parsed.nonce (client value) at authenticator.rs:156-159; there is no lookup of an issued-nonce table.
  • build_challenge() mints a fresh nonce (generate_nonce, line 248) but stores it nowhere.
  • No nc (nonce-count) tracking, no stale=true re-challenge, no expiry.

Attack

  1. Attacker observes one valid REGISTER (or INVITE) with a correct Authorization: Digest … nonce="N" response="R" ….
  2. Attacker replays the identical request. verify() recomputes H(HA1:N:…:HA2) using the replayed N and the stored password, gets R, and accepts it — indefinitely.
    • For REGISTER this is bounded by the AoR-binding authz added later in the handler (event_handler.rs user_may_register_aor), but the digest transaction itself is replay-accepting.
  3. Related: because the server accepts any client nonce, and it also accepts an RFC 2069 (no-qop, no-cnonce) response when the client simply omits qop (authenticator.rs:176 / auth/mod.rs:157-179), an attacker/MITM can downgrade away from the cnonce-bearing qop=auth form, removing what little replay friction cnonce provides.

Impact

Replay of authenticated SIP transactions; defeats the anti-replay purpose of the nonce. Medium — the credential itself is not disclosed, and full enforcement (registrar/INVITE binding) is M6 territory, but the digest primitive is wrong today.

Recommended fix

  • Issue stateful or keyed (stateless-HMAC) nonces and validate on verify: reject a nonce the server did not issue; enforce single-use or monotonic nc; expire nonces and re-challenge with stale=true rather than a hard 401 so clients retry seamlessly.
  • Reject qop downgrade for endpoints that were challenged with qop=auth (don't silently fall back to RFC 2069).
  • Asterisk's res_pjsip_authenticator_digest uses a timestamped, keyed nonce with an age window — a good model.

Deferred to M6 per the milestone plan; filing so the primitive gap is tracked. Entropy is OK; single-use/issuance-validation is the gap.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions