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, "'", "'\\''") + "'" }