1057 lines
32 KiB
Go
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
|
|
}
|