Files
orca/internal/identity/spiffe_test.go
T
Jon Chery 0d5ff663b4 fix(P13): step-ca /tmp hardening (REQ-128, F10)
---ci---
project: orca
phase: 13
milestone: v0.12
status: execute
---/ci---

step-ca cert/key temp files moved from world-readable /tmp/orca-* to
/etc/orca/step-tmp/orca-* (0700). mkdir + chmod 700 before writing.
Fixes both stepca.go and spiffe.go. All tests updated + pass.
2026-08-07 11:22:07 +00:00

267 lines
8.7 KiB
Go

package identity
import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"math/big"
"net/url"
"strings"
"testing"
"time"
)
type mockExec struct {
responses []mockResp
calls []string
}
type mockResp struct {
match string
out []byte
err error
}
func (m *mockExec) Exec(ctx context.Context, peer string, cmd string) ([]byte, error) {
m.calls = append(m.calls, cmd)
for _, r := range m.responses {
if r.match == "" || strings.Contains(cmd, r.match) {
return r.out, r.err
}
}
return nil, nil
}
func containsCall(t *testing.T, mx *mockExec, want string) {
t.Helper()
for _, c := range mx.calls {
if strings.Contains(c, want) {
return
}
}
t.Errorf("no exec call contained %q; calls were:\n%s", want, strings.Join(mx.calls, "\n"))
}
// mintTestSVIDCert builds a self-signed cert carrying the SPIFFE URI
// SAN spiffeID and returns its PEM encoding. The private key is
// discarded (only the cert is needed for parsing tests).
func mintTestSVIDCert(t *testing.T, spiffeID string) []byte {
t.Helper()
uri, err := url.Parse(spiffeID)
if err != nil {
t.Fatalf("parse spiffe id: %v", err)
}
key, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("generate key: %v", err)
}
tmpl := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{CommonName: spiffeID},
URIs: []*url.URL{uri},
NotBefore: time.Now().Add(-time.Minute),
NotAfter: time.Now().Add(24 * time.Hour),
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth},
}
der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
if err != nil {
t.Fatalf("create cert: %v", err)
}
return pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})
}
func TestSpiffeURI(t *testing.T) {
cases := []struct {
ns, sa, alloc, want string
}{
{"_defaults", "web", "abc123", "spiffe://orca.local/ns/_defaults/sa/web/abc123"},
{"prod", "api", "0", "spiffe://orca.local/ns/prod/sa/api/0"},
}
for _, tc := range cases {
got := SpiffeURI(tc.ns, tc.sa, tc.alloc)
if got != tc.want {
t.Errorf("SpiffeURI(%q,%q,%q) = %q, want %q", tc.ns, tc.sa, tc.alloc, got, tc.want)
}
}
}
func TestSubjectFromSpiffe(t *testing.T) {
ns, sa, alloc, err := SubjectFromSpiffe("spiffe://orca.local/ns/prod/sa/api/0")
if err != nil {
t.Fatalf("SubjectFromSpiffe: %v", err)
}
if ns != "prod" || sa != "api" || alloc != "0" {
t.Errorf("got ns=%q sa=%q alloc=%q", ns, sa, alloc)
}
}
func TestSubjectFromSpiffe_Malformed(t *testing.T) {
cases := []string{
"https://orca.local/ns/prod/sa/api/0",
"spiffe://other/ns/prod/sa/api/0",
"spiffe://orca.local/ns/prod/api/0",
}
for _, c := range cases {
if _, _, _, err := SubjectFromSpiffe(c); err == nil {
t.Errorf("SubjectFromSpiffe(%q): expected error, got nil", c)
}
}
}
func TestVerifySVID_Present(t *testing.T) {
spiffeID := "spiffe://orca.local/ns/_defaults/sa/web/abc123"
certPEM := mintTestSVIDCert(t, spiffeID)
if err := VerifySVID(certPEM, spiffeID); err != nil {
t.Errorf("VerifySVID: %v", err)
}
}
func TestVerifySVID_Missing(t *testing.T) {
certPEM := mintTestSVIDCert(t, "spiffe://orca.local/ns/_defaults/sa/web/abc123")
if err := VerifySVID(certPEM, "spiffe://orca.local/ns/prod/sa/api/0"); !errors.Is(err, ErrSpiffeURIMissing) {
t.Errorf("VerifySVID wrong id: err = %v, want ErrSpiffeURIMissing", err)
}
}
func TestVerifySVID_BadPEM(t *testing.T) {
if err := VerifySVID([]byte("not-a-pem"), "spiffe://orca.local/x"); !errors.Is(err, ErrStepCLI) {
t.Errorf("VerifySVID bad pem: err = %v, want ErrStepCLI", err)
}
}
func TestSpiffeIDFromCert(t *testing.T) {
spiffeID := "spiffe://orca.local/ns/_defaults/sa/web/abc123"
certPEM := mintTestSVIDCert(t, spiffeID)
block, _ := pem.Decode(certPEM)
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
t.Fatalf("parse cert: %v", err)
}
got := SpiffeIDFromCert(cert)
if got != spiffeID {
t.Errorf("SpiffeIDFromCert = %q, want %q", got, spiffeID)
}
}
func TestMintSVID_Success(t *testing.T) {
spiffeID := "spiffe://orca.local/ns/_defaults/sa/web/abc123"
certPEM := mintTestSVIDCert(t, spiffeID)
keyPEM := []byte("-----BEGIN PRIVATE KEY-----\nFAKE\n-----END PRIVATE KEY-----\n")
mx := &mockExec{responses: []mockResp{
{match: "step ca certificate", out: nil, err: nil},
{match: "cat '/etc/orca/step-tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.crt'", out: certPEM, err: nil},
{match: "cat '/etc/orca/step-tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.key'", out: keyPEM, err: nil},
{match: "rm -f", out: nil, err: nil},
}}
gotCert, gotKey, err := MintSVID(context.Background(), mx, "lead:22", "_defaults", "web", "abc123")
if err != nil {
t.Fatalf("MintSVID: %v", err)
}
if string(gotCert) != string(certPEM) {
t.Error("cert PEM mismatch")
}
if string(gotKey) != string(keyPEM) {
t.Error("key PEM mismatch")
}
containsCall(t, mx, "step ca certificate")
containsCall(t, mx, "--san 'spiffe://orca.local/ns/_defaults/sa/web/abc123'")
containsCall(t, mx, "--not-after '24h'")
containsCall(t, mx, "--provisioner 'orca-oidc'")
}
func TestMintSVID_StepFails(t *testing.T) {
mx := &mockExec{responses: []mockResp{
{match: "step ca certificate", out: nil, err: errors.New("step: exit 1")},
}}
_, _, err := MintSVID(context.Background(), mx, "lead:22", "_defaults", "web", "abc123")
if err == nil || !strings.Contains(err.Error(), "identity: mint") {
t.Errorf("err = %v, want wrapped 'identity: mint'", err)
}
}
func TestMintSVID_NilTransport(t *testing.T) {
_, _, err := MintSVID(context.Background(), nil, "lead:22", "_defaults", "web", "abc123")
if err == nil || !strings.Contains(err.Error(), "transport is nil") {
t.Errorf("err = %v, want 'transport is nil'", err)
}
}
func TestMintSVID_EmptyLead(t *testing.T) {
mx := &mockExec{}
_, _, err := MintSVID(context.Background(), mx, "", "_defaults", "web", "abc123")
if err == nil || !strings.Contains(err.Error(), "lead peer not set") {
t.Errorf("err = %v, want 'lead peer not set'", err)
}
}
func TestMintSVID_EmptyCert(t *testing.T) {
mx := &mockExec{responses: []mockResp{
{match: "step ca certificate", out: nil, err: nil},
{match: "cat '/etc/orca/step-tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.crt'", out: nil, err: nil},
{match: "cat '/etc/orca/step-tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.key'", out: []byte("KEY"), err: nil},
{match: "rm -f", out: nil, err: nil},
}}
_, _, err := MintSVID(context.Background(), mx, "lead:22", "_defaults", "web", "abc123")
if err == nil || !errors.Is(err, ErrStepCLI) {
t.Errorf("err = %v, want ErrStepCLI", err)
}
}
func TestMintSVID_URISANMissing(t *testing.T) {
wrongCert := mintTestSVIDCert(t, "spiffe://orca.local/ns/other/sa/api/0")
mx := &mockExec{responses: []mockResp{
{match: "step ca certificate", out: nil, err: nil},
{match: "cat '/etc/orca/step-tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.crt'", out: wrongCert, err: nil},
{match: "cat '/etc/orca/step-tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.key'", out: []byte("KEY"), err: nil},
{match: "rm -f", out: nil, err: nil},
}}
_, _, err := MintSVID(context.Background(), mx, "lead:22", "_defaults", "web", "abc123")
if err == nil || !errors.Is(err, ErrSpiffeURIMissing) {
t.Errorf("err = %v, want ErrSpiffeURIMissing", err)
}
}
func TestShellQuote(t *testing.T) {
if got := shellQuote("a'b"); got != "'a'\\''b'" {
t.Errorf("shellQuote = %q", got)
}
}
func TestSanitize(t *testing.T) {
got := sanitize("spiffe://orca.local/ns/_defaults/sa/web/abc123")
want := "spiffe-orca.local_ns__defaults_sa_web_abc123"
if got != want {
t.Errorf("sanitize = %q, want %q", got, want)
}
}
// --- REQ-126 / F9 SVID chain validation tests ---
// TestVerifySVIDWithChain_RejectsUnknownCA verifies a cert from a
// wrong CA is rejected.
func TestVerifySVIDWithChain_RejectsUnknownCA(t *testing.T) {
// Generate a cert signed by a different CA (not the pool's CA).
certPEM := mintTestSVIDCert(t, "spiffe://orca.local/ns/test/sa/web/alloc-1")
// Empty CA pool (no trusted roots).
emptyPool := x509.NewCertPool()
err := VerifySVIDWithChain(certPEM, "spiffe://orca.local/ns/test/sa/web/alloc-1", emptyPool)
if err == nil {
t.Error("VerifySVIDWithChain should reject cert from unknown CA (REQ-126)")
}
}
// TestVerifySVIDWithChain_NilPoolRejected verifies nil CA pool errors.
func TestVerifySVIDWithChain_NilPoolRejected(t *testing.T) {
certPEM := mintTestSVIDCert(t, "spiffe://orca.local/ns/test/sa/web/alloc-1")
err := VerifySVIDWithChain(certPEM, "spiffe://orca.local/ns/test/sa/web/alloc-1", nil)
if err == nil {
t.Error("nil CA pool should error (REQ-126)")
}
}