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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions