mirror of
https://github.com/shankar0123/certctl.git
synced 2026-06-10 18:18:52 +00:00
3f619bcaac
Add three new issuer connectors completing commercial and open-source CA coverage. Entrust uses mTLS client certificate auth with sync/async issuance. GlobalSign Atlas uses mTLS + API key/secret dual auth with serial-based tracking. EJBCA supports dual auth (mTLS or OAuth2) for self-hosted Keyfactor CAs. Each connector implements the full issuer.Connector interface (9 methods), includes httptest-based unit tests (~14 each), and follows established patterns (injectable HTTP clients, RFC 5280 revocation reason mapping, CRL/OCSP delegated to CA). Also includes: issuer factory cases, env var seeding, config structs, domain types, seed data (3 rows, all disabled), OpenAPI enum updates, frontend issuer catalog entries with config fields, and full docs (connectors.md, architecture.md, features.md, README). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
613 lines
18 KiB
Go
613 lines
18 KiB
Go
package ejbca_test
|
|
|
|
import (
|
|
"context"
|
|
"crypto/rand"
|
|
"crypto/rsa"
|
|
"crypto/x509"
|
|
"crypto/x509/pkix"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"encoding/pem"
|
|
"fmt"
|
|
"log/slog"
|
|
"math/big"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/shankar0123/certctl/internal/connector/issuer"
|
|
"github.com/shankar0123/certctl/internal/connector/issuer/ejbca"
|
|
)
|
|
|
|
func TestEJBCAConnector(t *testing.T) {
|
|
logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelDebug}))
|
|
ctx := context.Background()
|
|
|
|
t.Run("ValidateConfig_Success_mTLS", func(t *testing.T) {
|
|
config := ejbca.Config{
|
|
APIUrl: "https://ejbca.example.com:8443/ejbca/ejbca-rest-api/v1",
|
|
AuthMode: "mtls",
|
|
ClientCertPath: "/etc/ssl/certs/client.crt",
|
|
ClientKeyPath: "/etc/ssl/private/client.key",
|
|
CAName: "Management CA",
|
|
}
|
|
|
|
connector := ejbca.New(&config, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err != nil {
|
|
t.Fatalf("ValidateConfig failed: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_Success_OAuth2", func(t *testing.T) {
|
|
config := ejbca.Config{
|
|
APIUrl: "https://ejbca.example.com:8443/ejbca/ejbca-rest-api/v1",
|
|
AuthMode: "oauth2",
|
|
Token: "test-oauth2-token",
|
|
CAName: "Management CA",
|
|
}
|
|
|
|
connector := ejbca.New(&config, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err != nil {
|
|
t.Fatalf("ValidateConfig failed: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_MissingAPIUrl", func(t *testing.T) {
|
|
config := ejbca.Config{
|
|
AuthMode: "mtls",
|
|
CAName: "Management CA",
|
|
}
|
|
|
|
connector := ejbca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for missing api_url")
|
|
}
|
|
if !strings.Contains(err.Error(), "api_url is required") {
|
|
t.Errorf("Expected api_url required error, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_MissingCAName", func(t *testing.T) {
|
|
config := ejbca.Config{
|
|
APIUrl: "https://ejbca.example.com:8443/ejbca/ejbca-rest-api/v1",
|
|
AuthMode: "mtls",
|
|
}
|
|
|
|
connector := ejbca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for missing ca_name")
|
|
}
|
|
if !strings.Contains(err.Error(), "ca_name is required") {
|
|
t.Errorf("Expected ca_name required error, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_mTLS_MissingCertPath", func(t *testing.T) {
|
|
config := ejbca.Config{
|
|
APIUrl: "https://ejbca.example.com:8443/ejbca/ejbca-rest-api/v1",
|
|
AuthMode: "mtls",
|
|
ClientKeyPath: "/etc/ssl/private/client.key",
|
|
CAName: "Management CA",
|
|
}
|
|
|
|
connector := ejbca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for missing client_cert_path with auth_mode=mtls")
|
|
}
|
|
if !strings.Contains(err.Error(), "client_cert_path is required") {
|
|
t.Errorf("Expected client_cert_path required error, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_OAuth2_MissingToken", func(t *testing.T) {
|
|
config := ejbca.Config{
|
|
APIUrl: "https://ejbca.example.com:8443/ejbca/ejbca-rest-api/v1",
|
|
AuthMode: "oauth2",
|
|
CAName: "Management CA",
|
|
}
|
|
|
|
connector := ejbca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for missing token with auth_mode=oauth2")
|
|
}
|
|
if !strings.Contains(err.Error(), "token is required") {
|
|
t.Errorf("Expected token required error, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("ValidateConfig_InvalidAuthMode", func(t *testing.T) {
|
|
config := ejbca.Config{
|
|
APIUrl: "https://ejbca.example.com:8443/ejbca/ejbca-rest-api/v1",
|
|
AuthMode: "invalid",
|
|
CAName: "Management CA",
|
|
}
|
|
|
|
connector := ejbca.New(nil, logger)
|
|
rawConfig, _ := json.Marshal(config)
|
|
err := connector.ValidateConfig(ctx, rawConfig)
|
|
if err == nil {
|
|
t.Fatal("Expected error for invalid auth_mode")
|
|
}
|
|
if !strings.Contains(err.Error(), "auth_mode must be") {
|
|
t.Errorf("Expected auth_mode validation error, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("IssueCertificate_Synchronous", func(t *testing.T) {
|
|
testCertPEM, _ := generateTestCert(t)
|
|
testChainPEM, _ := generateTestCert(t)
|
|
|
|
// Extract DER from PEM for encoding
|
|
certBlock, _ := pem.Decode([]byte(testCertPEM))
|
|
chainBlock, _ := pem.Decode([]byte(testChainPEM))
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasSuffix(r.URL.Path, "/certificate/pkcs10enroll") && r.Method == http.MethodPost {
|
|
// Parse the CSR from request
|
|
var enrollReq map[string]interface{}
|
|
json.NewDecoder(r.Body).Decode(&enrollReq)
|
|
|
|
// Verify CSR is base64-encoded
|
|
if csrB64, ok := enrollReq["certificate_request"].(string); ok {
|
|
// Decode to verify it's valid base64
|
|
if _, err := base64.StdEncoding.DecodeString(csrB64); err != nil {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
|
|
respData := map[string]interface{}{
|
|
"certificate": base64.StdEncoding.EncodeToString(certBlock.Bytes),
|
|
"certificate_chain": []string{base64.StdEncoding.EncodeToString(chainBlock.Bytes)},
|
|
"serial_number": "123456",
|
|
}
|
|
json.NewEncoder(w).Encode(respData)
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &ejbca.Config{
|
|
APIUrl: srv.URL,
|
|
AuthMode: "oauth2",
|
|
Token: "test-token",
|
|
CAName: "Management CA",
|
|
}
|
|
connector := ejbca.NewWithHTTPClient(config, logger, srv.Client())
|
|
|
|
_, csrPEM := generateTestCSR(t, "test.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
SANs: []string{"test.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 should not be empty")
|
|
}
|
|
if result.OrderID == "" {
|
|
t.Error("OrderID should not be empty")
|
|
}
|
|
if !strings.Contains(result.OrderID, "::") {
|
|
t.Errorf("OrderID should contain issuer_dn::serial separator, got: %s", result.OrderID)
|
|
}
|
|
t.Logf("EJBCA issued cert: serial=%s, orderID=%s", result.Serial, result.OrderID)
|
|
})
|
|
|
|
t.Run("IssueCertificate_WithProfiles", func(t *testing.T) {
|
|
testCertPEM, _ := generateTestCert(t)
|
|
certBlock, _ := pem.Decode([]byte(testCertPEM))
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasSuffix(r.URL.Path, "/certificate/pkcs10enroll") && r.Method == http.MethodPost {
|
|
// Verify profiles are in request
|
|
var enrollReq map[string]interface{}
|
|
json.NewDecoder(r.Body).Decode(&enrollReq)
|
|
|
|
if certProfile, ok := enrollReq["certificate_profile_name"].(string); !ok || certProfile != "ENDUSER" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte(`{"error":"invalid certificate_profile_name"}`))
|
|
return
|
|
}
|
|
if eeProfile, ok := enrollReq["end_entity_profile_name"].(string); !ok || eeProfile != "ENDUSER" {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte(`{"error":"invalid end_entity_profile_name"}`))
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
respData := map[string]interface{}{
|
|
"certificate": base64.StdEncoding.EncodeToString(certBlock.Bytes),
|
|
"certificate_chain": []string{},
|
|
"serial_number": "789012",
|
|
}
|
|
json.NewEncoder(w).Encode(respData)
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &ejbca.Config{
|
|
APIUrl: srv.URL,
|
|
AuthMode: "oauth2",
|
|
Token: "test-token",
|
|
CAName: "Management CA",
|
|
CertProfile: "ENDUSER",
|
|
EEProfile: "ENDUSER",
|
|
}
|
|
connector := ejbca.NewWithHTTPClient(config, logger, srv.Client())
|
|
|
|
_, csrPEM := generateTestCSR(t, "app.example.com")
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "app.example.com",
|
|
CSRPEM: csrPEM,
|
|
}
|
|
|
|
result, err := connector.IssueCertificate(ctx, req)
|
|
if err != nil {
|
|
t.Fatalf("IssueCertificate with profiles failed: %v", err)
|
|
}
|
|
|
|
if result.CertPEM == "" {
|
|
t.Error("CertPEM should not be empty")
|
|
}
|
|
})
|
|
|
|
t.Run("IssueCertificate_Error", func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte(`{"error":"invalid CSR"}`))
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &ejbca.Config{
|
|
APIUrl: srv.URL,
|
|
AuthMode: "oauth2",
|
|
Token: "test-token",
|
|
CAName: "Management CA",
|
|
}
|
|
connector := ejbca.NewWithHTTPClient(config, logger, srv.Client())
|
|
|
|
req := issuer.IssuanceRequest{
|
|
CommonName: "test.example.com",
|
|
CSRPEM: "invalid-csr",
|
|
}
|
|
|
|
_, err := connector.IssueCertificate(ctx, req)
|
|
if err == nil {
|
|
t.Fatal("Expected error for invalid CSR")
|
|
}
|
|
})
|
|
|
|
t.Run("GetOrderStatus_Issued", func(t *testing.T) {
|
|
testCertPEM, _ := generateTestCert(t)
|
|
certBlock, _ := pem.Decode([]byte(testCertPEM))
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.Contains(r.URL.Path, "/certificate/") && r.Method == http.MethodGet {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
respData := map[string]interface{}{
|
|
"certificate": base64.StdEncoding.EncodeToString(certBlock.Bytes),
|
|
"certificate_chain": []string{},
|
|
"serial_number": "123456",
|
|
}
|
|
json.NewEncoder(w).Encode(respData)
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &ejbca.Config{
|
|
APIUrl: srv.URL,
|
|
AuthMode: "oauth2",
|
|
Token: "test-token",
|
|
CAName: "Management CA",
|
|
}
|
|
connector := ejbca.NewWithHTTPClient(config, logger, srv.Client())
|
|
|
|
orderID := "CN=Test CA::123456"
|
|
status, err := connector.GetOrderStatus(ctx, orderID)
|
|
if err != nil {
|
|
t.Fatalf("GetOrderStatus failed: %v", err)
|
|
}
|
|
|
|
if status.Status != "completed" {
|
|
t.Errorf("Expected status 'completed', got '%s'", status.Status)
|
|
}
|
|
if status.CertPEM == nil || *status.CertPEM == "" {
|
|
t.Error("CertPEM should not be empty for issued order")
|
|
}
|
|
})
|
|
|
|
t.Run("RenewCertificate_Success", func(t *testing.T) {
|
|
testCertPEM, _ := generateTestCert(t)
|
|
certBlock, _ := pem.Decode([]byte(testCertPEM))
|
|
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasSuffix(r.URL.Path, "/certificate/pkcs10enroll") && r.Method == http.MethodPost {
|
|
w.WriteHeader(http.StatusOK)
|
|
w.Header().Set("Content-Type", "application/json")
|
|
respData := map[string]interface{}{
|
|
"certificate": base64.StdEncoding.EncodeToString(certBlock.Bytes),
|
|
"certificate_chain": []string{},
|
|
"serial_number": "654321",
|
|
}
|
|
json.NewEncoder(w).Encode(respData)
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &ejbca.Config{
|
|
APIUrl: srv.URL,
|
|
AuthMode: "oauth2",
|
|
Token: "test-token",
|
|
CAName: "Management CA",
|
|
}
|
|
connector := ejbca.NewWithHTTPClient(config, logger, srv.Client())
|
|
|
|
_, 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.CertPEM == "" {
|
|
t.Error("CertPEM should not be empty")
|
|
}
|
|
if result.OrderID == "" {
|
|
t.Error("OrderID should not be empty")
|
|
}
|
|
})
|
|
|
|
t.Run("RevokeCertificate_Success", func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.Contains(r.URL.Path, "/revoke") && r.Method == http.MethodPut {
|
|
// Verify reason is in request
|
|
var revokeReq map[string]interface{}
|
|
json.NewDecoder(r.Body).Decode(&revokeReq)
|
|
|
|
if _, ok := revokeReq["reason"]; !ok {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
return
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &ejbca.Config{
|
|
APIUrl: srv.URL,
|
|
AuthMode: "oauth2",
|
|
Token: "test-token",
|
|
CAName: "Management CA",
|
|
}
|
|
connector := ejbca.NewWithHTTPClient(config, logger, srv.Client())
|
|
|
|
reason := "keyCompromise"
|
|
revokeReq := issuer.RevocationRequest{
|
|
Serial: "123456",
|
|
Reason: &reason,
|
|
}
|
|
|
|
err := connector.RevokeCertificate(ctx, revokeReq)
|
|
if err != nil {
|
|
t.Fatalf("RevokeCertificate failed: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("RevokeCertificate_ReasonMapping", func(t *testing.T) {
|
|
reasons := []struct {
|
|
name string
|
|
code int
|
|
mappedTo string
|
|
}{
|
|
{"keyCompromise", 1, "keyCompromise"},
|
|
{"caCompromise", 2, "caCompromise"},
|
|
{"superseded", 4, "superseded"},
|
|
{"cessationOfOperation", 5, "cessationOfOperation"},
|
|
}
|
|
|
|
for _, tc := range reasons {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.Contains(r.URL.Path, "/revoke") && r.Method == http.MethodPut {
|
|
var revokeReq map[string]interface{}
|
|
json.NewDecoder(r.Body).Decode(&revokeReq)
|
|
|
|
// Verify the reason code matches
|
|
if reason, ok := revokeReq["reason"].(float64); ok {
|
|
if int(reason) != tc.code {
|
|
w.WriteHeader(http.StatusBadRequest)
|
|
w.Write([]byte(fmt.Sprintf(`{"error":"expected reason %d, got %d"}`, tc.code, int(reason))))
|
|
return
|
|
}
|
|
}
|
|
|
|
w.WriteHeader(http.StatusNoContent)
|
|
return
|
|
}
|
|
http.NotFound(w, r)
|
|
}))
|
|
defer srv.Close()
|
|
|
|
config := &ejbca.Config{
|
|
APIUrl: srv.URL,
|
|
AuthMode: "oauth2",
|
|
Token: "test-token",
|
|
CAName: "Management CA",
|
|
}
|
|
connector := ejbca.NewWithHTTPClient(config, logger, srv.Client())
|
|
|
|
revokeReq := issuer.RevocationRequest{
|
|
Serial: "test-serial",
|
|
Reason: &tc.name,
|
|
}
|
|
|
|
err := connector.RevokeCertificate(ctx, revokeReq)
|
|
if err != nil {
|
|
t.Fatalf("RevokeCertificate with reason %s failed: %v", tc.name, err)
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("GetRenewalInfo_ReturnsNil", func(t *testing.T) {
|
|
config := &ejbca.Config{
|
|
APIUrl: "https://ejbca.example.com:8443/ejbca/ejbca-rest-api/v1",
|
|
AuthMode: "oauth2",
|
|
Token: "test-token",
|
|
CAName: "Management CA",
|
|
}
|
|
connector := ejbca.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 EJBCA")
|
|
}
|
|
})
|
|
|
|
t.Run("GenerateCRL_Unsupported", func(t *testing.T) {
|
|
config := &ejbca.Config{
|
|
APIUrl: "https://ejbca.example.com:8443/ejbca/ejbca-rest-api/v1",
|
|
AuthMode: "oauth2",
|
|
Token: "test-token",
|
|
CAName: "Management CA",
|
|
}
|
|
connector := ejbca.New(config, logger)
|
|
|
|
_, err := connector.GenerateCRL(ctx, []issuer.RevokedCertEntry{})
|
|
if err == nil {
|
|
t.Fatal("Expected error for unsupported GenerateCRL")
|
|
}
|
|
if !strings.Contains(err.Error(), "CRL distribution") {
|
|
t.Errorf("Expected CRL distribution error, got: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("SignOCSPResponse_Unsupported", func(t *testing.T) {
|
|
config := &ejbca.Config{
|
|
APIUrl: "https://ejbca.example.com:8443/ejbca/ejbca-rest-api/v1",
|
|
AuthMode: "oauth2",
|
|
Token: "test-token",
|
|
CAName: "Management CA",
|
|
}
|
|
connector := ejbca.New(config, logger)
|
|
|
|
_, err := connector.SignOCSPResponse(ctx, issuer.OCSPSignRequest{})
|
|
if err == nil {
|
|
t.Fatal("Expected error for unsupported SignOCSPResponse")
|
|
}
|
|
if !strings.Contains(err.Error(), "OCSP") {
|
|
t.Errorf("Expected OCSP error, got: %v", err)
|
|
}
|
|
})
|
|
}
|
|
|
|
// generateTestCert creates a self-signed test certificate and returns the PEM string.
|
|
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: fmt.Sprintf("Test Certificate %s", serial.String()[:8]),
|
|
},
|
|
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
|
|
}
|