first commit
This commit is contained in:
parent
f84c8f846a
commit
7b5d6968a5
35 changed files with 2035 additions and 215 deletions
|
@ -23,11 +23,12 @@ var (
|
|||
|
||||
// DB is a struct that implements the AcmeDB interface.
|
||||
type DB struct {
|
||||
db nosqlDB.DB
|
||||
db nosqlDB.DB
|
||||
authorityID string
|
||||
}
|
||||
|
||||
// New configures and returns a new ACME DB backend implemented using a nosql DB.
|
||||
func New(db nosqlDB.DB) (*DB, error) {
|
||||
func New(db nosqlDB.DB, authorityID string) (*DB, error) {
|
||||
tables := [][]byte{accountTable, accountByKeyIDTable, authzTable,
|
||||
challengeTable, nonceTable, orderTable, ordersByAccountIDTable, certTable}
|
||||
for _, b := range tables {
|
||||
|
@ -36,7 +37,7 @@ func New(db nosqlDB.DB) (*DB, error) {
|
|||
string(b))
|
||||
}
|
||||
}
|
||||
return &DB{db}, nil
|
||||
return &DB{db, authorityID}, nil
|
||||
}
|
||||
|
||||
// save writes the new data to the database, overwriting the old data if it
|
||||
|
|
|
@ -21,6 +21,7 @@ import (
|
|||
"github.com/go-chi/chi"
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
|
@ -32,7 +33,7 @@ type Authority interface {
|
|||
// context specifies the Authorize[Sign|Revoke|etc.] method.
|
||||
Authorize(ctx context.Context, ott string) ([]provisioner.SignOption, error)
|
||||
AuthorizeSign(ott string) ([]provisioner.SignOption, error)
|
||||
GetTLSOptions() *authority.TLSOptions
|
||||
GetTLSOptions() *config.TLSOptions
|
||||
Root(shasum string) (*x509.Certificate, error)
|
||||
Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
|
||||
Renew(peer *x509.Certificate) ([]*x509.Certificate, error)
|
||||
|
|
12
api/sign.go
12
api/sign.go
|
@ -5,7 +5,7 @@ import (
|
|||
"encoding/json"
|
||||
"net/http"
|
||||
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
)
|
||||
|
@ -37,11 +37,11 @@ func (s *SignRequest) Validate() error {
|
|||
|
||||
// SignResponse is the response object of the certificate signature request.
|
||||
type SignResponse struct {
|
||||
ServerPEM Certificate `json:"crt"`
|
||||
CaPEM Certificate `json:"ca"`
|
||||
CertChainPEM []Certificate `json:"certChain"`
|
||||
TLSOptions *authority.TLSOptions `json:"tlsOptions,omitempty"`
|
||||
TLS *tls.ConnectionState `json:"-"`
|
||||
ServerPEM Certificate `json:"crt"`
|
||||
CaPEM Certificate `json:"ca"`
|
||||
CertChainPEM []Certificate `json:"certChain"`
|
||||
TLSOptions *config.TLSOptions `json:"tlsOptions,omitempty"`
|
||||
TLS *tls.ConnectionState `json:"-"`
|
||||
}
|
||||
|
||||
// Sign is an HTTP handler that reads a certificate request and an
|
||||
|
|
15
api/ssh.go
15
api/ssh.go
|
@ -10,6 +10,7 @@ import (
|
|||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/templates"
|
||||
|
@ -22,12 +23,12 @@ type SSHAuthority interface {
|
|||
RenewSSH(ctx context.Context, cert *ssh.Certificate) (*ssh.Certificate, error)
|
||||
RekeySSH(ctx context.Context, cert *ssh.Certificate, key ssh.PublicKey, signOpts ...provisioner.SignOption) (*ssh.Certificate, error)
|
||||
SignSSHAddUser(ctx context.Context, key ssh.PublicKey, cert *ssh.Certificate) (*ssh.Certificate, error)
|
||||
GetSSHRoots(ctx context.Context) (*authority.SSHKeys, error)
|
||||
GetSSHFederation(ctx context.Context) (*authority.SSHKeys, error)
|
||||
GetSSHRoots(ctx context.Context) (*config.SSHKeys, error)
|
||||
GetSSHFederation(ctx context.Context) (*config.SSHKeys, error)
|
||||
GetSSHConfig(ctx context.Context, typ string, data map[string]string) ([]templates.Output, error)
|
||||
CheckSSHHost(ctx context.Context, principal string, token string) (bool, error)
|
||||
GetSSHHosts(ctx context.Context, cert *x509.Certificate) ([]authority.Host, error)
|
||||
GetSSHBastion(ctx context.Context, user string, hostname string) (*authority.Bastion, error)
|
||||
GetSSHHosts(ctx context.Context, cert *x509.Certificate) ([]config.Host, error)
|
||||
GetSSHBastion(ctx context.Context, user string, hostname string) (*config.Bastion, error)
|
||||
}
|
||||
|
||||
// SSHSignRequest is the request body of an SSH certificate request.
|
||||
|
@ -86,7 +87,7 @@ type SSHCertificate struct {
|
|||
// SSHGetHostsResponse is the response object that returns the list of valid
|
||||
// hosts for SSH.
|
||||
type SSHGetHostsResponse struct {
|
||||
Hosts []authority.Host `json:"hosts"`
|
||||
Hosts []config.Host `json:"hosts"`
|
||||
}
|
||||
|
||||
// MarshalJSON implements the json.Marshaler interface. Returns a quoted,
|
||||
|
@ -239,8 +240,8 @@ func (r *SSHBastionRequest) Validate() error {
|
|||
// SSHBastionResponse is the response body used to return the bastion for a
|
||||
// given host.
|
||||
type SSHBastionResponse struct {
|
||||
Hostname string `json:"hostname"`
|
||||
Bastion *authority.Bastion `json:"bastion,omitempty"`
|
||||
Hostname string `json:"hostname"`
|
||||
Bastion *config.Bastion `json:"bastion,omitempty"`
|
||||
}
|
||||
|
||||
// SSHSign is an HTTP handler that reads an SignSSHRequest with a one-time-token
|
||||
|
|
12
authority/admin.go
Normal file
12
authority/admin.go
Normal file
|
@ -0,0 +1,12 @@
|
|||
package authority
|
||||
|
||||
// Admin is the type definining Authority admins. Admins can update Authority
|
||||
// configuration, provisioners, and even other admins.
|
||||
type Admin struct {
|
||||
ID string `json:"-"`
|
||||
AuthorityID string `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Provisioner string `json:"provisioner"`
|
||||
IsSuperAdmin bool `json:"isSuperAdmin"`
|
||||
IsDeleted bool `json:"isDeleted"`
|
||||
}
|
|
@ -13,6 +13,9 @@ import (
|
|||
"github.com/smallstep/certificates/cas"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/mgmt"
|
||||
authMgmtNosql "github.com/smallstep/certificates/authority/mgmt/db/nosql"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
casapi "github.com/smallstep/certificates/cas/apiv1"
|
||||
"github.com/smallstep/certificates/db"
|
||||
|
@ -20,17 +23,15 @@ import (
|
|||
kmsapi "github.com/smallstep/certificates/kms/apiv1"
|
||||
"github.com/smallstep/certificates/kms/sshagentkms"
|
||||
"github.com/smallstep/certificates/templates"
|
||||
"github.com/smallstep/nosql"
|
||||
"go.step.sm/crypto/pemutil"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
const (
|
||||
legacyAuthority = "step-certificate-authority"
|
||||
)
|
||||
|
||||
// Authority implements the Certificate Authority internal interface.
|
||||
type Authority struct {
|
||||
config *Config
|
||||
config *config.Config
|
||||
mgmtDB *mgmt.DB
|
||||
keyManager kms.KeyManager
|
||||
provisioners *provisioner.Collection
|
||||
db db.AuthDB
|
||||
|
@ -55,14 +56,14 @@ type Authority struct {
|
|||
startTime time.Time
|
||||
|
||||
// Custom functions
|
||||
sshBastionFunc func(ctx context.Context, user, hostname string) (*Bastion, error)
|
||||
sshBastionFunc func(ctx context.Context, user, hostname string) (*config.Bastion, error)
|
||||
sshCheckHostFunc func(ctx context.Context, principal string, tok string, roots []*x509.Certificate) (bool, error)
|
||||
sshGetHostsFunc func(ctx context.Context, cert *x509.Certificate) ([]Host, error)
|
||||
sshGetHostsFunc func(ctx context.Context, cert *x509.Certificate) ([]config.Host, error)
|
||||
getIdentityFunc provisioner.GetIdentityFunc
|
||||
}
|
||||
|
||||
// New creates and initiates a new Authority type.
|
||||
func New(config *Config, opts ...Option) (*Authority, error) {
|
||||
func New(config *config.Config, opts ...Option) (*Authority, error) {
|
||||
err := config.Validate()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -92,7 +93,7 @@ func New(config *Config, opts ...Option) (*Authority, error) {
|
|||
// project without the limitations of the config.
|
||||
func NewEmbedded(opts ...Option) (*Authority, error) {
|
||||
a := &Authority{
|
||||
config: &Config{},
|
||||
config: &config.Config{},
|
||||
certificates: new(sync.Map),
|
||||
}
|
||||
|
||||
|
@ -116,7 +117,7 @@ func NewEmbedded(opts ...Option) (*Authority, error) {
|
|||
}
|
||||
|
||||
// Initialize config required fields.
|
||||
a.config.init()
|
||||
a.config.Init()
|
||||
|
||||
// Initialize authority from options or configuration.
|
||||
if err := a.init(); err != nil {
|
||||
|
@ -143,6 +144,22 @@ func (a *Authority) init() error {
|
|||
}
|
||||
}
|
||||
|
||||
// Pull AuthConfig from DB.
|
||||
if true {
|
||||
mgmtDB, err := authMgmtNosql.New(a.db.(nosql.DB), mgmt.DefaultAuthorityID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_ac, err := mgmtDB.GetAuthConfig(context.Background(), mgmt.DefaultAuthorityID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
a.config.AuthorityConfig, err = _ac.ToCertificates()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize key manager if it has not been set in the options.
|
||||
if a.keyManager == nil {
|
||||
var options kmsapi.Options
|
||||
|
@ -314,7 +331,7 @@ func (a *Authority) init() error {
|
|||
}
|
||||
|
||||
// Merge global and configuration claims
|
||||
claimer, err := provisioner.NewClaimer(a.config.AuthorityConfig.Claims, globalProvisionerClaims)
|
||||
claimer, err := provisioner.NewClaimer(a.config.AuthorityConfig.Claims, config.GlobalProvisionerClaims)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -326,7 +343,7 @@ func (a *Authority) init() error {
|
|||
return err
|
||||
}
|
||||
// Initialize provisioners
|
||||
audiences := a.config.getAudiences()
|
||||
audiences := a.config.GetAudiences()
|
||||
a.provisioners = provisioner.NewCollection(audiences)
|
||||
config := provisioner.Config{
|
||||
Claims: claimer.Claims(),
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
package authority
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
@ -15,6 +15,10 @@ import (
|
|||
"github.com/smallstep/certificates/templates"
|
||||
)
|
||||
|
||||
const (
|
||||
legacyAuthority = "step-certificate-authority"
|
||||
)
|
||||
|
||||
var (
|
||||
// DefaultTLSOptions represents the default TLS version as well as the cipher
|
||||
// suites used in the TLS certificates.
|
||||
|
@ -28,10 +32,12 @@ var (
|
|||
MaxVersion: 1.2,
|
||||
Renegotiation: false,
|
||||
}
|
||||
defaultBackdate = time.Minute
|
||||
defaultDisableRenewal = false
|
||||
defaultEnableSSHCA = false
|
||||
globalProvisionerClaims = provisioner.Claims{
|
||||
defaultBackdate = time.Minute
|
||||
defaultDisableRenewal = false
|
||||
defaultEnableSSHCA = false
|
||||
// GlobalProvisionerClaims default claims for the Authority. Can be overriden
|
||||
// by provisioner specific claims.
|
||||
GlobalProvisionerClaims = provisioner.Claims{
|
||||
MinTLSDur: &provisioner.Duration{Duration: 5 * time.Minute}, // TLS certs
|
||||
MaxTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
|
||||
DefaultTLSDur: &provisioner.Duration{Duration: 24 * time.Hour},
|
||||
|
@ -151,9 +157,9 @@ func LoadConfiguration(filename string) (*Config, error) {
|
|||
return &c, nil
|
||||
}
|
||||
|
||||
// initializes the minimal configuration required to create an authority. This
|
||||
// Init initializes the minimal configuration required to create an authority. This
|
||||
// is mainly used on embedded authorities.
|
||||
func (c *Config) init() {
|
||||
func (c *Config) Init() {
|
||||
if c.DNSNames == nil {
|
||||
c.DNSNames = []string{"localhost", "127.0.0.1", "::1"}
|
||||
}
|
||||
|
@ -246,13 +252,13 @@ func (c *Config) Validate() error {
|
|||
return err
|
||||
}
|
||||
|
||||
return c.AuthorityConfig.Validate(c.getAudiences())
|
||||
return c.AuthorityConfig.Validate(c.GetAudiences())
|
||||
}
|
||||
|
||||
// getAudiences returns the legacy and possible urls without the ports that will
|
||||
// GetAudiences returns the legacy and possible urls without the ports that will
|
||||
// be used as the default provisioner audiences. The CA might have proxies in
|
||||
// front so we cannot rely on the port.
|
||||
func (c *Config) getAudiences() provisioner.Audiences {
|
||||
func (c *Config) GetAudiences() provisioner.Audiences {
|
||||
audiences := provisioner.Audiences{
|
||||
Sign: []string{legacyAuthority},
|
||||
Revoke: []string{legacyAuthority},
|
|
@ -1,4 +1,4 @@
|
|||
package authority
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
94
authority/config/ssh.go
Normal file
94
authority/config/ssh.go
Normal file
|
@ -0,0 +1,94 @@
|
|||
package config
|
||||
|
||||
import (
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"go.step.sm/crypto/jose"
|
||||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// SSHConfig contains the user and host keys.
|
||||
type SSHConfig struct {
|
||||
HostKey string `json:"hostKey"`
|
||||
UserKey string `json:"userKey"`
|
||||
Keys []*SSHPublicKey `json:"keys,omitempty"`
|
||||
AddUserPrincipal string `json:"addUserPrincipal,omitempty"`
|
||||
AddUserCommand string `json:"addUserCommand,omitempty"`
|
||||
Bastion *Bastion `json:"bastion,omitempty"`
|
||||
}
|
||||
|
||||
// Bastion contains the custom properties used on bastion.
|
||||
type Bastion struct {
|
||||
Hostname string `json:"hostname"`
|
||||
User string `json:"user,omitempty"`
|
||||
Port string `json:"port,omitempty"`
|
||||
Command string `json:"cmd,omitempty"`
|
||||
Flags string `json:"flags,omitempty"`
|
||||
}
|
||||
|
||||
// HostTag are tagged with k,v pairs. These tags are how a user is ultimately
|
||||
// associated with a host.
|
||||
type HostTag struct {
|
||||
ID string
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
// Host defines expected attributes for an ssh host.
|
||||
type Host struct {
|
||||
HostID string `json:"hid"`
|
||||
HostTags []HostTag `json:"host_tags"`
|
||||
Hostname string `json:"hostname"`
|
||||
}
|
||||
|
||||
// Validate checks the fields in SSHConfig.
|
||||
func (c *SSHConfig) Validate() error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
for _, k := range c.Keys {
|
||||
if err := k.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SSHPublicKey contains a public key used by federated CAs to keep old signing
|
||||
// keys for this ca.
|
||||
type SSHPublicKey struct {
|
||||
Type string `json:"type"`
|
||||
Federated bool `json:"federated"`
|
||||
Key jose.JSONWebKey `json:"key"`
|
||||
publicKey ssh.PublicKey
|
||||
}
|
||||
|
||||
// Validate checks the fields in SSHPublicKey.
|
||||
func (k *SSHPublicKey) Validate() error {
|
||||
switch {
|
||||
case k.Type == "":
|
||||
return errors.New("type cannot be empty")
|
||||
case k.Type != provisioner.SSHHostCert && k.Type != provisioner.SSHUserCert:
|
||||
return errors.Errorf("invalid type %s, it must be user or host", k.Type)
|
||||
case !k.Key.IsPublic():
|
||||
return errors.New("invalid key type, it must be a public key")
|
||||
}
|
||||
|
||||
key, err := ssh.NewPublicKey(k.Key.Key)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating ssh key")
|
||||
}
|
||||
k.publicKey = key
|
||||
return nil
|
||||
}
|
||||
|
||||
// PublicKey returns the ssh public key.
|
||||
func (k *SSHPublicKey) PublicKey() ssh.PublicKey {
|
||||
return k.publicKey
|
||||
}
|
||||
|
||||
// SSHKeys represents the SSH User and Host public keys.
|
||||
type SSHKeys struct {
|
||||
UserKeys []ssh.PublicKey
|
||||
HostKeys []ssh.PublicKey
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package authority
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
|
@ -1,4 +1,4 @@
|
|||
package authority
|
||||
package config
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
|
@ -1,4 +1,4 @@
|
|||
package authority
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
|
@ -1,4 +1,4 @@
|
|||
package authority
|
||||
package config
|
||||
|
||||
import (
|
||||
"reflect"
|
112
authority/mgmt/api/admin.go
Normal file
112
authority/mgmt/api/admin.go
Normal file
|
@ -0,0 +1,112 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/smallstep/certificates/api"
|
||||
)
|
||||
|
||||
// CreateAdminRequest represents the body for a CreateAdmin request.
|
||||
type CreateAdminRequest struct {
|
||||
Name string `json:"name"`
|
||||
Provisioner string `json:"provisioner"`
|
||||
IsSuperAdmin bool `json:"isSuperAdmin"`
|
||||
}
|
||||
|
||||
// Validate validates a new-admin request body.
|
||||
func (car *CreateAdminRequest) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateAdminRequest represents the body for a UpdateAdmin request.
|
||||
type UpdateAdminRequest struct {
|
||||
Name string `json:"name"`
|
||||
Provisioner string `json:"provisioner"`
|
||||
IsSuperAdmin bool `json:"isSuperAdmin"`
|
||||
}
|
||||
|
||||
// Validate validates a new-admin request body.
|
||||
func (uar *UpdateAdminRequest) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAdmin returns the requested admin, or an error.
|
||||
func (h *Handler) GetAdmin(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
prov, err := h.db.GetAdmin(ctx, id)
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
api.JSON(w, prov)
|
||||
}
|
||||
|
||||
// GetAdmins returns all admins associated with the authority.
|
||||
func (h *Handler) GetAdmins(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
admins, err := h.db.GetAdmins(ctx)
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
api.JSON(w, admins)
|
||||
}
|
||||
|
||||
// CreateAdmin creates a new admin.
|
||||
func (h *Handler) CreateAdmin(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var body CreateAdminRequest
|
||||
if err := ReadJSON(r.Body, &body); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
if err := body.Validate(); err != nil {
|
||||
api.WriteError(w, err)
|
||||
}
|
||||
|
||||
adm := &config.Admin{
|
||||
Name: body.Name,
|
||||
Provisioner: body.Provisioner,
|
||||
IsSuperAdmin: body.IsSuperAdmin,
|
||||
}
|
||||
if err := h.db.CreateAdmin(ctx, adm); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
api.JSONStatus(w, adm, http.StatusCreated)
|
||||
}
|
||||
|
||||
// UpdateAdmin updates an existing admin.
|
||||
func (h *Handler) UpdateAdmin(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
var body UpdateAdminRequest
|
||||
if err := ReadJSON(r.Body, &body); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
if err := body.Validate(); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
if adm, err := h.db.GetAdmin(ctx, id); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
adm.Name = body.Name
|
||||
adm.Provisioner = body.Provisioner
|
||||
adm.IsSuperAdmin = body.IsSuperAdmin
|
||||
|
||||
if err := h.db.UpdateAdmin(ctx, adm); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
api.JSON(w, adm)
|
||||
}
|
121
authority/mgmt/api/authConfig.go
Normal file
121
authority/mgmt/api/authConfig.go
Normal file
|
@ -0,0 +1,121 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/smallstep/certificates/api"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
)
|
||||
|
||||
// CreateAuthConfigRequest represents the body for a CreateAuthConfig request.
|
||||
type CreateAuthConfigRequest struct {
|
||||
ASN1DN *authority.ASN1DN `json:"asn1dn,omitempty"`
|
||||
Claims *config.Claims `json:"claims,omitempty"`
|
||||
DisableIssuedAtCheck bool `json:"disableIssuedAtCheck,omitempty"`
|
||||
Backdate string `json:"backdate,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates a CreateAuthConfig request body.
|
||||
func (car *CreateAuthConfigRequest) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateAuthConfigRequest represents the body for a UpdateAuthConfig request.
|
||||
type UpdateAuthConfigRequest struct {
|
||||
ASN1DN *authority.ASN1DN `json:"asn1dn"`
|
||||
Claims *config.Claims `json:"claims"`
|
||||
DisableIssuedAtCheck bool `json:"disableIssuedAtCheck,omitempty"`
|
||||
Backdate string `json:"backdate,omitempty"`
|
||||
}
|
||||
|
||||
// Validate validates a new-admin request body.
|
||||
func (uar *UpdateAuthConfigRequest) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetAuthConfig returns the requested admin, or an error.
|
||||
func (h *Handler) GetAuthConfig(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
ac, err := h.db.GetAuthConfig(ctx, id)
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
api.JSON(w, ac)
|
||||
}
|
||||
|
||||
// CreateAuthConfig creates a new admin.
|
||||
func (h *Handler) CreateAuthConfig(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var body CreateAuthConfigRequest
|
||||
if err := ReadJSON(r.Body, &body); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
if err := body.Validate(); err != nil {
|
||||
api.WriteError(w, err)
|
||||
}
|
||||
|
||||
ac := config.AuthConfig{
|
||||
Status: config.StatusActive,
|
||||
DisableIssuedAtCheck: body.DisableIssuedAtCheck,
|
||||
Backdate: "1m",
|
||||
}
|
||||
if body.ASN1DN != nil {
|
||||
ac.ASN1DN = body.ASN1DN
|
||||
}
|
||||
if body.Claims != nil {
|
||||
ac.Claims = body.Claims
|
||||
}
|
||||
if body.Backdate != "" {
|
||||
ac.Backdate = body.Backdate
|
||||
}
|
||||
if err := h.db.CreateAuthConfig(ctx, ac); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
api.JSONStatus(w, ac, http.StatusCreated)
|
||||
}
|
||||
|
||||
// UpdateAuthConfig updates an existing AuthConfig.
|
||||
func (h *Handler) UpdateAuthConfig(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
var body UpdateAuthConfigRequest
|
||||
if err := ReadJSON(r.Body, &body); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
if err := body.Validate(); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
if ac, err := h.db.GetAuthConfig(ctx, id); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
ac.DisableIssuedAtCheck = body.DisableIssuedAtCheck
|
||||
ac.Status = body.Status
|
||||
if body.ASN1DN != nil {
|
||||
ac.ASN1DN = body.ASN1DN
|
||||
}
|
||||
if body.Claims != nil {
|
||||
ac.Claims = body.Claims
|
||||
}
|
||||
if body.Backdate != "" {
|
||||
ac.Backdate = body.Backdate
|
||||
}
|
||||
|
||||
if err := h.db.UpdateAuthConfig(ctx, ac); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
api.JSON(w, ac)
|
||||
}
|
50
authority/mgmt/api/handler.go
Normal file
50
authority/mgmt/api/handler.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/smallstep/certificates/api"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
)
|
||||
|
||||
// Clock that returns time in UTC rounded to seconds.
|
||||
type Clock struct{}
|
||||
|
||||
// Now returns the UTC time rounded to seconds.
|
||||
func (c *Clock) Now() time.Time {
|
||||
return time.Now().UTC().Truncate(time.Second)
|
||||
}
|
||||
|
||||
var clock Clock
|
||||
|
||||
// Handler is the ACME API request handler.
|
||||
type Handler struct {
|
||||
db config.DB
|
||||
}
|
||||
|
||||
// NewHandler returns a new Authority Config Handler.
|
||||
func NewHandler(db config.DB) api.RouterHandler {
|
||||
return &Handler{
|
||||
db: ops.DB,
|
||||
}
|
||||
}
|
||||
|
||||
// Route traffic and implement the Router interface.
|
||||
func (h *Handler) Route(r api.Router) {
|
||||
// Provisioners
|
||||
r.MethodFunc("GET", "/provisioner/{id}", h.GetProvisioner)
|
||||
r.MethodFunc("GET", "/provisioners", h.GetProvisioners)
|
||||
r.MethodFunc("POST", "/provisioner", h.CreateProvisioner)
|
||||
r.MethodFunc("PUT", "/provsiioner/{id}", h.UpdateProvisioner)
|
||||
|
||||
// Admins
|
||||
r.MethodFunc("GET", "/admin/{id}", h.GetAdmin)
|
||||
r.MethodFunc("GET", "/admins", h.GetAdmins)
|
||||
r.MethodFunc("POST", "/admin", h.CreateAdmin)
|
||||
r.MethodFunc("PUT", "/admin/{id}", h.UpdateAdmin)
|
||||
|
||||
// AuthConfig
|
||||
r.MethodFunc("GET", "/authconfig/{id}", h.GetAuthConfig)
|
||||
r.MethodFunc("POST", "/authconfig", h.CreateAuthConfig)
|
||||
r.MethodFunc("PUT", "/authconfig/{id}", h.UpdateAuthConfig)
|
||||
}
|
122
authority/mgmt/api/provisioner.go
Normal file
122
authority/mgmt/api/provisioner.go
Normal file
|
@ -0,0 +1,122 @@
|
|||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/go-chi/chi"
|
||||
"github.com/smallstep/certificates/api"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
)
|
||||
|
||||
// CreateProvisionerRequest represents the body for a CreateProvisioner request.
|
||||
type CreateProvisionerRequest struct {
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Claims *config.Claims `json:"claims"`
|
||||
Details interface{} `json:"details"`
|
||||
X509Template string `json:"x509Template"`
|
||||
SSHTemplate string `json:"sshTemplate"`
|
||||
}
|
||||
|
||||
// Validate validates a new-provisioner request body.
|
||||
func (car *CreateProvisionerRequest) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateProvisionerRequest represents the body for a UpdateProvisioner request.
|
||||
type UpdateProvisionerRequest struct {
|
||||
Claims *config.Claims `json:"claims"`
|
||||
Details interface{} `json:"details"`
|
||||
X509Template string `json:"x509Template"`
|
||||
SSHTemplate string `json:"sshTemplate"`
|
||||
}
|
||||
|
||||
// Validate validates a new-provisioner request body.
|
||||
func (uar *UpdateProvisionerRequest) Validate() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetProvisioner returns the requested provisioner, or an error.
|
||||
func (h *Handler) GetProvisioner(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
prov, err := h.db.GetProvisioner(ctx, id)
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
api.JSON(w, prov)
|
||||
}
|
||||
|
||||
// GetProvisioners returns all provisioners associated with the authority.
|
||||
func (h *Handler) GetProvisioners(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
provs, err := h.db.GetProvisioners(ctx)
|
||||
if err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
api.JSON(w, provs)
|
||||
}
|
||||
|
||||
// CreateProvisioner creates a new prov.
|
||||
func (h *Handler) CreateProvisioner(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
|
||||
var body CreateProvisionerRequest
|
||||
if err := ReadJSON(r.Body, &body); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
if err := body.Validate(); err != nil {
|
||||
api.WriteError(w, err)
|
||||
}
|
||||
|
||||
prov := &config.Provisioner{
|
||||
Type: body.Type,
|
||||
Name: body.Name,
|
||||
Claims: body.Claims,
|
||||
Details: body.Details,
|
||||
X509Template: body.X509Template,
|
||||
SSHTemplate: body.SSHTemplate,
|
||||
}
|
||||
if err := h.db.CreateProvisioner(ctx, prov); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
api.JSONStatus(w, prov, http.StatusCreated)
|
||||
}
|
||||
|
||||
// UpdateProvisioner updates an existing prov.
|
||||
func (h *Handler) UpdateProvisioner(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := r.Context()
|
||||
id := chi.URLParam(r, "id")
|
||||
|
||||
var body UpdateProvisionerRequest
|
||||
if err := ReadJSON(r.Body, &body); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
if err := body.Validate(); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
if prov, err := h.db.GetProvisioner(ctx, id); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
|
||||
prov.Claims = body.Claims
|
||||
prov.Details = body.Provisioner
|
||||
prov.X509Template = body.X509Template
|
||||
prov.SSHTemplate = body.SSHTemplate
|
||||
prov.Status = body.Status
|
||||
|
||||
if err := h.db.UpdateProvisioner(ctx, prov); err != nil {
|
||||
api.WriteError(w, err)
|
||||
return
|
||||
}
|
||||
api.JSON(w, prov)
|
||||
}
|
357
authority/mgmt/config.go
Normal file
357
authority/mgmt/config.go
Normal file
|
@ -0,0 +1,357 @@
|
|||
package mgmt
|
||||
|
||||
import (
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
authority "github.com/smallstep/certificates/authority/config"
|
||||
)
|
||||
|
||||
const (
|
||||
DefaultAuthorityID = "00000000-0000-0000-0000-000000000000"
|
||||
)
|
||||
|
||||
// StatusType is the type for status.
|
||||
type StatusType int
|
||||
|
||||
const (
|
||||
// StatusActive active
|
||||
StatusActive StatusType = iota
|
||||
// StatusDeleted deleted
|
||||
StatusDeleted
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
*X509Claims `json:"x509Claims"`
|
||||
*SSHClaims `json:"sshClaims"`
|
||||
DisableRenewal *bool `json:"disableRenewal"`
|
||||
}
|
||||
|
||||
type X509Claims struct {
|
||||
Durations *Durations `json:"durations"`
|
||||
}
|
||||
|
||||
type SSHClaims struct {
|
||||
UserDuration *Durations `json:"userDurations"`
|
||||
HostDuration *Durations `json:"hostDuration"`
|
||||
}
|
||||
|
||||
type Durations struct {
|
||||
Min string `json:"min"`
|
||||
Max string `json:"max"`
|
||||
Default string `json:"default"`
|
||||
}
|
||||
|
||||
// Admin type.
|
||||
type Admin struct {
|
||||
ID string `json:"-"`
|
||||
AuthorityID string `json:"-"`
|
||||
Name string `json:"name"`
|
||||
Provisioner string `json:"provisioner"`
|
||||
IsSuperAdmin bool `json:"isSuperAdmin"`
|
||||
Status StatusType `json:"status"`
|
||||
}
|
||||
|
||||
// Provisioner type.
|
||||
type Provisioner struct {
|
||||
ID string `json:"-"`
|
||||
AuthorityID string `json:"-"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Claims *Claims `json:"claims"`
|
||||
Details interface{} `json:"details"`
|
||||
X509Template string `json:"x509Template"`
|
||||
SSHTemplate string `json:"sshTemplate"`
|
||||
Status StatusType `json:"status"`
|
||||
}
|
||||
|
||||
// AuthConfig represents the Authority Configuration.
|
||||
type AuthConfig struct {
|
||||
//*cas.Options `json:"cas"`
|
||||
ID string `json:"id"`
|
||||
ASN1DN *config.ASN1DN `json:"template,omitempty"`
|
||||
Provisioners []*Provisioner `json:"-"`
|
||||
Claims *Claims `json:"claims,omitempty"`
|
||||
DisableIssuedAtCheck bool `json:"disableIssuedAtCheck,omitempty"`
|
||||
Backdate string `json:"backdate,omitempty"`
|
||||
Status StatusType `json:"status,omitempty"`
|
||||
}
|
||||
|
||||
func (ac *AuthConfig) ToCertificates() (*config.AuthConfig, error) {
|
||||
return &authority.AuthConfig{}, nil
|
||||
}
|
||||
|
||||
/*
|
||||
// ToCertificates converts the landlord provisioner type to the open source
|
||||
// provisioner type.
|
||||
func (p *Provisioner) ToCertificates(ctx context.Context, db database.DB) (provisioner.Interface, error) {
|
||||
claims, err := p.Claims.ToCertificates()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
details := p.Details.GetData()
|
||||
if details == nil {
|
||||
return nil, fmt.Errorf("provisioner does not have any details")
|
||||
}
|
||||
|
||||
options, err := p.getOptions(ctx, db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
switch d := details.(type) {
|
||||
case *ProvisionerDetails_JWK:
|
||||
k := d.JWK.GetKey()
|
||||
jwk := new(jose.JSONWebKey)
|
||||
if err := json.Unmarshal(k.Key.Public, &jwk); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &provisioner.JWK{
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
Key: jwk,
|
||||
EncryptedKey: string(k.Key.Private),
|
||||
Claims: claims,
|
||||
Options: options,
|
||||
}, nil
|
||||
case *ProvisionerDetails_OIDC:
|
||||
cfg := d.OIDC
|
||||
return &provisioner.OIDC{
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
TenantID: cfg.TenantId,
|
||||
ClientID: cfg.ClientId,
|
||||
ClientSecret: cfg.ClientSecret,
|
||||
ConfigurationEndpoint: cfg.ConfigurationEndpoint,
|
||||
Admins: cfg.Admins,
|
||||
Domains: cfg.Domains,
|
||||
Groups: cfg.Groups,
|
||||
ListenAddress: cfg.ListenAddress,
|
||||
Claims: claims,
|
||||
Options: options,
|
||||
}, nil
|
||||
case *ProvisionerDetails_GCP:
|
||||
cfg := d.GCP
|
||||
return &provisioner.GCP{
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
ServiceAccounts: cfg.ServiceAccounts,
|
||||
ProjectIDs: cfg.ProjectIds,
|
||||
DisableCustomSANs: cfg.DisableCustomSans,
|
||||
DisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse,
|
||||
InstanceAge: durationValue(cfg.InstanceAge),
|
||||
Claims: claims,
|
||||
Options: options,
|
||||
}, nil
|
||||
case *ProvisionerDetails_AWS:
|
||||
cfg := d.AWS
|
||||
return &provisioner.AWS{
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
Accounts: cfg.Accounts,
|
||||
DisableCustomSANs: cfg.DisableCustomSans,
|
||||
DisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse,
|
||||
InstanceAge: durationValue(cfg.InstanceAge),
|
||||
Claims: claims,
|
||||
Options: options,
|
||||
}, nil
|
||||
case *ProvisionerDetails_Azure:
|
||||
cfg := d.Azure
|
||||
return &provisioner.Azure{
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
TenantID: cfg.TenantId,
|
||||
ResourceGroups: cfg.ResourceGroups,
|
||||
Audience: cfg.Audience,
|
||||
DisableCustomSANs: cfg.DisableCustomSans,
|
||||
DisableTrustOnFirstUse: cfg.DisableTrustOnFirstUse,
|
||||
Claims: claims,
|
||||
Options: options,
|
||||
}, nil
|
||||
case *ProvisionerDetails_X5C:
|
||||
var roots []byte
|
||||
for i, k := range d.X5C.GetRoots() {
|
||||
if b := k.GetKey().GetPublic(); b != nil {
|
||||
if i > 0 {
|
||||
roots = append(roots, '\n')
|
||||
}
|
||||
roots = append(roots, b...)
|
||||
}
|
||||
}
|
||||
return &provisioner.X5C{
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
Roots: roots,
|
||||
Claims: claims,
|
||||
Options: options,
|
||||
}, nil
|
||||
case *ProvisionerDetails_K8SSA:
|
||||
var publicKeys []byte
|
||||
for i, k := range d.K8SSA.GetPublicKeys() {
|
||||
if b := k.GetKey().GetPublic(); b != nil {
|
||||
if i > 0 {
|
||||
publicKeys = append(publicKeys, '\n')
|
||||
}
|
||||
publicKeys = append(publicKeys, k.Key.Public...)
|
||||
}
|
||||
}
|
||||
return &provisioner.K8sSA{
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
PubKeys: publicKeys,
|
||||
Claims: claims,
|
||||
Options: options,
|
||||
}, nil
|
||||
case *ProvisionerDetails_SSHPOP:
|
||||
return &provisioner.SSHPOP{
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
Claims: claims,
|
||||
}, nil
|
||||
case *ProvisionerDetails_ACME:
|
||||
cfg := d.ACME
|
||||
return &provisioner.ACME{
|
||||
Type: p.Type.String(),
|
||||
Name: p.Name,
|
||||
ForceCN: cfg.ForceCn,
|
||||
Claims: claims,
|
||||
Options: options,
|
||||
}, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("provisioner %s not implemented", p.Type.String())
|
||||
}
|
||||
}
|
||||
|
||||
// ToCertificates converts the landlord provisioner claims type to the open source
|
||||
// (step-ca) claims type.
|
||||
func (c *Claims) ToCertificates() (*provisioner.Claims, error) {
|
||||
x509, ssh := c.GetX509(), c.GetSsh()
|
||||
x509Durations := x509.GetDurations()
|
||||
hostDurations := ssh.GetHostDurations()
|
||||
userDurations := ssh.GetUserDurations()
|
||||
enableSSHCA := ssh.GetEnabled()
|
||||
return &provisioner.Claims{
|
||||
MinTLSDur: durationPtr(x509Durations.GetMin()),
|
||||
MaxTLSDur: durationPtr(x509Durations.GetMax()),
|
||||
DefaultTLSDur: durationPtr(x509Durations.GetDefault()),
|
||||
DisableRenewal: &c.DisableRenewal,
|
||||
MinUserSSHDur: durationPtr(userDurations.GetMin()),
|
||||
MaxUserSSHDur: durationPtr(userDurations.GetMax()),
|
||||
DefaultUserSSHDur: durationPtr(userDurations.GetDefault()),
|
||||
MinHostSSHDur: durationPtr(hostDurations.GetMin()),
|
||||
MaxHostSSHDur: durationPtr(hostDurations.GetMax()),
|
||||
DefaultHostSSHDur: durationPtr(hostDurations.GetDefault()),
|
||||
EnableSSHCA: &enableSSHCA,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func durationPtr(d *duration.Duration) *provisioner.Duration {
|
||||
if d == nil {
|
||||
return nil
|
||||
}
|
||||
return &provisioner.Duration{
|
||||
Duration: time.Duration(d.Seconds)*time.Second + time.Duration(d.Nanos)*time.Nanosecond,
|
||||
}
|
||||
}
|
||||
|
||||
func durationValue(d *duration.Duration) provisioner.Duration {
|
||||
if d == nil {
|
||||
return provisioner.Duration{}
|
||||
}
|
||||
return provisioner.Duration{
|
||||
Duration: time.Duration(d.Seconds)*time.Second + time.Duration(d.Nanos)*time.Nanosecond,
|
||||
}
|
||||
}
|
||||
|
||||
func marshalDetails(d *ProvisionerDetails) (sql.NullString, error) {
|
||||
b, err := json.Marshal(d.GetData())
|
||||
if err != nil {
|
||||
return sql.NullString{}, nil
|
||||
}
|
||||
return sql.NullString{
|
||||
String: string(b),
|
||||
Valid: len(b) > 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func unmarshalDetails(ctx context.Context, db database.DB, typ ProvisionerType, s sql.NullString) (*ProvisionerDetails, error) {
|
||||
if !s.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
var v isProvisionerDetails_Data
|
||||
switch typ {
|
||||
case ProvisionerType_JWK:
|
||||
p := new(ProvisionerDetails_JWK)
|
||||
if err := json.Unmarshal([]byte(s.String), p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if p.JWK.Key.Key == nil {
|
||||
key, err := LoadKey(ctx, db, p.JWK.Key.Id.Id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
p.JWK.Key = key
|
||||
}
|
||||
return &ProvisionerDetails{Data: p}, nil
|
||||
case ProvisionerType_OIDC:
|
||||
v = new(ProvisionerDetails_OIDC)
|
||||
case ProvisionerType_GCP:
|
||||
v = new(ProvisionerDetails_GCP)
|
||||
case ProvisionerType_AWS:
|
||||
v = new(ProvisionerDetails_AWS)
|
||||
case ProvisionerType_AZURE:
|
||||
v = new(ProvisionerDetails_Azure)
|
||||
case ProvisionerType_ACME:
|
||||
v = new(ProvisionerDetails_ACME)
|
||||
case ProvisionerType_X5C:
|
||||
p := new(ProvisionerDetails_X5C)
|
||||
if err := json.Unmarshal([]byte(s.String), p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, k := range p.X5C.GetRoots() {
|
||||
if err := k.Select(ctx, db, k.Id.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &ProvisionerDetails{Data: p}, nil
|
||||
case ProvisionerType_K8SSA:
|
||||
p := new(ProvisionerDetails_K8SSA)
|
||||
if err := json.Unmarshal([]byte(s.String), p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, k := range p.K8SSA.GetPublicKeys() {
|
||||
if err := k.Select(ctx, db, k.Id.Id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &ProvisionerDetails{Data: p}, nil
|
||||
case ProvisionerType_SSHPOP:
|
||||
v = new(ProvisionerDetails_SSHPOP)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported provisioner type %s", typ)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal([]byte(s.String), v); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &ProvisionerDetails{Data: v}, nil
|
||||
}
|
||||
|
||||
func marshalClaims(c *Claims) (sql.NullString, error) {
|
||||
b, err := json.Marshal(c)
|
||||
if err != nil {
|
||||
return sql.NullString{}, nil
|
||||
}
|
||||
return sql.NullString{
|
||||
String: string(b),
|
||||
Valid: len(b) > 0,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func unmarshalClaims(s sql.NullString) (*Claims, error) {
|
||||
if !s.Valid {
|
||||
return nil, nil
|
||||
}
|
||||
v := new(Claims)
|
||||
return v, json.Unmarshal([]byte(s.String), v)
|
||||
}
|
||||
*/
|
149
authority/mgmt/db.go
Normal file
149
authority/mgmt/db.go
Normal file
|
@ -0,0 +1,149 @@
|
|||
package mgmt
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
// ErrNotFound is an error that should be used by the authority.DB interface to
|
||||
// indicate that an entity does not exist.
|
||||
var ErrNotFound = errors.New("not found")
|
||||
|
||||
// DB is the DB interface expected by the step-ca ACME API.
|
||||
type DB interface {
|
||||
CreateProvisioner(ctx context.Context, prov *Provisioner) error
|
||||
GetProvisioner(ctx context.Context, id string) (*Provisioner, error)
|
||||
GetProvisioners(ctx context.Context) ([]*Provisioner, error)
|
||||
UpdateProvisioner(ctx context.Context, prov *Provisioner) error
|
||||
|
||||
CreateAdmin(ctx context.Context, admin *Admin) error
|
||||
GetAdmin(ctx context.Context, id string) error
|
||||
GetAdmins(ctx context.Context) ([]*Admin, error)
|
||||
UpdateAdmin(ctx context.Context, admin *Admin) error
|
||||
|
||||
CreateAuthConfig(ctx context.Context, ac *AuthConfig) error
|
||||
GetAuthConfig(ctx context.Context, id string) (*AuthConfig, error)
|
||||
UpdateAuthConfig(ctx context.Context, ac *AuthConfig) error
|
||||
}
|
||||
|
||||
// MockDB is an implementation of the DB interface that should only be used as
|
||||
// a mock in tests.
|
||||
type MockDB struct {
|
||||
MockCreateProvisioner func(ctx context.Context, prov *Provisioner) error
|
||||
MockGetProvisioner func(ctx context.Context, id string) (*Provisioner, error)
|
||||
MockGetProvisioners func(ctx context.Context) ([]*Provisioner, error)
|
||||
MockUpdateProvisioner func(ctx context.Context, prov *Provisioner) error
|
||||
|
||||
MockCreateAdmin func(ctx context.Context, adm *Admin) error
|
||||
MockGetAdmin func(ctx context.Context, id string) (*Admin, error)
|
||||
MockGetAdmins func(ctx context.Context) ([]*Admin, error)
|
||||
MockUpdateAdmin func(ctx context.Context, adm *Admin) error
|
||||
|
||||
MockCreateAuthConfig func(ctx context.Context, ac *AuthConfig) error
|
||||
MockGetAuthConfig func(ctx context.Context, id string) (*AuthConfig, error)
|
||||
MockUpdateAuthConfig func(ctx context.Context, ac *AuthConfig) error
|
||||
|
||||
MockError error
|
||||
MockRet1 interface{}
|
||||
}
|
||||
|
||||
// CreateProvisioner mock.
|
||||
func (m *MockDB) CreateProvisioner(ctx context.Context, prov *Provisioner) error {
|
||||
if m.MockCreateProvisioner != nil {
|
||||
return m.MockCreateProvisioner(ctx, prov)
|
||||
} else if m.MockError != nil {
|
||||
return m.MockError
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// GetProvisioner mock.
|
||||
func (m *MockDB) GetProvisioner(ctx context.Context, id string) (*Provisioner, error) {
|
||||
if m.MockGetProvisioner != nil {
|
||||
return m.MockGetProvisioner(ctx, id)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.(*Provisioner), m.MockError
|
||||
}
|
||||
|
||||
// GetProvisioners mock
|
||||
func (m *MockDB) GetProvisioners(ctx context.Context) ([]*Provisioner, error) {
|
||||
if m.MockGetProvisioners != nil {
|
||||
return m.MockGetProvisioners(ctx)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.([]*Provisioner), m.MockError
|
||||
}
|
||||
|
||||
// UpdateProvisioner mock
|
||||
func (m *MockDB) UpdateProvisioner(ctx context.Context, prov *Provisioner) error {
|
||||
if m.MockUpdateProvisioner != nil {
|
||||
return m.MockUpdateProvisioner(ctx, prov)
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// CreateAdmin mock
|
||||
func (m *MockDB) CreateAdmin(ctx context.Context, admin *Admin) error {
|
||||
if m.MockCreateAdmin != nil {
|
||||
return m.MockCreateAdmin(ctx, admin)
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// GetAdmin mock.
|
||||
func (m *MockDB) GetAdmin(ctx context.Context, id string) (*Admin, error) {
|
||||
if m.MockGetAdmin != nil {
|
||||
return m.MockGetAdmin(ctx, id)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.(*Admin), m.MockError
|
||||
}
|
||||
|
||||
// GetAdmins mock
|
||||
func (m *MockDB) GetAdmins(ctx context.Context) ([]*Admin, error) {
|
||||
if m.MockGetAdmins != nil {
|
||||
return m.MockGetAdmins(ctx)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.([]*Admin), m.MockError
|
||||
}
|
||||
|
||||
// UpdateAdmin mock
|
||||
func (m *MockDB) UpdateAdmin(ctx context.Context, adm *Admin) error {
|
||||
if m.UpdateAdmin != nil {
|
||||
return m.MockUpdateAdmin(ctx, adm)
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// CreateAuthConfig mock
|
||||
func (m *MockDB) CreateAuthConfig(ctx context.Context, admin *AuthConfig) error {
|
||||
if m.MockCreateAuthConfig != nil {
|
||||
return m.MockCreateAuthConfig(ctx, admin)
|
||||
}
|
||||
return m.MockError
|
||||
}
|
||||
|
||||
// GetAuthConfig mock.
|
||||
func (m *MockDB) GetAuthConfig(ctx context.Context, id string) (*AuthConfig, error) {
|
||||
if m.MockGetAuthConfig != nil {
|
||||
return m.MockGetAuthConfig(ctx, id)
|
||||
} else if m.MockError != nil {
|
||||
return nil, m.MockError
|
||||
}
|
||||
return m.MockRet1.(*AuthConfig), m.MockError
|
||||
}
|
||||
|
||||
// UpdateAuthConfig mock
|
||||
func (m *MockDB) UpdateAuthConfig(ctx context.Context, adm *AuthConfig) error {
|
||||
if m.UpdateAuthConfig != nil {
|
||||
return m.MockUpdateAuthConfig(ctx, adm)
|
||||
}
|
||||
return m.MockError
|
||||
}
|
163
authority/mgmt/db/nosql/admin.go
Normal file
163
authority/mgmt/db/nosql/admin.go
Normal file
|
@ -0,0 +1,163 @@
|
|||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/acme"
|
||||
"github.com/smallstep/certificates/authority/mgmt"
|
||||
"github.com/smallstep/nosql"
|
||||
)
|
||||
|
||||
// dbAdmin is the database representation of the Admin type.
|
||||
type dbAdmin struct {
|
||||
ID string `json:"id"`
|
||||
AuthorityID string `json:"authorityID"`
|
||||
Name string `json:"name"`
|
||||
Provisioner string `json:"provisioner"`
|
||||
IsSuperAdmin bool `json:"isSuperAdmin"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
DeletedAt time.Time `json:"deletedAt"`
|
||||
}
|
||||
|
||||
func (dbp *dbAdmin) clone() *dbAdmin {
|
||||
u := *dbp
|
||||
return &u
|
||||
}
|
||||
|
||||
func (db *DB) getDBAdminBytes(ctx context.Context, id string) ([]byte, error) {
|
||||
data, err := db.db.Get(authorityAdminsTable, []byte(id))
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, mgmt.NewError(mgmt.ErrorNotFoundType, "admin %s not found", id)
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrapf(err, "error loading admin %s", id)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (db *DB) getDBAdmin(ctx context.Context, id string) (*dbAdmin, error) {
|
||||
data, err := db.getDBAdminBytes(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dba, err := unmarshalDBAdmin(data, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dba.AuthorityID != db.authorityID {
|
||||
return nil, mgmt.NewError(mgmt.ErrorAuthorityMismatchType,
|
||||
"admin %s is not owned by authority %s", dba.ID, db.authorityID)
|
||||
}
|
||||
return dba, nil
|
||||
}
|
||||
|
||||
// GetAdmin retrieves and unmarshals a admin from the database.
|
||||
func (db *DB) GetAdmin(ctx context.Context, id string) (*mgmt.Admin, error) {
|
||||
data, err := db.getDBAdminBytes(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
adm, err := unmarshalAdmin(data, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if adm.Status == mgmt.StatusDeleted {
|
||||
return nil, mgmt.NewError(mgmt.ErrorDeletedType, "admin %s is deleted")
|
||||
}
|
||||
if adm.AuthorityID != db.authorityID {
|
||||
return nil, mgmt.NewError(mgmt.ErrorAuthorityMismatchType,
|
||||
"admin %s is not owned by authority %s", adm.ID, db.authorityID)
|
||||
}
|
||||
return adm, nil
|
||||
}
|
||||
|
||||
func unmarshalDBAdmin(data []byte, id string) (*dbAdmin, error) {
|
||||
var dba = new(dbAdmin)
|
||||
if err := json.Unmarshal(data, dba); err != nil {
|
||||
return nil, errors.Wrapf(err, "error unmarshaling admin %s into dbAdmin", id)
|
||||
}
|
||||
return dba, nil
|
||||
}
|
||||
|
||||
func unmarshalAdmin(data []byte, id string) (*mgmt.Admin, error) {
|
||||
var dba = new(dbAdmin)
|
||||
if err := json.Unmarshal(data, dba); err != nil {
|
||||
return nil, errors.Wrapf(err, "error unmarshaling admin %s into dbAdmin", id)
|
||||
}
|
||||
adm := &mgmt.Admin{
|
||||
ID: dba.ID,
|
||||
Name: dba.Name,
|
||||
Provisioner: dba.Provisioner,
|
||||
IsSuperAdmin: dba.IsSuperAdmin,
|
||||
}
|
||||
if !dba.DeletedAt.IsZero() {
|
||||
adm.Status = mgmt.StatusDeleted
|
||||
}
|
||||
return adm, nil
|
||||
}
|
||||
|
||||
// GetAdmins retrieves and unmarshals all active (not deleted) admins
|
||||
// from the database.
|
||||
// TODO should we be paginating?
|
||||
func (db *DB) GetAdmins(ctx context.Context, az *acme.Authorization) ([]*mgmt.Admin, error) {
|
||||
dbEntries, err := db.db.List(authorityAdminsTable)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error loading admins")
|
||||
}
|
||||
var admins []*mgmt.Admin
|
||||
for _, entry := range dbEntries {
|
||||
adm, err := unmarshalAdmin(entry.Value, string(entry.Key))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if adm.Status == mgmt.StatusDeleted {
|
||||
continue
|
||||
}
|
||||
if adm.AuthorityID != db.authorityID {
|
||||
continue
|
||||
}
|
||||
admins = append(admins, adm)
|
||||
}
|
||||
return admins, nil
|
||||
}
|
||||
|
||||
// CreateAdmin stores a new admin to the database.
|
||||
func (db *DB) CreateAdmin(ctx context.Context, adm *mgmt.Admin) error {
|
||||
var err error
|
||||
adm.ID, err = randID()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error generating random id for admin")
|
||||
}
|
||||
|
||||
dba := &dbAdmin{
|
||||
ID: adm.ID,
|
||||
AuthorityID: db.authorityID,
|
||||
Name: adm.Name,
|
||||
Provisioner: adm.Provisioner,
|
||||
IsSuperAdmin: adm.IsSuperAdmin,
|
||||
CreatedAt: clock.Now(),
|
||||
}
|
||||
|
||||
return db.save(ctx, dba.ID, dba, nil, "admin", authorityAdminsTable)
|
||||
}
|
||||
|
||||
// UpdateAdmin saves an updated admin to the database.
|
||||
func (db *DB) UpdateAdmin(ctx context.Context, adm *mgmt.Admin) error {
|
||||
old, err := db.getDBAdmin(ctx, adm.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nu := old.clone()
|
||||
|
||||
// If the admin was active but is now deleted ...
|
||||
if old.DeletedAt.IsZero() && adm.Status == mgmt.StatusDeleted {
|
||||
nu.DeletedAt = clock.Now()
|
||||
}
|
||||
nu.Provisioner = adm.Provisioner
|
||||
nu.IsSuperAdmin = adm.IsSuperAdmin
|
||||
|
||||
return db.save(ctx, old.ID, nu, old, "admin", authorityAdminsTable)
|
||||
}
|
113
authority/mgmt/db/nosql/authConfig.go
Normal file
113
authority/mgmt/db/nosql/authConfig.go
Normal file
|
@ -0,0 +1,113 @@
|
|||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/mgmt"
|
||||
"github.com/smallstep/nosql"
|
||||
)
|
||||
|
||||
type dbAuthConfig struct {
|
||||
ID string `json:"id"`
|
||||
ASN1DN *config.ASN1DN `json:"asn1dn"`
|
||||
Claims *mgmt.Claims `json:"claims"`
|
||||
DisableIssuedAtCheck bool `json:"disableIssuedAtCheck,omitempty"`
|
||||
Backdate string `json:"backdate,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
DeletedAt time.Time `json:"deletedAt"`
|
||||
}
|
||||
|
||||
func (dbp *dbAuthConfig) clone() *dbAuthConfig {
|
||||
u := *dbp
|
||||
return &u
|
||||
}
|
||||
|
||||
func (db *DB) getDBAuthConfigBytes(ctx context.Context, id string) ([]byte, error) {
|
||||
data, err := db.db.Get(authorityConfigsTable, []byte(id))
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, mgmt.NewError(mgmt.ErrorNotFoundType, "authConfig %s not found", id)
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrapf(err, "error loading authConfig %s", id)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (db *DB) getDBAuthConfig(ctx context.Context, id string) (*dbAuthConfig, error) {
|
||||
data, err := db.getDBAuthConfigBytes(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var dba = new(dbAuthConfig)
|
||||
if err = json.Unmarshal(data, dba); err != nil {
|
||||
return nil, errors.Wrapf(err, "error unmarshaling authority %s into dbAuthConfig", id)
|
||||
}
|
||||
|
||||
return dba, nil
|
||||
}
|
||||
|
||||
// GetAuthConfig retrieves an AuthConfig configuration from the DB.
|
||||
func (db *DB) GetAuthConfig(ctx context.Context, id string) (*mgmt.AuthConfig, error) {
|
||||
dba, err := db.getDBAuthConfig(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
provs, err := db.GetProvisioners(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &mgmt.AuthConfig{
|
||||
ID: dba.ID,
|
||||
Provisioners: provs,
|
||||
ASN1DN: dba.ASN1DN,
|
||||
Backdate: dba.Backdate,
|
||||
Claims: dba.Claims,
|
||||
DisableIssuedAtCheck: dba.DisableIssuedAtCheck,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// CreateAuthConfig stores a new provisioner to the database.
|
||||
func (db *DB) CreateAuthConfig(ctx context.Context, ac *mgmt.AuthConfig) error {
|
||||
var err error
|
||||
ac.ID, err = randID()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error generating random id for provisioner")
|
||||
}
|
||||
|
||||
dba := &dbAuthConfig{
|
||||
ID: ac.ID,
|
||||
ASN1DN: ac.ASN1DN,
|
||||
Claims: ac.Claims,
|
||||
DisableIssuedAtCheck: ac.DisableIssuedAtCheck,
|
||||
Backdate: ac.Backdate,
|
||||
CreatedAt: clock.Now(),
|
||||
}
|
||||
|
||||
return db.save(ctx, dba.ID, dba, nil, "authConfig", authorityConfigsTable)
|
||||
}
|
||||
|
||||
// UpdateAuthConfig saves an updated provisioner to the database.
|
||||
func (db *DB) UpdateAuthConfig(ctx context.Context, ac *mgmt.AuthConfig) error {
|
||||
old, err := db.getDBAuthConfig(ctx, ac.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nu := old.clone()
|
||||
|
||||
// If the authority was active but is now deleted ...
|
||||
if old.DeletedAt.IsZero() && ac.Status == mgmt.StatusDeleted {
|
||||
nu.DeletedAt = clock.Now()
|
||||
}
|
||||
nu.Claims = ac.Claims
|
||||
nu.DisableIssuedAtCheck = ac.DisableIssuedAtCheck
|
||||
nu.Backdate = ac.Backdate
|
||||
|
||||
return db.save(ctx, old.ID, nu, old, "authConfig", authorityProvisionersTable)
|
||||
}
|
91
authority/mgmt/db/nosql/nosql.go
Normal file
91
authority/mgmt/db/nosql/nosql.go
Normal file
|
@ -0,0 +1,91 @@
|
|||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
nosqlDB "github.com/smallstep/nosql/database"
|
||||
"go.step.sm/crypto/randutil"
|
||||
)
|
||||
|
||||
var (
|
||||
authorityAdminsTable = []byte("authority_admins")
|
||||
authorityConfigsTable = []byte("authority_configs")
|
||||
authorityProvisionersTable = []byte("authority_provisioners")
|
||||
)
|
||||
|
||||
// DB is a struct that implements the AcmeDB interface.
|
||||
type DB struct {
|
||||
db nosqlDB.DB
|
||||
authorityID string
|
||||
}
|
||||
|
||||
// New configures and returns a new Authority DB backend implemented using a nosql DB.
|
||||
func New(db nosqlDB.DB, authorityID string) (*DB, error) {
|
||||
tables := [][]byte{authorityAdminsTable, authorityConfigsTable, authorityProvisionersTable}
|
||||
for _, b := range tables {
|
||||
if err := db.CreateTable(b); err != nil {
|
||||
return nil, errors.Wrapf(err, "error creating table %s",
|
||||
string(b))
|
||||
}
|
||||
}
|
||||
return &DB{db, authorityID}, nil
|
||||
}
|
||||
|
||||
// save writes the new data to the database, overwriting the old data if it
|
||||
// existed.
|
||||
func (db *DB) save(ctx context.Context, id string, nu interface{}, old interface{}, typ string, table []byte) error {
|
||||
var (
|
||||
err error
|
||||
newB []byte
|
||||
)
|
||||
if nu == nil {
|
||||
newB = nil
|
||||
} else {
|
||||
newB, err = json.Marshal(nu)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error marshaling authority type: %s, value: %v", typ, nu)
|
||||
}
|
||||
}
|
||||
var oldB []byte
|
||||
if old == nil {
|
||||
oldB = nil
|
||||
} else {
|
||||
oldB, err = json.Marshal(old)
|
||||
if err != nil {
|
||||
return errors.Wrapf(err, "error marshaling acme type: %s, value: %v", typ, old)
|
||||
}
|
||||
}
|
||||
|
||||
_, swapped, err := db.db.CmpAndSwap(table, []byte(id), oldB, newB)
|
||||
switch {
|
||||
case err != nil:
|
||||
return errors.Wrapf(err, "error saving authority %s", typ)
|
||||
case !swapped:
|
||||
return errors.Errorf("error saving authority %s; changed since last read", typ)
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
var idLen = 32
|
||||
|
||||
func randID() (val string, err error) {
|
||||
val, err = randutil.Alphanumeric(idLen)
|
||||
if err != nil {
|
||||
return "", errors.Wrap(err, "error generating random alphanumeric ID")
|
||||
}
|
||||
return val, nil
|
||||
}
|
||||
|
||||
// Clock that returns time in UTC rounded to seconds.
|
||||
type Clock struct{}
|
||||
|
||||
// Now returns the UTC time rounded to seconds.
|
||||
func (c *Clock) Now() time.Time {
|
||||
return time.Now().UTC().Truncate(time.Second)
|
||||
}
|
||||
|
||||
var clock = new(Clock)
|
174
authority/mgmt/db/nosql/provisioner.go
Normal file
174
authority/mgmt/db/nosql/provisioner.go
Normal file
|
@ -0,0 +1,174 @@
|
|||
package nosql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/mgmt"
|
||||
"github.com/smallstep/nosql"
|
||||
)
|
||||
|
||||
// dbProvisioner is the database representation of a Provisioner type.
|
||||
type dbProvisioner struct {
|
||||
ID string `json:"id"`
|
||||
AuthorityID string `json:"authorityID"`
|
||||
Type string `json:"type"`
|
||||
Name string `json:"name"`
|
||||
Claims *mgmt.Claims `json:"claims"`
|
||||
Details interface{} `json:"details"`
|
||||
X509Template string `json:"x509Template"`
|
||||
SSHTemplate string `json:"sshTemplate"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
DeletedAt time.Time `json:"deletedAt"`
|
||||
}
|
||||
|
||||
func (dbp *dbProvisioner) clone() *dbProvisioner {
|
||||
u := *dbp
|
||||
return &u
|
||||
}
|
||||
|
||||
func (db *DB) getDBProvisionerBytes(ctx context.Context, id string) ([]byte, error) {
|
||||
data, err := db.db.Get(authorityProvisionersTable, []byte(id))
|
||||
if nosql.IsErrNotFound(err) {
|
||||
return nil, mgmt.NewError(mgmt.ErrorNotFoundType, "provisioner %s not found", id)
|
||||
} else if err != nil {
|
||||
return nil, errors.Wrapf(err, "error loading provisioner %s", id)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (db *DB) getDBProvisioner(ctx context.Context, id string) (*dbProvisioner, error) {
|
||||
data, err := db.getDBProvisionerBytes(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dbp, err := unmarshalDBProvisioner(data, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dbp.AuthorityID != db.authorityID {
|
||||
return nil, mgmt.NewError(mgmt.ErrorAuthorityMismatchType,
|
||||
"provisioner %s is not owned by authority %s", dbp.ID, db.authorityID)
|
||||
}
|
||||
return dbp, nil
|
||||
}
|
||||
|
||||
// GetProvisioner retrieves and unmarshals a provisioner from the database.
|
||||
func (db *DB) GetProvisioner(ctx context.Context, id string) (*mgmt.Provisioner, error) {
|
||||
data, err := db.getDBProvisionerBytes(ctx, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prov, err := unmarshalProvisioner(data, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if prov.Status == mgmt.StatusDeleted {
|
||||
return nil, mgmt.NewError(mgmt.ErrorDeletedType, "provisioner %s is deleted", prov.ID)
|
||||
}
|
||||
if prov.AuthorityID != db.authorityID {
|
||||
return nil, mgmt.NewError(mgmt.ErrorAuthorityMismatchType,
|
||||
"provisioner %s is not owned by authority %s", prov.ID, db.authorityID)
|
||||
}
|
||||
return prov, nil
|
||||
}
|
||||
|
||||
func unmarshalDBProvisioner(data []byte, id string) (*dbProvisioner, error) {
|
||||
var dbp = new(dbProvisioner)
|
||||
if err := json.Unmarshal(data, dbp); err != nil {
|
||||
return nil, errors.Wrapf(err, "error unmarshaling provisioner %s into dbProvisioner", id)
|
||||
}
|
||||
return dbp, nil
|
||||
}
|
||||
|
||||
func unmarshalProvisioner(data []byte, id string) (*mgmt.Provisioner, error) {
|
||||
dbp, err := unmarshalDBProvisioner(data, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prov := &mgmt.Provisioner{
|
||||
ID: dbp.ID,
|
||||
Type: dbp.Type,
|
||||
Name: dbp.Name,
|
||||
Claims: dbp.Claims,
|
||||
X509Template: dbp.X509Template,
|
||||
SSHTemplate: dbp.SSHTemplate,
|
||||
}
|
||||
if !dbp.DeletedAt.IsZero() {
|
||||
prov.Status = mgmt.StatusDeleted
|
||||
}
|
||||
return prov, nil
|
||||
}
|
||||
|
||||
// GetProvisioners retrieves and unmarshals all active (not deleted) provisioners
|
||||
// from the database.
|
||||
// TODO should we be paginating?
|
||||
func (db *DB) GetProvisioners(ctx context.Context) ([]*mgmt.Provisioner, error) {
|
||||
dbEntries, err := db.db.List(authorityProvisionersTable)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error loading provisioners")
|
||||
}
|
||||
var provs []*mgmt.Provisioner
|
||||
for _, entry := range dbEntries {
|
||||
prov, err := unmarshalProvisioner(entry.Value, string(entry.Key))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if prov.Status == mgmt.StatusDeleted {
|
||||
continue
|
||||
}
|
||||
if prov.AuthorityID != db.authorityID {
|
||||
continue
|
||||
}
|
||||
provs = append(provs, prov)
|
||||
}
|
||||
return provs, nil
|
||||
}
|
||||
|
||||
// CreateProvisioner stores a new provisioner to the database.
|
||||
func (db *DB) CreateProvisioner(ctx context.Context, prov *mgmt.Provisioner) error {
|
||||
var err error
|
||||
prov.ID, err = randID()
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error generating random id for provisioner")
|
||||
}
|
||||
|
||||
dbp := &dbProvisioner{
|
||||
ID: prov.ID,
|
||||
AuthorityID: db.authorityID,
|
||||
Type: prov.Type,
|
||||
Name: prov.Name,
|
||||
Claims: prov.Claims,
|
||||
Details: prov.Details,
|
||||
X509Template: prov.X509Template,
|
||||
SSHTemplate: prov.SSHTemplate,
|
||||
CreatedAt: clock.Now(),
|
||||
}
|
||||
|
||||
return db.save(ctx, dbp.ID, dbp, nil, "provisioner", authorityProvisionersTable)
|
||||
}
|
||||
|
||||
// UpdateProvisioner saves an updated provisioner to the database.
|
||||
func (db *DB) UpdateProvisioner(ctx context.Context, prov *mgmt.Provisioner) error {
|
||||
old, err := db.getDBProvisioner(ctx, prov.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
nu := old.clone()
|
||||
|
||||
// If the provisioner was active but is now deleted ...
|
||||
if old.DeletedAt.IsZero() && prov.Status == mgmt.StatusDeleted {
|
||||
nu.DeletedAt = clock.Now()
|
||||
}
|
||||
nu.Claims = prov.Claims
|
||||
nu.Details = prov.Details
|
||||
nu.X509Template = prov.X509Template
|
||||
nu.SSHTemplate = prov.SSHTemplate
|
||||
|
||||
return db.save(ctx, old.ID, nu, old, "provisioner", authorityProvisionersTable)
|
||||
}
|
191
authority/mgmt/errors.go
Normal file
191
authority/mgmt/errors.go
Normal file
|
@ -0,0 +1,191 @@
|
|||
package mgmt
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
)
|
||||
|
||||
// ProblemType is the type of the ACME problem.
|
||||
type ProblemType int
|
||||
|
||||
const (
|
||||
// ErrorNotFoundType resource not found.
|
||||
ErrorNotFoundType ProblemType = iota
|
||||
// ErrorAuthorityMismatchType resource Authority ID does not match the
|
||||
// context Authority ID.
|
||||
ErrorAuthorityMismatchType
|
||||
// ErrorDeletedType resource has been deleted.
|
||||
ErrorDeletedType
|
||||
// ErrorServerInternalType internal server error.
|
||||
ErrorServerInternalType
|
||||
)
|
||||
|
||||
// String returns the string representation of the acme problem type,
|
||||
// fulfilling the Stringer interface.
|
||||
func (ap ProblemType) String() string {
|
||||
switch ap {
|
||||
case ErrorNotFoundType:
|
||||
return "notFound"
|
||||
case ErrorAuthorityMismatchType:
|
||||
return "authorityMismatch"
|
||||
case ErrorDeletedType:
|
||||
return "deleted"
|
||||
case ErrorServerInternalType:
|
||||
return "internalServerError"
|
||||
default:
|
||||
return fmt.Sprintf("unsupported error type '%d'", int(ap))
|
||||
}
|
||||
}
|
||||
|
||||
type errorMetadata struct {
|
||||
details string
|
||||
status int
|
||||
typ string
|
||||
String string
|
||||
}
|
||||
|
||||
var (
|
||||
errorServerInternalMetadata = errorMetadata{
|
||||
typ: ErrorServerInternalType.String(),
|
||||
details: "the server experienced an internal error",
|
||||
status: 500,
|
||||
}
|
||||
errorMap = map[ProblemType]errorMetadata{
|
||||
ErrorNotFoundType: {
|
||||
typ: ErrorNotFoundType.String(),
|
||||
details: "resource not found",
|
||||
status: 400,
|
||||
},
|
||||
ErrorAuthorityMismatchType: {
|
||||
typ: ErrorAuthorityMismatchType.String(),
|
||||
details: "resource not owned by authority",
|
||||
status: 401,
|
||||
},
|
||||
ErrorDeletedType: {
|
||||
typ: ErrorNotFoundType.String(),
|
||||
details: "resource is deleted",
|
||||
status: 403,
|
||||
},
|
||||
ErrorServerInternalType: errorServerInternalMetadata,
|
||||
}
|
||||
)
|
||||
|
||||
// Error represents an ACME
|
||||
type Error struct {
|
||||
Type string `json:"type"`
|
||||
Detail string `json:"detail"`
|
||||
Subproblems []interface{} `json:"subproblems,omitempty"`
|
||||
Identifier interface{} `json:"identifier,omitempty"`
|
||||
Err error `json:"-"`
|
||||
Status int `json:"-"`
|
||||
}
|
||||
|
||||
// NewError creates a new Error type.
|
||||
func NewError(pt ProblemType, msg string, args ...interface{}) *Error {
|
||||
return newError(pt, errors.Errorf(msg, args...))
|
||||
}
|
||||
|
||||
func newError(pt ProblemType, err error) *Error {
|
||||
meta, ok := errorMap[pt]
|
||||
if !ok {
|
||||
meta = errorServerInternalMetadata
|
||||
return &Error{
|
||||
Type: meta.typ,
|
||||
Detail: meta.details,
|
||||
Status: meta.status,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
return &Error{
|
||||
Type: meta.typ,
|
||||
Detail: meta.details,
|
||||
Status: meta.status,
|
||||
Err: err,
|
||||
}
|
||||
}
|
||||
|
||||
// NewErrorISE creates a new ErrorServerInternalType Error.
|
||||
func NewErrorISE(msg string, args ...interface{}) *Error {
|
||||
return NewError(ErrorServerInternalType, msg, args...)
|
||||
}
|
||||
|
||||
// WrapError attempts to wrap the internal error.
|
||||
func WrapError(typ ProblemType, err error, msg string, args ...interface{}) *Error {
|
||||
switch e := err.(type) {
|
||||
case nil:
|
||||
return nil
|
||||
case *Error:
|
||||
if e.Err == nil {
|
||||
e.Err = errors.Errorf(msg+"; "+e.Detail, args...)
|
||||
} else {
|
||||
e.Err = errors.Wrapf(e.Err, msg, args...)
|
||||
}
|
||||
return e
|
||||
default:
|
||||
return newError(typ, errors.Wrapf(err, msg, args...))
|
||||
}
|
||||
}
|
||||
|
||||
// WrapErrorISE shortcut to wrap an internal server error type.
|
||||
func WrapErrorISE(err error, msg string, args ...interface{}) *Error {
|
||||
return WrapError(ErrorServerInternalType, err, msg, args...)
|
||||
}
|
||||
|
||||
// StatusCode returns the status code and implements the StatusCoder interface.
|
||||
func (e *Error) StatusCode() int {
|
||||
return e.Status
|
||||
}
|
||||
|
||||
// Error allows AError to implement the error interface.
|
||||
func (e *Error) Error() string {
|
||||
return e.Detail
|
||||
}
|
||||
|
||||
// Cause returns the internal error and implements the Causer interface.
|
||||
func (e *Error) Cause() error {
|
||||
if e.Err == nil {
|
||||
return errors.New(e.Detail)
|
||||
}
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// ToLog implements the EnableLogger interface.
|
||||
func (e *Error) ToLog() (interface{}, error) {
|
||||
b, err := json.Marshal(e)
|
||||
if err != nil {
|
||||
return nil, WrapErrorISE(err, "error marshaling authority.Error for logging")
|
||||
}
|
||||
return string(b), nil
|
||||
}
|
||||
|
||||
// WriteError writes to w a JSON representation of the given error.
|
||||
func WriteError(w http.ResponseWriter, err *Error) {
|
||||
w.Header().Set("Content-Type", "application/problem+json")
|
||||
w.WriteHeader(err.StatusCode())
|
||||
|
||||
// Write errors in the response writer
|
||||
if rl, ok := w.(logging.ResponseLogger); ok {
|
||||
rl.WithFields(map[string]interface{}{
|
||||
"error": err.Err,
|
||||
})
|
||||
if os.Getenv("STEPDEBUG") == "1" {
|
||||
if e, ok := err.Err.(errs.StackTracer); ok {
|
||||
rl.WithFields(map[string]interface{}{
|
||||
"stack-trace": fmt.Sprintf("%+v", e),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := json.NewEncoder(w).Encode(err); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import (
|
|||
"encoding/pem"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/cas"
|
||||
casapi "github.com/smallstep/certificates/cas/apiv1"
|
||||
|
@ -20,7 +21,7 @@ type Option func(*Authority) error
|
|||
|
||||
// WithConfig replaces the current config with the given one. No validation is
|
||||
// performed in the given value.
|
||||
func WithConfig(config *Config) Option {
|
||||
func WithConfig(config *config.Config) Option {
|
||||
return func(a *Authority) error {
|
||||
a.config = config
|
||||
return nil
|
||||
|
@ -31,7 +32,7 @@ func WithConfig(config *Config) Option {
|
|||
// the current one. No validation is performed in the given configuration.
|
||||
func WithConfigFile(filename string) Option {
|
||||
return func(a *Authority) (err error) {
|
||||
a.config, err = LoadConfiguration(filename)
|
||||
a.config, err = config.LoadConfiguration(filename)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +57,7 @@ func WithGetIdentityFunc(fn func(ctx context.Context, p provisioner.Interface, e
|
|||
|
||||
// WithSSHBastionFunc sets a custom function to get the bastion for a
|
||||
// given user-host pair.
|
||||
func WithSSHBastionFunc(fn func(ctx context.Context, user, host string) (*Bastion, error)) Option {
|
||||
func WithSSHBastionFunc(fn func(ctx context.Context, user, host string) (*config.Bastion, error)) Option {
|
||||
return func(a *Authority) error {
|
||||
a.sshBastionFunc = fn
|
||||
return nil
|
||||
|
@ -65,7 +66,7 @@ func WithSSHBastionFunc(fn func(ctx context.Context, user, host string) (*Bastio
|
|||
|
||||
// WithSSHGetHosts sets a custom function to get the bastion for a
|
||||
// given user-host pair.
|
||||
func WithSSHGetHosts(fn func(ctx context.Context, cert *x509.Certificate) ([]Host, error)) Option {
|
||||
func WithSSHGetHosts(fn func(ctx context.Context, cert *x509.Certificate) ([]config.Host, error)) Option {
|
||||
return func(a *Authority) error {
|
||||
a.sshGetHostsFunc = fn
|
||||
return nil
|
||||
|
|
|
@ -7,6 +7,27 @@ import (
|
|||
"golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
type _Claims struct {
|
||||
*X509Claims `json:"x509Claims"`
|
||||
*SSHClaims `json:"sshClaims"`
|
||||
DisableRenewal *bool `json:"disableRenewal"`
|
||||
}
|
||||
|
||||
type X509Claims struct {
|
||||
Durations *Durations `json:"durations"`
|
||||
}
|
||||
|
||||
type SSHClaims struct {
|
||||
UserDuration *Durations `json:"userDurations"`
|
||||
HostDuration *Durations `json:"hostDuration"`
|
||||
}
|
||||
|
||||
type Durations struct {
|
||||
Min string `json:"min"`
|
||||
Max string `json:"max"`
|
||||
Default string `json:"default"`
|
||||
}
|
||||
|
||||
// Claims so that individual provisioners can override global claims.
|
||||
type Claims struct {
|
||||
// TLS CA properties
|
||||
|
|
104
authority/ssh.go
104
authority/ssh.go
|
@ -10,11 +10,11 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/certificates/errs"
|
||||
"github.com/smallstep/certificates/templates"
|
||||
"go.step.sm/crypto/jose"
|
||||
"go.step.sm/crypto/randutil"
|
||||
"go.step.sm/crypto/sshutil"
|
||||
"golang.org/x/crypto/ssh"
|
||||
|
@ -32,103 +32,17 @@ const (
|
|||
SSHAddUserCommand = "sudo useradd -m <principal>; nc -q0 localhost 22"
|
||||
)
|
||||
|
||||
// SSHConfig contains the user and host keys.
|
||||
type SSHConfig struct {
|
||||
HostKey string `json:"hostKey"`
|
||||
UserKey string `json:"userKey"`
|
||||
Keys []*SSHPublicKey `json:"keys,omitempty"`
|
||||
AddUserPrincipal string `json:"addUserPrincipal,omitempty"`
|
||||
AddUserCommand string `json:"addUserCommand,omitempty"`
|
||||
Bastion *Bastion `json:"bastion,omitempty"`
|
||||
}
|
||||
|
||||
// Bastion contains the custom properties used on bastion.
|
||||
type Bastion struct {
|
||||
Hostname string `json:"hostname"`
|
||||
User string `json:"user,omitempty"`
|
||||
Port string `json:"port,omitempty"`
|
||||
Command string `json:"cmd,omitempty"`
|
||||
Flags string `json:"flags,omitempty"`
|
||||
}
|
||||
|
||||
// HostTag are tagged with k,v pairs. These tags are how a user is ultimately
|
||||
// associated with a host.
|
||||
type HostTag struct {
|
||||
ID string
|
||||
Name string
|
||||
Value string
|
||||
}
|
||||
|
||||
// Host defines expected attributes for an ssh host.
|
||||
type Host struct {
|
||||
HostID string `json:"hid"`
|
||||
HostTags []HostTag `json:"host_tags"`
|
||||
Hostname string `json:"hostname"`
|
||||
}
|
||||
|
||||
// Validate checks the fields in SSHConfig.
|
||||
func (c *SSHConfig) Validate() error {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
for _, k := range c.Keys {
|
||||
if err := k.Validate(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SSHPublicKey contains a public key used by federated CAs to keep old signing
|
||||
// keys for this ca.
|
||||
type SSHPublicKey struct {
|
||||
Type string `json:"type"`
|
||||
Federated bool `json:"federated"`
|
||||
Key jose.JSONWebKey `json:"key"`
|
||||
publicKey ssh.PublicKey
|
||||
}
|
||||
|
||||
// Validate checks the fields in SSHPublicKey.
|
||||
func (k *SSHPublicKey) Validate() error {
|
||||
switch {
|
||||
case k.Type == "":
|
||||
return errors.New("type cannot be empty")
|
||||
case k.Type != provisioner.SSHHostCert && k.Type != provisioner.SSHUserCert:
|
||||
return errors.Errorf("invalid type %s, it must be user or host", k.Type)
|
||||
case !k.Key.IsPublic():
|
||||
return errors.New("invalid key type, it must be a public key")
|
||||
}
|
||||
|
||||
key, err := ssh.NewPublicKey(k.Key.Key)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error creating ssh key")
|
||||
}
|
||||
k.publicKey = key
|
||||
return nil
|
||||
}
|
||||
|
||||
// PublicKey returns the ssh public key.
|
||||
func (k *SSHPublicKey) PublicKey() ssh.PublicKey {
|
||||
return k.publicKey
|
||||
}
|
||||
|
||||
// SSHKeys represents the SSH User and Host public keys.
|
||||
type SSHKeys struct {
|
||||
UserKeys []ssh.PublicKey
|
||||
HostKeys []ssh.PublicKey
|
||||
}
|
||||
|
||||
// GetSSHRoots returns the SSH User and Host public keys.
|
||||
func (a *Authority) GetSSHRoots(context.Context) (*SSHKeys, error) {
|
||||
return &SSHKeys{
|
||||
func (a *Authority) GetSSHRoots(context.Context) (*config.SSHKeys, error) {
|
||||
return &config.SSHKeys{
|
||||
HostKeys: a.sshCAHostCerts,
|
||||
UserKeys: a.sshCAUserCerts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetSSHFederation returns the public keys for federated SSH signers.
|
||||
func (a *Authority) GetSSHFederation(context.Context) (*SSHKeys, error) {
|
||||
return &SSHKeys{
|
||||
func (a *Authority) GetSSHFederation(context.Context) (*config.SSHKeys, error) {
|
||||
return &config.SSHKeys{
|
||||
HostKeys: a.sshCAHostFederatedCerts,
|
||||
UserKeys: a.sshCAUserFederatedCerts,
|
||||
}, nil
|
||||
|
@ -194,7 +108,7 @@ func (a *Authority) GetSSHConfig(ctx context.Context, typ string, data map[strin
|
|||
|
||||
// GetSSHBastion returns the bastion configuration, for the given pair user,
|
||||
// hostname.
|
||||
func (a *Authority) GetSSHBastion(ctx context.Context, user string, hostname string) (*Bastion, error) {
|
||||
func (a *Authority) GetSSHBastion(ctx context.Context, user string, hostname string) (*config.Bastion, error) {
|
||||
if a.sshBastionFunc != nil {
|
||||
bs, err := a.sshBastionFunc(ctx, user, hostname)
|
||||
return bs, errs.Wrap(http.StatusInternalServerError, err, "authority.GetSSHBastion")
|
||||
|
@ -568,7 +482,7 @@ func (a *Authority) CheckSSHHost(ctx context.Context, principal string, token st
|
|||
}
|
||||
|
||||
// GetSSHHosts returns a list of valid host principals.
|
||||
func (a *Authority) GetSSHHosts(ctx context.Context, cert *x509.Certificate) ([]Host, error) {
|
||||
func (a *Authority) GetSSHHosts(ctx context.Context, cert *x509.Certificate) ([]config.Host, error) {
|
||||
if a.sshGetHostsFunc != nil {
|
||||
hosts, err := a.sshGetHostsFunc(ctx, cert)
|
||||
return hosts, errs.Wrap(http.StatusInternalServerError, err, "getSSHHosts")
|
||||
|
@ -578,9 +492,9 @@ func (a *Authority) GetSSHHosts(ctx context.Context, cert *x509.Certificate) ([]
|
|||
return nil, errs.Wrap(http.StatusInternalServerError, err, "getSSHHosts")
|
||||
}
|
||||
|
||||
hosts := make([]Host, len(hostnames))
|
||||
hosts := make([]config.Host, len(hostnames))
|
||||
for i, hn := range hostnames {
|
||||
hosts[i] = Host{Hostname: hn}
|
||||
hosts[i] = config.Host{Hostname: hn}
|
||||
}
|
||||
return hosts, nil
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
casapi "github.com/smallstep/certificates/cas/apiv1"
|
||||
"github.com/smallstep/certificates/db"
|
||||
|
@ -23,14 +24,14 @@ import (
|
|||
)
|
||||
|
||||
// GetTLSOptions returns the tls options configured.
|
||||
func (a *Authority) GetTLSOptions() *TLSOptions {
|
||||
func (a *Authority) GetTLSOptions() *config.TLSOptions {
|
||||
return a.config.TLS
|
||||
}
|
||||
|
||||
var oidAuthorityKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 35}
|
||||
var oidSubjectKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 14}
|
||||
|
||||
func withDefaultASN1DN(def *ASN1DN) provisioner.CertificateModifierFunc {
|
||||
func withDefaultASN1DN(def *config.ASN1DN) provisioner.CertificateModifierFunc {
|
||||
return func(crt *x509.Certificate, opts provisioner.SignOptions) error {
|
||||
if def == nil {
|
||||
return errors.New("default ASN1DN template cannot be nil")
|
||||
|
|
|
@ -39,6 +39,53 @@ func Bootstrap(token string) (*Client, error) {
|
|||
return NewClient(claims.Audience[0], WithRootSHA256(claims.SHA))
|
||||
}
|
||||
|
||||
// BootstrapClient is a helper function that using the given bootstrap token
|
||||
// return an http.Client configured with a Transport prepared to do TLS
|
||||
// connections using the client certificate returned by the certificate
|
||||
// authority. By default the server will kick off a routine that will renew the
|
||||
// certificate after 2/3rd of the certificate's lifetime has expired.
|
||||
//
|
||||
// Usage:
|
||||
// // Default example with certificate rotation.
|
||||
// client, err := ca.BootstrapClient(ctx.Background(), token)
|
||||
//
|
||||
// // Example canceling automatic certificate rotation.
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// defer cancel()
|
||||
// client, err := ca.BootstrapClient(ctx, token)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// resp, err := client.Get("https://internal.smallstep.com")
|
||||
func BootstrapClient(ctx context.Context, token string, options ...TLSOption) (*http.Client, error) {
|
||||
client, err := Bootstrap(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, pk, err := CreateSignRequest(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sign, err := client.Sign(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure the tlsConfig have all supported roots on RootCAs
|
||||
options = append(options, AddRootsToRootCAs())
|
||||
|
||||
transport, err := client.Transport(ctx, sign, pk, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BootstrapServer is a helper function that using the given token returns the
|
||||
// given http.Server configured with a TLS certificate signed by the Certificate
|
||||
// Authority. By default the server will kick off a routine that will renew the
|
||||
|
@ -100,53 +147,6 @@ func BootstrapServer(ctx context.Context, token string, base *http.Server, optio
|
|||
return base, nil
|
||||
}
|
||||
|
||||
// BootstrapClient is a helper function that using the given bootstrap token
|
||||
// return an http.Client configured with a Transport prepared to do TLS
|
||||
// connections using the client certificate returned by the certificate
|
||||
// authority. By default the server will kick off a routine that will renew the
|
||||
// certificate after 2/3rd of the certificate's lifetime has expired.
|
||||
//
|
||||
// Usage:
|
||||
// // Default example with certificate rotation.
|
||||
// client, err := ca.BootstrapClient(ctx.Background(), token)
|
||||
//
|
||||
// // Example canceling automatic certificate rotation.
|
||||
// ctx, cancel := context.WithCancel(context.Background())
|
||||
// defer cancel()
|
||||
// client, err := ca.BootstrapClient(ctx, token)
|
||||
// if err != nil {
|
||||
// return err
|
||||
// }
|
||||
// resp, err := client.Get("https://internal.smallstep.com")
|
||||
func BootstrapClient(ctx context.Context, token string, options ...TLSOption) (*http.Client, error) {
|
||||
client, err := Bootstrap(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
req, pk, err := CreateSignRequest(token)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sign, err := client.Sign(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Make sure the tlsConfig have all supported roots on RootCAs
|
||||
options = append(options, AddRootsToRootCAs())
|
||||
|
||||
transport, err := client.Transport(ctx, sign, pk, options...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &http.Client{
|
||||
Transport: transport,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// BootstrapListener is a helper function that using the given token returns a
|
||||
// TLS listener which accepts connections from an inner listener and wraps each
|
||||
// connection with Server.
|
||||
|
|
12
ca/ca.go
12
ca/ca.go
|
@ -16,6 +16,8 @@ import (
|
|||
acmeNoSQL "github.com/smallstep/certificates/acme/db/nosql"
|
||||
"github.com/smallstep/certificates/api"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/mgmt"
|
||||
"github.com/smallstep/certificates/db"
|
||||
"github.com/smallstep/certificates/logging"
|
||||
"github.com/smallstep/certificates/monitoring"
|
||||
|
@ -74,14 +76,14 @@ func WithDatabase(db db.AuthDB) Option {
|
|||
// the HTTP server, set ups the middlewares and the HTTP handlers.
|
||||
type CA struct {
|
||||
auth *authority.Authority
|
||||
config *authority.Config
|
||||
config *config.Config
|
||||
srv *server.Server
|
||||
opts *options
|
||||
renewer *TLSRenewer
|
||||
}
|
||||
|
||||
// New creates and initializes the CA with the given configuration and options.
|
||||
func New(config *authority.Config, opts ...Option) (*CA, error) {
|
||||
func New(config *config.Config, opts ...Option) (*CA, error) {
|
||||
ca := &CA{
|
||||
config: config,
|
||||
opts: new(options),
|
||||
|
@ -91,7 +93,7 @@ func New(config *authority.Config, opts ...Option) (*CA, error) {
|
|||
}
|
||||
|
||||
// Init initializes the CA with the given configuration.
|
||||
func (ca *CA) Init(config *authority.Config) (*CA, error) {
|
||||
func (ca *CA) Init(config *config.Config) (*CA, error) {
|
||||
// Intermediate Password.
|
||||
if len(ca.opts.password) > 0 {
|
||||
ca.config.Password = string(ca.opts.password)
|
||||
|
@ -146,7 +148,7 @@ func (ca *CA) Init(config *authority.Config) (*CA, error) {
|
|||
if config.DB == nil {
|
||||
acmeDB = nil
|
||||
} else {
|
||||
acmeDB, err = acmeNoSQL.New(auth.GetDatabase().(nosql.DB))
|
||||
acmeDB, err = acmeNoSQL.New(auth.GetDatabase().(nosql.DB), mgmt.DefaultAuthorityID)
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error configuring ACME DB interface")
|
||||
}
|
||||
|
@ -218,7 +220,7 @@ func (ca *CA) Stop() error {
|
|||
// Reload reloads the configuration of the CA and calls to the server Reload
|
||||
// method.
|
||||
func (ca *CA) Reload() error {
|
||||
config, err := authority.LoadConfiguration(ca.opts.configFile)
|
||||
config, err := config.LoadConfiguration(ca.opts.configFile)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "error reloading ca configuration")
|
||||
}
|
||||
|
|
107
ca/mgmtClient.go
Normal file
107
ca/mgmtClient.go
Normal file
|
@ -0,0 +1,107 @@
|
|||
package ca
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority/mgmt"
|
||||
)
|
||||
|
||||
// MgmtClient implements an HTTP client for the CA server.
|
||||
type MgmtClient struct {
|
||||
client *uaClient
|
||||
endpoint *url.URL
|
||||
retryFunc RetryFunc
|
||||
opts []ClientOption
|
||||
}
|
||||
|
||||
// NewMgmtClient creates a new MgmtClient with the given endpoint and options.
|
||||
func NewMgmtClient(endpoint string, opts ...ClientOption) (*MgmtClient, error) {
|
||||
u, err := parseEndpoint(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Retrieve transport from options.
|
||||
o := new(clientOptions)
|
||||
if err := o.apply(opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tr, err := o.getTransport(endpoint)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &MgmtClient{
|
||||
client: newClient(tr),
|
||||
endpoint: u,
|
||||
retryFunc: o.retryFunc,
|
||||
opts: opts,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (c *MgmtClient) retryOnError(r *http.Response) bool {
|
||||
if c.retryFunc != nil {
|
||||
if c.retryFunc(r.StatusCode) {
|
||||
o := new(clientOptions)
|
||||
if err := o.apply(c.opts); err != nil {
|
||||
return false
|
||||
}
|
||||
tr, err := o.getTransport(c.endpoint.String())
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
r.Body.Close()
|
||||
c.client.SetTransport(tr)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetAdmin performs the GET /config/admin/{id} request to the CA.
|
||||
func (c *MgmtClient) GetAdmin(id string) (*mgmt.Admin, error) {
|
||||
var retried bool
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: path.Join("/config/admin", id)})
|
||||
retry:
|
||||
resp, err := c.client.Get(u.String())
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "client GET %s failed", u)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
return nil, readError(resp.Body)
|
||||
}
|
||||
var adm = new(mgmt.Admin)
|
||||
if err := readJSON(resp.Body, adm); err != nil {
|
||||
return nil, errors.Wrapf(err, "error reading %s", u)
|
||||
}
|
||||
return adm, nil
|
||||
}
|
||||
|
||||
// GetAdmins performs the GET /config/admins request to the CA.
|
||||
func (c *MgmtClient) GetAdmins() ([]*mgmt.Admin, error) {
|
||||
var retried bool
|
||||
u := c.endpoint.ResolveReference(&url.URL{Path: "/config/admins"})
|
||||
retry:
|
||||
resp, err := c.client.Get(u.String())
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "client GET %s failed", u)
|
||||
}
|
||||
if resp.StatusCode >= 400 {
|
||||
if !retried && c.retryOnError(resp) {
|
||||
retried = true
|
||||
goto retry
|
||||
}
|
||||
return nil, readError(resp.Body)
|
||||
}
|
||||
var admins = new([]*mgmt.Admin)
|
||||
if err := readJSON(resp.Body, admins); err != nil {
|
||||
return nil, errors.Wrapf(err, "error reading %s", u)
|
||||
}
|
||||
return *admins, nil
|
||||
}
|
|
@ -103,7 +103,6 @@ func (c *Client) getClientTLSConfig(ctx context.Context, sign *api.SignResponse,
|
|||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Update renew function with transport
|
||||
tr := getDefaultTransport(tlsConfig)
|
||||
// Use mutable tls.Config on renew
|
||||
tr.DialTLS = c.buildDialTLS(tlsCtx) // nolint:staticcheck
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
"unicode"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/ca"
|
||||
"github.com/urfave/cli"
|
||||
"go.step.sm/cli-utils/errs"
|
||||
|
@ -56,7 +56,7 @@ func appAction(ctx *cli.Context) error {
|
|||
}
|
||||
|
||||
configFile := ctx.Args().Get(0)
|
||||
config, err := authority.LoadConfiguration(configFile)
|
||||
config, err := config.LoadConfiguration(configFile)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
"github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/ca"
|
||||
"github.com/smallstep/certificates/cas/apiv1"
|
||||
"github.com/smallstep/certificates/pki"
|
||||
|
@ -162,7 +162,7 @@ func onboardAction(ctx *cli.Context) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func onboardPKI(config onboardingConfiguration) (*authority.Config, string, error) {
|
||||
func onboardPKI(config onboardingConfiguration) (*config.Config, string, error) {
|
||||
p, err := pki.New(apiv1.Options{
|
||||
Type: apiv1.SoftCAS,
|
||||
IsCreator: true,
|
||||
|
|
26
pki/pki.go
26
pki/pki.go
|
@ -19,7 +19,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"github.com/smallstep/certificates/authority"
|
||||
authconfig "github.com/smallstep/certificates/authority/config"
|
||||
"github.com/smallstep/certificates/authority/provisioner"
|
||||
"github.com/smallstep/certificates/ca"
|
||||
"github.com/smallstep/certificates/cas"
|
||||
|
@ -481,12 +481,12 @@ type caDefaults struct {
|
|||
}
|
||||
|
||||
// Option is the type for modifiers over the auth config object.
|
||||
type Option func(c *authority.Config) error
|
||||
type Option func(c *authconfig.Config) error
|
||||
|
||||
// WithDefaultDB is a configuration modifier that adds a default DB stanza to
|
||||
// the authority config.
|
||||
func WithDefaultDB() Option {
|
||||
return func(c *authority.Config) error {
|
||||
return func(c *authconfig.Config) error {
|
||||
c.DB = &db.Config{
|
||||
Type: "badger",
|
||||
DataSource: GetDBPath(),
|
||||
|
@ -498,14 +498,14 @@ func WithDefaultDB() Option {
|
|||
// WithoutDB is a configuration modifier that adds a default DB stanza to
|
||||
// the authority config.
|
||||
func WithoutDB() Option {
|
||||
return func(c *authority.Config) error {
|
||||
return func(c *authconfig.Config) error {
|
||||
c.DB = nil
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateConfig returns the step certificates configuration.
|
||||
func (p *PKI) GenerateConfig(opt ...Option) (*authority.Config, error) {
|
||||
func (p *PKI) GenerateConfig(opt ...Option) (*authconfig.Config, error) {
|
||||
key, err := p.ottPrivateKey.CompactSerialize()
|
||||
if err != nil {
|
||||
return nil, errors.Wrap(err, "error serializing private key")
|
||||
|
@ -523,7 +523,7 @@ func (p *PKI) GenerateConfig(opt ...Option) (*authority.Config, error) {
|
|||
authorityOptions = &p.casOptions
|
||||
}
|
||||
|
||||
config := &authority.Config{
|
||||
config := &authconfig.Config{
|
||||
Root: []string{p.root},
|
||||
FederatedRoots: []string{},
|
||||
IntermediateCert: p.intermediate,
|
||||
|
@ -535,22 +535,22 @@ func (p *PKI) GenerateConfig(opt ...Option) (*authority.Config, error) {
|
|||
Type: "badger",
|
||||
DataSource: GetDBPath(),
|
||||
},
|
||||
AuthorityConfig: &authority.AuthConfig{
|
||||
AuthorityConfig: &authconfig.AuthConfig{
|
||||
Options: authorityOptions,
|
||||
DisableIssuedAtCheck: false,
|
||||
Provisioners: provisioner.List{prov},
|
||||
},
|
||||
TLS: &authority.TLSOptions{
|
||||
MinVersion: authority.DefaultTLSMinVersion,
|
||||
MaxVersion: authority.DefaultTLSMaxVersion,
|
||||
Renegotiation: authority.DefaultTLSRenegotiation,
|
||||
CipherSuites: authority.DefaultTLSCipherSuites,
|
||||
TLS: &authconfig.TLSOptions{
|
||||
MinVersion: authconfig.DefaultTLSMinVersion,
|
||||
MaxVersion: authconfig.DefaultTLSMaxVersion,
|
||||
Renegotiation: authconfig.DefaultTLSRenegotiation,
|
||||
CipherSuites: authconfig.DefaultTLSCipherSuites,
|
||||
},
|
||||
Templates: p.getTemplates(),
|
||||
}
|
||||
if p.enableSSH {
|
||||
enableSSHCA := true
|
||||
config.SSH = &authority.SSHConfig{
|
||||
config.SSH = &authconfig.SSHConfig{
|
||||
HostKey: p.sshHostKey,
|
||||
UserKey: p.sshUserKey,
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue