Files
julian.kiedaischandClaude Opus 5.5 416d5fdea3 feat: F5.6c measured and accepted on test-filius; tickets up to 5 s ahead of F's clock
test-filius refused every ticket as from the future: its clock was 886 ms
behind the dashboard's machine, with NTP still 8 ms (one ticket in 125, as a
ticket carries whole seconds). F now takes a ticket signed up to 5 s ahead
(TICKET_AHEAD_SECONDS); itsdangerous refuses one second, and the oracle
keeps that on record.

tools/live measures and accepts on a live F, with fresh projects each run:
m3 (200 rooms of the largest example with pings: probe p95 37 ms), m4 (63 KB
document; dragging makes an update per pointer move, 59/s), m5 (an hour
through the proxy), acceptance (two browsers, 14 checks, passed three times)
and clock (F's clock against ours).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-04 19:22:22 +00:00

98 lines
3.8 KiB
Python

"""The dashboard's entry tickets, made by itsdangerous itself (plan §8.5).
The dashboard signs a ticket with itsdangerous' URLSafeTimedSerializer (the
same as Scratch's), server F checks it in Node. This writes tickets as the
dashboard makes them, with a fixed clock, and what itsdangerous answers when
it reads each one back at a given moment: the payload, or which error.
packages/server/test/tickets.python.json holds the result; the server's
tests read every ticket with the Node check and expect the same answer.
"""
import json
import sys
import itsdangerous
from itsdangerous import BadSignature, SignatureExpired, URLSafeTimedSerializer
from itsdangerous.timed import TimestampSigner
SECRET = "filius-test-secret"
SALT = "mdg-filius-ticket"
TTL = 60
SIGNED_AT = 1_790_000_000
clock = {"now": SIGNED_AT}
TimestampSigner.get_timestamp = lambda self: clock["now"]
PERSON = "0b8f5f2e-1a3c-4d5e-8f90-123456789abc"
PROJECT = "6f1d2c3b-4a59-4e8d-9c7b-a1b2c3d4e5f6"
def claims(**more):
base = {"u": PERSON, "n": "ab12", "d": "Ada Beispiel", "c": "#3366cc", "lang": "de", "p": PROJECT, "lvl": "write"}
base.update(more)
return base
def sign(payload, secret=SECRET, salt=SALT, at=SIGNED_AT):
clock["now"] = at
return URLSafeTimedSerializer(secret, salt=salt).dumps(payload)
def answer(ticket, now):
clock["now"] = now
try:
return {"payload": URLSafeTimedSerializer(SECRET, salt=SALT).loads(ticket, max_age=TTL)}
except SignatureExpired:
return {"error": "expired"}
except BadSignature:
return {"error": "badSignature"}
def tampered(ticket):
# One letter of the payload changed (the first after its first two).
letter = ticket[2]
return ticket[:2] + ("A" if letter != "A" else "B") + ticket[3:]
def main(out):
plain = sign(claims())
long_name = "Maximiliane Sophie Charlotte von und zu Beispielhausen-Musterstein"
cases = [
("plain", plain, SIGNED_AT),
("readLevel", sign(claims(lvl="read")), SIGNED_AT),
("umlautsAndEmoji", sign(claims(d="Jörg Ömer Weiß 🐧", lang="en")), SIGNED_AT),
("compressed", sign(claims(d=long_name, n=long_name.lower().replace(" ", "."))), SIGNED_AT),
("sixtySecondsOld", plain, SIGNED_AT + 60),
("sixtyOneSecondsOld", plain, SIGNED_AT + 61),
("fromTheFuture", plain, SIGNED_AT - 1),
("fiveSecondsAhead", plain, SIGNED_AT - 5),
("sixSecondsAhead", plain, SIGNED_AT - 6),
("otherSecret", sign(claims(), secret="another-secret"), SIGNED_AT),
("scratchSalt", sign(claims(), salt="mdg-scratch-ticket"), SIGNED_AT),
("tamperedPayload", tampered(plain), SIGNED_AT),
("noSignature", plain.rsplit(".", 1)[0], SIGNED_AT),
("garbage", "not a ticket", SIGNED_AT),
("empty", "", SIGNED_AT),
("listPayload", sign(["u", "p"]), SIGNED_AT),
("withoutLevel", sign({k: v for k, v in claims().items() if k != "lvl"}), SIGNED_AT),
("unknownLevel", sign(claims(lvl="admin")), SIGNED_AT),
("projectNotAUuid", sign(claims(p="../etc")), SIGNED_AT),
("withoutName", sign({k: v for k, v in claims().items() if k not in ("n", "d")}), SIGNED_AT),
("onlyRequired", sign({"u": PERSON, "n": "ab12", "p": PROJECT, "lvl": "read"}), SIGNED_AT),
]
result = {
"itsdangerous": __import__("importlib.metadata").metadata.version("itsdangerous"),
"python": sys.version.split()[0],
"secret": SECRET,
"salt": SALT,
"ttl": TTL,
"signedAt": SIGNED_AT,
"cases": [{"name": name, "ticket": ticket, "now": now, **answer(ticket, now)} for name, ticket, now in cases],
}
with open(out, "w", encoding="utf-8") as file:
json.dump(result, file, ensure_ascii=False, indent=2)
file.write("\n")
if __name__ == "__main__":
main(sys.argv[1])