Skip to content

Security

Germán Luis Aracil Boned edited this page Jul 2, 2026 · 4 revisions

Security and Robustness

chan_sofia was hardened deliberately, not incidentally. This page summarizes the security model and the robustness posture, each grounded in the source. The feature list is Features; the WebRTC trust model is detailed in WebRTC.

Authentication

  • Digest auth, MD5 + SHA-256 (RFC 7616). Selectable per deployment via auth_algorithms, with anti-downgrade so the server accepts only an algorithm it actually offered (channels/chan_sofia.c:6713-6796, channels/sofia/include/chan_sofia_internal.h:91-93).
  • Fresh, crypto-secure nonces. Each challenge uses a 128-bit nonce from /dev/urandom with a configurable TTL; an expired-nonce client is re-challenged (stale) rather than rejected (sofia_secure_nonce_gen(), channels/chan_sofia.c:6681).
  • Constant-time digest compare. The response is compared with a constant-time memcmp (volatile accumulator + memory barrier) to deny a timing oracle (sofia_ct_memcmp, channels/chan_sofia.c:6664-6676).
  • Fail-closed challenges. Under alwaysauthreject, an unknown peer gets a fresh-nonce, timing-equalized 401 indistinguishable from a known-peer bad password; allowguest=no returns 403 to an unknown caller rather than routing it (toll-fraud) (channels/chan_sofia.c:5471-5486). insecure=invite is overridable by force_invite_auth, which raises an auditable InsecureInviteBypass AMI event (channels/chan_sofia.c:5434-5455).

Access control

  • Three independent ACL chains per peer plus a global localha: source permit/deny (applied to the inbound INVITE before auth, with a timing-equalized 403 so it is not a peer-existence oracle), contactpermit/contactdeny (REGISTER Contact gate), and directmediapermit/directmediadeny (applied cross-leg before a direct-media re-INVITE). channels/chan_sofia.c:2868-2892, :5362-5388, :3143-3178.
  • Multi-Contact REGISTER preflight. A REGISTER carrying several Contacts is validated in full before any binding is applied, so a later fail-closed Contact cannot leave earlier ones partially bound (channels/chan_sofia.c:6343-6347).
  • Local anti-abuse SIP blacklist. Abusive source IPs (failed-auth probes, malformed requests, unknown-peer REGISTERs) are counted and banned to blunt brute-force and scanning (channels/sofia/sofia_blacklist.c, checked at channels/chan_sofia.c:10701).
  • REGISTER expiry bounds. Minimum/maximum lifetimes are enforced (423 Interval Too Brief + Min-Expires), which blunts registration-flood abuse (channels/chan_sofia.c:2325).

Transport security (TLS / WSS)

  • TLS hardening knobs on the TLS listener: tlsverify (verify server cert), tlsverifyclient (mutual TLS / client cert), tls_min_version (1.0–1.3 floor), tls_ciphers, tls_verify_depth, and a verify-date check. This closes the accept-any-cert MITM hole sofia-sip leaves open by default (channels/chan_sofia.c:11647-11681).
  • Modern TLS floor by default. tls_min_version defaults to 1.2, so the TLS/WSS listener negotiates TLS 1.2 or 1.3 out of the box. The former 1.3-only default rejected otherwise-valid endpoints that cap at TLS 1.2; the 1.2 floor restores that interoperability without admitting the deprecated TLS 1.0/1.1, and a deployment that requires 1.3-only can still raise the floor explicitly (channels/chan_sofia.c:11647-11681).
  • WSS builds its own SSL context and auto-aliases the WSS cert/ca-bundle files from the standard TLS material (channels/chan_sofia.c:11611-11632).

Media security

  • SRTP/SDES, validated and staged. a=crypto is validated in full (no truncation) and staged, then committed only past every reject gate, so a bad a=crypto never silently downgrades a live encrypted call to plaintext (channels/sofia/sdp_crypto.c, commit point in channels/sofia/sofia_sdp.c).
  • Deferred SRTP keying. Crypto is validated/staged with defer=1 but the live add_srtp_policy is withheld until the single commit block runs after the gates — so a re-INVITE that would be rejected can never have touched the live keys (channels/sofia/sofia_sdp.c:780-793, :1882-1918).
  • DTLS-SRTP trust anchored on the fingerprint, fail-closed (RFC 8827/5763). The self-signed WebRTC cert cannot chain-verify, so the SDP a=fingerprint is the sole trust anchor: on handshake completion the engine demands a peer cert, requires a stored remote fingerprint, and does a hash-matched X509_digest + memcmp; any absence or mismatch tears the call down as a possible MITM (res/res_rtp_gabpbx.c:1097-1120). When GABPBX is the offerer, a DTLS record arriving before the answer's a=fingerprint is installed is deferred (DTLS retransmits) rather than completing against no anchor — the verification still happens, never bypassed, once the fingerprint is in place (RFC 5763/8842).
  • Anti-downgrade SDP gating (RFC 5763/8842). A SAVPF/DTLS offer to a non-WebRTC peer is rejected rather than answered as plain RTP/AVP, a DTLS video offer in a non-WebRTC-audio session is rejected, and on the offerer leg a plain RTP/AVP answer to a DTLS-SRTP offer is rejected — so media never connects silently insecure (channels/sofia/sofia_sdp.c:1840-1866).
  • Encryption never bridged. Direct media is force-disabled on any SRTP/DTLS leg, so keys are never relayed to a non-secure peer (channels/chan_sofia.c:3129-3134).
  • RFC 7983 demux with a flood guard on the muxed WebRTC socket, dormant unless DTLS is active (res/res_rtp_gabpbx.c:1732-1786).
  • RED (RFC 2198) redundant-text sender, memory-safety hardened. The T.140 redundant-text generator bounds-checks its per-generation block arrays, clamps or rejects any block larger than the RFC 2198 1023-byte length limit, and caps the redundancy buffer, so a malformed or oversized redundant payload can never overrun the send buffer (res/res_rtp_gabpbx.c).

Concurrency and robustness

The driver does not leave its concurrency model implicit.

  • One canonical lock order, documented as an authoritative invariant block at the top of the file: channel-lock -> pvt->lock -> peer-family, never inverted; every inline lock site cites that one block (channels/chan_sofia.c:70-113).
  • Single-owner thread model. One sofia_thread (the su_root event loop) owns all mutable peer/dialog signaling state, runs every nua_* callback and sofia_process_* handler, and runs config reload (dispatched onto it). Everyone else (PBX/bridge callbacks, dialplan, CLI/AMI, sched, qualify) only reads, under lock. A race exists only if a party runs off sofia_thread — which makes every concurrency question a per-party thread check (channels/chan_sofia.c:74-83).
  • Snapshot-under-lock off the Sofia thread. Any off-thread access to freeable peer/owner data must snapshot under lock first, because ast_string_field_set frees the old pool on growth — a lock-free off-thread read is a use-after-free. The codified idiom (copy into a local under peer->lock, release, then use) is at channels/chan_sofia.c:98-107.
  • Reload use-after-free fixes. A sip reload frees and rebuilds peer stringfields/ACLs while readers touch them; the reload writer holds peer->lock across the whole reset+repopulate span so readers never see a torn peer, and off-thread readers were converted to snapshot-under-lock. The reload posts work onto sofia_thread and blocks on a condvar with a 30 s deadline, so there is exactly one consumer of config state during a reload (channels/chan_sofia.c:14078-14112).
  • Dialog teardown-race mitigation. An in-dialog event carries the dialog as the handle magic, but a concurrent hangup can free it; the dispatcher re-validates the hmagic and pins a +1 ref for the whole event, and the destructor unbinds the handle (nua_handle_bind(nh, NULL)) before the async destroy, so a late event reads hmagic == NULL and short-circuits instead of dereferencing freed memory (channels/chan_sofia.c:108-112, :1643-1656).
  • Foreign-thread nua_* marshalling. Because Sofia-SIP's nua_*/su_* APIs are not safe from arbitrary threads, every off-thread operation that must touch the stack is marshalled onto sofia_thread via sofia_dispatch_to_root_thread (channels/chan_sofia.c:14045-14076).
  • Validate-then-commit SDP. A re-INVITE that fails any gate leaves the established call fully unchanged per RFC 3261 §14: all mutations are staged into locals or snapshotted and committed only past the last reject gate; the reject label restores snapshots and destroys any was-new instances (channels/sofia/sofia_sdp.c).
  • Bounded SDP build buffers. The generator uses a checked appender with a sticky overflow flag, so a generated offer/answer is never silently truncated into an invalid SDP; on overflow it returns NULL and callers handle it (channels/sofia/sofia_sdp.c:59-68).

How the posture was reached

The hardening is the product of a structured, repeated adversarial review — multiple robustness rounds plus a leak audit and a reload-UAF worklist, each run as independent read-only passes (a reject-path lens and a memory-safety/RFC lens) cross-reviewed to consensus, with every candidate bug grounded at file:line and RFC section. The review repeatedly caught defects in the first-draft fixes themselves.

A repeatable concurrency stress harness is the standing regression net: it floods the listener with INVITE + re-INVITE(hold) + BYE while hammering sip reload in a loop under DEBUG_THREADS, then asserts PASS/FAIL by PID stability, absence of deadlock/assert/segv in the log, and a clean core show locks. It is run after any change touching locks, reload, dialog/peer lifetime, or event dispatch.

Clone this wiki locally