- 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>
244 lines
7 KiB
Go
244 lines
7 KiB
Go
package sshcert
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestNewBuilderRequiresTrustDomain(t *testing.T) {
|
|
_, err := NewBuilder(Config{})
|
|
if err == nil {
|
|
t.Fatal("expected error for empty trust domain")
|
|
}
|
|
}
|
|
|
|
func TestNewBuilderAcceptsValidConfig(t *testing.T) {
|
|
b, err := NewBuilder(Config{TrustDomain: "example.org"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if b == nil {
|
|
t.Fatal("builder should not be nil")
|
|
}
|
|
}
|
|
|
|
func TestNewBuilderAppliesSpecDefaults(t *testing.T) {
|
|
b, err := NewBuilder(Config{TrustDomain: "example.org"})
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if b.config.MaxValidSeconds != 3600 {
|
|
t.Errorf("MaxValidSeconds: got %d, want 3600", b.config.MaxValidSeconds)
|
|
}
|
|
if b.config.MinValidSeconds != 30 {
|
|
t.Errorf("MinValidSeconds: got %d, want 30", b.config.MinValidSeconds)
|
|
}
|
|
if b.config.DefaultValidSeconds != 300 {
|
|
t.Errorf("DefaultValidSeconds: got %d, want 300", b.config.DefaultValidSeconds)
|
|
}
|
|
}
|
|
|
|
func TestNewBuilderRejectsInvalidTTLRange(t *testing.T) {
|
|
_, err := NewBuilder(Config{
|
|
TrustDomain: "example.org",
|
|
MinValidSeconds: 500,
|
|
MaxValidSeconds: 100,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error when min > max")
|
|
}
|
|
if !strings.Contains(err.Error(), "min TTL") {
|
|
t.Errorf("expected min/max error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestNewBuilderRejectsDefaultOutsideRange(t *testing.T) {
|
|
_, err := NewBuilder(Config{
|
|
TrustDomain: "example.org",
|
|
MinValidSeconds: 60,
|
|
MaxValidSeconds: 3600,
|
|
DefaultValidSeconds: 10, // below min
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error when default < min")
|
|
}
|
|
if !strings.Contains(err.Error(), "default TTL") {
|
|
t.Errorf("expected default-outside-range error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBuildRequiresSpiffeID(t *testing.T) {
|
|
b, _ := NewBuilder(Config{TrustDomain: "example.org"})
|
|
_, err := b.Build(&CertRequest{})
|
|
if err == nil {
|
|
t.Fatal("expected error for empty spiffe ID")
|
|
}
|
|
}
|
|
|
|
func TestBuildRejectsTTLAboveMax(t *testing.T) {
|
|
b, _ := NewBuilder(Config{
|
|
TrustDomain: "example.org",
|
|
MaxValidSeconds: 3600,
|
|
})
|
|
_, err := b.Build(&CertRequest{
|
|
SpiffeID: "spiffe://example.org/ns/prod/sa/web",
|
|
ValidSeconds: 86400, // 24 hours
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for TTL above max")
|
|
}
|
|
if !strings.Contains(err.Error(), "exceeds maximum") {
|
|
t.Errorf("expected max TTL error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBuildRejectsTTLBelowMin(t *testing.T) {
|
|
b, _ := NewBuilder(Config{
|
|
TrustDomain: "example.org",
|
|
MinValidSeconds: 30,
|
|
})
|
|
_, err := b.Build(&CertRequest{
|
|
SpiffeID: "spiffe://example.org/ns/prod/sa/web",
|
|
ValidSeconds: 5,
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for TTL below min")
|
|
}
|
|
if !strings.Contains(err.Error(), "below minimum") {
|
|
t.Errorf("expected min TTL error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBuildDefaultsTTL(t *testing.T) {
|
|
b, _ := NewBuilder(Config{
|
|
TrustDomain: "example.org",
|
|
DefaultValidSeconds: 300,
|
|
})
|
|
// ValidSeconds 0 should use default — Build should succeed past TTL checks
|
|
_, err := b.Build(&CertRequest{
|
|
SpiffeID: "spiffe://example.org/ns/prod/sa/web",
|
|
ValidSeconds: 0,
|
|
})
|
|
// Should get "not yet implemented" (past TTL validation)
|
|
if err == nil || !strings.Contains(err.Error(), "not yet implemented") {
|
|
t.Fatalf("expected not-yet-implemented with default TTL, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBuildAcceptsTTLInRange(t *testing.T) {
|
|
b, _ := NewBuilder(Config{
|
|
TrustDomain: "example.org",
|
|
MinValidSeconds: 30,
|
|
MaxValidSeconds: 3600,
|
|
})
|
|
_, err := b.Build(&CertRequest{
|
|
SpiffeID: "spiffe://example.org/ns/prod/sa/web",
|
|
ValidSeconds: 600,
|
|
})
|
|
// Should get past TTL checks to "not yet implemented"
|
|
if err == nil || !strings.Contains(err.Error(), "not yet implemented") {
|
|
t.Fatalf("expected not-yet-implemented with valid TTL, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBuildRejectsUnauthorizedPrincipal(t *testing.T) {
|
|
b, _ := NewBuilder(Config{
|
|
TrustDomain: "example.org",
|
|
AllowedPrincipals: []string{"web", "api"},
|
|
})
|
|
_, err := b.Build(&CertRequest{
|
|
SpiffeID: "spiffe://example.org/ns/prod/sa/web",
|
|
Principals: []string{"root"},
|
|
})
|
|
if err == nil {
|
|
t.Fatal("expected error for unauthorized principal")
|
|
}
|
|
if !strings.Contains(err.Error(), "not in allowed list") {
|
|
t.Errorf("expected principal error, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBuildAcceptsAllowedPrincipal(t *testing.T) {
|
|
b, _ := NewBuilder(Config{
|
|
TrustDomain: "example.org",
|
|
AllowedPrincipals: []string{"web", "api"},
|
|
})
|
|
_, err := b.Build(&CertRequest{
|
|
SpiffeID: "spiffe://example.org/ns/prod/sa/web",
|
|
Principals: []string{"web"},
|
|
})
|
|
// Should get past principal checks to "not yet implemented"
|
|
if err == nil || !strings.Contains(err.Error(), "not yet implemented") {
|
|
t.Fatalf("expected not-yet-implemented with allowed principal, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBuildDefaultsPrincipalToSPIFFELeaf(t *testing.T) {
|
|
b, _ := NewBuilder(Config{TrustDomain: "example.org"})
|
|
// Empty Principals should default to SPIFFE ID leaf
|
|
_, err := b.Build(&CertRequest{
|
|
SpiffeID: "spiffe://example.org/ns/prod/sa/web-server",
|
|
})
|
|
// Should get past principal checks to "not yet implemented"
|
|
if err == nil || !strings.Contains(err.Error(), "not yet implemented") {
|
|
t.Fatalf("expected not-yet-implemented with default principal, got: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestBuildRejectsInvalidSPIFFEID(t *testing.T) {
|
|
b, _ := NewBuilder(Config{TrustDomain: "example.org"})
|
|
|
|
tests := []struct {
|
|
name string
|
|
spiffeID string
|
|
}{
|
|
{"http scheme", "http://example.org/sa/web"},
|
|
{"no path", "spiffe://example.org"},
|
|
{"trailing slash", "spiffe://example.org/sa/web/"},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
_, err := b.Build(&CertRequest{SpiffeID: tt.spiffeID})
|
|
if err == nil {
|
|
t.Fatalf("expected error for SPIFFE ID %q", tt.spiffeID)
|
|
}
|
|
if !strings.Contains(err.Error(), "SPIFFE ID") && !strings.Contains(err.Error(), "spiffe") {
|
|
t.Errorf("expected SPIFFE ID error, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSpiffeIDLeaf(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
wantLeaf string
|
|
wantErr bool
|
|
}{
|
|
{"spiffe://guildhouse.dev/ns/prod/sa/web-server", "web-server", false},
|
|
{"spiffe://guildhouse.dev/operator/tyler", "tyler", false},
|
|
{"spiffe://guildhouse.dev/service", "service", false},
|
|
{"spiffe://guildhouse.dev", "", true}, // no path
|
|
{"spiffe://guildhouse.dev/", "", true}, // empty path
|
|
{"spiffe://guildhouse.dev/ns/prod/sa/web/", "", true}, // trailing slash
|
|
{"http://example.com/web", "", true}, // wrong scheme
|
|
{"not-a-uri", "", true}, // no scheme at all
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.input, func(t *testing.T) {
|
|
leaf, err := SpiffeIDLeaf(tt.input)
|
|
if tt.wantErr {
|
|
if err == nil {
|
|
t.Fatalf("expected error for %q, got leaf %q", tt.input, leaf)
|
|
}
|
|
} else {
|
|
if err != nil {
|
|
t.Fatalf("unexpected error for %q: %v", tt.input, err)
|
|
}
|
|
if leaf != tt.wantLeaf {
|
|
t.Errorf("SpiffeIDLeaf(%q) = %q, want %q", tt.input, leaf, tt.wantLeaf)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|