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
- Attacker observes one valid REGISTER (or INVITE) with a correct
Authorization: Digest … nonce="N" response="R" ….
- 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.
- 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.
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 validAuthorizationheader 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.rsverify()builds the challenge to recompute against fromparsed.nonce(client value) atauthenticator.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.nc(nonce-count) tracking, nostale=truere-challenge, no expiry.Attack
Authorization: Digest … nonce="N" response="R" ….verify()recomputesH(HA1:N:…:HA2)using the replayedNand the stored password, getsR, and accepts it — indefinitely.event_handler.rsuser_may_register_aor), but the digest transaction itself is replay-accepting.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
noncethe server did not issue; enforce single-use or monotonicnc; expire nonces and re-challenge withstale=truerather than a hard 401 so clients retry seamlessly.qop=auth(don't silently fall back to RFC 2069).res_pjsip_authenticator_digestuses 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.