Files
orca/internal/identity/spiffe.go
T
Jon Chery 734c9fa0fa feat(P01.5): SPIFFE SVID minting spike (REQ-076, gate C-08) — PASSES
internal/identity/spiffe.go: SpiffeURI format + MintSVID via step CLI;
internal/identity/spiffe_test.go: mock-transport tests with self-signed
SPIFFE URI SAN cert. Spike passes: step CLI supports --san with URI SANs.
Fallback to mTLS identity NOT needed.

---ci---
project: orca
phase: 01.5
milestone: v0.11
status: execute
---/ci---
2026-08-07 04:30:43 +00:00

137 lines
4.1 KiB
Go

package identity
import (
"context"
"crypto/x509"
"encoding/pem"
"errors"
"fmt"
"net/url"
"strings"
)
var (
ErrStepCLI = errors.New("identity: step CLI failed")
ErrSpiffeURIMissing = errors.New("identity: spiffe URI SAN missing")
)
const (
SpiffeTrustDomain = "orca.local"
SVIDNotAfter = "24h"
DefaultProvisioner = "orca-admin"
)
type execer interface {
Exec(ctx context.Context, peer string, cmd string) ([]byte, error)
}
func SpiffeURI(namespace, sa, allocID string) string {
return fmt.Sprintf("spiffe://%s/ns/%s/sa/%s/%s", SpiffeTrustDomain, namespace, sa, allocID)
}
func MintSVID(ctx context.Context, transport execer, leadPeer, namespace, sa, allocID string) (certPEM, keyPEM []byte, err error) {
if transport == nil {
return nil, nil, errors.New("identity: transport is nil")
}
if leadPeer == "" {
return nil, nil, errors.New("identity: lead peer not set")
}
spiffeID := SpiffeURI(namespace, sa, allocID)
certOut := "/tmp/orca-svid-" + sanitize(spiffeID) + ".crt"
keyOut := "/tmp/orca-svid-" + sanitize(spiffeID) + ".key"
var sb strings.Builder
sb.WriteString("step ca certificate ")
sb.WriteString(shellQuote(spiffeID))
sb.WriteString(" ")
sb.WriteString(shellQuote(certOut))
sb.WriteString(" ")
sb.WriteString(shellQuote(keyOut))
sb.WriteString(" --san ")
sb.WriteString(shellQuote(spiffeID))
sb.WriteString(" --not-after ")
sb.WriteString(shellQuote(SVIDNotAfter))
sb.WriteString(" --provisioner ")
sb.WriteString(shellQuote(DefaultProvisioner))
sb.WriteString(" --password-file /dev/stdin --force")
cmd := sb.String()
if _, err := transport.Exec(ctx, leadPeer, cmd); err != nil {
return nil, nil, fmt.Errorf("identity: mint %s: %w", spiffeID, err)
}
certOut2, err := transport.Exec(ctx, leadPeer, fmt.Sprintf("cat %s", shellQuote(certOut)))
if err != nil {
return nil, nil, fmt.Errorf("identity: read cert: %w", err)
}
if len(certOut2) == 0 {
return nil, nil, fmt.Errorf("identity: empty cert at %s: %w", certOut, ErrStepCLI)
}
keyOut2, err := transport.Exec(ctx, leadPeer, fmt.Sprintf("cat %s", shellQuote(keyOut)))
if err != nil {
return nil, nil, fmt.Errorf("identity: read key: %w", err)
}
if len(keyOut2) == 0 {
return nil, nil, fmt.Errorf("identity: empty key at %s: %w", keyOut, ErrStepCLI)
}
_, _ = transport.Exec(ctx, leadPeer, fmt.Sprintf("rm -f %s %s", shellQuote(certOut), shellQuote(keyOut)))
if verr := VerifySVID(certOut2, spiffeID); verr != nil {
return nil, nil, verr
}
return certOut2, keyOut2, nil
}
func VerifySVID(certPEM []byte, spiffeID string) error {
block, _ := pem.Decode(certPEM)
if block == nil {
return fmt.Errorf("identity: parse cert: PEM decode failed: %w", ErrStepCLI)
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return fmt.Errorf("identity: parse cert: %w", err)
}
want, err := url.Parse(spiffeID)
if err != nil {
return fmt.Errorf("identity: parse spiffe id: %w", err)
}
for _, u := range cert.URIs {
if u.String() == want.String() {
return nil
}
}
return fmt.Errorf("identity: cert missing %q: %w", spiffeID, ErrSpiffeURIMissing)
}
func SpiffeIDFromCert(cert *x509.Certificate) string {
for _, u := range cert.URIs {
if u.Scheme == "spiffe" {
return u.String()
}
}
return ""
}
func SubjectFromSpiffe(spiffeID string) (namespace, sa, allocID string, err error) {
u, err := url.Parse(spiffeID)
if err != nil {
return "", "", "", fmt.Errorf("identity: parse spiffe id: %w", err)
}
if u.Scheme != "spiffe" {
return "", "", "", fmt.Errorf("identity: not a spiffe URI: %q", spiffeID)
}
if u.Host != SpiffeTrustDomain {
return "", "", "", fmt.Errorf("identity: wrong trust domain %q, want %q", u.Host, SpiffeTrustDomain)
}
parts := strings.Split(strings.TrimPrefix(u.Path, "/"), "/")
if len(parts) != 5 || parts[0] != "ns" || parts[2] != "sa" {
return "", "", "", fmt.Errorf("identity: malformed spiffe path %q", u.Path)
}
return parts[1], parts[3], parts[4], nil
}
func sanitize(s string) string {
r := strings.NewReplacer("://", "-", "/", "_", ":", "_", " ", "_")
return r.Replace(s)
}
func shellQuote(s string) string {
return "'" + strings.ReplaceAll(s, "'", "'\\''") + "'"
}