Skip to content

Auth & security

Authentication and the security-agreement machinery: SIP digest and IMS-AKA challenges, P-CSCF IPsec sec-agree (3GPP TS 33.203 / RFC 3329), and STIR/SHAKEN signing and verification.

from siphon import auth

@proxy.on_request("INVITE")
def route(request):
    if not auth.verify_digest(request, "example.com"):
        auth.require_proxy_digest(request, "example.com")
        return
    request.relay()

auth namespace

Mock authentication namespace.

Control auth behavior in tests::

from siphon import auth
auth._allow = True   # all auth checks pass
auth._allow = False  # all auth checks fail (challenge sent)

add_user

add_user(realm: str, username: str, password: str) -> None

Add credentials for testing (test helper).

Parameters:

Name Type Description Default
realm str

Auth realm (e.g. "example.com").

required
username str

Username.

required
password str

Password.

required

require_www_digest

require_www_digest(
    request: Any, realm: Optional[str] = None
) -> bool

Challenge with 401 WWW-Authenticate, or verify existing credentials.

If credentials are valid: sets request.auth_user, returns True. Otherwise: sends 401 response, returns False.

Parameters:

Name Type Description Default
request Any

The SIP request.

required
realm Optional[str]

Auth realm (e.g. "example.com").

None

Returns:

Type Description
bool

True if authenticated, False if challenge was sent.

require_proxy_digest

require_proxy_digest(
    request: Any, realm: Optional[str] = None
) -> bool

Challenge with 407 Proxy-Authenticate.

Same as :meth:require_www_digest but uses 407.

Parameters:

Name Type Description Default
request Any

The SIP request.

required
realm Optional[str]

Auth realm.

None

require_digest

require_digest(
    request: Any, realm: Optional[str] = None
) -> bool

Convenience alias for :meth:require_www_digest.

require_ims_digest

require_ims_digest(
    request: Any, realm: Optional[str] = None
) -> bool

IMS digest authentication via Diameter Cx MAR/MAA.

Sends a Multimedia-Auth-Request to the HSS and uses the returned authentication vector to challenge or verify the UE.

Returns:

Type Description
bool

True if credentials are valid, False if a 401 challenge was sent.

require_aka_digest

require_aka_digest(
    request: Any, realm: Optional[str] = None
) -> bool

IMS AKA digest authentication using local Milenage credentials.

Uses locally-configured K/OP/AMF credentials (from auth.aka_credentials in siphon.yaml) to generate AKA authentication vectors — no Diameter HSS connection needed. The nonce contains base64(RAND || AUTN) per 3GPP TS 33.203, and CK/IK are derived for IPsec SA creation.

Example::

if not auth.require_aka_digest(request, realm="ims.test"):
    log.info("sent 401 AKA challenge")
    return

Returns:

Type Description
bool

True if credentials are valid, False if a 401 challenge was sent.

verify_digest

verify_digest(
    request: Any, realm: Optional[str] = None
) -> bool

Verify credentials without sending a challenge.

Returns:

Type Description
bool

True if valid credentials are present.

ipsec namespace

P-CSCF IPsec security association management for the sec-agree handshake.

Mock :class:Ipsec namespace.

set_allocate_failure

set_allocate_failure(
    exc_type: Optional[type[BaseException]],
    message: str = "mock allocate failure",
) -> None

Configure the next allocate() call to raise exc_type.

Pass None to clear and let allocate succeed again.

set_pcscf_families

set_pcscf_families(
    v4: bool = True, v6: bool = True
) -> None

Model which address families this P-CSCF has a listener for.

Both default to available. Set one to False to model a single-stack P-CSCF so allocate() raises for a UE of the missing family, exactly as the Rust binding does.

SecurityOffer

A Security-Client offer parsed from a REGISTER (request.parse_security_client()).

Mock :class:SecurityOffer — UE-side IPsec proposal.

Transform

An operator-policy transform choice (Transform.HmacSha1_96Null, …).

Mock :class:Transform enum — operator policy choice.

AuthVectorHandle

The opaque CK/IK container produced by reply.take_av().

Mock :class:AuthVectorHandle — opaque CK/IK container.

The bytes are not exposed to Python in the real binding; the mock keeps them accessible via _ck/_ik for tests, but treats them as consumed after one allocate.

PendingSA

An allocated-but-not-yet-active SA pair, returned by ipsec.allocate(...).

Mock :class:PendingSA.

activate

activate(
    *, hard_lifetime_secs: Optional[int] = None
) -> None

Mark the SA pair active.

hard_lifetime_secs (optional) re-pins the kernel hard-lifetime on all four SAs of the pair via XFRM_MSG_UPDSA, without rekeying or disturbing selectors / SPIs. Use on the path that processes the 200 OK to the auth REGISTER to tighten the SA expiry from the placeholder value installed at allocation time (typically the UE's Expires: ask) to the actual grant from the registrar of record (3GPP TS 33.203 §7.4 — IPsec SA lifetime tracks SIP registration lifetime).

None (default) preserves the original metadata-only transition.

In the mock, this only updates self.expires_secs so tests can assert the script wired the grant through correctly.

SecurityServerParams

The Security-Server parameters to echo back to the UE.

Mock :class:SecurityServerParams.

SAHandle

A read-only view of the active SA that decrypted a request (request.matched_sa).

Mock :class:SAHandle — read-only view of an active SA returned by request.matched_sa. Tests can construct one directly and assign it to request._matched_sa.

stir namespace

STIR/SHAKEN Identity-header signing and verification.

Mock stir namespace — STIR/SHAKEN signing and verification.

Scripts use::

from siphon import stir

@proxy.on_request("INVITE")
def on_invite(request):
    origid = stir.sign(request, attestation="A")   # add Identity header
    request.relay()

@proxy.on_request("INVITE")
def verify_inbound(request):
    result = stir.verify(request)
    if result.verstat == "TN-Validation-Failed":
        request.reply(438, "Invalid Identity Header")
        return
    stir.apply_verstat(request, result)
    request.relay()

Test helpers: set :attr:signing_enabled / :attr:verification_enabled to simulate config; call :meth:set_verify_result to pin the next :meth:verify outcome; inspect :attr:signed / :attr:applied_verstats.

sign

sign(
    request: Any,
    attestation: str = "A",
    origid: Optional[str] = None,
    orig_tn: Optional[str] = None,
    dest_tn: Optional[str] = None,
) -> str

Build a SHAKEN Identity header and add it to request.

Parameters:

Name Type Description Default
request Any

The outbound SIP request.

required
attestation str

"A" / "B" / "C" (full / partial / gateway).

'A'
origid Optional[str]

UUID origin identifier; a fresh v4 is generated if None.

None
orig_tn Optional[str]

Originating TN; defaults to the From user part.

None
dest_tn Optional[str]

Destination TN; defaults to the To / R-URI user part.

None

Returns:

Type Description
str

The origid used.

Raises:

Type Description
RuntimeError

if signing is not configured.

ValueError

if the orig/dest TN cannot be determined.

sign_div

sign_div(
    request: Any,
    orig_tn: Optional[str] = None,
    dest_tn: Optional[str] = None,
    div_tn: Optional[str] = None,
) -> None

Build a diverted-call (div) Identity header (RFC 8946).

Parameters:

Name Type Description Default
request Any

The outbound (retargeted) SIP request.

required
orig_tn Optional[str]

Originating TN; defaults to the From user part.

None
dest_tn Optional[str]

New destination TN; defaults to the To / R-URI user part.

None
div_tn Optional[str]

Diverting TN; defaults to the History-Info / Diversion user.

None

verify

verify(request: Any) -> MockStirResult

Verify the Identity header(s) on request.

Returns a :class:MockStirResult. By default returns a passing result when an Identity header is present, else No-TN-Validation; override with :meth:set_verify_result.

Raises:

Type Description
RuntimeError

if verification is not configured.

apply_verstat

apply_verstat(request: Any, result: MockStirResult) -> None

Stamp the verstat parameter onto the asserted identity (P-Asserted-Identity if present, else From) per ATIS-1000074 §5.3.1.

set_verify_result

set_verify_result(
    verstat: str = "TN-Validation-Passed",
    passed: bool = True,
    attestation: Optional[str] = None,
    origid: Optional[str] = None,
    orig_tn: Optional[str] = None,
    reason: str = "ok",
    passports: Optional[list[dict[str, Any]]] = None,
) -> None

Pin the result returned by the next :meth:verify call(s).

StirResult

The outcome of stir.verify(...).

Result of :meth:MockStir.verify — mirrors the Rust StirResult.

Attributes:

Name Type Description
verstat

"TN-Validation-Passed" | "TN-Validation-Failed" | "No-TN-Validation" (ATIS-1000074 §5.3.1).

passed

True only when the SHAKEN PASSporT validated end to end.

attestation

"A" / "B" / "C" from the SHAKEN PASSporT.

origid

origid (UUID) from the SHAKEN PASSporT.

orig_tn

originating TN from the SHAKEN PASSporT.

reason

human-readable diagnostic / failure cause.

passports list[dict[str, Any]]

decoded PASSporT claim dicts.

passports property

passports: list[dict[str, Any]]

Decoded claim sets of every PASSporT that parsed.