- 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>
178 lines
5.3 KiB
Go
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
|
|
}
|