package client import ( "bytes" "crypto/tls" "crypto/x509" "encoding/json" "fmt" "io" "net/http" "os" "time" "github.com/rs/zerolog" "github.com/gosec/gsc-ops-api/internal/config" ) // EJBCAClient is an mTLS HTTP client for the EJBCA REST API type EJBCAClient struct { baseURL string client *http.Client logger zerolog.Logger } // NewEJBCAClient creates a new EJBCA client with mTLS func NewEJBCAClient(cfg config.EJBCAConfig, logger zerolog.Logger) (*EJBCAClient, error) { cert, err := tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile) if err != nil { return nil, fmt.Errorf("failed to load EJBCA client cert: %w", err) } tlsCfg := &tls.Config{ Certificates: []tls.Certificate{cert}, MinVersion: tls.VersionTLS12, } if cfg.CAFile != "" { caCert, err := os.ReadFile(cfg.CAFile) if err != nil { return nil, fmt.Errorf("failed to read EJBCA CA file: %w", err) } pool := x509.NewCertPool() pool.AppendCertsFromPEM(caCert) tlsCfg.RootCAs = pool } return &EJBCAClient{ baseURL: cfg.BaseURL, client: &http.Client{ Timeout: 30 * time.Second, Transport: &http.Transport{TLSClientConfig: tlsCfg}, }, logger: logger.With().Str("component", "ejbca").Logger(), }, nil } // EJBCACert represents a certificate from EJBCA type EJBCACert struct { SerialNumber string `json:"serial_number"` SubjectDN string `json:"subject_dn"` IssuerDN string `json:"issuer_dn"` Status string `json:"status"` NotBefore string `json:"not_before"` NotAfter string `json:"not_after"` CertificateData string `json:"certificate"` CAName string `json:"ca_name,omitempty"` } // CertSearchRequest is the request body for searching certificates type CertSearchRequest struct { MaxResults int `json:"max_number_of_results"` Criteria []CertSearchCriterion `json:"criteria"` } // CertSearchCriterion is a single search criterion type CertSearchCriterion struct { Property string `json:"property"` Value string `json:"value"` Operation string `json:"operation"` } // CertEnrollRequest is the request body for enrolling a certificate type CertEnrollRequest struct { CertificateRequest string `json:"certificate_request,omitempty"` CertificateProfileName string `json:"certificate_profile_name"` EndEntityProfileName string `json:"end_entity_profile_name"` CAName string `json:"certificate_authority_name"` Username string `json:"username"` Password string `json:"password"` IncludeChain bool `json:"include_chain"` SubjectAltName string `json:"subject_alternative_name,omitempty"` } // CertRevokeRequest is the request body for revoking a certificate type CertRevokeRequest struct { IssuerDN string `json:"issuer_dn"` SerialNumber string `json:"serial_number"` Reason string `json:"reason"` } func (c *EJBCAClient) do(method, path string, body interface{}, result interface{}) error { url := c.baseURL + path var reqBody io.Reader if body != nil { data, err := json.Marshal(body) if err != nil { return fmt.Errorf("failed to marshal request: %w", err) } reqBody = bytes.NewReader(data) } req, err := http.NewRequest(method, url, reqBody) if err != nil { return fmt.Errorf("failed to create request: %w", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Accept", "application/json") resp, err := c.client.Do(req) if err != nil { return fmt.Errorf("request failed: %w", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("failed to read response: %w", err) } if resp.StatusCode >= 400 { return fmt.Errorf("EJBCA error %d: %s", resp.StatusCode, string(respBody)) } if result != nil && len(respBody) > 0 { if err := json.Unmarshal(respBody, result); err != nil { return fmt.Errorf("failed to parse response: %w", err) } } return nil } // SearchCertificates searches for certificates matching criteria func (c *EJBCAClient) SearchCertificates(req *CertSearchRequest) ([]EJBCACert, error) { var result struct { Certificates []EJBCACert `json:"certificates"` } err := c.do("POST", "/ejbca/ejbca-rest-api/v1/certificate/search", req, &result) if err != nil { return nil, err } return result.Certificates, nil } // GetCertificate gets a certificate by serial number and issuer DN func (c *EJBCAClient) GetCertificate(issuerDN, serialNumber string) (*EJBCACert, error) { path := fmt.Sprintf("/ejbca/ejbca-rest-api/v1/certificate/%s/%s", issuerDN, serialNumber) var cert EJBCACert err := c.do("GET", path, nil, &cert) if err != nil { return nil, err } return &cert, nil } // EnrollCertificate requests a new certificate func (c *EJBCAClient) EnrollCertificate(req *CertEnrollRequest) (*EJBCACert, error) { var cert EJBCACert err := c.do("POST", "/ejbca/ejbca-rest-api/v1/certificate/enrollkeystore", req, &cert) if err != nil { return nil, err } return &cert, nil } // RevokeCertificate revokes a certificate func (c *EJBCAClient) RevokeCertificate(issuerDN, serialNumber, reason string) error { path := fmt.Sprintf("/ejbca/ejbca-rest-api/v1/certificate/%s/%s/revoke?reason=%s", issuerDN, serialNumber, reason) return c.do("PUT", path, nil, nil) } // Health checks EJBCA connectivity func (c *EJBCAClient) Health() error { return c.do("GET", "/ejbca/ejbca-rest-api/v1/certificate/status", nil, nil) }