Implement comprehensive audit and security infrastructure: - Immutable audit logs with platform-specific backends (Linux/Other) - Sealed log entries with tamper-evident checksums - Audit alert system for real-time security notifications - Log rotation with retention policies - Checkpoint-based audit verification Add multi-tenant security features: - Tenant manager with quota enforcement - Middleware for tenant authentication/authorization - Per-tenant cryptographic key isolation - Supply chain security for container verification - Cross-platform secure file utilities (Unix/Windows) Add test coverage: - Unit tests for audit alerts and sealed logs - Platform-specific audit backend tests
295 lines
8.6 KiB
Go
295 lines
8.6 KiB
Go
// Package crypto provides tenant-scoped encryption key management for multi-tenant deployments.
|
|
// This implements Phase 9.4: Per-Tenant Encryption Keys.
|
|
package crypto
|
|
|
|
import (
|
|
"crypto/aes"
|
|
"crypto/cipher"
|
|
"crypto/rand"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// KeyHierarchy defines the tenant key structure
|
|
// Root Key (per tenant) -> Data Encryption Keys (per artifact)
|
|
type KeyHierarchy struct {
|
|
TenantID string `json:"tenant_id"`
|
|
RootKeyID string `json:"root_key_id"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
Algorithm string `json:"algorithm"` // Always "AES-256-GCM"
|
|
}
|
|
|
|
// TenantKeyManager manages per-tenant encryption keys
|
|
// In production, root keys should be stored in a KMS (HashiCorp Vault, AWS KMS, etc.)
|
|
type TenantKeyManager struct {
|
|
// In-memory store for development; use external KMS in production
|
|
rootKeys map[string][]byte // tenantID -> root key
|
|
}
|
|
|
|
// NewTenantKeyManager creates a new tenant key manager
|
|
func NewTenantKeyManager() *TenantKeyManager {
|
|
return &TenantKeyManager{
|
|
rootKeys: make(map[string][]byte),
|
|
}
|
|
}
|
|
|
|
// ProvisionTenant creates a new root key for a tenant
|
|
// In production, this would call out to a KMS to create a key
|
|
func (km *TenantKeyManager) ProvisionTenant(tenantID string) (*KeyHierarchy, error) {
|
|
if strings.TrimSpace(tenantID) == "" {
|
|
return nil, fmt.Errorf("tenant ID cannot be empty")
|
|
}
|
|
|
|
// Generate root key (32 bytes for AES-256)
|
|
rootKey := make([]byte, 32)
|
|
if _, err := io.ReadFull(rand.Reader, rootKey); err != nil {
|
|
return nil, fmt.Errorf("failed to generate root key: %w", err)
|
|
}
|
|
|
|
// Create key ID from hash of key (for reference, not for key derivation)
|
|
h := sha256.Sum256(rootKey)
|
|
rootKeyID := hex.EncodeToString(h[:8]) // First 8 bytes as ID
|
|
|
|
// Store root key
|
|
km.rootKeys[tenantID] = rootKey
|
|
|
|
return &KeyHierarchy{
|
|
TenantID: tenantID,
|
|
RootKeyID: rootKeyID,
|
|
CreatedAt: time.Now().UTC(),
|
|
Algorithm: "AES-256-GCM",
|
|
}, nil
|
|
}
|
|
|
|
// RotateTenantKey rotates the root key for a tenant
|
|
// Existing data must be re-encrypted with the new key
|
|
func (km *TenantKeyManager) RotateTenantKey(tenantID string) (*KeyHierarchy, error) {
|
|
// Delete old key
|
|
delete(km.rootKeys, tenantID)
|
|
|
|
// Provision new key
|
|
return km.ProvisionTenant(tenantID)
|
|
}
|
|
|
|
// RevokeTenant removes all keys for a tenant
|
|
// This effectively makes all encrypted data inaccessible
|
|
func (km *TenantKeyManager) RevokeTenant(tenantID string) error {
|
|
if _, exists := km.rootKeys[tenantID]; !exists {
|
|
return fmt.Errorf("tenant %s not found", tenantID)
|
|
}
|
|
|
|
// Overwrite key before deleting (best effort)
|
|
key := km.rootKeys[tenantID]
|
|
for i := range key {
|
|
key[i] = 0
|
|
}
|
|
delete(km.rootKeys, tenantID)
|
|
|
|
return nil
|
|
}
|
|
|
|
// GenerateDataEncryptionKey creates a unique DEK for an artifact
|
|
// The DEK is wrapped (encrypted) under the tenant's root key
|
|
func (km *TenantKeyManager) GenerateDataEncryptionKey(tenantID string, artifactID string) (*WrappedDEK, error) {
|
|
rootKey, exists := km.rootKeys[tenantID]
|
|
if !exists {
|
|
return nil, fmt.Errorf("no root key found for tenant %s", tenantID)
|
|
}
|
|
|
|
// Generate unique DEK (32 bytes for AES-256)
|
|
dek := make([]byte, 32)
|
|
if _, err := io.ReadFull(rand.Reader, dek); err != nil {
|
|
return nil, fmt.Errorf("failed to generate DEK: %w", err)
|
|
}
|
|
|
|
// Wrap DEK with root key
|
|
wrappedKey, err := km.wrapKey(rootKey, dek)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to wrap DEK: %w", err)
|
|
}
|
|
|
|
// Clear plaintext DEK from memory
|
|
for i := range dek {
|
|
dek[i] = 0
|
|
}
|
|
|
|
return &WrappedDEK{
|
|
TenantID: tenantID,
|
|
ArtifactID: artifactID,
|
|
WrappedKey: wrappedKey,
|
|
Algorithm: "AES-256-GCM",
|
|
CreatedAt: time.Now().UTC(),
|
|
}, nil
|
|
}
|
|
|
|
// UnwrapDataEncryptionKey decrypts a wrapped DEK using the tenant's root key
|
|
func (km *TenantKeyManager) UnwrapDataEncryptionKey(wrappedDEK *WrappedDEK) ([]byte, error) {
|
|
rootKey, exists := km.rootKeys[wrappedDEK.TenantID]
|
|
if !exists {
|
|
return nil, fmt.Errorf("no root key found for tenant %s", wrappedDEK.TenantID)
|
|
}
|
|
|
|
return km.unwrapKey(rootKey, wrappedDEK.WrappedKey)
|
|
}
|
|
|
|
// WrappedDEK represents a data encryption key wrapped under a tenant root key
|
|
type WrappedDEK struct {
|
|
TenantID string `json:"tenant_id"`
|
|
ArtifactID string `json:"artifact_id"`
|
|
WrappedKey string `json:"wrapped_key"` // base64 encoded
|
|
Algorithm string `json:"algorithm"`
|
|
CreatedAt time.Time `json:"created_at"`
|
|
}
|
|
|
|
// wrapKey encrypts a key using AES-256-GCM with the provided root key
|
|
func (km *TenantKeyManager) wrapKey(rootKey, keyToWrap []byte) (string, error) {
|
|
block, err := aes.NewCipher(rootKey)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create cipher: %w", err)
|
|
}
|
|
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return "", fmt.Errorf("failed to create GCM: %w", err)
|
|
}
|
|
|
|
nonce := make([]byte, gcm.NonceSize())
|
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
|
return "", fmt.Errorf("failed to generate nonce: %w", err)
|
|
}
|
|
|
|
ciphertext := gcm.Seal(nonce, nonce, keyToWrap, nil)
|
|
return base64.StdEncoding.EncodeToString(ciphertext), nil
|
|
}
|
|
|
|
// unwrapKey decrypts a wrapped key using AES-256-GCM
|
|
func (km *TenantKeyManager) unwrapKey(rootKey []byte, wrappedKey string) ([]byte, error) {
|
|
ciphertext, err := base64.StdEncoding.DecodeString(wrappedKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode wrapped key: %w", err)
|
|
}
|
|
|
|
block, err := aes.NewCipher(rootKey)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create cipher: %w", err)
|
|
}
|
|
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create GCM: %w", err)
|
|
}
|
|
|
|
nonceSize := gcm.NonceSize()
|
|
if len(ciphertext) < nonceSize {
|
|
return nil, fmt.Errorf("ciphertext too short")
|
|
}
|
|
|
|
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
|
return gcm.Open(nil, nonce, ciphertext, nil)
|
|
}
|
|
|
|
// EncryptArtifact encrypts artifact data using a tenant-specific DEK
|
|
func (km *TenantKeyManager) EncryptArtifact(tenantID string, artifactID string, plaintext []byte) (*EncryptedArtifact, error) {
|
|
// Generate a new DEK for this artifact
|
|
wrappedDEK, err := km.GenerateDataEncryptionKey(tenantID, artifactID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Unwrap the DEK for use
|
|
dek, err := km.UnwrapDataEncryptionKey(wrappedDEK)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
// Clear DEK from memory after use
|
|
for i := range dek {
|
|
dek[i] = 0
|
|
}
|
|
}()
|
|
|
|
// Encrypt the data with the DEK
|
|
block, err := aes.NewCipher(dek)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create cipher: %w", err)
|
|
}
|
|
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create GCM: %w", err)
|
|
}
|
|
|
|
nonce := make([]byte, gcm.NonceSize())
|
|
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
|
return nil, fmt.Errorf("failed to generate nonce: %w", err)
|
|
}
|
|
|
|
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
|
|
|
|
return &EncryptedArtifact{
|
|
Ciphertext: base64.StdEncoding.EncodeToString(ciphertext),
|
|
DEK: wrappedDEK,
|
|
Algorithm: "AES-256-GCM",
|
|
}, nil
|
|
}
|
|
|
|
// DecryptArtifact decrypts artifact data using its wrapped DEK
|
|
func (km *TenantKeyManager) DecryptArtifact(encrypted *EncryptedArtifact) ([]byte, error) {
|
|
// Unwrap the DEK
|
|
dek, err := km.UnwrapDataEncryptionKey(encrypted.DEK)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to unwrap DEK: %w", err)
|
|
}
|
|
defer func() {
|
|
for i := range dek {
|
|
dek[i] = 0
|
|
}
|
|
}()
|
|
|
|
// Decrypt the data
|
|
ciphertext, err := base64.StdEncoding.DecodeString(encrypted.Ciphertext)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to decode ciphertext: %w", err)
|
|
}
|
|
|
|
block, err := aes.NewCipher(dek)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create cipher: %w", err)
|
|
}
|
|
|
|
gcm, err := cipher.NewGCM(block)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to create GCM: %w", err)
|
|
}
|
|
|
|
nonceSize := gcm.NonceSize()
|
|
if len(ciphertext) < nonceSize {
|
|
return nil, fmt.Errorf("ciphertext too short")
|
|
}
|
|
|
|
nonce, ciphertext := ciphertext[:nonceSize], ciphertext[nonceSize:]
|
|
return gcm.Open(nil, nonce, ciphertext, nil)
|
|
}
|
|
|
|
// EncryptedArtifact represents an encrypted artifact with its wrapped DEK
|
|
type EncryptedArtifact struct {
|
|
Ciphertext string `json:"ciphertext"` // base64 encoded
|
|
DEK *WrappedDEK `json:"dek"`
|
|
Algorithm string `json:"algorithm"`
|
|
}
|
|
|
|
// AuditLogEntry represents an audit log entry for encryption/decryption operations
|
|
type AuditLogEntry struct {
|
|
Timestamp time.Time `json:"timestamp"`
|
|
Operation string `json:"operation"` // "encrypt", "decrypt", "key_rotation"
|
|
TenantID string `json:"tenant_id"`
|
|
ArtifactID string `json:"artifact_id,omitempty"`
|
|
KeyID string `json:"key_id"`
|
|
Success bool `json:"success"`
|
|
Error string `json:"error,omitempty"`
|
|
}
|