Skip to content

fix(workcontext)!: core's verifier is not re-exported; a verifier's keys come from an admitted endpoint, on the host's literal transport rule - #53

Merged
antoinetoussaint-byte merged 1 commit into
mainfrom
issue-52-security-posture-audit-2026-10-04-the-findings
Oct 8, 2026
Merged

antoinetoussaint-byte merged 1 commit into
mainfrom
issue-52-security-posture-audit-2026-10-04-the-findings

Conversation

@antoinetoussaint-byte

@antoinetoussaint-byte antoinetoussaint-byte commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Closes the two findings of the 2026-10-04 security posture audit this repository
owns (#52): SA-F-WC-ISSUER and SA-F-WC-TTL. This repository is public, so
each is described by the invariant it restores and by the required behaviour; the
scenarios stay in the private register under the same ID.

Third pass, answering the xhigh BLOCK at 3fcd4a6b.

Breaking: workcontext.Verifier is no longer exported. See R53-01 below.

Rebased onto main; the branch is one commit again. sdk-go#57 ("adopt core
v0.12.0 endpoint resolution") merged as 9b9d219, and this PR is now 3f4671a
on top of it. 91a2f91 — the for.go endpoint-location fix that was pushed to
this branch mid-pass — is dropped, not carried: #57 owns that change, per the
Lane 3 coordinator (handbook#196), and main's version is what this branch
carries.

Two things differed from the rehearsal and were recomputed rather than replayed:

  • main resolves core v0.13.0, not the v0.12.0 in fix!: adopt core v0.12.0 endpoint resolution #57's title. Every gate
    below ran against v0.13.0. scripts/security-posture.sh is the check that
    matters here — it is what would catch a core bump dropping the derivation clamp
    or changing the verifier surface, and it passes, so the audit invariants hold
    against the version this repo actually resolves.
  • for.go needed two patches, not one. A single patch spanning this PR's two
    earlier passes silently includes 91a2f91's External() line and collides with
    main's rewritten resolver. Split per pass, both verified to contain no
    External/Visibility line, both apply clean.

So main owns resolveLocalNetworkInstance entirely — SelectEndpointForReference
and selected.External() are untouched here — and the only for.go lines this PR
adds are (*Query).IsLocalRun and (*Query).WorkspaceValueIfSet, beside
WorkspaceValue. codefly_test.go keeps both sides: #57's
prepareDeclaredEndpointWorkspace and this PR's two accessor tests. AGENTS.md
carries both #57's core-pin bullet and this PR's verifier bullet, at 195 lines of
its 200-line budget. endpoint_location_test.go and the root README.md come
from main untouched.

Nothing about the audit findings changed in the rebase.

Review findings

Finding Disposition
R53-01 the transport contract and key provenance fixed. Core's verifier is no longer re-exported, so there is one way to get one; KeySet has a single constructor that fetches from the admitted endpoint, and NewVerifier takes nothing else.
R53-07 nonliteral mesh assertions authorize plaintext fixed. The assertion is compared byte for byte; the test expectations that asserted padded values succeed were wrong and are corrected.
R53-08 the declaration error discloses credential values fixed. The refusal names the setting and the permitted values; the value and the source's error text are kept in the chain for errors.Is and out of the message.
R53-09 declaration-read failures are treated as absence fixed. A new three-valued accessor on the root module separates "not configured" from "could not be read", and a failed read refuses every admission including an https one.
R53-10 the sweep accepts malformed Go source fixed. scripts/importsof parses whole files; three malformed shapes are committed as sweep probes.
R53-06 public prose the PR body is this one, written against the current head. The source and commit prose the reviewer could read already passed.
catalogue SP-WC-01 and SP-WC-05 commands do not work fixed, in this repository. scripts/security-posture.sh is the check, and it fails when the invariant is violated — proved five ways below. The counts are corrected.
— key-set validation is a local length check, not a core-owned validator unchanged, and named. Core exports no key-configuration validator. See Not verified.

R53-01 — the verifier, the endpoint and the keys are one operation

Two things were open: the exported verifier remained usable without the helper,
and the admitted endpoint did not establish where verifier.Keys came from.

Core's verifier is no longer re-exported. type verifier = corework.Verifier
is unexported in core.go; PinnedVerifier is the only type here with a Verify
method and NewVerifier is its only constructor. Every field of core's verifier
has a usable zero value, so exporting it was a second way in that skipped every
check — the shape GuardStreams already exists to avoid in this module, where
the correct wiring took two calls and only one was reachable from the type
system. A component that genuinely is the issuer imports core/workcontext
directly, which core.go already says about ErrNoSeal and the verify-only
Authenticator.

The keys come from the admitted endpoint. KeySetEndpoint and KeySet are
opaque with one constructor each, AcquireKeySet is the only thing that produces
a KeySet, and NewVerifier takes nothing else:

endpoint, err := workcontext.ResolveKeySetEndpoint(rawURL, codefly.For(ctx))
keys, err := workcontext.AcquireKeySet(ctx, endpoint, decodeMyKeySet)
verifier, err := workcontext.NewVerifier(settings, keys)

The decoding stays the caller's and the keys cross as plain []byte, because
reading a key set means naming a signature primitive — crypto/ed25519, base64
decode and encoding/json outside mint.go are each refused by the import
allowlist that ten rounds of review built, and re-opening it here would undo the
thing the four gates exist for. fillVerificationKeys is generic so the element
type is inferred from the field it fills, which names nothing.

The fetch stays on the admitted address: a redirect is refused rather than
followed, the body is bounded, a non-200 is ErrKeySetUnavailable rather than a
configuration fault, and the HTTP client is this module's — a caller supplies
none, on the mint client's terms.

workcontext/grpctransport/two_service_test.go, which is the consumer-shaped
test here, now builds its verifiers through that whole path against a real
loopback endpoint rather than by constructing one.

What this does not close, stated rather than claimed: a consumer can still
import core/workcontext and build core's verifier itself. That is deliberate —
an issuer must — and it is now visible in a grep rather than available one import
shorter. The consumer-tree half of the SP-WC-05 command below is what reads it.

R53-07 — the assertion is compared literally

true asserts the mesh. Unset, empty and false keep plaintext refused. Every
other value stops the process, a value carrying whitespace included.

The previous revision trimmed first, with the host's reasoning: a value delivered
by file carries the newline the file ends with. The reviewer is right that this is
the wrong trade. TrimSpace trims every Unicode space, so " true" and six
other shapes asserted the mesh while not being the assertion. A delivery's shape
is the delivery's to fix, and the refusal names what the value must be.

Seven whitespace variants and nine other misspellings are committed refusals.
This is stricter than the host's own copy (meshtransport, module-saas-starter#1008),
which still trims; the same finding applies there, and that is the host's to
make. Noted under Operator steps because a composition delivering the assertion
by file now has to deliver it exactly.

R53-08 — no refusal echoes a configured value

The assertion is read from the public namespace and then the secret one, so its
value may be a credential, and a configuration error reaches boot-time logging.
The refusal now names the setting and the permitted values; the value itself and
the source's error text stay in the error chain for errors.Is and out of the
message. The same treatment covers the decoder's error and the transport's, both
of which name what they were given.

R53-09 — a failed read is not absence

WorkspaceValue reports "not configured" and "the delivery could not be read"
with one error, because its callers want a value or nothing. A posture question
cannot live with that. So TransportSource requires
WorkspaceValueIfSet(name, key) (string, bool, error), added to the root
module's *Query, which returns:

  • the value and true when it is set;
  • ("", false, nil) when nothing carries it — absence is an answer;
  • an error for an unreadable file carrier, a value delivered both inline and by
    file, and ErrAuthorityValueChanged — a drifted authority-bearing value.

A failed read refuses every admission decision, including an https one, so a
declaration that cannot be read never leaves the process admitting. Covered by a
root-module test driving the real carriers, including the ReadAuthority drift
case the review used.

R53-10 — the sweep parses whole files

parser.ImportsOnly stops at the first non-import declaration, so a file whose
body does not parse read as a file with imports and no problem — against the
sweep's own documented claim that "a file that does not parse is a failure, not a
file with no imports" — and an import placed after a declaration was never read
at all. It is a full syntax parse now (still not a type-check: resolving a file at
a two-year-old tag would need that tag's dependencies). The three shapes are
committed in TestTheImportScannerReadsOneLineOnce, and restoring ImportsOnly
turns all three red.

SA-F-WC-TTL — unchanged and still partly this repo's

TestDerivedContextNeverOutlivesParent is unchanged and still passes against the
resolved core v0.9.0, mutation-checked below. The verification half needs an
absolute-deadline claim that codefly.base.v0.WorkContextV1 does not carry and
WorkActorV1 has no per-hop timestamp for; that remains codefly-dev/core's,
and nothing is worked around here.

The catalogue checks, which now fail when the invariant does

The old commands counted lines: go test … | grep -c '^--- PASS' answers 0
for a passing run without -v, 1 whatever the subtests did, and 0 for a
test that no longer exists. A check that cannot fail reports green forever, which
is worse than no check — the rule stops being audited and nothing says so.

scripts/security-posture.sh replaces them. It reads the exit status, and
separately requires every named test to have run, so a renamed, deleted or
filtered-out test fails rather than passing silently.

./scripts/security-posture.sh SP-WC-01   # a derived context never outlives its parent
./scripts/security-posture.sh SP-WC-05   # every verifier pins issuer and audience
./scripts/security-posture.sh            # both; exit 0 means both hold

SP-WC-05 has a second half, over each consumer tree, which is what reads the
residue named above:

grep -rnE '(corework|workcontext)\.Verifier\b' <consumer tree> --include='*.go' | grep -v _test   # no output

Proved by violating it, five ways — each run after the violation, each exit 1:

Violation security-posture.sh
core's derive no longer clamps (behind a filesystem replace) SP-WC-01 exit 1
the mesh assertion is not consulted SP-WC-05 exit 1
core's verifier is exported again SP-WC-05 exit 1, naming TestEveryVerificationPathIsPinned
a declaration read failure reads as absence SP-WC-05 exit 1
a rule's test is renamed away SP-WC-01 exit 1, naming the test that did not run

The counts, corrected to what the commands actually print. The review is
right and the previous body was wrong:

Prints
SP-WC-01's catalogue command verbatim 0, and the pipeline exits 1
the same with -v, catalogue's anchored ^--- PASS 1
the same with -v, counting indented subtests 31
./scripts/security-posture.sh the rule names and ok, exit 0

Test plan

  • go test ./... (ok, 46.6s) and (cd workcontext && go test ./...) (ok, 7.9s) — against core v0.13.0
  • go test -race ./... (ok, 18.0s) and (cd workcontext && go test -race ./...) (ok, 16.7s)
  • golangci-lint run in each module — 0 issues, 0 issues. .golangci.yaml extends its existing ST1005 proper-noun exclusion (Codefly… sentinels) to verifier.go, beside mint.go and stream.go.
  • go mod tidy + git diff --exit-code -- go.mod go.sum clean in each module
  • coverage: root 79.2%, workcontext 94.3% (gate ≥ 75% each). verifier.go is 95–100% per function; AcquireKeySet 95.2% and NewVerifier 95.5%, the uncovered statements being http.NewRequestWithContext failing on a URL that already parsed.
  • scripts/check-one-implementation.sh → ok working tree (78 Go files)
  • scripts/security-posture.sh → ok SP-WC-01, ok SP-WC-05
  • scripts/allowed-imports.txt unchanged. verifier.go adds context, errors, io, net/http and time to the fmt, net/url, slices, strings it already had — every one already on the leaf module's list, because the mint client needs them. No crypto, no codec, no base64.

Mutation checks

Each is run against the whole leaf module suite, not a selected test, so a
mutation nothing committed catches shows as green. All twenty-four are red.

The three new findings and the provenance fix:

Mutation Red on
N1 the assertion is trimmed before comparison all seven whitespace variants
N2 a lookup failure reads as absence the lookup-failure and every-endpoint tests
N3 the assertion value is echoed into its refusal TestTheAssertionRefusalDoesNotEchoTheValue
N4 the lookup failure's text is reported the same
N5 core's verifier is exported again TestEveryVerificationPathIsPinned
N6 a second type grows a Verify method the same
N7 the key set is not bound to the endpoint /a_key_set_that_was_never_acquired
N8 a redirect is followed /a_redirect,_which_leaves_the_admitted_address
N9 the decoder's error text is reported /a_decoder_error,_whose_text_is_the_caller's

The transport contract and the pin, re-run against the rewritten file: M1 the
assertion not consulted · M2 the address rule not consulted · M3 the suffix taken
as "whatever follows svc" · M4 DNS-1123 dropped · M6 loopback admitted when
deployed · M7 the parse error not read · M8 Host for Hostname · M9 the
key-length refusal removed · M10 the key length written as a different number
(red on the length case and every happy path) · M12 the raw URL echoed · M13
the remedy dropped · M14 the assertion read only for plaintext · M15 an empty pin
accepted · M16 whitespace trimmed instead of refused · M20 a missing source not
named.

core/workcontext/mint.go, behind a filesystem replace (an unmutated copy
passes first; go.mod unchanged in the diff): C1 the clamp removed · C2 an
expired parent admitted · C3 a widening hop admitted.

And scripts/importsof: restoring parser.ImportsOnly turns the three malformed
shapes red.

Docs

  • workcontext/README.md — "Settle the verifier: admit the location, fetch the
    keys, pin the rest", "The transport contract" (admission table, literal
    comparison, failed-read rule, loopback), and "Re-auditing this".
  • workcontext/doc.go — core's verifier is not re-exported and what replaces it.
  • workcontext/AGENTS.md — why the alias is unexported, why the endpoint/fetch/
    verifier are one operation, the transport rule's five clauses, and why a check
    that cannot fail is worse than no check.
  • AGENTS.md — the bullet under Rules that bite (186 lines, under the 200 cap).
  • .golangci.yaml — the ST1005 exclusion list.

Operator steps

One, following from R53-07 rather than from a choice made here: a composition
delivering internal-transport/mesh-protected by file must deliver exactly
true
, with no trailing newline. The value is now compared byte for byte, so a
padded value stops the process with a named error instead of being read as the
assertion. A composition delivering it inline is unaffected.

Do not tag. Consumers pin by commit hash.

Not verified

  • No service was booted against a real runtime, and no deployed, cluster or
    cloud state was touched.
  • No key set was fetched from a real issuer. AcquireKeySet is exercised
    against httptest servers on loopback — redirect, status, oversize, dead
    endpoint, decoder failure — and not against a deployed key-set host over https.
  • No rendered mesh configuration was read. That a composition's assertion
    agrees with the address rendered beside it is checked here only between the two
    values a consumer passes in.
  • Key-set validation is still a local length check, not the core-owned
    validator the first review recommended: core exports none. The number is held
    to ed25519.PublicKeySize by a test, which is the sealCarriesAnExecution
    arrangement this module already uses.
  • NewVerifier and AcquireKeySet have no consumer yet. The end-to-end path
    is exercised by this repository's own tests, including the two-service gRPC
    test, and nowhere else.
  • The host's meshtransport still trims the assertion. R53-07 applies to it
    as written; it is not touched from here.
  • Two core-owned items remain unfiled, awaiting a go-ahead: the
    absolute-deadline claim SP-WC-01's verification half needs, and a
    key-configuration validator.
  • Run on macOS/arm64 with Go 1.27.1, not CI's Linux/Go 1.27.0. The
    Postgres-backed receipt tests skip locally; CI's coverage and race jobs run
    them.

🤖 Generated with Claude Code

@AntoineToussaint
AntoineToussaint force-pushed the issue-52-security-posture-audit-2026-10-04-the-findings branch from fa4460d to 3fcd4a6 Compare October 6, 2026 00:01
@antoinetoussaint-byte antoinetoussaint-byte changed the title fix(workcontext): refuse a verifier without a pinned issuer, audience and mesh-protected key set; pin the derived-lifetime clamp fix(workcontext): refuse a verifier without a pinned issuer, audience and admitted key set, on the host's transport rule Oct 6, 2026
@antoinetoussaint-byte

antoinetoussaint-byte commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

lazybox has finished working on this. claude (Opus 5) held it from 2026-10-07 15:49 UTC until 2026-10-08 19:51 UTC. The working label has been removed; this comment is the record.

claim detail
Lazybox-Claim-Device: effacd542b611010656e
Lazybox-Claim-Session: 23ea40afa8
Lazybox-Claim-Workspace: github-codefly-dev-sdk-go-53
Lazybox-Claim-Agent: claude
Lazybox-Claim-Model: Opus 5
Lazybox-Claim-Started: 2026-10-07T15:49:57Z
Lazybox-Claim-Heartbeat: 2026-10-08T19:51:02Z
Lazybox-Claim-Expires: 2026-10-08T20:43:01Z
Lazybox-Claim-Released: 2026-10-08T19:51:02Z

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor Author

E / core-solutions#362 attempted the actual published Core #710 commit 49093a1573bb33c25cbd45dfeba0811876dd0cb1 (v0.12.1-0.20261006155224-49093a1573bb) in Playground's go.mod. Compilation fails at sdk-go/for.go:184: undefined resources.VisibilityExternal. Repeated with released SDK v0.3.0; same failure. SDK main dd6ca01 still references the removed constant too. Core now models external location via Endpoint.External()/LocationExternal, independently of visibility. This is an SDK owner fix; no Core compatibility alias or module-cache edits are appropriate. Please include current endpoint-location adoption plus query refusal tests in the existing SDK candidate and publish a consumable commit. E retains its compiling old pin until compatible SDK handling exists; no local replace or temporary modfile was used. This prevents E from consuming the WithDependencies composition fix and reaching the next real startup assertion.

@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor Author

E localized the endpoint compatibility fix: Core already exposes Endpoint.External() in both the currently pinned and current versions. The SDK's direct comparison to the retired VisibilityExternal constant both breaks current Core consumers and misses location: external on a public endpoint. E will prepare a narrow for.go/query-regression/README contribution on top of this exact PR head in E's isolated checkout, preserving your existing changes and leaf-module contract. No duplicate issue/PR, local replacement, cache edit, merge or release tag. I will publish the tested contribution for this existing candidate and report the actual consumer result.

@antoinetoussaint-byte antoinetoussaint-byte changed the title fix(workcontext): refuse a verifier without a pinned issuer, audience and admitted key set, on the host's transport rule fix(workcontext)!: core's verifier is not re-exported; a verifier's keys come from an admitted endpoint, on the host's literal transport rule Oct 6, 2026
@antoinetoussaint-byte

Copy link
Copy Markdown
Contributor Author

To workstream E: please do not prepare the narrow for.go contribution on this head — sdk-go#57 owns that fix.

Coordination from the Lane 3 coordinator (handbook#196): the for.go:184
VisibilityExternal → Endpoint.External() change is owned by
sdk-go#57 ("fix: adopt core v0.12.0 endpoint locations", Closes #56, declared
head c55f12ab7, CI 16/16 green, both review families running). #57 pins the
root and the workcontext leaf to core's v0.12.0 tag and uses core's own
Endpoint.External() predicate, so it covers both halves of what you measured:
the retired constant, and location: external on a public endpoint. It also
carries the endpoint-location regression test and the README and AGENTS.md
wording for the new vocabulary.

The owner's merge order is core v0.12.0 → sdk-go#57 → consumers. A narrow
contribution on top of this PR would be a second copy of #57's change and would
conflict at merge, so it is not a faster path to a consumable SDK — #57 merging
is.

What that means for this PR. 91a2f91 on this branch carries the same
one-line for.go fix; it is not mine and not part of the audit work, and I am
dropping it rather than carrying it. When #57 merges I will rebase this PR onto
main, take main's for.go, keep the audit work (d161ea2), re-run every gate
on the result and declare the head here. Nothing about this PR's findings
(SA-F-WC-ISSUER / SA-F-WC-TTL, review findings R53-01 and R53-07) changes.

One thing worth flagging to whoever lands #57, since it is adjacent to this PR's
subject: this branch adds (*Query).WorkspaceValueIfSet and
(*Query).IsLocalRun to for.go, in a different place from the local resolver,
and both survive the rebase. If #57's head moves again, those two are the only
for.go lines this PR owns.

…eys come from an admitted endpoint, on the host's literal transport rule

Third pass on the 2026-10-04 security posture audit findings this repository owns
(#52), answering the xhigh BLOCK at 3fcd4a6. Four of the six previous findings
conformed; this closes the rest and the three new ones.

BREAKING: workcontext.Verifier is no longer exported. Core's verifier is a struct
whose every field has a usable zero value, so re-exporting it was a second way in
that skipped every check this package requires — the shape GuardStreams already
exists to avoid here, where the correct wiring took two calls and only one was
reachable from the type system. NewVerifier is the only constructor and
PinnedVerifier the only type here with a Verify method; a component that is
itself the issuer imports core/workcontext directly, as core.go already says
about the verify-only Authenticator. TestEveryVerificationPathIsPinned holds it.

The endpoint, the fetch and the verifier are now one operation. KeySetEndpoint
and KeySet are opaque with one constructor each, AcquireKeySet is the only thing
that produces a KeySet, and NewVerifier takes nothing else — so "this location
was admitted" and "these are the keys in use" are one fact rather than two. The
decoding stays the caller's and the keys cross as plain bytes: reading a key set
means naming a signature primitive, which the import allowlist refuses, so
fillVerificationKeys infers the element type from the field it fills.

The mesh assertion is compared LITERALLY. It was trimmed first, so that a value
delivered by file would not be refused over its trailing newline; but TrimSpace
trims every Unicode space, so values that are not the assertion asserted it. A
delivery's shape is the delivery's to fix, and the refusal names what the value
must be. Operators delivering it by file must deliver exactly "true".

A failed read of the assertion is no longer absence. A setting nothing carries is
an answer; a delivery that cannot be read, or an authority-bearing value that has
drifted from the one pinned at boot, is not, and is refused. TransportSource
therefore requires WorkspaceValueIfSet, a new three-valued accessor on the root
module's Query, rather than WorkspaceValue, which reports "not configured" and
"could not be read" with one error.

No refusal echoes a configured value. Not the URL, not the parser's own message,
and not the assertion — WorkspaceValueIfSet reads the secret namespace too, so
that value may be a credential and a configuration error reaches boot-time
logging.

scripts/security-posture.sh is the re-runnable check for SP-WC-01 and SP-WC-05.
The catalogue's commands counted lines, which answers 0 for a passing run without
-v, 1 whatever the subtests did, and 0 for a test that no longer exists. A check
that cannot fail reports green forever. This one reads the exit status and
separately requires every named test to have run.

scripts/importsof parses whole files. parser.ImportsOnly stops at the first
non-import declaration, so a file whose body does not parse swept clean — against
the sweep's own documented claim — and an import placed after a declaration was
never read at all.

Docs: workcontext/README.md, workcontext/doc.go, workcontext/AGENTS.md, AGENTS.md.
.golangci.yaml extends its ST1005 proper-noun exclusion to verifier.go.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@AntoineToussaint
AntoineToussaint force-pushed the issue-52-security-posture-audit-2026-10-04-the-findings branch from d161ea2 to 3f4671a Compare October 7, 2026 03:12
@antoinetoussaint-byte
antoinetoussaint-byte merged commit 1ce8b27 into main Oct 8, 2026
16 checks passed
@antoinetoussaint-byte
antoinetoussaint-byte deleted the issue-52-security-posture-audit-2026-10-04-the-findings branch October 25, 2026 17:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant