package security import ( "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/pem" "os" "path/filepath" "testing" "time" ) // TestRoundTrip exercises the full CA → CSR → SignCSR → x509.Verify chain // in a single test. The point is to catch protocol mismatches early: if // SignCSR produces a cert that doesn't chain to the CA, the verification // step will fail and this test will surface the bug. func TestRoundTrip(t *testing.T) { dir := t.TempDir() // 1. Init a CA. ca, err := CAInit(dir, "orca-test-ca") if err != nil { t.Fatalf("CAInit: %v", err) } if ca == nil || ca.Cert == nil { t.Fatal("CAInit returned nil cert") } if !ca.Cert.IsCA { t.Error("CA cert IsCA is false") } if got := ca.Cert.KeyUsage & x509.KeyUsageCertSign; got == 0 { t.Error("CA cert missing KeyUsageCertSign") } // 2. Generate a server CSR with SANs. commonName := "test.orca.local" sans := []string{"test.orca.local", "127.0.0.1"} keyPEM, csrPEM, err := GenerateCSR(commonName, sans) if err != nil { t.Fatalf("GenerateCSR: %v", err) } if len(keyPEM) == 0 || len(csrPEM) == 0 { t.Fatal("GenerateCSR returned empty PEM") } // 3. Sign the CSR. signedPEM, err := ca.SignCSR(csrPEM) if err != nil { t.Fatalf("SignCSR: %v", err) } if len(signedPEM) == 0 { t.Fatal("SignCSR returned empty cert") } // 4. Verify the chain programmatically with x509.Verify. caPool := x509.NewCertPool() caPool.AddCert(ca.Cert) leafBlock, _ := pem.Decode(signedPEM) if leafBlock == nil { t.Fatal("pem.Decode: no cert block") } leaf, err := x509.ParseCertificate(leafBlock.Bytes) if err != nil { t.Fatalf("ParseCertificate (leaf): %v", err) } _, err = leaf.Verify(x509.VerifyOptions{ Roots: caPool, KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, CurrentTime: time.Now(), }) if err != nil { t.Fatalf("leaf.Verify: %v", err) } // 5. Sanity-check the SANs survived signing. if len(leaf.DNSNames) != 1 || leaf.DNSNames[0] != "test.orca.local" { t.Errorf("expected DNS SAN [test.orca.local], got %v", leaf.DNSNames) } if len(leaf.IPAddresses) != 1 || leaf.IPAddresses[0].String() != "127.0.0.1" { t.Errorf("expected IP SAN [127.0.0.1], got %v", leaf.IPAddresses) } if leaf.Subject.CommonName != commonName { t.Errorf("expected CN %q, got %q", commonName, leaf.Subject.CommonName) } // 6. CA fingerprint pin should match the on-disk ca.crt. caFingerprint, err := Fingerprint(filepath.Join(dir, CACertFile)) if err != nil { t.Fatalf("Fingerprint: %v", err) } if caFingerprint != ca.Fingerprint() { t.Errorf("Fingerprint mismatch: file=%q CA.Fingerprint()=%q", caFingerprint, ca.Fingerprint()) } if len(caFingerprint) != 64 { t.Errorf("expected 64 hex chars, got %d (%q)", len(caFingerprint), caFingerprint) } } // TestCAFileModes verifies REQ-033: ca.crt must be 0644, ca.key must be 0600. func TestCAFileModes(t *testing.T) { dir := t.TempDir() if _, err := CAInit(dir, "orca-mode-test"); err != nil { t.Fatalf("CAInit: %v", err) } certInfo, err := os.Stat(filepath.Join(dir, CACertFile)) if err != nil { t.Fatalf("stat ca.crt: %v", err) } keyInfo, err := os.Stat(filepath.Join(dir, CAKeyFile)) if err != nil { t.Fatalf("stat ca.key: %v", err) } if got := certInfo.Mode().Perm(); got != CACPEMMode { t.Errorf("ca.crt mode = %04o, want %04o (REQ-033)", got, CACPEMMode) } if got := keyInfo.Mode().Perm(); got != CAMode { t.Errorf("ca.key mode = %04o, want %04o (REQ-033)", got, CAMode) } } // TestCAEnforceFileModes verifies that EnforceFileModes refuses to load a CA // whose file modes are wrong (e.g., ca.key is world-readable). func TestCAEnforceFileModes(t *testing.T) { dir := t.TempDir() if _, err := CAInit(dir, "orca-enforce-test"); err != nil { t.Fatalf("CAInit: %v", err) } // Make ca.key world-readable — should fail EnforceFileModes. if err := os.Chmod(filepath.Join(dir, CAKeyFile), 0o644); err != nil { t.Fatalf("chmod: %v", err) } if err := EnforceFileModes(dir); err == nil { t.Error("expected EnforceFileModes to fail with world-readable ca.key") } // And LoadCA should refuse too. if _, err := LoadCA(dir); err == nil { t.Error("expected LoadCA to fail with world-readable ca.key") } // Restore mode; should pass again. if err := os.Chmod(filepath.Join(dir, CAKeyFile), CAMode); err != nil { t.Fatalf("chmod restore: %v", err) } if err := EnforceFileModes(dir); err != nil { t.Errorf("EnforceFileModes after restore: %v", err) } } // TestRotationAlarmFiresAt30Days verifies REQ-034: a cert with NotAfter // 30 days from now triggers RotationAlarm; a cert with 31 days does not. func TestRotationAlarmFiresAt30Days(t *testing.T) { now := time.Now() tests := []struct { name string notAfter time.Time wantError bool }{ { name: "31 days remaining", notAfter: now.Add(31 * 24 * time.Hour), wantError: false, }, { name: "30 days remaining (boundary, fires)", notAfter: now.Add(30 * 24 * time.Hour), wantError: true, }, { name: "15 days remaining (fires)", notAfter: now.Add(15 * 24 * time.Hour), wantError: true, }, { name: "expired (fires, days=0)", notAfter: now.Add(-1 * time.Hour), wantError: true, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { cert := &x509.Certificate{NotAfter: tc.notAfter} err := RotationAlarmAt(cert, now) if tc.wantError && err == nil { t.Errorf("expected alarm, got nil") } if !tc.wantError && err != nil { t.Errorf("expected no alarm, got %v", err) } }) } } // TestRedactStripsPrivateKey verifies REQ-035: the Redact helper strips // PEM private key blocks from arbitrary input. func TestRedactStripsPrivateKey(t *testing.T) { in := []byte(`hello -----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEAxxxx -----END RSA PRIVATE KEY----- world -----BEGIN CERTIFICATE----- MIIDazCCAlOgAwIBAgI... -----END CERTIFICATE----- trailing `) out := string(Redact(in)) if contains(out, "PRIVATE KEY-----") { t.Errorf("Redact output still contains PRIVATE KEY header: %q", out) } if contains(out, "BEGIN RSA PRIVATE KEY") { t.Errorf("Redact output still contains BEGIN RSA PRIVATE KEY: %q", out) } if !contains(out, "[REDACTED PRIVATE KEY]") { t.Errorf("expected redaction marker in output: %q", out) } if !contains(out, "BEGIN CERTIFICATE") { t.Errorf("expected CERTIFICATE block to survive redaction: %q", out) } if !contains(out, "hello") || !contains(out, "world") || !contains(out, "trailing") { t.Errorf("expected non-key content preserved: %q", out) } } // TestRedactNoKey verifies Redact is a no-op (other than a copy) when no // private key blocks are present. func TestRedactNoKey(t *testing.T) { in := []byte("just a cert\n-----BEGIN CERTIFICATE-----\n...\n-----END CERTIFICATE-----\n") out := string(Redact(in)) if out != string(in) { t.Errorf("Redact changed input without any keys present:\n got=%q\nwant=%q", out, in) } } // TestGenerateCSRRequiresSANs verifies REQ-036: a CSR without any SANs is // rejected at generation time. func TestGenerateCSRRequiresSANs(t *testing.T) { if _, _, err := GenerateCSR("foo", nil); err == nil { t.Error("expected GenerateCSR to fail with empty sans") } if _, _, err := GenerateCSR("", []string{"foo"}); err == nil { t.Error("expected GenerateCSR to fail with empty commonName") } } // TestSignCSRRejectsSANless verifies REQ-036: even a syntactically valid CSR // with no SANs is rejected at sign-time. func TestSignCSRRejectsSANless(t *testing.T) { dir := t.TempDir() ca, err := CAInit(dir, "orca-sign-reject-test") if err != nil { t.Fatalf("CAInit: %v", err) } // Build a CSR directly with no SANs to bypass the GenerateCSR guard. key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatalf("generate key: %v", err) } csr := &x509.CertificateRequest{ Subject: pkix.Name{CommonName: "nosan.example"}, DNSNames: nil, } csrDER, err := x509.CreateCertificateRequest(rand.Reader, csr, key) if err != nil { t.Fatalf("CreateCertificateRequest: %v", err) } csrPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE REQUEST", Bytes: csrDER}) if _, err := ca.SignCSR(csrPEM); err == nil { t.Error("expected SignCSR to fail with SAN-less CSR (REQ-036)") } } // TestFingerprintStable verifies the SHA-256 hex is identical across two // computations of the same DER. func TestFingerprintStable(t *testing.T) { dir := t.TempDir() if _, err := CAInit(dir, "orca-fp-test"); err != nil { t.Fatalf("CAInit: %v", err) } caPEM, err := os.ReadFile(filepath.Join(dir, CACertFile)) if err != nil { t.Fatalf("read ca.crt: %v", err) } der, err := firstCertDER(caPEM) if err != nil { t.Fatalf("firstCertDER: %v", err) } fp1 := FingerprintOf(der) fp2 := FingerprintOf(der) if fp1 != fp2 { t.Errorf("FingerprintOf not stable: %q vs %q", fp1, fp2) } if len(fp1) != 64 { t.Errorf("expected 64 hex chars, got %d", len(fp1)) } } func contains(haystack, needle string) bool { return indexOf(haystack, needle) >= 0 } func indexOf(s, sub string) int { for i := 0; i+len(sub) <= len(s); i++ { if s[i:i+len(sub)] == sub { return i } } return -1 }