Skip to content

Commit 177e903

Browse files
redcourageclaude
andcommitted
fix(keys): KeysExist must respect WithKeyParts
Background ---------- Vault delivers only the Encryption-side material (EncKey) to clients via the agent manifest — Eval/Sec stay inside Vault for FHE evaluation and re-encryption. A consumer reading that bundle calls OpenKeysFromFile(WithKeyPath(...), ..., WithKeyParts(KeyPartEnc)) OpenKeysFromFile begins with a `KeysExist(opts...)` gate; if it returns false, the open fails fast with ErrKeysNotFound. Bug --- KeysExist walked all three slots (Enc / Eval / Sec) unconditionally, ignoring the caller's WithKeyParts. So the Vault-delivered Enc-only bundle was rejected even when the caller had explicitly said "I only need Enc". Reproducer (the path the rune-mcp boot loop hit on first GetAgentManifest): dir := t.TempDir() os.WriteFile(filepath.Join(dir, "EncKey.json"), []byte("{}"), 0o600) KeysExist( WithKeyPath(dir), WithKeyID("k"), WithKeyDim(1024), WithKeyParts(KeyPartEnc), ) // pre-fix: false → bundle rejected // fixed: true → bundle accepted Fix --- KeysExist now resolves the requested KeyParts via the same helper OpenKeysFromFile uses (resolveKeyParts) and only checks the slots the caller actually requested. Default behavior (no WithKeyParts) is preserved: resolveKeyParts returns enc=true,eval=true,sec=true, so a bare KeysExist(WithKeyPath(...)) still requires all three. Tests ----- TestKeysExist_PartsAware covers: - enc-only dir + WithKeyParts(KeyPartEnc) → true (Vault use case) - enc-only dir + default parts → false (backward compat) - enc-only dir + WithKeyParts(KeyPartEval) → false (other parts still missing) Existing TestKeysExist_FalseWhenEmpty / TestGenerateKeys_CreatesAllThreeFiles keep their prior expectations — both are exercised through the same resolveKeyParts code path now. Discovered while wiring rune-mcp v0.4 (Go port) against Vault's GetAgentManifest flow; the boot loop's envector adapter opens keys with KeyPartEnc only and was bouncing on this gate every retry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 171f75b commit 177e903

2 files changed

Lines changed: 53 additions & 6 deletions

File tree

keys.go

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,19 @@ func KeysExist(opts ...KeysOption) bool {
118118
if o.Path == "" {
119119
return false
120120
}
121-
slots := [3][2]string{
122-
{encKeyBinFile, encKeyJSONFile},
123-
{evalKeyBinFile, evalKeyJSONFile},
124-
{secKeyBinFile, secKeyJSONFile},
121+
wantEnc, wantEval, wantSec := resolveKeyParts(o.Parts)
122+
if wantEnc {
123+
if _, _, ok := resolveKeySlot(o.Path, encKeyBinFile, encKeyJSONFile); !ok {
124+
return false
125+
}
125126
}
126-
for _, s := range slots {
127-
if _, _, ok := resolveKeySlot(o.Path, s[0], s[1]); !ok {
127+
if wantEval {
128+
if _, _, ok := resolveKeySlot(o.Path, evalKeyBinFile, evalKeyJSONFile); !ok {
129+
return false
130+
}
131+
}
132+
if wantSec {
133+
if _, _, ok := resolveKeySlot(o.Path, secKeyBinFile, secKeyJSONFile); !ok {
128134
return false
129135
}
130136
}

keys_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,3 +320,44 @@ func TestRegisterKeys_WithoutEvalPart_ReturnsErr(t *testing.T) {
320320
t.Errorf("ActivateKeys without KeyPartEval: got %v, want ErrKeysNotForRegister", err)
321321
}
322322
}
323+
324+
// TestKeysExist_PartsAware exercises the KeyParts-aware lookup. Vault's
325+
// agent-manifest delivery only ships EncKey to the client (Eval/Sec stay
326+
// in Vault), so the consumer opens that directory with
327+
// WithKeyParts(KeyPartEnc) and expects KeysExist to return true on an
328+
// Enc-only directory. The previous implementation walked all three slots
329+
// unconditionally and rejected the bundle.
330+
func TestKeysExist_PartsAware(t *testing.T) {
331+
encOnly := func(t *testing.T) string {
332+
t.Helper()
333+
dir := t.TempDir()
334+
if err := os.WriteFile(filepath.Join(dir, encKeyJSONFile), []byte("{}"), 0o600); err != nil {
335+
t.Fatal(err)
336+
}
337+
return dir
338+
}
339+
340+
t.Run("enc-only dir + WithKeyParts(KeyPartEnc) → true", func(t *testing.T) {
341+
dir := encOnly(t)
342+
opts := append(baseKeyOpts(dir), WithKeyParts(KeyPartEnc))
343+
if !KeysExist(opts...) {
344+
t.Error("KeysExist with KeyPartEnc must accept Enc-only directory")
345+
}
346+
})
347+
348+
t.Run("enc-only dir + default parts (= all three) → false", func(t *testing.T) {
349+
dir := encOnly(t)
350+
// No WithKeyParts → resolveKeyParts treats it as all three required.
351+
if KeysExist(baseKeyOpts(dir)...) {
352+
t.Error("default parts must require all 3 slots")
353+
}
354+
})
355+
356+
t.Run("enc-only dir + WithKeyParts(KeyPartEval) → false (eval missing)", func(t *testing.T) {
357+
dir := encOnly(t)
358+
opts := append(baseKeyOpts(dir), WithKeyParts(KeyPartEval))
359+
if KeysExist(opts...) {
360+
t.Error("requesting Eval on an Enc-only dir must fail")
361+
}
362+
})
363+
}

0 commit comments

Comments
 (0)