P1 (part 1): move skilltools core -> tool/ (clean, verbatim)
executus CI / test (push) Successful in 36s
executus CI / test (push) Successful in 36s
The tool registry core (registry, permission model, Invocation, gated-tool wrapper, ssrf guard, hmac, encryption, argcoerce, helpers, rootrun, session_tools, webhook_rate_limit) had zero mort coupling — it imports only majordomo/llm + x/crypto/hkdf — so it moves verbatim with a package rename (skilltools -> tool). All same-package tests came along and pass; the SSRF, gated-wrapper, encryption and output-pattern invariants are re-anchored here. majordomo re-enters the module graph (now pinned to the latest, incl. the front-loaded-output fix). model/ + llmmeta + structured follow next. Docs: CLAUDE.md now requires README/examples to stay in sync with changes in the same commit; CI skips docs/example-only pushes via paths-ignore. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,205 @@
|
||||
package tool
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Why: round-trip is the bedrock — without it, every other test is
|
||||
// meaningless. What: encrypt then decrypt; assert plaintext returns.
|
||||
// Test: write a non-trivial plaintext and confirm exact byte equality.
|
||||
func TestEncryption_RoundTrip(t *testing.T) {
|
||||
t.Parallel()
|
||||
master := masterTestKey()
|
||||
key, err := DeriveSkillKey(master, "skill-abc")
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveSkillKey: %v", err)
|
||||
}
|
||||
plaintext := []byte(`{"hello":"world","n":42,"arr":[1,2,3]}`)
|
||||
envelope, err := Encrypt(key, plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Encrypt: %v", err)
|
||||
}
|
||||
if envelope[0] != envelopeFormatV1 {
|
||||
t.Fatalf("envelope[0] = %d, want %d", envelope[0], envelopeFormatV1)
|
||||
}
|
||||
got, err := Decrypt(key, envelope)
|
||||
if err != nil {
|
||||
t.Fatalf("Decrypt: %v", err)
|
||||
}
|
||||
if !bytes.Equal(got, plaintext) {
|
||||
t.Fatalf("round-trip mismatch:\n got: %q\nwant: %q", got, plaintext)
|
||||
}
|
||||
}
|
||||
|
||||
// Why: GCM is authenticated encryption — flipping any bit MUST be
|
||||
// detected. What: tamper with the ciphertext; assert ErrEncryptionTampered.
|
||||
// Test: flip one byte of the ciphertext suffix, decrypt, expect tamper error.
|
||||
func TestEncryption_TamperDetected(t *testing.T) {
|
||||
t.Parallel()
|
||||
master := masterTestKey()
|
||||
key, err := DeriveSkillKey(master, "skill-tamper")
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveSkillKey: %v", err)
|
||||
}
|
||||
envelope, err := Encrypt(key, []byte("sensitive data"))
|
||||
if err != nil {
|
||||
t.Fatalf("Encrypt: %v", err)
|
||||
}
|
||||
// Flip a byte in the ciphertext (after version + nonce).
|
||||
envelope[1+gcmNonceSize] ^= 0x01
|
||||
_, err = Decrypt(key, envelope)
|
||||
if !errors.Is(err, ErrEncryptionTampered) {
|
||||
t.Fatalf("Decrypt after tamper = %v, want ErrEncryptionTampered", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Why: nonce reuse under GCM is catastrophic — verify the impl uses
|
||||
// fresh randomness on every call. What: encrypt the same plaintext twice;
|
||||
// the envelopes must differ.
|
||||
func TestEncryption_FreshNoncePerCall(t *testing.T) {
|
||||
t.Parallel()
|
||||
master := masterTestKey()
|
||||
key, err := DeriveSkillKey(master, "skill-nonce")
|
||||
if err != nil {
|
||||
t.Fatalf("DeriveSkillKey: %v", err)
|
||||
}
|
||||
plaintext := []byte("fixed payload")
|
||||
a, err := Encrypt(key, plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Encrypt #1: %v", err)
|
||||
}
|
||||
b, err := Encrypt(key, plaintext)
|
||||
if err != nil {
|
||||
t.Fatalf("Encrypt #2: %v", err)
|
||||
}
|
||||
if bytes.Equal(a, b) {
|
||||
t.Fatalf("two encryptions of the same plaintext produced identical envelopes (nonce not random)")
|
||||
}
|
||||
// Both must still decrypt to the same plaintext.
|
||||
for i, env := range [][]byte{a, b} {
|
||||
got, err := Decrypt(key, env)
|
||||
if err != nil {
|
||||
t.Fatalf("Decrypt #%d: %v", i, err)
|
||||
}
|
||||
if !bytes.Equal(got, plaintext) {
|
||||
t.Fatalf("Decrypt #%d mismatch", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Why: per-skill key derivation MUST give different skills different
|
||||
// keys so a leaked skillkey doesn't cross-decrypt. What: derive keys
|
||||
// for skill A and skill B; encrypt under A; decrypt under B; expect
|
||||
// tamper error.
|
||||
func TestEncryption_PerSkillIsolation(t *testing.T) {
|
||||
t.Parallel()
|
||||
master := masterTestKey()
|
||||
keyA, _ := DeriveSkillKey(master, "skill-a")
|
||||
keyB, _ := DeriveSkillKey(master, "skill-b")
|
||||
if bytes.Equal(keyA, keyB) {
|
||||
t.Fatalf("derived keys for distinct skills are identical (HKDF salt not effective)")
|
||||
}
|
||||
envelope, err := Encrypt(keyA, []byte("only skill A may read"))
|
||||
if err != nil {
|
||||
t.Fatalf("Encrypt: %v", err)
|
||||
}
|
||||
_, err = Decrypt(keyB, envelope)
|
||||
if !errors.Is(err, ErrEncryptionTampered) {
|
||||
t.Fatalf("Decrypt under wrong skill key = %v, want ErrEncryptionTampered", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Why: a future format change must be detectable, not silently
|
||||
// corrupting reads. What: hand-craft an envelope with version byte
|
||||
// 0xFF; assert ErrEncryptionUnknownVersion.
|
||||
func TestEncryption_UnknownVersionRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
key := make([]byte, 32)
|
||||
envelope := make([]byte, 1+gcmNonceSize+16)
|
||||
envelope[0] = 0xFF
|
||||
_, err := Decrypt(key, envelope)
|
||||
if !errors.Is(err, ErrEncryptionUnknownVersion) {
|
||||
t.Fatalf("Decrypt with bad version = %v, want ErrEncryptionUnknownVersion", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Why: short inputs must not panic. What: feed a 5-byte envelope to
|
||||
// Decrypt; assert ErrEncryptionShortInput.
|
||||
func TestEncryption_ShortInputRejected(t *testing.T) {
|
||||
t.Parallel()
|
||||
key := make([]byte, 32)
|
||||
_, err := Decrypt(key, []byte{1, 2, 3, 4, 5})
|
||||
if !errors.Is(err, ErrEncryptionShortInput) {
|
||||
t.Fatalf("Decrypt with short input = %v, want ErrEncryptionShortInput", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Why: empty master = encryption disabled. What: DeriveSkillKey with
|
||||
// empty master returns ErrEncryptionDisabled.
|
||||
func TestEncryption_EmptyMasterDisabled(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := DeriveSkillKey(nil, "skill-x")
|
||||
if !errors.Is(err, ErrEncryptionDisabled) {
|
||||
t.Fatalf("DeriveSkillKey(nil) = %v, want ErrEncryptionDisabled", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Why: callers commonly paste random hex/base64 of varying length;
|
||||
// MasterKeyFromEnv should normalize to 32 bytes via SHA-256. What:
|
||||
// set the env var; assert returned bytes match SHA-256 of input.
|
||||
func TestEncryption_MasterKeyFromEnvNormalizesLength(t *testing.T) {
|
||||
// not parallel — we mutate process env
|
||||
const raw = "this-is-a-fake-master-key-for-testing-only-totally-not-secure"
|
||||
t.Setenv(EncryptionMasterKeyEnv, raw)
|
||||
got, present := MasterKeyFromEnv()
|
||||
if !present {
|
||||
t.Fatalf("MasterKeyFromEnv reported absent for non-empty env var")
|
||||
}
|
||||
if len(got) != 32 {
|
||||
t.Fatalf("len(masterKey) = %d, want 32", len(got))
|
||||
}
|
||||
want := sha256.Sum256([]byte(raw))
|
||||
if !bytes.Equal(got, want[:]) {
|
||||
t.Fatalf("masterKey does not match SHA-256 of env var")
|
||||
}
|
||||
}
|
||||
|
||||
// Why: empty env var = encryption off (instance-wide). What:
|
||||
// MasterKeyFromEnv returns nil + present=false.
|
||||
func TestEncryption_MasterKeyFromEnvEmpty(t *testing.T) {
|
||||
t.Setenv(EncryptionMasterKeyEnv, "")
|
||||
got, present := MasterKeyFromEnv()
|
||||
if present {
|
||||
t.Fatalf("MasterKeyFromEnv reported present for empty env var")
|
||||
}
|
||||
if got != nil {
|
||||
t.Fatalf("MasterKeyFromEnv returned non-nil bytes for empty env var")
|
||||
}
|
||||
}
|
||||
|
||||
// Why: defence in depth — explicit non-32 key sizes should error
|
||||
// rather than panic.
|
||||
func TestEncryption_BadKeySize(t *testing.T) {
|
||||
t.Parallel()
|
||||
_, err := Encrypt(make([]byte, 16), []byte("x"))
|
||||
if err == nil {
|
||||
t.Fatalf("Encrypt with 16-byte key did not error")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "32-byte key") {
|
||||
t.Fatalf("error did not mention key size: %v", err)
|
||||
}
|
||||
_, err = Decrypt(make([]byte, 16), make([]byte, 32))
|
||||
if err == nil {
|
||||
t.Fatalf("Decrypt with 16-byte key did not error")
|
||||
}
|
||||
}
|
||||
|
||||
func masterTestKey() []byte {
|
||||
// fixed deterministic master so test runs are reproducible.
|
||||
sum := sha256.Sum256([]byte("test-master-do-not-use-in-prod"))
|
||||
return sum[:]
|
||||
}
|
||||
Reference in New Issue
Block a user