Skip to content

release: v1.11.5 - #224

Merged
Jaro-c merged 2 commits into
mainfrom
develop
Jul 27, 2026
Merged

release: v1.11.5#224
Jaro-c merged 2 commits into
mainfrom
develop

Conversation

@Jaro-c

@Jaro-c Jaro-c commented Jul 27, 2026

Copy link
Copy Markdown
Member

Release PR for v1.11.5.

What changes for a consumer

Patch: no API change, and no behaviour change beyond ceasing to widen permissions.

Verified on go1.26.5

go build, go vet, golangci-lint (0 issues), go test -race ./... across all nine packages, and govulncheck reporting no vulnerabilities my code calls. Aggregate coverage 91.1%; internal/keymanager moves 85.7% -> 87.4%.

Both changes were mutation-tested, and where a mutation failed to turn a test red I said so in the pull request rather than trusting the green — that is how #222 was found in the first place.

Jaro-c added 2 commits July 26, 2026 22:56
Closes #222.

`New` chmodded `KeysDir` to `0700` unconditionally. The comment above it
says the point is to tighten a directory that arrives too open — but
with no read of the current mode it also *widened* a stricter one, so a
directory handed over at `0500` came back `0700` after the first start.
`docs/key-management.md` tells people to mount that directory read-only,
and it holds the Ed25519 private key and the refresh secret, so granting
write back unasked is the opposite of what the code claims.

`tightenDirMode` now reads the mode and only clears bits: `0755` ->
`0700`, `0500` stays `0500`.

### Measured, go1.26.5

The observable effect is the directory mode, so both directions are
asserted — a fix that just deleted the chmod would pass the first test
while silently dropping the tightening the code exists for.

| | Before | After |
|---|---|---|
| Directory at `0500` | becomes `0700` | stays `0500` |
| Directory at `0755` | becomes `0700` | becomes `0700` |

The first row is a failing test on `develop`, which is how I confirmed
the bug is real rather than a reading of the source.

`go build`, `go vet`, `golangci-lint` (0 issues) and `go test -race
./...` all pass.

### Mutation testing, including two that did not bite

| Mutation | Result |
|---|---|
| Remove the `tightenDirMode` call | tightening test fails, as expected
|
| Remove only the early return | **still passes** |
| Change only `chmod(dir, mode&dirMode)` back to `chmod(dir, dirMode)` |
**still passes** |
| Remove both | loosening test fails, as expected |

The two that survive are worth stating rather than hiding: the early
return and the mask each independently prevent the loosening, so neither
alone is load-bearing. I only trusted the test once the mutation that
removes both — which is exactly the code as it stands on `develop` —
turned it red.

### Relationship to #221

Same package, different change, so it is a separate branch off
`develop`. Whichever merges second may need a trivial rebase; the hunks
are in different parts of `New`.

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
Closes #220.

The key directory carried three files and nothing saying what wrote
them, which made this project's own migrate-forward rule unenforceable:
with no marker, a future format change could only guess from the shape
of a file or regenerate — and regenerating invalidates every
refresh-token and API-key hash a consumer has stored, logging out all of
their users.

`metadata.json` now records the format version, when the material was
first written, and the live signing key's id. No secret in it; the key
id already travels in every JWT header.

```json
{
  "format": 1,
  "created": "2026-07-27T02:41:09Z",
  "key_id": "3f9a1c07d5b2e846"
}
```

### The decisions, so they are easy to argue with

- **A separate file, not a PEM header.** People read those `.pem` files
with `openssl`; putting authcore's bookkeeping inside them breaks that
expectation.
- **Read before anything is parsed or generated.** A directory from a
newer authcore is refused while its files are still untouched, rather
than after this build has interpreted material in a layout it does not
know.
- **A corrupt marker fails closed** — a loader that cannot tell what
wrote the keys must not guess — but the error names the escape, because
the file holds no key material: delete it and the next start re-adopts
the keys. The test exercises that escape rather than trusting the
sentence.
- **No marker means the pre-marker layout**, adopted in place. This is
the one migration that has to exist from day one.
- **Adoption dates from the private key's mtime**, not from the moment
of the upgrade, so a directory that is months old does not claim to have
been created by the release that adopted it.
- **Writing is never fatal.** A read-only KeysDir is a supported
deployment and this is bookkeeping.

### What I measured, go1.26.5

`go build`, `go vet`, `golangci-lint` (0 issues), and `go test -race
./...` all pass; `internal/keymanager` coverage moves 85.7% -> 87.5%.

The assertions are that key material *survives*, not that code ran, so I
mutated the implementation to check the tests actually bite:

| Mutation | Expected | Result |
|---|---|---|
| Drop the newer-format guard | future-directory test fails | fails |
| Rewrite the descriptor on every load | no-churn test fails | fails |
| Always stamp `time.Now()` instead of the key mtime | adoption-date
test fails | fails |
| Make a failed descriptor write fatal | write-failure test fails |
fails |

That last one is worth a note, because my first version of the test did
**not** catch it. I had written it as "chmod the directory to 0500 and
expect startup to survive" — but `New` chmods the key directory back to
`0700` before writing anything, so the permission bits never survive to
the write, and the mutation sailed through green. The permission-bit
scenario is unreachable through `New`; the real one is a filesystem that
refuses the write, which a missing directory reproduces portably. The
test is now white-box against `syncMetadata`, and it fails when the
mutation lands. Worth knowing separately: **`New` will loosen a
deliberately read-only-by-mode KeysDir to 0700**, which is not what the
surrounding comments imply.

### Not in scope

`internal/keymanager` is still under the coverage gate at 87.5% — that
is #218, unchanged by this beyond the small rise.

---------

Signed-off-by: Jaro-c <75870284+Jaro-c@users.noreply.github.com>
@Jaro-c Jaro-c added type:chore Maintenance with no product impact prio:P2 Medium priority status:review In review effort:XS Extra small labels Jul 27, 2026
@Jaro-c
Jaro-c merged commit 30f019a into main Jul 27, 2026
26 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

effort:XS Extra small prio:P2 Medium priority status:review In review type:chore Maintenance with no product impact

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant