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 }