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) }