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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Web Bot Auth Python

![GitHub License](https://img.shields.io/github/license/cloudflareresearch/web-bot-auth)
![GitHub License](https://img.shields.io/github/license/cyberstormdotmu/bot-authentication)
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)

Implementation of Web Bot Auth in Python.
Expand Down
6 changes: 5 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "bot-auth"
version = "0.2.2"
version = "0.3.2"
description = "A library to check for AI Bot Authentication using the latest HTTP header Signature."
readme = "README.md"
requires-python = ">=3.11"
Expand All @@ -15,6 +15,10 @@ dependencies = [
"http-message-signatures>=0.7.0",
"requests>=2.25.0",
]
[project.urls]
homepage = "https://github.com/cyberstormdotmu/Bot-Authentication"
source = "https://github.com/cyberstormdotmu/Bot-Authentication.git"
issues = "https://github.com/cyberstormdotmu/Bot-Authentication/issues"

[build-system]
requires = ["uv_build>=0.8.0,<0.9"]
Expand Down
48 changes: 38 additions & 10 deletions src/bot_auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
A library to check for AI Bot Authentication using the latest HTTP header Signature.
"""

__version__ = "0.1.0"
__version__ = "0.3.1"

import base64
import hashlib
import json
import requests
import time
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
Expand Down Expand Up @@ -49,15 +51,12 @@ class BotAuth:
- get_local_keys() -> -> list[dict[str, str]]
- get_remote_keys() -> list[dict[str, str]]
- get_header()-> dict[str, str]

@info:
© 2025 Atish Joottun
"""

def __init__(
self,
localKeys,
signAgent="http-message-signatures-example.research.cloudflare.com",
signAgent=None,
):
self.localKeys = localKeys
self.signAgent = signAgent
Expand Down Expand Up @@ -115,9 +114,30 @@ def _base64url_decode(self, val):
def _base64_encode_bytes(self, val):
return base64.b64encode(val).decode("ascii")

def _base64url_nopad_encode_bytes(self, val):
return base64.urlsafe_b64encode(val).decode("ascii").strip("=")

def _jwt_to_private_key(self, jwk):
return Ed25519PrivateKey.from_private_bytes(self._base64url_decode(jwk["d"]))

def _public_key_to_jwk_thumbprint(self, public_key):
"""
Compute the base64url JWK SHA-256 Thumbprint for an Ed25519 public key.
"""
# JWK Thumbprint according to RFC 7638, base64url with padding and sha256

jwk_dict = {
"crv": "Ed25519",
"kty": "OKP",
"x": self._base64url_nopad_encode_bytes(public_key.public_bytes_raw()),
}

jwk_json = json.dumps(jwk_dict, separators=(",", ":"), sort_keys=True)
sha256_hash = hashlib.sha256(jwk_json.encode("utf-8")).digest()
thumbprint = self._base64url_nopad_encode_bytes(sha256_hash)

return thumbprint

# def _jwk_to_public_key_bytes(self, jwk):
# private_key = self.jwk_to_private_key(jwk)
# public_key = private_key.public_key()
Expand All @@ -133,34 +153,42 @@ def get_bot_signature_header(self, url) -> dict[str, str]:
## Get similar key in local repo
selected_key = local_keys[0]

resolver = SingleKeyResolver(self._jwt_to_private_key(selected_key))
private_key = self._jwt_to_private_key(selected_key)
resolver = SingleKeyResolver(private_key)
signer = HTTPMessageSigner(
signature_algorithm=algorithms.ED25519, key_resolver=resolver
)

created = datetime.fromtimestamp(time.time())
expires = created + timedelta(minutes=5)

headers = {"Signature-Agent": self.signAgent} if self.signAgent else {}
request = requests.Request(
"GET",
url,
headers={
"Signature-Agent": self.signAgent,
**headers,
},
)

key_id = self._public_key_to_jwk_thumbprint(private_key.public_key())
covered_components = (
("@authority", "signature-agent") if self.signAgent else ["@authority"]
)
signer.sign(
request,
key_id="compute-jwk-thumbprint",
covered_component_ids=("@authority", "signature-agent"),
key_id=key_id,
covered_component_ids=covered_components,
created=created,
expires=expires,
tag="web-bot-auth",
label="sig1",
)

header = {
"Signature-Agent": request.headers["Signature-Agent"],
"Signature-Input": request.headers["Signature-Input"],
"Signature": request.headers["Signature"],
**headers,
}

return header
Expand Down
62 changes: 31 additions & 31 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.