package googlecas_test import ( "context" "crypto/rand" "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "encoding/json" "encoding/pem" "fmt" "log/slog" "math/big" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "github.com/shankar0123/certctl/internal/connector/issuer" "github.com/shankar0123/certctl/internal/connector/issuer/googlecas" ) func TestGoogleCASConnector(t *testing.T) { logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug})) ctx := context.Background() t.Run("ValidateConfig_Success", func(t *testing.T) { credPath := createTestCredentialsFile(t) config := googlecas.Config{ Project: "my-project", Location: "us-central1", CAPool: "my-pool", Credentials: credPath, TTL: "8760h", } connector := googlecas.New(nil, logger) rawConfig, _ := json.Marshal(config) err := connector.ValidateConfig(ctx, rawConfig) if err != nil { t.Fatalf("ValidateConfig failed: %v", err) } }) t.Run("ValidateConfig_MissingProject", func(t *testing.T) { config := googlecas.Config{ Location: "us-central1", CAPool: "my-pool", Credentials: "/tmp/creds.json", } connector := googlecas.New(nil, logger) rawConfig, _ := json.Marshal(config) err := connector.ValidateConfig(ctx, rawConfig) if err == nil { t.Fatal("Expected error for missing project") } if !strings.Contains(err.Error(), "project is required") { t.Errorf("Expected project required error, got: %v", err) } }) t.Run("ValidateConfig_MissingLocation", func(t *testing.T) { config := googlecas.Config{ Project: "my-project", CAPool: "my-pool", Credentials: "/tmp/creds.json", } connector := googlecas.New(nil, logger) rawConfig, _ := json.Marshal(config) err := connector.ValidateConfig(ctx, rawConfig) if err == nil { t.Fatal("Expected error for missing location") } if !strings.Contains(err.Error(), "location is required") { t.Errorf("Expected location required error, got: %v", err) } }) t.Run("ValidateConfig_MissingCAPool", func(t *testing.T) { config := googlecas.Config{ Project: "my-project", Location: "us-central1", Credentials: "/tmp/creds.json", } connector := googlecas.New(nil, logger) rawConfig, _ := json.Marshal(config) err := connector.ValidateConfig(ctx, rawConfig) if err == nil { t.Fatal("Expected error for missing CA pool") } if !strings.Contains(err.Error(), "CA pool is required") { t.Errorf("Expected CA pool required error, got: %v", err) } }) t.Run("ValidateConfig_MissingCredentials", func(t *testing.T) { config := googlecas.Config{ Project: "my-project", Location: "us-central1", CAPool: "my-pool", } connector := googlecas.New(nil, logger) rawConfig, _ := json.Marshal(config) err := connector.ValidateConfig(ctx, rawConfig) if err == nil { t.Fatal("Expected error for missing credentials") } if !strings.Contains(err.Error(), "credentials path is required") { t.Errorf("Expected credentials required error, got: %v", err) } }) t.Run("ValidateConfig_InvalidCredentialsFile", func(t *testing.T) { config := googlecas.Config{ Project: "my-project", Location: "us-central1", CAPool: "my-pool", Credentials: "/nonexistent/path/credentials.json", } connector := googlecas.New(nil, logger) rawConfig, _ := json.Marshal(config) err := connector.ValidateConfig(ctx, rawConfig) if err == nil { t.Fatal("Expected error for invalid credentials file") } if !strings.Contains(err.Error(), "credentials invalid") { t.Errorf("Expected credentials invalid error, got: %v", err) } }) t.Run("ValidateConfig_MalformedCredentialsJSON", func(t *testing.T) { tmpDir := t.TempDir() badFile := filepath.Join(tmpDir, "bad-creds.json") if err := os.WriteFile(badFile, []byte("not json"), 0600); err != nil { t.Fatal(err) } config := googlecas.Config{ Project: "my-project", Location: "us-central1", CAPool: "my-pool", Credentials: badFile, } connector := googlecas.New(nil, logger) rawConfig, _ := json.Marshal(config) err := connector.ValidateConfig(ctx, rawConfig) if err == nil { t.Fatal("Expected error for malformed credentials JSON") } if !strings.Contains(err.Error(), "credentials invalid") { t.Errorf("Expected credentials invalid error, got: %v", err) } }) t.Run("IssueCertificate_Success", func(t *testing.T) { testCertPEM, _ := generateTestCert(t) credPath := createTestCredentialsFile(t) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/token": w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"access_token":"test-token-12345","expires_in":3600,"token_type":"Bearer"}`)) case strings.Contains(r.URL.Path, "/certificates") && r.Method == http.MethodPost && !strings.Contains(r.URL.Path, ":revoke") && !strings.Contains(r.URL.Path, ":fetchCaCerts"): // Verify auth header auth := r.Header.Get("Authorization") if auth != "Bearer test-token-12345" { w.WriteHeader(http.StatusForbidden) w.Write([]byte(`{"error":{"code":403,"message":"Permission denied","status":"PERMISSION_DENIED"}}`)) return } // Verify certificateId query param certID := r.URL.Query().Get("certificateId") if certID == "" { t.Error("Missing certificateId query parameter") } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) chainCert, _ := generateTestCert(t) resp := fmt.Sprintf(`{ "name": "projects/test-project/locations/us-central1/caPools/test-pool/certificates/%s", "pemCertificate": %q, "pemCertificateChain": [%q] }`, certID, testCertPEM, chainCert) w.Write([]byte(resp)) default: http.NotFound(w, r) } })) defer srv.Close() config := &googlecas.Config{ Project: "test-project", Location: "us-central1", CAPool: "test-pool", Credentials: credPath, TTL: "8760h", BaseURL: srv.URL, TokenURL: srv.URL + "/token", } connector := googlecas.New(config, logger) _, csrPEM := generateTestCSR(t, "app.example.com") req := issuer.IssuanceRequest{ CommonName: "app.example.com", SANs: []string{"app.example.com", "www.example.com"}, CSRPEM: csrPEM, } result, err := connector.IssueCertificate(ctx, req) if err != nil { t.Fatalf("IssueCertificate failed: %v", err) } if result.CertPEM == "" { t.Error("CertPEM is empty") } if result.Serial == "" { t.Error("Serial is empty") } if result.OrderID == "" { t.Error("OrderID is empty") } if !strings.HasPrefix(result.OrderID, "projects/") { t.Errorf("Expected OrderID to be full resource name, got '%s'", result.OrderID) } if result.ChainPEM == "" { t.Error("ChainPEM is empty") } if result.NotBefore.IsZero() { t.Error("NotBefore is zero") } if result.NotAfter.IsZero() { t.Error("NotAfter is zero") } t.Logf("Google CAS issued cert: serial=%s, orderID=%s", result.Serial, result.OrderID) }) t.Run("IssueCertificate_ServerError", func(t *testing.T) { credPath := createTestCredentialsFile(t) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/token": w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"access_token":"test-token","expires_in":3600,"token_type":"Bearer"}`)) case strings.Contains(r.URL.Path, "/certificates"): w.WriteHeader(http.StatusBadRequest) w.Write([]byte(`{"error":{"code":400,"message":"Invalid CSR","status":"INVALID_ARGUMENT"}}`)) default: http.NotFound(w, r) } })) defer srv.Close() config := &googlecas.Config{ Project: "test-project", Location: "us-central1", CAPool: "test-pool", Credentials: credPath, TTL: "8760h", BaseURL: srv.URL, TokenURL: srv.URL + "/token", } connector := googlecas.New(config, logger) _, csrPEM := generateTestCSR(t, "test.example.com") req := issuer.IssuanceRequest{ CommonName: "test.example.com", CSRPEM: csrPEM, } _, err := connector.IssueCertificate(ctx, req) if err == nil { t.Fatal("Expected error for server error response") } if !strings.Contains(err.Error(), "Invalid CSR") { t.Logf("Got error: %v", err) } }) t.Run("IssueCertificate_InvalidResponse", func(t *testing.T) { credPath := createTestCredentialsFile(t) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/token": w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"access_token":"test-token","expires_in":3600,"token_type":"Bearer"}`)) case strings.Contains(r.URL.Path, "/certificates"): w.WriteHeader(http.StatusOK) w.Write([]byte(`not-json`)) default: http.NotFound(w, r) } })) defer srv.Close() config := &googlecas.Config{ Project: "test-project", Location: "us-central1", CAPool: "test-pool", Credentials: credPath, TTL: "8760h", BaseURL: srv.URL, TokenURL: srv.URL + "/token", } connector := googlecas.New(config, logger) _, csrPEM := generateTestCSR(t, "test.example.com") req := issuer.IssuanceRequest{ CommonName: "test.example.com", CSRPEM: csrPEM, } _, err := connector.IssueCertificate(ctx, req) if err == nil { t.Fatal("Expected error for invalid response") } if !strings.Contains(err.Error(), "parse") { t.Logf("Got error: %v", err) } }) t.Run("GetOrderStatus_AlwaysCompleted", func(t *testing.T) { config := &googlecas.Config{ Project: "test-project", Location: "us-central1", CAPool: "test-pool", TTL: "8760h", } connector := googlecas.New(config, logger) status, err := connector.GetOrderStatus(ctx, "projects/p/locations/l/caPools/cp/certificates/cert-123") if err != nil { t.Fatalf("GetOrderStatus failed: %v", err) } if status.Status != "completed" { t.Errorf("Expected status 'completed', got '%s'", status.Status) } if status.OrderID != "projects/p/locations/l/caPools/cp/certificates/cert-123" { t.Errorf("Expected OrderID preserved, got '%s'", status.OrderID) } }) t.Run("RenewCertificate_NewCert", func(t *testing.T) { testCertPEM, _ := generateTestCert(t) credPath := createTestCredentialsFile(t) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/token": w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"access_token":"test-token","expires_in":3600,"token_type":"Bearer"}`)) case strings.Contains(r.URL.Path, "/certificates") && r.Method == http.MethodPost && !strings.Contains(r.URL.Path, ":revoke"): w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) resp := fmt.Sprintf(`{ "name": "projects/test-project/locations/us-central1/caPools/test-pool/certificates/certctl-renew", "pemCertificate": %q, "pemCertificateChain": [] }`, testCertPEM) w.Write([]byte(resp)) default: http.NotFound(w, r) } })) defer srv.Close() config := &googlecas.Config{ Project: "test-project", Location: "us-central1", CAPool: "test-pool", Credentials: credPath, TTL: "8760h", BaseURL: srv.URL, TokenURL: srv.URL + "/token", } connector := googlecas.New(config, logger) _, csrPEM := generateTestCSR(t, "renew.example.com") renewReq := issuer.RenewalRequest{ CommonName: "renew.example.com", CSRPEM: csrPEM, } result, err := connector.RenewCertificate(ctx, renewReq) if err != nil { t.Fatalf("RenewCertificate failed: %v", err) } if result.Serial == "" { t.Error("Serial is empty") } }) t.Run("RevokeCertificate_Success", func(t *testing.T) { credPath := createTestCredentialsFile(t) var receivedReason string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/token": w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"access_token":"test-token","expires_in":3600,"token_type":"Bearer"}`)) case strings.Contains(r.URL.Path, ":revoke"): var body map[string]interface{} json.NewDecoder(r.Body).Decode(&body) receivedReason = body["reason"].(string) w.WriteHeader(http.StatusOK) w.Write([]byte(`{"name":"projects/p/locations/l/caPools/cp/certificates/cert-123"}`)) default: http.NotFound(w, r) } })) defer srv.Close() config := &googlecas.Config{ Project: "test-project", Location: "us-central1", CAPool: "test-pool", Credentials: credPath, TTL: "8760h", BaseURL: srv.URL, TokenURL: srv.URL + "/token", } connector := googlecas.New(config, logger) reason := "keyCompromise" revokeReq := issuer.RevocationRequest{ Serial: "projects/test-project/locations/us-central1/caPools/test-pool/certificates/cert-123", Reason: &reason, } err := connector.RevokeCertificate(ctx, revokeReq) if err != nil { t.Fatalf("RevokeCertificate failed: %v", err) } if receivedReason != "KEY_COMPROMISE" { t.Errorf("Expected reason 'KEY_COMPROMISE', got '%s'", receivedReason) } }) t.Run("RevokeCertificate_Error", func(t *testing.T) { credPath := createTestCredentialsFile(t) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/token": w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"access_token":"test-token","expires_in":3600,"token_type":"Bearer"}`)) case strings.Contains(r.URL.Path, ":revoke"): w.WriteHeader(http.StatusNotFound) w.Write([]byte(`{"error":{"code":404,"message":"Certificate not found","status":"NOT_FOUND"}}`)) default: http.NotFound(w, r) } })) defer srv.Close() config := &googlecas.Config{ Project: "test-project", Location: "us-central1", CAPool: "test-pool", Credentials: credPath, TTL: "8760h", BaseURL: srv.URL, TokenURL: srv.URL + "/token", } connector := googlecas.New(config, logger) revokeReq := issuer.RevocationRequest{ Serial: "projects/test-project/locations/us-central1/caPools/test-pool/certificates/nonexistent", } err := connector.RevokeCertificate(ctx, revokeReq) if err == nil { t.Fatal("Expected error for revoke of nonexistent certificate") } if !strings.Contains(err.Error(), "Certificate not found") { t.Logf("Got error: %v", err) } }) t.Run("RevocationReasonMapping", func(t *testing.T) { credPath := createTestCredentialsFile(t) tests := []struct { name string reason string expected string }{ {"keyCompromise", "keyCompromise", "KEY_COMPROMISE"}, {"caCompromise", "caCompromise", "CERTIFICATE_AUTHORITY_COMPROMISE"}, {"affiliationChanged", "affiliationChanged", "AFFILIATION_CHANGED"}, {"superseded", "superseded", "SUPERSEDED"}, {"cessationOfOperation", "cessationOfOperation", "CESSATION_OF_OPERATION"}, {"certificateHold", "certificateHold", "CERTIFICATE_HOLD"}, {"privilegeWithdrawn", "privilegeWithdrawn", "PRIVILEGE_WITHDRAWN"}, {"unspecified", "unspecified", "REVOCATION_REASON_UNSPECIFIED"}, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { var receivedReason string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/token": w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"access_token":"test-token","expires_in":3600,"token_type":"Bearer"}`)) case strings.Contains(r.URL.Path, ":revoke"): var body map[string]interface{} json.NewDecoder(r.Body).Decode(&body) receivedReason = body["reason"].(string) w.WriteHeader(http.StatusOK) w.Write([]byte(`{}`)) default: http.NotFound(w, r) } })) defer srv.Close() config := &googlecas.Config{ Project: "test-project", Location: "us-central1", CAPool: "test-pool", Credentials: credPath, TTL: "8760h", BaseURL: srv.URL, TokenURL: srv.URL + "/token", } connector := googlecas.New(config, logger) reason := tc.reason err := connector.RevokeCertificate(ctx, issuer.RevocationRequest{ Serial: "projects/p/locations/l/caPools/cp/certificates/cert-1", Reason: &reason, }) if err != nil { t.Fatalf("RevokeCertificate failed: %v", err) } if receivedReason != tc.expected { t.Errorf("Expected reason '%s', got '%s'", tc.expected, receivedReason) } }) } }) t.Run("GetCACertPEM_Success", func(t *testing.T) { credPath := createTestCredentialsFile(t) caCertPEM, _ := generateTestCert(t) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/token": w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"access_token":"test-token","expires_in":3600,"token_type":"Bearer"}`)) case strings.Contains(r.URL.Path, ":fetchCaCerts"): w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) resp := fmt.Sprintf(`{"caCerts":[{"certificates":[%q]}]}`, caCertPEM) w.Write([]byte(resp)) default: http.NotFound(w, r) } })) defer srv.Close() config := &googlecas.Config{ Project: "test-project", Location: "us-central1", CAPool: "test-pool", Credentials: credPath, TTL: "8760h", BaseURL: srv.URL, TokenURL: srv.URL + "/token", } connector := googlecas.New(config, logger) caPEM, err := connector.GetCACertPEM(ctx) if err != nil { t.Fatalf("GetCACertPEM failed: %v", err) } if !strings.Contains(caPEM, "BEGIN CERTIFICATE") { t.Errorf("Expected CA PEM to contain certificate, got: %s", caPEM[:50]) } }) t.Run("GetCACertPEM_Error", func(t *testing.T) { credPath := createTestCredentialsFile(t) srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/token": w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"access_token":"test-token","expires_in":3600,"token_type":"Bearer"}`)) case strings.Contains(r.URL.Path, ":fetchCaCerts"): w.WriteHeader(http.StatusForbidden) w.Write([]byte(`{"error":{"code":403,"message":"Permission denied","status":"PERMISSION_DENIED"}}`)) default: http.NotFound(w, r) } })) defer srv.Close() config := &googlecas.Config{ Project: "test-project", Location: "us-central1", CAPool: "test-pool", Credentials: credPath, TTL: "8760h", BaseURL: srv.URL, TokenURL: srv.URL + "/token", } connector := googlecas.New(config, logger) _, err := connector.GetCACertPEM(ctx) if err == nil { t.Fatal("Expected error for permission denied") } }) t.Run("GetRenewalInfo_ReturnsNil", func(t *testing.T) { config := &googlecas.Config{ Project: "test-project", Location: "us-central1", CAPool: "test-pool", } connector := googlecas.New(config, logger) result, err := connector.GetRenewalInfo(ctx, "-----BEGIN CERTIFICATE-----\ntest\n-----END CERTIFICATE-----") if err != nil { t.Fatalf("GetRenewalInfo should not return error, got: %v", err) } if result != nil { t.Fatal("GetRenewalInfo should return nil for Google CAS") } }) t.Run("AuthHeader_BearerToken", func(t *testing.T) { testCertPEM, _ := generateTestCert(t) credPath := createTestCredentialsFile(t) var authHeader string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case r.URL.Path == "/token": w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) w.Write([]byte(`{"access_token":"verified-token-abc","expires_in":3600,"token_type":"Bearer"}`)) case strings.Contains(r.URL.Path, "/certificates") && r.Method == http.MethodPost: authHeader = r.Header.Get("Authorization") w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusOK) resp := fmt.Sprintf(`{ "name": "projects/p/locations/l/caPools/cp/certificates/c1", "pemCertificate": %q, "pemCertificateChain": [] }`, testCertPEM) w.Write([]byte(resp)) default: http.NotFound(w, r) } })) defer srv.Close() config := &googlecas.Config{ Project: "test-project", Location: "us-central1", CAPool: "test-pool", Credentials: credPath, TTL: "8760h", BaseURL: srv.URL, TokenURL: srv.URL + "/token", } connector := googlecas.New(config, logger) _, csrPEM := generateTestCSR(t, "auth-test.example.com") _, err := connector.IssueCertificate(ctx, issuer.IssuanceRequest{ CommonName: "auth-test.example.com", CSRPEM: csrPEM, }) if err != nil { t.Fatalf("IssueCertificate failed: %v", err) } if authHeader != "Bearer verified-token-abc" { t.Errorf("Expected 'Bearer verified-token-abc', got '%s'", authHeader) } }) } // createTestCredentialsFile generates a temporary service account JSON file with a test RSA key. func createTestCredentialsFile(t *testing.T) string { t.Helper() key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatalf("Failed to generate RSA key: %v", err) } keyPEM := pem.EncodeToMemory(&pem.Block{ Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key), }) creds := map[string]interface{}{ "type": "service_account", "project_id": "test-project", "private_key_id": "key-123", "private_key": string(keyPEM), "client_email": "certctl@test-project.iam.gserviceaccount.com", "token_uri": "https://oauth2.googleapis.com/token", } data, err := json.Marshal(creds) if err != nil { t.Fatalf("Failed to marshal credentials: %v", err) } tmpDir := t.TempDir() credPath := filepath.Join(tmpDir, "credentials.json") if err := os.WriteFile(credPath, data, 0600); err != nil { t.Fatalf("Failed to write credentials file: %v", err) } return credPath } // generateTestCert creates a self-signed test certificate and returns the PEM strings. func generateTestCert(t *testing.T) (certPEM string, keyPEM string) { t.Helper() key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatalf("Failed to generate key: %v", err) } serial, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) template := &x509.Certificate{ SerialNumber: serial, Subject: pkix.Name{ CommonName: "Test Certificate", }, NotBefore: time.Now().Add(-1 * time.Hour), NotAfter: time.Now().Add(24 * time.Hour), DNSNames: []string{"test.example.com"}, KeyUsage: x509.KeyUsageDigitalSignature, ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, } certBytes, err := x509.CreateCertificate(rand.Reader, template, template, &key.PublicKey, key) if err != nil { t.Fatalf("Failed to create certificate: %v", err) } certPEM = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes})) keyPEM = string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})) return certPEM, keyPEM } // generateTestCSR creates a test CSR for the given common name. func generateTestCSR(t *testing.T, commonName string) (*x509.CertificateRequest, string) { t.Helper() key, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { t.Fatalf("Failed to generate key: %v", err) } csrTemplate := x509.CertificateRequest{ Subject: pkix.Name{ CommonName: commonName, }, DNSNames: []string{commonName}, SignatureAlgorithm: x509.SHA256WithRSA, } csrBytes, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, key) if err != nil { t.Fatalf("Failed to create CSR: %v", err) } csrPEM := string(pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE REQUEST", Bytes: csrBytes, })) csr, err := x509.ParseCertificateRequest(csrBytes) if err != nil { t.Fatalf("Failed to parse CSR: %v", err) } return csr, csrPEM }