All import paths updated to git.wntrmute.dev/mc/. Bumps mcdsl to v1.2.0. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1026 lines
27 KiB
Go
1026 lines
27 KiB
Go
package transit
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
|
|
"git.wntrmute.dev/mc/metacrypt/internal/barrier"
|
|
"git.wntrmute.dev/mc/metacrypt/internal/engine"
|
|
)
|
|
|
|
// memBarrier is an in-memory barrier for testing.
|
|
type memBarrier struct {
|
|
data map[string][]byte
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func newMemBarrier() *memBarrier {
|
|
return &memBarrier{data: make(map[string][]byte)}
|
|
}
|
|
|
|
func (m *memBarrier) Unseal(_ []byte) error { return nil }
|
|
func (m *memBarrier) Seal() error { return nil }
|
|
func (m *memBarrier) IsSealed() bool { return false }
|
|
|
|
func (m *memBarrier) Get(_ context.Context, path string) ([]byte, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
v, ok := m.data[path]
|
|
if !ok {
|
|
return nil, barrier.ErrNotFound
|
|
}
|
|
cp := make([]byte, len(v))
|
|
copy(cp, v)
|
|
return cp, nil
|
|
}
|
|
|
|
func (m *memBarrier) Put(_ context.Context, path string, value []byte) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
cp := make([]byte, len(value))
|
|
copy(cp, value)
|
|
m.data[path] = cp
|
|
return nil
|
|
}
|
|
|
|
func (m *memBarrier) Delete(_ context.Context, path string) error {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
delete(m.data, path)
|
|
return nil
|
|
}
|
|
|
|
func (m *memBarrier) List(_ context.Context, prefix string) ([]string, error) {
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
var paths []string
|
|
for k := range m.data {
|
|
if strings.HasPrefix(k, prefix) {
|
|
paths = append(paths, strings.TrimPrefix(k, prefix))
|
|
}
|
|
}
|
|
return paths, nil
|
|
}
|
|
|
|
func adminCaller() *engine.CallerInfo {
|
|
return &engine.CallerInfo{Username: "admin", Roles: []string{"admin"}, IsAdmin: true}
|
|
}
|
|
|
|
func userCaller() *engine.CallerInfo {
|
|
return &engine.CallerInfo{Username: "user", Roles: []string{"user"}, IsAdmin: false}
|
|
}
|
|
|
|
func guestCaller() *engine.CallerInfo {
|
|
return &engine.CallerInfo{Username: "guest", Roles: []string{"guest"}, IsAdmin: false}
|
|
}
|
|
|
|
func setupEngine(t *testing.T) (*TransitEngine, *memBarrier) {
|
|
t.Helper()
|
|
b := newMemBarrier()
|
|
eng := NewTransitEngine()
|
|
ctx := context.Background()
|
|
mountPath := "engine/transit/test/"
|
|
|
|
err := eng.Initialize(ctx, b, mountPath, nil)
|
|
if err != nil {
|
|
t.Fatalf("Initialize: %v", err)
|
|
}
|
|
|
|
te := eng.(*TransitEngine)
|
|
return te, b
|
|
}
|
|
|
|
func setupEngineWithUnseal(t *testing.T) (*TransitEngine, *memBarrier) {
|
|
t.Helper()
|
|
te, b := setupEngine(t)
|
|
|
|
// Seal and unseal to test the full lifecycle.
|
|
if err := te.Seal(); err != nil {
|
|
t.Fatalf("Seal: %v", err)
|
|
}
|
|
|
|
eng2 := NewTransitEngine()
|
|
ctx := context.Background()
|
|
if err := eng2.Unseal(ctx, b, "engine/transit/test/"); err != nil {
|
|
t.Fatalf("Unseal: %v", err)
|
|
}
|
|
return eng2.(*TransitEngine), b
|
|
}
|
|
|
|
func createKey(t *testing.T, te *TransitEngine, name, keyType string) {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
_, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "create-key",
|
|
CallerInfo: adminCaller(),
|
|
Data: map[string]interface{}{
|
|
"name": name,
|
|
"type": keyType,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("create-key %s: %v", name, err)
|
|
}
|
|
}
|
|
|
|
func TestInitializeAndUnseal(t *testing.T) {
|
|
te, b := setupEngine(t)
|
|
|
|
// Create a key so unseal has something to load.
|
|
createKey(t, te, "mykey", "aes256-gcm")
|
|
|
|
if err := te.Seal(); err != nil {
|
|
t.Fatalf("Seal: %v", err)
|
|
}
|
|
|
|
eng2 := NewTransitEngine()
|
|
if err := eng2.Unseal(context.Background(), b, "engine/transit/test/"); err != nil {
|
|
t.Fatalf("Unseal: %v", err)
|
|
}
|
|
|
|
te2 := eng2.(*TransitEngine)
|
|
if _, ok := te2.keys["mykey"]; !ok {
|
|
t.Fatal("expected key 'mykey' after unseal")
|
|
}
|
|
}
|
|
|
|
func TestCreateKeyAllTypes(t *testing.T) {
|
|
types := []string{"aes256-gcm", "chacha20-poly", "ed25519", "ecdsa-p256", "ecdsa-p384", "hmac-sha256", "hmac-sha512"}
|
|
for _, kt := range types {
|
|
t.Run(kt, func(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
createKey(t, te, "key-"+kt, kt)
|
|
|
|
ks, ok := te.keys["key-"+kt]
|
|
if !ok {
|
|
t.Fatal("key not created")
|
|
}
|
|
if ks.config.CurrentVersion != 1 {
|
|
t.Fatalf("expected version 1, got %d", ks.config.CurrentVersion)
|
|
}
|
|
if ks.config.MinDecryptionVersion != 1 {
|
|
t.Fatalf("expected min_decryption_version 1, got %d", ks.config.MinDecryptionVersion)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestEncryptDecryptAES(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
createKey(t, te, "aes-key", "aes256-gcm")
|
|
ctx := context.Background()
|
|
|
|
plaintext := base64.StdEncoding.EncodeToString([]byte("hello world"))
|
|
|
|
// Encrypt.
|
|
resp, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "encrypt",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{
|
|
"key": "aes-key",
|
|
"plaintext": plaintext,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("encrypt: %v", err)
|
|
}
|
|
|
|
ct, _ := resp.Data["ciphertext"].(string)
|
|
if !strings.HasPrefix(ct, "metacrypt:v1:") {
|
|
t.Fatalf("unexpected ciphertext format: %s", ct)
|
|
}
|
|
|
|
// Decrypt.
|
|
resp, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "decrypt",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{
|
|
"key": "aes-key",
|
|
"ciphertext": ct,
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("decrypt: %v", err)
|
|
}
|
|
|
|
ptB64, _ := resp.Data["plaintext"].(string)
|
|
decoded, _ := base64.StdEncoding.DecodeString(ptB64)
|
|
if string(decoded) != "hello world" {
|
|
t.Fatalf("expected 'hello world', got %q", string(decoded))
|
|
}
|
|
}
|
|
|
|
func TestEncryptDecryptChaCha(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
createKey(t, te, "chacha-key", "chacha20-poly")
|
|
ctx := context.Background()
|
|
|
|
plaintext := base64.StdEncoding.EncodeToString([]byte("secret data"))
|
|
|
|
resp, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "encrypt",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "chacha-key", "plaintext": plaintext},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("encrypt: %v", err)
|
|
}
|
|
|
|
ct, _ := resp.Data["ciphertext"].(string)
|
|
if !strings.HasPrefix(ct, "metacrypt:v1:") {
|
|
t.Fatalf("unexpected ciphertext format: %s", ct)
|
|
}
|
|
|
|
resp, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "decrypt",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "chacha-key", "ciphertext": ct},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("decrypt: %v", err)
|
|
}
|
|
|
|
ptB64, _ := resp.Data["plaintext"].(string)
|
|
decoded, _ := base64.StdEncoding.DecodeString(ptB64)
|
|
if string(decoded) != "secret data" {
|
|
t.Fatalf("expected 'secret data', got %q", string(decoded))
|
|
}
|
|
}
|
|
|
|
func TestEncryptWithContext(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
createKey(t, te, "ctx-key", "aes256-gcm")
|
|
ctx := context.Background()
|
|
|
|
plaintext := base64.StdEncoding.EncodeToString([]byte("context test"))
|
|
aadB64 := base64.StdEncoding.EncodeToString([]byte("my-context"))
|
|
|
|
resp, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "encrypt",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "ctx-key", "plaintext": plaintext, "context": aadB64},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("encrypt: %v", err)
|
|
}
|
|
ct, _ := resp.Data["ciphertext"].(string)
|
|
|
|
// Decrypt with correct context.
|
|
resp, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "decrypt",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "ctx-key", "ciphertext": ct, "context": aadB64},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("decrypt: %v", err)
|
|
}
|
|
|
|
// Decrypt with wrong context should fail.
|
|
wrongCtx := base64.StdEncoding.EncodeToString([]byte("wrong-context"))
|
|
_, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "decrypt",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "ctx-key", "ciphertext": ct, "context": wrongCtx},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected decrypt to fail with wrong context")
|
|
}
|
|
}
|
|
|
|
func TestKeyRotationAndDecrypt(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
createKey(t, te, "rot-key", "aes256-gcm")
|
|
ctx := context.Background()
|
|
|
|
plaintext := base64.StdEncoding.EncodeToString([]byte("rotate me"))
|
|
|
|
// Encrypt with v1.
|
|
resp, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "encrypt",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "rot-key", "plaintext": plaintext},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("encrypt v1: %v", err)
|
|
}
|
|
ctV1, _ := resp.Data["ciphertext"].(string)
|
|
|
|
// Rotate.
|
|
_, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "rotate-key",
|
|
CallerInfo: adminCaller(),
|
|
Data: map[string]interface{}{"name": "rot-key"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("rotate: %v", err)
|
|
}
|
|
|
|
// Encrypt with v2.
|
|
resp, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "encrypt",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "rot-key", "plaintext": plaintext},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("encrypt v2: %v", err)
|
|
}
|
|
ctV2, _ := resp.Data["ciphertext"].(string)
|
|
|
|
if !strings.HasPrefix(ctV2, "metacrypt:v2:") {
|
|
t.Fatalf("expected v2 ciphertext, got %s", ctV2)
|
|
}
|
|
|
|
// Both ciphertexts should decrypt.
|
|
for _, ct := range []string{ctV1, ctV2} {
|
|
resp, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "decrypt",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "rot-key", "ciphertext": ct},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("decrypt: %v", err)
|
|
}
|
|
ptB64, _ := resp.Data["plaintext"].(string)
|
|
decoded, _ := base64.StdEncoding.DecodeString(ptB64)
|
|
if string(decoded) != "rotate me" {
|
|
t.Fatalf("expected 'rotate me', got %q", string(decoded))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUpdateKeyConfig(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
createKey(t, te, "cfg-key", "aes256-gcm")
|
|
ctx := context.Background()
|
|
|
|
plaintext := base64.StdEncoding.EncodeToString([]byte("old data"))
|
|
|
|
// Encrypt with v1.
|
|
resp, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "encrypt",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "cfg-key", "plaintext": plaintext},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("encrypt: %v", err)
|
|
}
|
|
ctV1, _ := resp.Data["ciphertext"].(string)
|
|
|
|
// Rotate to v2.
|
|
_, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "rotate-key",
|
|
CallerInfo: adminCaller(),
|
|
Data: map[string]interface{}{"name": "cfg-key"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("rotate: %v", err)
|
|
}
|
|
|
|
// Advance min_decryption_version to 2.
|
|
_, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "update-key-config",
|
|
CallerInfo: adminCaller(),
|
|
Data: map[string]interface{}{"name": "cfg-key", "min_decryption_version": float64(2)},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("update-key-config: %v", err)
|
|
}
|
|
|
|
// v1 ciphertext should be rejected.
|
|
_, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "decrypt",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "cfg-key", "ciphertext": ctV1},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected decrypt to fail for v1 ciphertext")
|
|
}
|
|
|
|
// Cannot decrease min_decryption_version.
|
|
_, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "update-key-config",
|
|
CallerInfo: adminCaller(),
|
|
Data: map[string]interface{}{"name": "cfg-key", "min_decryption_version": float64(1)},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error decreasing min_decryption_version")
|
|
}
|
|
|
|
// Cannot exceed current version.
|
|
_, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "update-key-config",
|
|
CallerInfo: adminCaller(),
|
|
Data: map[string]interface{}{"name": "cfg-key", "min_decryption_version": float64(99)},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error exceeding current version")
|
|
}
|
|
}
|
|
|
|
func TestTrimKey(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
createKey(t, te, "trim-key", "aes256-gcm")
|
|
ctx := context.Background()
|
|
|
|
// Rotate to v2, v3.
|
|
for i := 0; i < 2; i++ {
|
|
_, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "rotate-key",
|
|
CallerInfo: adminCaller(),
|
|
Data: map[string]interface{}{"name": "trim-key"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("rotate: %v", err)
|
|
}
|
|
}
|
|
|
|
// Set min_decryption_version to 2.
|
|
_, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "update-key-config",
|
|
CallerInfo: adminCaller(),
|
|
Data: map[string]interface{}{"name": "trim-key", "min_decryption_version": float64(2)},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("update-key-config: %v", err)
|
|
}
|
|
|
|
// Trim.
|
|
resp, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "trim-key",
|
|
CallerInfo: adminCaller(),
|
|
Data: map[string]interface{}{"name": "trim-key"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("trim-key: %v", err)
|
|
}
|
|
|
|
trimmed, _ := resp.Data["trimmed"].(int)
|
|
if trimmed != 1 {
|
|
t.Fatalf("expected 1 trimmed, got %d", trimmed)
|
|
}
|
|
|
|
// Version 1 should be gone.
|
|
if _, ok := te.keys["trim-key"].versions[1]; ok {
|
|
t.Fatal("version 1 should have been trimmed")
|
|
}
|
|
if _, ok := te.keys["trim-key"].versions[2]; !ok {
|
|
t.Fatal("version 2 should still exist")
|
|
}
|
|
}
|
|
|
|
func TestSignVerifyEd25519(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
createKey(t, te, "ed-key", "ed25519")
|
|
ctx := context.Background()
|
|
|
|
input := base64.StdEncoding.EncodeToString([]byte("sign this"))
|
|
|
|
resp, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "sign",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "ed-key", "input": input},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("sign: %v", err)
|
|
}
|
|
sig, _ := resp.Data["signature"].(string)
|
|
if !strings.HasPrefix(sig, "metacrypt:v1:") {
|
|
t.Fatalf("unexpected signature format: %s", sig)
|
|
}
|
|
|
|
// Verify.
|
|
resp, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "verify",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "ed-key", "input": input, "signature": sig},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("verify: %v", err)
|
|
}
|
|
valid, _ := resp.Data["valid"].(bool)
|
|
if !valid {
|
|
t.Fatal("expected valid signature")
|
|
}
|
|
|
|
// Wrong input should fail verification.
|
|
wrongInput := base64.StdEncoding.EncodeToString([]byte("wrong data"))
|
|
resp, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "verify",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "ed-key", "input": wrongInput, "signature": sig},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("verify: %v", err)
|
|
}
|
|
valid, _ = resp.Data["valid"].(bool)
|
|
if valid {
|
|
t.Fatal("expected invalid signature for wrong input")
|
|
}
|
|
}
|
|
|
|
func TestSignVerifyECDSA(t *testing.T) {
|
|
for _, keyType := range []string{"ecdsa-p256", "ecdsa-p384"} {
|
|
t.Run(keyType, func(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
createKey(t, te, "ec-key", keyType)
|
|
ctx := context.Background()
|
|
|
|
input := base64.StdEncoding.EncodeToString([]byte("ecdsa test"))
|
|
|
|
resp, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "sign",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "ec-key", "input": input},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("sign: %v", err)
|
|
}
|
|
sig, _ := resp.Data["signature"].(string)
|
|
|
|
resp, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "verify",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "ec-key", "input": input, "signature": sig},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("verify: %v", err)
|
|
}
|
|
valid, _ := resp.Data["valid"].(bool)
|
|
if !valid {
|
|
t.Fatal("expected valid signature")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSignRejectsSymmetricAndHMAC(t *testing.T) {
|
|
for _, keyType := range []string{"aes256-gcm", "hmac-sha256"} {
|
|
t.Run(keyType, func(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
createKey(t, te, "sym-key", keyType)
|
|
ctx := context.Background()
|
|
|
|
input := base64.StdEncoding.EncodeToString([]byte("test"))
|
|
_, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "sign",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "sym-key", "input": input},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error signing with non-asymmetric key")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHMACComputeAndVerify(t *testing.T) {
|
|
for _, keyType := range []string{"hmac-sha256", "hmac-sha512"} {
|
|
t.Run(keyType, func(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
createKey(t, te, "hmac-key", keyType)
|
|
ctx := context.Background()
|
|
|
|
input := base64.StdEncoding.EncodeToString([]byte("hmac me"))
|
|
|
|
// Compute.
|
|
resp, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "hmac",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "hmac-key", "input": input},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("hmac compute: %v", err)
|
|
}
|
|
hmacStr, _ := resp.Data["hmac"].(string)
|
|
if !strings.HasPrefix(hmacStr, "metacrypt:v1:") {
|
|
t.Fatalf("unexpected hmac format: %s", hmacStr)
|
|
}
|
|
|
|
// Verify.
|
|
resp, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "hmac",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "hmac-key", "input": input, "hmac": hmacStr},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("hmac verify: %v", err)
|
|
}
|
|
valid, _ := resp.Data["valid"].(bool)
|
|
if !valid {
|
|
t.Fatal("expected valid HMAC")
|
|
}
|
|
|
|
// Wrong input should fail.
|
|
wrongInput := base64.StdEncoding.EncodeToString([]byte("wrong data"))
|
|
resp, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "hmac",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "hmac-key", "input": wrongInput, "hmac": hmacStr},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("hmac verify wrong: %v", err)
|
|
}
|
|
valid, _ = resp.Data["valid"].(bool)
|
|
if valid {
|
|
t.Fatal("expected invalid HMAC for wrong input")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestBatchEncryptDecrypt(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
createKey(t, te, "batch-key", "aes256-gcm")
|
|
ctx := context.Background()
|
|
|
|
items := []interface{}{
|
|
map[string]interface{}{
|
|
"plaintext": base64.StdEncoding.EncodeToString([]byte("item1")),
|
|
"reference": "ref1",
|
|
},
|
|
map[string]interface{}{
|
|
"plaintext": base64.StdEncoding.EncodeToString([]byte("item2")),
|
|
"reference": "ref2",
|
|
},
|
|
}
|
|
|
|
// Batch encrypt.
|
|
resp, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "batch-encrypt",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "batch-key", "items": items},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("batch-encrypt: %v", err)
|
|
}
|
|
|
|
results, _ := resp.Data["results"].([]interface{})
|
|
if len(results) != 2 {
|
|
t.Fatalf("expected 2 results, got %d", len(results))
|
|
}
|
|
|
|
// Build decrypt items.
|
|
decryptItems := make([]interface{}, len(results))
|
|
for i, r := range results {
|
|
br, ok := r.(batchResult)
|
|
if !ok {
|
|
t.Fatalf("expected batchResult, got %T", r)
|
|
}
|
|
if br.Error != "" {
|
|
t.Fatalf("batch encrypt item %d error: %s", i, br.Error)
|
|
}
|
|
decryptItems[i] = map[string]interface{}{
|
|
"ciphertext": br.Ciphertext,
|
|
"reference": br.Reference,
|
|
}
|
|
}
|
|
|
|
// Batch decrypt.
|
|
resp, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "batch-decrypt",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "batch-key", "items": decryptItems},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("batch-decrypt: %v", err)
|
|
}
|
|
|
|
results, _ = resp.Data["results"].([]interface{})
|
|
expected := []string{"item1", "item2"}
|
|
for i, r := range results {
|
|
br, ok := r.(batchResult)
|
|
if !ok {
|
|
t.Fatalf("expected batchResult, got %T", r)
|
|
}
|
|
if br.Error != "" {
|
|
t.Fatalf("batch decrypt item %d error: %s", i, br.Error)
|
|
}
|
|
decoded, _ := base64.StdEncoding.DecodeString(br.Plaintext)
|
|
if string(decoded) != expected[i] {
|
|
t.Fatalf("item %d: expected %q, got %q", i, expected[i], string(decoded))
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBatchPartialErrors(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
createKey(t, te, "batch-err-key", "aes256-gcm")
|
|
ctx := context.Background()
|
|
|
|
items := []interface{}{
|
|
map[string]interface{}{
|
|
"ciphertext": "metacrypt:v1:invalidbase64!!!",
|
|
"reference": "bad",
|
|
},
|
|
map[string]interface{}{
|
|
"ciphertext": "metacrypt:v1:" + base64.StdEncoding.EncodeToString([]byte("not-valid-ciphertext")),
|
|
"reference": "also-bad",
|
|
},
|
|
}
|
|
|
|
resp, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "batch-decrypt",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "batch-err-key", "items": items},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("batch-decrypt: %v", err)
|
|
}
|
|
|
|
results, _ := resp.Data["results"].([]interface{})
|
|
if len(results) != 2 {
|
|
t.Fatalf("expected 2 results, got %d", len(results))
|
|
}
|
|
for i, r := range results {
|
|
br, ok := r.(batchResult)
|
|
if !ok {
|
|
t.Fatalf("expected batchResult, got %T", r)
|
|
}
|
|
if br.Error == "" {
|
|
t.Fatalf("item %d: expected error", i)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestRewrap(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
createKey(t, te, "rewrap-key", "aes256-gcm")
|
|
ctx := context.Background()
|
|
|
|
plaintext := base64.StdEncoding.EncodeToString([]byte("rewrap me"))
|
|
|
|
// Encrypt with v1.
|
|
resp, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "encrypt",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "rewrap-key", "plaintext": plaintext},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("encrypt: %v", err)
|
|
}
|
|
ctV1, _ := resp.Data["ciphertext"].(string)
|
|
|
|
// Rotate.
|
|
_, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "rotate-key",
|
|
CallerInfo: adminCaller(),
|
|
Data: map[string]interface{}{"name": "rewrap-key"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("rotate: %v", err)
|
|
}
|
|
|
|
// Rewrap.
|
|
resp, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "rewrap",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "rewrap-key", "ciphertext": ctV1},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("rewrap: %v", err)
|
|
}
|
|
ctV2, _ := resp.Data["ciphertext"].(string)
|
|
if !strings.HasPrefix(ctV2, "metacrypt:v2:") {
|
|
t.Fatalf("expected v2 ciphertext after rewrap, got %s", ctV2)
|
|
}
|
|
|
|
// Decrypt rewrapped ciphertext.
|
|
resp, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "decrypt",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "rewrap-key", "ciphertext": ctV2},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("decrypt rewrapped: %v", err)
|
|
}
|
|
ptB64, _ := resp.Data["plaintext"].(string)
|
|
decoded, _ := base64.StdEncoding.DecodeString(ptB64)
|
|
if string(decoded) != "rewrap me" {
|
|
t.Fatalf("expected 'rewrap me', got %q", string(decoded))
|
|
}
|
|
}
|
|
|
|
func TestAuthEnforcement(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
ctx := context.Background()
|
|
|
|
// Admin-only operations should fail for users.
|
|
_, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "create-key",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"name": "test", "type": "aes256-gcm"},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected create-key to fail for user")
|
|
}
|
|
|
|
// Admin-only operations should fail for guests.
|
|
_, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "create-key",
|
|
CallerInfo: guestCaller(),
|
|
Data: map[string]interface{}{"name": "test", "type": "aes256-gcm"},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected create-key to fail for guest")
|
|
}
|
|
|
|
// User operations should fail for guests.
|
|
createKey(t, te, "auth-key", "aes256-gcm")
|
|
|
|
_, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "list-keys",
|
|
CallerInfo: guestCaller(),
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected list-keys to fail for guest")
|
|
}
|
|
|
|
// Encrypt should fail for guest.
|
|
_, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "encrypt",
|
|
CallerInfo: guestCaller(),
|
|
Data: map[string]interface{}{"key": "auth-key", "plaintext": base64.StdEncoding.EncodeToString([]byte("test"))},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected encrypt to fail for guest")
|
|
}
|
|
|
|
// Unauthenticated should fail.
|
|
_, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "create-key",
|
|
CallerInfo: nil,
|
|
Data: map[string]interface{}{"name": "test", "type": "aes256-gcm"},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected create-key to fail without auth")
|
|
}
|
|
}
|
|
|
|
func TestDeleteKeyWithAndWithoutAllowDeletion(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
ctx := context.Background()
|
|
|
|
createKey(t, te, "nodelete-key", "aes256-gcm")
|
|
|
|
// Should fail: allow_deletion is false.
|
|
_, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "delete-key",
|
|
CallerInfo: adminCaller(),
|
|
Data: map[string]interface{}{"name": "nodelete-key"},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected delete to fail without allow_deletion")
|
|
}
|
|
|
|
// Enable deletion.
|
|
_, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "update-key-config",
|
|
CallerInfo: adminCaller(),
|
|
Data: map[string]interface{}{"name": "nodelete-key", "allow_deletion": true},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("update-key-config: %v", err)
|
|
}
|
|
|
|
// Should succeed now.
|
|
_, err = te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "delete-key",
|
|
CallerInfo: adminCaller(),
|
|
Data: map[string]interface{}{"name": "nodelete-key"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("delete-key: %v", err)
|
|
}
|
|
|
|
if _, ok := te.keys["nodelete-key"]; ok {
|
|
t.Fatal("key should have been deleted")
|
|
}
|
|
}
|
|
|
|
func TestGetPublicKey(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
createKey(t, te, "pubkey", "ed25519")
|
|
ctx := context.Background()
|
|
|
|
resp, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "get-public-key",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"name": "pubkey"},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("get-public-key: %v", err)
|
|
}
|
|
pk, _ := resp.Data["public_key"].(string)
|
|
if pk == "" {
|
|
t.Fatal("expected non-empty public key")
|
|
}
|
|
ver, _ := resp.Data["version"].(int)
|
|
if ver != 1 {
|
|
t.Fatalf("expected version 1, got %d", ver)
|
|
}
|
|
}
|
|
|
|
func TestGetPublicKeyRejectsSymmetric(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
createKey(t, te, "sym", "aes256-gcm")
|
|
ctx := context.Background()
|
|
|
|
_, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "get-public-key",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"name": "sym"},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error getting public key for symmetric key")
|
|
}
|
|
}
|
|
|
|
func TestBatchRewrap(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
createKey(t, te, "brwrap-key", "aes256-gcm")
|
|
ctx := context.Background()
|
|
|
|
pt1 := base64.StdEncoding.EncodeToString([]byte("item1"))
|
|
pt2 := base64.StdEncoding.EncodeToString([]byte("item2"))
|
|
|
|
// Encrypt two items.
|
|
var cts []string
|
|
for _, pt := range []string{pt1, pt2} {
|
|
resp, _ := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "encrypt",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "brwrap-key", "plaintext": pt},
|
|
})
|
|
ct, _ := resp.Data["ciphertext"].(string)
|
|
cts = append(cts, ct)
|
|
}
|
|
|
|
// Rotate.
|
|
te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "rotate-key",
|
|
CallerInfo: adminCaller(),
|
|
Data: map[string]interface{}{"name": "brwrap-key"},
|
|
})
|
|
|
|
// Batch rewrap.
|
|
items := []interface{}{
|
|
map[string]interface{}{"ciphertext": cts[0], "reference": "r1"},
|
|
map[string]interface{}{"ciphertext": cts[1], "reference": "r2"},
|
|
}
|
|
|
|
resp, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "batch-rewrap",
|
|
CallerInfo: userCaller(),
|
|
Data: map[string]interface{}{"key": "brwrap-key", "items": items},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("batch-rewrap: %v", err)
|
|
}
|
|
|
|
results, _ := resp.Data["results"].([]interface{})
|
|
for i, r := range results {
|
|
br, ok := r.(batchResult)
|
|
if !ok {
|
|
t.Fatalf("expected batchResult, got %T", r)
|
|
}
|
|
if br.Error != "" {
|
|
t.Fatalf("item %d error: %s", i, br.Error)
|
|
}
|
|
if !strings.HasPrefix(br.Ciphertext, "metacrypt:v2:") {
|
|
t.Fatalf("item %d: expected v2 ciphertext, got %s", i, br.Ciphertext)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestDuplicateKeyCreation(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
createKey(t, te, "dup-key", "aes256-gcm")
|
|
ctx := context.Background()
|
|
|
|
_, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "create-key",
|
|
CallerInfo: adminCaller(),
|
|
Data: map[string]interface{}{"name": "dup-key", "type": "aes256-gcm"},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error creating duplicate key")
|
|
}
|
|
}
|
|
|
|
func TestInvalidKeyType(t *testing.T) {
|
|
te, _ := setupEngine(t)
|
|
ctx := context.Background()
|
|
|
|
_, err := te.HandleRequest(ctx, &engine.Request{
|
|
Operation: "create-key",
|
|
CallerInfo: adminCaller(),
|
|
Data: map[string]interface{}{"name": "bad", "type": "invalid-type"},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for invalid key type")
|
|
}
|
|
}
|