
- 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
784 lines
22 KiB
Go
784 lines
22 KiB
Go
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
|
|
}
|