734c9fa0fa
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---
243 lines
7.7 KiB
Go
243 lines
7.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 '/tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.crt'", out: certPEM, err: nil},
|
|
{match: "cat '/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-admin'")
|
|
}
|
|
|
|
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 '/tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.crt'", out: nil, err: nil},
|
|
{match: "cat '/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 '/tmp/orca-svid-spiffe-orca.local_ns__defaults_sa_web_abc123.crt'", out: wrongCert, err: nil},
|
|
{match: "cat '/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)
|
|
}
|
|
}
|