chore(release): v0.2.0 #25

Merged
alex merged 1 commit from release-prep/rolling into main 2026-08-06 10:12:18 +02:00

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.

  • 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, and 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, and sat in the configuration table as "how long an issued certificate is valid" — the same unexplained-number state the tracker exists to prevent, with a different 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.

### 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`. - **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, and 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, and sat in the configuration table as "how long an issued certificate is valid" — the same unexplained-number state the tracker exists to prevent, with a different 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.
Generated by release-toolkit rt prep.

Tracker: frankenbit/release-toolkit#1
forgejo-actions force-pushed release-prep/rolling from 9ee867a448 to 6ccf321f9d 2026-08-05 22:53:22 +02:00 Compare
forgejo-actions force-pushed release-prep/rolling from 6ccf321f9d to cd9491a036 2026-08-05 23:07:01 +02:00 Compare
forgejo-actions force-pushed release-prep/rolling from cd9491a036 to de82f7e0a9 2026-08-05 23:10:58 +02:00 Compare
forgejo-actions force-pushed release-prep/rolling from de82f7e0a9 to b3e9a671af 2026-08-05 23:18:41 +02:00 Compare
forgejo-actions force-pushed release-prep/rolling from b3e9a671af to 81017daf31 2026-08-05 23:29:42 +02:00 Compare
forgejo-actions force-pushed release-prep/rolling from 81017daf31 to c4317afae1 2026-08-05 23:32:08 +02:00 Compare
forgejo-actions force-pushed release-prep/rolling from c4317afae1 to 9f732257f8
Some checks failed
go-ci / lint + build + test (push) Successful in 48s
release / decide + act (push) Failing after 5s
release / release (push) Failing after 0s
2026-08-05 23:36:51 +02:00
Compare
alex merged commit 9f732257f8 into main 2026-08-06 10:12:18 +02:00
alex deleted branch release-prep/rolling 2026-08-06 10:12:18 +02:00
Sign in to join this conversation.
No description provided.