package utils import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "crypto/x509/pkix" "encoding/asn1" "encoding/pem" "fmt" "math/big" "net" "net/url" "strings" "time" "git.secnex.io/secnex/certman/models" ) // KeyType represents the type of cryptographic key type KeyType string const ( KeyTypeRSA KeyType = "rsa" KeyTypeECDSA KeyType = "ecdsa" ) // KeySize represents the size of RSA keys type KeySize int const ( KeySize2048 KeySize = 2048 KeySize3072 KeySize = 3072 KeySize4096 KeySize = 4096 ) // Curve represents ECDSA curves type Curve string const ( CurveP256 Curve = "P-256" CurveP384 Curve = "P-384" CurveP521 Curve = "P-521" ) // CertificateConfig holds configuration for certificate generation type CertificateConfig struct { // Basic Information CommonName string Organization string OrganizationalUnit string Country string State string Locality string Street string PostalCode string Email string // Certificate Details SerialNumber *big.Int NotBefore time.Time NotAfter time.Time CertificateType models.CertificateType // Subject Alternative Names DNSNames []string IPAddresses []net.IP EmailAddresses []string URIs []string // Key Configuration KeyType KeyType KeySize KeySize // Only used for RSA Curve Curve // Only used for ECDSA // CA Configuration IsCA bool MaxPathLen int MaxPathLenZero bool BasicConstraintsValid bool // Key Usage DigitalSignature bool ContentCommitment bool KeyEncipherment bool DataEncipherment bool KeyAgreement bool KeyCertSign bool CRLSign bool EncipherOnly bool DecipherOnly bool // Extended Key Usage ServerAuth bool ClientAuth bool CodeSigning bool EmailProtection bool TimeStamping bool OCSPSigning bool // Additional Extensions SubjectKeyID bool AuthorityKeyID bool CRLDistributionPoints []string OCSPServers []string // iOS-specific extensions AuthorityInfoAccess []string // AIA URLs for iOS compatibility IssuingCertificateURL string // URL to issuing certificate // Validity period configuration ValidityYears int // Custom validity period in years (overrides default) // Platform compatibility UseSHA256ForKeyIDs bool // Use SHA-256 for Key IDs (iOS compatible, MacBook compatible) } // DefaultCertificateConfig returns a default configuration for the given certificate type func DefaultCertificateConfig(certType models.CertificateType) *CertificateConfig { config := &CertificateConfig{ CommonName: "", Organization: "", OrganizationalUnit: "", Country: "DE", State: "", Locality: "", Street: "", PostalCode: "", Email: "", SerialNumber: generateSerialNumber(), NotBefore: time.Now(), NotAfter: time.Now().AddDate(0, 3, 0), // 3 months default CertificateType: certType, DNSNames: []string{}, IPAddresses: []net.IP{}, EmailAddresses: []string{}, URIs: []string{}, KeyType: KeyTypeRSA, KeySize: KeySize2048, Curve: CurveP256, IsCA: false, MaxPathLen: -1, MaxPathLenZero: false, BasicConstraintsValid: true, SubjectKeyID: true, AuthorityKeyID: true, CRLDistributionPoints: []string{}, OCSPServers: []string{}, // iOS-specific defaults AuthorityInfoAccess: []string{}, IssuingCertificateURL: "", // Validity period defaults ValidityYears: 0, // 0 means use default // Platform compatibility defaults UseSHA256ForKeyIDs: true, // Use SHA-256 for modern compatibility (iOS + MacBook) } // Set defaults based on certificate type switch certType { case models.CertificateTypeCA: config.IsCA = true config.KeyCertSign = true config.CRLSign = true config.NotAfter = calculateValidityPeriod(config.NotBefore, config.ValidityYears, 10) // Default 10 years for CA config.MaxPathLen = 0 case models.CertificateTypeWeb, models.CertificateTypeServer: config.DigitalSignature = true config.KeyEncipherment = true config.ServerAuth = true config.NotAfter = calculateValidityPeriodMonths(config.NotBefore, config.ValidityYears, 3) // Default 3 months for web/server // Enterprise defaults: Use larger key size if not specified if config.KeySize == KeySize2048 && config.ValidityYears > 1 { config.KeySize = KeySize4096 // Use 4096 bit for enterprise certificates } // iOS-compatibility: Always ensure Common Name is in SAN if config.CommonName != "" { found := false for _, dns := range config.DNSNames { if dns == config.CommonName { found = true break } } if !found { config.DNSNames = append(config.DNSNames, config.CommonName) } } // iOS-compatibility: Add Authority Information Access config.AuthorityInfoAccess = []string{"http://ca.secnex.internal/ca.crt"} config.IssuingCertificateURL = "http://ca.secnex.internal/ca.crt" case models.CertificateTypeClient: config.DigitalSignature = true config.KeyEncipherment = true config.ClientAuth = true config.NotAfter = calculateValidityPeriod(config.NotBefore, config.ValidityYears, 1) // Default 1 year for client case models.CertificateTypeEmail: config.DigitalSignature = true config.KeyEncipherment = true config.EmailProtection = true config.NotAfter = calculateValidityPeriod(config.NotBefore, config.ValidityYears, 1) // Default 1 year for email case models.CertificateTypeCode: config.DigitalSignature = true config.CodeSigning = true config.NotAfter = calculateValidityPeriod(config.NotBefore, config.ValidityYears, 2) // Default 2 years for code signing } return config } // CACertificateConfig returns a configuration specifically for CA certificates func CACertificateConfig(isRoot bool) *CertificateConfig { config := DefaultCertificateConfig(models.CertificateTypeCA) config.IsCA = true config.KeyCertSign = true config.CRLSign = true if isRoot { config.NotAfter = calculateValidityPeriod(config.NotBefore, config.ValidityYears, 20) // Default 20 years for root CA config.MaxPathLen = -1 // No limit for root CA } else { config.NotAfter = calculateValidityPeriod(config.NotBefore, config.ValidityYears, 10) // Default 10 years for intermediate CA config.MaxPathLen = 0 // Intermediate CA cannot issue other CAs by default } return config } // CACertificateConfigWithValidity returns a CA configuration with custom validity period func CACertificateConfigWithValidity(isRoot bool, validityYears int) *CertificateConfig { config := CACertificateConfig(isRoot) config.ValidityYears = validityYears if isRoot { config.NotAfter = calculateValidityPeriod(config.NotBefore, validityYears, 20) } else { config.NotAfter = calculateValidityPeriod(config.NotBefore, validityYears, 10) } return config } // WebServerCertificateConfig returns a configuration for web server certificates func WebServerCertificateConfig(domains []string) *CertificateConfig { config := DefaultCertificateConfig(models.CertificateTypeWeb) config.DNSNames = domains config.DigitalSignature = true config.KeyEncipherment = true config.ServerAuth = true config.NotAfter = config.NotBefore.AddDate(0, 3, 0) // 3 months for web certificates return config } // ClientCertificateConfig returns a configuration for client certificates func ClientCertificateConfig() *CertificateConfig { config := DefaultCertificateConfig(models.CertificateTypeClient) config.DigitalSignature = true config.KeyEncipherment = true config.ClientAuth = true return config } // EmailCertificateConfig returns a configuration for email certificates func EmailCertificateConfig(email string) *CertificateConfig { config := DefaultCertificateConfig(models.CertificateTypeEmail) config.EmailAddresses = []string{email} config.DigitalSignature = true config.KeyEncipherment = true config.EmailProtection = true return config } // CodeSigningCertificateConfig returns a configuration for code signing certificates func CodeSigningCertificateConfig() *CertificateConfig { config := DefaultCertificateConfig(models.CertificateTypeCode) config.DigitalSignature = true config.CodeSigning = true return config } // CertificateGenerator handles certificate generation type CertificateGenerator struct { config *CertificateConfig } // NewCertificateGenerator creates a new certificate generator func NewCertificateGenerator(config *CertificateConfig) *CertificateGenerator { return &CertificateGenerator{ config: config, } } // GenerateKeyPair generates a key pair based on the configuration func (cg *CertificateGenerator) GenerateKeyPair() (interface{}, error) { switch cg.config.KeyType { case KeyTypeRSA: return rsa.GenerateKey(rand.Reader, int(cg.config.KeySize)) case KeyTypeECDSA: var curve elliptic.Curve switch cg.config.Curve { case CurveP256: curve = elliptic.P256() case CurveP384: curve = elliptic.P384() case CurveP521: curve = elliptic.P521() default: return nil, fmt.Errorf("unsupported ECDSA curve: %s", cg.config.Curve) } return ecdsa.GenerateKey(curve, rand.Reader) default: return nil, fmt.Errorf("unsupported key type: %s", cg.config.KeyType) } } // GenerateSelfSignedCertificate generates a self-signed certificate func (cg *CertificateGenerator) GenerateSelfSignedCertificate() (*x509.Certificate, interface{}, error) { // Generate key pair privateKey, err := cg.GenerateKeyPair() if err != nil { return nil, nil, fmt.Errorf("failed to generate key pair: %w", err) } // Create certificate template template := cg.createCertificateTemplate() // Generate certificate certDER, err := x509.CreateCertificate(rand.Reader, template, template, cg.getPublicKey(privateKey), privateKey) if err != nil { return nil, nil, fmt.Errorf("failed to create certificate: %w", err) } // Parse certificate cert, err := x509.ParseCertificate(certDER) if err != nil { return nil, nil, fmt.Errorf("failed to parse certificate: %w", err) } return cert, privateKey, nil } // GenerateCertificate generates a certificate signed by a CA func (cg *CertificateGenerator) GenerateCertificate(caCert *x509.Certificate, caPrivateKey interface{}) (*x509.Certificate, interface{}, error) { // Generate key pair privateKey, err := cg.GenerateKeyPair() if err != nil { return nil, nil, fmt.Errorf("failed to generate key pair: %w", err) } // Create certificate template template := cg.createCertificateTemplate() // Generate certificate certDER, err := x509.CreateCertificate(rand.Reader, template, caCert, cg.getPublicKey(privateKey), caPrivateKey) if err != nil { return nil, nil, fmt.Errorf("failed to create certificate: %w", err) } // Parse certificate cert, err := x509.ParseCertificate(certDER) if err != nil { return nil, nil, fmt.Errorf("failed to parse certificate: %w", err) } return cert, privateKey, nil } // createCertificateTemplate creates an x509.Certificate template from the configuration func (cg *CertificateGenerator) createCertificateTemplate() *x509.Certificate { template := &x509.Certificate{ SerialNumber: cg.config.SerialNumber, Subject: pkix.Name{ CommonName: cg.config.CommonName, Organization: []string{cg.config.Organization}, OrganizationalUnit: []string{cg.config.OrganizationalUnit}, Country: []string{cg.config.Country}, Province: []string{cg.config.State}, Locality: []string{cg.config.Locality}, StreetAddress: []string{cg.config.Street}, PostalCode: []string{cg.config.PostalCode}, }, NotBefore: cg.config.NotBefore, NotAfter: cg.config.NotAfter, DNSNames: cg.config.DNSNames, IPAddresses: cg.config.IPAddresses, EmailAddresses: cg.config.EmailAddresses, URIs: parseURIs(cg.config.URIs), } // Set key usage var keyUsage x509.KeyUsage if cg.config.DigitalSignature { keyUsage |= x509.KeyUsageDigitalSignature } if cg.config.ContentCommitment { keyUsage |= x509.KeyUsageContentCommitment } if cg.config.KeyEncipherment { keyUsage |= x509.KeyUsageKeyEncipherment } if cg.config.DataEncipherment { keyUsage |= x509.KeyUsageDataEncipherment } if cg.config.KeyAgreement { keyUsage |= x509.KeyUsageKeyAgreement } if cg.config.KeyCertSign { keyUsage |= x509.KeyUsageCertSign } if cg.config.CRLSign { keyUsage |= x509.KeyUsageCRLSign } if cg.config.EncipherOnly { keyUsage |= x509.KeyUsageEncipherOnly } if cg.config.DecipherOnly { keyUsage |= x509.KeyUsageDecipherOnly } template.KeyUsage = keyUsage // Set extended key usage var extKeyUsage []x509.ExtKeyUsage if cg.config.ServerAuth { extKeyUsage = append(extKeyUsage, x509.ExtKeyUsageServerAuth) } if cg.config.ClientAuth { extKeyUsage = append(extKeyUsage, x509.ExtKeyUsageClientAuth) } if cg.config.CodeSigning { extKeyUsage = append(extKeyUsage, x509.ExtKeyUsageCodeSigning) } if cg.config.EmailProtection { extKeyUsage = append(extKeyUsage, x509.ExtKeyUsageEmailProtection) } if cg.config.TimeStamping { extKeyUsage = append(extKeyUsage, x509.ExtKeyUsageTimeStamping) } if cg.config.OCSPSigning { extKeyUsage = append(extKeyUsage, x509.ExtKeyUsageOCSPSigning) } template.ExtKeyUsage = extKeyUsage // Set basic constraints if cg.config.BasicConstraintsValid { template.BasicConstraintsValid = true template.IsCA = cg.config.IsCA if cg.config.IsCA { template.MaxPathLen = cg.config.MaxPathLen template.MaxPathLenZero = cg.config.MaxPathLenZero } } // Set subject key identifier if cg.config.SubjectKeyID { template.SubjectKeyId = generateSubjectKeyID(cg.getPublicKey(nil)) } // Set authority key identifier if cg.config.AuthorityKeyID { template.AuthorityKeyId = generateAuthorityKeyID(cg.getPublicKey(nil)) } // Add CRL distribution points if len(cg.config.CRLDistributionPoints) > 0 { template.CRLDistributionPoints = cg.config.CRLDistributionPoints } // Add OCSP servers if len(cg.config.OCSPServers) > 0 { template.OCSPServer = cg.config.OCSPServers } // Add Authority Information Access (AIA) for iOS compatibility if len(cg.config.AuthorityInfoAccess) > 0 { template.IssuingCertificateURL = cg.config.AuthorityInfoAccess } return template } // getPublicKey extracts the public key from a private key func (cg *CertificateGenerator) getPublicKey(privateKey interface{}) interface{} { if privateKey == nil { return nil } switch k := privateKey.(type) { case *rsa.PrivateKey: return &k.PublicKey case *ecdsa.PrivateKey: return &k.PublicKey default: return nil } } // CertificateExporter handles certificate export functionality type CertificateExporter struct{} // NewCertificateExporter creates a new certificate exporter func NewCertificateExporter() *CertificateExporter { return &CertificateExporter{} } // ExportCertificateToPEM exports a certificate to PEM format func (ce *CertificateExporter) ExportCertificateToPEM(cert *x509.Certificate) ([]byte, error) { return pem.EncodeToMemory(&pem.Block{ Type: "CERTIFICATE", Bytes: cert.Raw, }), nil } // ExportPrivateKeyToPEM exports a private key to PEM format func (ce *CertificateExporter) ExportPrivateKeyToPEM(privateKey interface{}) ([]byte, error) { var pemType string var keyBytes []byte var err error switch k := privateKey.(type) { case *rsa.PrivateKey: pemType = "RSA PRIVATE KEY" keyBytes = x509.MarshalPKCS1PrivateKey(k) case *ecdsa.PrivateKey: pemType = "EC PRIVATE KEY" keyBytes, err = x509.MarshalECPrivateKey(k) if err != nil { return nil, fmt.Errorf("failed to marshal ECDSA private key: %w", err) } default: return nil, fmt.Errorf("unsupported private key type") } return pem.EncodeToMemory(&pem.Block{ Type: pemType, Bytes: keyBytes, }), nil } // ExportCertificateToDER exports a certificate to DER format func (ce *CertificateExporter) ExportCertificateToDER(cert *x509.Certificate) []byte { return cert.Raw } // ExportPrivateKeyToDER exports a private key to DER format func (ce *CertificateExporter) ExportPrivateKeyToDER(privateKey interface{}) ([]byte, error) { switch k := privateKey.(type) { case *rsa.PrivateKey: return x509.MarshalPKCS1PrivateKey(k), nil case *ecdsa.PrivateKey: return x509.MarshalECPrivateKey(k) default: return nil, fmt.Errorf("unsupported private key type") } } // Utility functions // calculateValidityPeriod calculates the validity period based on custom years or default func calculateValidityPeriod(notBefore time.Time, customYears int, defaultYears int) time.Time { if customYears > 0 { return notBefore.AddDate(customYears, 0, 0) } return notBefore.AddDate(defaultYears, 0, 0) } // calculateValidityPeriodMonths calculates the validity period based on custom years or default months func calculateValidityPeriodMonths(notBefore time.Time, customYears int, defaultMonths int) time.Time { if customYears > 0 { return notBefore.AddDate(customYears, 0, 0) } return notBefore.AddDate(0, defaultMonths, 0) } // generateSerialNumber generates a random serial number func generateSerialNumber() *big.Int { serial, _ := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) return serial } // generateSubjectKeyID generates a subject key identifier using SHA-256 (modern compatibility) func generateSubjectKeyID(publicKey interface{}) []byte { if publicKey == nil { return nil } var publicKeyBytes []byte var err error switch k := publicKey.(type) { case *rsa.PublicKey: publicKeyBytes, err = asn1.Marshal(k) case *ecdsa.PublicKey: publicKeyBytes, err = asn1.Marshal(k) default: return nil } if err != nil { return nil } // Use SHA-256 for modern compatibility (iOS + MacBook + modern browsers) hash := sha256.Sum256(publicKeyBytes) return hash[:] } // generateAuthorityKeyID generates an authority key identifier func generateAuthorityKeyID(publicKey interface{}) []byte { return generateSubjectKeyID(publicKey) } // parseURIs converts string URIs to *url.URL func parseURIs(uriStrings []string) []*url.URL { var uris []*url.URL for _, uriStr := range uriStrings { if uri, err := url.Parse(uriStr); err == nil { uris = append(uris, uri) } } return uris } // ParseSANs parses Subject Alternative Names from a string func ParseSANs(sans string) ([]string, []net.IP, []string, []string) { var dnsNames []string var ipAddresses []net.IP var emailAddresses []string var uris []string if sans == "" { return dnsNames, ipAddresses, emailAddresses, uris } parts := strings.Split(sans, ",") for _, part := range parts { part = strings.TrimSpace(part) if part == "" { continue } // Check if it's an IP address if ip := net.ParseIP(part); ip != nil { ipAddresses = append(ipAddresses, ip) continue } // Check if it's an email address if strings.Contains(part, "@") { emailAddresses = append(emailAddresses, part) continue } // Check if it's a URI if strings.HasPrefix(part, "http://") || strings.HasPrefix(part, "https://") { uris = append(uris, part) continue } // Default to DNS name dnsNames = append(dnsNames, part) } return dnsNames, ipAddresses, emailAddresses, uris } // ValidateCertificateConfig validates a certificate configuration func ValidateCertificateConfig(config *CertificateConfig) error { if config.CommonName == "" { return fmt.Errorf("common name is required") } if config.Organization == "" { return fmt.Errorf("organization is required") } if config.Country == "" { return fmt.Errorf("country is required") } if config.NotBefore.After(config.NotAfter) { return fmt.Errorf("not before date must be before not after date") } if config.IsCA && !config.KeyCertSign { return fmt.Errorf("CA certificates must have key cert sign usage") } // Modern browser and iOS validations if config.CertificateType == models.CertificateTypeWeb { // Ensure Common Name is in SAN (required by modern browsers and iOS) if config.CommonName != "" { found := false for _, dns := range config.DNSNames { if dns == config.CommonName { found = true break } } if !found { return fmt.Errorf("Common Name must be included in Subject Alternative Names for modern browser compatibility") } } // Validate key size (minimum 2048 bits required) if config.KeyType == KeyTypeRSA && config.KeySize < KeySize2048 { return fmt.Errorf("RSA keys must be at least 2048 bits") } // Validate validity period (maximum 825 days, recommended 398 days) validityDays := int(config.NotAfter.Sub(config.NotBefore).Hours() / 24) if validityDays > 825 { return fmt.Errorf("certificate validity period exceeds 825 days (current: %d days)", validityDays) } } return nil } // CreateCertificateFromModel creates a certificate configuration from a model func CreateCertificateFromModel(cert *models.Certificate) *CertificateConfig { config := DefaultCertificateConfig(cert.Type) config.CommonName = cert.AttributeCommonName config.Organization = cert.AttributeOrganization config.OrganizationalUnit = cert.AttributeOrganizationUnit config.Country = cert.AttributeCountry config.State = cert.AttributeState config.Locality = cert.AttributeLocality config.Street = cert.AttributeStreet config.PostalCode = cert.AttributePostalCode config.Email = cert.AttributeEmail config.NotBefore = cert.AttributeNotBefore config.NotAfter = cert.AttributeNotAfter // Parse SANs if cert.AttributeSubjectAlternativeName != "" { dnsNames, ipAddresses, emailAddresses, uris := ParseSANs(cert.AttributeSubjectAlternativeName) config.DNSNames = dnsNames config.IPAddresses = ipAddresses config.EmailAddresses = emailAddresses config.URIs = uris } return config } // CreateCAFromModel creates a CA configuration from a model func CreateCAFromModel(ca *models.CertificateAuthority) *CertificateConfig { config := CACertificateConfig(ca.Root) config.CommonName = ca.AttributeCommonName config.Organization = ca.AttributeOrganization config.OrganizationalUnit = ca.AttributeOrganizationUnit config.Country = ca.AttributeCountry config.State = ca.AttributeState config.Locality = ca.AttributeLocality config.Street = ca.AttributeStreet config.PostalCode = ca.AttributePostalCode config.Email = ca.AttributeEmail config.NotBefore = ca.AttributeNotBefore config.NotAfter = ca.AttributeNotAfter return config }