package certificate import ( "crypto/x509" "encoding/pem" "fmt" "os" "path/filepath" "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" ) // CertificateAuthorityService handles certificate authority operations type CertificateAuthorityService struct { caRepo *repositories.CertificateAuthorityRepository orgRepo *repositories.OrganizationRepository certDir string privateDir string } // NewCertificateAuthorityService creates a new certificate authority service func NewCertificateAuthorityService( db *gorm.DB, certDir string, privateDir string, ) *CertificateAuthorityService { return &CertificateAuthorityService{ caRepo: repositories.NewCertificateAuthorityRepository(db), orgRepo: repositories.NewOrganizationRepository(db), certDir: certDir, privateDir: privateDir, } } // CreateRootCA creates a new root certificate authority func (s *CertificateAuthorityService) CreateRootCA(req *CreateRootCARequest) (*models.CertificateAuthority, error) { // Validate request if err := s.validateRootCARequest(req); err != nil { return nil, fmt.Errorf("invalid request: %w", err) } // Check if organization exists org, err := s.orgRepo.GetByID(req.OrganizationID.String()) if err != nil { return nil, fmt.Errorf("organization not found: %w", err) } // Check if root CA already exists for this organization existingCAs, err := s.caRepo.GetAll() if err != nil { return nil, fmt.Errorf("failed to check existing CAs: %w", err) } for _, ca := range existingCAs { if ca.Root && ca.OrganizationID == req.OrganizationID { return nil, fmt.Errorf("root CA already exists for organization %s", org.Name) } } // Create certificate configuration var config *utils.CertificateConfig if req.ValidityYears > 0 { config = utils.CACertificateConfigWithValidity(true, req.ValidityYears) // true = isRoot } else { config = utils.CACertificateConfig(true) // true = isRoot } config.CommonName = req.CommonName config.Organization = org.Name 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 // 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.GenerateSelfSignedCertificate() if err != nil { return nil, fmt.Errorf("failed to generate root CA certificate: %w", err) } // Save certificate and private key to files certFileID, err := s.saveCertificate(cert, "root-ca") if err != nil { return nil, fmt.Errorf("failed to save certificate: %w", err) } privateKeyFileID, err := s.savePrivateKey(privateKey, "root-ca") if err != nil { return nil, fmt.Errorf("failed to save private key: %w", err) } // Create CA model ca := &models.CertificateAuthority{ Name: req.Name, Description: req.Description, SerialNumber: cert.SerialNumber.String(), AttributeCommonName: cert.Subject.CommonName, 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, Root: true, ParentID: nil, // Root CA has no parent OrganizationID: req.OrganizationID, FileID: certFileID, PrivateKeyID: privateKeyFileID, } // Save to database createdCA, err := s.caRepo.Create(*ca) if err != nil { // Clean up files if database save fails s.cleanupFiles(certFileID, privateKeyFileID) return nil, fmt.Errorf("failed to save CA to database: %w", err) } return &createdCA, nil } // CreateIntermediateCA creates a new intermediate certificate authority func (s *CertificateAuthorityService) CreateIntermediateCA(req *CreateIntermediateCARequest) (*models.CertificateAuthority, error) { // Validate request if err := s.validateIntermediateCARequest(req); err != nil { return nil, fmt.Errorf("invalid request: %w", err) } // Check if organization exists org, err := s.orgRepo.GetByID(req.OrganizationID.String()) if err != nil { return nil, fmt.Errorf("organization not found: %w", err) } // Get parent CA parentCA, err := s.caRepo.GetByID(req.ParentCAID.String()) if err != nil { return nil, fmt.Errorf("parent CA not found: %w", err) } // Load parent CA certificate and private key parentCert, parentPrivateKey, err := s.loadCACertificateAndKey(&parentCA) if err != nil { return nil, fmt.Errorf("failed to load parent CA certificate: %w", err) } // Create certificate configuration var config *utils.CertificateConfig if req.ValidityYears > 0 { config = utils.CACertificateConfigWithValidity(false, req.ValidityYears) // false = not root } else { config = utils.CACertificateConfig(false) // false = not root } config.CommonName = req.CommonName config.Organization = org.Name 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 // 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(parentCert, parentPrivateKey) if err != nil { return nil, fmt.Errorf("failed to generate intermediate CA certificate: %w", err) } // Save certificate and private key to files certFileID, err := s.saveCertificate(cert, "intermediate-ca") if err != nil { return nil, fmt.Errorf("failed to save certificate: %w", err) } privateKeyFileID, err := s.savePrivateKey(privateKey, "intermediate-ca") if err != nil { return nil, fmt.Errorf("failed to save private key: %w", err) } // Create CA model ca := &models.CertificateAuthority{ Name: req.Name, Description: req.Description, SerialNumber: cert.SerialNumber.String(), AttributeCommonName: cert.Subject.CommonName, 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, Root: false, ParentID: &req.ParentCAID, OrganizationID: req.OrganizationID, FileID: certFileID, PrivateKeyID: privateKeyFileID, } // Save to database createdCA, err := s.caRepo.Create(*ca) if err != nil { // Clean up files if database save fails s.cleanupFiles(certFileID, privateKeyFileID) return nil, fmt.Errorf("failed to save CA to database: %w", err) } return &createdCA, nil } // GetCA retrieves a certificate authority by ID func (s *CertificateAuthorityService) GetCA(id string) (*models.CertificateAuthority, error) { ca, err := s.caRepo.GetByID(id) if err != nil { return nil, fmt.Errorf("CA not found: %w", err) } return &ca, nil } // GetAllCAs retrieves all certificate authorities func (s *CertificateAuthorityService) GetAllCAs() ([]models.CertificateAuthority, error) { cas, err := s.caRepo.GetAll() if err != nil { return nil, fmt.Errorf("failed to retrieve CAs: %w", err) } return cas, nil } // GetRootCAs retrieves all root certificate authorities func (s *CertificateAuthorityService) GetRootCAs() ([]models.CertificateAuthority, error) { allCAs, err := s.caRepo.GetAll() if err != nil { return nil, fmt.Errorf("failed to retrieve CAs: %w", err) } var rootCAs []models.CertificateAuthority for _, ca := range allCAs { if ca.Root { rootCAs = append(rootCAs, ca) } } return rootCAs, nil } // GetIntermediateCAs retrieves all intermediate certificate authorities func (s *CertificateAuthorityService) GetIntermediateCAs() ([]models.CertificateAuthority, error) { allCAs, err := s.caRepo.GetAll() if err != nil { return nil, fmt.Errorf("failed to retrieve CAs: %w", err) } var intermediateCAs []models.CertificateAuthority for _, ca := range allCAs { if !ca.Root { intermediateCAs = append(intermediateCAs, ca) } } return intermediateCAs, nil } // GetCAByParent retrieves all certificate authorities under a specific parent func (s *CertificateAuthorityService) GetCAByParent(parentID string) ([]models.CertificateAuthority, error) { allCAs, err := s.caRepo.GetAll() if err != nil { return nil, fmt.Errorf("failed to retrieve CAs: %w", err) } var childCAs []models.CertificateAuthority for _, ca := range allCAs { if ca.ParentID != nil && ca.ParentID.String() == parentID { childCAs = append(childCAs, ca) } } return childCAs, nil } // GetCACertificate retrieves the certificate for a CA func (s *CertificateAuthorityService) GetCACertificate(caID string) (*x509.Certificate, error) { ca, err := s.caRepo.GetByID(caID) if err != nil { return nil, fmt.Errorf("CA not found: %w", err) } cert, _, err := s.loadCACertificateAndKey(&ca) if err != nil { return nil, fmt.Errorf("failed to load CA certificate: %w", err) } return cert, nil } // GetCAPrivateKey retrieves the private key for a CA func (s *CertificateAuthorityService) GetCAPrivateKey(caID string) (interface{}, error) { ca, err := s.caRepo.GetByID(caID) if err != nil { return nil, fmt.Errorf("CA not found: %w", err) } _, privateKey, err := s.loadCACertificateAndKey(&ca) if err != nil { return nil, fmt.Errorf("failed to load CA private key: %w", err) } return privateKey, nil } // UpdateCA updates a certificate authority func (s *CertificateAuthorityService) UpdateCA(ca *models.CertificateAuthority) error { return s.caRepo.Update(*ca) } // DeleteCA deletes a certificate authority func (s *CertificateAuthorityService) DeleteCA(caID string) error { ca, err := s.caRepo.GetByID(caID) if err != nil { return fmt.Errorf("CA not found: %w", err) } // Check if CA has children children, err := s.GetCAByParent(caID) if err != nil { return fmt.Errorf("failed to check CA children: %w", err) } if len(children) > 0 { return fmt.Errorf("cannot delete CA with children: %d intermediate CAs depend on this CA", len(children)) } // Delete from database if err := s.caRepo.Delete(caID); err != nil { return fmt.Errorf("failed to delete CA from database: %w", err) } // Clean up files s.cleanupFiles(ca.FileID, ca.PrivateKeyID) return nil } // RevokeCA revokes a certificate authority (marks as revoked but keeps in database) func (s *CertificateAuthorityService) RevokeCA(caID string, reason string) error { ca, err := s.caRepo.GetByID(caID) if err != nil { return fmt.Errorf("CA not found: %w", err) } // Update CA with revocation information ca.Description = fmt.Sprintf("%s [REVOKED: %s]", ca.Description, reason) if err := s.caRepo.Update(ca); err != nil { return fmt.Errorf("failed to revoke CA: %w", err) } return nil } // ValidateCAChain validates the certificate chain for a CA func (s *CertificateAuthorityService) ValidateCAChain(caID string) error { ca, err := s.caRepo.GetByID(caID) if err != nil { return fmt.Errorf("CA not found: %w", err) } // Load CA certificate cert, _, err := s.loadCACertificateAndKey(&ca) if err != nil { return fmt.Errorf("failed to load CA certificate: %w", err) } // If it's a root CA, validate it's self-signed if ca.Root { if err := cert.CheckSignatureFrom(cert); err != nil { return fmt.Errorf("root CA certificate is not properly self-signed: %w", err) } return nil } // For intermediate CAs, validate the chain if ca.ParentID == nil { return fmt.Errorf("intermediate CA must have a parent") } parentCA, err := s.caRepo.GetByID(ca.ParentID.String()) if err != nil { return fmt.Errorf("parent CA not found: %w", err) } parentCert, _, err := s.loadCACertificateAndKey(&parentCA) if err != nil { return fmt.Errorf("failed to load parent CA certificate: %w", err) } // Validate certificate chain if err := cert.CheckSignatureFrom(parentCert); err != nil { return fmt.Errorf("CA certificate is not properly signed by parent: %w", err) } // Check validity period now := time.Now() if now.Before(cert.NotBefore) { return fmt.Errorf("CA certificate is not yet valid") } if now.After(cert.NotAfter) { return fmt.Errorf("CA certificate has expired") } return nil } // Helper methods // saveCertificate saves a certificate to a file and returns the file ID func (s *CertificateAuthorityService) saveCertificate(cert *x509.Certificate, prefix string) (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", prefix, 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 *CertificateAuthorityService) savePrivateKey(privateKey interface{}, prefix string) (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", prefix, 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 } // loadCACertificateAndKey loads a CA certificate and private key from files func (s *CertificateAuthorityService) loadCACertificateAndKey(ca *models.CertificateAuthority) (*x509.Certificate, interface{}, error) { // Load certificate certPath := filepath.Join(s.certDir, fmt.Sprintf("%s.crt", ca.FileID)) certPEM, err := os.ReadFile(certPath) if err != nil { return nil, nil, fmt.Errorf("failed to read certificate file: %w", err) } block, _ := pem.Decode(certPEM) if block == nil { return nil, nil, fmt.Errorf("failed to decode certificate PEM") } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, nil, fmt.Errorf("failed to parse certificate: %w", err) } // Load private key keyPath := filepath.Join(s.privateDir, fmt.Sprintf("%s.key", ca.PrivateKeyID)) keyPEM, err := os.ReadFile(keyPath) if err != nil { return nil, nil, fmt.Errorf("failed to read private key file: %w", err) } block, _ = pem.Decode(keyPEM) if block == nil { return nil, 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, nil, fmt.Errorf("unsupported private key type: %s", block.Type) } if err != nil { return nil, nil, fmt.Errorf("failed to parse private key: %w", err) } return cert, privateKey, nil } // cleanupFiles removes certificate and private key files func (s *CertificateAuthorityService) 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) } } // validateRootCARequest validates a root CA creation request func (s *CertificateAuthorityService) validateRootCARequest(req *CreateRootCARequest) 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.OrganizationID == uuid.Nil { return fmt.Errorf("organization ID is required") } return nil } // validateIntermediateCARequest validates an intermediate CA creation request func (s *CertificateAuthorityService) validateIntermediateCARequest(req *CreateIntermediateCARequest) 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.OrganizationID == uuid.Nil { return fmt.Errorf("organization ID is required") } if req.ParentCAID == uuid.Nil { return fmt.Errorf("parent CA ID is required") } return nil } // Request structures // CreateRootCARequest represents a request to create a root CA type CreateRootCARequest 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 OrganizationID uuid.UUID ValidityYears int // Custom validity period in years (0 = use default) } // CreateIntermediateCARequest represents a request to create an intermediate CA type CreateIntermediateCARequest 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 OrganizationID uuid.UUID ParentCAID uuid.UUID ValidityYears int // Custom validity period in years (0 = use default) }