• v0.2.0 1036335189

    Release v0.2.0
    All checks were successful
    go-ci / lint + build + test (push) Successful in 48s
    release / decide + act (push) Successful in 5s
    release / release (push) Successful in 0s
    Stable

    Ghost released this 2026-08-06 11:50:19 +02:00 | 21 commits to main since this release

    Added

    • store: expired certificates' bundles are purged, so a stolen backup carries less (#3)

      A sweep runs at startup and hourly, clearing the stored .p12 ciphertext of any certificate past its not_after. The audit row stays; only the key material goes. Keyed on not_after and never on revoked_at — a revoked-but-unexpired record keeps its bundle, because the 403 refusal path reads it. A download resolving to a purged record now returns 410 rather than a zero-byte file.

    • deploy: pass VERSION to the build and assert the deployed binary reports it

    • deploy: probe the #5 regression surface, and make the probe parser field-safe

    • deploy: deploy purser from CI on release cuts

    Changed

    None.

    Fixed

    • version: inject release tag via Dockerfile VERSION build-arg (#16)

      docker compose build --build-arg VERSION=v0.1.0 now stamps the binary; without the arg the build falls back to the VCS commit hash or "dev". The ldflags path is -X git.frankenbit.de/frankenbit/purser/internal/version.Tag.

    • changelog: split three sentences over the 30-word density ceiling

    • deploy: arm 5 must survive a version-less page, and the disclosure contradicted it

    • docs: 120 bits bounds the password search, not the bundle's strength

    • docs: legacy-des does not use PBKDF2 — and the error was in three places

    • deploy: arm 4 asserts the exact 404 contract, not merely "not a redirect"

    • deploy: wait for health before probing — the inspect raced the healthcheck

    Removed

    None.

    Deprecated

    None.

    Upgrade

    None.

    Internal

    • purser: every post-sign failure path is now proven to revoke, not just inspected (#12)

      The existing TestAbandon_* tests called abandon() directly, so removing its call from any exit in Issue() left them green while a live certificate leaked. Four Issue()-level tests now cover the drift, password, packaging and store exits, each mutation-verified to redden only for its own branch. No behaviour change.

    • purser: PURSER_EMBED_CA_ROOT is now verified against the bundle it produces (#13)

      The loadCARoot tests check the loader in isolation and would all pass with Service.CARoot assigned nowhere — the original defect. A new test decodes the issued .p12 and asserts the root is present with the flag set and absent without it. No behaviour change.

    • purser: the profile gate's key baseline is pinned to the measured RSA-3072, not derived from config (#2)

      expectedProfile() used to take the key expectation from PURSER_KEY_TYPE, so setting ECDSA made ECDSA the expectation and the gate fell silent at the one moment it exists to speak. A setting proves intent, not that the device accepts it. PURSER_ALLOW_PROFILE_DRIFT remains the named path to change it deliberately and measure. The override and the conforming happy path are now tested at Issue(); both were untested.

    • docs: the backup-alone claim now states both legs it rests on (#3)

      The claim lived in internal/store while the entropy supporting it was stated in internal/purser, so a reader had to cross packages to learn what it depended on. It rests on a conjunction. The password contributes 120 bits against a password search; the encryption is the other, equally load-bearing leg. Under the deployed legacy-des the 3DES cipher binds first, at ~112 bits. Also records why the PKCS#12 iteration count is deliberately not raised.

    • docs: the 168h certificate lifetime now carries its reasoning (#4)

      The value was chosen and proven. The configuration table still described it only as "how long an issued certificate is valid". That is the unexplained-number state this tracker exists to prevent, with a new number in it. docs/operations.md now states the deployed value, the 8760h provisioner ceiling, and the reason not to reach for it. ocserv consults no CRL, so NotAfter is the only mechanism that ends access.

    Downloads