• ANNOUNCE: jwt 1.1 released

    From =?UTF-8?Q?Alexander_Sch=C3=B6pe?=@ete-sep@mxbo.de to comp.lang.tcl on Fri Jul 31 22:08:13 2026
    From Newsgroup: comp.lang.tcl

    ANNOUNCE: jwt 1.1 released
    ==========================

    jwt is a pure Tcl extension for JSON Web Tokens: it signs and verifies
    compact JWT/JWS tokens, with HMAC (HS256, HS512) or NaCl onetime authentication. Written by Alexander Schoepe, it requires Tcl 8.6 or
    newer, including Tcl 9, plus the C extensions nacl 1.3 or newer and rl_json.

    1.1 is the first standalone release. The code was embedded in websh up
    to version 3.7.7 and is now maintained as a package of its own, with a
    TEA build, a tcltest suite and the BSD 3-Clause license. There is no C
    code in it: one installed copy serves Tcl 8.6 and Tcl 9 alike.

    Download / Repository: https://fossil.sowaswie.de/jwt


    English
    -------

    The API:

    * ::jwt::sign header payload secret takes header and payload as JSON
    text and returns the token as header.payload.signature in base64url. The algorithm is read from the header field alg, with HS256 as the default,
    plus HS512 and NaCl.
    * ::jwt::verify token secret ?-json? ?-claims? ?-leeway seconds? returns
    a boolean by default and checks the signature only.
    * ::jwt::base64url_encode and ::jwt::base64url_decode are public,
    because callers need them for salts, keys and binary blobs of their own.

    The secret is used at whatever length it has. For HS256 and HS512 it is
    handed to nacl unchanged, which prepares the HMAC key as RFC 2104
    section 2 defines it: a key longer than the hash block size is replaced
    by its hash, a shorter one is padded with zero bytes, and there is no
    fixed length anywhere in between. This matters because in JWT the key is rarely chosen by the verifying side. The Nextcloud app OIDC Identity
    Provider, for one, generates client secrets of 64 characters and accepts
    32 to 64 in its web interface; the same holds for webhook secrets from
    GitHub, Stripe or GitLab. A token signed here with such a secret is now
    byte for byte what PHP's hash_hmac, Python's hmac or jwt.io produce for
    it. Only the NaCl algorithm keeps a fixed key length, because Poly1305
    is defined with 32 bytes; a secret for that one is padded or truncated
    as before.

    This is why nacl 1.3 is a hard requirement rather than a recommendation.
    Up to 1.2 the package accepted keys of exactly 32 bytes and rejected everything else, so with an older nacl the HMAC algorithms now fail with
    an explicit error instead of quietly signing with a shortened key.

    Signature comparison in verify happens inside nacl, in constant time via crypto_verify_32, rather than through a string comparison in Tcl.

    Behaviour worth knowing:

    * Time claims are opt-in. Only with -claims are exp and nbf evaluated according to RFC 7519; claims that are absent are not enforced. Without
    the option an expired token still verifies, which keeps the
    signature-only contract for existing callers.
    * -leeway adds a tolerance in seconds against clock skew between the
    signing and the verifying host. It is effective together with -claims
    only; a non-integer or negative value is treated as zero.
    * With -json the result is an rl_json document holding verify, header
    and payload, the latter two as raw, re-parsable JSON text rather than
    embedded objects. Together with -claims it also carries reason, one of
    ok, signature, notbefore, expired or payload, so a forged token can be
    told apart from one that merely ran out of time.
    * The runtime dependencies are loaded lazily on the first sign or verify
    call, so sourcing the package costs nothing if it ends up unused.

    Build, test, install:

    * ./configure picks up the default Tcl via tclConfig.sh, make test runs
    the suite, make install places the package in lib/jwt1.1 of that Tcl installation. To serve a second installation as well, re-run configure
    with --with-tcl against it.
    * The build works out of tree: every source file is referenced through
    srcdir, so an object directory beside the source tree is fine.

    Tests:

    * 30 tcltest cases covering signing, signature verification with the
    right and the wrong secret, the shape of the -json result document, exp
    and nbf handling under -claims, and the leeway window including the case
    where -leeway is given without -claims.
    * Six of them cover secret lengths and check HS256 and HS512 with a 64
    byte key against RFC 2104 reference values, so a regression that trims
    the key again fails the suite rather than surfacing as a mysterious
    rejection at the other end.
    * All of them are guarded by the jwtDeps constraint, which requires jwt,
    nacl and rl_json to load. If one of them is missing, make test succeeds
    with everything skipped, so it is worth reading the skipped count and
    not only the exit status.

    Limitation: characters beyond the BMP, above U+FFFF, have different
    string representations in Tcl 8.6 and Tcl 9. Payloads containing them
    are therefore not portable across Tcl versions when they are coupled to ciphertext or a CRC.


    Deutsch
    -------

    Die API:

    * ::jwt::sign header payload secret erwartet Header und Payload als
    JSON-Text und liefert das Token als header.payload.signature in
    base64url. Der Algorithmus stammt aus dem Header-Feld alg, Vorgabe ist
    HS256, daneben stehen HS512 und NaCl zur Verf|+gung.
    * ::jwt::verify token secret ?-json? ?-claims? ?-leeway sekunden?
    liefert standardm|n|fig einen Boolean und pr|+ft ausschlie|flich die Signatur. * ::jwt::base64url_encode und ::jwt::base64url_decode sind ||ffentlich,
    weil Aufrufer sie f|+r eigene Salts, Schl|+ssel und Bin|nrdaten ben||tigen.

    Das Secret wird in der L|nnge verwendet, die es hat. Bei HS256 und HS512
    geht es unver|nndert an nacl, das den HMAC-Schl|+ssel so vorbereitet, wie
    RFC 2104 Abschnitt 2 es definiert: Ein Schl|+ssel l|nnger als die
    Blockgr|||fe des Hash wird durch seinen Hash ersetzt, ein k|+rzerer mit Nullbytes aufgef|+llt, und dazwischen gibt es keine feste L|nnge. Das ist deshalb wichtig, weil der Schl|+ssel bei JWT selten von der pr|+fenden
    Seite gew|nhlt wird. Die Nextcloud-App OIDC Identity Provider etwa
    erzeugt Client-Secrets mit 64 Zeichen und akzeptiert in ihrer
    Weboberfl|nche 32 bis 64; f|+r Webhook-Secrets von GitHub, Stripe oder
    GitLab gilt dasselbe. Ein hier signiertes Token stimmt mit einem solchen Secret jetzt Byte f|+r Byte mit dem |+berein, was PHPs hash_hmac, Pythons
    hmac oder jwt.io daf|+r erzeugen. Nur der Algorithmus NaCl beh|nlt eine
    feste Schl|+ssell|nnge, weil Poly1305 mit 32 Byte definiert ist; ein
    Secret daf|+r wird wie bisher aufgef|+llt oder gek|+rzt.

    Deshalb ist nacl 1.3 zwingend und nicht blo|f empfohlen. Bis 1.2 nahm das Paket ausschlie|flich Schl|+ssel von genau 32 Byte an und wies alles
    andere zur|+ck; mit einem |nlteren nacl brechen die HMAC-Algorithmen jetzt also mit einer klaren Fehlermeldung ab, statt still mit einem verk|+rzten Schl|+ssel zu signieren.

    Der Signaturvergleich in verify findet innerhalb von nacl statt, in
    konstanter Zeit |+ber crypto_verify_32, und nicht als Stringvergleich in Tcl.

    Wissenswertes zum Verhalten:

    * Die Zeitclaims sind opt-in. Nur mit -claims werden exp und nbf gem|n|f
    RFC 7519 ausgewertet, nicht vorhandene Claims werden dabei nicht
    erzwungen. Ohne die Option verifiziert auch ein abgelaufenes Token
    weiterhin, womit der Vertrag der reinen Signaturpr|+fung f|+r bestehende Aufrufer erhalten bleibt.
    * -leeway erg|nnzt eine Toleranz in Sekunden gegen Uhrenversatz zwischen signierendem und pr|+fendem Rechner. Sie wirkt nur zusammen mit -claims;
    ein nicht ganzzahliger oder negativer Wert gilt als null.
    * Mit -json ist das Ergebnis ein rl_json-Dokument mit verify, header und payload, wobei Header und Payload als roher, wieder parsbarer JSON-Text abgelegt sind und nicht als eingebettete Objekte. Zusammen mit -claims
    kommt reason hinzu, mit einem der Werte ok, signature, notbefore,
    expired oder payload, sodass sich ein gef|nlschtes Token von einem blo|f abgelaufenen unterscheiden l|nsst.
    * Die Laufzeitabh|nngigkeiten werden erst beim ersten sign- oder
    verify-Aufruf geladen, das Einbinden des Pakets kostet also nichts,
    solange es ungenutzt bleibt.

    Bauen, testen, installieren:

    * ./configure ermittelt das voreingestellte Tcl |+ber tclConfig.sh, make
    test f|+hrt die Suite aus, make install legt das Paket unter lib/jwt1.1
    dieser Tcl-Installation ab. F|+r eine zweite Installation gen|+gt ein
    erneutes configure mit --with-tcl.
    * Der Build funktioniert au|ferhalb des Quellbaums: Alle Quelldateien
    werden |+ber srcdir angesprochen, ein eigenes Build-Verzeichnis neben den Quellen ist damit m||glich.

    Tests:

    * 30 tcltest-F|nlle f|+r das Signieren, die Signaturpr|+fung mit richtigem
    und falschem Secret, die Struktur des -json-Ergebnisdokuments, die
    Behandlung von exp und nbf unter -claims sowie das Leeway-Fenster einschlie|flich des Falls, dass -leeway ohne -claims angegeben wird.
    * Sechs davon betreffen die Secret-L|nngen und pr|+fen HS256 und HS512 mit einem 64-Byte-Schl|+ssel gegen Referenzwerte nach RFC 2104. Ein R|+ckfall,
    der den Schl|+ssel wieder k|+rzt, l|nsst damit die Suite scheitern, statt
    am anderen Ende als unerkl|nrliche Zur|+ckweisung aufzutauchen.
    * Alle sind |+ber das Constraint jwtDeps abgesichert, das jwt, nacl und rl_json voraussetzt. Fehlt eines davon, meldet make test Erfolg bei
    komplett |+bersprungener Suite; ein Blick auf die Anzahl der
    |+bersprungenen Tests lohnt sich daher mehr als der Exit-Status allein.

    Einschr|nnkung: Zeichen jenseits der BMP, also oberhalb von U+FFFF, haben
    in Tcl 8.6 und Tcl 9 unterschiedliche String-Repr|nsentationen. Payloads
    mit solchen Zeichen sind deshalb nicht zwischen den Tcl-Versionen
    portabel, sobald sie an Ciphertext oder eine Pr|+fsumme gekoppelt sind.
    --- Synchronet 3.22a-Linux NewsLink 1.2