Skip to content

Commit 5d3a122

Browse files
committed
fix: expires with utc datetime
1 parent 5e51ad3 commit 5d3a122

File tree

2 files changed

+46
-1
lines changed

2 files changed

+46
-1
lines changed

altcha/altcha.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import urllib.parse
1010
from typing import Literal, TypedDict, cast, overload
1111
import datetime
12+
from datetime import timezone
1213

1314
# Define algorithms
1415
SHA1: Literal["SHA-1"] = "SHA-1"
@@ -426,7 +427,16 @@ def create_challenge(
426427
salt_params = dict(urllib.parse.parse_qsl(salt_query))
427428

428429
if options.expires:
429-
salt_params["expires"] = str(int(time.mktime(options.expires.timetuple())))
430+
expires = options.expires
431+
432+
if expires.tzinfo is None:
433+
# Backward compatibility: assume naive datetimes are local time
434+
timestamp = int(time.mktime(expires.timetuple()))
435+
else:
436+
# Aware datetimes: use true UTC timestamp
437+
timestamp = int(expires.timestamp())
438+
439+
salt_params["expires"] = str(timestamp)
430440

431441
if options.params:
432442
salt_params.update(options.params)
@@ -723,3 +733,14 @@ def solve_challenge(
723733
return Solution(n, took)
724734

725735
return None
736+
737+
def _normalize_expires(expires: datetime.datetime | None) -> datetime.datetime | None:
738+
if expires is None:
739+
return None
740+
741+
# Case 1: naive datetime → assume UTC (backward compatibility)
742+
if expires.tzinfo is None:
743+
return expires.replace(tzinfo=timezone.utc)
744+
745+
# Case 2: aware datetime (any timezone) → convert to UTC
746+
return expires.astimezone(timezone.utc)

tests/test_altcha.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,30 @@ def test_verify_solution_max_number(self):
129129
result, _ = verify_solution(payload_encoded, self.hmac_key, check_expires=False)
130130
self.assertTrue(result)
131131

132+
def test_verify_solution_expires_utc(self):
133+
options = ChallengeOptions(
134+
algorithm="SHA-256",
135+
max_number=1000,
136+
salt_length=16,
137+
hmac_key=self.hmac_key,
138+
salt="somesalt",
139+
number=123,
140+
expires=datetime.datetime.now(datetime.timezone.utc),
141+
)
142+
challenge = create_challenge(options)
143+
payload = Payload(
144+
algorithm="SHA-256",
145+
challenge=challenge.challenge,
146+
number=123,
147+
salt=challenge.salt,
148+
signature=challenge.signature,
149+
)
150+
payload_encoded = base64.b64encode(
151+
json.dumps(payload.__dict__).encode()
152+
).decode()
153+
result, _ = verify_solution(payload_encoded, self.hmac_key, check_expires=True)
154+
self.assertFalse(result)
155+
132156
def test_verify_solution_not_expired(self):
133157
options = ChallengeOptions(
134158
algorithm="SHA-256",

0 commit comments

Comments
 (0)