Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 13 additions & 2 deletions src/elevenlabs/speech_engine/resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import hmac
import json
import logging
import re
import time
import typing

Expand All @@ -18,6 +19,17 @@
_ISSUER = "https://api.elevenlabs.io/convai/speech-engine"
_SUBJECT = "convai_speech_engine_upstream"
_LEEWAY_SECONDS = 60
_RESIDENCY_KEY_SUFFIX = re.compile(r"_residency_[a-z0-9]+$")


def _normalize_api_key_for_signing(api_key: str) -> str:
"""Trim the key and drop any ``_residency_<region>`` suffix.

The API signs Speech Engine JWTs with the SHA-256 of the *base* key, so a
data-residency key such as ``sk_..._residency_in`` must have its suffix
removed before it is hashed here.
"""
return _RESIDENCY_KEY_SUFFIX.sub("", api_key.strip())


def _base64url_decode(data: str) -> bytes:
Expand Down Expand Up @@ -49,8 +61,7 @@ def verify_speech_engine_jwt(value: str, api_key: str) -> typing.Dict[str, typin
except Exception:
raise ValueError("Invalid JWT: failed to decode payload")

trimmed_key = api_key.strip()
secret = hashlib.sha256(trimmed_key.encode("utf-8")).digest()
secret = hashlib.sha256(_normalize_api_key_for_signing(api_key).encode("utf-8")).digest()

expected_sig = hmac.new(
secret, f"{header_b64}.{payload_b64}".encode(), hashlib.sha256
Expand Down
11 changes: 11 additions & 0 deletions tests/test_speech_engine_auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,17 @@ def test_rejects_wrong_key(self) -> None:
with pytest.raises(ValueError, match="signature mismatch"):
verify_speech_engine_jwt(token, TEST_API_KEY)

def test_strips_residency_suffix_before_hashing(self) -> None:
# The API signs with the base key; the server may hold the full residency key.
token = _create_test_jwt(_valid_payload(), api_key="sk_abc123")
payload = verify_speech_engine_jwt(token, "sk_abc123_residency_in")
assert payload["iss"] == JWT_ISSUER

def test_rejects_residency_key_with_wrong_base(self) -> None:
token = _create_test_jwt(_valid_payload(), api_key="sk_other")
with pytest.raises(ValueError, match="signature mismatch"):
verify_speech_engine_jwt(token, "sk_abc123_residency_eu")

def test_rejects_wrong_issuer(self) -> None:
token = _create_test_jwt(_valid_payload(iss="https://evil.com"))
with pytest.raises(ValueError, match="expected issuer"):
Expand Down