1125 lines
36 KiB
Go
1125 lines
36 KiB
Go
package handlers
|
|
|
|
import "crypto/ed25519"
|
|
import "crypto/ecdsa"
|
|
import "crypto/elliptic"
|
|
import "crypto/rand"
|
|
import "crypto/rsa"
|
|
import "crypto/x509"
|
|
import "database/sql"
|
|
import "encoding/pem"
|
|
import "errors"
|
|
import "fmt"
|
|
import "net"
|
|
import "net/http"
|
|
import "sort"
|
|
import "strconv"
|
|
import "strings"
|
|
import "time"
|
|
|
|
import "codit/internal/db"
|
|
import "golang.org/x/crypto/ssh"
|
|
|
|
import "codit/internal/middleware"
|
|
import "codit/internal/models"
|
|
import "codit/internal/util"
|
|
|
|
type sshUserCACreateRequest struct {
|
|
Name string `json:"name"`
|
|
Algorithm string `json:"algorithm"`
|
|
PrivateKeyPEM string `json:"private_key_pem"`
|
|
Enabled bool `json:"enabled"`
|
|
AllowUserSign bool `json:"allow_user_sign"`
|
|
MaxUserValidSeconds int64 `json:"max_user_valid_seconds"`
|
|
}
|
|
|
|
type sshUserCAUpdateRequest struct {
|
|
Name string `json:"name"`
|
|
Enabled bool `json:"enabled"`
|
|
AllowUserSign bool `json:"allow_user_sign"`
|
|
MaxUserValidSeconds int64 `json:"max_user_valid_seconds"`
|
|
}
|
|
|
|
type sshUserCASummary struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
Algorithm string `json:"algorithm"`
|
|
PublicKey string `json:"public_key"`
|
|
Fingerprint string `json:"fingerprint"`
|
|
SerialCounter uint64 `json:"serial_counter"`
|
|
Enabled bool `json:"enabled"`
|
|
AllowUserSign bool `json:"allow_user_sign"`
|
|
MaxUserValidSeconds int64 `json:"max_user_valid_seconds"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
UpdatedAt int64 `json:"updated_at"`
|
|
}
|
|
|
|
type sshSignUserKeyRequest struct {
|
|
PublicKey string `json:"public_key"`
|
|
KeyID string `json:"key_id"`
|
|
Principals []string `json:"principals"`
|
|
ValidSeconds int64 `json:"valid_seconds"`
|
|
}
|
|
|
|
type sshSignSelfUserKeyRequest struct {
|
|
PublicKey string `json:"public_key"`
|
|
KeyID string `json:"key_id"`
|
|
GrantIDs []string `json:"grant_ids"`
|
|
Principals []string `json:"principals"`
|
|
ValidSeconds int64 `json:"valid_seconds"`
|
|
}
|
|
|
|
type sshInspectCertificateRequest struct {
|
|
Certificate string `json:"certificate"`
|
|
}
|
|
|
|
type sshSignUserKeyResponse struct {
|
|
CAID string `json:"ca_id"`
|
|
Certificate string `json:"certificate"`
|
|
KeyID string `json:"key_id"`
|
|
Principals []string `json:"principals"`
|
|
ValidAfter int64 `json:"valid_after"`
|
|
ValidBefore int64 `json:"valid_before"`
|
|
Serial uint64 `json:"serial"`
|
|
}
|
|
|
|
type sshUserCAIssuanceSummary struct {
|
|
ID string `json:"id"`
|
|
CAID string `json:"ca_id"`
|
|
CAName string `json:"ca_name"`
|
|
IssuerUserID string `json:"issuer_user_id"`
|
|
IssuerUsername string `json:"issuer_username"`
|
|
IssuerKind string `json:"issuer_kind"`
|
|
SourcePublicKey string `json:"source_public_key"`
|
|
SourcePublicKeyFingerprint string `json:"source_public_key_fingerprint"`
|
|
Certificate string `json:"certificate"`
|
|
KeyID string `json:"key_id"`
|
|
Principals []string `json:"principals"`
|
|
ValidAfter int64 `json:"valid_after"`
|
|
ValidBefore int64 `json:"valid_before"`
|
|
Serial uint64 `json:"serial"`
|
|
RemoteAddr string `json:"remote_addr"`
|
|
UserAgent string `json:"user_agent"`
|
|
CreatedAt int64 `json:"created_at"`
|
|
}
|
|
|
|
const logIDSSHCA string = "ssh-ca"
|
|
|
|
func (api *API) ListSSHUserCAs(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var items []models.SSHUserCA
|
|
var out []sshUserCASummary
|
|
var i int
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
items, err = api.store(r).ListSSHUserCAs()
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
for i = 0; i < len(items); i++ {
|
|
out = append(out, buildSSHUserCASummary(items[i]))
|
|
}
|
|
WriteJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
func (api *API) GetSSHUserCA(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var item models.SSHUserCA
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
item, err = api.store(r).GetSSHUserCA(params["id"])
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "SSH user CA not found"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, buildSSHUserCASummary(item))
|
|
}
|
|
|
|
func (api *API) CreateSSHUserCA(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var req sshUserCACreateRequest
|
|
var item models.SSHUserCA
|
|
var summary sshUserCASummary
|
|
var signer ssh.Signer
|
|
var privateKeyPEM string
|
|
var publicKey string
|
|
var fingerprint string
|
|
var algorithm string
|
|
var maxUserValidSeconds int64
|
|
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
|
|
}
|
|
req.Name = strings.TrimSpace(req.Name)
|
|
if req.Name == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
|
|
return
|
|
}
|
|
algorithm, err = normalizeSSHCAAlgorithm(req.Algorithm)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
maxUserValidSeconds, err = normalizeSSHUserCAMaxValidSeconds(req.MaxUserValidSeconds)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if strings.TrimSpace(req.PrivateKeyPEM) == "" {
|
|
privateKeyPEM, publicKey, fingerprint, err = generateSSHUserCAKeyPair(algorithm)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
} else {
|
|
signer, err = parseSSHSignerFromPEM(strings.TrimSpace(req.PrivateKeyPEM))
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
privateKeyPEM = strings.TrimSpace(req.PrivateKeyPEM)
|
|
publicKey = strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signer.PublicKey())))
|
|
fingerprint = strings.TrimSpace(ssh.FingerprintSHA256(signer.PublicKey()))
|
|
}
|
|
item = models.SSHUserCA{
|
|
Name: req.Name,
|
|
Algorithm: publicKeyAlgorithm(publicKey),
|
|
PublicKey: publicKey,
|
|
PrivateKeyPEM: privateKeyPEM,
|
|
Fingerprint: fingerprint,
|
|
SerialCounter: 1,
|
|
Enabled: req.Enabled,
|
|
AllowUserSign: req.AllowUserSign,
|
|
MaxUserValidSeconds: maxUserValidSeconds,
|
|
}
|
|
item, err = api.store(r).CreateSSHUserCA(item)
|
|
if err != nil {
|
|
api.Logger.Write(logIDSSHCA, util.LOG_WARN, "create failed name=%s err=%v", req.Name, err)
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
summary = buildSSHUserCASummary(item)
|
|
api.Logger.Write(logIDSSHCA, util.LOG_INFO, "create success id=%s name=%s algorithm=%s", summary.ID, summary.Name, summary.Algorithm)
|
|
WriteJSON(w, http.StatusCreated, summary)
|
|
}
|
|
|
|
func (api *API) UpdateSSHUserCA(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req sshUserCAUpdateRequest
|
|
var item models.SSHUserCA
|
|
var maxUserValidSeconds int64
|
|
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
|
|
}
|
|
req.Name = strings.TrimSpace(req.Name)
|
|
if req.Name == "" {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "name is required"})
|
|
return
|
|
}
|
|
maxUserValidSeconds, err = normalizeSSHUserCAMaxValidSeconds(req.MaxUserValidSeconds)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
item, err = api.store(r).GetSSHUserCA(params["id"])
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "SSH user CA not found"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
item.Name = req.Name
|
|
item.Enabled = req.Enabled
|
|
item.AllowUserSign = req.AllowUserSign
|
|
item.MaxUserValidSeconds = maxUserValidSeconds
|
|
item, err = api.store(r).UpdateSSHUserCA(item)
|
|
if err != nil {
|
|
api.Logger.Write(logIDSSHCA, util.LOG_WARN, "update failed id=%s err=%v", params["id"], err)
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
api.Logger.Write(logIDSSHCA, util.LOG_INFO, "update success id=%s name=%s enabled=%t", item.ID, item.Name, item.Enabled)
|
|
WriteJSON(w, http.StatusOK, buildSSHUserCASummary(item))
|
|
}
|
|
|
|
func (api *API) DeleteSSHUserCA(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
err = api.store(r).DeleteSSHUserCA(params["id"])
|
|
if err != nil {
|
|
api.Logger.Write(logIDSSHCA, util.LOG_WARN, "delete failed id=%s err=%v", params["id"], err)
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
api.Logger.Write(logIDSSHCA, util.LOG_INFO, "delete success id=%s", params["id"])
|
|
WriteJSON(w, http.StatusOK, map[string]string{"status": "deleted"})
|
|
}
|
|
|
|
func (api *API) DownloadSSHUserCAPublicKey(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var item models.SSHUserCA
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
item, err = api.store(r).GetSSHUserCA(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "SSH user CA not found"})
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.pub\"", sanitizeFilename(item.Name)))
|
|
_, _ = w.Write([]byte(strings.TrimSpace(item.PublicKey) + "\n"))
|
|
}
|
|
|
|
func (api *API) DownloadSSHUserCAPublicKeyForSelf(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var item models.SSHUserCA
|
|
var user models.User
|
|
var ok bool
|
|
var err error
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok || user.Disabled {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
item, err = api.store(r).GetSSHUserCAForUser(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "SSH user CA not found"})
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.pub\"", sanitizeFilename(item.Name)))
|
|
_, _ = w.Write([]byte(strings.TrimSpace(item.PublicKey) + "\n"))
|
|
}
|
|
|
|
func (api *API) DownloadSSHUserCAPrivateKey(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var item models.SSHUserCA
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
item, err = api.store(r).GetSSHUserCA(params["id"])
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "SSH user CA not found"})
|
|
return
|
|
}
|
|
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
|
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=\"%s.key\"", sanitizeFilename(item.Name)))
|
|
_, _ = w.Write([]byte(strings.TrimSpace(item.PrivateKeyPEM) + "\n"))
|
|
}
|
|
|
|
func (api *API) SignSSHUserKey(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req sshSignUserKeyRequest
|
|
var user models.User
|
|
var ok bool
|
|
var principals []string
|
|
var validSeconds int64
|
|
var sourcePublicKey string
|
|
var sourcePublicKeyFingerprint string
|
|
var response sshSignUserKeyResponse
|
|
var item models.SSHUserCA
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
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
|
|
}
|
|
item, err = api.store(r).GetSSHUserCA(params["id"])
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "SSH user CA not found"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if !item.Enabled {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "SSH user CA is disabled"})
|
|
return
|
|
}
|
|
principals = normalizeSSHPrincipals(req.Principals)
|
|
if len(principals) == 0 {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "at least one principal is required"})
|
|
return
|
|
}
|
|
sourcePublicKey = strings.TrimSpace(req.PublicKey)
|
|
sourcePublicKeyFingerprint, err = sshAuthorizedKeyFingerprint(sourcePublicKey)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
validSeconds = req.ValidSeconds
|
|
response, err = api.signSSHUserKeyWithCA(api.store(r), item, sourcePublicKey, strings.TrimSpace(req.KeyID), principals, validSeconds, 604800)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
err = api.recordSSHUserCAIssuance(r, item, user, "admin", sourcePublicKey, sourcePublicKeyFingerprint, response)
|
|
if err != nil {
|
|
api.Logger.Write(logIDSSHCA, util.LOG_WARN, "audit write failed ca_id=%s serial=%d err=%v", item.ID, response.Serial, err)
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to record issuance audit"})
|
|
return
|
|
}
|
|
api.Logger.Write(logIDSSHCA, util.LOG_INFO, "sign success ca_id=%s serial=%d key_id=%s principals=%s", item.ID, response.Serial, response.KeyID, strings.Join(principals, ","))
|
|
WriteJSON(w, http.StatusOK, response)
|
|
}
|
|
|
|
func (api *API) ListSSHUserCAsForSelf(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var items []models.SSHUserCA
|
|
var out []sshUserCASummary
|
|
var user models.User
|
|
var ok bool
|
|
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).ListSSHUserCAsForUser()
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
for i = 0; i < len(items); i++ {
|
|
out = append(out, buildSSHUserCASummary(items[i]))
|
|
}
|
|
WriteJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
func (api *API) GetSSHUserCAForSelf(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var item models.SSHUserCA
|
|
var user models.User
|
|
var ok bool
|
|
var err error
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok || user.Disabled {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
item, err = api.store(r).GetSSHUserCAForUser(params["id"])
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "SSH user CA not found"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusOK, buildSSHUserCASummary(item))
|
|
}
|
|
|
|
func (api *API) ListSSHUserCAIssuances(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var ca models.SSHUserCA
|
|
var limit int
|
|
var items []models.SSHUserCAIssuance
|
|
var out []sshUserCAIssuanceSummary
|
|
var i int
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
ca, err = api.store(r).GetSSHUserCA(params["id"])
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "SSH user CA not found"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
limit = parseSSHUserCAIssuanceLimit(r.URL.Query().Get("limit"))
|
|
items, err = api.store(r).ListSSHUserCAIssuancesByCA(ca.ID, limit)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
for i = 0; i < len(items); i++ {
|
|
out = append(out, buildSSHUserCAIssuanceSummary(items[i], ca.Name))
|
|
}
|
|
WriteJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
func (api *API) ListSSHUserCAIssuancesAll(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var limit int
|
|
var caID string
|
|
var items []models.SSHUserCAIssuance
|
|
var cas []models.SSHUserCA
|
|
var caByID map[string]string
|
|
var out []sshUserCAIssuanceSummary
|
|
var caName string
|
|
var ok bool
|
|
var i int
|
|
var err error
|
|
if !api.requireAdmin(w, r) {
|
|
return
|
|
}
|
|
limit = parseSSHUserCAIssuanceLimit(r.URL.Query().Get("limit"))
|
|
caID = strings.TrimSpace(r.URL.Query().Get("ca_id"))
|
|
items, err = api.store(r).ListSSHUserCAIssuances(limit, caID)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
cas, err = api.store(r).ListSSHUserCAs()
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
caByID = map[string]string{}
|
|
for i = 0; i < len(cas); i++ {
|
|
caByID[cas[i].ID] = cas[i].Name
|
|
}
|
|
for i = 0; i < len(items); i++ {
|
|
caName, ok = caByID[items[i].CAID]
|
|
if !ok {
|
|
caName = ""
|
|
}
|
|
out = append(out, buildSSHUserCAIssuanceSummary(items[i], caName))
|
|
}
|
|
WriteJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
func (api *API) ListSSHUserCAIssuancesForSelf(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var user models.User
|
|
var ok bool
|
|
var limit int
|
|
var items []models.SSHUserCAIssuance
|
|
var out []sshUserCAIssuanceSummary
|
|
var caByID map[string]string
|
|
var ca models.SSHUserCA
|
|
var caName string
|
|
var i int
|
|
var err error
|
|
user, ok = middleware.UserFromContext(r.Context())
|
|
if !ok || user.Disabled {
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
return
|
|
}
|
|
limit = parseSSHUserCAIssuanceLimit(r.URL.Query().Get("limit"))
|
|
items, err = api.store(r).ListSSHUserCAIssuancesForSelf(user.ID, limit)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
caByID = map[string]string{}
|
|
for i = 0; i < len(items); i++ {
|
|
caName, ok = caByID[items[i].CAID]
|
|
if !ok {
|
|
ca, err = api.store(r).GetSSHUserCA(items[i].CAID)
|
|
if err == nil {
|
|
caName = ca.Name
|
|
} else {
|
|
caName = ""
|
|
}
|
|
caByID[items[i].CAID] = caName
|
|
}
|
|
out = append(out, buildSSHUserCAIssuanceSummary(items[i], caName))
|
|
}
|
|
WriteJSON(w, http.StatusOK, out)
|
|
}
|
|
|
|
func (api *API) InspectSSHCertificate(w http.ResponseWriter, r *http.Request, _ map[string]string) {
|
|
var req sshInspectCertificateRequest
|
|
var user models.User
|
|
var ok bool
|
|
var key ssh.PublicKey
|
|
var cert *ssh.Certificate
|
|
var dump 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
|
|
}
|
|
key, _, _, _, err = ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(req.Certificate)))
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid SSH certificate"})
|
|
return
|
|
}
|
|
cert, ok = key.(*ssh.Certificate)
|
|
if !ok {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "not an SSH certificate"})
|
|
return
|
|
}
|
|
dump = buildSSHCertificateDump(cert)
|
|
WriteJSON(w, http.StatusOK, map[string]string{"dump": dump})
|
|
}
|
|
|
|
func (api *API) SignSSHUserKeyForSelf(w http.ResponseWriter, r *http.Request, params map[string]string) {
|
|
var req sshSignSelfUserKeyRequest
|
|
var item models.SSHUserCA
|
|
var user models.User
|
|
var ok bool
|
|
var grants []models.SSHPrincipalGrant
|
|
var grantsByID map[string]models.SSHPrincipalGrant
|
|
var grantsByPrincipal map[string]string
|
|
var selectedGrantIDs map[string]bool
|
|
var selectedIDs []string
|
|
var grantIDs []string
|
|
var requestedPrincipals []string
|
|
var principals []string
|
|
var principalsSeen map[string]bool
|
|
var grant models.SSHPrincipalGrant
|
|
var grantID string
|
|
var principal string
|
|
var maxGrantValidSeconds int64
|
|
var now int64
|
|
var i int
|
|
var j int
|
|
var validSeconds int64
|
|
var sourcePublicKey string
|
|
var sourcePublicKeyFingerprint string
|
|
var response sshSignUserKeyResponse
|
|
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
|
|
}
|
|
item, err = api.store(r).GetSSHUserCA(params["id"])
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
WriteJSON(w, http.StatusNotFound, map[string]string{"error": "SSH user CA not found"})
|
|
return
|
|
}
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
if !item.Enabled {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "SSH user CA is disabled"})
|
|
return
|
|
}
|
|
if !item.AllowUserSign {
|
|
WriteJSON(w, http.StatusForbidden, map[string]string{"error": "self-sign is not allowed for this CA"})
|
|
return
|
|
}
|
|
now = time.Now().UTC().Unix()
|
|
grants, err = api.store(r).ListActiveSSHPrincipalGrantsForUser(user.ID, now)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
sourcePublicKey = strings.TrimSpace(req.PublicKey)
|
|
sourcePublicKeyFingerprint, err = sshAuthorizedKeyFingerprint(sourcePublicKey)
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
requestedPrincipals = normalizeSSHPrincipals(req.Principals)
|
|
if len(grants) == 0 {
|
|
principals = []string{user.Username}
|
|
} else {
|
|
grantsByID = map[string]models.SSHPrincipalGrant{}
|
|
grantsByPrincipal = map[string]string{}
|
|
selectedGrantIDs = map[string]bool{}
|
|
principalsSeen = map[string]bool{}
|
|
for i = 0; i < len(grants); i++ {
|
|
grantsByID[grants[i].ID] = grants[i]
|
|
for j = 0; j < len(grants[i].Principals); j++ {
|
|
principal = grants[i].Principals[j]
|
|
if _, ok = grantsByPrincipal[principal]; !ok {
|
|
grantsByPrincipal[principal] = grants[i].ID
|
|
}
|
|
}
|
|
}
|
|
grantIDs = normalizeSSHGrantIDs(req.GrantIDs)
|
|
if len(grantIDs) == 0 {
|
|
requestedPrincipals = normalizeSSHPrincipals(req.Principals)
|
|
for i = 0; i < len(requestedPrincipals); i++ {
|
|
grantID, ok = grantsByPrincipal[requestedPrincipals[i]]
|
|
if !ok {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "selected principal is not granted"})
|
|
return
|
|
}
|
|
selectedGrantIDs[grantID] = true
|
|
}
|
|
} else {
|
|
for i = 0; i < len(grantIDs); i++ {
|
|
selectedGrantIDs[grantIDs[i]] = true
|
|
}
|
|
}
|
|
if len(selectedGrantIDs) == 0 {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "at least one granted principal is required"})
|
|
return
|
|
}
|
|
for grantID = range selectedGrantIDs {
|
|
selectedIDs = append(selectedIDs, grantID)
|
|
}
|
|
sort.Strings(selectedIDs)
|
|
for i = 0; i < len(selectedIDs); i++ {
|
|
grant, ok = grantsByID[selectedIDs[i]]
|
|
if !ok {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "selected grant is not active"})
|
|
return
|
|
}
|
|
for j = 0; j < len(grant.Principals); j++ {
|
|
principal = grant.Principals[j]
|
|
if !principalsSeen[principal] {
|
|
principals = append(principals, principal)
|
|
principalsSeen[principal] = true
|
|
}
|
|
}
|
|
selectedGrantIDs[grant.ID] = true
|
|
if grant.MaxCertValidSeconds > 0 && (maxGrantValidSeconds == 0 || grant.MaxCertValidSeconds < maxGrantValidSeconds) {
|
|
maxGrantValidSeconds = grant.MaxCertValidSeconds
|
|
}
|
|
}
|
|
}
|
|
validSeconds = req.ValidSeconds
|
|
if maxGrantValidSeconds > 0 && maxGrantValidSeconds < item.MaxUserValidSeconds {
|
|
response, err = api.signSSHUserKeyWithCA(api.store(r), item, sourcePublicKey, strings.TrimSpace(req.KeyID), principals, validSeconds, maxGrantValidSeconds)
|
|
} else {
|
|
response, err = api.signSSHUserKeyWithCA(api.store(r), item, sourcePublicKey, strings.TrimSpace(req.KeyID), principals, validSeconds, item.MaxUserValidSeconds)
|
|
}
|
|
if err != nil {
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
|
return
|
|
}
|
|
for grantID = range selectedGrantIDs {
|
|
err = api.store(r).MarkSSHPrincipalGrantUsed(grantID)
|
|
if err != nil {
|
|
api.Logger.Write(logIDSSHCA, util.LOG_WARN, "self-sign grant consume failed ca_id=%s user=%s grant_id=%s err=%v", item.ID, user.Username, grantID, err)
|
|
WriteJSON(w, http.StatusBadRequest, map[string]string{"error": "selected principal grant is not active"})
|
|
return
|
|
}
|
|
}
|
|
err = api.recordSSHUserCAIssuance(r, item, user, "self", sourcePublicKey, sourcePublicKeyFingerprint, response)
|
|
if err != nil {
|
|
api.Logger.Write(logIDSSHCA, util.LOG_WARN, "self-sign audit write failed ca_id=%s user=%s serial=%d err=%v", item.ID, user.Username, response.Serial, err)
|
|
WriteJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to record issuance audit"})
|
|
return
|
|
}
|
|
api.Logger.Write(logIDSSHCA, util.LOG_INFO, "self-sign success ca_id=%s user=%s serial=%d key_id=%s", item.ID, user.Username, response.Serial, response.KeyID)
|
|
WriteJSON(w, http.StatusOK, response)
|
|
}
|
|
|
|
func (api *API) signSSHUserKeyWithCA(store *db.Store, item models.SSHUserCA, publicKey string, keyID string, principals []string, validSeconds int64, maxValidSeconds int64) (sshSignUserKeyResponse, error) {
|
|
var response sshSignUserKeyResponse
|
|
var signer ssh.Signer
|
|
var key ssh.PublicKey
|
|
var serial uint64
|
|
var cert ssh.Certificate
|
|
var now int64
|
|
var err error
|
|
if strings.TrimSpace(publicKey) == "" {
|
|
return response, errors.New("public_key is required")
|
|
}
|
|
signer, err = parseSSHSignerFromPEM(item.PrivateKeyPEM)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
key, _, _, _, err = ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(publicKey)))
|
|
if err != nil {
|
|
return response, errors.New("invalid user public key")
|
|
}
|
|
if maxValidSeconds <= 0 {
|
|
maxValidSeconds = 1800
|
|
}
|
|
if validSeconds <= 0 {
|
|
validSeconds = 1800
|
|
if validSeconds > maxValidSeconds {
|
|
validSeconds = maxValidSeconds
|
|
}
|
|
}
|
|
if validSeconds < 60 || validSeconds > maxValidSeconds {
|
|
return response, fmt.Errorf("valid_seconds must be between 60 and %d", maxValidSeconds)
|
|
}
|
|
serial, err = store.NextSSHUserCASerial(item.ID)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
if keyID == "" {
|
|
keyID = fmt.Sprintf("codit-%s-%d", item.Name, serial)
|
|
}
|
|
now = time.Now().UTC().Unix()
|
|
cert = ssh.Certificate{
|
|
Key: key,
|
|
Serial: serial,
|
|
CertType: ssh.UserCert,
|
|
KeyId: keyID,
|
|
ValidPrincipals: principals,
|
|
ValidAfter: uint64(now - 30),
|
|
ValidBefore: uint64(now + validSeconds),
|
|
Permissions: ssh.Permissions{Extensions: map[string]string{
|
|
"permit-agent-forwarding": "",
|
|
"permit-port-forwarding": "",
|
|
"permit-pty": "",
|
|
"permit-user-rc": "",
|
|
"permit-X11-forwarding": "",
|
|
}},
|
|
}
|
|
err = cert.SignCert(rand.Reader, signer)
|
|
if err != nil {
|
|
return response, err
|
|
}
|
|
response = sshSignUserKeyResponse{
|
|
CAID: item.ID,
|
|
Certificate: strings.TrimSpace(string(ssh.MarshalAuthorizedKey(&cert))),
|
|
KeyID: keyID,
|
|
Principals: principals,
|
|
ValidAfter: now - 30,
|
|
ValidBefore: now + validSeconds,
|
|
Serial: serial,
|
|
}
|
|
return response, nil
|
|
}
|
|
|
|
func (api *API) recordSSHUserCAIssuance(r *http.Request, ca models.SSHUserCA, issuer models.User, issuerKind string, sourcePublicKey string, sourcePublicKeyFingerprint string, signed sshSignUserKeyResponse) error {
|
|
var row models.SSHUserCAIssuance
|
|
var err error
|
|
row = models.SSHUserCAIssuance{
|
|
CAID: ca.ID,
|
|
IssuerUserID: issuer.ID,
|
|
IssuerUsername: issuer.Username,
|
|
IssuerKind: strings.TrimSpace(issuerKind),
|
|
SourcePublicKey: strings.TrimSpace(sourcePublicKey),
|
|
SourcePublicKeyFingerprint: strings.TrimSpace(sourcePublicKeyFingerprint),
|
|
Certificate: signed.Certificate,
|
|
KeyID: signed.KeyID,
|
|
Principals: signed.Principals,
|
|
ValidAfter: signed.ValidAfter,
|
|
ValidBefore: signed.ValidBefore,
|
|
Serial: signed.Serial,
|
|
RemoteAddr: requestRemoteAddr(r),
|
|
UserAgent: strings.TrimSpace(r.UserAgent()),
|
|
}
|
|
_, err = api.store(r).CreateSSHUserCAIssuance(row)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func buildSSHUserCAIssuanceSummary(item models.SSHUserCAIssuance, caName string) sshUserCAIssuanceSummary {
|
|
return sshUserCAIssuanceSummary{
|
|
ID: item.ID,
|
|
CAID: item.CAID,
|
|
CAName: caName,
|
|
IssuerUserID: item.IssuerUserID,
|
|
IssuerUsername: item.IssuerUsername,
|
|
IssuerKind: item.IssuerKind,
|
|
SourcePublicKey: item.SourcePublicKey,
|
|
SourcePublicKeyFingerprint: item.SourcePublicKeyFingerprint,
|
|
Certificate: item.Certificate,
|
|
KeyID: item.KeyID,
|
|
Principals: item.Principals,
|
|
ValidAfter: item.ValidAfter,
|
|
ValidBefore: item.ValidBefore,
|
|
Serial: item.Serial,
|
|
RemoteAddr: item.RemoteAddr,
|
|
UserAgent: item.UserAgent,
|
|
CreatedAt: item.CreatedAt,
|
|
}
|
|
}
|
|
|
|
func parseSSHUserCAIssuanceLimit(raw string) int {
|
|
var value int
|
|
var err error
|
|
value = 50
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return value
|
|
}
|
|
value, err = strconv.Atoi(raw)
|
|
if err != nil {
|
|
return 50
|
|
}
|
|
if value <= 0 {
|
|
return 50
|
|
}
|
|
if value > 500 {
|
|
return 500
|
|
}
|
|
return value
|
|
}
|
|
|
|
func requestRemoteAddr(r *http.Request) string {
|
|
var host string
|
|
var err error
|
|
host, _, err = net.SplitHostPort(strings.TrimSpace(r.RemoteAddr))
|
|
if err == nil && strings.TrimSpace(host) != "" {
|
|
return strings.TrimSpace(host)
|
|
}
|
|
return strings.TrimSpace(r.RemoteAddr)
|
|
}
|
|
|
|
func buildSSHUserCASummary(item models.SSHUserCA) sshUserCASummary {
|
|
return sshUserCASummary{
|
|
ID: item.ID,
|
|
Name: item.Name,
|
|
Algorithm: item.Algorithm,
|
|
PublicKey: item.PublicKey,
|
|
Fingerprint: item.Fingerprint,
|
|
SerialCounter: item.SerialCounter,
|
|
Enabled: item.Enabled,
|
|
AllowUserSign: item.AllowUserSign,
|
|
MaxUserValidSeconds: item.MaxUserValidSeconds,
|
|
CreatedAt: item.CreatedAt,
|
|
UpdatedAt: item.UpdatedAt,
|
|
}
|
|
}
|
|
|
|
func sshAuthorizedKeyFingerprint(raw string) (string, error) {
|
|
var key ssh.PublicKey
|
|
var err error
|
|
key, _, _, _, err = ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(raw)))
|
|
if err != nil {
|
|
return "", errors.New("invalid user public key")
|
|
}
|
|
return strings.TrimSpace(ssh.FingerprintSHA256(key)), nil
|
|
}
|
|
|
|
func buildSSHCertificateDump(cert *ssh.Certificate) string {
|
|
var sb strings.Builder
|
|
var criticalKeys []string
|
|
var extensionKeys []string
|
|
var i int
|
|
var key string
|
|
fmt.Fprintf(&sb, "Type: %s %s certificate\n", cert.Type(), sshCertificateTypeName(cert.CertType))
|
|
fmt.Fprintf(&sb, "Public key: %s %s\n", cert.Key.Type(), ssh.FingerprintSHA256(cert.Key))
|
|
if cert.SignatureKey != nil {
|
|
fmt.Fprintf(&sb, "Signing CA: %s %s\n", cert.SignatureKey.Type(), ssh.FingerprintSHA256(cert.SignatureKey))
|
|
}
|
|
fmt.Fprintf(&sb, "Key ID: \"%s\"\n", cert.KeyId)
|
|
fmt.Fprintf(&sb, "Serial: %d\n", cert.Serial)
|
|
fmt.Fprintf(&sb, "Valid: from %s to %s\n", formatSSHCertificateTime(cert.ValidAfter), formatSSHCertificateTime(cert.ValidBefore))
|
|
fmt.Fprintf(&sb, "Principals:\n")
|
|
if len(cert.ValidPrincipals) == 0 {
|
|
fmt.Fprintf(&sb, " (none)\n")
|
|
} else {
|
|
for i = 0; i < len(cert.ValidPrincipals); i++ {
|
|
fmt.Fprintf(&sb, " %s\n", cert.ValidPrincipals[i])
|
|
}
|
|
}
|
|
fmt.Fprintf(&sb, "Critical Options:\n")
|
|
if len(cert.Permissions.CriticalOptions) == 0 {
|
|
fmt.Fprintf(&sb, " (none)\n")
|
|
} else {
|
|
for key = range cert.Permissions.CriticalOptions {
|
|
criticalKeys = append(criticalKeys, key)
|
|
}
|
|
sort.Strings(criticalKeys)
|
|
for i = 0; i < len(criticalKeys); i++ {
|
|
key = criticalKeys[i]
|
|
if cert.Permissions.CriticalOptions[key] == "" {
|
|
fmt.Fprintf(&sb, " %s\n", key)
|
|
} else {
|
|
fmt.Fprintf(&sb, " %s %s\n", key, cert.Permissions.CriticalOptions[key])
|
|
}
|
|
}
|
|
}
|
|
fmt.Fprintf(&sb, "Extensions:\n")
|
|
if len(cert.Permissions.Extensions) == 0 {
|
|
fmt.Fprintf(&sb, " (none)\n")
|
|
} else {
|
|
for key = range cert.Permissions.Extensions {
|
|
extensionKeys = append(extensionKeys, key)
|
|
}
|
|
sort.Strings(extensionKeys)
|
|
for i = 0; i < len(extensionKeys); i++ {
|
|
key = extensionKeys[i]
|
|
if cert.Permissions.Extensions[key] == "" {
|
|
fmt.Fprintf(&sb, " %s\n", key)
|
|
} else {
|
|
fmt.Fprintf(&sb, " %s %s\n", key, cert.Permissions.Extensions[key])
|
|
}
|
|
}
|
|
}
|
|
return sb.String()
|
|
}
|
|
|
|
func sshCertificateTypeName(value uint32) string {
|
|
if value == ssh.UserCert {
|
|
return "user"
|
|
}
|
|
if value == ssh.HostCert {
|
|
return "host"
|
|
}
|
|
return "unknown"
|
|
}
|
|
|
|
func formatSSHCertificateTime(value uint64) string {
|
|
if value == ssh.CertTimeInfinity {
|
|
return "forever"
|
|
}
|
|
return time.Unix(int64(value), 0).UTC().Format(time.RFC3339)
|
|
}
|
|
|
|
func parseSSHSignerFromPEM(raw string) (ssh.Signer, error) {
|
|
var privateKey any
|
|
var signer ssh.Signer
|
|
var err error
|
|
privateKey, err = ssh.ParseRawPrivateKey([]byte(strings.TrimSpace(raw)))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
signer, err = ssh.NewSignerFromKey(privateKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return signer, nil
|
|
}
|
|
|
|
func normalizeSSHCAAlgorithm(raw string) (string, error) {
|
|
var value string
|
|
value = strings.ToLower(strings.TrimSpace(raw))
|
|
if value == "" {
|
|
return "ed25519", nil
|
|
}
|
|
if value == "ed25519" || value == "ssh-ed25519" {
|
|
return "ed25519", nil
|
|
}
|
|
if value == "rsa-4096" || value == "rsa4096" || value == "ssh-rsa" {
|
|
return "rsa-4096", nil
|
|
}
|
|
if value == "ecdsa-p256" || value == "ecdsa256" || value == "ecdsa-sha2-nistp256" {
|
|
return "ecdsa-p256", nil
|
|
}
|
|
return "", errors.New("algorithm must be ed25519, rsa-4096, or ecdsa-p256")
|
|
}
|
|
|
|
func normalizeSSHUserCAMaxValidSeconds(raw int64) (int64, error) {
|
|
var value int64
|
|
value = raw
|
|
if value <= 0 {
|
|
value = 1800
|
|
}
|
|
if value < 60 || value > 604800 {
|
|
return 0, errors.New("max_user_valid_seconds must be between 60 and 604800")
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func generateSSHUserCAKeyPair(algorithm string) (string, string, string, error) {
|
|
var privateKey any
|
|
var edPrivate ed25519.PrivateKey
|
|
var rsaPrivate *rsa.PrivateKey
|
|
var ecdsaPrivate *ecdsa.PrivateKey
|
|
var pkcs8 []byte
|
|
var privatePEM string
|
|
var signer ssh.Signer
|
|
var err error
|
|
if algorithm == "ed25519" {
|
|
_, edPrivate, err = ed25519.GenerateKey(rand.Reader)
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
privateKey = edPrivate
|
|
} else if algorithm == "rsa-4096" {
|
|
rsaPrivate, err = rsa.GenerateKey(rand.Reader, 4096)
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
privateKey = rsaPrivate
|
|
} else if algorithm == "ecdsa-p256" {
|
|
ecdsaPrivate, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
privateKey = ecdsaPrivate
|
|
} else {
|
|
return "", "", "", errors.New("unsupported algorithm")
|
|
}
|
|
pkcs8, err = x509.MarshalPKCS8PrivateKey(privateKey)
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
privatePEM = strings.TrimSpace(string(pem.EncodeToMemory(&pem.Block{Type: "PRIVATE KEY", Bytes: pkcs8})))
|
|
signer, err = ssh.NewSignerFromKey(privateKey)
|
|
if err != nil {
|
|
return "", "", "", err
|
|
}
|
|
return privatePEM,
|
|
strings.TrimSpace(string(ssh.MarshalAuthorizedKey(signer.PublicKey()))),
|
|
strings.TrimSpace(ssh.FingerprintSHA256(signer.PublicKey())),
|
|
nil
|
|
}
|
|
|
|
func publicKeyAlgorithm(publicKey string) string {
|
|
var key ssh.PublicKey
|
|
var err error
|
|
key, _, _, _, err = ssh.ParseAuthorizedKey([]byte(strings.TrimSpace(publicKey)))
|
|
if err != nil {
|
|
return "unknown"
|
|
}
|
|
return key.Type()
|
|
}
|
|
|
|
func normalizeSSHPrincipals(items []string) []string {
|
|
var out []string
|
|
var seen map[string]bool
|
|
var i int
|
|
var value string
|
|
seen = map[string]bool{}
|
|
for i = 0; i < len(items); i++ {
|
|
value = strings.TrimSpace(items[i])
|
|
if value == "" {
|
|
continue
|
|
}
|
|
if seen[value] {
|
|
continue
|
|
}
|
|
seen[value] = true
|
|
out = append(out, value)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func normalizeSSHGrantIDs(items []string) []string {
|
|
var out []string
|
|
var seen map[string]bool
|
|
var i int
|
|
var value string
|
|
seen = map[string]bool{}
|
|
for i = 0; i < len(items); i++ {
|
|
value = strings.TrimSpace(items[i])
|
|
if value == "" {
|
|
continue
|
|
}
|
|
if seen[value] {
|
|
continue
|
|
}
|
|
seen[value] = true
|
|
out = append(out, value)
|
|
}
|
|
return out
|
|
}
|