package handlers import "archive/zip" import "bytes" import "crypto/rand" import "crypto/rsa" import "crypto/sha256" import "crypto/tls" import "crypto/x509" import "crypto/x509/pkix" import "database/sql" import "encoding/hex" import "encoding/pem" import "io" import "math/big" import "net" import "net/http" import "strconv" import "strings" import "time" import "codit/internal/models" type pkiCreateRootRequest struct { Name string `json:"name"` CommonName string `json:"common_name"` Days int `json:"days"` CertPEM string `json:"cert_pem"` KeyPEM string `json:"key_pem"` } type pkiCreateIntermediateRequest struct { Name string `json:"name"` ParentCAID string `json:"parent_ca_id"` CommonName string `json:"common_name"` Days int `json:"days"` CertPEM string `json:"cert_pem"` KeyPEM string `json:"key_pem"` } type pkiIssueCertRequest struct { CAID string `json:"ca_id"` CommonName string `json:"common_name"` SANDNS []string `json:"san_dns"` SANIPs []string `json:"san_ips"` Days int `json:"days"` IsCA bool `json:"is_ca"` } type pkiImportCertRequest struct { CAID string `json:"ca_id"` CertPEM string `json:"cert_pem"` KeyPEM string `json:"key_pem"` } type pkiRevokeRequest struct { Reason string `json:"reason"` } type pkiUpdateCARequest struct { Name string `json:"name"` } type pkiCASummary struct { ID string `json:"id"` Name string `json:"name"` ParentCAID string `json:"parent_ca_id"` IsRoot bool `json:"is_root"` Status string `json:"status"` CreatedAt int64 `json:"created_at"` UpdatedAt int64 `json:"updated_at"` } type pkiCertSummary struct { ID string `json:"id"` CAID string `json:"ca_id"` SerialHex string `json:"serial_hex"` CommonName string `json:"common_name"` Fingerprint string `json:"fingerprint"` SANDNS string `json:"san_dns"` SANIPs string `json:"san_ips"` IsCA bool `json:"is_ca"` NotBefore int64 `json:"not_before"` NotAfter int64 `json:"not_after"` Status string `json:"status"` RevokedAt int64 `json:"revoked_at"` RevocationReason string `json:"revocation_reason"` CreatedAt int64 `json:"created_at"` } func (api *API) ListPKICAs(w http.ResponseWriter, r *http.Request, _ map[string]string) { var items []models.PKICA var out []pkiCASummary var i int var err error if !api.requireAdmin(w, r) { return } items, err = api.Store.ListPKICAs() if err != nil { WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } for i = 0; i < len(items); i++ { out = append(out, pkiCASummary{ID: items[i].ID, Name: items[i].Name, ParentCAID: items[i].ParentCAID, IsRoot: items[i].IsRoot, Status: items[i].Status, CreatedAt: items[i].CreatedAt, UpdatedAt: items[i].UpdatedAt}) } WriteJSON(w, http.StatusOK, out) } func (api *API) GetPKICA(w http.ResponseWriter, r *http.Request, params map[string]string) { var ca models.PKICA var err error if !api.requireAdmin(w, r) { return } ca, err = api.Store.GetPKICA(params["id"]) if err != nil { WriteJSON(w, http.StatusNotFound, map[string]string{"error": "CA not found"}) return } WriteJSON(w, http.StatusOK, ca) } func (api *API) UpdatePKICA(w http.ResponseWriter, r *http.Request, params map[string]string) { var req pkiUpdateCARequest var ca models.PKICA var err error if !api.requireAdmin(w, r) { return } ca, err = api.Store.GetPKICA(params["id"]) if err != nil { WriteJSON(w, http.StatusNotFound, map[string]string{"error": "CA not found"}) return } err = DecodeJSON(r, &req) if err != nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"}) return } req.Name = strings.TrimSpace(req.Name) if req.Name == "" { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"}) return } err = api.Store.UpdatePKICAName(ca.ID, req.Name) if err != nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } ca, err = api.Store.GetPKICA(ca.ID) if err != nil { WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } WriteJSON(w, http.StatusOK, ca) } func (api *API) CreatePKIRootCA(w http.ResponseWriter, r *http.Request, _ map[string]string) { var req pkiCreateRootRequest var ca models.PKICA var cert *x509.Certificate var key *rsa.PrivateKey var certPEM string var keyPEM string var err error if !api.requireAdmin(w, r) { return } err = DecodeJSON(r, &req) if err != nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"}) return } if strings.TrimSpace(req.Name) == "" { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"}) return } if strings.TrimSpace(req.CertPEM) != "" || strings.TrimSpace(req.KeyPEM) != "" { cert, key, err = parseCertKeyPair(req.CertPEM, req.KeyPEM) if err != nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } if !cert.IsCA { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "imported certificate is not a CA"}) return } certPEM = req.CertPEM keyPEM = req.KeyPEM _ = key } else { certPEM, keyPEM, err = generateRootCA(strings.TrimSpace(req.CommonName), req.Days) if err != nil { WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } } ca = models.PKICA{Name: strings.TrimSpace(req.Name), ParentCAID: "", IsRoot: true, CertPEM: certPEM, KeyPEM: keyPEM, SerialCounter: 1, Status: "active"} ca, err = api.Store.CreatePKICA(ca) if err != nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } WriteJSON(w, http.StatusCreated, pkiCASummary{ID: ca.ID, Name: ca.Name, ParentCAID: ca.ParentCAID, IsRoot: ca.IsRoot, Status: ca.Status, CreatedAt: ca.CreatedAt, UpdatedAt: ca.UpdatedAt}) } func (api *API) CreatePKIIntermediateCA(w http.ResponseWriter, r *http.Request, _ map[string]string) { var req pkiCreateIntermediateRequest var parent models.PKICA var child models.PKICA var parentCert *x509.Certificate var importedCert *x509.Certificate var importedKey *rsa.PrivateKey var parentBlock *pem.Block var certPEM string var keyPEM string var err error if !api.requireAdmin(w, r) { return } err = DecodeJSON(r, &req) if err != nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"}) return } if strings.TrimSpace(req.Name) == "" || strings.TrimSpace(req.ParentCAID) == "" { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name and parent_ca_id are required"}) return } parent, err = api.Store.GetPKICA(strings.TrimSpace(req.ParentCAID)) if err != nil { WriteJSON(w, http.StatusNotFound, map[string]string{"error": "parent CA not found"}) return } if strings.TrimSpace(req.CertPEM) != "" || strings.TrimSpace(req.KeyPEM) != "" { importedCert, importedKey, err = parseCertKeyPair(req.CertPEM, req.KeyPEM) _ = importedKey if err != nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } if !importedCert.IsCA { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "imported certificate is not a CA"}) return } parentBlock, _ = pem.Decode([]byte(parent.CertPEM)) if parentBlock == nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid parent CA certificate"}) return } parentCert, err = x509.ParseCertificate(parentBlock.Bytes) if err != nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid parent CA certificate"}) return } err = importedCert.CheckSignatureFrom(parentCert) if err != nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "imported intermediate is not signed by selected parent CA"}) return } certPEM = req.CertPEM keyPEM = req.KeyPEM } else { certPEM, keyPEM, err = generateIssuedCA(parent, strings.TrimSpace(req.CommonName), req.Days) if err != nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } } child = models.PKICA{Name: strings.TrimSpace(req.Name), ParentCAID: parent.ID, IsRoot: false, CertPEM: certPEM, KeyPEM: keyPEM, SerialCounter: 1, Status: "active"} child, err = api.Store.CreatePKICA(child) if err != nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } WriteJSON(w, http.StatusCreated, pkiCASummary{ID: child.ID, Name: child.Name, ParentCAID: child.ParentCAID, IsRoot: child.IsRoot, Status: child.Status, CreatedAt: child.CreatedAt, UpdatedAt: child.UpdatedAt}) } func (api *API) ListPKICerts(w http.ResponseWriter, r *http.Request, _ map[string]string) { var caID string var items []models.PKICert var out []pkiCertSummary var fp string var i int var err error if !api.requireAdmin(w, r) { return } caID = strings.TrimSpace(r.URL.Query().Get("ca_id")) items, err = api.Store.ListPKICerts(caID) if err != nil { WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } for i = 0; i < len(items); i++ { fp = certificateFingerprint(items[i].CertPEM) out = append(out, pkiCertSummary{ID: items[i].ID, CAID: items[i].CAID, SerialHex: items[i].SerialHex, CommonName: items[i].CommonName, Fingerprint: fp, SANDNS: items[i].SANDNS, SANIPs: items[i].SANIPs, IsCA: items[i].IsCA, NotBefore: items[i].NotBefore, NotAfter: items[i].NotAfter, Status: items[i].Status, RevokedAt: items[i].RevokedAt, RevocationReason: items[i].RevocationReason, CreatedAt: items[i].CreatedAt}) } WriteJSON(w, http.StatusOK, out) } func (api *API) GetPKICert(w http.ResponseWriter, r *http.Request, params map[string]string) { var cert models.PKICert var err error if !api.requireAdmin(w, r) { return } cert, err = api.Store.GetPKICert(params["id"]) if err != nil { WriteJSON(w, http.StatusNotFound, map[string]string{"error": "certificate not found"}) return } WriteJSON(w, http.StatusOK, cert) } func (api *API) IssuePKICert(w http.ResponseWriter, r *http.Request, _ map[string]string) { var req pkiIssueCertRequest var ca models.PKICA var serial int64 var serialHex string var certPEM string var keyPEM string var notBefore int64 var notAfter int64 var cert models.PKICert var err error if !api.requireAdmin(w, r) { return } err = DecodeJSON(r, &req) if err != nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"}) return } if strings.TrimSpace(req.CAID) == "" || strings.TrimSpace(req.CommonName) == "" { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "ca_id and common_name are required"}) return } ca, err = api.Store.GetPKICA(strings.TrimSpace(req.CAID)) if err != nil { WriteJSON(w, http.StatusNotFound, map[string]string{"error": "CA not found"}) return } serial, err = api.Store.NextPKICASerial(ca.ID) if err != nil { WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } serialHex = strconv.FormatInt(serial, 16) certPEM, keyPEM, notBefore, notAfter, err = issueCertFromCA(ca, serial, strings.TrimSpace(req.CommonName), req.SANDNS, req.SANIPs, req.Days, req.IsCA) if err != nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } cert = models.PKICert{CAID: ca.ID, SerialHex: serialHex, CommonName: strings.TrimSpace(req.CommonName), SANDNS: strings.Join(req.SANDNS, ","), SANIPs: strings.Join(req.SANIPs, ","), IsCA: req.IsCA, CertPEM: certPEM, KeyPEM: keyPEM, NotBefore: notBefore, NotAfter: notAfter, Status: "active"} cert, err = api.Store.CreatePKICert(cert) if err != nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } WriteJSON(w, http.StatusCreated, pkiCertSummary{ID: cert.ID, CAID: cert.CAID, SerialHex: cert.SerialHex, CommonName: cert.CommonName, Fingerprint: certificateFingerprint(cert.CertPEM), SANDNS: cert.SANDNS, SANIPs: cert.SANIPs, IsCA: cert.IsCA, NotBefore: cert.NotBefore, NotAfter: cert.NotAfter, Status: cert.Status, RevokedAt: cert.RevokedAt, RevocationReason: cert.RevocationReason, CreatedAt: cert.CreatedAt}) } func (api *API) ImportPKICert(w http.ResponseWriter, r *http.Request, _ map[string]string) { var req pkiImportCertRequest var ca models.PKICA var importedCert *x509.Certificate var certPEM string var keyPEM string var certBlock *pem.Block var caCert *x509.Certificate var cert models.PKICert var dnsList []string var ipList []string var i int var sanIP string var serialHex string var verifyWithCA bool var err error if !api.requireAdmin(w, r) { return } err = DecodeJSON(r, &req) if err != nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid json"}) return } req.CAID = strings.TrimSpace(req.CAID) certPEM = strings.TrimSpace(req.CertPEM) keyPEM = strings.TrimSpace(req.KeyPEM) if certPEM == "" || keyPEM == "" { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "cert_pem and key_pem are required"}) return } importedCert, err = parseTLSCertificatePair(certPEM, keyPEM) if err != nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } if importedCert.IsCA { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "imported certificate is a CA; import it as a CA instead"}) return } verifyWithCA = req.CAID != "" if verifyWithCA { ca, err = api.Store.GetPKICA(req.CAID) if err != nil { WriteJSON(w, http.StatusNotFound, map[string]string{"error": "CA not found"}) return } certBlock, _ = pem.Decode([]byte(ca.CertPEM)) if certBlock == nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid issuer CA certificate"}) return } caCert, err = x509.ParseCertificate(certBlock.Bytes) if err != nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid issuer CA certificate"}) return } err = importedCert.CheckSignatureFrom(caCert) if err != nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "imported certificate is not signed by selected CA"}) return } } serialHex = strings.ToLower(importedCert.SerialNumber.Text(16)) for i = 0; i < len(importedCert.DNSNames); i++ { if strings.TrimSpace(importedCert.DNSNames[i]) == "" { continue } dnsList = append(dnsList, strings.TrimSpace(importedCert.DNSNames[i])) } for i = 0; i < len(importedCert.IPAddresses); i++ { sanIP = strings.TrimSpace(importedCert.IPAddresses[i].String()) if sanIP == "" { continue } ipList = append(ipList, sanIP) } cert = models.PKICert{ CAID: req.CAID, SerialHex: serialHex, CommonName: strings.TrimSpace(importedCert.Subject.CommonName), SANDNS: strings.Join(dnsList, ","), SANIPs: strings.Join(ipList, ","), IsCA: false, CertPEM: certPEM, KeyPEM: keyPEM, NotBefore: importedCert.NotBefore.UTC().Unix(), NotAfter: importedCert.NotAfter.UTC().Unix(), Status: "active", RevokedAt: 0, RevocationReason: "", } cert, err = api.Store.CreatePKICert(cert) if err != nil { WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } WriteJSON(w, http.StatusCreated, pkiCertSummary{ID: cert.ID, CAID: cert.CAID, SerialHex: cert.SerialHex, CommonName: cert.CommonName, Fingerprint: certificateFingerprint(cert.CertPEM), SANDNS: cert.SANDNS, SANIPs: cert.SANIPs, IsCA: cert.IsCA, NotBefore: cert.NotBefore, NotAfter: cert.NotAfter, Status: cert.Status, RevokedAt: cert.RevokedAt, RevocationReason: cert.RevocationReason, CreatedAt: cert.CreatedAt}) } func certificateFingerprint(certPEM string) string { var block *pem.Block var cert *x509.Certificate var sum [32]byte var err error block, _ = pem.Decode([]byte(certPEM)) if block == nil { return "" } cert, err = x509.ParseCertificate(block.Bytes) if err != nil { return "" } sum = sha256.Sum256(cert.Raw) return hex.EncodeToString(sum[:]) } func (api *API) RevokePKICert(w http.ResponseWriter, r *http.Request, params map[string]string) { var req pkiRevokeRequest var cert models.PKICert var err error if !api.requireAdmin(w, r) { return } cert, err = api.Store.GetPKICert(params["id"]) if err != nil { WriteJSON(w, http.StatusNotFound, map[string]string{"error": "certificate not found"}) return } if cert.Status == "revoked" { WriteJSON(w, http.StatusOK, map[string]string{"status": "already revoked"}) return } err = DecodeJSON(r, &req) if err != nil { req.Reason = "" } err = api.Store.RevokePKICert(cert.ID, strings.TrimSpace(req.Reason)) if err != nil { WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } WriteJSON(w, http.StatusOK, map[string]string{"status": "revoked"}) } func (api *API) DeletePKICert(w http.ResponseWriter, r *http.Request, params map[string]string) { var cert models.PKICert var appRefs int var listenerRefs int var err error if !api.requireAdmin(w, r) { return } cert, err = api.Store.GetPKICert(params["id"]) if err != nil { WriteJSON(w, http.StatusNotFound, map[string]string{"error": "certificate not found"}) return } appRefs, listenerRefs, err = api.Store.CountTLSServerCertReferences(cert.ID) if err != nil { WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if appRefs+listenerRefs > 0 { WriteJSON(w, http.StatusConflict, map[string]any{ "error": "certificate is referenced by TLS settings", "main_listener_ref_count": appRefs, "extra_listener_ref_count": listenerRefs, }) return } err = api.Store.DeletePKICert(cert.ID) if err != nil { WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } w.WriteHeader(http.StatusNoContent) } func (api *API) DeletePKICA(w http.ResponseWriter, r *http.Request, params map[string]string) { var ca models.PKICA var refCount int var appRefs int var listenerRefs int var children int var certs int var force bool var err error if !api.requireAdmin(w, r) { return } ca, err = api.Store.GetPKICA(params["id"]) if err != nil { WriteJSON(w, http.StatusNotFound, map[string]string{"error": "CA not found"}) return } force = false if r.URL.Query().Get("force") == "1" || strings.EqualFold(strings.TrimSpace(r.URL.Query().Get("force")), "true") { force = true } refCount, appRefs, listenerRefs, err = api.countTLSClientCARefsForDelete(ca.ID, force) if err != nil { WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if refCount > 0 { WriteJSON(w, http.StatusConflict, map[string]any{ "error": "CA is referenced by TLS client CA settings", "main_listener_ref_count": appRefs, "extra_listener_ref_count": listenerRefs, "total_ref_count": refCount, }) return } children, err = api.Store.CountPKICAChildren(ca.ID) if err != nil { WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } certs, err = api.Store.CountPKICertsByCA(ca.ID) if err != nil { WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } if !force && (children > 0 || certs > 0) { WriteJSON(w, http.StatusConflict, map[string]any{ "error": "CA has child CAs or issued certificates; use force delete", "child_ca_count": children, "issued_cert_count": certs, }) return } if force { err = api.Store.DeletePKICASubtree(ca.ID) } else { err = api.Store.DeletePKICA(ca.ID) } if err != nil { WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } w.WriteHeader(http.StatusNoContent) } func (api *API) countTLSClientCARefsForDelete(caID string, includeSubtree bool) (int, int, int, error) { var cas []models.PKICA var ids []string var include map[string]bool var total int var appTotal int var listenerTotal int var i int var j int var progress bool var appRefs int var listenerRefs int var err error if !includeSubtree { appRefs, listenerRefs, err = api.Store.CountTLSClientCAReferences(caID) if err != nil { return 0, 0, 0, err } return appRefs + listenerRefs, appRefs, listenerRefs, nil } cas, err = api.Store.ListPKICAs() if err != nil { return 0, 0, 0, err } include = map[string]bool{caID: true} for { progress = false for i = 0; i < len(cas); i++ { if include[cas[i].ID] { continue } if cas[i].ParentCAID != "" && include[cas[i].ParentCAID] { include[cas[i].ID] = true progress = true } } if !progress { break } } for j = 0; j < len(cas); j++ { if include[cas[j].ID] { ids = append(ids, cas[j].ID) } } for i = 0; i < len(ids); i++ { appRefs, listenerRefs, err = api.Store.CountTLSClientCAReferences(ids[i]) if err != nil { return 0, 0, 0, err } appTotal = appTotal + appRefs listenerTotal = listenerTotal + listenerRefs } total = appTotal + listenerTotal return total, appTotal, listenerTotal, nil } func (api *API) GetPKICRL(w http.ResponseWriter, r *http.Request, params map[string]string) { var ca models.PKICA var certs []models.PKICert var crlPEM string var err error if !api.requireAdmin(w, r) { return } ca, err = api.Store.GetPKICA(params["id"]) if err != nil { WriteJSON(w, http.StatusNotFound, map[string]string{"error": "CA not found"}) return } certs, err = api.Store.ListPKICerts(ca.ID) if err != nil { WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } crlPEM, err = buildCRLPEM(ca, certs) if err != nil { WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } WriteJSON(w, http.StatusOK, map[string]string{"crl_pem": crlPEM}) } func (api *API) DownloadPKICABundle(w http.ResponseWriter, r *http.Request, params map[string]string) { var ca models.PKICA var data []byte var err error if !api.requireAdmin(w, r) { return } ca, err = api.Store.GetPKICA(params["id"]) if err != nil { WriteJSON(w, http.StatusNotFound, map[string]string{"error": "CA not found"}) return } data, err = buildPKIZipBundle(ca.Name, ca.CertPEM, ca.KeyPEM, true) if err != nil { WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } w.Header().Set("Content-Type", "application/zip") w.Header().Set("Content-Disposition", "attachment; filename=\""+sanitizeBundleName(ca.Name)+".ca.bundle.zip\"") w.WriteHeader(http.StatusOK) _, _ = w.Write(data) } func (api *API) DownloadPKICertBundle(w http.ResponseWriter, r *http.Request, params map[string]string) { var cert models.PKICert var data []byte var err error if !api.requireAdmin(w, r) { return } cert, err = api.Store.GetPKICert(params["id"]) if err != nil { WriteJSON(w, http.StatusNotFound, map[string]string{"error": "certificate not found"}) return } data, err = buildPKIZipBundle(cert.CommonName, cert.CertPEM, cert.KeyPEM, false) if err != nil { WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()}) return } w.Header().Set("Content-Type", "application/zip") w.Header().Set("Content-Disposition", "attachment; filename=\""+sanitizeBundleName(cert.CommonName)+".bundle.zip\"") w.WriteHeader(http.StatusOK) _, _ = w.Write(data) } func (api *API) ServePKICRL(w http.ResponseWriter, r *http.Request) { var path string var base string var caID string var ca models.PKICA var certs []models.PKICert var crlPEM string var err error path = strings.TrimPrefix(r.URL.Path, "/pki/crl/") base = path if strings.HasSuffix(base, ".pem") { base = strings.TrimSuffix(base, ".pem") } caID = strings.TrimSpace(base) if caID == "" { w.WriteHeader(http.StatusNotFound) return } ca, err = api.Store.GetPKICA(caID) if err != nil { w.WriteHeader(http.StatusNotFound) return } certs, err = api.Store.ListPKICerts(ca.ID) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } crlPEM, err = buildCRLPEM(ca, certs) if err != nil { w.WriteHeader(http.StatusInternalServerError) return } w.Header().Set("Content-Type", "application/x-pem-file") w.WriteHeader(http.StatusOK) _, _ = w.Write([]byte(crlPEM)) } func parseCertKeyPair(certPEM string, keyPEM string) (*x509.Certificate, *rsa.PrivateKey, error) { var certBlock *pem.Block var keyBlock *pem.Block var cert *x509.Certificate var key *rsa.PrivateKey var keyAny any var err error certBlock, _ = pem.Decode([]byte(certPEM)) if certBlock == nil { return nil, nil, sql.ErrNoRows } cert, err = x509.ParseCertificate(certBlock.Bytes) if err != nil { return nil, nil, err } keyBlock, _ = pem.Decode([]byte(keyPEM)) if keyBlock == nil { return nil, nil, sql.ErrNoRows } keyAny, err = x509.ParsePKCS8PrivateKey(keyBlock.Bytes) if err != nil { key, err = x509.ParsePKCS1PrivateKey(keyBlock.Bytes) if err != nil { return nil, nil, err } } else { key, _ = keyAny.(*rsa.PrivateKey) if key == nil { return nil, nil, sql.ErrNoRows } } err = cert.CheckSignatureFrom(cert) if cert.IsCA && err != nil { err = nil } return cert, key, nil } func parseTLSCertificatePair(certPEM string, keyPEM string) (*x509.Certificate, error) { var certBlock *pem.Block var parsedCert *x509.Certificate var err error _, err = tls.X509KeyPair([]byte(certPEM), []byte(keyPEM)) if err != nil { return nil, err } certBlock, _ = pem.Decode([]byte(certPEM)) if certBlock == nil { return nil, sql.ErrNoRows } parsedCert, err = x509.ParseCertificate(certBlock.Bytes) if err != nil { return nil, err } return parsedCert, nil } func generateRootCA(commonName string, days int) (string, string, error) { var key *rsa.PrivateKey var now time.Time var end time.Time var serial *big.Int var tmpl x509.Certificate var der []byte var certPEM string var keyPEM string var err error if strings.TrimSpace(commonName) == "" { commonName = "Codit Root CA" } if days <= 0 { days = 3650 } key, err = rsa.GenerateKey(rand.Reader, 2048) if err != nil { return "", "", err } now = time.Now().UTC() end = now.Add(time.Duration(days) * 24 * time.Hour) serial = big.NewInt(now.UnixNano()) tmpl = x509.Certificate{SerialNumber: serial, Subject: pkix.Name{CommonName: commonName}, NotBefore: now, NotAfter: end, KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature, BasicConstraintsValid: true, IsCA: true, MaxPathLenZero: false} der, err = x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, &key.PublicKey, key) if err != nil { return "", "", err } certPEM = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})) keyPEM = string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})) return certPEM, keyPEM, nil } func generateIssuedCA(parent models.PKICA, commonName string, days int) (string, string, error) { var serial int64 var serialHex string var certPEM string var keyPEM string var notBefore int64 var notAfter int64 var err error if days <= 0 { days = 1825 } serial = time.Now().UTC().UnixNano() serialHex = strconv.FormatInt(serial, 16) _ = serialHex certPEM, keyPEM, notBefore, notAfter, err = issueCertFromCA(parent, serial, commonName, nil, nil, days, true) _ = notBefore _ = notAfter if err != nil { return "", "", err } return certPEM, keyPEM, nil } func issueCertFromCA(ca models.PKICA, serial int64, commonName string, dns []string, ips []string, days int, isCA bool) (string, string, int64, int64, error) { var caCert *x509.Certificate var caKey *rsa.PrivateKey var certBlock *pem.Block var key *rsa.PrivateKey var now time.Time var end time.Time var serialNum *big.Int var tmpl x509.Certificate var i int var ip net.IP var der []byte var certPEM string var keyPEM string var err error if days <= 0 { days = 365 } if strings.TrimSpace(commonName) == "" { return "", "", 0, 0, sql.ErrNoRows } certBlock, _ = pem.Decode([]byte(ca.CertPEM)) if certBlock == nil { return "", "", 0, 0, sql.ErrNoRows } caCert, err = x509.ParseCertificate(certBlock.Bytes) if err != nil { return "", "", 0, 0, err } _, caKey, err = parseCertKeyPair(ca.CertPEM, ca.KeyPEM) if err != nil { return "", "", 0, 0, err } key, err = rsa.GenerateKey(rand.Reader, 2048) if err != nil { return "", "", 0, 0, err } now = time.Now().UTC() end = now.Add(time.Duration(days) * 24 * time.Hour) serialNum = big.NewInt(serial) tmpl = x509.Certificate{SerialNumber: serialNum, Subject: pkix.Name{CommonName: commonName}, NotBefore: now, NotAfter: end, KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, BasicConstraintsValid: true, IsCA: isCA} if isCA { tmpl.KeyUsage = x509.KeyUsageCertSign | x509.KeyUsageCRLSign | x509.KeyUsageDigitalSignature tmpl.MaxPathLenZero = false } else { tmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth, x509.ExtKeyUsageClientAuth} } for i = 0; i < len(dns); i++ { if strings.TrimSpace(dns[i]) != "" { tmpl.DNSNames = append(tmpl.DNSNames, strings.TrimSpace(dns[i])) } } for i = 0; i < len(ips); i++ { ip = net.ParseIP(strings.TrimSpace(ips[i])) if ip != nil { tmpl.IPAddresses = append(tmpl.IPAddresses, ip) } } der, err = x509.CreateCertificate(rand.Reader, &tmpl, caCert, &key.PublicKey, caKey) if err != nil { return "", "", 0, 0, err } certPEM = string(pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})) keyPEM = string(pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(key)})) return certPEM, keyPEM, now.Unix(), end.Unix(), nil } func buildCRLPEM(ca models.PKICA, certs []models.PKICert) (string, error) { var certBlock *pem.Block var caCert *x509.Certificate var caKey *rsa.PrivateKey var revoked []pkix.RevokedCertificate var i int var serialBytes []byte var serial *big.Int var when time.Time var der []byte var pemText string var err error certBlock, _ = pem.Decode([]byte(ca.CertPEM)) if certBlock == nil { return "", sql.ErrNoRows } caCert, err = x509.ParseCertificate(certBlock.Bytes) if err != nil { return "", err } _, caKey, err = parseCertKeyPair(ca.CertPEM, ca.KeyPEM) if err != nil { return "", err } for i = 0; i < len(certs); i++ { if certs[i].Status != "revoked" { continue } serialBytes, err = hex.DecodeString(certs[i].SerialHex) if err != nil { continue } serial = new(big.Int).SetBytes(serialBytes) if certs[i].RevokedAt > 0 { when = time.Unix(certs[i].RevokedAt, 0).UTC() } else { when = time.Now().UTC() } revoked = append(revoked, pkix.RevokedCertificate{SerialNumber: serial, RevocationTime: when}) } der, err = caCert.CreateCRL(rand.Reader, caKey, revoked, time.Now().UTC(), time.Now().UTC().Add(7*24*time.Hour)) if err != nil { return "", err } pemText = string(pem.EncodeToMemory(&pem.Block{Type: "X509 CRL", Bytes: der})) return pemText, nil } func buildPKIZipBundle(name string, certPEM string, keyPEM string, isCA bool) ([]byte, error) { var buf bytes.Buffer var zw *zip.Writer var certName string var keyName string var file io.Writer var err error zw = zip.NewWriter(&buf) if isCA { certName = sanitizeBundleName(name) + ".ca.crt.pem" keyName = sanitizeBundleName(name) + ".ca.key.pem" } else { certName = sanitizeBundleName(name) + ".crt.pem" keyName = sanitizeBundleName(name) + ".key.pem" } file, err = zw.Create(certName) if err != nil { _ = zw.Close() return nil, err } _, err = file.Write([]byte(certPEM)) if err != nil { _ = zw.Close() return nil, err } file, err = zw.Create(keyName) if err != nil { _ = zw.Close() return nil, err } _, err = file.Write([]byte(keyPEM)) if err != nil { _ = zw.Close() return nil, err } err = zw.Close() if err != nil { return nil, err } return buf.Bytes(), nil } func sanitizeBundleName(name string) string { var cleaned string cleaned = strings.TrimSpace(name) if cleaned == "" { return "certificate" } cleaned = strings.ReplaceAll(cleaned, " ", "-") cleaned = strings.ReplaceAll(cleaned, "/", "-") cleaned = strings.ReplaceAll(cleaned, "\\", "-") return cleaned }