first commit

pull/599/head
max furman 3 years ago
parent f84c8f846a
commit 7b5d6968a5

@ -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)

@ -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

@ -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

@ -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"

@ -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"

@ -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)
}

@ -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)
}

@ -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)
}

@ -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)
}

@ -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)
}
*/

@ -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
}

@ -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)
}

@ -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)
}

@ -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)

@ -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)
}

@ -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

@ -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,40 +39,25 @@ func Bootstrap(token string) (*Client, error) {
return NewClient(claims.Audience[0], WithRootSHA256(claims.SHA))
}
// 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
// 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.
//
// Without any extra option the server will be configured for mTLS, it will
// require and verify clients certificates, but options can be used to drop this
// requirement, the most common will be only verify the certs if given with
// ca.VerifyClientCertIfGiven(), or add extra CAs with
// ca.AddClientCA(*x509.Certificate).
//
// Usage:
// // Default example with certificate rotation.
// srv, err := ca.BootstrapServer(context.Background(), token, &http.Server{
// Addr: ":443",
// Handler: handler,
// })
// client, err := ca.BootstrapClient(ctx.Background(), token)
//
// // Example canceling automatic certificate rotation.
// ctx, cancel := context.WithCancel(context.Background())
// defer cancel()
// srv, err := ca.BootstrapServer(ctx, token, &http.Server{
// Addr: ":443",
// Handler: handler,
// })
// client, err := ca.BootstrapClient(ctx, token)
// if err != nil {
// return err
// return err
// }
// srv.ListenAndServeTLS("", "")
func BootstrapServer(ctx context.Context, token string, base *http.Server, options ...TLSOption) (*http.Server, error) {
if base.TLSConfig != nil {
return nil, errors.New("server TLSConfig is already set")
}
// 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
@ -88,37 +73,53 @@ func BootstrapServer(ctx context.Context, token string, base *http.Server, optio
return nil, err
}
// Make sure the tlsConfig have all supported roots on ClientCAs and RootCAs
options = append(options, AddRootsToCAs())
// Make sure the tlsConfig have all supported roots on RootCAs
options = append(options, AddRootsToRootCAs())
tlsConfig, err := client.GetServerTLSConfig(ctx, sign, pk, options...)
transport, err := client.Transport(ctx, sign, pk, options...)
if err != nil {
return nil, err
}
base.TLSConfig = tlsConfig
return base, nil
return &http.Client{
Transport: transport,
}, 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
// 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
// certificate after 2/3rd of the certificate's lifetime has expired.
//
// Without any extra option the server will be configured for mTLS, it will
// require and verify clients certificates, but options can be used to drop this
// requirement, the most common will be only verify the certs if given with
// ca.VerifyClientCertIfGiven(), or add extra CAs with
// ca.AddClientCA(*x509.Certificate).
//
// Usage:
// // Default example with certificate rotation.
// client, err := ca.BootstrapClient(ctx.Background(), token)
// srv, err := ca.BootstrapServer(context.Background(), token, &http.Server{
// Addr: ":443",
// Handler: handler,
// })
//
// // Example canceling automatic certificate rotation.
// ctx, cancel := context.WithCancel(context.Background())
// defer cancel()
// client, err := ca.BootstrapClient(ctx, token)
// srv, err := ca.BootstrapServer(ctx, token, &http.Server{
// Addr: ":443",
// Handler: handler,
// })
// if err != nil {
// return err
// return err
// }
// resp, err := client.Get("https://internal.smallstep.com")
func BootstrapClient(ctx context.Context, token string, options ...TLSOption) (*http.Client, error) {
// srv.ListenAndServeTLS("", "")
func BootstrapServer(ctx context.Context, token string, base *http.Server, options ...TLSOption) (*http.Server, error) {
if base.TLSConfig != nil {
return nil, errors.New("server TLSConfig is already set")
}
client, err := Bootstrap(token)
if err != nil {
return nil, err
@ -134,17 +135,16 @@ func BootstrapClient(ctx context.Context, token string, options ...TLSOption) (*
return nil, err
}
// Make sure the tlsConfig have all supported roots on RootCAs
options = append(options, AddRootsToRootCAs())
// Make sure the tlsConfig have all supported roots on ClientCAs and RootCAs
options = append(options, AddRootsToCAs())
transport, err := client.Transport(ctx, sign, pk, options...)
tlsConfig, err := client.GetServerTLSConfig(ctx, sign, pk, options...)
if err != nil {
return nil, err
}
return &http.Client{
Transport: transport,
}, nil
base.TLSConfig = tlsConfig
return base, nil
}
// BootstrapListener is a helper function that using the given token returns a

@ -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")
}

@ -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,

@ -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…
Cancel
Save