package main import ( "context" "crypto" "crypto/ecdsa" "crypto/ed25519" "crypto/elliptic" "crypto/rand" "crypto/sha256" "encoding/hex" "fmt" "log" "sync" "github.com/guildhouse-cooperative/guildhouse-spire-plugins/pkg/config" "github.com/guildhouse-cooperative/guildhouse-spire-plugins/pkg/governance" ) // SubstrateKeyManager implements the SPIRE KeyManager plugin interface. // // SPIRE Server uses KeyManager plugins to generate, store, and use signing // keys for SVID issuance. This plugin adds governance awareness: // // - Key generation: Standard Ed25519/ECDSA key generation // - Key storage: Keys stored in memory (ephemeral) or filesystem (persistent) // - Key rotation: Triggers a governance ceremony when Accord policy requires it // - Audit: Key lifecycle events (generate, rotate, destroy) are merkle-anchored // // The governance integration ensures that CA key changes (which affect all // issued SVIDs) are treated as high-impact governed mutations, typically // requiring quorum approval. type SubstrateKeyManager struct { mu sync.RWMutex keys map[string]*managedKey // keyID → key govClient *governance.Client config *config.PluginConfig algorithm string // "ed25519" or "ecdsa-p256" } type managedKey struct { id string signer crypto.Signer publicKey crypto.PublicKey algorithm string intentID string // governance intent that authorized this key } // SubstrateKeyManagerConfig holds plugin-specific configuration. type SubstrateKeyManagerConfig struct { Algorithm string `hcl:"algorithm"` // "ed25519" or "ecdsa-p256" CeremonyOnRotation bool `hcl:"ceremony_on_rotation"` // require ceremony for key rotation } // Configure initializes the key manager with plugin configuration. func (km *SubstrateKeyManager) Configure(pluginConfig *config.PluginConfig, kmConfig SubstrateKeyManagerConfig) error { if kmConfig.Algorithm == "" { kmConfig.Algorithm = "ed25519" } if kmConfig.Algorithm != "ed25519" && kmConfig.Algorithm != "ecdsa-p256" { return fmt.Errorf("substrate-keymanager: unsupported algorithm %q", kmConfig.Algorithm) } govClient, err := governance.NewClient(governance.Config{ GovernanceAddr: pluginConfig.GovernanceAddr, CeremonyAddr: pluginConfig.CeremonyAddr, NotaryAddr: pluginConfig.NotaryAddr, }) if err != nil { return fmt.Errorf("substrate-keymanager: governance client: %w", err) } km.keys = make(map[string]*managedKey) km.govClient = govClient km.config = pluginConfig km.algorithm = kmConfig.Algorithm return nil } // GenerateKey creates a new signing key and registers it with governance. func (km *SubstrateKeyManager) GenerateKey(ctx context.Context, keyID string) (crypto.PublicKey, error) { // Create governance intent for key generation. intent, err := km.govClient.CreateIntent(ctx, "signing_key", "generate", keyID, km.config.ClusterID) if err != nil { log.Printf("substrate-keymanager: governance intent for generate failed (proceeding): %v", err) } var signer crypto.Signer var pubKey crypto.PublicKey switch km.algorithm { case "ed25519": pub, priv, err := ed25519.GenerateKey(rand.Reader) if err != nil { return nil, fmt.Errorf("substrate-keymanager: generate ed25519: %w", err) } signer = priv pubKey = pub case "ecdsa-p256": priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return nil, fmt.Errorf("substrate-keymanager: generate ecdsa-p256: %w", err) } signer = priv pubKey = &priv.PublicKey default: return nil, fmt.Errorf("substrate-keymanager: unsupported algorithm %q", km.algorithm) } intentID := "" if intent != nil { intentID = intent.IntentID } km.mu.Lock() km.keys[keyID] = &managedKey{ id: keyID, signer: signer, publicKey: pubKey, algorithm: km.algorithm, intentID: intentID, } km.mu.Unlock() // Merkle-anchor the key generation event. if intentID != "" { pubKeyBytes, _ := pubKeyFingerprint(pubKey) _ = km.govClient.NotarizeCredentialEvent(ctx, governance.CredentialEvent{ EventType: "key_generate", IntentID: intentID, CredentialFingerprint: pubKeyBytes, SpiffeID: keyID, TenantID: km.config.ClusterID, }) } log.Printf("substrate-keymanager: generated %s key %s (intent=%s)", km.algorithm, keyID, intentID) return pubKey, nil } // GetKey retrieves a signing key by ID. func (km *SubstrateKeyManager) GetKey(keyID string) (crypto.Signer, error) { km.mu.RLock() defer km.mu.RUnlock() key, ok := km.keys[keyID] if !ok { return nil, fmt.Errorf("substrate-keymanager: key %q not found", keyID) } return key.signer, nil } // GetPublicKey retrieves a public key by ID. func (km *SubstrateKeyManager) GetPublicKey(keyID string) (crypto.PublicKey, error) { km.mu.RLock() defer km.mu.RUnlock() key, ok := km.keys[keyID] if !ok { return nil, fmt.Errorf("substrate-keymanager: key %q not found", keyID) } return key.publicKey, nil } // pubKeyFingerprint computes the SHA-256 hex fingerprint of a public key. func pubKeyFingerprint(pub crypto.PublicKey) (string, error) { var keyBytes []byte switch k := pub.(type) { case ed25519.PublicKey: keyBytes = []byte(k) case *ecdsa.PublicKey: keyBytes = elliptic.Marshal(k.Curve, k.X, k.Y) default: return "", fmt.Errorf("unsupported key type") } h := sha256.Sum256(keyBytes) return hex.EncodeToString(h[:]), nil }