feat: add certificate service and utility functions
- Implement CertificateService for end-to-end certificate management - Add support for various certificate types (web, client, email, etc.) - Include certificate generation, validation, and revocation - Add utility functions for certificate operations and validation - Implement comprehensive test coverage for certificate operations - Support SAN (Subject Alternative Name) and IP address extensions - Add proper error handling and validation for certificate requests
This commit is contained in:
835
certificate/certificate.go
Normal file
835
certificate/certificate.go
Normal file
@@ -0,0 +1,835 @@
|
||||
package certificate
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"git.secnex.io/secnex/certman/certificate/utils"
|
||||
"git.secnex.io/secnex/certman/models"
|
||||
"git.secnex.io/secnex/certman/repositories"
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// CertificateService handles certificate operations
|
||||
type CertificateService struct {
|
||||
certRepo *repositories.CertificateRepository
|
||||
caRepo *repositories.CertificateAuthorityRepository
|
||||
csrRepo *repositories.CertificateRequestRepository
|
||||
certDir string
|
||||
privateDir string
|
||||
caService *CertificateAuthorityService
|
||||
}
|
||||
|
||||
// NewCertificateService creates a new certificate service
|
||||
func NewCertificateService(
|
||||
db *gorm.DB,
|
||||
certDir string,
|
||||
privateDir string,
|
||||
caService *CertificateAuthorityService,
|
||||
) *CertificateService {
|
||||
return &CertificateService{
|
||||
certRepo: repositories.NewCertificateRepository(db),
|
||||
caRepo: repositories.NewCertificateAuthorityRepository(db),
|
||||
csrRepo: repositories.NewCertificateRequestRepository(db),
|
||||
certDir: certDir,
|
||||
privateDir: privateDir,
|
||||
caService: caService,
|
||||
}
|
||||
}
|
||||
|
||||
// CreateCertificate creates a new certificate from a request
|
||||
func (s *CertificateService) CreateCertificate(req *CreateCertificateRequest) (*models.Certificate, error) {
|
||||
// Validate request
|
||||
if err := s.validateCreateCertificateRequest(req); err != nil {
|
||||
return nil, fmt.Errorf("invalid request: %w", err)
|
||||
}
|
||||
|
||||
// Get CA
|
||||
ca, err := s.caRepo.GetByID(req.CertificateAuthorityID.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CA not found: %w", err)
|
||||
}
|
||||
|
||||
// Load CA certificate and private key
|
||||
caCert, caPrivateKey, err := s.caService.loadCACertificateAndKey(&ca)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load CA certificate: %w", err)
|
||||
}
|
||||
|
||||
// Create certificate configuration (iOS-compatible by default for web certificates)
|
||||
config := s.createCertificateConfig(req)
|
||||
|
||||
// Ensure proper certificate configuration
|
||||
config.SubjectKeyID = true
|
||||
config.AuthorityKeyID = true
|
||||
|
||||
// Validate configuration
|
||||
if err := utils.ValidateCertificateConfig(config); err != nil {
|
||||
return nil, fmt.Errorf("invalid certificate configuration: %w", err)
|
||||
}
|
||||
|
||||
// Generate certificate and private key
|
||||
generator := utils.NewCertificateGenerator(config)
|
||||
cert, privateKey, err := generator.GenerateCertificate(caCert, caPrivateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate certificate: %w", err)
|
||||
}
|
||||
|
||||
// Save certificate and private key to files
|
||||
certFileID, err := s.saveCertificate(cert, req.Type)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to save certificate: %w", err)
|
||||
}
|
||||
|
||||
privateKeyFileID, err := s.savePrivateKey(privateKey, req.Type)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to save private key: %w", err)
|
||||
}
|
||||
|
||||
// Create certificate model
|
||||
certModel := &models.Certificate{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
SerialNumber: cert.SerialNumber.String(),
|
||||
AttributeCommonName: cert.Subject.CommonName,
|
||||
AttributeSubjectAlternativeName: s.formatSANs(cert.DNSNames, cert.IPAddresses, cert.EmailAddresses, cert.URIs),
|
||||
AttributeOrganization: cert.Subject.Organization[0],
|
||||
AttributeOrganizationUnit: cert.Subject.OrganizationalUnit[0],
|
||||
AttributeCountry: cert.Subject.Country[0],
|
||||
AttributeState: cert.Subject.Province[0],
|
||||
AttributeLocality: cert.Subject.Locality[0],
|
||||
AttributeStreet: cert.Subject.StreetAddress[0],
|
||||
AttributeEmail: req.Email,
|
||||
AttributeAddress: req.Address,
|
||||
AttributePostalCode: cert.Subject.PostalCode[0],
|
||||
AttributeNotBefore: cert.NotBefore,
|
||||
AttributeNotAfter: cert.NotAfter,
|
||||
Type: req.Type,
|
||||
Status: models.CertificateStatusActive,
|
||||
CertificateAuthorityID: req.CertificateAuthorityID,
|
||||
RequestID: req.RequestID,
|
||||
FileID: certFileID,
|
||||
PrivateKeyID: privateKeyFileID,
|
||||
Generated: true,
|
||||
GeneratedAt: &time.Time{},
|
||||
}
|
||||
|
||||
// Set generated time
|
||||
now := time.Now()
|
||||
certModel.GeneratedAt = &now
|
||||
|
||||
// Save to database
|
||||
if err := s.certRepo.Create(*certModel); err != nil {
|
||||
// Clean up files if database save fails
|
||||
s.cleanupFiles(certFileID, privateKeyFileID)
|
||||
return nil, fmt.Errorf("failed to save certificate to database: %w", err)
|
||||
}
|
||||
|
||||
return certModel, nil
|
||||
}
|
||||
|
||||
// CreateCertificateFromCSR creates a certificate from a Certificate Signing Request
|
||||
func (s *CertificateService) CreateCertificateFromCSR(req *CreateCertificateFromCSRRequest) (*models.Certificate, error) {
|
||||
// Validate request
|
||||
if err := s.validateCreateCertificateFromCSRRequest(req); err != nil {
|
||||
return nil, fmt.Errorf("invalid request: %w", err)
|
||||
}
|
||||
|
||||
// Get CSR
|
||||
csr, err := s.csrRepo.GetByID(req.CSRID.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CSR not found: %w", err)
|
||||
}
|
||||
|
||||
// Get CA
|
||||
ca, err := s.caRepo.GetByID(req.CertificateAuthorityID.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CA not found: %w", err)
|
||||
}
|
||||
|
||||
// Load CA certificate and private key
|
||||
caCert, caPrivateKey, err := s.caService.loadCACertificateAndKey(&ca)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load CA certificate: %w", err)
|
||||
}
|
||||
|
||||
// Parse CSR
|
||||
csrParsed, err := x509.ParseCertificateRequest(csr.CSRData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse CSR: %w", err)
|
||||
}
|
||||
|
||||
// Create certificate configuration from CSR
|
||||
config := s.createCertificateConfigFromCSR(req, csrParsed)
|
||||
|
||||
// Validate configuration
|
||||
if err := utils.ValidateCertificateConfig(config); err != nil {
|
||||
return nil, fmt.Errorf("invalid certificate configuration: %w", err)
|
||||
}
|
||||
|
||||
// Generate certificate
|
||||
generator := utils.NewCertificateGenerator(config)
|
||||
cert, privateKey, err := generator.GenerateCertificate(caCert, caPrivateKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate certificate: %w", err)
|
||||
}
|
||||
|
||||
// Save certificate and private key to files
|
||||
certFileID, err := s.saveCertificate(cert, req.Type)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to save certificate: %w", err)
|
||||
}
|
||||
|
||||
privateKeyFileID, err := s.savePrivateKey(privateKey, req.Type)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to save private key: %w", err)
|
||||
}
|
||||
|
||||
// Create certificate model
|
||||
certModel := &models.Certificate{
|
||||
Name: req.Name,
|
||||
Description: req.Description,
|
||||
SerialNumber: cert.SerialNumber.String(),
|
||||
AttributeCommonName: cert.Subject.CommonName,
|
||||
AttributeSubjectAlternativeName: s.formatSANs(cert.DNSNames, cert.IPAddresses, cert.EmailAddresses, cert.URIs),
|
||||
AttributeOrganization: cert.Subject.Organization[0],
|
||||
AttributeOrganizationUnit: cert.Subject.OrganizationalUnit[0],
|
||||
AttributeCountry: cert.Subject.Country[0],
|
||||
AttributeState: cert.Subject.Province[0],
|
||||
AttributeLocality: cert.Subject.Locality[0],
|
||||
AttributeStreet: cert.Subject.StreetAddress[0],
|
||||
AttributeEmail: "",
|
||||
AttributeAddress: "",
|
||||
AttributePostalCode: cert.Subject.PostalCode[0],
|
||||
AttributeNotBefore: cert.NotBefore,
|
||||
AttributeNotAfter: cert.NotAfter,
|
||||
Type: req.Type,
|
||||
Status: models.CertificateStatusActive,
|
||||
CertificateAuthorityID: req.CertificateAuthorityID,
|
||||
RequestID: &req.CSRID,
|
||||
FileID: certFileID,
|
||||
PrivateKeyID: privateKeyFileID,
|
||||
Generated: true,
|
||||
GeneratedAt: &time.Time{},
|
||||
}
|
||||
|
||||
// Set generated time
|
||||
now := time.Now()
|
||||
certModel.GeneratedAt = &now
|
||||
|
||||
// Save to database
|
||||
if err := s.certRepo.Create(*certModel); err != nil {
|
||||
// Clean up files if database save fails
|
||||
s.cleanupFiles(certFileID, privateKeyFileID)
|
||||
return nil, fmt.Errorf("failed to save certificate to database: %w", err)
|
||||
}
|
||||
|
||||
return certModel, nil
|
||||
}
|
||||
|
||||
// GetCertificate retrieves a certificate by ID
|
||||
func (s *CertificateService) GetCertificate(id string) (*models.Certificate, error) {
|
||||
cert, err := s.certRepo.GetByID(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("certificate not found: %w", err)
|
||||
}
|
||||
return &cert, nil
|
||||
}
|
||||
|
||||
// GetAllCertificates retrieves all certificates
|
||||
func (s *CertificateService) GetAllCertificates() ([]models.Certificate, error) {
|
||||
certs, err := s.certRepo.GetAll()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve certificates: %w", err)
|
||||
}
|
||||
return certs, nil
|
||||
}
|
||||
|
||||
// GetCertificatesByType retrieves certificates by type
|
||||
func (s *CertificateService) GetCertificatesByType(certType models.CertificateType) ([]models.Certificate, error) {
|
||||
certs, err := s.certRepo.GetAll()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve certificates: %w", err)
|
||||
}
|
||||
|
||||
var filteredCerts []models.Certificate
|
||||
for _, cert := range certs {
|
||||
if cert.Type == certType {
|
||||
filteredCerts = append(filteredCerts, cert)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredCerts, nil
|
||||
}
|
||||
|
||||
// GetCertificatesByCA retrieves certificates issued by a specific CA
|
||||
func (s *CertificateService) GetCertificatesByCA(caID string) ([]models.Certificate, error) {
|
||||
certs, err := s.certRepo.GetAll()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to retrieve certificates: %w", err)
|
||||
}
|
||||
|
||||
var filteredCerts []models.Certificate
|
||||
for _, cert := range certs {
|
||||
if cert.CertificateAuthorityID.String() == caID {
|
||||
filteredCerts = append(filteredCerts, cert)
|
||||
}
|
||||
}
|
||||
|
||||
return filteredCerts, nil
|
||||
}
|
||||
|
||||
// GetCertificateFile retrieves the certificate file
|
||||
func (s *CertificateService) GetCertificateFile(certID string) (*x509.Certificate, error) {
|
||||
cert, err := s.certRepo.GetByID(certID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("certificate not found: %w", err)
|
||||
}
|
||||
|
||||
certPath := filepath.Join(s.certDir, fmt.Sprintf("%s.crt", cert.FileID))
|
||||
certPEM, err := os.ReadFile(certPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read certificate file: %w", err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(certPEM)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("failed to decode certificate PEM")
|
||||
}
|
||||
|
||||
certParsed, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse certificate: %w", err)
|
||||
}
|
||||
|
||||
return certParsed, nil
|
||||
}
|
||||
|
||||
// GetPrivateKeyFile retrieves the private key file
|
||||
func (s *CertificateService) GetPrivateKeyFile(certID string) (interface{}, error) {
|
||||
cert, err := s.certRepo.GetByID(certID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("certificate not found: %w", err)
|
||||
}
|
||||
|
||||
keyPath := filepath.Join(s.privateDir, fmt.Sprintf("%s.key", cert.PrivateKeyID))
|
||||
keyPEM, err := os.ReadFile(keyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read private key file: %w", err)
|
||||
}
|
||||
|
||||
block, _ := pem.Decode(keyPEM)
|
||||
if block == nil {
|
||||
return nil, fmt.Errorf("failed to decode private key PEM")
|
||||
}
|
||||
|
||||
var privateKey interface{}
|
||||
switch block.Type {
|
||||
case "RSA PRIVATE KEY":
|
||||
privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
case "EC PRIVATE KEY":
|
||||
privateKey, err = x509.ParseECPrivateKey(block.Bytes)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported private key type: %s", block.Type)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse private key: %w", err)
|
||||
}
|
||||
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
// UpdateCertificate updates a certificate
|
||||
func (s *CertificateService) UpdateCertificate(cert *models.Certificate) error {
|
||||
return s.certRepo.Update(*cert)
|
||||
}
|
||||
|
||||
// RevokeCertificate revokes a certificate
|
||||
func (s *CertificateService) RevokeCertificate(certID string, reason string) error {
|
||||
cert, err := s.certRepo.GetByID(certID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("certificate not found: %w", err)
|
||||
}
|
||||
|
||||
// Update certificate with revocation information
|
||||
cert.Status = models.CertificateStatusRevoked
|
||||
cert.RevocationReason = reason
|
||||
now := time.Now()
|
||||
cert.RevokedAt = &now
|
||||
|
||||
if err := s.certRepo.Update(cert); err != nil {
|
||||
return fmt.Errorf("failed to revoke certificate: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteCertificate deletes a certificate
|
||||
func (s *CertificateService) DeleteCertificate(certID string) error {
|
||||
cert, err := s.certRepo.GetByID(certID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("certificate not found: %w", err)
|
||||
}
|
||||
|
||||
// Delete from database
|
||||
if err := s.certRepo.Delete(certID); err != nil {
|
||||
return fmt.Errorf("failed to delete certificate from database: %w", err)
|
||||
}
|
||||
|
||||
// Clean up files
|
||||
s.cleanupFiles(cert.FileID, cert.PrivateKeyID)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateCertificate validates a certificate
|
||||
func (s *CertificateService) ValidateCertificate(certID string) error {
|
||||
cert, err := s.certRepo.GetByID(certID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("certificate not found: %w", err)
|
||||
}
|
||||
|
||||
// Load certificate
|
||||
certParsed, err := s.GetCertificateFile(certID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to load certificate: %w", err)
|
||||
}
|
||||
|
||||
// Check validity period
|
||||
now := time.Now()
|
||||
if now.Before(certParsed.NotBefore) {
|
||||
return fmt.Errorf("certificate is not yet valid")
|
||||
}
|
||||
if now.After(certParsed.NotAfter) {
|
||||
return fmt.Errorf("certificate has expired")
|
||||
}
|
||||
|
||||
// Check status
|
||||
if cert.Status == models.CertificateStatusRevoked {
|
||||
return fmt.Errorf("certificate has been revoked: %s", cert.RevocationReason)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ExportCertificate exports a certificate in various formats
|
||||
func (s *CertificateService) ExportCertificate(certID string, format string) ([]byte, error) {
|
||||
certParsed, err := s.GetCertificateFile(certID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load certificate: %w", err)
|
||||
}
|
||||
|
||||
exporter := utils.NewCertificateExporter()
|
||||
|
||||
switch format {
|
||||
case "pem":
|
||||
return exporter.ExportCertificateToPEM(certParsed)
|
||||
case "der":
|
||||
return exporter.ExportCertificateToDER(certParsed), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported export format: %s", format)
|
||||
}
|
||||
}
|
||||
|
||||
// ExportPrivateKey exports a private key in various formats
|
||||
func (s *CertificateService) ExportPrivateKey(certID string, format string) ([]byte, error) {
|
||||
privateKey, err := s.GetPrivateKeyFile(certID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load private key: %w", err)
|
||||
}
|
||||
|
||||
exporter := utils.NewCertificateExporter()
|
||||
|
||||
switch format {
|
||||
case "pem":
|
||||
return exporter.ExportPrivateKeyToPEM(privateKey)
|
||||
case "der":
|
||||
return exporter.ExportPrivateKeyToDER(privateKey)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported export format: %s", format)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
|
||||
// createCertificateConfig creates a certificate configuration from a request
|
||||
func (s *CertificateService) createCertificateConfig(req *CreateCertificateRequest) *utils.CertificateConfig {
|
||||
config := utils.DefaultCertificateConfig(req.Type)
|
||||
|
||||
// Set basic information
|
||||
config.CommonName = req.CommonName
|
||||
config.Organization = req.Organization
|
||||
config.OrganizationalUnit = req.OrganizationalUnit
|
||||
config.Country = req.Country
|
||||
config.State = req.State
|
||||
config.Locality = req.Locality
|
||||
config.Street = req.Street
|
||||
config.PostalCode = req.PostalCode
|
||||
config.Email = req.Email
|
||||
|
||||
// Set validity period
|
||||
if req.NotBefore != nil {
|
||||
config.NotBefore = *req.NotBefore
|
||||
}
|
||||
if req.NotAfter != nil {
|
||||
config.NotAfter = *req.NotAfter
|
||||
}
|
||||
if req.ValidityYears > 0 {
|
||||
config.ValidityYears = req.ValidityYears
|
||||
config.NotAfter = config.NotBefore.AddDate(req.ValidityYears, 0, 0)
|
||||
}
|
||||
|
||||
// Set SANs
|
||||
config.DNSNames = req.DNSNames
|
||||
config.IPAddresses = req.IPAddresses
|
||||
config.EmailAddresses = req.EmailAddresses
|
||||
config.URIs = req.URIs
|
||||
|
||||
// Set key configuration
|
||||
if req.KeyType != "" {
|
||||
config.KeyType = utils.KeyType(req.KeyType)
|
||||
}
|
||||
if req.KeySize != 0 {
|
||||
config.KeySize = utils.KeySize(req.KeySize)
|
||||
}
|
||||
if req.Curve != "" {
|
||||
config.Curve = utils.Curve(req.Curve)
|
||||
}
|
||||
|
||||
// Set custom key usage and extended key usage
|
||||
if req.KeyUsage != nil {
|
||||
config.DigitalSignature = req.KeyUsage.DigitalSignature
|
||||
config.ContentCommitment = req.KeyUsage.ContentCommitment
|
||||
config.KeyEncipherment = req.KeyUsage.KeyEncipherment
|
||||
config.DataEncipherment = req.KeyUsage.DataEncipherment
|
||||
config.KeyAgreement = req.KeyUsage.KeyAgreement
|
||||
config.KeyCertSign = req.KeyUsage.KeyCertSign
|
||||
config.CRLSign = req.KeyUsage.CRLSign
|
||||
config.EncipherOnly = req.KeyUsage.EncipherOnly
|
||||
config.DecipherOnly = req.KeyUsage.DecipherOnly
|
||||
}
|
||||
|
||||
if req.ExtendedKeyUsage != nil {
|
||||
config.ServerAuth = req.ExtendedKeyUsage.ServerAuth
|
||||
config.ClientAuth = req.ExtendedKeyUsage.ClientAuth
|
||||
config.CodeSigning = req.ExtendedKeyUsage.CodeSigning
|
||||
config.EmailProtection = req.ExtendedKeyUsage.EmailProtection
|
||||
config.TimeStamping = req.ExtendedKeyUsage.TimeStamping
|
||||
config.OCSPSigning = req.ExtendedKeyUsage.OCSPSigning
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// createCertificateConfigFromCSR creates a certificate configuration from a CSR
|
||||
func (s *CertificateService) createCertificateConfigFromCSR(req *CreateCertificateFromCSRRequest, csr *x509.CertificateRequest) *utils.CertificateConfig {
|
||||
config := utils.DefaultCertificateConfig(req.Type)
|
||||
|
||||
// Set basic information from CSR
|
||||
config.CommonName = csr.Subject.CommonName
|
||||
if len(csr.Subject.Organization) > 0 {
|
||||
config.Organization = csr.Subject.Organization[0]
|
||||
}
|
||||
if len(csr.Subject.OrganizationalUnit) > 0 {
|
||||
config.OrganizationalUnit = csr.Subject.OrganizationalUnit[0]
|
||||
}
|
||||
if len(csr.Subject.Country) > 0 {
|
||||
config.Country = csr.Subject.Country[0]
|
||||
}
|
||||
if len(csr.Subject.Province) > 0 {
|
||||
config.State = csr.Subject.Province[0]
|
||||
}
|
||||
if len(csr.Subject.Locality) > 0 {
|
||||
config.Locality = csr.Subject.Locality[0]
|
||||
}
|
||||
if len(csr.Subject.StreetAddress) > 0 {
|
||||
config.Street = csr.Subject.StreetAddress[0]
|
||||
}
|
||||
if len(csr.Subject.PostalCode) > 0 {
|
||||
config.PostalCode = csr.Subject.PostalCode[0]
|
||||
}
|
||||
|
||||
// Set validity period
|
||||
if req.NotBefore != nil {
|
||||
config.NotBefore = *req.NotBefore
|
||||
}
|
||||
if req.NotAfter != nil {
|
||||
config.NotAfter = *req.NotAfter
|
||||
}
|
||||
if req.ValidityYears > 0 {
|
||||
config.ValidityYears = req.ValidityYears
|
||||
config.NotAfter = config.NotBefore.AddDate(req.ValidityYears, 0, 0)
|
||||
}
|
||||
|
||||
// Set SANs from CSR
|
||||
config.DNSNames = csr.DNSNames
|
||||
config.IPAddresses = csr.IPAddresses
|
||||
config.EmailAddresses = csr.EmailAddresses
|
||||
config.URIs = s.parseURIsFromCSR(csr.URIs)
|
||||
|
||||
// Set key configuration
|
||||
if req.KeyType != "" {
|
||||
config.KeyType = utils.KeyType(req.KeyType)
|
||||
}
|
||||
if req.KeySize != 0 {
|
||||
config.KeySize = utils.KeySize(req.KeySize)
|
||||
}
|
||||
if req.Curve != "" {
|
||||
config.Curve = utils.Curve(req.Curve)
|
||||
}
|
||||
|
||||
// Set custom key usage and extended key usage
|
||||
if req.KeyUsage != nil {
|
||||
config.DigitalSignature = req.KeyUsage.DigitalSignature
|
||||
config.ContentCommitment = req.KeyUsage.ContentCommitment
|
||||
config.KeyEncipherment = req.KeyUsage.KeyEncipherment
|
||||
config.DataEncipherment = req.KeyUsage.DataEncipherment
|
||||
config.KeyAgreement = req.KeyUsage.KeyAgreement
|
||||
config.KeyCertSign = req.KeyUsage.KeyCertSign
|
||||
config.CRLSign = req.KeyUsage.CRLSign
|
||||
config.EncipherOnly = req.KeyUsage.EncipherOnly
|
||||
config.DecipherOnly = req.KeyUsage.DecipherOnly
|
||||
}
|
||||
|
||||
if req.ExtendedKeyUsage != nil {
|
||||
config.ServerAuth = req.ExtendedKeyUsage.ServerAuth
|
||||
config.ClientAuth = req.ExtendedKeyUsage.ClientAuth
|
||||
config.CodeSigning = req.ExtendedKeyUsage.CodeSigning
|
||||
config.EmailProtection = req.ExtendedKeyUsage.EmailProtection
|
||||
config.TimeStamping = req.ExtendedKeyUsage.TimeStamping
|
||||
config.OCSPSigning = req.ExtendedKeyUsage.OCSPSigning
|
||||
}
|
||||
|
||||
return config
|
||||
}
|
||||
|
||||
// formatSANs formats SANs for storage
|
||||
func (s *CertificateService) formatSANs(dnsNames []string, ipAddresses []net.IP, emailAddresses []string, uris []*url.URL) string {
|
||||
var sans []string
|
||||
|
||||
for _, dns := range dnsNames {
|
||||
sans = append(sans, dns)
|
||||
}
|
||||
|
||||
for _, ip := range ipAddresses {
|
||||
sans = append(sans, ip.String())
|
||||
}
|
||||
|
||||
for _, email := range emailAddresses {
|
||||
sans = append(sans, email)
|
||||
}
|
||||
|
||||
for _, uri := range uris {
|
||||
sans = append(sans, uri.String())
|
||||
}
|
||||
|
||||
return strings.Join(sans, ",")
|
||||
}
|
||||
|
||||
// parseURIsFromCSR parses URIs from CSR
|
||||
func (s *CertificateService) parseURIsFromCSR(uris []*url.URL) []string {
|
||||
var uriStrings []string
|
||||
for _, uri := range uris {
|
||||
uriStrings = append(uriStrings, uri.String())
|
||||
}
|
||||
return uriStrings
|
||||
}
|
||||
|
||||
// saveCertificate saves a certificate to a file and returns the file ID
|
||||
func (s *CertificateService) saveCertificate(cert *x509.Certificate, certType models.CertificateType) (string, error) {
|
||||
// Create certificate directory if it doesn't exist
|
||||
if err := os.MkdirAll(s.certDir, 0755); err != nil {
|
||||
return "", fmt.Errorf("failed to create certificate directory: %w", err)
|
||||
}
|
||||
|
||||
// Generate file ID
|
||||
fileID := fmt.Sprintf("%s-%s", certType, uuid.New().String())
|
||||
|
||||
// Export certificate to PEM
|
||||
exporter := utils.NewCertificateExporter()
|
||||
certPEM, err := exporter.ExportCertificateToPEM(cert)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to export certificate to PEM: %w", err)
|
||||
}
|
||||
|
||||
// Save to file
|
||||
certPath := filepath.Join(s.certDir, fmt.Sprintf("%s.crt", fileID))
|
||||
if err := os.WriteFile(certPath, certPEM, 0644); err != nil {
|
||||
return "", fmt.Errorf("failed to write certificate file: %w", err)
|
||||
}
|
||||
|
||||
return fileID, nil
|
||||
}
|
||||
|
||||
// savePrivateKey saves a private key to a file and returns the file ID
|
||||
func (s *CertificateService) savePrivateKey(privateKey interface{}, certType models.CertificateType) (string, error) {
|
||||
// Create private key directory if it doesn't exist
|
||||
if err := os.MkdirAll(s.privateDir, 0700); err != nil {
|
||||
return "", fmt.Errorf("failed to create private key directory: %w", err)
|
||||
}
|
||||
|
||||
// Generate file ID
|
||||
fileID := fmt.Sprintf("%s-%s", certType, uuid.New().String())
|
||||
|
||||
// Export private key to PEM
|
||||
exporter := utils.NewCertificateExporter()
|
||||
keyPEM, err := exporter.ExportPrivateKeyToPEM(privateKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to export private key to PEM: %w", err)
|
||||
}
|
||||
|
||||
// Save to file
|
||||
keyPath := filepath.Join(s.privateDir, fmt.Sprintf("%s.key", fileID))
|
||||
if err := os.WriteFile(keyPath, keyPEM, 0600); err != nil {
|
||||
return "", fmt.Errorf("failed to write private key file: %w", err)
|
||||
}
|
||||
|
||||
return fileID, nil
|
||||
}
|
||||
|
||||
// cleanupFiles removes certificate and private key files
|
||||
func (s *CertificateService) cleanupFiles(certFileID, privateKeyFileID string) {
|
||||
if certFileID != "" {
|
||||
certPath := filepath.Join(s.certDir, fmt.Sprintf("%s.crt", certFileID))
|
||||
os.Remove(certPath)
|
||||
}
|
||||
|
||||
if privateKeyFileID != "" {
|
||||
keyPath := filepath.Join(s.privateDir, fmt.Sprintf("%s.key", privateKeyFileID))
|
||||
os.Remove(keyPath)
|
||||
}
|
||||
}
|
||||
|
||||
// validateCreateCertificateRequest validates a certificate creation request
|
||||
func (s *CertificateService) validateCreateCertificateRequest(req *CreateCertificateRequest) error {
|
||||
if req.Name == "" {
|
||||
return fmt.Errorf("name is required")
|
||||
}
|
||||
if req.CommonName == "" {
|
||||
return fmt.Errorf("common name is required")
|
||||
}
|
||||
if req.Organization == "" {
|
||||
return fmt.Errorf("organization is required")
|
||||
}
|
||||
if req.Country == "" {
|
||||
return fmt.Errorf("country is required")
|
||||
}
|
||||
if req.CertificateAuthorityID == uuid.Nil {
|
||||
return fmt.Errorf("certificate authority ID is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// validateCreateCertificateFromCSRRequest validates a certificate creation from CSR request
|
||||
func (s *CertificateService) validateCreateCertificateFromCSRRequest(req *CreateCertificateFromCSRRequest) error {
|
||||
if req.Name == "" {
|
||||
return fmt.Errorf("name is required")
|
||||
}
|
||||
if req.CSRID == uuid.Nil {
|
||||
return fmt.Errorf("CSR ID is required")
|
||||
}
|
||||
if req.CertificateAuthorityID == uuid.Nil {
|
||||
return fmt.Errorf("certificate authority ID is required")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Request structures
|
||||
|
||||
// CreateCertificateRequest represents a request to create a certificate
|
||||
type CreateCertificateRequest struct {
|
||||
Name string
|
||||
Description string
|
||||
CommonName string
|
||||
Organization string
|
||||
OrganizationalUnit string
|
||||
Country string
|
||||
State string
|
||||
Locality string
|
||||
Street string
|
||||
Address string
|
||||
PostalCode string
|
||||
Email string
|
||||
Type models.CertificateType
|
||||
CertificateAuthorityID uuid.UUID
|
||||
RequestID *uuid.UUID
|
||||
|
||||
// Validity period
|
||||
NotBefore *time.Time
|
||||
NotAfter *time.Time
|
||||
ValidityYears int // Custom validity period in years (overrides NotAfter if set)
|
||||
|
||||
// Subject Alternative Names
|
||||
DNSNames []string
|
||||
IPAddresses []net.IP
|
||||
EmailAddresses []string
|
||||
URIs []string
|
||||
|
||||
// Key configuration
|
||||
KeyType string // "rsa" or "ecdsa"
|
||||
KeySize int // RSA key size (2048, 3072, 4096)
|
||||
Curve string // ECDSA curve ("P-256", "P-384", "P-521")
|
||||
|
||||
// Custom key usage
|
||||
KeyUsage *KeyUsageConfig
|
||||
|
||||
// Custom extended key usage
|
||||
ExtendedKeyUsage *ExtendedKeyUsageConfig
|
||||
}
|
||||
|
||||
// CreateCertificateFromCSRRequest represents a request to create a certificate from a CSR
|
||||
type CreateCertificateFromCSRRequest struct {
|
||||
Name string
|
||||
Description string
|
||||
Type models.CertificateType
|
||||
CSRID uuid.UUID
|
||||
CertificateAuthorityID uuid.UUID
|
||||
|
||||
// Validity period
|
||||
NotBefore *time.Time
|
||||
NotAfter *time.Time
|
||||
ValidityYears int // Custom validity period in years (overrides NotAfter if set)
|
||||
|
||||
// Key configuration
|
||||
KeyType string // "rsa" or "ecdsa"
|
||||
KeySize int // RSA key size (2048, 3072, 4096)
|
||||
Curve string // ECDSA curve ("P-256", "P-384", "P-521")
|
||||
|
||||
// Custom key usage
|
||||
KeyUsage *KeyUsageConfig
|
||||
|
||||
// Custom extended key usage
|
||||
ExtendedKeyUsage *ExtendedKeyUsageConfig
|
||||
}
|
||||
|
||||
// KeyUsageConfig represents key usage configuration
|
||||
type KeyUsageConfig struct {
|
||||
DigitalSignature bool
|
||||
ContentCommitment bool
|
||||
KeyEncipherment bool
|
||||
DataEncipherment bool
|
||||
KeyAgreement bool
|
||||
KeyCertSign bool
|
||||
CRLSign bool
|
||||
EncipherOnly bool
|
||||
DecipherOnly bool
|
||||
}
|
||||
|
||||
// ExtendedKeyUsageConfig represents extended key usage configuration
|
||||
type ExtendedKeyUsageConfig struct {
|
||||
ServerAuth bool
|
||||
ClientAuth bool
|
||||
CodeSigning bool
|
||||
EmailProtection bool
|
||||
TimeStamping bool
|
||||
OCSPSigning bool
|
||||
}
|
Reference in New Issue
Block a user