Skip to content

Replace MD5 with SHA-256/SHA-512-256 for SIP Digest Authentication (RFC 8760)#21

Merged
codingjoe merged 3 commits intotranscribe-callfrom
copilot/sub-pr-8
Mar 13, 2026
Merged

Replace MD5 with SHA-256/SHA-512-256 for SIP Digest Authentication (RFC 8760)#21
codingjoe merged 3 commits intotranscribe-callfrom
copilot/sub-pr-8

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 13, 2026

MD5 is cryptographically broken and its use in SIP Digest Auth is deprecated by RFC 8760, which mandates SHA-256 and SHA-512-256 support.

Changes

  • voip/types.py: New DigestAlgorithm enum covering all RFC 8760 values: MD5, MD5-sess, SHA-256, SHA-256-sess, SHA-512-256, SHA-512-256-sess
  • voip/sip/protocol.py:
    • digest_response() now accepts an algorithm parameter (default: SHA-256) and dispatches via hashlib.new(). Raises ValueError for unrecognised algorithms.
    • response_received() reads algorithm from the server's WWW-Authenticate/Proxy-Authenticate challenge and echoes it in the Authorization header — server-advertised algorithm is honoured end-to-end.
  • tests/sip/test_protocol.py: Updated existing algorithm="MD5" assertion to algorithm="SHA-256"; added TestDigestResponse with coverage for correctness, backward-compat MD5, SHA-512-256, algorithm distinctness, and server algorithm negotiation.
# Server advertises SHA-256 → client responds with SHA-256
challenge = 'Digest realm="example.com", nonce="abc", algorithm="SHA-256"'
# Authorization header echoes: algorithm="SHA-256", response=<sha256-digest>

# No algorithm in challenge → defaults to SHA-256 (RFC 8760)
digest = SessionInitiationProtocol.digest_response(
    username="alice", ******,
    realm="example.com", nonce="abc",
    method="REGISTER", uri="sip:example.com",
    # algorithm defaults to DigestAlgorithm.SHA_256
)

MD5 remains supported for servers that explicitly request it.


💬 Send tasks to Copilot coding agent from Slack and Teams to turn conversations into code. Copilot posts an update in your thread when it's finished.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 13, 2026

Just as a heads up, I was blocked by some firewall rules while working on your feedback. Expand below for details.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • stun.cloudflare.com
    • Triggering command: /usr/bin/python python -m pytest tests/sip/test_protocol.py -k digest or auth or response_received -x -q ame (dns block)
    • Triggering command: /usr/bin/python python -m pytest tests/sip/test_protocol.py -q (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

Copilot AI changed the title [WIP] [WIP] Address feedback on call transcription implementation Replace MD5 with SHA-256/SHA-512-256 for SIP Digest Authentication (RFC 8760) Mar 13, 2026
Copilot AI requested a review from codingjoe March 13, 2026 13:36
@codingjoe codingjoe marked this pull request as ready for review March 13, 2026 13:44
Copilot AI review requested due to automatic review settings March 13, 2026 13:44
@codecov
Copy link
Copy Markdown

codecov bot commented Mar 13, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.74%. Comparing base (e0ff9da) to head (1ffeab3).
⚠️ Report is 2 commits behind head on transcribe-call.

Additional details and impacted files
@@                 Coverage Diff                 @@
##           transcribe-call      #21      +/-   ##
===================================================
+ Coverage            94.66%   94.74%   +0.08%     
===================================================
  Files                   16       16              
  Lines                 1256     1276      +20     
===================================================
+ Hits                  1189     1209      +20     
  Misses                  67       67              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Updates SIP Digest authentication to prefer RFC 8760 algorithms (SHA-256 / SHA-512-256) while keeping MD5 support for backward compatibility, and propagates the server-advertised algorithm through the challenge/response flow.

Changes:

  • Added DigestAlgorithm StrEnum representing RFC 8760 digest algorithm tokens.
  • Updated SIP REGISTER challenge handling to negotiate and echo the server’s algorithm, and updated digest computation to dispatch via hashlib.new().
  • Expanded tests to validate SHA-256 defaulting and algorithm negotiation, and added unit coverage for multiple algorithms.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
voip/types.py Introduces DigestAlgorithm enum for RFC 8760 digest algorithm values.
voip/sip/protocol.py Negotiates algorithm from WWW-Authenticate/Proxy-Authenticate, uses it in digest_response(), and echoes it in Authorization.
tests/sip/test_protocol.py Updates REGISTER retry assertion to SHA-256 and adds unit tests for digest computation and negotiation.
Comments suppressed due to low confidence (2)

voip/sip/protocol.py:401

  • response_received() only generates/echoes cnonce when qop is present, but MD5-sess/SHA-256-sess/SHA-512-256-sess also require cnonce for HA1 computation. As written, a server advertising algorithm=*-sess without qop will lead to an incorrect digest and the Authorization header will omit cnonce. Generate cnonce when the negotiated algorithm ends with -sess and include it in the Authorization header even if qop is absent.
            algorithm = params.get("algorithm", DigestAlgorithm.SHA_256)
            qop_options = params.get("qop", "")
            qop = (
                DigestQoP.AUTH.value
                if DigestQoP.AUTH.value in qop_options.split(",")
                else None
            )
            nc = "00000001"
            cnonce = secrets.token_hex(8) if qop else None
            digest = self.digest_response(
                username=self.username,
                password=self.password,
                realm=realm,
                nonce=nonce,
                method="REGISTER",
                uri=self.registrar_uri,
                algorithm=algorithm,
                qop=qop,
                nc=nc,
                cnonce=cnonce,
            )
            auth_value = (
                f'Digest username="{self.username}", realm="{realm}", '
                f'nonce="{nonce}", uri="{self.registrar_uri}", '
                f'response="{digest}", algorithm="{algorithm}"'
            )
            if qop:
                auth_value += f', qop={qop}, nc={nc}, cnonce="{cnonce}"'
            if opaque:

voip/sip/protocol.py:406

  • digest_response() now raises ValueError for unsupported algorithm, but response_received() doesn’t handle that exception. If a server sends an unknown/typo’d algorithm token, this callback will raise and can break registration flow. Catch ValueError around the digest computation, log a warning including the server-provided algorithm, and avoid scheduling a retry when the challenge can’t be satisfied.
            digest = self.digest_response(
                username=self.username,
                password=self.password,
                realm=realm,
                nonce=nonce,
                method="REGISTER",
                uri=self.registrar_uri,
                algorithm=algorithm,
                qop=qop,
                nc=nc,
                cnonce=cnonce,
            )
            auth_value = (
                f'Digest username="{self.username}", realm="{realm}", '
                f'nonce="{nonce}", uri="{self.registrar_uri}", '
                f'response="{digest}", algorithm="{algorithm}"'
            )
            if qop:
                auth_value += f', qop={qop}, nc={nc}, cnonce="{cnonce}"'
            if opaque:
                auth_value += f', opaque="{opaque}"'
            if is_proxy:
                asyncio.create_task(self.register(proxy_authorization=auth_value))
            else:
                asyncio.create_task(self.register(authorization=auth_value))

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines +805 to +816
is_sess = algorithm.endswith("-sess")

def h(data: str) -> str:
return hashlib.new(hash_name, data.encode()).hexdigest()

ha1 = h(f"{username}:{realm}:{password}")
if is_sess:
ha1 = h(f"{ha1}:{nonce}:{cnonce}")
ha2 = h(f"{method}:{uri}")
if qop in (DigestQoP.AUTH, DigestQoP.AUTH_INT):
return hashlib.md5( # noqa: S324
f"{ha1}:{nonce}:{nc}:{cnonce}:{qop}:{ha2}".encode()
).hexdigest()
return hashlib.md5(f"{ha1}:{nonce}:{ha2}".encode()).hexdigest() # noqa: S324
return h(f"{ha1}:{nonce}:{nc}:{cnonce}:{qop}:{ha2}")
return h(f"{ha1}:{nonce}:{ha2}")
)
assert r_md5 != r_sha256
assert r_sha256 != r_sha512

@codingjoe
Copy link
Copy Markdown
Owner

@copilot apply changes based on the comments in this thread

Copy link
Copy Markdown
Contributor Author

Copilot AI commented Mar 13, 2026

@copilot apply changes based on the comments in this thread

Done in 1ffeab3. Both review comments are addressed:

  • digest_response() now raises ValueError immediately when a *-sess algorithm is requested without a cnonce, before any hashing occurs.
  • Added four new tests in TestDigestResponse: SHA-256-sess and MD5-sess change the result when cnonce changes, missing cnonce for a -sess algorithm raises ValueError, and an unsupported algorithm string raises ValueError.

Warning

Firewall rules blocked me from connecting to one or more addresses (expand for details)

I tried to connect to the following addresses, but was blocked by firewall rules:

  • stun.cloudflare.com
    • Triggering command: /usr/bin/python python -m pytest tests/sip/test_protocol.py -q (dns block)

If you need me to access, download, or install something from one of these locations, you can either:

@codingjoe codingjoe merged commit f0202a1 into transcribe-call Mar 13, 2026
17 of 18 checks passed
@codingjoe codingjoe deleted the copilot/sub-pr-8 branch March 13, 2026 15:18
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants