package certs import ( "context" "crypto" "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/tls" "crypto/x509" "crypto/x509/pkix" "encoding/hex" "encoding/json" "encoding/pem" "errors" "fmt" "math/big" "net" "os" "path/filepath" "strings" "sync" "time" "golang.org/x/crypto/acme" "debtik/internal/logging" ) const ( DefaultCertsDir = "/etc/debtik/certs" DefaultMetadataFile = "certs.json" TypeCA = "ca" TypeServer = "server" TypeClient = "client" TypeACME = "acme" KeyRSA2048 = "RSA-2048" KeyRSA4096 = "RSA-4096" KeyECDSAP256 = "ECDSA-P256" ) type CertificateInfo struct { ACMERequest *LetsEncryptRequest `json:"acme_request,omitempty"` RenewalError string `json:"renewal_error,omitempty"` LastRenewAttempt time.Time `json:"last_renew_attempt,omitempty"` ID string `json:"id"` Name string `json:"name"` CommonName string `json:"common_name"` SANs []string `json:"sans"` IsCA bool `json:"is_ca"` Issuer string `json:"issuer"` KeyType string `json:"key_type"` ValidFrom time.Time `json:"valid_from"` ValidTo time.Time `json:"valid_to"` Fingerprint string `json:"fingerprint"` CertPath string `json:"cert_path"` KeyPath string `json:"key_path"` Type string `json:"type"` Comment string `json:"comment,omitempty"` } type CreateCARequest struct { Name string `json:"name"` CommonName string `json:"common_name"` Organization string `json:"organization,omitempty"` Country string `json:"country,omitempty"` Days int `json:"days,omitempty"` KeyType string `json:"key_type,omitempty"` Comment string `json:"comment,omitempty"` } type CreateCertRequest struct { Name string `json:"name"` CommonName string `json:"common_name"` CAName string `json:"ca_name,omitempty"` SANs []string `json:"sans,omitempty"` Days int `json:"days,omitempty"` KeyType string `json:"key_type,omitempty"` IsServer bool `json:"is_server"` Organization string `json:"organization,omitempty"` Country string `json:"country,omitempty"` Comment string `json:"comment,omitempty"` } type ImportCertRequest struct { Name string `json:"name"` CertPEM string `json:"cert_pem"` KeyPEM string `json:"key_pem"` Comment string `json:"comment,omitempty"` } type LetsEncryptRequest struct { renew bool Name string `json:"name"` Domain string `json:"domain"` Email string `json:"email"` Staging bool `json:"staging"` Comment string `json:"comment,omitempty"` } type CertManager struct { dir string metaPath string mu sync.RWMutex certs map[string]*CertificateInfo challenges map[string]string // token -> keyAuth challengeMu sync.RWMutex renewMu sync.Mutex accountMu sync.Mutex } func NewCertManager(customDir string) (*CertManager, error) { d := customDir if d == "" { d = DefaultCertsDir } if err := os.MkdirAll(d, 0700); err != nil { return nil, fmt.Errorf("failed to create certs directory %s: %w", d, err) } cm := &CertManager{ dir: d, metaPath: filepath.Join(d, DefaultMetadataFile), certs: make(map[string]*CertificateInfo), challenges: make(map[string]string), } if err := cm.load(); err != nil { logging.Log("certs,warning", "Warning loading cert metadata: %v", err) } return cm, nil } func (cm *CertManager) load() error { cm.mu.Lock() defer cm.mu.Unlock() data, err := os.ReadFile(cm.metaPath) if err != nil { if os.IsNotExist(err) { return nil } return err } var list []*CertificateInfo if err := json.Unmarshal(data, &list); err != nil { return err } for _, c := range list { if c.Name != "" { cm.certs[strings.ToLower(c.Name)] = c } } return nil } func (cm *CertManager) saveLocked() error { var list []*CertificateInfo for _, c := range cm.certs { list = append(list, c) } data, err := json.MarshalIndent(list, "", " ") if err != nil { return err } tmp := fmt.Sprintf("%s.tmp.%d", cm.metaPath, time.Now().UnixNano()) if err := os.WriteFile(tmp, data, 0600); err != nil { return err } return os.Rename(tmp, cm.metaPath) } // RegisterHTTP01Challenge stores an ACME challenge token and its response for HTTP-01 verification. func (cm *CertManager) RegisterHTTP01Challenge(token, keyAuth string) { cm.challengeMu.Lock() defer cm.challengeMu.Unlock() cm.challenges[token] = keyAuth } // UnregisterHTTP01Challenge removes an ACME challenge token. func (cm *CertManager) UnregisterHTTP01Challenge(token string) { cm.challengeMu.Lock() defer cm.challengeMu.Unlock() delete(cm.challenges, token) } // GetHTTP01Challenge looks up an ACME challenge token for HTTP-01 verification. func (cm *CertManager) GetHTTP01Challenge(token string) (string, bool) { cm.challengeMu.RLock() defer cm.challengeMu.RUnlock() auth, ok := cm.challenges[token] return auth, ok } // ListCertificates returns all certificates known to the manager. func (cm *CertManager) ListCertificates() []*CertificateInfo { cm.mu.RLock() defer cm.mu.RUnlock() var result []*CertificateInfo for _, c := range cm.certs { copyItem := *c result = append(result, ©Item) } return result } // GetCertificate returns metadata for a certificate by name. func (cm *CertManager) GetCertificate(name string) (*CertificateInfo, error) { cm.mu.RLock() defer cm.mu.RUnlock() c, ok := cm.certs[strings.ToLower(name)] if !ok { return nil, fmt.Errorf("certificate %q not found", name) } copyItem := *c return ©Item, nil } // GetTLSCertificate loads a parsed tls.Certificate ready for TLS listeners. func (cm *CertManager) GetTLSCertificate(name string) (tls.Certificate, error) { cm.mu.RLock() c, ok := cm.certs[strings.ToLower(name)] cm.mu.RUnlock() if !ok { return tls.Certificate{}, fmt.Errorf("certificate %q not found", name) } if _, err := os.Stat(c.CertPath); err != nil { return tls.Certificate{}, fmt.Errorf("certificate file missing: %w", err) } if _, err := os.Stat(c.KeyPath); err != nil { return tls.Certificate{}, fmt.Errorf("private key file missing: %w", err) } return tls.LoadX509KeyPair(c.CertPath, c.KeyPath) } // ExportPEM returns the PEM encoded certificate and private key content. func (cm *CertManager) ExportPEM(name string) (certPEM string, keyPEM string, err error) { cm.mu.RLock() c, ok := cm.certs[strings.ToLower(name)] cm.mu.RUnlock() if !ok { return "", "", fmt.Errorf("certificate %q not found", name) } certBytes, err := os.ReadFile(c.CertPath) if err != nil { return "", "", fmt.Errorf("failed to read cert file: %w", err) } keyBytes, err := os.ReadFile(c.KeyPath) if err != nil { return "", "", fmt.Errorf("failed to read key file: %w", err) } return string(certBytes), string(keyBytes), nil } // DeleteCertificate removes a certificate, its files, and metadata. func (cm *CertManager) DeleteCertificate(name string) error { cm.mu.Lock() defer cm.mu.Unlock() key := strings.ToLower(name) c, ok := cm.certs[key] if !ok { return fmt.Errorf("certificate %q not found", name) } _ = os.Remove(c.CertPath) _ = os.Remove(c.KeyPath) delete(cm.certs, key) if err := cm.saveLocked(); err != nil { return err } logging.Log("certs,info", "Certificate %s deleted", name) return nil } // CreateCA creates a new Self-Signed Certificate Authority (CA). func (cm *CertManager) CreateCA(req CreateCARequest) (*CertificateInfo, error) { name := strings.TrimSpace(req.Name) if name == "" { return nil, errors.New("certificate name is required") } cn := strings.TrimSpace(req.CommonName) if cn == "" { cn = name } cm.mu.Lock() defer cm.mu.Unlock() if _, exists := cm.certs[strings.ToLower(name)]; exists { return nil, fmt.Errorf("certificate %q already exists", name) } days := req.Days if days <= 0 { days = 3650 // 10 years default for CA } keyType := req.KeyType if keyType == "" { keyType = KeyRSA2048 } privKey, pubKey, err := generateKeyPair(keyType) if err != nil { return nil, fmt.Errorf("failed generating key: %w", err) } serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) if err != nil { return nil, fmt.Errorf("failed generating serial: %w", err) } org := req.Organization if org == "" { org = "DebTik Network Appliance" } country := req.Country if country == "" { country = "GR" } now := time.Now().UTC() template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ CommonName: cn, Organization: []string{org}, Country: []string{country}, }, NotBefore: now.Add(-1 * time.Hour), NotAfter: now.AddDate(0, 0, days), IsCA: true, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature, BasicConstraintsValid: true, MaxPathLenZero: false, } certBytes, err := x509.CreateCertificate(rand.Reader, &template, &template, pubKey, privKey) if err != nil { return nil, fmt.Errorf("failed creating CA certificate: %w", err) } certPath := filepath.Join(cm.dir, fmt.Sprintf("%s.crt", name)) keyPath := filepath.Join(cm.dir, fmt.Sprintf("%s.key", name)) if err := savePEM(certPath, "CERTIFICATE", certBytes); err != nil { return nil, err } if err := savePrivateKeyPEM(keyPath, privKey); err != nil { _ = os.Remove(certPath) return nil, err } fp := sha256.Sum256(certBytes) info := &CertificateInfo{ ID: fmt.Sprintf("cert-%d", time.Now().UnixNano()), Name: name, CommonName: cn, IsCA: true, Issuer: "Self-Signed CA", KeyType: keyType, ValidFrom: template.NotBefore, ValidTo: template.NotAfter, Fingerprint: formatFingerprint(fp[:]), CertPath: certPath, KeyPath: keyPath, Type: TypeCA, Comment: req.Comment, } cm.certs[strings.ToLower(name)] = info if err := cm.saveLocked(); err != nil { return nil, err } logging.Log("certs,info", "Created new Certificate Authority: %s (CN: %s)", name, cn) return info, nil } // CreateCertificate issues a server or client certificate signed by a CA or self-signed. func (cm *CertManager) CreateCertificate(req CreateCertRequest) (*CertificateInfo, error) { name := strings.TrimSpace(req.Name) if name == "" { return nil, errors.New("certificate name is required") } cn := strings.TrimSpace(req.CommonName) if cn == "" { return nil, errors.New("common name is required") } cm.mu.Lock() defer cm.mu.Unlock() if _, exists := cm.certs[strings.ToLower(name)]; exists { return nil, fmt.Errorf("certificate %q already exists", name) } days := req.Days if days <= 0 { days = 365 // 1 year default } keyType := req.KeyType if keyType == "" { keyType = KeyRSA2048 } privKey, pubKey, err := generateKeyPair(keyType) if err != nil { return nil, fmt.Errorf("failed generating key: %w", err) } serialNumber, err := rand.Int(rand.Reader, new(big.Int).Lsh(big.NewInt(1), 128)) if err != nil { return nil, fmt.Errorf("failed generating serial: %w", err) } org := req.Organization if org == "" { org = "DebTik Network Appliance" } country := req.Country if country == "" { country = "GR" } now := time.Now().UTC() template := x509.Certificate{ SerialNumber: serialNumber, Subject: pkix.Name{ CommonName: cn, Organization: []string{org}, Country: []string{country}, }, NotBefore: now.Add(-1 * time.Hour), NotAfter: now.AddDate(0, 0, days), KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, BasicConstraintsValid: true, IsCA: false, } if req.IsServer { template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageServerAuth) } else { template.ExtKeyUsage = append(template.ExtKeyUsage, x509.ExtKeyUsageClientAuth) } // Process SANs (DNS names & IP addresses) sansMap := make(map[string]bool) if ip := net.ParseIP(cn); ip != nil { template.IPAddresses = append(template.IPAddresses, ip) sansMap[cn] = true } else if strings.Contains(cn, ".") || cn != "" { template.DNSNames = append(template.DNSNames, cn) sansMap[cn] = true } for _, s := range req.SANs { s = strings.TrimSpace(s) if s == "" || sansMap[s] { continue } sansMap[s] = true if ip := net.ParseIP(s); ip != nil { template.IPAddresses = append(template.IPAddresses, ip) } else { template.DNSNames = append(template.DNSNames, s) } } var sansList []string for s := range sansMap { sansList = append(sansList, s) } var signerCert *x509.Certificate var signerKey crypto.PrivateKey issuerName := "Self-Signed" var caPEMBytes []byte if req.CAName != "" { caInfo, ok := cm.certs[strings.ToLower(req.CAName)] if !ok || !caInfo.IsCA { return nil, fmt.Errorf("signing CA %q not found or is not a CA", req.CAName) } caCert, caKey, err := loadCertAndKey(caInfo.CertPath, caInfo.KeyPath) if err != nil { return nil, fmt.Errorf("failed loading CA cert/key: %w", err) } signerCert = caCert signerKey = caKey issuerName = caInfo.Name caPEMBytes, _ = os.ReadFile(caInfo.CertPath) } else { signerCert = &template signerKey = privKey } certBytes, err := x509.CreateCertificate(rand.Reader, &template, signerCert, pubKey, signerKey) if err != nil { return nil, fmt.Errorf("failed creating certificate: %w", err) } certPath := filepath.Join(cm.dir, fmt.Sprintf("%s.crt", name)) keyPath := filepath.Join(cm.dir, fmt.Sprintf("%s.key", name)) // If signed by CA, build chain: [Cert] + [CA Cert] certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: certBytes}) if len(caPEMBytes) > 0 { certPEM = append(certPEM, caPEMBytes...) } if err := os.WriteFile(certPath, certPEM, 0644); err != nil { return nil, err } if err := savePrivateKeyPEM(keyPath, privKey); err != nil { _ = os.Remove(certPath) return nil, err } fp := sha256.Sum256(certBytes) info := &CertificateInfo{ ID: fmt.Sprintf("cert-%d", time.Now().UnixNano()), Name: name, CommonName: cn, SANs: sansList, IsCA: false, Issuer: issuerName, KeyType: keyType, ValidFrom: template.NotBefore, ValidTo: template.NotAfter, Fingerprint: formatFingerprint(fp[:]), CertPath: certPath, KeyPath: keyPath, Type: TypeServer, Comment: req.Comment, } if !req.IsServer { info.Type = TypeClient } cm.certs[strings.ToLower(name)] = info if err := cm.saveLocked(); err != nil { return nil, err } logging.Log("certs,info", "Created new certificate %s (Issuer: %s, CN: %s)", name, issuerName, cn) return info, nil } // ImportCertificate imports user-provided PEM certificate and private key. func (cm *CertManager) ImportCertificate(req ImportCertRequest) (*CertificateInfo, error) { name := strings.TrimSpace(req.Name) if name == "" { return nil, errors.New("certificate name is required") } certBlock, _ := pem.Decode([]byte(req.CertPEM)) if certBlock == nil || certBlock.Type != "CERTIFICATE" { return nil, errors.New("invalid or empty PEM certificate") } parsedCert, err := x509.ParseCertificate(certBlock.Bytes) if err != nil { return nil, fmt.Errorf("failed to parse certificate: %w", err) } // Verify key matches certificate _, err = tls.X509KeyPair([]byte(req.CertPEM), []byte(req.KeyPEM)) if err != nil { return nil, fmt.Errorf("private key does not match certificate: %w", err) } cm.mu.Lock() defer cm.mu.Unlock() if _, exists := cm.certs[strings.ToLower(name)]; exists { return nil, fmt.Errorf("certificate %q already exists", name) } certPath := filepath.Join(cm.dir, fmt.Sprintf("%s.crt", name)) keyPath := filepath.Join(cm.dir, fmt.Sprintf("%s.key", name)) if err := os.WriteFile(certPath, []byte(strings.TrimSpace(req.CertPEM)+"\n"), 0644); err != nil { return nil, err } if err := os.WriteFile(keyPath, []byte(strings.TrimSpace(req.KeyPEM)+"\n"), 0600); err != nil { _ = os.Remove(certPath) return nil, err } var sans []string for _, dns := range parsedCert.DNSNames { sans = append(sans, dns) } for _, ip := range parsedCert.IPAddresses { sans = append(sans, ip.String()) } fp := sha256.Sum256(parsedCert.Raw) cType := TypeServer if parsedCert.IsCA { cType = TypeCA } keyType := "Unknown" switch parsedCert.PublicKeyAlgorithm { case x509.RSA: keyType = "RSA" case x509.ECDSA: keyType = "ECDSA" } info := &CertificateInfo{ ID: fmt.Sprintf("cert-%d", time.Now().UnixNano()), Name: name, CommonName: parsedCert.Subject.CommonName, SANs: sans, IsCA: parsedCert.IsCA, Issuer: parsedCert.Issuer.CommonName, KeyType: keyType, ValidFrom: parsedCert.NotBefore, ValidTo: parsedCert.NotAfter, Fingerprint: formatFingerprint(fp[:]), CertPath: certPath, KeyPath: keyPath, Type: cType, Comment: req.Comment, } cm.certs[strings.ToLower(name)] = info if err := cm.saveLocked(); err != nil { return nil, err } logging.Log("certs,info", "Imported certificate: %s (CN: %s)", name, info.CommonName) return info, nil } // IssueLetsEncrypt requests and issues a certificate from Let's Encrypt via ACME HTTP-01 challenge. func (cm *CertManager) IssueLetsEncrypt(ctx context.Context, req LetsEncryptRequest) (*CertificateInfo, error) { name := strings.TrimSpace(req.Name) domain := strings.TrimSpace(req.Domain) email := strings.TrimSpace(req.Email) if domain == "" { return nil, errors.New("domain is required for Let's Encrypt") } if name == "" { name = domain } cm.mu.Lock() if old, exists := cm.certs[strings.ToLower(name)]; exists && (!req.renew || old.Type != TypeACME) { cm.mu.Unlock() return nil, fmt.Errorf("certificate %q already exists", name) } cm.mu.Unlock() logging.Log("certs,info", "Starting Let's Encrypt order for domain %s (Staging=%t)", domain, req.Staging) // Generate ACME account key accountKey, err := cm.acmeAccountKey(req.Staging) if err != nil { return nil, fmt.Errorf("failed generating ACME account key: %w", err) } client := &acme.Client{ Key: accountKey, } if req.Staging { client.DirectoryURL = "https://acme-staging-v02.api.letsencrypt.org/directory" } else { client.DirectoryURL = acme.LetsEncryptURL } // Register account account := &acme.Account{} if email != "" { account.Contact = []string{"mailto:" + email} } _, err = client.Register(ctx, account, acme.AcceptTOS) if err != nil && !strings.Contains(err.Error(), "already exists") { return nil, fmt.Errorf("failed registering ACME account: %w", err) } // Create order for domain order, err := client.AuthorizeOrder(ctx, acme.DomainIDs(domain)) if err != nil { return nil, fmt.Errorf("failed requesting order for %s: %w", domain, err) } // Complete authorization for each authorization URL for _, authzURL := range order.AuthzURLs { authz, err := client.GetAuthorization(ctx, authzURL) if err != nil { return nil, fmt.Errorf("failed getting authorization %s: %w", authzURL, err) } if authz.Status == acme.StatusValid { continue } var challenge *acme.Challenge for _, ch := range authz.Challenges { if ch.Type == "http-01" { challenge = ch break } } if challenge == nil { return nil, fmt.Errorf("no http-01 challenge available for domain %s", domain) } keyAuth, err := client.HTTP01ChallengeResponse(challenge.Token) if err != nil { return nil, fmt.Errorf("failed generating challenge response: %w", err) } // Register in our web server challenge map cm.RegisterHTTP01Challenge(challenge.Token, keyAuth) defer cm.UnregisterHTTP01Challenge(challenge.Token) // Inform ACME server challenge is ready if _, err := client.Accept(ctx, challenge); err != nil { return nil, fmt.Errorf("failed accepting challenge: %w", err) } // Wait for authorization to succeed if _, err := client.WaitAuthorization(ctx, challenge.URI); err != nil { return nil, fmt.Errorf("ACME authorization failed for %s: %w", domain, err) } } // Generate certificate private key certPrivKey, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, fmt.Errorf("failed generating certificate key: %w", err) } csrTemplate := x509.CertificateRequest{ Subject: pkix.Name{CommonName: domain}, DNSNames: []string{domain}, } csr, err := x509.CreateCertificateRequest(rand.Reader, &csrTemplate, certPrivKey) if err != nil { return nil, fmt.Errorf("failed creating CSR: %w", err) } // Finalize order certDerList, certURL, err := client.CreateOrderCert(ctx, order.FinalizeURL, csr, true) if err != nil { return nil, fmt.Errorf("failed finalizing ACME order: %w", err) } if len(certDerList) == 0 && certURL != "" { certDerList, err = client.FetchCert(ctx, certURL, true) if err != nil { return nil, fmt.Errorf("failed fetching issued cert: %w", err) } } // Build certificate chain in PEM format var certPEM []byte var firstCert *x509.Certificate for idx, der := range certDerList { certPEM = append(certPEM, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})...) if idx == 0 { firstCert, _ = x509.ParseCertificate(der) } } certPath := filepath.Join(cm.dir, fmt.Sprintf("%s.crt", name)) keyPath := filepath.Join(cm.dir, fmt.Sprintf("%s.key", name)) if err := os.WriteFile(certPath, certPEM, 0644); err != nil { return nil, err } if err := savePrivateKeyPEM(keyPath, certPrivKey); err != nil { _ = os.Remove(certPath) return nil, err } cm.mu.Lock() defer cm.mu.Unlock() var fp [32]byte validFrom := time.Now() validTo := time.Now().AddDate(0, 0, 90) if firstCert != nil { fp = sha256.Sum256(firstCert.Raw) validFrom = firstCert.NotBefore validTo = firstCert.NotAfter } issuer := "Let's Encrypt" if req.Staging { issuer = "Let's Encrypt (Staging)" } requestCopy := req requestCopy.Name = name requestCopy.renew = false info := &CertificateInfo{ ACMERequest: &requestCopy, ID: fmt.Sprintf("cert-%d", time.Now().UnixNano()), Name: name, CommonName: domain, SANs: []string{domain}, IsCA: false, Issuer: issuer, KeyType: KeyRSA2048, ValidFrom: validFrom, ValidTo: validTo, Fingerprint: formatFingerprint(fp[:]), CertPath: certPath, KeyPath: keyPath, Type: TypeACME, Comment: req.Comment, } cm.certs[strings.ToLower(name)] = info if err := cm.saveLocked(); err != nil { return nil, err } logging.Log("certs,info", "Successfully issued Let's Encrypt certificate for %s", domain) return info, nil } // Helpers func generateKeyPair(keyType string) (crypto.PrivateKey, crypto.PublicKey, error) { switch strings.ToUpper(keyType) { case KeyRSA4096: priv, err := rsa.GenerateKey(rand.Reader, 4096) if err != nil { return nil, nil, err } return priv, &priv.PublicKey, nil case KeyECDSAP256: priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) if err != nil { return nil, nil, err } return priv, &priv.PublicKey, nil default: // KeyRSA2048 priv, err := rsa.GenerateKey(rand.Reader, 2048) if err != nil { return nil, nil, err } return priv, &priv.PublicKey, nil } } func savePEM(path, blockType string, data []byte) error { block := &pem.Block{Type: blockType, Bytes: data} return os.WriteFile(path, pem.EncodeToMemory(block), 0644) } func savePrivateKeyPEM(path string, key crypto.PrivateKey) error { var block *pem.Block switch k := key.(type) { case *rsa.PrivateKey: block = &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(k)} case *ecdsa.PrivateKey: b, err := x509.MarshalECPrivateKey(k) if err != nil { return err } block = &pem.Block{Type: "EC PRIVATE KEY", Bytes: b} default: b, err := x509.MarshalPKCS8PrivateKey(key) if err != nil { return err } block = &pem.Block{Type: "PRIVATE KEY", Bytes: b} } return os.WriteFile(path, pem.EncodeToMemory(block), 0600) } func loadCertAndKey(certPath, keyPath string) (*x509.Certificate, crypto.PrivateKey, error) { certPEM, err := os.ReadFile(certPath) if err != nil { return nil, nil, err } block, _ := pem.Decode(certPEM) if block == nil { return nil, nil, errors.New("failed decoding certificate PEM") } cert, err := x509.ParseCertificate(block.Bytes) if err != nil { return nil, nil, err } keyPEM, err := os.ReadFile(keyPath) if err != nil { return nil, nil, err } keyBlock, _ := pem.Decode(keyPEM) if keyBlock == nil { return nil, nil, errors.New("failed decoding private key PEM") } if k, err := x509.ParsePKCS1PrivateKey(keyBlock.Bytes); err == nil { return cert, k, nil } if k, err := x509.ParseECPrivateKey(keyBlock.Bytes); err == nil { return cert, k, nil } if k, err := x509.ParsePKCS8PrivateKey(keyBlock.Bytes); err == nil { return cert, k, nil } return nil, nil, errors.New("unrecognized private key format") } func formatFingerprint(b []byte) string { hexStr := hex.EncodeToString(b) var parts []string for i := 0; i < len(hexStr); i += 2 { parts = append(parts, strings.ToUpper(hexStr[i:i+2])) } return strings.Join(parts, ":") }