- 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>
122 lines
4.2 KiB
Go
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
|
|
}
|