Files

1057 lines
32 KiB
Go

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
}