certificates/authority/authority.go
Herman Slatman 8c5b12e21d
Add non-TLS server and improve crypto.Decrypter interface
A server without TLS was added to serve the SCEP endpoints. According
to the RFC, SCEP has to be served via HTTP. The `sscep` client, for
example, will stop any URL that does not start with `http://` from
being used, so serving SCEP seems to be the right way to do it.

This commit adds a second server for which no TLS configuration is
configured. A distinct field in the configuration, `insecureAddress`
was added to specify the address for the insecure server.

The SCEP endpoints will also still be served via HTTPS. Some clients
may be able to work with that.

This commit also improves how the crypto.Decrypter interface is
handled for the different types of KMSes supported by step. The
apiv1.Decrypter interface was added. Currently only SoftKMS
implements this interface, providing a crypto.Decrypter required
for SCEP operations.
2021-03-12 14:18:36 +01:00

466 lines
14 KiB
Go

package authority
import (
"context"
"crypto"
"crypto/sha256"
"crypto/x509"
"encoding/hex"
"log"
"sync"
"time"
"github.com/smallstep/certificates/cas"
"github.com/smallstep/certificates/scep"
"github.com/pkg/errors"
"github.com/smallstep/certificates/authority/provisioner"
casapi "github.com/smallstep/certificates/cas/apiv1"
"github.com/smallstep/certificates/db"
"github.com/smallstep/certificates/kms"
"github.com/smallstep/certificates/kms/apiv1"
kmsapi "github.com/smallstep/certificates/kms/apiv1"
"github.com/smallstep/certificates/kms/sshagentkms"
"github.com/smallstep/certificates/templates"
"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
keyManager kms.KeyManager
provisioners *provisioner.Collection
db db.AuthDB
templates *templates.Templates
// X509 CA
x509CAService cas.CertificateAuthorityService
rootX509Certs []*x509.Certificate
federatedX509Certs []*x509.Certificate
certificates *sync.Map
// SCEP CA
scepService *scep.Service
// SSH CA
sshCAUserCertSignKey ssh.Signer
sshCAHostCertSignKey ssh.Signer
sshCAUserCerts []ssh.PublicKey
sshCAHostCerts []ssh.PublicKey
sshCAUserFederatedCerts []ssh.PublicKey
sshCAHostFederatedCerts []ssh.PublicKey
// Do not re-initialize
initOnce bool
startTime time.Time
// Custom functions
sshBastionFunc func(ctx context.Context, user, hostname string) (*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)
getIdentityFunc provisioner.GetIdentityFunc
}
// New creates and initiates a new Authority type.
func New(config *Config, opts ...Option) (*Authority, error) {
err := config.Validate()
if err != nil {
return nil, err
}
var a = &Authority{
config: config,
certificates: new(sync.Map),
}
// Apply options.
for _, fn := range opts {
if err := fn(a); err != nil {
return nil, err
}
}
// Initialize authority from options or configuration.
if err := a.init(); err != nil {
return nil, err
}
return a, nil
}
// NewEmbedded initializes an authority that can be embedded in a different
// project without the limitations of the config.
func NewEmbedded(opts ...Option) (*Authority, error) {
a := &Authority{
config: &Config{},
certificates: new(sync.Map),
}
// Apply options.
for _, fn := range opts {
if err := fn(a); err != nil {
return nil, err
}
}
// Validate required options
switch {
case a.config == nil:
return nil, errors.New("cannot create an authority without a configuration")
case len(a.rootX509Certs) == 0 && a.config.Root.HasEmpties():
return nil, errors.New("cannot create an authority without a root certificate")
case a.x509CAService == nil && a.config.IntermediateCert == "":
return nil, errors.New("cannot create an authority without an issuer certificate")
case a.x509CAService == nil && a.config.IntermediateKey == "":
return nil, errors.New("cannot create an authority without an issuer signer")
}
// Initialize config required fields.
a.config.init()
// Initialize authority from options or configuration.
if err := a.init(); err != nil {
return nil, err
}
return a, nil
}
// init performs validation and initializes the fields of an Authority struct.
func (a *Authority) init() error {
// Check if handler has already been validated/initialized.
if a.initOnce {
return nil
}
var err error
// Initialize step-ca Database if it's not already initialized with WithDB.
// If a.config.DB is nil then a simple, barebones in memory DB will be used.
if a.db == nil {
if a.db, err = db.New(a.config.DB); err != nil {
return err
}
}
// Initialize key manager if it has not been set in the options.
if a.keyManager == nil {
var options kmsapi.Options
if a.config.KMS != nil {
options = *a.config.KMS
}
a.keyManager, err = kms.New(context.Background(), options)
if err != nil {
return err
}
}
// Initialize the X.509 CA Service if it has not been set in the options.
if a.x509CAService == nil {
var options casapi.Options
if a.config.AuthorityConfig.Options != nil {
options = *a.config.AuthorityConfig.Options
}
// Read intermediate and create X509 signer for default CAS.
if options.Is(casapi.SoftCAS) {
options.CertificateChain, err = pemutil.ReadCertificateBundle(a.config.IntermediateCert)
if err != nil {
return err
}
options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
SigningKey: a.config.IntermediateKey,
Password: []byte(a.config.Password),
})
if err != nil {
return err
}
}
a.x509CAService, err = cas.New(context.Background(), options)
if err != nil {
return err
}
// Get root certificate from CAS.
if srv, ok := a.x509CAService.(casapi.CertificateAuthorityGetter); ok {
resp, err := srv.GetCertificateAuthority(&casapi.GetCertificateAuthorityRequest{
Name: options.CertificateAuthority,
})
if err != nil {
return err
}
a.rootX509Certs = append(a.rootX509Certs, resp.RootCertificate)
sum := sha256.Sum256(resp.RootCertificate.Raw)
log.Printf("Using root fingerprint '%s'", hex.EncodeToString(sum[:]))
}
}
// TODO: decide if this is a good approach for providing the SCEP functionality
// It currently mirrors the logic for the x509CAServer
if a.scepService == nil {
var options casapi.Options
if a.config.AuthorityConfig.Options != nil {
options = *a.config.AuthorityConfig.Options
}
// Read intermediate and create X509 signer and decrypter for default CAS.
if options.Is(casapi.SoftCAS) {
options.CertificateChain, err = pemutil.ReadCertificateBundle(a.config.IntermediateCert)
if err != nil {
return err
}
options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
SigningKey: a.config.IntermediateKey,
Password: []byte(a.config.Password),
})
if err != nil {
return err
}
if km, ok := a.keyManager.(apiv1.Decrypter); ok {
options.Decrypter, err = km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
DecryptionKey: a.config.IntermediateKey,
Password: []byte(a.config.Password),
})
if err != nil {
return err
}
}
}
a.scepService = &scep.Service{
Signer: options.Signer,
Decrypter: options.Decrypter,
}
}
// Read root certificates and store them in the certificates map.
if len(a.rootX509Certs) == 0 {
a.rootX509Certs = make([]*x509.Certificate, len(a.config.Root))
for i, path := range a.config.Root {
crt, err := pemutil.ReadCertificate(path)
if err != nil {
return err
}
a.rootX509Certs[i] = crt
}
}
for _, crt := range a.rootX509Certs {
sum := sha256.Sum256(crt.Raw)
a.certificates.Store(hex.EncodeToString(sum[:]), crt)
}
// Read federated certificates and store them in the certificates map.
if len(a.federatedX509Certs) == 0 {
a.federatedX509Certs = make([]*x509.Certificate, len(a.config.FederatedRoots))
for i, path := range a.config.FederatedRoots {
crt, err := pemutil.ReadCertificate(path)
if err != nil {
return err
}
a.federatedX509Certs[i] = crt
}
}
for _, crt := range a.federatedX509Certs {
sum := sha256.Sum256(crt.Raw)
a.certificates.Store(hex.EncodeToString(sum[:]), crt)
}
// Decrypt and load SSH keys
var tmplVars templates.Step
if a.config.SSH != nil {
if a.config.SSH.HostKey != "" {
signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
SigningKey: a.config.SSH.HostKey,
Password: []byte(a.config.Password),
})
if err != nil {
return err
}
// If our signer is from sshagentkms, just unwrap it instead of
// wrapping it in another layer, and this prevents crypto from
// erroring out with: ssh: unsupported key type *agent.Key
switch s := signer.(type) {
case *sshagentkms.WrappedSSHSigner:
a.sshCAHostCertSignKey = s.Sshsigner
case crypto.Signer:
a.sshCAHostCertSignKey, err = ssh.NewSignerFromSigner(s)
default:
return errors.Errorf("unsupported signer type %T", signer)
}
if err != nil {
return errors.Wrap(err, "error creating ssh signer")
}
// Append public key to list of host certs
a.sshCAHostCerts = append(a.sshCAHostCerts, a.sshCAHostCertSignKey.PublicKey())
a.sshCAHostFederatedCerts = append(a.sshCAHostFederatedCerts, a.sshCAHostCertSignKey.PublicKey())
}
if a.config.SSH.UserKey != "" {
signer, err := a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
SigningKey: a.config.SSH.UserKey,
Password: []byte(a.config.Password),
})
if err != nil {
return err
}
// If our signer is from sshagentkms, just unwrap it instead of
// wrapping it in another layer, and this prevents crypto from
// erroring out with: ssh: unsupported key type *agent.Key
switch s := signer.(type) {
case *sshagentkms.WrappedSSHSigner:
a.sshCAUserCertSignKey = s.Sshsigner
case crypto.Signer:
a.sshCAUserCertSignKey, err = ssh.NewSignerFromSigner(s)
default:
return errors.Errorf("unsupported signer type %T", signer)
}
if err != nil {
return errors.Wrap(err, "error creating ssh signer")
}
// Append public key to list of user certs
a.sshCAUserCerts = append(a.sshCAUserCerts, a.sshCAUserCertSignKey.PublicKey())
a.sshCAUserFederatedCerts = append(a.sshCAUserFederatedCerts, a.sshCAUserCertSignKey.PublicKey())
}
// Append other public keys
for _, key := range a.config.SSH.Keys {
switch key.Type {
case provisioner.SSHHostCert:
if key.Federated {
a.sshCAHostFederatedCerts = append(a.sshCAHostFederatedCerts, key.PublicKey())
} else {
a.sshCAHostCerts = append(a.sshCAHostCerts, key.PublicKey())
}
case provisioner.SSHUserCert:
if key.Federated {
a.sshCAUserFederatedCerts = append(a.sshCAUserFederatedCerts, key.PublicKey())
} else {
a.sshCAUserCerts = append(a.sshCAUserCerts, key.PublicKey())
}
default:
return errors.Errorf("unsupported type %s", key.Type)
}
}
// Configure template variables.
tmplVars.SSH.HostKey = a.sshCAHostCertSignKey.PublicKey()
tmplVars.SSH.UserKey = a.sshCAUserCertSignKey.PublicKey()
// On the templates we skip the first one because there's a distinction
// between the main key and federated keys.
tmplVars.SSH.HostFederatedKeys = append(tmplVars.SSH.HostFederatedKeys, a.sshCAHostFederatedCerts[1:]...)
tmplVars.SSH.UserFederatedKeys = append(tmplVars.SSH.UserFederatedKeys, a.sshCAUserFederatedCerts[1:]...)
}
// Merge global and configuration claims
claimer, err := provisioner.NewClaimer(a.config.AuthorityConfig.Claims, globalProvisionerClaims)
if err != nil {
return err
}
// TODO: should we also be combining the ssh federated roots here?
// If we rotate ssh roots keys, sshpop provisioner will lose ability to
// validate old SSH certificates, unless they are added as federated certs.
sshKeys, err := a.GetSSHRoots(context.Background())
if err != nil {
return err
}
// Initialize provisioners
audiences := a.config.getAudiences()
a.provisioners = provisioner.NewCollection(audiences)
config := provisioner.Config{
Claims: claimer.Claims(),
Audiences: audiences,
DB: a.db,
SSHKeys: &provisioner.SSHKeys{
UserKeys: sshKeys.UserKeys,
HostKeys: sshKeys.HostKeys,
},
GetIdentityFunc: a.getIdentityFunc,
}
// Check if a KMS with decryption capability is required and available
if a.requiresDecrypter() {
if _, ok := a.keyManager.(apiv1.Decrypter); !ok {
return errors.New("keymanager doesn't provide crypto.Decrypter")
}
}
// Store all the provisioners
for _, p := range a.config.AuthorityConfig.Provisioners {
if err := p.Init(config); err != nil {
return err
}
if err := a.provisioners.Store(p); err != nil {
return err
}
}
// Configure templates, currently only ssh templates are supported.
if a.sshCAHostCertSignKey != nil || a.sshCAUserCertSignKey != nil {
a.templates = a.config.Templates
if a.templates == nil {
a.templates = templates.DefaultTemplates()
}
if a.templates.Data == nil {
a.templates.Data = make(map[string]interface{})
}
a.templates.Data["Step"] = tmplVars
}
// JWT numeric dates are seconds.
a.startTime = time.Now().Truncate(time.Second)
// Set flag indicating that initialization has been completed, and should
// not be repeated.
a.initOnce = true
return nil
}
// GetDatabase returns the authority database. If the configuration does not
// define a database, GetDatabase will return a db.SimpleDB instance.
func (a *Authority) GetDatabase() db.AuthDB {
return a.db
}
// Shutdown safely shuts down any clients, databases, etc. held by the Authority.
func (a *Authority) Shutdown() error {
if err := a.keyManager.Close(); err != nil {
log.Printf("error closing the key manager: %v", err)
}
return a.db.Shutdown()
}
// CloseForReload closes internal services, to allow a safe reload.
func (a *Authority) CloseForReload() {
if err := a.keyManager.Close(); err != nil {
log.Printf("error closing the key manager: %v", err)
}
}
// requiresDecrypter iterates over the configured provisioners
// and determines if the Authority requires a KMS that provides
// a crypto.Decrypter by implementing the apiv1.Decrypter
// interface. Currently only the SCEP provider requires this,
// but others may be added in the future.
func (a *Authority) requiresDecrypter() bool {
for _, p := range a.config.AuthorityConfig.Provisioners {
if p.GetType() == provisioner.TypeSCEP {
return true
}
}
return false
}
// GetSCEPService returns the configured SCEP Service
// TODO: this function is intended to exist temporarily
// in order to make SCEP work more easily. It can be
// made more correct by using the right interfaces/abstractions
// after it works as expected.
func (a *Authority) GetSCEPService() scep.Service {
return *a.scepService
}