Compare commits

...

6 commits

Author SHA1 Message Date
Herman Slatman
b2bf2c330b
Simplify SCEP provisioner context handling 2023-06-01 16:22:00 +02:00
Herman Slatman
8fc3a46387
Refactor the SCEP authority initialization
Instead of relying on an intermediate `scep.Service` struct,
initialize the `scep.Authority` directly. This removes one redundant
layer of indirection.
2023-06-01 15:50:51 +02:00
Herman Slatman
6985b4be62
Clean up the SCEP authority and provisioner 2023-06-01 14:43:32 +02:00
Herman Slatman
a1f187e3df
Merge branch 'master' into herman/scep-provisioner-decrypter 2023-06-01 12:12:12 +02:00
Herman Slatman
180162bd6a
Refactor SCEP provisioner and decrypter 2023-06-01 12:10:54 +02:00
Herman Slatman
0377fe559b
Add basic version of provisioner specific SCEP decrypter 2023-05-26 23:52:49 +02:00
15 changed files with 353 additions and 282 deletions

View file

@ -244,11 +244,24 @@ func (p ProvisionersResponse) MarshalJSON() ([]byte, error) {
continue continue
} }
old := scepProv.ChallengePassword type old struct {
challengePassword string
decrypterCertificate []byte
decrypterKey string
decrypterKeyPassword string
}
o := old{scepProv.ChallengePassword, scepProv.DecrypterCertificate, scepProv.DecrypterKey, scepProv.DecrypterKeyPassword}
scepProv.ChallengePassword = "*** REDACTED ***" scepProv.ChallengePassword = "*** REDACTED ***"
defer func(p string) { //nolint:gocritic // defer in loop required to restore initial state of provisioners scepProv.DecrypterCertificate = []byte("*** REDACTED ***")
scepProv.ChallengePassword = p scepProv.DecrypterKey = "*** REDACTED ***"
}(old) scepProv.DecrypterKeyPassword = "*** REDACTED ***"
defer func(o old) { //nolint:gocritic // defer in loop required to restore initial state of provisioners
scepProv.ChallengePassword = o.challengePassword
scepProv.DecrypterCertificate = o.decrypterCertificate
scepProv.DecrypterKey = o.decrypterKey
scepProv.DecrypterKeyPassword = o.decrypterKeyPassword
}(o)
} }
var list = struct { var list = struct {

View file

@ -4,6 +4,7 @@ import (
"bytes" "bytes"
"context" "context"
"crypto" "crypto"
"crypto/rsa"
"crypto/sha256" "crypto/sha256"
"crypto/x509" "crypto/x509"
"encoding/hex" "encoding/hex"
@ -61,7 +62,7 @@ type Authority struct {
x509Enforcers []provisioner.CertificateEnforcer x509Enforcers []provisioner.CertificateEnforcer
// SCEP CA // SCEP CA
scepService *scep.Service scepAuthority *scep.Authority
// SSH CA // SSH CA
sshHostPassword []byte sshHostPassword []byte
@ -261,6 +262,15 @@ func (a *Authority) ReloadAdminResources(ctx context.Context) error {
a.config.AuthorityConfig.Admins = adminList a.config.AuthorityConfig.Admins = adminList
a.admins = adminClxn a.admins = adminClxn
// update the SCEP service with the currently active SCEP
// provisioner names and revalidate the configuration.
if a.scepAuthority != nil {
a.scepAuthority.UpdateProvisioners(a.getSCEPProvisionerNames())
if err := a.scepAuthority.Validate(); err != nil {
log.Printf("failed validating SCEP authority: %v\n", err)
}
}
return nil return nil
} }
@ -640,48 +650,63 @@ func (a *Authority) init() error {
return err return err
} }
// Check if a KMS with decryption capability is required and available // The SCEP functionality is provided through an instance of
if a.requiresDecrypter() { // scep.Service. It is initialized once when the CA is started.
if _, ok := a.keyManager.(kmsapi.Decrypter); !ok { // TODO(hs): should the SCEP service support reloading? For example,
return errors.New("keymanager doesn't provide crypto.Decrypter") // when the admin resources are reloaded, specifically the provisioners,
} // it can happen that the SCEP service is no longer required and can
} // be destroyed, or that it needs to be instantiated. It may also need
// to be revalidated, because not all SCEP provisioner may have a
// TODO: decide if this is a good approach for providing the SCEP functionality // valid decrypter available.
// It currently mirrors the logic for the x509CAService if a.requiresSCEP() && a.GetSCEP() == nil {
if a.requiresSCEPService() && a.scepService == nil {
var options scep.Options var options scep.Options
options.Roots = a.rootX509Certs
// Read intermediate and create X509 signer and decrypter for default CAS. options.Intermediates, err = pemutil.ReadCertificateBundle(a.config.IntermediateCert)
options.CertificateChain, err = pemutil.ReadCertificateBundle(a.config.IntermediateCert)
if err != nil { if err != nil {
return err return err
} }
options.CertificateChain = append(options.CertificateChain, a.rootX509Certs...) options.SignerCert = options.Intermediates[0]
options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ if options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{
SigningKey: a.config.IntermediateKey, SigningKey: a.config.IntermediateKey,
Password: a.password, Password: a.password,
}) }); err != nil {
if err != nil {
return err return err
} }
if km, ok := a.keyManager.(kmsapi.Decrypter); ok { // TODO(hs): instead of creating the decrypter here, pass the
options.Decrypter, err = km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{ // intermediate key + chain down to the SCEP service / authority,
// and only instantiate it when required there. Is that possible?
// Also with entering passwords?
// TODO(hs): if moving the logic, try improving the logic for the
// decrypter password too? Right now it needs to be entered multiple
// times; I've observed it to be three times maximum, every time
// the intermediate key is read.
_, isRSA := options.Signer.Public().(*rsa.PublicKey)
if km, ok := a.keyManager.(kmsapi.Decrypter); ok && isRSA {
if decrypter, err := km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
DecryptionKey: a.config.IntermediateKey, DecryptionKey: a.config.IntermediateKey,
Password: a.password, Password: a.password,
}) }); err == nil {
if err != nil { // only pass the decrypter down when it was successfully created,
return err // meaning it's an RSA key, and `CreateDecrypter` did not fail.
options.Decrypter = decrypter
} }
} }
a.scepService, err = scep.NewService(ctx, options) // provide the current SCEP provisioner names, so that the provisioners
// can be validated when the CA is started.
options.SCEPProvisionerNames = a.getSCEPProvisionerNames()
// create a new SCEP authority
a.scepAuthority, err = scep.New(a, options)
if err != nil { if err != nil {
return err return err
} }
// TODO: mimick the x509CAService GetCertificateAuthority here too? // validate the SCEP authority
if err := a.scepAuthority.Validate(); err != nil {
a.initLogf("failed validating SCEP authority: %v", err)
}
} }
// Load X509 constraints engine. // Load X509 constraints engine.
@ -833,17 +858,9 @@ func (a *Authority) IsRevoked(sn string) (bool, error) {
return a.db.IsRevoked(sn) return a.db.IsRevoked(sn)
} }
// requiresDecrypter returns whether the Authority // requiresSCEP iterates over the configured provisioners
// requires a KMS that provides a crypto.Decrypter // and determines if at least one of them is a SCEP provisioner.
// Currently this is only required when SCEP is func (a *Authority) requiresSCEP() bool {
// enabled.
func (a *Authority) requiresDecrypter() bool {
return a.requiresSCEPService()
}
// requiresSCEPService iterates over the configured provisioners
// and determines if one of them is a SCEP provisioner.
func (a *Authority) requiresSCEPService() bool {
for _, p := range a.config.AuthorityConfig.Provisioners { for _, p := range a.config.AuthorityConfig.Provisioners {
if p.GetType() == provisioner.TypeSCEP { if p.GetType() == provisioner.TypeSCEP {
return true return true
@ -852,13 +869,18 @@ func (a *Authority) requiresSCEPService() bool {
return false return false
} }
// GetSCEPService returns the configured SCEP Service. func (a *Authority) getSCEPProvisionerNames() (names []string) {
// for _, p := range a.config.AuthorityConfig.Provisioners {
// TODO: this function is intended to exist temporarily in order to make SCEP if p.GetType() == provisioner.TypeSCEP {
// work more easily. It can be made more correct by using the right names = append(names, p.GetName())
// interfaces/abstractions after it works as expected. }
func (a *Authority) GetSCEPService() *scep.Service { }
return a.scepService return
}
// GetSCEP returns the configured SCEP Authority
func (a *Authority) GetSCEP() *scep.Authority {
return a.scepAuthority
} }
func (a *Authority) startCRLGenerator() error { func (a *Authority) startCRLGenerator() error {

View file

@ -478,7 +478,7 @@ func testScepAuthority(t *testing.T, opts ...Option) *Authority {
return a return a
} }
func TestAuthority_GetSCEPService(t *testing.T) { func TestAuthority_GetSCEP(t *testing.T) {
_ = testScepAuthority(t) _ = testScepAuthority(t)
p := provisioner.List{ p := provisioner.List{
&provisioner.SCEP{ &provisioner.SCEP{
@ -542,7 +542,7 @@ func TestAuthority_GetSCEPService(t *testing.T) {
return return
} }
if tt.wantService { if tt.wantService {
if got := a.GetSCEPService(); (got != nil) != tt.wantService { if got := a.GetSCEP(); (got != nil) != tt.wantService {
t.Errorf("Authority.GetSCEPService() = %v, wantService %v", got, tt.wantService) t.Errorf("Authority.GetSCEPService() = %v, wantService %v", got, tt.wantService)
} }
} }

View file

@ -2,13 +2,19 @@ package provisioner
import ( import (
"context" "context"
"crypto"
"crypto/rsa"
"crypto/subtle" "crypto/subtle"
"crypto/x509"
"encoding/pem"
"fmt" "fmt"
"net/http" "net/http"
"time" "time"
"github.com/pkg/errors" "github.com/pkg/errors"
"go.step.sm/crypto/kms"
kmsapi "go.step.sm/crypto/kms/apiv1"
"go.step.sm/linkedca" "go.step.sm/linkedca"
"github.com/smallstep/certificates/webhook" "github.com/smallstep/certificates/webhook"
@ -32,6 +38,12 @@ type SCEP struct {
// MinimumPublicKeyLength is the minimum length for public keys in CSRs // MinimumPublicKeyLength is the minimum length for public keys in CSRs
MinimumPublicKeyLength int `json:"minimumPublicKeyLength,omitempty"` MinimumPublicKeyLength int `json:"minimumPublicKeyLength,omitempty"`
// TODO
KMS *kms.Options `json:"kms,omitempty"`
DecrypterCertificate []byte `json:"decrypterCertificate"`
DecrypterKey string `json:"decrypterKey"`
DecrypterKeyPassword string `json:"decrypterKeyPassword"`
// Numerical identifier for the ContentEncryptionAlgorithm as defined in github.com/mozilla-services/pkcs7 // Numerical identifier for the ContentEncryptionAlgorithm as defined in github.com/mozilla-services/pkcs7
// at https://github.com/mozilla-services/pkcs7/blob/33d05740a3526e382af6395d3513e73d4e66d1cb/encrypt.go#L63 // at https://github.com/mozilla-services/pkcs7/blob/33d05740a3526e382af6395d3513e73d4e66d1cb/encrypt.go#L63
// Defaults to 0, being DES-CBC // Defaults to 0, being DES-CBC
@ -41,6 +53,9 @@ type SCEP struct {
ctl *Controller ctl *Controller
encryptionAlgorithm int encryptionAlgorithm int
challengeValidationController *challengeValidationController challengeValidationController *challengeValidationController
keyManager kmsapi.KeyManager
decrypter crypto.Decrypter
decrypterCertificate *x509.Certificate
} }
// GetID returns the provisioner unique identifier. // GetID returns the provisioner unique identifier.
@ -177,6 +192,45 @@ func (s *SCEP) Init(config Config) (err error) {
s.GetOptions().GetWebhooks(), s.GetOptions().GetWebhooks(),
) )
if s.KMS != nil {
if s.keyManager, err = kms.New(context.Background(), *s.KMS); err != nil {
return fmt.Errorf("failed initializing kms: %w", err)
}
km, ok := s.keyManager.(kmsapi.Decrypter)
if !ok {
return fmt.Errorf("%q is not a kmsapi.Decrypter", s.KMS.Type)
}
if s.DecrypterKey != "" || len(s.DecrypterCertificate) > 0 {
if s.decrypter, err = km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{
DecryptionKey: s.DecrypterKey,
Password: []byte(s.DecrypterKeyPassword),
}); err != nil {
return fmt.Errorf("failed creating decrypter: %w", err)
}
// parse the decrypter certificate
block, rest := pem.Decode(s.DecrypterCertificate)
if len(rest) > 0 {
return errors.New("failed parsing decrypter certificate: trailing data")
}
if block == nil {
return errors.New("failed parsing decrypter certificate: no PEM block found")
}
if s.decrypterCertificate, err = x509.ParseCertificate(block.Bytes); err != nil {
return fmt.Errorf("failed parsing decrypter certificate: %w", err)
}
// validate the decrypter key
decrypterPublicKey, ok := s.decrypter.Public().(*rsa.PublicKey)
if !ok {
return fmt.Errorf("only RSA keys are supported")
}
if !decrypterPublicKey.Equal(s.decrypterCertificate.PublicKey) {
return errors.New("mismatch between decryption certificate and decrypter public keys")
}
}
}
// TODO: add other, SCEP specific, options? // TODO: add other, SCEP specific, options?
s.ctl, err = NewController(s, s.Claims, config, s.Options) s.ctl, err = NewController(s, s.Claims, config, s.Options)
@ -259,3 +313,7 @@ func (s *SCEP) selectValidationMethod() validationMethod {
} }
return validationMethodNone return validationMethodNone
} }
func (s *SCEP) GetDecrypter() (*x509.Certificate, crypto.Decrypter) {
return s.decrypterCertificate, s.decrypter
}

View file

@ -8,6 +8,7 @@ import (
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"os" "os"
"strings"
"github.com/pkg/errors" "github.com/pkg/errors"
"gopkg.in/square/go-jose.v2/jwt" "gopkg.in/square/go-jose.v2/jwt"
@ -15,6 +16,7 @@ import (
"go.step.sm/cli-utils/step" "go.step.sm/cli-utils/step"
"go.step.sm/cli-utils/ui" "go.step.sm/cli-utils/ui"
"go.step.sm/crypto/jose" "go.step.sm/crypto/jose"
"go.step.sm/crypto/kms"
"go.step.sm/linkedca" "go.step.sm/linkedca"
"github.com/smallstep/certificates/authority/admin" "github.com/smallstep/certificates/authority/admin"
@ -235,7 +237,7 @@ func (a *Authority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisi
} }
if err := certProv.Init(provisionerConfig); err != nil { if err := certProv.Init(provisionerConfig); err != nil {
return admin.WrapError(admin.ErrorBadRequestType, err, "error validating configuration for provisioner %s", prov.Name) return admin.WrapError(admin.ErrorBadRequestType, err, "error validating configuration for provisioner %q", prov.Name)
} }
// Store to database -- this will set the ID. // Store to database -- this will set the ID.
@ -960,7 +962,7 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface,
}, nil }, nil
case *linkedca.ProvisionerDetails_SCEP: case *linkedca.ProvisionerDetails_SCEP:
cfg := d.SCEP cfg := d.SCEP
return &provisioner.SCEP{ s := &provisioner.SCEP{
ID: p.Id, ID: p.Id,
Type: p.Type.String(), Type: p.Type.String(),
Name: p.Name, Name: p.Name,
@ -972,7 +974,19 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface,
EncryptionAlgorithmIdentifier: int(cfg.EncryptionAlgorithmIdentifier), EncryptionAlgorithmIdentifier: int(cfg.EncryptionAlgorithmIdentifier),
Claims: claims, Claims: claims,
Options: options, Options: options,
}, nil }
if decrypter := cfg.GetDecrypter(); decrypter != nil {
if dkms := decrypter.GetKms(); dkms != nil {
s.KMS = &kms.Options{
Type: kms.Type(strings.ToLower(linkedca.KMS_Type_name[int32(dkms.Type)])),
CredentialsFile: dkms.CredentialsFile,
}
}
s.DecrypterCertificate = decrypter.DecrypterCertificate
s.DecrypterKey = decrypter.DecrypterKey
s.DecrypterKeyPassword = decrypter.DecrypterKeyPassword
}
return s, nil
case *linkedca.ProvisionerDetails_Nebula: case *linkedca.ProvisionerDetails_Nebula:
var roots []byte var roots []byte
for i, root := range d.Nebula.GetRoots() { for i, root := range d.Nebula.GetRoots() {

View file

@ -250,19 +250,24 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) {
var scepAuthority *scep.Authority var scepAuthority *scep.Authority
if ca.shouldServeSCEPEndpoints() { if ca.shouldServeSCEPEndpoints() {
scepPrefix := "scep" // validate the SCEP authority configuration. Currently this
scepAuthority, err = scep.New(auth, scep.AuthorityOptions{ // will not result in a failure to start if one or more SCEP
Service: auth.GetSCEPService(), // provisioners are not correctly configured. Only a log will
DNS: dns, // be emitted.
Prefix: scepPrefix, scepAuthority = auth.GetSCEP()
}) if err := scepAuthority.Validate(); err != nil {
if err != nil { err = errors.Wrap(err, "failed validating SCEP authority")
return nil, errors.Wrap(err, "error creating SCEP authority") shouldFail := false
if shouldFail {
return nil, err
}
log.Println(err)
} }
// According to the RFC (https://tools.ietf.org/html/rfc8894#section-7.10), // According to the RFC (https://tools.ietf.org/html/rfc8894#section-7.10),
// SCEP operations are performed using HTTP, so that's why the API is mounted // SCEP operations are performed using HTTP, so that's why the API is mounted
// to the insecure mux. // to the insecure mux.
scepPrefix := "scep"
insecureMux.Route("/"+scepPrefix, func(r chi.Router) { insecureMux.Route("/"+scepPrefix, func(r chi.Router) {
scepAPI.Route(r) scepAPI.Route(r)
}) })
@ -584,10 +589,10 @@ func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, *tls.Config,
// shouldServeSCEPEndpoints returns if the CA should be // shouldServeSCEPEndpoints returns if the CA should be
// configured with endpoints for SCEP. This is assumed to be // configured with endpoints for SCEP. This is assumed to be
// true if a SCEPService exists, which is true in case a // true if a SCEPService exists, which is true in case at
// SCEP provisioner was configured. // least one SCEP provisioner was configured.
func (ca *CA) shouldServeSCEPEndpoints() bool { func (ca *CA) shouldServeSCEPEndpoints() bool {
return ca.auth.GetSCEPService() != nil return ca.auth.GetSCEP() != nil
} }
//nolint:unused // useful for debugging //nolint:unused // useful for debugging

2
go.mod
View file

@ -146,3 +146,5 @@ require (
// use github.com/smallstep/pkcs7 fork with patches applied // use github.com/smallstep/pkcs7 fork with patches applied
replace go.mozilla.org/pkcs7 => github.com/smallstep/pkcs7 v0.0.0-20230302202335-4c094085c948 replace go.mozilla.org/pkcs7 => github.com/smallstep/pkcs7 v0.0.0-20230302202335-4c094085c948
replace go.step.sm/linkedca => ./../linkedca

2
go.sum
View file

@ -1066,8 +1066,6 @@ go.step.sm/cli-utils v0.7.6 h1:YkpLVrepmy2c5+eaz/wduiGxlgrRx3YdAStE37if25g=
go.step.sm/cli-utils v0.7.6/go.mod h1:j+FxFZ2gbWkAJl0eded/rksuxmNqWpmyxbkXcukGJaY= go.step.sm/cli-utils v0.7.6/go.mod h1:j+FxFZ2gbWkAJl0eded/rksuxmNqWpmyxbkXcukGJaY=
go.step.sm/crypto v0.31.0 h1:8ZG/BxC+0+LzPpk/764h5yubpG3GfxcRVR4E+Aye72g= go.step.sm/crypto v0.31.0 h1:8ZG/BxC+0+LzPpk/764h5yubpG3GfxcRVR4E+Aye72g=
go.step.sm/crypto v0.31.0/go.mod h1:Dv4lpkijKiZVkoc6zp+Xaw1xmy+voia1mykvbpQIvuc= go.step.sm/crypto v0.31.0/go.mod h1:Dv4lpkijKiZVkoc6zp+Xaw1xmy+voia1mykvbpQIvuc=
go.step.sm/linkedca v0.19.1 h1:uY0ByT/uB3FCQ8zIo9mU7MWG7HKf5sDXNEBeN94MuP8=
go.step.sm/linkedca v0.19.1/go.mod h1:vPV2ad3LFQJmV7XWt87VlnJSs6UOqgsbVGVWe3veEmI=
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=

View file

@ -221,7 +221,7 @@ func lookupProvisioner(next http.HandlerFunc) http.HandlerFunc {
return return
} }
ctx = context.WithValue(ctx, scep.ProvisionerContextKey, scep.Provisioner(prov)) ctx = scep.NewProvisionerContext(ctx, scep.Provisioner(prov))
next(w, r.WithContext(ctx)) next(w, r.WithContext(ctx))
} }
} }

View file

@ -2,10 +2,10 @@ package scep
import ( import (
"context" "context"
"crypto"
"crypto/x509" "crypto/x509"
"errors" "errors"
"fmt" "fmt"
"net/url"
microx509util "github.com/micromdm/scep/v2/cryptoutil/x509util" microx509util "github.com/micromdm/scep/v2/cryptoutil/x509util"
microscep "github.com/micromdm/scep/v2/scep" microscep "github.com/micromdm/scep/v2/scep"
@ -18,12 +18,13 @@ import (
// Authority is the layer that handles all SCEP interactions. // Authority is the layer that handles all SCEP interactions.
type Authority struct { type Authority struct {
prefix string signAuth SignAuthority
dns string roots []*x509.Certificate
intermediateCertificate *x509.Certificate intermediates []*x509.Certificate
caCerts []*x509.Certificate // TODO(hs): change to use these instead of root and intermediate signerCertificate *x509.Certificate
service *Service signer crypto.Signer
signAuth SignAuthority defaultDecrypter crypto.Decrypter
scepProvisionerNames []string
} }
type authorityKey struct{} type authorityKey struct{}
@ -49,19 +50,6 @@ func MustFromContext(ctx context.Context) *Authority {
} }
} }
// AuthorityOptions required to create a new SCEP Authority.
type AuthorityOptions struct {
// Service provides the certificate chain, the signer and the decrypter to the Authority
Service *Service
// DNS is the host used to generate accurate SCEP links. By default the authority
// will use the Host from the request, so this value will only be used if
// request.Host is empty.
DNS string
// Prefix is a URL path prefix under which the SCEP api is served. This
// prefix is required to generate accurate SCEP links.
Prefix string
}
// SignAuthority is the interface for a signing authority // SignAuthority is the interface for a signing authority
type SignAuthority interface { type SignAuthority interface {
Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error)
@ -69,26 +57,54 @@ type SignAuthority interface {
} }
// New returns a new Authority that implements the SCEP interface. // New returns a new Authority that implements the SCEP interface.
func New(signAuth SignAuthority, ops AuthorityOptions) (*Authority, error) { func New(signAuth SignAuthority, opts Options) (*Authority, error) {
if err := opts.Validate(); err != nil {
return nil, err
}
authority := &Authority{ authority := &Authority{
prefix: ops.Prefix, signAuth: signAuth, // TODO: provide signAuth through context instead?
dns: ops.DNS, roots: opts.Roots,
signAuth: signAuth, intermediates: opts.Intermediates,
signerCertificate: opts.SignerCert,
signer: opts.Signer,
defaultDecrypter: opts.Decrypter,
scepProvisionerNames: opts.SCEPProvisionerNames,
} }
// TODO: this is not really nice to do; the Service should be removed
// in its entirety to make this more interoperable with the rest of
// step-ca, I think.
if ops.Service != nil {
authority.caCerts = ops.Service.certificateChain
// TODO(hs): look into refactoring SCEP into using just caCerts everywhere, if it makes sense for more elaborate SCEP configuration. Keeping it like this for clarity (for now).
authority.intermediateCertificate = ops.Service.certificateChain[0]
authority.service = ops.Service
}
return authority, nil return authority, nil
} }
// Validate validates if the SCEP Authority has a valid configuration.
// The validation includes a check if a decrypter is available, either
// an authority wide decrypter, or a provisioner specific decrypter.
func (a *Authority) Validate() error {
noDefaultDecrypterAvailable := a.defaultDecrypter == nil
for _, name := range a.scepProvisionerNames {
p, err := a.LoadProvisionerByName(name)
if err != nil {
return fmt.Errorf("failed loading provisioner %q: %w", name, err)
}
if scepProv, ok := p.(*provisioner.SCEP); ok {
cert, decrypter := scepProv.GetDecrypter()
// TODO(hs): return sentinel/typed error, to be able to ignore/log these cases during init?
if cert == nil && noDefaultDecrypterAvailable {
return fmt.Errorf("SCEP provisioner %q does not have a decrypter certificate", name)
}
if decrypter == nil && noDefaultDecrypterAvailable {
return fmt.Errorf("SCEP provisioner %q does not have decrypter", name)
}
}
}
return nil
}
// UpdateProvisioners updates the SCEP Authority with the new, and hopefully
// current SCEP provisioners configured. This allows the Authority to be
// validated with the latest data.
func (a *Authority) UpdateProvisioners(scepProvisionerNames []string) {
a.scepProvisionerNames = scepProvisionerNames
}
var ( var (
// TODO: check the default capabilities; https://tools.ietf.org/html/rfc8894#section-3.5.2 // TODO: check the default capabilities; https://tools.ietf.org/html/rfc8894#section-3.5.2
defaultCapabilities = []string{ defaultCapabilities = []string{
@ -108,87 +124,52 @@ func (a *Authority) LoadProvisionerByName(name string) (provisioner.Interface, e
return a.signAuth.LoadProvisionerByName(name) return a.signAuth.LoadProvisionerByName(name)
} }
// GetLinkExplicit returns the requested link from the directory. // GetCACertificates returns the certificate (chain) for the CA.
func (a *Authority) GetLinkExplicit(provName string, abs bool, baseURL *url.URL, inputs ...string) string { //
return a.getLinkExplicit(provName, abs, baseURL, inputs...) // This methods returns the "SCEP Server (RA)" certificate, the issuing CA up to and excl. the root.
} // Some clients do need the root certificate however; also see: https://github.com/openxpki/openxpki/issues/73
//
// In case a provisioner specific decrypter is available, this is used as the "SCEP Server (RA)" certificate
// instead of the CA intermediate directly. This uses a distinct instance of a KMS for doing the SCEp key
// operations, so that RSA can be used for just SCEP.
//
// Using an RA does not seem to exist in https://tools.ietf.org/html/rfc8894, but is mentioned in
// https://tools.ietf.org/id/draft-nourse-scep-21.html.
func (a *Authority) GetCACertificates(ctx context.Context) (certs []*x509.Certificate, err error) {
p := provisionerFromContext(ctx)
// getLinkExplicit returns an absolute or partial path to the given resource and a base // if a provisioner specific RSA decrypter is available, it is returned as
// URL dynamically obtained from the request for which the link is being calculated. // the first certificate.
func (a *Authority) getLinkExplicit(provisionerName string, abs bool, baseURL *url.URL, _ ...string) string { if decrypterCertificate, _ := p.GetDecrypter(); decrypterCertificate != nil {
link := "/" + provisionerName certs = append(certs, decrypterCertificate)
if abs {
// Copy the baseURL value from the pointer. https://github.com/golang/go/issues/38351
u := url.URL{}
if baseURL != nil {
u = *baseURL
}
// If no Scheme is set, then default to http (in case of SCEP)
if u.Scheme == "" {
u.Scheme = "http"
}
// If no Host is set, then use the default (first DNS attr in the ca.json).
if u.Host == "" {
u.Host = a.dns
}
u.Path = a.prefix + link
return u.String()
} }
return link // TODO(hs): ensure logic is in place that checks the signer is the first
} // intermediate and that there are no double certificates.
certs = append(certs, a.intermediates...)
// GetCACertificates returns the certificate (chain) for the CA // the CA roots are added for completeness when configured to do so. Clients
func (a *Authority) GetCACertificates(ctx context.Context) ([]*x509.Certificate, error) { // are responsible to select the right cert(s) to store and use.
// TODO: this should return: the "SCEP Server (RA)" certificate, the issuing CA up to and excl. the root if p.ShouldIncludeRootInChain() {
// Some clients do need the root certificate however; also see: https://github.com/openxpki/openxpki/issues/73 certs = append(certs, a.roots...)
//
// This means we might need to think about if we should use the current intermediate CA
// certificate as the "SCEP Server (RA)" certificate. It might be better to have a distinct
// RA certificate, with a corresponding rsa.PrivateKey, just for SCEP usage, which is signed by
// the intermediate CA. Will need to look how we can provide this nicely within step-ca.
//
// This might also mean that we might want to use a distinct instance of KMS for doing the key operations,
// so that we can use RSA just for SCEP.
//
// Using an RA does not seem to exist in https://tools.ietf.org/html/rfc8894, but is mentioned in
// https://tools.ietf.org/id/draft-nourse-scep-21.html. Will continue using the CA directly for now.
//
// The certificate to use should probably depend on the (configured) provisioner and may
// use a distinct certificate, apart from the intermediate.
p, err := provisionerFromContext(ctx)
if err != nil {
return nil, err
}
if len(a.caCerts) == 0 {
return nil, errors.New("no intermediate certificate available in SCEP authority")
}
certs := []*x509.Certificate{}
certs = append(certs, a.caCerts[0])
// NOTE: we're adding the CA roots here, but they are (highly likely) different than what the RFC means.
// Clients are responsible to select the right cert(s) to use, though.
if p.ShouldIncludeRootInChain() && len(a.caCerts) > 1 {
certs = append(certs, a.caCerts[1])
} }
return certs, nil return certs, nil
} }
// DecryptPKIEnvelope decrypts an enveloped message // DecryptPKIEnvelope decrypts an enveloped message
func (a *Authority) DecryptPKIEnvelope(_ context.Context, msg *PKIMessage) error { func (a *Authority) DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) error {
p7c, err := pkcs7.Parse(msg.P7.Content) p7c, err := pkcs7.Parse(msg.P7.Content)
if err != nil { if err != nil {
return fmt.Errorf("error parsing pkcs7 content: %w", err) return fmt.Errorf("error parsing pkcs7 content: %w", err)
} }
envelope, err := p7c.Decrypt(a.intermediateCertificate, a.service.decrypter) cert, pkey, err := a.selectDecrypter(ctx)
if err != nil {
return fmt.Errorf("failed selecting decrypter: %w", err)
}
envelope, err := p7c.Decrypt(cert, pkey)
if err != nil { if err != nil {
return fmt.Errorf("error decrypting encrypted pkcs7 content: %w", err) return fmt.Errorf("error decrypting encrypted pkcs7 content: %w", err)
} }
@ -208,6 +189,9 @@ func (a *Authority) DecryptPKIEnvelope(_ context.Context, msg *PKIMessage) error
if err != nil { if err != nil {
return fmt.Errorf("parse CSR from pkiEnvelope: %w", err) return fmt.Errorf("parse CSR from pkiEnvelope: %w", err)
} }
if err := csr.CheckSignature(); err != nil {
return fmt.Errorf("invalid CSR signature; %w", err)
}
// check for challengePassword // check for challengePassword
cp, err := microx509util.ParseChallengePassword(msg.pkiEnvelope) cp, err := microx509util.ParseChallengePassword(msg.pkiEnvelope)
if err != nil { if err != nil {
@ -226,6 +210,21 @@ func (a *Authority) DecryptPKIEnvelope(_ context.Context, msg *PKIMessage) error
return nil return nil
} }
func (a *Authority) selectDecrypter(ctx context.Context) (cert *x509.Certificate, pkey crypto.PrivateKey, err error) {
p := provisionerFromContext(ctx)
// return provisioner specific decrypter, if available
if cert, pkey = p.GetDecrypter(); cert != nil && pkey != nil {
return
}
// fallback to the CA wide decrypter
cert = a.signerCertificate
pkey = a.defaultDecrypter
return
}
// SignCSR creates an x509.Certificate based on a CSR template and Cert Authority credentials // SignCSR creates an x509.Certificate based on a CSR template and Cert Authority credentials
// returns a new PKIMessage with CertRep data // returns a new PKIMessage with CertRep data
func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, msg *PKIMessage) (*PKIMessage, error) { func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, msg *PKIMessage) (*PKIMessage, error) {
@ -234,10 +233,7 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
// poll for the status. It seems to be similar as what can happen in ACME, so might want to model // poll for the status. It seems to be similar as what can happen in ACME, so might want to model
// the implementation after the one in the ACME authority. Requires storage, etc. // the implementation after the one in the ACME authority. Requires storage, etc.
p, err := provisionerFromContext(ctx) p := provisionerFromContext(ctx)
if err != nil {
return nil, err
}
// check if CSRReqMessage has already been decrypted // check if CSRReqMessage has already been decrypted
if msg.CSRReqMessage.CSR == nil { if msg.CSRReqMessage.CSR == nil {
@ -358,10 +354,11 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m
// as the first certificate in the array // as the first certificate in the array
signedData.AddCertificate(cert) signedData.AddCertificate(cert)
authCert := a.intermediateCertificate authCert := a.signerCertificate
signer := a.signer
// sign the attributes // sign the attributes
if err := signedData.AddSigner(authCert, a.service.signer, config); err != nil { if err := signedData.AddSigner(authCert, signer, config); err != nil {
return nil, err return nil, err
} }
@ -429,7 +426,7 @@ func (a *Authority) CreateFailureResponse(_ context.Context, _ *x509.Certificate
} }
// sign the attributes // sign the attributes
if err := signedData.AddSigner(a.intermediateCertificate, a.service.signer, config); err != nil { if err := signedData.AddSigner(a.signerCertificate, a.signer, config); err != nil {
return nil, err return nil, err
} }
@ -457,10 +454,7 @@ func (a *Authority) CreateFailureResponse(_ context.Context, _ *x509.Certificate
// GetCACaps returns the CA capabilities // GetCACaps returns the CA capabilities
func (a *Authority) GetCACaps(ctx context.Context) []string { func (a *Authority) GetCACaps(ctx context.Context) []string {
p, err := provisionerFromContext(ctx) p := provisionerFromContext(ctx)
if err != nil {
return defaultCapabilities
}
caps := p.GetCapabilities() caps := p.GetCapabilities()
if len(caps) == 0 { if len(caps) == 0 {
@ -477,9 +471,6 @@ func (a *Authority) GetCACaps(ctx context.Context) []string {
} }
func (a *Authority) ValidateChallenge(ctx context.Context, challenge, transactionID string) error { func (a *Authority) ValidateChallenge(ctx context.Context, challenge, transactionID string) error {
p, err := provisionerFromContext(ctx) p := provisionerFromContext(ctx)
if err != nil {
return err
}
return p.ValidateChallenge(ctx, challenge, transactionID) return p.ValidateChallenge(ctx, challenge, transactionID)
} }

View file

@ -1,29 +0,0 @@
package scep
import (
"context"
"errors"
)
// ContextKey is the key type for storing and searching for SCEP request
// essentials in the context of a request.
type ContextKey string
const (
// ProvisionerContextKey provisioner key
ProvisionerContextKey = ContextKey("provisioner")
)
// provisionerFromContext searches the context for a SCEP provisioner.
// Returns the provisioner or an error.
func provisionerFromContext(ctx context.Context) (Provisioner, error) {
val := ctx.Value(ProvisionerContextKey)
if val == nil {
return nil, errors.New("provisioner expected in request context")
}
p, ok := val.(Provisioner)
if !ok || p == nil {
return nil, errors.New("provisioner in context is not a SCEP provisioner")
}
return p, nil
}

View file

@ -1,7 +0,0 @@
package scep
import "crypto/x509"
type DB interface {
StoreCertificate(crt *x509.Certificate) error
}

View file

@ -4,65 +4,76 @@ import (
"crypto" "crypto"
"crypto/rsa" "crypto/rsa"
"crypto/x509" "crypto/x509"
"errors"
"github.com/pkg/errors"
) )
type Options struct { type Options struct {
// CertificateChain is the issuer certificate, along with any other bundled certificates // Roots contains the (federated) CA roots certificate(s)
// to be returned in the chain for consumers. Configured in the ca.json crt property. Roots []*x509.Certificate `json:"-"`
CertificateChain []*x509.Certificate // Intermediates points issuer certificate, along with any other bundled certificates
// to be returned in the chain for consumers.
Intermediates []*x509.Certificate `json:"-"`
// SignerCert points to the certificate of the CA signer. It usually is the same as the
// first certificate in the CertificateChain.
SignerCert *x509.Certificate `json:"-"`
// Signer signs CSRs in SCEP. Configured in the ca.json key property. // Signer signs CSRs in SCEP. Configured in the ca.json key property.
Signer crypto.Signer `json:"-"` Signer crypto.Signer `json:"-"`
// Decrypter decrypts encrypted SCEP messages. Configured in the ca.json key property. // Decrypter decrypts encrypted SCEP messages. Configured in the ca.json key property.
Decrypter crypto.Decrypter `json:"-"` Decrypter crypto.Decrypter `json:"-"`
// SCEPProvisionerNames contains the currently configured SCEP provioner names. These
// are used to be able to load the provisioners when the SCEP authority is being
// validated.
SCEPProvisionerNames []string
}
type comparablePublicKey interface {
Equal(crypto.PublicKey) bool
} }
// Validate checks the fields in Options. // Validate checks the fields in Options.
func (o *Options) Validate() error { func (o *Options) Validate() error {
if o.CertificateChain == nil { switch {
return errors.New("certificate chain not configured correctly") case len(o.Intermediates) == 0:
return errors.New("no intermediate certificate available for SCEP authority")
case o.Signer == nil:
return errors.New("no signer available for SCEP authority")
case o.SignerCert == nil:
return errors.New("no signer certificate available for SCEP authority")
} }
if len(o.CertificateChain) < 1 { // check if the signer (intermediate CA) certificate has the same public key as
return errors.New("certificate chain should at least have one certificate") // the signer. According to the RFC it seems valid to have different keys for
// the intermediate and the CA signing new certificates, so this might change
// in the future.
signerPublicKey := o.Signer.Public().(comparablePublicKey)
if !signerPublicKey.Equal(o.SignerCert.PublicKey) {
return errors.New("mismatch between signer certificate and public key")
} }
// According to the RFC: https://tools.ietf.org/html/rfc8894#section-3.1, SCEP // decrypter can be nil in case a signing only key is used; validation complete.
// can be used with something different than RSA, but requires the encryption if o.Decrypter == nil {
// to be performed using the challenge password. An older version of specification return nil
// states that only RSA is supported: https://tools.ietf.org/html/draft-nourse-scep-23#section-2.1.1 }
// Other algorithms than RSA do not seem to be supported in certnanny/sscep, but it might work
// If a decrypter is available, check that it's backed by an RSA key. According to the
// RFC: https://tools.ietf.org/html/rfc8894#section-3.1, SCEP can be used with something
// different than RSA, but requires the encryption to be performed using the challenge
// password in that case. An older version of specification states that only RSA is
// supported: https://tools.ietf.org/html/draft-nourse-scep-23#section-2.1.1. Other
// algorithms do not seem to be supported in certnanny/sscep, but it might work
// in micromdm/scep. Currently only RSA is allowed, but it might be an option // in micromdm/scep. Currently only RSA is allowed, but it might be an option
// to try other algorithms in the future. // to try other algorithms in the future.
intermediate := o.CertificateChain[0]
if intermediate.PublicKeyAlgorithm != x509.RSA {
return errors.New("only the RSA algorithm is (currently) supported")
}
// TODO: add checks for key usage?
signerPublicKey, ok := o.Signer.Public().(*rsa.PublicKey)
if !ok {
return errors.New("only RSA public keys are (currently) supported as signers")
}
// check if the intermediate ca certificate has the same public key as the signer.
// According to the RFC it seems valid to have different keys for the intermediate
// and the CA signing new certificates, so this might change in the future.
if !signerPublicKey.Equal(intermediate.PublicKey) {
return errors.New("mismatch between certificate chain and signer public keys")
}
decrypterPublicKey, ok := o.Decrypter.Public().(*rsa.PublicKey) decrypterPublicKey, ok := o.Decrypter.Public().(*rsa.PublicKey)
if !ok { if !ok {
return errors.New("only RSA public keys are (currently) supported as decrypters") return errors.New("only RSA keys are (currently) supported as decrypters")
} }
// check if intermediate public key is the same as the decrypter public key. // check if intermediate public key is the same as the decrypter public key.
// In certnanny/sscep it's mentioned that the signing key can be different // In certnanny/sscep it's mentioned that the signing key can be different
// from the decrypting (and encrypting) key. Currently that's not supported. // from the decrypting (and encrypting) key. These options are only used and
if !decrypterPublicKey.Equal(intermediate.PublicKey) { // validated when the intermediate CA is also used as the decrypter, though,
// so they should match.
if !decrypterPublicKey.Equal(o.SignerCert.PublicKey) {
return errors.New("mismatch between certificate chain and decrypter public keys") return errors.New("mismatch between certificate chain and decrypter public keys")
} }

View file

@ -2,6 +2,8 @@ package scep
import ( import (
"context" "context"
"crypto"
"crypto/x509"
"time" "time"
"github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/authority/provisioner"
@ -16,6 +18,25 @@ type Provisioner interface {
GetOptions() *provisioner.Options GetOptions() *provisioner.Options
GetCapabilities() []string GetCapabilities() []string
ShouldIncludeRootInChain() bool ShouldIncludeRootInChain() bool
GetDecrypter() (*x509.Certificate, crypto.Decrypter)
GetContentEncryptionAlgorithm() int GetContentEncryptionAlgorithm() int
ValidateChallenge(ctx context.Context, challenge, transactionID string) error ValidateChallenge(ctx context.Context, challenge, transactionID string) error
} }
// provisionerKey is the key type for storing and searching a
// SCEP provisioner in the context.
type provisionerKey struct{}
// provisionerFromContext searches the context for a SCEP provisioner.
// Returns the provisioner or panics if no SCEP provisioner is found.
func provisionerFromContext(ctx context.Context) Provisioner {
p, ok := ctx.Value(provisionerKey{}).(Provisioner)
if !ok {
panic("SCEP provisioner expected in request context")
}
return p
}
func NewProvisionerContext(ctx context.Context, p Provisioner) context.Context {
return context.WithValue(ctx, provisionerKey{}, p)
}

View file

@ -1,28 +0,0 @@
package scep
import (
"context"
"crypto"
"crypto/x509"
)
// Service is a wrapper for crypto.Signer and crypto.Decrypter
type Service struct {
certificateChain []*x509.Certificate
signer crypto.Signer
decrypter crypto.Decrypter
}
// NewService returns a new Service type.
func NewService(_ context.Context, opts Options) (*Service, error) {
if err := opts.Validate(); err != nil {
return nil, err
}
// TODO: should this become similar to the New CertificateAuthorityService as in x509CAService?
return &Service{
certificateChain: opts.CertificateChain,
signer: opts.Signer,
decrypter: opts.Decrypter,
}, nil
}