7 months ago
Hello .
I’m experiencing an issue with the hmac and hashlib modules in my Python 3.11 service, and I’d appreciate your help in understanding what might be happening.
Problem:
The standard hmac.new() function (and manual HMAC implementations using hashlib.sha256) are not producing the expected SHA256 HMAC hashes, even in a clean Docker environment (python:3.11-slim). This breaks Telegram bot authentication, which relies on correct HMAC-SHA256 verification.
What I’ve tried:
Using uvicorn with a custom start.py to correctly read $PORT
Building with a Dockerfile to ensure a clean Python environment
Testing HMAC logic in isolation — it consistently returns incorrect results
Verified inputs (secret_key, data_check_string) are correct
Despite everything, hmac.new(key, msg, hashlib.sha256).hexdigest() returns a hash that does not match the one expected by Telegram.
Environment:
Runtime: Docker (python:3.11-slim)
Custom start.py used to avoid $PORT issues
hmac, hashlib, and sha256 appear to be available, but results are incorrect
Question:
Is there any known issue or patch applied to the Python environment (e.g., in the base image, musl, or C libraries) that could affect the behavior of hmac or hashlib.sha256?
Could such cryptographic functions be modified or replaced in any way in the underlying build environment?
Any insight would be very helpful. I’d love to keep using Railway, but this issue is blocking a core part of my app.
1 Replies
15 days ago
Railway is not replacing Python's hmac or hashlib. HMAC-SHA256 is deterministic, so the same key bytes and message bytes in python:3.11-slim will produce the same digest locally and on Railway. This mismatch is almost certainly in the Telegram validation recipe or in the exact bytes being signed.
First confirm the standard library with a non-secret test vector in both environments:
import hashlib, hmac
print(hmac.new(b"key", b"message", hashlib.sha256).hexdigest())The result must be:
6e9ef29b75fffc5b7abae527d58fdadb2fe42e7219011976917343065f58ed4aTelegram has two similar-looking but different HMAC recipes. Mixing them is a common cause of this exact symptom.
For Mini App Telegram.WebApp.initData, the bot token is HMACed using WebAppData as the key:
from urllib.parse import parse_qsl
import hashlib
import hmac
def valid_mini_app_init_data(raw_init_data: str, bot_token: str) -> bool:
pairs = parse_qsl(raw_init_data, keep_blank_values=True)
received_hash = next((value for key, value in pairs if key == "hash"), "")
fields = sorted((key, value) for key, value in pairs if key != "hash")
data_check_string = "\n".join(f"{key}={value}" for key, value in fields)
secret_key = hmac.new(
b"WebAppData",
bot_token.encode("utf-8"),
hashlib.sha256,
).digest()
calculated_hash = hmac.new(
secret_key,
data_check_string.encode("utf-8"),
hashlib.sha256,
).hexdigest()
return hmac.compare_digest(calculated_hash, received_hash)For the legacy Telegram Login Widget, the secret is instead the plain SHA256 digest of the bot token:
secret_key = hashlib.sha256(bot_token.encode("utf-8")).digest()
calculated_hash = hmac.new(
secret_key,
data_check_string.encode("utf-8"),
hashlib.sha256,
).hexdigest()Telegram documents those two different derivations here:
- Mini Apps: https://core.telegram.org/bots/webapps#validating-data-received-via-the-mini-app
- Legacy Login Widget: https://core.telegram.org/widgets/login#checking-authorization
Also check these byte-level details:
- Build the check string from every received field except
hash, sorted by key and joined with a literal line feed (\n). Current Mini App data can contain asignaturefield; include it in this bot-token HMAC check. Removing bothhashandsignatureis only for Telegram's separate third-party Ed25519 validation flow. - URL-decode the raw query string exactly once. Do not parse the
userJSON and serialize it again, because whitespace/escaping changes the bytes. - Strip no characters from field values. Only remove an accidental newline around the bot-token environment variable if one exists.
- Compare with
hmac.compare_digest, and reject staleauth_datevalues after the signature is valid.
Do not post the bot token or the full production initData. If the non-secret test vector matches in both environments but Telegram still fails, print only repr(data_check_string), its byte length, and which of the two Telegram flows you are validating; that will expose an encoding/recipe mismatch without exposing the token.