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>
This commit is contained in:
@@ -3,8 +3,13 @@
|
||||
// JSON, zlib-packed when that is shorter (then a leading "."), base64url;
|
||||
// the moment of signing as the shortest big-endian bytes, base64url; an
|
||||
// HMAC-SHA1 over both under the key sha1(salt + "signer" + secret)
|
||||
// ("django-concat"), base64url. Valid for TICKET_SECONDS after signing and
|
||||
// not before it, as itsdangerous counts (whole seconds). What lies under a
|
||||
// ("django-concat"), base64url. Valid for TICKET_SECONDS after signing, as
|
||||
// itsdangerous counts (whole seconds), and — unlike itsdangerous, which
|
||||
// refuses a ticket a second ahead — from TICKET_AHEAD_SECONDS before it:
|
||||
// the dashboard's machine and F's each have their clock, and a ticket
|
||||
// carries whole seconds. F's clock 8 ms behind refused one ticket in 125,
|
||||
// 886 ms behind nearly nine in ten (measured on test-filius, 04.10.2026);
|
||||
// a person then sees "invalid ticket" for nothing they did. What lies under a
|
||||
// matching signature the dashboard made: its base64 is read as Node reads
|
||||
// it (leniently, as Python does). Python's own tickets are the oracle
|
||||
// (tools/ticket-oracle).
|
||||
@@ -17,6 +22,8 @@ import { createHash, createHmac, timingSafeEqual } from 'node:crypto';
|
||||
import { inflateSync } from 'node:zlib';
|
||||
|
||||
export const TICKET_SECONDS = 60;
|
||||
/** How far ahead of F's clock a ticket may be signed: two machines' clocks, never quite one. */
|
||||
export const TICKET_AHEAD_SECONDS = 5;
|
||||
export const TICKET_SALT = 'mdg-filius-ticket';
|
||||
export const LEVELS = ['read', 'write'] as const;
|
||||
export type Level = (typeof LEVELS)[number];
|
||||
@@ -61,7 +68,7 @@ export function readSigned(ticket: string, secret: string, now: number, salt = T
|
||||
if (stamp < 0) throw new TicketError('no timestamp');
|
||||
const age = now - Buffer.from(value.slice(stamp + 1), 'base64url').reduce((sum, byte) => sum * 256 + byte, 0);
|
||||
if (age > TICKET_SECONDS) throw new TicketError(`ticket ${age} s old`);
|
||||
if (age < 0) throw new TicketError('ticket from the future');
|
||||
if (age < -TICKET_AHEAD_SECONDS) throw new TicketError('ticket from the future');
|
||||
const payload = value.slice(0, stamp);
|
||||
const packed = payload.startsWith('.');
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user