Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ go.work.sum

# Output of the go coverage tool, specifically when used with LiteIDE
*.out
.gocache

# Others
*.swp
Expand Down
22 changes: 16 additions & 6 deletions api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -257,22 +257,32 @@ func scepFromProvisioner(p *provisioner.SCEP) *models.SCEP {
}
}

func estFromProvisioner(p *provisioner.EST) *provisioner.EST {
prov := *p
// prov.ClientCertificateRoots = []byte(redacted)
// prov.BasicAuthUsername = redacted
// prov.BasicAuthPassword = redacted
return &prov
}

// MarshalJSON implements json.Marshaler. It marshals the ProvisionersResponse
// into a byte slice.
//
// Special treatment is given to the SCEP provisioner, as it contains a
// challenge secret that MUST NOT be leaked in (public) HTTP responses. The
// challenge value is thus redacted in HTTP responses.
// challenge value is thus redacted in HTTP responses. EST provisioners also
// contain a shared secret and are redacted in responses.
func (p ProvisionersResponse) MarshalJSON() ([]byte, error) {
var responseProvisioners provisioner.List
for _, item := range p.Provisioners {
scepProv, ok := item.(*provisioner.SCEP)
if !ok {
switch prov := item.(type) {
case *provisioner.SCEP:
responseProvisioners = append(responseProvisioners, scepFromProvisioner(prov))
case *provisioner.EST:
responseProvisioners = append(responseProvisioners, estFromProvisioner(prov))
default:
responseProvisioners = append(responseProvisioners, item)
continue
}

responseProvisioners = append(responseProvisioners, scepFromProvisioner(scepProv))
}

var list = struct {
Expand Down
2 changes: 2 additions & 0 deletions authority/admin/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ func UnmarshalProvisionerDetails(typ linkedca.Provisioner_Type, data []byte) (*l
v.Data = new(linkedca.ProvisionerDetails_SCEP)
case linkedca.Provisioner_NEBULA:
v.Data = new(linkedca.ProvisionerDetails_Nebula)
case linkedca.Provisioner_EST:
v.Data = new(linkedca.ProvisionerDetails_EST)
default:
return nil, fmt.Errorf("unsupported provisioner type %s", typ)
}
Expand Down
58 changes: 57 additions & 1 deletion authority/authority.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/smallstep/certificates/cas"
casapi "github.com/smallstep/certificates/cas/apiv1"
"github.com/smallstep/certificates/db"
"github.com/smallstep/certificates/est"
"github.com/smallstep/certificates/internal/httptransport"
"github.com/smallstep/certificates/scep"
"github.com/smallstep/certificates/templates"
Expand Down Expand Up @@ -72,6 +73,10 @@ type Authority struct {
scepAuthority *scep.Authority
scepKeyManager provisioner.SCEPKeyManager

// EST CA
estOptions *est.Options
estAuthority *est.Authority

// SSH CA
sshHostPassword []byte
sshUserPassword []byte
Expand Down Expand Up @@ -816,6 +821,28 @@ func (a *Authority) init() error {
}
}

// EST functionality is provided through an instance of est.Authority.
switch {
case a.requiresEST() && a.GetEST() == nil:
if a.estOptions == nil {
a.estOptions = &est.Options{
Roots: a.rootX509Certs,
Intermediates: a.intermediateX509Certs,
}
}

estAuthority, err := est.New(a, *a.estOptions)
if err != nil {
return err
}

a.estAuthority = estAuthority
case !a.requiresEST() && a.GetEST() != nil:
a.estAuthority = nil
case a.requiresEST() && a.GetEST() != nil:
// no-op
}

// Load X509 constraints engine.
//
// This is currently only available in CA mode.
Expand Down Expand Up @@ -1004,7 +1031,19 @@ func (a *Authority) GetSCEP() *scep.Authority {
return a.scepAuthority
}

// HasACMEProvisioner returns true if at least one ACME provisioner is configured.
// requiresEST iterates over the configured provisioners
// and determines if at least one of them is an EST provisioner.
func (a *Authority) requiresEST() bool {
for _, p := range a.config.AuthorityConfig.Provisioners {
if p.GetType() == provisioner.TypeEST {
return true
}
}
return false
}

// HasACMEProvisioner iterates over the configured provisioners
// and determines if at least one of them is an ACME provisioner.
func (a *Authority) HasACMEProvisioner() bool {
for _, p := range a.config.AuthorityConfig.Provisioners {
if p.GetType() == provisioner.TypeACME {
Expand All @@ -1014,6 +1053,23 @@ func (a *Authority) HasACMEProvisioner() bool {
return false
}

// getESTProvisionerNames returns the names of the EST provisioners
// that are currently available in the CA.
func (a *Authority) getESTProvisionerNames() (names []string) {
for _, p := range a.config.AuthorityConfig.Provisioners {
if p.GetType() == provisioner.TypeEST {
names = append(names, p.GetName())
}
}

return
}

// GetEST returns the configured EST Authority
func (a *Authority) GetEST() *est.Authority {
return a.estAuthority
}

func (a *Authority) startCRLGenerator() error {
if !a.config.CRL.IsEnabled() {
return nil
Expand Down
11 changes: 11 additions & 0 deletions authority/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
"github.com/smallstep/certificates/cas"
casapi "github.com/smallstep/certificates/cas/apiv1"
"github.com/smallstep/certificates/db"
"github.com/smallstep/certificates/est"
"github.com/smallstep/certificates/internal/httptransport"
"github.com/smallstep/certificates/scep"
)
Expand Down Expand Up @@ -252,6 +253,16 @@ func WithFullSCEPOptions(options *scep.Options) Option {
}
}

// WithFullESTOptions defines the options used for EST support.
//
// This feature is EXPERIMENTAL and might change at any time.
func WithFullESTOptions(options *est.Options) Option {
return func(a *Authority) error {
a.estOptions = options
return nil
}
}

// WithSCEPKeyManager defines the key manager used on SCEP provisioners.
//
// This feature is EXPERIMENTAL and might change at any time.
Expand Down
176 changes: 176 additions & 0 deletions authority/provisioner/est.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
package provisioner

import (
"context"
"crypto"
"crypto/x509"
"time"

"github.com/pkg/errors"

"github.com/smallstep/linkedca"

"github.com/smallstep/certificates/authority/provisioner/est"

"github.com/smallstep/certificates/internal/httptransport"
)

// EST is the EST provisioner type, an entity that can authorize the EST flow.
type EST struct {
*base
ID string `json:"-"`
Type string `json:"type"`
Name string `json:"name"`

ForceCN bool `json:"forceCN,omitempty"`
MinimumPublicKeyLength int `json:"minimumPublicKeyLength,omitempty"`

// Authentication configures accepted client authentication. At
// least one method is required; RFC 7030 3.2.3 permits requiring
// HTTP authentication in addition to TLS client authentication.
Authentication est.Authentication `json:"authentication"`

// Operations enables EST operations. Defaults to cacerts,
// csrattrs, simpleenroll and simplereenroll.
Operations []est.Operation `json:"operations,omitempty"`

CACerts est.CACerts `json:"cacerts,omitempty"`
CSRAttributes *est.CSRAttributes `json:"csrAttributes,omitempty"`

// ProofOfPossession controls tls-unique identity/PoP linking
// (RFC 7030 3.5): disabled | optional | required.
ProofOfPossession est.PoPMode `json:"proofOfPossession,omitempty"`

DummyBool *bool
DummyString string

// EnableTLSClientCertificate *bool `json:"enableTlsClientCertificate,omitempty"`
// ForwardedTLSClientCertHeader string `json:"forwardedTlsClientCertHeader,omitempty"`
// EnableHTTPBasicAuth *bool `json:"enableHTTPBasicAuth,omitempty"`
// BasicAuthUsername string `json:"basicAuthUsername,omitempty"`
// BasicAuthPassword string `json:"basicAuthPassword,omitempty"`
// ClientCertificateRoots []byte `json:"clientCertificateRoots,omitempty"`

Options *Options `json:"options,omitempty"`
Claims *Claims `json:"claims,omitempty"`

ctl *Controller
signer crypto.Signer
signerCertificate *x509.Certificate
challengeValidationController *challengeValidationController
clientCertificateRootPool *x509.CertPool
}

// GetID returns the provisioner unique identifier.
func (s *EST) GetID() string {
if s.ID != "" {
return s.ID
}
return s.GetIDForToken()
}

// GetIDForToken returns an identifier that will be used to load the provisioner from a token.
func (s *EST) GetIDForToken() string {
return "est/" + s.Name
}

// GetName returns the name of the provisioner.
func (s *EST) GetName() string {
return s.Name
}

// GetType returns the type of provisioner.
func (s *EST) GetType() Type {
return TypeEST
}

// GetEncryptedKey returns the base provisioner encrypted key if it's defined.
func (s *EST) GetEncryptedKey() (string, string, bool) {
return "", "", false
}

// GetTokenID returns the identifier of the token. This provisioner does not support tokens.
func (s *EST) GetTokenID(string) (string, error) {
return "", ErrTokenFlowNotSupported
}

// GetOptions returns the configured provisioner options.
func (s *EST) GetOptions() *Options {
return s.Options
}

// DefaultTLSCertDuration returns the default TLS cert duration enforced by the provisioner.
func (s *EST) DefaultTLSCertDuration() time.Duration {
return s.ctl.Claimer.DefaultTLSCertDuration()
}

// newChallengeValidationController creates a new challengeValidationController
// that performs challenge validation through webhooks.
func newESTChallengeValidationController(client HTTPClient, tw httptransport.Wrapper, webhooks []*Webhook) *challengeValidationController {
estHooks := []*Webhook{}
for _, wh := range webhooks {
if wh.Kind != linkedca.Webhook_ESTCHALLENGE.String() {
continue
}
estHooks = append(estHooks, wh)
}
return &challengeValidationController{
client: client,
wrapTransport: tw,
webhooks: estHooks,
}
}

// Init initializes and validates the fields of an EST type.
func (s *EST) Init(config Config) (err error) {
switch {
case s.Type == "":
return errors.New("provisioner type cannot be empty")
case s.Name == "":
return errors.New("provisioner name cannot be empty")
}

if s.MinimumPublicKeyLength == 0 {
s.MinimumPublicKeyLength = 2048
}
if s.MinimumPublicKeyLength%8 != 0 {
return errors.Errorf("%d bits is not exactly divisible by 8", s.MinimumPublicKeyLength)
}

// Prepare the EST challenge validator
s.challengeValidationController = newESTChallengeValidationController(
config.WebhookClient,
config.WrapTransport,
s.GetOptions().GetWebhooks(),
)

// if err := s.parseClientCertificateRoots(); err != nil {
// return err
// }

// if err := s.normalizeAuthConfig(); err != nil {
// return err
// }

s.ctl, err = NewController(s, s.Claims, config, s.Options)
return err
}

// AuthorizeSign does not do any verification; main validation is in the EST protocol.
func (s *EST) AuthorizeSign(context.Context, string) ([]SignOption, error) {
return []SignOption{
s,
newProvisionerExtensionOption(TypeEST, s.Name, "").WithControllerOptions(s.ctl),
newForceCNOption(s.ForceCN),
profileDefaultDuration(s.ctl.Claimer.DefaultTLSCertDuration()),
newPublicKeyMinimumLengthValidator(s.MinimumPublicKeyLength),
newValidityValidator(s.ctl.Claimer.MinTLSCertDuration(), s.ctl.Claimer.MaxTLSCertDuration()),
newX509NamePolicyValidator(s.ctl.getPolicy().getX509()),
s.ctl.newWebhookController(nil, linkedca.Webhook_X509),
}, nil
}

// GetCSRAttributes returns the CSR attributes to signal to clients.
func (s *EST) GetCSRAttributes(context.Context) ([]byte, error) {
return nil, nil // TODO(hs): refactor
}
Loading
Loading