guildhouse-spire-plugins/cmd/substrate-keymanager/plugin.go
Tyler King a58d548518 feat: network-policy extension, governance lifecycle, audit remediation
- Network-policy SPIRE plugin extension
- Governance event notification with merkle anchoring
- Shellstream specs for consent channels + HFL embedded ABI
- All 17 audit findings from AUDIT.md remediated
- SSH credential composer + substrate key manager updates
- Test coverage for config + sshcert packages

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-18 15:54:46 -04:00

178 lines
5.3 KiB
Go

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
}