guildhouse-spire-plugins/cmd/ssh-credential-composer/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

122 lines
4.2 KiB
Go

package main
import (
"context"
"crypto/sha256"
"encoding/hex"
"fmt"
"log"
"github.com/guildhouse-cooperative/guildhouse-spire-plugins/pkg/config"
"github.com/guildhouse-cooperative/guildhouse-spire-plugins/pkg/governance"
"github.com/guildhouse-cooperative/guildhouse-spire-plugins/pkg/shellstream"
"github.com/guildhouse-cooperative/guildhouse-spire-plugins/pkg/sshcert"
)
// SSHCredentialComposer implements the SPIRE CredentialComposer plugin interface.
//
// This is a merged plugin that handles both SSH certificate generation and
// Shellstream extension injection. In SPIRE's model, CredentialComposer plugins
// can modify credentials during the minting pipeline.
//
// The plugin:
// - Creates an SSH user certificate with the SPIFFE ID as the primary principal
// - Embeds Shellstream @guildhouse.dev extensions carrying governance metadata
// - Signs the certificate using the SSH CA key (from KeyManager)
// - Returns the certificate as part of the composed credential bundle
//
// This was originally designed as two separate plugins (ssh-svid-handler and
// shellstream-composer) but merged because both are CredentialComposer plugins
// performing conceptually one operation.
type SSHCredentialComposer struct {
builder *sshcert.Builder
govClient *governance.Client
config *config.PluginConfig
}
// Configure initializes the composer with SPIRE plugin configuration.
func (c *SSHCredentialComposer) Configure(pluginConfig *config.PluginConfig) error {
builder, err := sshcert.NewBuilder(sshcert.Config{
TrustDomain: pluginConfig.TrustDomain,
})
if err != nil {
return fmt.Errorf("ssh-credential-composer: builder: %w", err)
}
govClient, err := governance.NewClient(governance.Config{
GovernanceAddr: pluginConfig.GovernanceAddr,
CeremonyAddr: pluginConfig.CeremonyAddr,
NotaryAddr: pluginConfig.NotaryAddr,
})
if err != nil {
return fmt.Errorf("ssh-credential-composer: governance client: %w", err)
}
c.builder = builder
c.govClient = govClient
c.config = pluginConfig
return nil
}
// ComposeServerSSHSVID composes an SSH-SVID with Shellstream governance extensions.
// Called by SPIRE Server during credential minting.
func (c *SSHCredentialComposer) ComposeServerSSHSVID(ctx context.Context, spiffeID, tenantID string, roles []string, satBytes []byte) ([]byte, error) {
// Create a governance intent for this credential issuance.
intent, err := c.govClient.CreateIntent(ctx, "credential", "issue", spiffeID, tenantID)
if err != nil {
log.Printf("ssh-credential-composer: governance intent failed (proceeding without): %v", err)
// Non-fatal — compose without governance intent for availability.
}
// Compute SAT hash if SAT bytes are present.
var satHash string
var satScopes []*shellstream.SatScope
if len(satBytes) > 0 {
h := sha256.Sum256(satBytes)
satHash = hex.EncodeToString(h[:])
// Default scope — will be refined by the SAT's actual scopes.
satScopes = []*shellstream.SatScope{{
RegistryType: "credential",
Verbs: []string{"read", "propose"},
ResourcePattern: "*",
}}
}
// Build Shellstream extensions.
extensions := &shellstream.ShellstreamExtensions{
TenantID: tenantID,
Roles: roles,
SatHash: satHash,
SatScopes: satScopes,
}
if intent != nil && intent.IntentID != "" {
extensions.GovernanceIntent = intent.IntentID
}
// Build the SSH certificate with extensions.
certBytes, err := c.builder.Build(&sshcert.CertRequest{
SpiffeID: spiffeID,
Extensions: extensions,
})
if err != nil {
return nil, fmt.Errorf("ssh-credential-composer: build certificate: %w", err)
}
// Notarize the credential issuance if governance intent succeeded.
if intent != nil && intent.IntentID != "" && c.govClient != nil {
certHash := sha256.Sum256(certBytes)
fingerprint := hex.EncodeToString(certHash[:])
if notarizeErr := c.govClient.NotarizeCredentialEvent(ctx, governance.CredentialEvent{
EventType: "issue",
IntentID: intent.IntentID,
CredentialFingerprint: fingerprint,
SpiffeID: spiffeID,
TenantID: tenantID,
}); notarizeErr != nil {
log.Printf("ssh-credential-composer: notarization failed (non-fatal): %v", notarizeErr)
}
}
return certBytes, nil
}