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