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 }