Skip to content

GET /license/status returns 500 instead of clock_untrusted when the clock is behind #1939

Closed

Description

@MODSetter

The gap

docs/architecture/license/app.md lists this under Known gaps:

A stored file whose meta.issued is more than five minutes ahead of the clock makes GET /license/status fail with 500 instead of answering clock_untrusted: status() re-runs verify(), and nothing catches its rejection.

Why it matters

clock_untrusted is a state the app is designed to show: Settings has a notice for it, and the sidebar draws a red status row. But the one case that most obviously produces it — a machine whose clock is behind the time the license was issued — never reaches that code. verify() raises before _status() can classify anything, so the user gets a 500 and a broken Settings pane instead of "This computer's clock is behind the time the license was issued."

A laptop clock running fast is the ordinary cause, not tampering. It should read as a explainable state, not a crash.

Where

Note the asymmetry worth preserving: import_certificate() should keep rejecting a bad file with 422, because the user is handing one over right then. Only reading the status of an already-stored file should degrade to a state instead of an error. Fix status(), not verify().

What done looks like

  • GET /license/status on a stored file whose meta.issued is more than MAX_CLOCK_DRIFT ahead answers 200 with state: "clock_untrusted".
  • PUT /license still answers 422 for the same file — importing is a different moment.
  • Decide deliberately what the other rejections (bad_signature, not_a_license_file, unsupported_algorithm) should do on a stored file. A file that was valid at import cannot become unsigned, so if you leave those raising, say why in the PR. Don't silently swallow every rejection into one state.
  • Update docs/architecture/license/app.md if the answer changes what the State table describes, and delete the Known gaps line in the same PR.

How to verify

surfsense_local/backend/tests/integration/license/test_license.py already covers these routes and shows how to sign a fixture file. Add a failing test first: store a file, move the clock back past the drift window, read the status.

cd surfsense_local/backend
uv run pytest -m integration -k license
uv run ruff check .

Before you start

Read CONTRIBUTING.md, the license doc and ADR 0019. Open your PR against dev, not main. Comment here to claim this.

Activity

  1. added
    bugSomething isn't working
    help wantedExtra attention is needed
    desktopSurfSense desktop app (surfsense_local)
    on Sep 28, 2026
  2. ybai08 commented on Sep 29, 2026

    @ybai08
    Contributor

    I'd like to take this. Plan: add a failing integration test at GET /license/status, then have status() trust the stored file's signature only and let the existing watermark classify the clock, so import keeps answering 422. The doc's Known gaps line goes in the same PR.

  3. added a commit that references this issue on Sep 29, 2026
    bc0b796
  4. MODSetter commented on Sep 29, 2026

    @MODSetter
    OwnerAuthor

    Fixed by #2019, fix(local): read a stored license issued ahead of the clock as clock_untrusted, by @ybai08. Merged into dev.

    Closing by hand rather than by keyword: Fixes #1939 only auto-closes on a merge to the default branch, and this repo merges through dev (CONTRIBUTING), so this would have stayed open and claimable while the work was already done. It reaches main with the next release.

    Nothing left to pick up here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdesktopSurfSense desktop app (surfsense_local)good first issueGood for newcomershelp wantedExtra attention is needed

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions