Files
codit/backend/internal/handlers/pki_client_profiles.go

980 lines
32 KiB
Go

package handlers
import "crypto/rand"
import "crypto/rsa"
import "crypto/x509"
import "crypto/x509/pkix"
import "database/sql"
import "encoding/asn1"
import "encoding/pem"
import "errors"
import "math/big"
import "net"
import "net/http"
import "net/url"
import "sort"
import "strconv"
import "strings"
import "time"
import "codit/internal/middleware"
import "codit/internal/models"
import "codit/internal/pkiutil"
import "codit/internal/util"
const logIDPKI string = "pki"
const pkiClientMinValidSeconds int64 = 60
const pkiClientMaxValidSeconds int64 = 604800
type pkiClientProfileTargetRequest struct {
TargetType string `json:"target_type"`
TargetID string `json:"target_id"`
}
type pkiClientProfileRequest struct {
Name string `json:"name"`
CAID string `json:"ca_id"`
SubjectOrganization string `json:"subject_organization"`
SANURIPrefix string `json:"san_uri_prefix"`
AllowServerAuth bool `json:"allow_server_auth"`
AuthzPermissions []string `json:"authz_permissions"`
AuthzScope string `json:"authz_scope"`
DefaultValidSeconds int64 `json:"default_valid_seconds"`
MaxValidSeconds int64 `json:"max_valid_seconds"`
Enabled bool `json:"enabled"`
Targets []pkiClientProfileTargetRequest `json:"targets"`
}
type pkiClientIssueRequest struct {
ProfileID string `json:"profile_id"`
ValidSeconds int64 `json:"valid_seconds"`
CommonName string `json:"common_name"`
SANDNS []string `json:"san_dns"`
SANIPs []string `json:"san_ips"`
}
type pkiClientProfileTargetSummary struct {
ProfileID string `json:"profile_id"`
TargetType string `json:"target_type"`
TargetID string `json:"target_id"`
TargetName string `json:"target_name"`
TargetActive bool `json:"target_active"`
CreatedAt int64 `json:"created_at"`
}
type pkiClientProfileSummary struct {
ID string `json:"id"`
Name string `json:"name"`
CAID string `json:"ca_id"`
CAName string `json:"ca_name"`
SubjectOrganization string `json:"subject_organization"`
SANURIPrefix string `json:"san_uri_prefix"`
AllowServerAuth bool `json:"allow_server_auth"`
AuthzPermissions []string `json:"authz_permissions"`
AuthzScope string `json:"authz_scope"`
DefaultValidSeconds int64 `json:"default_valid_seconds"`
MaxValidSeconds int64 `json:"max_valid_seconds"`
Enabled bool `json:"enabled"`
CreatedByUserID string `json:"created_by_user_id"`
CreatedByUsername string `json:"created_by_username"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
Targets []pkiClientProfileTargetSummary `json:"targets"`
}
type pkiClientIssuanceSummary struct {
ID string `json:"id"`
CertID string `json:"cert_id"`
UserID string `json:"user_id"`
Username string `json:"username"`
ProfileID string `json:"profile_id"`
ProfileName string `json:"profile_name"`
SerialHex string `json:"serial_hex"`
CommonName string `json:"common_name"`
SANURI string `json:"san_uri"`
AuthzPermissions []string `json:"authz_permissions"`
AuthzScope string `json:"authz_scope"`
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"`
}
type pkiClientIssueResponse struct {
Issuance pkiClientIssuanceSummary `json:"issuance"`
CertPEM string `json:"cert_pem"`
KeyPEM string `json:"key_pem"`
CACertPEM string `json:"ca_cert_pem"`
}
type coditMTLSAuthorization struct {
Version int
UserID string
Username string
ProfileID string
Permissions []string
Scope string
}
func (api *API) ListPKIClientProfiles(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var targetType string
var targetID string
var items []models.PKIClientProfile
var out []pkiClientProfileSummary
var i int
var err error
if !api.requireAdmin(w, r) {
return
}
targetType = strings.TrimSpace(r.URL.Query().Get("target_type"))
targetID = strings.TrimSpace(r.URL.Query().Get("target_id"))
items, err = api.store(r).ListPKIClientProfiles(targetType, targetID)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
for i = 0; i < len(items); i++ {
out = append(out, buildPKIClientProfileSummary(items[i]))
}
WriteJSON(w, http.StatusOK, out)
}
func (api *API) CreatePKIClientProfile(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var req pkiClientProfileRequest
var currentUser models.User
var hasCurrentUser bool
var ca models.PKICA
var permissions []string
var permissionsValue string
var targets []models.PKIClientProfileTarget
var item models.PKIClientProfile
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 request"})
return
}
ca, err = api.store(r).GetPKICA(strings.TrimSpace(req.CAID))
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "CA not found"})
return
}
if ca.Status != "active" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "selected CA is not active"})
return
}
permissions, permissionsValue, err = normalizePKIClientProfilePermissions(req.AuthzPermissions)
_ = permissions
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
targets, err = normalizePKIClientProfileTargets(req.Targets)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
err = validatePKIClientProfileWindow(req.DefaultValidSeconds, req.MaxValidSeconds)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
req.SubjectOrganization = normalizePKIClientSubjectOrganization(req.SubjectOrganization)
req.SANURIPrefix, err = normalizePKIClientSANURIPrefix(req.SANURIPrefix)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
currentUser, hasCurrentUser = middleware.UserFromContext(r.Context())
item = models.PKIClientProfile{
Name: strings.TrimSpace(req.Name),
CAID: ca.ID,
SubjectOrganization: req.SubjectOrganization,
SANURIPrefix: req.SANURIPrefix,
AllowServerAuth: req.AllowServerAuth,
AuthzPermissions: permissionsValue,
AuthzScope: strings.TrimSpace(req.AuthzScope),
DefaultValidSeconds: req.DefaultValidSeconds,
MaxValidSeconds: req.MaxValidSeconds,
Enabled: req.Enabled,
Targets: targets,
}
if strings.TrimSpace(item.Name) == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
return
}
if hasCurrentUser {
item.CreatedByUserID = currentUser.ID
}
item, err = api.store(r).CreatePKIClientProfile(item)
if err != nil {
api.Logger.Write(logIDPKI, util.LOG_WARN, "client profile create failed name=%s err=%v", item.Name, err)
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
api.Logger.Write(logIDPKI, util.LOG_INFO, "client profile create success id=%s name=%s targets=%d", item.ID, item.Name, len(item.Targets))
WriteJSON(w, http.StatusCreated, buildPKIClientProfileSummary(item))
}
func (api *API) UpdatePKIClientProfile(w http.ResponseWriter, r *http.Request, params map[string]string) {
var req pkiClientProfileRequest
var existing models.PKIClientProfile
var ca models.PKICA
var permissions []string
var permissionsValue string
var targets []models.PKIClientProfileTarget
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 request"})
return
}
existing, err = api.store(r).GetPKIClientProfile(params["id"])
if err != nil {
if err == sql.ErrNoRows {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "profile not found"})
return
}
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
ca, err = api.store(r).GetPKICA(strings.TrimSpace(req.CAID))
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "CA not found"})
return
}
if ca.Status != "active" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "selected CA is not active"})
return
}
permissions, permissionsValue, err = normalizePKIClientProfilePermissions(req.AuthzPermissions)
_ = permissions
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
targets, err = normalizePKIClientProfileTargets(req.Targets)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
err = validatePKIClientProfileWindow(req.DefaultValidSeconds, req.MaxValidSeconds)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
req.SubjectOrganization = normalizePKIClientSubjectOrganization(req.SubjectOrganization)
req.SANURIPrefix, err = normalizePKIClientSANURIPrefix(req.SANURIPrefix)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
existing.Name = strings.TrimSpace(req.Name)
existing.CAID = ca.ID
existing.CAName = ca.Name
existing.SubjectOrganization = req.SubjectOrganization
existing.SANURIPrefix = req.SANURIPrefix
existing.AllowServerAuth = req.AllowServerAuth
existing.AuthzPermissions = permissionsValue
existing.AuthzScope = strings.TrimSpace(req.AuthzScope)
existing.DefaultValidSeconds = req.DefaultValidSeconds
existing.MaxValidSeconds = req.MaxValidSeconds
existing.Enabled = req.Enabled
existing.Targets = targets
if strings.TrimSpace(existing.Name) == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
return
}
existing, err = api.store(r).UpdatePKIClientProfile(existing)
if err != nil {
api.Logger.Write(logIDPKI, util.LOG_WARN, "client profile update failed id=%s err=%v", params["id"], err)
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
api.Logger.Write(logIDPKI, util.LOG_INFO, "client profile update success id=%s name=%s", existing.ID, existing.Name)
WriteJSON(w, http.StatusOK, buildPKIClientProfileSummary(existing))
}
func (api *API) DeletePKIClientProfile(w http.ResponseWriter, r *http.Request, params map[string]string) {
var err error
if !api.requireAdmin(w, r) {
return
}
err = api.store(r).DeletePKIClientProfile(params["id"])
if err != nil {
api.Logger.Write(logIDPKI, util.LOG_WARN, "client profile delete failed id=%s err=%v", params["id"], err)
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
api.Logger.Write(logIDPKI, util.LOG_INFO, "client profile delete success id=%s", params["id"])
WriteJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
}
func (api *API) ListPKIClientProfilesForSelf(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var user models.User
var ok bool
var items []models.PKIClientProfile
var out []pkiClientProfileSummary
var i int
var err error
user, ok = middleware.UserFromContext(r.Context())
if !ok || user.Disabled {
w.WriteHeader(http.StatusUnauthorized)
return
}
items, err = api.store(r).ListActivePKIClientProfilesForUser(user.ID)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
for i = 0; i < len(items); i++ {
out = append(out, buildPKIClientProfileSummary(items[i]))
}
WriteJSON(w, http.StatusOK, out)
}
func (api *API) ListPKIClientIssuancesForSelf(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var user models.User
var ok bool
var items []models.PKIClientIssuance
var out []pkiClientIssuanceSummary
var i int
var err error
user, ok = middleware.UserFromContext(r.Context())
if !ok || user.Disabled {
w.WriteHeader(http.StatusUnauthorized)
return
}
items, err = api.store(r).ListPKIClientIssuancesForUser(user.ID)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
for i = 0; i < len(items); i++ {
out = append(out, buildPKIClientIssuanceSummary(items[i]))
}
WriteJSON(w, http.StatusOK, out)
}
func (api *API) IssuePKIClientCertForSelf(w http.ResponseWriter, r *http.Request, _ map[string]string) {
var req pkiClientIssueRequest
var user models.User
var ok bool
var profile models.PKIClientProfile
var ca models.PKICA
var caPEMs []string
var serial int64
var serialHex string
var permissions []string
var validSeconds int64
var sanURI string
var commonName string
var sanDNS []string
var sanIPs []string
var certPEM string
var keyPEM string
var notBefore int64
var notAfter int64
var cert models.PKICert
var issuance models.PKIClientIssuance
var createdByKind string
var createdBySubjectID string
var createdBySubjectName string
var issuanceSource string
var err error
user, ok = middleware.UserFromContext(r.Context())
if !ok || user.Disabled {
w.WriteHeader(http.StatusUnauthorized)
return
}
err = DecodeJSON(r, &req)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request"})
return
}
profile, err = api.store(r).GetActivePKIClientProfileForUser(strings.TrimSpace(req.ProfileID), user.ID)
if err != nil {
if err == sql.ErrNoRows {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "profile not found or not allowed"})
return
}
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
ca, err = api.store(r).GetPKICA(profile.CAID)
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "issuer CA not found"})
return
}
permissions = splitPKIClientPermissionList(profile.AuthzPermissions)
validSeconds, err = normalizePKIClientRequestedValidSeconds(req.ValidSeconds, profile.DefaultValidSeconds, profile.MaxValidSeconds)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
commonName = strings.TrimSpace(req.CommonName)
sanDNS = trimPKIClientStringList(req.SANDNS)
sanIPs = trimPKIClientStringList(req.SANIPs)
if profile.AllowServerAuth {
if commonName == "" {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "common_name is required for profiles that allow server authentication"})
return
}
} else {
if commonName != "" || len(sanDNS) > 0 || len(sanIPs) > 0 {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "common_name, san_dns, and san_ips are only allowed when the profile allows server authentication"})
return
}
commonName = buildPKIClientCommonName(user)
}
sanURI = profile.SANURIPrefix + user.ID
_, err = url.ParseRequestURI(sanURI)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "generated SAN URI is invalid"})
return
}
serial, err = api.store(r).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 = issuePKIClientCertFromProfile(ca, serial, user, profile, permissions, commonName, sanDNS, sanIPs, sanURI, validSeconds)
if err != nil {
api.Logger.Write(logIDPKI, util.LOG_WARN, "client cert issue failed user=%s profile=%s err=%v", user.Username, profile.ID, err)
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
createdByKind, createdBySubjectID, createdBySubjectName, issuanceSource = pkiCertProvenanceFromRequest(r, "client_profile")
cert = models.PKICert{
CAID: ca.ID,
CreatedByKind: createdByKind,
CreatedBySubjectID: createdBySubjectID,
CreatedBySubjectName: createdBySubjectName,
IssuanceSource: issuanceSource,
SerialHex: serialHex,
CommonName: commonName,
SANDNS: strings.Join(sanDNS, ","),
SANIPs: strings.Join(sanIPs, ","),
IsCA: false,
CertPEM: certPEM,
KeyPEM: keyPEM,
NotBefore: notBefore,
NotAfter: notAfter,
Status: "active",
RevokedAt: 0,
RevocationReason: "",
}
cert, err = api.store(r).CreatePKICert(cert)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
return
}
issuance = models.PKIClientIssuance{
CertID: cert.ID,
UserID: user.ID,
Username: user.Username,
ProfileID: profile.ID,
ProfileName: profile.Name,
SerialHex: cert.SerialHex,
CommonName: cert.CommonName,
SANURI: sanURI,
AuthzPermissions: profile.AuthzPermissions,
AuthzScope: profile.AuthzScope,
NotBefore: notBefore,
NotAfter: notAfter,
Status: cert.Status,
RevokedAt: cert.RevokedAt,
RevocationReason: cert.RevocationReason,
}
issuance, err = api.store(r).CreatePKIClientIssuance(issuance)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
caPEMs, err = api.pkiCertChainPEMs(r.Context(), cert.CAID)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
api.Logger.Write(logIDPKI, util.LOG_INFO, "client cert issue success user=%s profile=%s cert=%s perms=%s scope=%s", user.Username, profile.ID, cert.ID, profile.AuthzPermissions, profile.AuthzScope)
WriteJSON(w, http.StatusCreated, pkiClientIssueResponse{
Issuance: buildPKIClientIssuanceSummary(issuance),
CertPEM: cert.CertPEM,
KeyPEM: cert.KeyPEM,
CACertPEM: joinPKIPEMs(caPEMs),
})
}
func (api *API) DownloadPKIClientCertBundleForSelf(w http.ResponseWriter, r *http.Request, params map[string]string) {
var user models.User
var ok bool
var owns bool
var cert models.PKICert
var caPEMs []string
var data []byte
var err error
user, ok = middleware.UserFromContext(r.Context())
if !ok || user.Disabled {
w.WriteHeader(http.StatusUnauthorized)
return
}
owns, err = api.store(r).UserOwnsPKIClientCert(user.ID, params["id"])
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if !owns {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "certificate not found"})
return
}
cert, err = api.store(r).GetPKICert(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "certificate not found"})
return
}
caPEMs, err = api.pkiCertChainPEMs(r.Context(), cert.CAID)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
data, err = buildPKIZipBundle(cert.CommonName, cert.CertPEM, cert.KeyPEM, false, caPEMs)
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)+".zip\"")
_, _ = w.Write(data)
}
func (api *API) GetPKIClientCertForSelf(w http.ResponseWriter, r *http.Request, params map[string]string) {
var user models.User
var ok bool
var owns bool
var cert models.PKICert
var caPEMs []string
var err error
user, ok = middleware.UserFromContext(r.Context())
if !ok || user.Disabled {
w.WriteHeader(http.StatusUnauthorized)
return
}
owns, err = api.store(r).UserOwnsPKIClientCert(user.ID, params["id"])
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if !owns {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "certificate not found"})
return
}
cert, err = api.store(r).GetPKICert(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "certificate not found"})
return
}
caPEMs, err = api.pkiCertChainPEMs(r.Context(), cert.CAID)
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
cert.CACertPEM = joinPKIPEMs(caPEMs)
WriteJSON(w, http.StatusOK, cert)
}
func (api *API) GetPKIClientCertInspectForSelf(w http.ResponseWriter, r *http.Request, params map[string]string) {
var user models.User
var ok bool
var owns bool
var cert models.PKICert
var parsed *x509.Certificate
var dump string
var err error
user, ok = middleware.UserFromContext(r.Context())
if !ok || user.Disabled {
w.WriteHeader(http.StatusUnauthorized)
return
}
owns, err = api.store(r).UserOwnsPKIClientCert(user.ID, params["id"])
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if !owns {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "certificate not found"})
return
}
cert, err = api.store(r).GetPKICert(params["id"])
if err != nil {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "certificate not found"})
return
}
parsed, err = parseCertificateFromPEM(cert.CertPEM)
if err != nil {
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid certificate"})
return
}
dump = buildX509CertificateDump(parsed)
WriteJSON(w, http.StatusOK, map[string]string{"dump": dump})
}
func (api *API) RevokePKIClientCertForSelf(w http.ResponseWriter, r *http.Request, params map[string]string) {
var user models.User
var ok bool
var owns bool
var cert models.PKICert
var err error
user, ok = middleware.UserFromContext(r.Context())
if !ok || user.Disabled {
w.WriteHeader(http.StatusUnauthorized)
return
}
owns, err = api.store(r).UserOwnsPKIClientCert(user.ID, params["id"])
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
if !owns {
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "certificate not found"})
return
}
cert, err = api.store(r).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 = api.store(r).RevokePKICert(cert.ID, "revoked by owner")
if err != nil {
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
return
}
api.Logger.Write(logIDPKI, util.LOG_INFO, "client cert revoke success user=%s cert=%s", user.Username, cert.ID)
WriteJSON(w, http.StatusOK, map[string]string{"status": "revoked"})
}
func buildPKIClientProfileSummary(item models.PKIClientProfile) pkiClientProfileSummary {
var targets []pkiClientProfileTargetSummary
var i int
for i = 0; i < len(item.Targets); i++ {
targets = append(targets, pkiClientProfileTargetSummary{
ProfileID: item.Targets[i].ProfileID,
TargetType: item.Targets[i].TargetType,
TargetID: item.Targets[i].TargetID,
TargetName: item.Targets[i].TargetName,
TargetActive: item.Targets[i].TargetActive,
CreatedAt: item.Targets[i].CreatedAt,
})
}
return pkiClientProfileSummary{
ID: item.ID,
Name: item.Name,
CAID: item.CAID,
CAName: item.CAName,
SubjectOrganization: item.SubjectOrganization,
SANURIPrefix: item.SANURIPrefix,
AllowServerAuth: item.AllowServerAuth,
AuthzPermissions: splitPKIClientPermissionList(item.AuthzPermissions),
AuthzScope: item.AuthzScope,
DefaultValidSeconds: item.DefaultValidSeconds,
MaxValidSeconds: item.MaxValidSeconds,
Enabled: item.Enabled,
CreatedByUserID: item.CreatedByUserID,
CreatedByUsername: item.CreatedByUsername,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
Targets: targets,
}
}
func buildPKIClientIssuanceSummary(item models.PKIClientIssuance) pkiClientIssuanceSummary {
return pkiClientIssuanceSummary{
ID: item.ID,
CertID: item.CertID,
UserID: item.UserID,
Username: item.Username,
ProfileID: item.ProfileID,
ProfileName: item.ProfileName,
SerialHex: item.SerialHex,
CommonName: item.CommonName,
SANURI: item.SANURI,
AuthzPermissions: splitPKIClientPermissionList(item.AuthzPermissions),
AuthzScope: item.AuthzScope,
NotBefore: item.NotBefore,
NotAfter: item.NotAfter,
Status: item.Status,
RevokedAt: item.RevokedAt,
RevocationReason: item.RevocationReason,
CreatedAt: item.CreatedAt,
}
}
func normalizePKIClientProfilePermissions(raw []string) ([]string, string, error) {
var values []string
var seen map[string]bool
var value string
var i int
seen = map[string]bool{}
for i = 0; i < len(raw); i++ {
value = strings.ToLower(strings.TrimSpace(raw[i]))
if value == "" {
continue
}
if strings.ContainsAny(value, " \t\r\n,") {
return nil, "", errors.New("permissions must not contain spaces or commas")
}
if seen[value] {
continue
}
seen[value] = true
values = append(values, value)
}
if len(values) == 0 {
return nil, "", errors.New("at least one permission is required")
}
sort.Strings(values)
return values, strings.Join(values, ","), nil
}
func normalizePKIClientProfileTargets(raw []pkiClientProfileTargetRequest) ([]models.PKIClientProfileTarget, error) {
var out []models.PKIClientProfileTarget
var dedupe map[string]bool
var targetType string
var targetID string
var key string
var i int
dedupe = map[string]bool{}
for i = 0; i < len(raw); i++ {
targetType = strings.ToLower(strings.TrimSpace(raw[i].TargetType))
targetID = strings.TrimSpace(raw[i].TargetID)
if targetType != "user" && targetType != "group" {
return nil, errors.New("target_type must be user or group")
}
if targetID == "" {
return nil, errors.New("target_id is required")
}
key = targetType + ":" + targetID
if dedupe[key] {
continue
}
dedupe[key] = true
out = append(out, models.PKIClientProfileTarget{TargetType: targetType, TargetID: targetID})
}
if len(out) == 0 {
return nil, errors.New("at least one target is required")
}
return out, nil
}
func normalizePKIClientSubjectOrganization(raw string) string {
var value string
value = strings.TrimSpace(raw)
if value == "" {
return "Codit"
}
return value
}
func normalizePKIClientSANURIPrefix(raw string) (string, error) {
var value string
var probe string
var err error
value = strings.TrimSpace(raw)
if value == "" {
value = "urn:codit:user:"
}
probe = value + "probe"
_, err = url.ParseRequestURI(probe)
if err != nil {
return "", errors.New("san_uri_prefix must produce a valid URI")
}
return value, nil
}
func validatePKIClientProfileWindow(defaultValidSeconds int64, maxValidSeconds int64) error {
if defaultValidSeconds < pkiClientMinValidSeconds || defaultValidSeconds > pkiClientMaxValidSeconds {
return errors.New("default_valid_seconds must be between 60 and 604800")
}
if maxValidSeconds < pkiClientMinValidSeconds || maxValidSeconds > pkiClientMaxValidSeconds {
return errors.New("max_valid_seconds must be between 60 and 604800")
}
if defaultValidSeconds > maxValidSeconds {
return errors.New("default_valid_seconds must be less than or equal to max_valid_seconds")
}
return nil
}
func normalizePKIClientRequestedValidSeconds(requested int64, defaultValidSeconds int64, maxValidSeconds int64) (int64, error) {
var value int64
value = requested
if value <= 0 {
value = defaultValidSeconds
}
if value < pkiClientMinValidSeconds {
return 0, errors.New("valid_seconds must be at least 60")
}
if value > maxValidSeconds {
return 0, errors.New("valid_seconds exceeds profile maximum")
}
return value, nil
}
func splitPKIClientPermissionList(value string) []string {
var out []string
var part string
var parts []string
var i int
parts = strings.Split(strings.TrimSpace(value), ",")
for i = 0; i < len(parts); i++ {
part = strings.TrimSpace(parts[i])
if part == "" {
continue
}
out = append(out, part)
}
return out
}
func trimPKIClientStringList(value []string) []string {
var out []string
var item string
var i int
for i = 0; i < len(value); i++ {
item = strings.TrimSpace(value[i])
if item == "" {
continue
}
out = append(out, item)
}
return out
}
func buildPKIClientCommonName(user models.User) string {
if strings.TrimSpace(user.Username) != "" {
return strings.TrimSpace(user.Username)
}
return strings.TrimSpace(user.DisplayName)
}
func buildPKIClientAuthorizationExtension(user models.User, profile models.PKIClientProfile, permissions []string) (pkix.Extension, error) {
var value coditMTLSAuthorization
var raw []byte
var err error
value = coditMTLSAuthorization{
Version: 1,
UserID: user.ID,
Username: user.Username,
ProfileID: profile.ID,
Permissions: permissions,
Scope: strings.TrimSpace(profile.AuthzScope),
}
raw, err = asn1.Marshal(value)
if err != nil {
return pkix.Extension{}, err
}
return pkix.Extension{Id: pkiutil.OIDCoditMTLSAuthorization, Critical: false, Value: raw}, nil
}
func issuePKIClientCertFromProfile(ca models.PKICA, serial int64, user models.User, profile models.PKIClientProfile, permissions []string, commonName string, sanDNS []string, sanIPs []string, sanURI string, validSeconds int64) (string, string, int64, int64, error) {
var caCert *x509.Certificate
var caKey *rsa.PrivateKey
var serialNum *big.Int
var key *rsa.PrivateKey
var tmpl x509.Certificate
var extension pkix.Extension
var uri *url.URL
var ip net.IP
var i int
var der []byte
var certPEM string
var keyPEM string
var now time.Time
var end time.Time
var err error
caCert, 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
}
serialNum = big.NewInt(serial)
now = time.Now().UTC()
end = now.Add(time.Duration(validSeconds) * time.Second)
if strings.TrimSpace(commonName) == "" {
return "", "", 0, 0, errors.New("common_name is required")
}
uri, err = url.Parse(sanURI)
if err != nil {
return "", "", 0, 0, err
}
extension, err = buildPKIClientAuthorizationExtension(user, profile, permissions)
if err != nil {
return "", "", 0, 0, err
}
tmpl = x509.Certificate{
SerialNumber: serialNum,
Subject: pkix.Name{CommonName: strings.TrimSpace(commonName), Organization: []string{profile.SubjectOrganization}},
NotBefore: now,
NotAfter: end,
KeyUsage: x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
BasicConstraintsValid: true,
IsCA: false,
URIs: []*url.URL{uri},
ExtraExtensions: []pkix.Extension{extension},
}
if profile.AllowServerAuth {
tmpl.KeyUsage = x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment
tmpl.ExtKeyUsage = []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth, x509.ExtKeyUsageServerAuth}
}
for i = 0; i < len(sanDNS); i++ {
tmpl.DNSNames = append(tmpl.DNSNames, sanDNS[i])
}
for i = 0; i < len(sanIPs); i++ {
ip = net.ParseIP(sanIPs[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
}