unslothai/unsloth · warning · HTTPException

nonce must decode to 16-128 bytes

Error message

nonce must decode to 16-128 bytes

What it means

After decoding, the /identity nonce must be between 16 and 128 bytes; anything shorter or longer returns HTTP 400 'nonce must decode to 16-128 bytes'. The floor prevents trivially guessable nonces (replay/forgery risk) and the ceiling caps HMAC input size. Length is checked on the decoded bytes, not the encoded string length.

Source

Thrown at studio/backend/routes/auth.py:408


# Sync def (not async): compute_identity_proof touches SQLite on the first call,
# so FastAPI runs it in the threadpool rather than blocking the event loop.
@router.get("/identity")
def identity(nonce: str, request: Request) -> dict:
    """Challenge-response proof this is the real local Unsloth: caller sends a nonce,
    gets HMAC(install identity secret, nonce, connection address + port).
    Unauthenticated and side-effect free; a process that can't read the same-user
    secret can't forge a proof, and binding to the address/port the connection
    landed on stops a squatter relaying a proof from the real Unsloth elsewhere."""
    try:
        raw = base64.urlsafe_b64decode(nonce)
    except Exception:
        raise HTTPException(
            status_code = status.HTTP_400_BAD_REQUEST, detail = "nonce must be base64url"
        )
    if not 16 <= len(raw) <= 128:
        raise HTTPException(
            status_code = status.HTTP_400_BAD_REQUEST, detail = "nonce must decode to 16-128 bytes"
        )
    # The address + port the connection actually landed on, from the socket
    # (request.scope is getsockname, so it is the real local address even when
    # bound to 0.0.0.0), never the client-controlled Host header.
    server = request.scope.get("server") or ("", 0)
    host = server[0] or ""
    port = server[1] if server[1] is not None else 0
    return {"proof": storage.compute_identity_proof(raw, host, port)}


@router.get("/status", response_model = AuthStatusResponse)
async def auth_status() -> AuthStatusResponse:
    """Auth initialization state; ``default_username`` is exposed for first-boot UI prefill only."""
    return AuthStatusResponse(
        initialized = storage.is_initialized(),
        default_username = storage.DEFAULT_ADMIN_USERNAME,
        requires_password_change = storage.requires_password_change(storage.DEFAULT_ADMIN_USERNAME)

View on GitHub (pinned to 203007d190)

Solutions

  1. Generate the nonce from 16-128 raw bytes: `base64.urlsafe_b64encode(secrets.token_bytes(32)).decode().rstrip('=')`
  2. If the nonce check fails, log the decoded byte length client-side to confirm it is in range
  3. For tests, use a fixed but in-range nonce (e.g. 32 bytes) rather than a short placeholder

Example fix

# before
resp = client.get("/auth/identity", params={"nonce": base64.urlsafe_b64encode(b"1234567").decode()})  # 7 bytes
# after
resp = client.get("/auth/identity", params={"nonce": base64.urlsafe_b64encode(secrets.token_bytes(32)).decode().rstrip("=")})
Defensive patterns

Strategy: validation

Validate before calling

import base64, os, secrets

def make_nonce(nbytes: int = 32) -> str:
    assert 16 <= nbytes <= 128
    return base64.urlsafe_b64encode(secrets.token_bytes(nbytes)).decode().rstrip("=")

Try / catch

r = client.get("/auth/identity", params={"nonce": nonce})
if r.status_code == 400 and "16-128 bytes" in r.json()["detail"]:
    nonce = make_nonce(32)
    r = client.get("/auth/identity", params={"nonce": nonce})

Prevention

When it happens

Trigger: Sending a base64url nonce that decodes to fewer than 16 bytes (e.g. a 8-byte random value) or more than 128 bytes (e.g. a UUID concatenated with a timestamp and extra entropy).

Common situations: Clients using short fixed strings or 1-2 word tokens as the nonce; over-enthusiastic clients concatenating multiple entropy sources past 128 bytes; tests reusing a hardcoded tiny nonce.

Related errors


AI-assisted analysis of unslothai/unsloth@203007d190 (2026-08-15). Data as JSON: /api/errors/a171a6426900c43a. Report an issue: GitHub.