iop/apps/control-plane/internal/credentialseal/aead_test.go
toki 4c8441e6c9 feat(credential): Provider Credential Slot 라우팅을 구현한다
사용자별 credential 저장, lease, projection, runtime 전달과 OpenAI-compatible 계약 및 검증 근거를 함께 반영한다.
2026-08-02 09:10:11 +09:00

364 lines
13 KiB
Go

package credentialseal
import (
"bytes"
"context"
"encoding/base64"
"fmt"
"os"
"path/filepath"
"strings"
"testing"
"github.com/stretchr/testify/require"
"iop/apps/control-plane/internal/credentialstore"
)
// keyMaterial returns base64 of a deterministic 32-byte key filled with fill.
func keyMaterial(fill byte) string {
return base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{fill}, aeadKeyLength))
}
// manifestYAML renders a strict manifest for the given entries.
func manifestYAML(entries ...manifestKey) string {
var sb strings.Builder
sb.WriteString("keys:\n")
for _, e := range entries {
sb.WriteString(fmt.Sprintf(" - id: %q\n version: %d\n material: %q\n", e.ID, e.Version, e.Material))
}
return sb.String()
}
func newTestKeyring(t *testing.T, active keyRef, entries ...manifestKey) *Keyring {
t.Helper()
kr, err := newKeyring([]byte(manifestYAML(entries...)), active, nil)
require.NoError(t, err)
return kr
}
func writeFileMode(t *testing.T, path, body string, perm os.FileMode) {
t.Helper()
require.NoError(t, os.WriteFile(path, []byte(body), perm))
}
func testContext() Context {
return Context{PrincipalID: "principal-1", SlotID: "slot-1", Kind: "bearer"}
}
func TestKeyringSealOpenRoundTrip(t *testing.T) {
kr := newTestKeyring(t, keyRef{"primary", 1}, manifestKey{"primary", 1, keyMaterial(0x11)})
ctx := context.Background()
plaintext := []byte("provider-secret-payload")
env, err := kr.Seal(ctx, plaintext, testContext())
require.NoError(t, err)
require.Equal(t, AlgorithmAES256GCM, env.Algorithm)
require.Equal(t, "primary", env.KeyID)
require.Equal(t, uint64(1), env.KeyVersion)
require.Len(t, env.Nonce, 12)
require.NotEmpty(t, env.Ciphertext)
require.NotEmpty(t, env.AAD)
require.NotEqual(t, plaintext, env.Ciphertext)
require.NotContains(t, string(env.Ciphertext), "provider-secret-payload")
opened, err := kr.Open(ctx, env, testContext())
require.NoError(t, err)
require.Equal(t, plaintext, opened)
}
func TestKeyringNonceUniqueness(t *testing.T) {
kr := newTestKeyring(t, keyRef{"primary", 1}, manifestKey{"primary", 1, keyMaterial(0x22)})
ctx := context.Background()
plaintext := []byte("same-plaintext")
env1, err := kr.Seal(ctx, plaintext, testContext())
require.NoError(t, err)
env2, err := kr.Seal(ctx, plaintext, testContext())
require.NoError(t, err)
require.NotEqual(t, env1.Nonce, env2.Nonce)
require.NotEqual(t, env1.Ciphertext, env2.Ciphertext)
}
func TestKeyringWrongContextFails(t *testing.T) {
kr := newTestKeyring(t, keyRef{"primary", 1}, manifestKey{"primary", 1, keyMaterial(0x33)})
ctx := context.Background()
env, err := kr.Seal(ctx, []byte("secret"), testContext())
require.NoError(t, err)
for _, wrong := range []Context{
{PrincipalID: "other", SlotID: "slot-1", Kind: "bearer"},
{PrincipalID: "principal-1", SlotID: "other", Kind: "bearer"},
{PrincipalID: "principal-1", SlotID: "slot-1", Kind: "api_key"},
} {
_, err := kr.Open(ctx, env, wrong)
require.ErrorIs(t, err, ErrOpenFailed)
}
}
func TestKeyringTamperFails(t *testing.T) {
kr := newTestKeyring(t, keyRef{"primary", 1}, manifestKey{"primary", 1, keyMaterial(0x44)})
ctx := context.Background()
base, err := kr.Seal(ctx, []byte("tamper-target"), testContext())
require.NoError(t, err)
tamperCiphertext := clone(base)
tamperCiphertext.Ciphertext[0] ^= 0xFF
_, err = kr.Open(ctx, tamperCiphertext, testContext())
require.ErrorIs(t, err, ErrOpenFailed)
tamperNonce := clone(base)
tamperNonce.Nonce[0] ^= 0xFF
_, err = kr.Open(ctx, tamperNonce, testContext())
require.ErrorIs(t, err, ErrOpenFailed)
tamperAAD := clone(base)
tamperAAD.AAD[len(tamperAAD.AAD)-1] ^= 0xFF
_, err = kr.Open(ctx, tamperAAD, testContext())
require.ErrorIs(t, err, ErrOpenFailed)
}
func clone(env credentialstore.SecretEnvelope) credentialstore.SecretEnvelope {
return credentialstore.SecretEnvelope{
Algorithm: env.Algorithm,
KeyID: env.KeyID,
KeyVersion: env.KeyVersion,
Nonce: append([]byte(nil), env.Nonce...),
Ciphertext: append([]byte(nil), env.Ciphertext...),
AAD: append([]byte(nil), env.AAD...),
}
}
func TestKeyringKeyVersionSelection(t *testing.T) {
ctx := context.Background()
// Two versions of the same key id with distinct material.
entries := []manifestKey{{"primary", 1, keyMaterial(0x55)}, {"primary", 2, keyMaterial(0x66)}}
sealV1 := newTestKeyring(t, keyRef{"primary", 1}, entries...)
envV1, err := sealV1.Seal(ctx, []byte("v1-secret"), testContext())
require.NoError(t, err)
require.Equal(t, uint64(1), envV1.KeyVersion)
sealV2 := newTestKeyring(t, keyRef{"primary", 2}, entries...)
envV2, err := sealV2.Seal(ctx, []byte("v2-secret"), testContext())
require.NoError(t, err)
require.Equal(t, uint64(2), envV2.KeyVersion)
// A keyring holding both versions opens either revision by its version.
both := newTestKeyring(t, keyRef{"primary", 2}, entries...)
openedV1, err := both.Open(ctx, envV1, testContext())
require.NoError(t, err)
require.Equal(t, []byte("v1-secret"), openedV1)
openedV2, err := both.Open(ctx, envV2, testContext())
require.NoError(t, err)
require.Equal(t, []byte("v2-secret"), openedV2)
// A keyring missing v1 cannot open a v1 envelope.
v2Only := newTestKeyring(t, keyRef{"primary", 2}, manifestKey{"primary", 2, keyMaterial(0x66)})
_, err = v2Only.Open(ctx, envV1, testContext())
require.ErrorIs(t, err, ErrOpenFailed)
has, err := both.HasEnvelopeKey(ctx, "primary", 1)
require.NoError(t, err)
require.True(t, has)
has, err = v2Only.HasEnvelopeKey(ctx, "primary", 1)
require.NoError(t, err)
require.False(t, has)
}
func TestKeyringRestartOpensAcrossReload(t *testing.T) {
ctx := context.Background()
dir := t.TempDir()
path := filepath.Join(dir, "manifest.yaml")
writeFileMode(t, path, manifestYAML(manifestKey{"primary", 1, keyMaterial(0x77)}), 0o600)
cfg := FileConfig{KeyFile: path, ActiveKeyID: "primary", ActiveKeyVersion: 1}
kr1, err := LoadFile(cfg)
require.NoError(t, err)
require.NotNil(t, kr1)
env, err := kr1.Seal(ctx, []byte("restart-secret"), testContext())
require.NoError(t, err)
// A fresh process reloads the same external file and opens the envelope.
kr2, err := LoadFile(cfg)
require.NoError(t, err)
opened, err := kr2.Open(ctx, env, testContext())
require.NoError(t, err)
require.Equal(t, []byte("restart-secret"), opened)
}
func TestKeyringErrorsAndEnvelopeCarryNoSecret(t *testing.T) {
ctx := context.Background()
material := keyMaterial(0x88)
kr := newTestKeyring(t, keyRef{"primary", 1}, manifestKey{"primary", 1, material})
secret := "PLAINTEXT_SECRET_SENTINEL_1234567890"
env, err := kr.Seal(ctx, []byte(secret), testContext())
require.NoError(t, err)
// No envelope field may carry the plaintext or the raw key material.
for _, field := range [][]byte{env.Nonce, env.Ciphertext, env.AAD} {
require.NotContains(t, string(field), secret)
require.NotContains(t, string(field), material)
}
// Constant errors never carry secret material.
_, openErr := kr.Open(ctx, env, Context{PrincipalID: "wrong"})
require.ErrorIs(t, openErr, ErrOpenFailed)
for _, e := range []error{ErrSealFailed, ErrOpenFailed, openErr} {
require.NotContains(t, e.Error(), secret)
require.NotContains(t, e.Error(), material)
}
}
func TestKeyringUnknownVersionAtOpen(t *testing.T) {
ctx := context.Background()
kr := newTestKeyring(t, keyRef{"primary", 1}, manifestKey{"primary", 1, keyMaterial(0x99)})
env, err := kr.Seal(ctx, []byte("secret"), testContext())
require.NoError(t, err)
env.KeyVersion = 99
_, err = kr.Open(ctx, env, testContext())
require.ErrorIs(t, err, ErrOpenFailed)
}
// ---------------------------------------------------------------------------
// LoadFile: config, manifest, and secure-file behavior
// ---------------------------------------------------------------------------
func TestLoadFileDisabledWhenAllOmitted(t *testing.T) {
kr, err := LoadFile(FileConfig{})
require.NoError(t, err)
require.Nil(t, kr)
}
func TestLoadFilePartialConfigFails(t *testing.T) {
for name, cfg := range map[string]FileConfig{
"only-file": {KeyFile: "/tmp/x"},
"only-id": {ActiveKeyID: "primary"},
"only-version": {ActiveKeyVersion: 1},
"file-id": {KeyFile: "/tmp/x", ActiveKeyID: "primary"},
"file-version": {KeyFile: "/tmp/x", ActiveKeyVersion: 1},
"id-version": {ActiveKeyID: "primary", ActiveKeyVersion: 1},
} {
t.Run(name, func(t *testing.T) {
_, err := LoadFile(cfg)
require.ErrorIs(t, err, ErrConfigIncomplete)
})
}
}
func TestLoadFileValidManifest(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "manifest.yaml")
writeFileMode(t, path, manifestYAML(manifestKey{"primary", 1, keyMaterial(0x01)}), 0o600)
kr, err := LoadFile(FileConfig{KeyFile: path, ActiveKeyID: "primary", ActiveKeyVersion: 1})
require.NoError(t, err)
require.NotNil(t, kr)
_, err = kr.Seal(context.Background(), []byte("secret"), testContext())
require.NoError(t, err)
}
func TestLoadFileAtomicSymlinkAndSwap(t *testing.T) {
ctx := context.Background()
dir := t.TempDir()
// Kubernetes-style secret mount: manifest.yaml -> ..data/manifest.yaml,
// ..data -> a versioned data directory that is swapped atomically.
dataV1 := filepath.Join(dir, "..2026_01_data")
require.NoError(t, os.Mkdir(dataV1, 0o700))
writeFileMode(t, filepath.Join(dataV1, "manifest.yaml"), manifestYAML(manifestKey{"primary", 1, keyMaterial(0x0A)}), 0o600)
require.NoError(t, os.Symlink(dataV1, filepath.Join(dir, "..data")))
require.NoError(t, os.Symlink(filepath.Join("..data", "manifest.yaml"), filepath.Join(dir, "manifest.yaml")))
keyFile := filepath.Join(dir, "manifest.yaml")
cfg := FileConfig{KeyFile: keyFile, ActiveKeyID: "primary", ActiveKeyVersion: 1}
krOld, err := LoadFile(cfg)
require.NoError(t, err)
require.NotNil(t, krOld)
envOld, err := krOld.Seal(ctx, []byte("secret"), testContext())
require.NoError(t, err)
// Atomically swap ..data to a new versioned directory with different key
// material for the same id/version.
dataV2 := filepath.Join(dir, "..2026_02_data")
require.NoError(t, os.Mkdir(dataV2, 0o700))
writeFileMode(t, filepath.Join(dataV2, "manifest.yaml"), manifestYAML(manifestKey{"primary", 1, keyMaterial(0x0B)}), 0o600)
tmpLink := filepath.Join(dir, "..data_tmp")
require.NoError(t, os.Symlink(dataV2, tmpLink))
require.NoError(t, os.Rename(tmpLink, filepath.Join(dir, "..data")))
krNew, err := LoadFile(cfg)
require.NoError(t, err)
require.NotNil(t, krNew)
// The reload picked up the swapped material: the new keyring cannot open a
// ciphertext sealed under the old material even at the same id/version.
_, err = krNew.Open(ctx, envOld, testContext())
require.ErrorIs(t, err, ErrOpenFailed)
}
func TestLoadFileDanglingSymlink(t *testing.T) {
dir := t.TempDir()
link := filepath.Join(dir, "manifest.yaml")
require.NoError(t, os.Symlink(filepath.Join(dir, "nonexistent"), link))
_, err := LoadFile(FileConfig{KeyFile: link, ActiveKeyID: "primary", ActiveKeyVersion: 1})
require.ErrorIs(t, err, ErrKeyFileAccess)
}
func TestLoadFileMissingFile(t *testing.T) {
_, err := LoadFile(FileConfig{KeyFile: filepath.Join(t.TempDir(), "absent.yaml"), ActiveKeyID: "primary", ActiveKeyVersion: 1})
require.ErrorIs(t, err, ErrKeyFileAccess)
}
func TestLoadFileNonRegularTarget(t *testing.T) {
dir := t.TempDir()
sub := filepath.Join(dir, "adir")
require.NoError(t, os.Mkdir(sub, 0o700))
_, err := LoadFile(FileConfig{KeyFile: sub, ActiveKeyID: "primary", ActiveKeyVersion: 1})
require.ErrorIs(t, err, ErrKeyFileInsecure)
}
func TestLoadFileInsecurePermissions(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "manifest.yaml")
writeFileMode(t, path, manifestYAML(manifestKey{"primary", 1, keyMaterial(0x02)}), 0o644)
_, err := LoadFile(FileConfig{KeyFile: path, ActiveKeyID: "primary", ActiveKeyVersion: 1})
require.ErrorIs(t, err, ErrKeyFileInsecure)
}
func TestLoadFileManifestErrors(t *testing.T) {
dir := t.TempDir()
cases := map[string]struct {
body string
want error
}{
"malformed": {"keys: [this is not valid", ErrManifest},
"unknown-field": {manifestYAML(manifestKey{"primary", 1, keyMaterial(0x03)}) + "extra: value\n", ErrManifest},
"empty": {"keys: []\n", ErrManifest},
"duplicate": {manifestYAML(manifestKey{"primary", 1, keyMaterial(0x03)}, manifestKey{"primary", 1, keyMaterial(0x04)}), ErrManifest},
"zero-version": {manifestYAML(manifestKey{"primary", 0, keyMaterial(0x03)}), ErrManifest},
"empty-id": {manifestYAML(manifestKey{"", 1, keyMaterial(0x03)}), ErrManifest},
"bad-key-length": {manifestYAML(manifestKey{"primary", 1, base64.StdEncoding.EncodeToString(bytes.Repeat([]byte{0x1}, 16))}), ErrKeyMaterial},
"bad-key-encoding": {manifestYAML(manifestKey{"primary", 1, "not-base64!!!"}), ErrKeyMaterial},
}
for name, tc := range cases {
t.Run(name, func(t *testing.T) {
path := filepath.Join(dir, name+".yaml")
writeFileMode(t, path, tc.body, 0o600)
_, err := LoadFile(FileConfig{KeyFile: path, ActiveKeyID: "primary", ActiveKeyVersion: 1})
require.ErrorIs(t, err, tc.want)
})
}
}
func TestLoadFileUnknownActiveKey(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "manifest.yaml")
writeFileMode(t, path, manifestYAML(manifestKey{"primary", 1, keyMaterial(0x05)}), 0o600)
_, err := LoadFile(FileConfig{KeyFile: path, ActiveKeyID: "primary", ActiveKeyVersion: 9})
require.ErrorIs(t, err, ErrUnknownActiveKey)
}