mirror of
https://github.com/aiogram/aiogram.git
synced 2026-04-08 16:37:47 +00:00
Support validating init data using only bot id. (#1715)
* Support validating init data using only bot id. * Add changes file. * Add tests. * Install `signature` dependencies in CI.
This commit is contained in:
parent
ab8af773cf
commit
6aa6e008c2
5 changed files with 147 additions and 10 deletions
20
.github/workflows/tests.yml
vendored
20
.github/workflows/tests.yml
vendored
|
|
@ -29,11 +29,11 @@ jobs:
|
||||||
- macos-latest
|
- macos-latest
|
||||||
- windows-latest
|
- windows-latest
|
||||||
python-version:
|
python-version:
|
||||||
- '3.9'
|
- "3.9"
|
||||||
- '3.10'
|
- "3.10"
|
||||||
- '3.11'
|
- "3.11"
|
||||||
- '3.12'
|
- "3.12"
|
||||||
- '3.13'
|
- "3.13"
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
# Windows sucks. Force use bash instead of PowerShell
|
# Windows sucks. Force use bash instead of PowerShell
|
||||||
|
|
@ -63,7 +63,7 @@ jobs:
|
||||||
|
|
||||||
- name: Install project dependencies
|
- name: Install project dependencies
|
||||||
run: |
|
run: |
|
||||||
pip install -e .[dev,test,redis,mongo,proxy,i18n,fast]
|
pip install -e .[dev,test,redis,mongo,proxy,i18n,fast,signature]
|
||||||
|
|
||||||
- name: Lint code
|
- name: Lint code
|
||||||
run: |
|
run: |
|
||||||
|
|
@ -81,7 +81,7 @@ jobs:
|
||||||
if: ${{ env.IS_UBUNTU == 'true' }}
|
if: ${{ env.IS_UBUNTU == 'true' }}
|
||||||
uses: supercharge/mongodb-github-action@1.10.0
|
uses: supercharge/mongodb-github-action@1.10.0
|
||||||
with:
|
with:
|
||||||
mongodb-version: '7.0'
|
mongodb-version: "7.0"
|
||||||
mongodb-username: mongo
|
mongodb-username: mongo
|
||||||
mongodb-password: mongo
|
mongodb-password: mongo
|
||||||
mongodb-port: 27017
|
mongodb-port: 27017
|
||||||
|
|
@ -111,8 +111,8 @@ jobs:
|
||||||
- macos-latest
|
- macos-latest
|
||||||
# - windows-latest
|
# - windows-latest
|
||||||
python-version:
|
python-version:
|
||||||
- 'pypy3.9'
|
- "pypy3.9"
|
||||||
- 'pypy3.10'
|
- "pypy3.10"
|
||||||
|
|
||||||
defaults:
|
defaults:
|
||||||
# Windows sucks. Force use bash instead of PowerShell
|
# Windows sucks. Force use bash instead of PowerShell
|
||||||
|
|
@ -134,7 +134,7 @@ jobs:
|
||||||
|
|
||||||
- name: Install project dependencies
|
- name: Install project dependencies
|
||||||
run: |
|
run: |
|
||||||
pip install -e .[dev,test,redis,mongo,proxy,i18n,fast]
|
pip install -e .[dev,test,redis,mongo,proxy,i18n,fast,signature]
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: |
|
run: |
|
||||||
|
|
|
||||||
1
CHANGES/1715.feature.rst
Normal file
1
CHANGES/1715.feature.rst
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
Support validating init data using only bot id.
|
||||||
70
aiogram/utils/web_app_signature.py
Normal file
70
aiogram/utils/web_app_signature.py
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
import base64
|
||||||
|
from operator import itemgetter
|
||||||
|
from urllib.parse import parse_qsl
|
||||||
|
|
||||||
|
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PublicKey
|
||||||
|
from cryptography.exceptions import InvalidSignature
|
||||||
|
|
||||||
|
from .web_app import parse_webapp_init_data, WebAppInitData
|
||||||
|
|
||||||
|
PRODUCTION_PUBLIC_KEY = bytes.fromhex(
|
||||||
|
"e7bf03a2fa4602af4580703d88dda5bb59f32ed8b02a56c187fe7d34caed242d"
|
||||||
|
)
|
||||||
|
TEST_PUBLIC_KEY = bytes.fromhex("40055058a4ee38156a06562e52eece92a771bcd8346a8c4615cb7376eddf72ec")
|
||||||
|
|
||||||
|
|
||||||
|
def check_webapp_signature(
|
||||||
|
bot_id: int, init_data: str, public_key_bytes: bytes = PRODUCTION_PUBLIC_KEY
|
||||||
|
) -> bool:
|
||||||
|
"""
|
||||||
|
Check incoming WebApp init data signature without bot token using only bot id.
|
||||||
|
|
||||||
|
Source: https://core.telegram.org/bots/webapps#validating-data-for-third-party-use
|
||||||
|
|
||||||
|
:param bot_id: Bot ID
|
||||||
|
:param init_data: WebApp init data
|
||||||
|
:param public_key: Public key
|
||||||
|
:return: True if signature is valid, False otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
parsed_data = dict(parse_qsl(init_data, strict_parsing=True))
|
||||||
|
except ValueError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
signature_b64 = parsed_data.pop("signature", None)
|
||||||
|
if not signature_b64:
|
||||||
|
return False
|
||||||
|
|
||||||
|
parsed_data.pop("hash", None)
|
||||||
|
|
||||||
|
data_check_string = f"{bot_id}:WebAppData\n" + "\n".join(
|
||||||
|
f"{k}={v}" for k, v in sorted(parsed_data.items(), key=itemgetter(0))
|
||||||
|
)
|
||||||
|
message = data_check_string.encode()
|
||||||
|
|
||||||
|
padding = "=" * (-len(signature_b64) % 4)
|
||||||
|
signature = base64.urlsafe_b64decode(signature_b64 + padding)
|
||||||
|
|
||||||
|
public_key = Ed25519PublicKey.from_public_bytes(public_key_bytes)
|
||||||
|
|
||||||
|
try:
|
||||||
|
public_key.verify(signature, message)
|
||||||
|
return True
|
||||||
|
except InvalidSignature:
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def safe_check_webapp_init_data_from_signature(
|
||||||
|
bot_id: int, init_data: str, public_key_bytes: bytes = PRODUCTION_PUBLIC_KEY
|
||||||
|
) -> WebAppInitData:
|
||||||
|
"""
|
||||||
|
Validate raw WebApp init data using only bot id and return it as WebAppInitData object
|
||||||
|
|
||||||
|
:param bot_id: bot id
|
||||||
|
:param init_data: data from frontend to be parsed and validated
|
||||||
|
:param public_key_bytes: public key
|
||||||
|
:return: WebAppInitData object
|
||||||
|
"""
|
||||||
|
if check_webapp_signature(bot_id, init_data, public_key_bytes):
|
||||||
|
return parse_webapp_init_data(init_data)
|
||||||
|
raise ValueError("Invalid init data signature")
|
||||||
|
|
@ -74,6 +74,9 @@ i18n = [
|
||||||
cli = [
|
cli = [
|
||||||
"aiogram-cli>=1.1.0,<2.0.0",
|
"aiogram-cli>=1.1.0,<2.0.0",
|
||||||
]
|
]
|
||||||
|
signature = [
|
||||||
|
"cryptography>=43.0.0",
|
||||||
|
]
|
||||||
test = [
|
test = [
|
||||||
"pytest~=7.4.2",
|
"pytest~=7.4.2",
|
||||||
"pytest-html~=4.0.2",
|
"pytest-html~=4.0.2",
|
||||||
|
|
|
||||||
63
tests/test_utils/test_web_app_signature.py
Normal file
63
tests/test_utils/test_web_app_signature.py
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from aiogram.utils.web_app import WebAppInitData
|
||||||
|
from aiogram.utils.web_app_signature import (
|
||||||
|
check_webapp_signature,
|
||||||
|
safe_check_webapp_init_data_from_signature,
|
||||||
|
)
|
||||||
|
|
||||||
|
PRIVATE_KEY = bytes.fromhex("c80e09dc60f5efcf2e1f8d0793358e0ea3371267bef0024588f7bf67cf48dfb9")
|
||||||
|
PUBLIC_KEY = bytes.fromhex("4112765021341e5415e772cd65903f6b94e3ea1c2ab669e6d3e18ee2db00da61")
|
||||||
|
|
||||||
|
|
||||||
|
class TestWebAppSignature:
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"bot_id,case,result",
|
||||||
|
[
|
||||||
|
[
|
||||||
|
42,
|
||||||
|
"auth_date=1650385342&user=%7B%22id%22%3A42%2C%22first_name%22%3A%22Test%22%7D&query_id=test&signature=JQ0JR2tjC65yq_jNZV0wuJVX6J-SWPMV0mprUXG34g-NvxL4RcF1Rz5n4VVo00VRghEUBf5t___uoeb1-jU_Cw",
|
||||||
|
True,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42,
|
||||||
|
"auth_date=1650385342&user=%7B%22id%22%3A42%2C%22first_name%22%3A%22Test%22%7D&query_id=test&signature=JQ0JR2tjC65yq_jNZV0wuJVX6J-SWPMV0mprUXG34g-NvxL4RcF1Rz5n4VVo00VRghEUBf5t___uoeb1-j1U_w",
|
||||||
|
False,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42,
|
||||||
|
"auth_date=1650385342&user=%7B%22id%22%3A42%2C%22first_name%22%3A%22Test%22%7D&query_id=test",
|
||||||
|
False,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
42,
|
||||||
|
"",
|
||||||
|
False,
|
||||||
|
],
|
||||||
|
[42, "test&foo=bar=baz", False],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_check_webapp_signature(self, bot_id: int, case: str, result: bool):
|
||||||
|
assert check_webapp_signature(bot_id, case, PUBLIC_KEY) is result
|
||||||
|
|
||||||
|
def test_safe_check_webapp_init_data_from_signature(self):
|
||||||
|
result = safe_check_webapp_init_data_from_signature(
|
||||||
|
42,
|
||||||
|
"auth_date=1650385342&user=%7B%22id%22%3A42%2C%22first_name%22%3A%22Test%22%7D&query_id=test&hash=123&signature=JQ0JR2tjC65yq_jNZV0wuJVX6J-SWPMV0mprUXG34g-NvxL4RcF1Rz5n4VVo00VRghEUBf5t___uoeb1-jU_Cw",
|
||||||
|
PUBLIC_KEY,
|
||||||
|
)
|
||||||
|
assert isinstance(result, WebAppInitData)
|
||||||
|
assert result.user is not None
|
||||||
|
assert result.user.id == 42
|
||||||
|
assert result.user.first_name == "Test"
|
||||||
|
assert result.query_id == "test"
|
||||||
|
assert result.auth_date.year == 2022
|
||||||
|
assert result.hash == "123"
|
||||||
|
|
||||||
|
def test_safe_check_webapp_init_data_from_signature_invalid(self):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
safe_check_webapp_init_data_from_signature(
|
||||||
|
42,
|
||||||
|
"auth_date=1650385342&user=%7B%22id%22%3A42%2C%22first_name%22%3A%22Test%22%7D&query_id=test&hash=123&signature=JQ0JR2tjC65yq_jNZV0wuJVX6J-SWPMV0mprUXG34g-NvxL4RcF1Rz5n4VVo00VRghEUBf5t___uoeb1-j1U_w",
|
||||||
|
PUBLIC_KEY,
|
||||||
|
)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue