Conversation
Agents have signed their responses since the GHSA-3jh4 fix, but nothing checked those signatures. The controller verified with pkiSigner.ControllerPublicKey(), which on the controller is the field an agent fills in at enrollment and is therefore nil there — so every response took the "no key available" path and was returned unverified. Verification needs to know whose key to check against, and a response carried no identity a verifier could trust: job.Response has only a self-reported hostname, the envelope only a self-reported fingerprint, and the KV key is derived from the hostname. The registry maps hostnames to machine IDs, but that is the unauthenticated surface GHSA-j73r describes, so using it would let an accepted agent repoint a hostname at its own key and pass verification — defeating the thing this fixes. So the signer now stamps its machine ID on the envelope. That field is self-reported and is treated as nothing more than an index: it selects which stored record to verify against, and a signature that does not match that record's key is rejected. The hostname in the payload must also match the one recorded at acceptance, so an accepted agent cannot answer for a host it did not enrol as. All three response paths fail closed: a single-target job reports failure, and a broadcast drops the response so that agent shows as not having answered rather than as answered by whoever sent it. Every rejection carries a distinct cause. Enforcement is opt-in: with no store wired the old path runs unchanged. Closes the deferred half of GHSA-3jh4. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FuKUsHFG1EqZXamffh9M2c
Codecov Report✅ All modified and coverable lines are covered by tests. @@ Coverage Diff @@
## main #517 +/- ##
=======================================
Coverage 99.95% 99.95%
=======================================
Files 490 491 +1
Lines 23249 23334 +85
=======================================
+ Hits 23239 23324 +85
Misses 10 10
Continue to review full report in Codecov by Harness.
🚀 New features to boost your workflow:
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
US1 (T014-T019) of
components/osapi/specs/002-agent-key-store, on top of thestore merged in #516. Closes GHSA-3jh4's deferred half.
Why verification was inert
The controller verified responses with
pkiSigner.ControllerPublicKey(). Thatfield holds the controller's key as an agent records it at enrollment, so
on the controller it is nil — every response took the "no public key available"
branch and was returned unverified. The signing added for GHSA-3jh4 was
therefore decorative on the receiving end.
The problem the task list missed
T014 says "look up by the responding agent's machine ID" — but a response
carries no machine ID.
job.Responsehas a self-reportedHostname, theenvelope a self-reported
Fingerprint, and the KV key isresponses.<jobID>.<hostname>.<nano>. The store is keyed by machine ID.The registry does map hostname to machine ID, but that is exactly the
unauthenticated surface GHSA-j73r describes: an accepted-but-hostile agent
could repoint a hostname at its own key and sail through verification. Using it
would defeat the purpose of this change.
Resolution: the signer stamps its machine ID into
SignedEnvelope. Thefield is self-reported and is treated purely as an index — it chooses which
stored record to verify against, and a signature that doesn't match that
record's key is rejected. The payload's hostname must also equal the hostname
recorded at acceptance, so an accepted agent cannot answer for a host it never
enrolled as. Old agents omit the field and are refused while enforcing, which
is the staged rollout FR-009 describes.
Shape
AgentKeyStore+AgentKeyare declared ininternal/job/client, mirroringPKISigner, andenrollmentadapts to them. The dependency runscontroller -> shared client, never the reverse, and the error mapping lives in
a package held at 100% coverage rather than in
cmd/.invalid, hostname mismatch.
and the stored-response path drop it.
SetAgentKeyStore(nil)is the pre-enforcement behaviour, byte for byte.Gate
just react-build && just ready && just testgreen: coverage 100.0% againstthe 99.9% target. The only zero-count blocks in the filtered profile are the
eight pre-existing documented-unreachable ones; none are in this change.
🤖 Generated with Claude Code
https://claude.ai/code/session_01FuKUsHFG1EqZXamffh9M2c