diff --git a/api/api.go b/api/api.go index f76bdd9e..c586a43a 100644 --- a/api/api.go +++ b/api/api.go @@ -25,6 +25,7 @@ import ( "golang.org/x/crypto/ssh" "github.com/smallstep/certificates/api/log" + "github.com/smallstep/certificates/api/models" "github.com/smallstep/certificates/api/render" "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/config" @@ -231,6 +232,29 @@ type ProvisionersResponse struct { NextCursor string } +const redacted = "*** REDACTED ***" + +func scepFromProvisioner(p *provisioner.SCEP) *models.SCEP { + return &models.SCEP{ + ID: p.ID, + Type: p.Type, + Name: p.Name, + ForceCN: p.ForceCN, + ChallengePassword: redacted, + Capabilities: p.Capabilities, + IncludeRoot: p.IncludeRoot, + ExcludeIntermediate: p.ExcludeIntermediate, + MinimumPublicKeyLength: p.MinimumPublicKeyLength, + DecrypterCertificate: []byte(redacted), + DecrypterKeyPEM: []byte(redacted), + DecrypterKeyURI: redacted, + DecrypterKeyPassword: []byte(redacted), + EncryptionAlgorithmIdentifier: p.EncryptionAlgorithmIdentifier, + Options: p.Options, + Claims: p.Claims, + } +} + // MarshalJSON implements json.Marshaler. It marshals the ProvisionersResponse // into a byte slice. // @@ -238,24 +262,22 @@ type ProvisionersResponse struct { // challenge secret that MUST NOT be leaked in (public) HTTP responses. The // challenge value is thus redacted in HTTP responses. func (p ProvisionersResponse) MarshalJSON() ([]byte, error) { + var responseProvisioners provisioner.List for _, item := range p.Provisioners { scepProv, ok := item.(*provisioner.SCEP) if !ok { + responseProvisioners = append(responseProvisioners, item) continue } - old := scepProv.ChallengePassword - scepProv.ChallengePassword = "*** REDACTED ***" - defer func(p string) { //nolint:gocritic // defer in loop required to restore initial state of provisioners - scepProv.ChallengePassword = p - }(old) + responseProvisioners = append(responseProvisioners, scepFromProvisioner(scepProv)) } var list = struct { Provisioners []provisioner.Interface `json:"provisioners"` NextCursor string `json:"nextCursor"` }{ - Provisioners: []provisioner.Interface(p.Provisioners), + Provisioners: []provisioner.Interface(responseProvisioners), NextCursor: p.NextCursor, } diff --git a/api/api_test.go b/api/api_test.go index ae3904e0..efca024a 100644 --- a/api/api_test.go +++ b/api/api_test.go @@ -1569,7 +1569,6 @@ func mustCertificate(t *testing.T, pub, priv interface{}) *x509.Certificate { } func TestProvisionersResponse_MarshalJSON(t *testing.T) { - k := map[string]any{ "use": "sig", "kty": "EC", @@ -1581,9 +1580,14 @@ func TestProvisionersResponse_MarshalJSON(t *testing.T) { } key := squarejose.JSONWebKey{} b, err := json.Marshal(k) - assert.FatalError(t, err) + require.NoError(t, err) err = json.Unmarshal(b, &key) - assert.FatalError(t, err) + require.NoError(t, err) + + var encodedPassword bytes.Buffer + enc := base64.NewEncoder(base64.StdEncoding, &encodedPassword) + _, err = enc.Write([]byte("super-secret-password")) + require.NoError(t, err) r := ProvisionersResponse{ Provisioners: provisioner.List{ @@ -1593,6 +1597,12 @@ func TestProvisionersResponse_MarshalJSON(t *testing.T) { ChallengePassword: "not-so-secret", MinimumPublicKeyLength: 2048, EncryptionAlgorithmIdentifier: 2, + IncludeRoot: true, + ExcludeIntermediate: true, + DecrypterCertificate: []byte{1, 2, 3, 4}, + DecrypterKeyPEM: []byte{5, 6, 7, 8}, + DecrypterKeyURI: "softkms:path=/path/to/private.key", + DecrypterKeyPassword: encodedPassword.Bytes(), }, &provisioner.JWK{ EncryptedKey: "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlhOdmYxQjgxSUlLMFA2NUkwcmtGTGcifQ.XaN9zcPQeWt49zchUDm34FECUTHfQTn_.tmNHPQDqR3ebsWfd.9WZr3YVdeOyJh36vvx0VlRtluhvYp4K7jJ1KGDr1qypwZ3ziBVSNbYYQ71du7fTtrnfG1wgGTVR39tWSzBU-zwQ5hdV3rpMAaEbod5zeW6SHd95H3Bvcb43YiiqJFNL5sGZzFb7FqzVmpsZ1efiv6sZaGDHtnCAL6r12UG5EZuqGfM0jGCZitUz2m9TUKXJL5DJ7MOYbFfkCEsUBPDm_TInliSVn2kMJhFa0VOe5wZk5YOuYM3lNYW64HGtbf-llN2Xk-4O9TfeSPizBx9ZqGpeu8pz13efUDT2WL9tWo6-0UE-CrG0bScm8lFTncTkHcu49_a5NaUBkYlBjEiw.thPcx3t1AUcWuEygXIY3Fg", @@ -1609,7 +1619,14 @@ func TestProvisionersResponse_MarshalJSON(t *testing.T) { { "type": "scep", "name": "scep", + "forceCN": false, + "includeRoot": true, + "excludeIntermediate": true, "challenge": "*** REDACTED ***", + "decrypterCertificate": []byte("*** REDACTED ***"), + "decrypterKey": "*** REDACTED ***", + "decrypterKeyPEM": []byte("*** REDACTED ***"), + "decrypterKeyPassword": []byte("*** REDACTED ***"), "minimumPublicKeyLength": 2048, "encryptionAlgorithmIdentifier": 2, }, @@ -1646,6 +1663,12 @@ func TestProvisionersResponse_MarshalJSON(t *testing.T) { ChallengePassword: "not-so-secret", MinimumPublicKeyLength: 2048, EncryptionAlgorithmIdentifier: 2, + IncludeRoot: true, + ExcludeIntermediate: true, + DecrypterCertificate: []byte{1, 2, 3, 4}, + DecrypterKeyPEM: []byte{5, 6, 7, 8}, + DecrypterKeyURI: "softkms:path=/path/to/private.key", + DecrypterKeyPassword: encodedPassword.Bytes(), }, &provisioner.JWK{ EncryptedKey: "eyJhbGciOiJQQkVTMi1IUzI1NitBMTI4S1ciLCJlbmMiOiJBMTI4R0NNIiwicDJjIjoxMDAwMDAsInAycyI6IlhOdmYxQjgxSUlLMFA2NUkwcmtGTGcifQ.XaN9zcPQeWt49zchUDm34FECUTHfQTn_.tmNHPQDqR3ebsWfd.9WZr3YVdeOyJh36vvx0VlRtluhvYp4K7jJ1KGDr1qypwZ3ziBVSNbYYQ71du7fTtrnfG1wgGTVR39tWSzBU-zwQ5hdV3rpMAaEbod5zeW6SHd95H3Bvcb43YiiqJFNL5sGZzFb7FqzVmpsZ1efiv6sZaGDHtnCAL6r12UG5EZuqGfM0jGCZitUz2m9TUKXJL5DJ7MOYbFfkCEsUBPDm_TInliSVn2kMJhFa0VOe5wZk5YOuYM3lNYW64HGtbf-llN2Xk-4O9TfeSPizBx9ZqGpeu8pz13efUDT2WL9tWo6-0UE-CrG0bScm8lFTncTkHcu49_a5NaUBkYlBjEiw.thPcx3t1AUcWuEygXIY3Fg", diff --git a/api/models/scep.go b/api/models/scep.go new file mode 100644 index 00000000..c4fea502 --- /dev/null +++ b/api/models/scep.go @@ -0,0 +1,118 @@ +package models + +import ( + "context" + "crypto/x509" + "errors" + + "github.com/smallstep/certificates/authority/provisioner" + "golang.org/x/crypto/ssh" +) + +var errDummyImplementation = errors.New("dummy implementation") + +// SCEP is the SCEP provisioner model used solely in CA API +// responses. All methods for the [provisioner.Interface] interface +// are implemented, but return a dummy error. +// TODO(hs): remove reliance on the interface for the API responses +type SCEP struct { + ID string `json:"-"` + Type string `json:"type"` + Name string `json:"name"` + ForceCN bool `json:"forceCN"` + ChallengePassword string `json:"challenge"` + Capabilities []string `json:"capabilities,omitempty"` + IncludeRoot bool `json:"includeRoot"` + ExcludeIntermediate bool `json:"excludeIntermediate"` + MinimumPublicKeyLength int `json:"minimumPublicKeyLength"` + DecrypterCertificate []byte `json:"decrypterCertificate"` + DecrypterKeyPEM []byte `json:"decrypterKeyPEM"` + DecrypterKeyURI string `json:"decrypterKey"` + DecrypterKeyPassword []byte `json:"decrypterKeyPassword"` + EncryptionAlgorithmIdentifier int `json:"encryptionAlgorithmIdentifier"` + Options *provisioner.Options `json:"options,omitempty"` + Claims *provisioner.Claims `json:"claims,omitempty"` +} + +// GetID returns the provisioner unique identifier. +func (s *SCEP) 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 *SCEP) GetIDForToken() string { + return "scep/" + s.Name +} + +// GetName returns the name of the provisioner. +func (s *SCEP) GetName() string { + return s.Name +} + +// GetType returns the type of provisioner. +func (s *SCEP) GetType() provisioner.Type { + return provisioner.TypeSCEP +} + +// GetEncryptedKey returns the base provisioner encrypted key if it's defined. +func (s *SCEP) GetEncryptedKey() (string, string, bool) { + return "", "", false +} + +// GetTokenID returns the identifier of the token. +func (s *SCEP) GetTokenID(string) (string, error) { + return "", errDummyImplementation +} + +// Init initializes and validates the fields of a SCEP type. +func (s *SCEP) Init(_ provisioner.Config) (err error) { + return errDummyImplementation +} + +// AuthorizeSign returns an unimplemented error. Provisioners should overwrite +// this method if they will support authorizing tokens for signing x509 Certificates. +func (s *SCEP) AuthorizeSign(context.Context, string) ([]provisioner.SignOption, error) { + return nil, errDummyImplementation +} + +// AuthorizeRevoke returns an unimplemented error. Provisioners should overwrite +// this method if they will support authorizing tokens for revoking x509 Certificates. +func (s *SCEP) AuthorizeRevoke(context.Context, string) error { + return errDummyImplementation +} + +// AuthorizeRenew returns an unimplemented error. Provisioners should overwrite +// this method if they will support authorizing tokens for renewing x509 Certificates. +func (s *SCEP) AuthorizeRenew(context.Context, *x509.Certificate) error { + return errDummyImplementation +} + +// AuthorizeSSHSign returns an unimplemented error. Provisioners should overwrite +// this method if they will support authorizing tokens for signing SSH Certificates. +func (s *SCEP) AuthorizeSSHSign(context.Context, string) ([]provisioner.SignOption, error) { + return nil, errDummyImplementation +} + +// AuthorizeRevoke returns an unimplemented error. Provisioners should overwrite +// this method if they will support authorizing tokens for revoking SSH Certificates. +func (s *SCEP) AuthorizeSSHRevoke(context.Context, string) error { + return errDummyImplementation +} + +// AuthorizeSSHRenew returns an unimplemented error. Provisioners should overwrite +// this method if they will support authorizing tokens for renewing SSH Certificates. +func (s *SCEP) AuthorizeSSHRenew(context.Context, string) (*ssh.Certificate, error) { + return nil, errDummyImplementation +} + +// AuthorizeSSHRekey returns an unimplemented error. Provisioners should overwrite +// this method if they will support authorizing tokens for rekeying SSH Certificates. +func (s *SCEP) AuthorizeSSHRekey(context.Context, string) (*ssh.Certificate, []provisioner.SignOption, error) { + return nil, nil, errDummyImplementation +} + +var _ provisioner.Interface = (*SCEP)(nil) diff --git a/authority/admin/api/webhook.go b/authority/admin/api/webhook.go index 3f301ba0..f01ddb65 100644 --- a/authority/admin/api/webhook.go +++ b/authority/admin/api/webhook.go @@ -56,9 +56,7 @@ func validateWebhook(webhook *linkedca.Webhook) error { } // kind - switch webhook.Kind { - case linkedca.Webhook_ENRICHING, linkedca.Webhook_AUTHORIZING, linkedca.Webhook_SCEPCHALLENGE: - default: + if _, ok := linkedca.Webhook_Kind_name[int32(webhook.Kind)]; !ok || webhook.Kind == linkedca.Webhook_NO_KIND { return admin.NewError(admin.ErrorBadRequestType, "webhook kind %q is invalid", webhook.Kind) } diff --git a/authority/authority.go b/authority/authority.go index ae85c018..a4a76293 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "crypto" + "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/hex" @@ -61,7 +62,9 @@ type Authority struct { x509Enforcers []provisioner.CertificateEnforcer // SCEP CA - scepService *scep.Service + scepOptions *scep.Options + validateSCEP bool + scepAuthority *scep.Authority // SSH CA sshHostPassword []byte @@ -122,6 +125,7 @@ func New(cfg *config.Config, opts ...Option) (*Authority, error) { var a = &Authority{ config: cfg, certificates: new(sync.Map), + validateSCEP: true, } // Apply options. @@ -261,6 +265,24 @@ func (a *Authority) ReloadAdminResources(ctx context.Context) error { a.config.AuthorityConfig.Admins = adminList a.admins = adminClxn + switch { + case a.requiresSCEP() && a.GetSCEP() == nil: + // TODO(hs): try to initialize SCEP here too? It's a bit + // problematic if this method is called as part of an update + // via Admin API and a password needs to be provided. + case a.requiresSCEP() && a.GetSCEP() != nil: + // update the SCEP Authority with the currently active SCEP + // provisioner names and revalidate the configuration. + a.scepAuthority.UpdateProvisioners(a.getSCEPProvisionerNames()) + if err := a.scepAuthority.Validate(); err != nil { + log.Printf("failed validating SCEP authority: %v\n", err) + } + case !a.requiresSCEP() && a.GetSCEP() != nil: + // TODO(hs): don't remove the authority if we can't also + // reload it. + //a.scepAuthority = nil + } + return nil } @@ -640,48 +662,83 @@ func (a *Authority) init() error { return err } - // Check if a KMS with decryption capability is required and available - if a.requiresDecrypter() { - if _, ok := a.keyManager.(kmsapi.Decrypter); !ok { - return errors.New("keymanager doesn't provide crypto.Decrypter") + // The SCEP functionality is provided through an instance of + // scep.Authority. It is initialized when the CA is started and + // if it doesn't exist yet. It gets refreshed if it already + // exists. If the SCEP authority is no longer required on reload, + // it gets removed. + // TODO(hs): reloading through SIGHUP doesn't hit these cases. This + // is because an entirely new authority.Authority is created, including + // a new scep.Authority. Look into this to see if we want this to + // keep working like that, or want to reuse a single instance and + // update that. + switch { + case a.requiresSCEP() && a.GetSCEP() == nil: + if a.scepOptions == nil { + options := &scep.Options{ + Roots: a.rootX509Certs, + Intermediates: a.intermediateX509Certs, + SignerCert: a.intermediateX509Certs[0], + } + if options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ + SigningKey: a.config.IntermediateKey, + Password: a.password, + }); err != nil { + return err + } + // TODO(hs): instead of creating the decrypter here, pass the + // intermediate key + chain down to the SCEP 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, + Password: a.password, + }); err == nil { + // only pass the decrypter down when it was successfully created, + // meaning it's an RSA key, and `CreateDecrypter` did not fail. + options.Decrypter = decrypter + options.DecrypterCert = options.Intermediates[0] + } + } + + a.scepOptions = options } - } - // TODO: decide if this is a good approach for providing the SCEP functionality - // It currently mirrors the logic for the x509CAService - if a.requiresSCEPService() && a.scepService == nil { - var options scep.Options + // provide the current SCEP provisioner names, so that the provisioners + // can be validated when the CA is started. + a.scepOptions.SCEPProvisionerNames = a.getSCEPProvisionerNames() - // Read intermediate and create X509 signer and decrypter for default CAS. - options.CertificateChain, err = pemutil.ReadCertificateBundle(a.config.IntermediateCert) - if err != nil { - return err - } - options.CertificateChain = append(options.CertificateChain, a.rootX509Certs...) - options.Signer, err = a.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ - SigningKey: a.config.IntermediateKey, - Password: a.password, - }) + // create a new SCEP authority + scepAuthority, err := scep.New(a, *a.scepOptions) if err != nil { return err } - if km, ok := a.keyManager.(kmsapi.Decrypter); ok { - options.Decrypter, err = km.CreateDecrypter(&kmsapi.CreateDecrypterRequest{ - DecryptionKey: a.config.IntermediateKey, - Password: a.password, - }) - if err != nil { - return err + if a.validateSCEP { + // validate the SCEP authority + if err := scepAuthority.Validate(); err != nil { + a.initLogf("failed validating SCEP authority: %v", err) } } - a.scepService, err = scep.NewService(ctx, options) - if err != nil { - return err + // set the SCEP authority + a.scepAuthority = scepAuthority + case !a.requiresSCEP() && a.GetSCEP() != nil: + // clear the SCEP authority if it's no longer required + a.scepAuthority = nil + case a.requiresSCEP() && a.GetSCEP() != nil: + // update the SCEP Authority with the currently active SCEP + // provisioner names and revalidate the configuration. + a.scepAuthority.UpdateProvisioners(a.getSCEPProvisionerNames()) + if err := a.scepAuthority.Validate(); err != nil { + log.Printf("failed validating SCEP authority: %v\n", err) } - - // TODO: mimick the x509CAService GetCertificateAuthority here too? } // Load X509 constraints engine. @@ -833,17 +890,9 @@ func (a *Authority) IsRevoked(sn string) (bool, error) { return a.db.IsRevoked(sn) } -// requiresDecrypter returns whether the Authority -// requires a KMS that provides a crypto.Decrypter -// Currently this is only required when SCEP is -// 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 { +// requiresSCEP iterates over the configured provisioners +// and determines if at least one of them is a SCEP provisioner. +func (a *Authority) requiresSCEP() bool { for _, p := range a.config.AuthorityConfig.Provisioners { if p.GetType() == provisioner.TypeSCEP { return true @@ -852,13 +901,21 @@ func (a *Authority) requiresSCEPService() bool { 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 +// getSCEPProvisionerNames returns the names of the SCEP provisioners +// that are currently available in the CA. +func (a *Authority) getSCEPProvisionerNames() (names []string) { + for _, p := range a.config.AuthorityConfig.Provisioners { + if p.GetType() == provisioner.TypeSCEP { + names = append(names, p.GetName()) + } + } + + return +} + +// GetSCEP returns the configured SCEP Authority +func (a *Authority) GetSCEP() *scep.Authority { + return a.scepAuthority } func (a *Authority) startCRLGenerator() error { diff --git a/authority/authority_test.go b/authority/authority_test.go index 82a05a3e..45c7cd86 100644 --- a/authority/authority_test.go +++ b/authority/authority_test.go @@ -478,7 +478,7 @@ func testScepAuthority(t *testing.T, opts ...Option) *Authority { return a } -func TestAuthority_GetSCEPService(t *testing.T) { +func TestAuthority_GetSCEP(t *testing.T) { _ = testScepAuthority(t) p := provisioner.List{ &provisioner.SCEP{ @@ -542,7 +542,7 @@ func TestAuthority_GetSCEPService(t *testing.T) { return } 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) } } diff --git a/authority/options.go b/authority/options.go index bf443ed6..4fc5a20f 100644 --- a/authority/options.go +++ b/authority/options.go @@ -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/scep" ) // Option sets options to the Authority. @@ -205,6 +206,17 @@ func WithX509SignerFunc(fn func() ([]*x509.Certificate, crypto.Signer, error)) O } } +// WithFullSCEPOptions defines the options used for SCEP support. +// +// This feature is EXPERIMENTAL and might change at any time. +func WithFullSCEPOptions(options *scep.Options) Option { + return func(a *Authority) error { + a.scepOptions = options + a.validateSCEP = false + return nil + } +} + // WithSSHUserSigner defines the signer used to sign SSH user certificates. func WithSSHUserSigner(s crypto.Signer) Option { return func(a *Authority) error { diff --git a/authority/provisioner/scep.go b/authority/provisioner/scep.go index ff5b28d2..7648d3b0 100644 --- a/authority/provisioner/scep.go +++ b/authority/provisioner/scep.go @@ -2,13 +2,20 @@ package provisioner import ( "context" + "crypto" + "crypto/rsa" "crypto/subtle" + "crypto/x509" + "encoding/pem" "fmt" "net/http" "time" "github.com/pkg/errors" + "go.step.sm/crypto/kms" + kmsapi "go.step.sm/crypto/kms/apiv1" + "go.step.sm/crypto/kms/uri" "go.step.sm/linkedca" "github.com/smallstep/certificates/webhook" @@ -29,9 +36,19 @@ type SCEP struct { // intermediate in the GetCACerts response IncludeRoot bool `json:"includeRoot,omitempty"` + // ExcludeIntermediate makes the provisioner skip the intermediate CA in the + // GetCACerts response + ExcludeIntermediate bool `json:"excludeIntermediate,omitempty"` + // MinimumPublicKeyLength is the minimum length for public keys in CSRs MinimumPublicKeyLength int `json:"minimumPublicKeyLength,omitempty"` + // TODO(hs): also support a separate signer configuration? + DecrypterCertificate []byte `json:"decrypterCertificate,omitempty"` + DecrypterKeyPEM []byte `json:"decrypterKeyPEM,omitempty"` + DecrypterKeyURI string `json:"decrypterKey,omitempty"` + DecrypterKeyPassword []byte `json:"decrypterKeyPassword,omitempty"` + // 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 // Defaults to 0, being DES-CBC @@ -41,6 +58,12 @@ type SCEP struct { ctl *Controller encryptionAlgorithm int challengeValidationController *challengeValidationController + notificationController *notificationController + keyManager kmsapi.KeyManager + decrypter crypto.Decrypter + decrypterCertificate *x509.Certificate + signer crypto.Signer + signerCertificate *x509.Certificate } // GetID returns the provisioner unique identifier. @@ -113,7 +136,8 @@ func newChallengeValidationController(client *http.Client, webhooks []*Webhook) } var ( - ErrSCEPChallengeInvalid = errors.New("webhook server did not allow request") + ErrSCEPChallengeInvalid = errors.New("webhook server did not allow request") + ErrSCEPNotificationFailed = errors.New("scep notification failed") ) // Validate executes zero or more configured webhooks to @@ -122,12 +146,14 @@ var ( // that case, the other webhooks will be skipped. If none of // the webhooks indicates the value of the challenge was accepted, // an error is returned. -func (c *challengeValidationController) Validate(ctx context.Context, challenge, transactionID string) error { +func (c *challengeValidationController) Validate(ctx context.Context, csr *x509.CertificateRequest, challenge, transactionID string) error { for _, wh := range c.webhooks { - req := &webhook.RequestBody{ - SCEPChallenge: challenge, - SCEPTransactionID: transactionID, + req, err := webhook.NewRequestBody(webhook.WithX509CertificateRequest(csr)) + if err != nil { + return fmt.Errorf("failed creating new webhook request: %w", err) } + req.SCEPChallenge = challenge + req.SCEPTransactionID = transactionID resp, err := wh.DoWithContext(ctx, c.client, req, nil) // TODO(hs): support templated URL? Requires some refactoring if err != nil { return fmt.Errorf("failed executing webhook request: %w", err) @@ -140,6 +166,63 @@ func (c *challengeValidationController) Validate(ctx context.Context, challenge, return ErrSCEPChallengeInvalid } +type notificationController struct { + client *http.Client + webhooks []*Webhook +} + +// newNotificationController creates a new notificationController +// that performs SCEP notifications through webhooks. +func newNotificationController(client *http.Client, webhooks []*Webhook) *notificationController { + scepHooks := []*Webhook{} + for _, wh := range webhooks { + if wh.Kind != linkedca.Webhook_NOTIFYING.String() { + continue + } + if !isCertTypeOK(wh) { + continue + } + scepHooks = append(scepHooks, wh) + } + return ¬ificationController{ + client: client, + webhooks: scepHooks, + } +} + +func (c *notificationController) Success(ctx context.Context, csr *x509.CertificateRequest, cert *x509.Certificate, transactionID string) error { + for _, wh := range c.webhooks { + req, err := webhook.NewRequestBody(webhook.WithX509CertificateRequest(csr), webhook.WithX509Certificate(nil, cert)) // TODO(hs): pass in the x509util.Certifiate too? + if err != nil { + return fmt.Errorf("failed creating new webhook request: %w", err) + } + req.X509Certificate.Raw = cert.Raw // adding the full certificate DER bytes + req.SCEPTransactionID = transactionID + if _, err = wh.DoWithContext(ctx, c.client, req, nil); err != nil { + return fmt.Errorf("failed executing webhook request: %w: %w", ErrSCEPNotificationFailed, err) + } + } + + return nil +} + +func (c *notificationController) Failure(ctx context.Context, csr *x509.CertificateRequest, transactionID string, errorCode int, errorDescription string) error { + for _, wh := range c.webhooks { + req, err := webhook.NewRequestBody(webhook.WithX509CertificateRequest(csr)) + if err != nil { + return fmt.Errorf("failed creating new webhook request: %w", err) + } + req.SCEPTransactionID = transactionID + req.SCEPErrorCode = errorCode + req.SCEPErrorDescription = errorDescription + if _, err = wh.DoWithContext(ctx, c.client, req, nil); err != nil { + return fmt.Errorf("failed executing webhook request: %w: %w", ErrSCEPNotificationFailed, err) + } + } + + return nil +} + // isCertTypeOK returns whether or not the webhook can be used // with the SCEP challenge validation webhook controller. func isCertTypeOK(wh *Webhook) bool { @@ -162,21 +245,139 @@ func (s *SCEP) Init(config Config) (err error) { 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) } + // Set the encryption algorithm to use s.encryptionAlgorithm = s.EncryptionAlgorithmIdentifier // TODO(hs): we might want to upgrade the default security to AES-CBC? if s.encryptionAlgorithm < 0 || s.encryptionAlgorithm > 4 { return errors.New("only encryption algorithm identifiers from 0 to 4 are valid") } + // Prepare the SCEP challenge validator s.challengeValidationController = newChallengeValidationController( config.WebhookClient, s.GetOptions().GetWebhooks(), ) + // Prepare the SCEP notification controller + s.notificationController = newNotificationController( + config.WebhookClient, + s.GetOptions().GetWebhooks(), + ) + + // parse the decrypter key PEM contents if available + if decryptionKeyPEM := s.DecrypterKeyPEM; len(decryptionKeyPEM) > 0 { + // try reading the PEM for validation + block, rest := pem.Decode(decryptionKeyPEM) + if len(rest) > 0 { + return errors.New("failed parsing decrypter key: trailing data") + } + if block == nil { + return errors.New("failed parsing decrypter key: no PEM block found") + } + opts := kms.Options{ + Type: kmsapi.SoftKMS, + } + if s.keyManager, err = kms.New(context.Background(), opts); err != nil { + return fmt.Errorf("failed initializing kms: %w", err) + } + kmsDecrypter, ok := s.keyManager.(kmsapi.Decrypter) + if !ok { + return fmt.Errorf("%q is not a kmsapi.Decrypter", opts.Type) + } + if s.decrypter, err = kmsDecrypter.CreateDecrypter(&kmsapi.CreateDecrypterRequest{ + DecryptionKeyPEM: decryptionKeyPEM, + Password: s.DecrypterKeyPassword, + PasswordPrompter: kmsapi.NonInteractivePasswordPrompter, + }); err != nil { + return fmt.Errorf("failed creating decrypter: %w", err) + } + if s.signer, err = s.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ + SigningKeyPEM: decryptionKeyPEM, // TODO(hs): support distinct signer key in the future? + Password: s.DecrypterKeyPassword, + PasswordPrompter: kmsapi.NonInteractivePasswordPrompter, + }); err != nil { + return fmt.Errorf("failed creating signer: %w", err) + } + } + + if decryptionKeyURI := s.DecrypterKeyURI; len(decryptionKeyURI) > 0 { + u, err := uri.Parse(s.DecrypterKeyURI) + if err != nil { + return fmt.Errorf("failed parsing decrypter key: %w", err) + } + var kmsType kmsapi.Type + switch { + case u.Scheme != "": + kmsType = kms.Type(u.Scheme) + default: + kmsType = kmsapi.SoftKMS + } + opts := kms.Options{ + Type: kmsType, + URI: s.DecrypterKeyURI, + } + if s.keyManager, err = kms.New(context.Background(), opts); err != nil { + return fmt.Errorf("failed initializing kms: %w", err) + } + kmsDecrypter, ok := s.keyManager.(kmsapi.Decrypter) + if !ok { + return fmt.Errorf("%q is not a kmsapi.Decrypter", opts.Type) + } + if kmsType != "softkms" { // TODO(hs): this should likely become more transparent? + decryptionKeyURI = u.Opaque + } + if s.decrypter, err = kmsDecrypter.CreateDecrypter(&kmsapi.CreateDecrypterRequest{ + DecryptionKey: decryptionKeyURI, + Password: s.DecrypterKeyPassword, + PasswordPrompter: kmsapi.NonInteractivePasswordPrompter, + }); err != nil { + return fmt.Errorf("failed creating decrypter: %w", err) + } + if s.signer, err = s.keyManager.CreateSigner(&kmsapi.CreateSignerRequest{ + SigningKey: decryptionKeyURI, // TODO(hs): support distinct signer key in the future? + Password: s.DecrypterKeyPassword, + PasswordPrompter: kmsapi.NonInteractivePasswordPrompter, + }); err != nil { + return fmt.Errorf("failed creating signer: %w", err) + } + } + + // parse the decrypter certificate contents if available + if len(s.DecrypterCertificate) > 0 { + 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) + } + // the decrypter certificate is also the signer certificate + s.signerCertificate = s.decrypterCertificate + } + + // TODO(hs): alternatively, check if the KMS keyManager is a CertificateManager + // and load the certificate corresponding to the decryption key? + + // Final validation for the decrypter. + if s.decrypter != nil { + decrypterPublicKey, ok := s.decrypter.Public().(*rsa.PublicKey) + if !ok { + return fmt.Errorf("only RSA keys are supported") + } + if s.decrypterCertificate == nil { + return fmt.Errorf("provisioner %q does not have a decrypter certificate set", s.Name) + } + if !decrypterPublicKey.Equal(s.decrypterCertificate.PublicKey) { + return errors.New("mismatch between decrypter certificate and decrypter public keys") + } + } + // TODO: add other, SCEP specific, options? s.ctl, err = NewController(s, s.Claims, config, s.Options) @@ -214,6 +415,15 @@ func (s *SCEP) ShouldIncludeRootInChain() bool { return s.IncludeRoot } +// ShouldIncludeIntermediateInChain indicates if the +// CA should include the intermediate CA certificate in the +// GetCACerts response. This is true by default, but can be +// overridden through configuration in case SCEP clients +// don't pick the right recipient. +func (s *SCEP) ShouldIncludeIntermediateInChain() bool { + return !s.ExcludeIntermediate +} + // GetContentEncryptionAlgorithm returns the numeric identifier // for the pkcs7 package encryption algorithm to use. func (s *SCEP) GetContentEncryptionAlgorithm() int { @@ -223,13 +433,13 @@ func (s *SCEP) GetContentEncryptionAlgorithm() int { // ValidateChallenge validates the provided challenge. It starts by // selecting the validation method to use, then performs validation // according to that method. -func (s *SCEP) ValidateChallenge(ctx context.Context, challenge, transactionID string) error { +func (s *SCEP) ValidateChallenge(ctx context.Context, csr *x509.CertificateRequest, challenge, transactionID string) error { if s.challengeValidationController == nil { return fmt.Errorf("provisioner %q wasn't initialized", s.Name) } switch s.selectValidationMethod() { case validationMethodWebhook: - return s.challengeValidationController.Validate(ctx, challenge, transactionID) + return s.challengeValidationController.Validate(ctx, csr, challenge, transactionID) default: if subtle.ConstantTimeCompare([]byte(s.ChallengePassword), []byte(challenge)) == 0 { return errors.New("invalid challenge password provided") @@ -238,6 +448,20 @@ func (s *SCEP) ValidateChallenge(ctx context.Context, challenge, transactionID s } } +func (s *SCEP) NotifySuccess(ctx context.Context, csr *x509.CertificateRequest, cert *x509.Certificate, transactionID string) error { + if s.notificationController == nil { + return fmt.Errorf("provisioner %q wasn't initialized", s.Name) + } + return s.notificationController.Success(ctx, csr, cert, transactionID) +} + +func (s *SCEP) NotifyFailure(ctx context.Context, csr *x509.CertificateRequest, transactionID string, errorCode int, errorDescription string) error { + if s.notificationController == nil { + return fmt.Errorf("provisioner %q wasn't initialized", s.Name) + } + return s.notificationController.Failure(ctx, csr, transactionID, errorCode, errorDescription) +} + type validationMethod string const ( @@ -259,3 +483,20 @@ func (s *SCEP) selectValidationMethod() validationMethod { } return validationMethodNone } + +// GetDecrypter returns the provisioner specific decrypter, +// used to decrypt SCEP request messages sent by a SCEP client. +// The decrypter consists of a crypto.Decrypter (a private key) +// and a certificate for the public key corresponding to the +// private key. +func (s *SCEP) GetDecrypter() (*x509.Certificate, crypto.Decrypter) { + return s.decrypterCertificate, s.decrypter +} + +// GetSigner returns the provisioner specific signer, used to +// sign SCEP response messages for the client. The signer consists +// of a crypto.Signer and a certificate for the public key +// corresponding to the private key. +func (s *SCEP) GetSigner() (*x509.Certificate, crypto.Signer) { + return s.signerCertificate, s.signer +} diff --git a/authority/provisioner/scep_test.go b/authority/provisioner/scep_test.go index acf047fb..4efb3dd8 100644 --- a/authority/provisioner/scep_test.go +++ b/authority/provisioner/scep_test.go @@ -2,6 +2,7 @@ package provisioner import ( "context" + "crypto/x509" "encoding/json" "errors" "net/http" @@ -12,12 +13,18 @@ import ( "github.com/stretchr/testify/require" "go.step.sm/linkedca" + + "github.com/smallstep/certificates/webhook" ) func Test_challengeValidationController_Validate(t *testing.T) { + dummyCSR := &x509.CertificateRequest{ + Raw: []byte{1}, + } type request struct { - Challenge string `json:"scepChallenge"` - TransactionID string `json:"scepTransactionID"` + Request *webhook.X509CertificateRequest `json:"x509CertificateRequest,omitempty"` + Challenge string `json:"scepChallenge"` + TransactionID string `json:"scepTransactionID"` } type response struct { Allow bool `json:"allow"` @@ -39,6 +46,9 @@ func Test_challengeValidationController_Validate(t *testing.T) { require.NoError(t, err) assert.Equal(t, "challenge", req.Challenge) assert.Equal(t, "transaction-1", req.TransactionID) + if assert.NotNil(t, req.Request) { + assert.Equal(t, []byte{1}, req.Request.Raw) + } b, err := json.Marshal(response{Allow: true}) require.NoError(t, err) w.WriteHeader(200) @@ -141,7 +151,7 @@ func Test_challengeValidationController_Validate(t *testing.T) { } ctx := context.Background() - err := c.Validate(ctx, tt.args.challenge, tt.args.transactionID) + err := c.Validate(ctx, dummyCSR, tt.args.challenge, tt.args.transactionID) if tt.expErr != nil { assert.EqualError(t, err, tt.expErr.Error()) @@ -221,9 +231,13 @@ func Test_selectValidationMethod(t *testing.T) { } func TestSCEP_ValidateChallenge(t *testing.T) { + dummyCSR := &x509.CertificateRequest{ + Raw: []byte{1}, + } type request struct { - Challenge string `json:"scepChallenge"` - TransactionID string `json:"scepTransactionID"` + Request *webhook.X509CertificateRequest `json:"x509CertificateRequest,omitempty"` + Challenge string `json:"scepChallenge"` + TransactionID string `json:"scepTransactionID"` } type response struct { Allow bool `json:"allow"` @@ -234,6 +248,9 @@ func TestSCEP_ValidateChallenge(t *testing.T) { require.NoError(t, err) assert.Equal(t, "webhook-challenge", req.Challenge) assert.Equal(t, "webhook-transaction-1", req.TransactionID) + if assert.NotNil(t, req.Request) { + assert.Equal(t, []byte{1}, req.Request.Raw) + } b, err := json.Marshal(response{Allow: true}) require.NoError(t, err) w.WriteHeader(200) @@ -330,7 +347,7 @@ func TestSCEP_ValidateChallenge(t *testing.T) { require.NoError(t, err) ctx := context.Background() - err = tt.p.ValidateChallenge(ctx, tt.args.challenge, tt.args.transactionID) + err = tt.p.ValidateChallenge(ctx, dummyCSR, tt.args.challenge, tt.args.transactionID) if tt.expErr != nil { assert.EqualError(t, err, tt.expErr.Error()) return diff --git a/authority/provisioners.go b/authority/provisioners.go index 0d38f667..747517c9 100644 --- a/authority/provisioners.go +++ b/authority/provisioners.go @@ -235,7 +235,7 @@ func (a *Authority) StoreProvisioner(ctx context.Context, prov *linkedca.Provisi } 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. @@ -974,7 +974,7 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface, }, nil case *linkedca.ProvisionerDetails_SCEP: cfg := d.SCEP - return &provisioner.SCEP{ + s := &provisioner.SCEP{ ID: p.Id, Type: p.Type.String(), Name: p.Name, @@ -982,11 +982,19 @@ func ProvisionerToCertificates(p *linkedca.Provisioner) (provisioner.Interface, ChallengePassword: cfg.Challenge, Capabilities: cfg.Capabilities, IncludeRoot: cfg.IncludeRoot, + ExcludeIntermediate: cfg.ExcludeIntermediate, MinimumPublicKeyLength: int(cfg.MinimumPublicKeyLength), EncryptionAlgorithmIdentifier: int(cfg.EncryptionAlgorithmIdentifier), Claims: claims, Options: options, - }, nil + } + if decrypter := cfg.GetDecrypter(); decrypter != nil { + s.DecrypterCertificate = decrypter.Certificate + s.DecrypterKeyPEM = decrypter.Key + s.DecrypterKeyURI = decrypter.KeyUri + s.DecrypterKeyPassword = decrypter.KeyPassword + } + return s, nil case *linkedca.ProvisionerDetails_Nebula: var roots []byte for i, root := range d.Nebula.GetRoots() { @@ -1241,7 +1249,14 @@ func ProvisionerToLinkedca(p provisioner.Interface) (*linkedca.Provisioner, erro Capabilities: p.Capabilities, MinimumPublicKeyLength: int32(p.MinimumPublicKeyLength), IncludeRoot: p.IncludeRoot, + ExcludeIntermediate: p.ExcludeIntermediate, EncryptionAlgorithmIdentifier: int32(p.EncryptionAlgorithmIdentifier), + Decrypter: &linkedca.SCEPDecrypter{ + Certificate: p.DecrypterCertificate, + Key: p.DecrypterKeyPEM, + KeyUri: p.DecrypterKeyURI, + KeyPassword: p.DecrypterKeyPassword, + }, }, }, }, diff --git a/ca/ca.go b/ca/ca.go index 7a86ac14..7baf2419 100644 --- a/ca/ca.go +++ b/ca/ca.go @@ -250,19 +250,14 @@ func (ca *CA) Init(cfg *config.Config) (*CA, error) { var scepAuthority *scep.Authority if ca.shouldServeSCEPEndpoints() { - scepPrefix := "scep" - scepAuthority, err = scep.New(auth, scep.AuthorityOptions{ - Service: auth.GetSCEPService(), - DNS: dns, - Prefix: scepPrefix, - }) - if err != nil { - return nil, errors.Wrap(err, "error creating SCEP authority") - } + // get the SCEP authority configuration. Validation is + // performed within the authority instantiation process. + scepAuthority = auth.GetSCEP() // 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 // to the insecure mux. + scepPrefix := "scep" insecureMux.Route("/"+scepPrefix, func(r chi.Router) { scepAPI.Route(r) }) @@ -584,10 +579,10 @@ func (ca *CA) getTLSConfig(auth *authority.Authority) (*tls.Config, *tls.Config, // shouldServeSCEPEndpoints returns if the CA should be // configured with endpoints for SCEP. This is assumed to be -// true if a SCEPService exists, which is true in case a -// SCEP provisioner was configured. +// true if a SCEPService exists, which is true in case at +// least one SCEP provisioner was configured. func (ca *CA) shouldServeSCEPEndpoints() bool { - return ca.auth.GetSCEPService() != nil + return ca.auth.GetSCEP() != nil } //nolint:unused // useful for debugging diff --git a/go.mod b/go.mod index 9f9814d4..0895bdce 100644 --- a/go.mod +++ b/go.mod @@ -32,7 +32,7 @@ require ( go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.step.sm/cli-utils v0.8.0 go.step.sm/crypto v0.35.1 - go.step.sm/linkedca v0.20.0 + go.step.sm/linkedca v0.20.1 golang.org/x/crypto v0.13.0 golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 golang.org/x/net v0.15.0 @@ -43,7 +43,7 @@ require ( ) require ( - cloud.google.com/go v0.110.6 // indirect + cloud.google.com/go v0.110.7 // indirect cloud.google.com/go/compute v1.23.0 // indirect cloud.google.com/go/compute/metadata v0.2.3 // indirect cloud.google.com/go/iam v1.1.1 // indirect @@ -69,19 +69,19 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dgraph-io/ristretto v0.1.0 // indirect github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect - github.com/dustin/go-humanize v1.0.0 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect github.com/go-jose/go-jose/v3 v3.0.0 // indirect github.com/go-kit/kit v0.10.0 // indirect github.com/go-logfmt/logfmt v0.5.1 // indirect github.com/go-piv/piv-go v1.11.0 // indirect - github.com/go-sql-driver/mysql v1.7.0 // indirect + github.com/go-sql-driver/mysql v1.7.1 // indirect github.com/golang-jwt/jwt/v5 v5.0.0 // indirect github.com/golang/glog v1.1.0 // indirect github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/golang/snappy v0.0.4 // indirect github.com/google/btree v1.1.2 // indirect - github.com/google/certificate-transparency-go v1.1.4 // indirect + github.com/google/certificate-transparency-go v1.1.6 // indirect github.com/google/go-tpm-tools v0.4.1 // indirect github.com/google/go-tspi v0.3.0 // indirect github.com/google/s2a-go v0.1.7 // indirect @@ -136,13 +136,13 @@ require ( golang.org/x/sync v0.3.0 // indirect golang.org/x/sys v0.12.0 // indirect golang.org/x/text v0.13.0 // indirect - golang.org/x/time v0.1.0 // indirect + golang.org/x/time v0.3.0 // indirect google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 // indirect + google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb // indirect google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) // 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-20230615175518-7ce6486b74eb diff --git a/go.sum b/go.sum index 28cda95e..403f6eaf 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.110.6 h1:8uYAkj3YHTP/1iwReuHPxLSbdcyc+dSBbzFMrVwDR6Q= -cloud.google.com/go v0.110.6/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= +cloud.google.com/go v0.110.7 h1:rJyC7nWRg2jWGZ4wSJ5nY65GTdYJkg0cd/uXb+ACI6o= +cloud.google.com/go v0.110.7/go.mod h1:+EYjdK8e5RME/VY/qLCAtuyALQ9q67dvuum8i+H5xsI= cloud.google.com/go/compute v1.23.0 h1:tP41Zoavr8ptEqaW6j+LQOnyBBhO7OkOMAGrgLopTwY= cloud.google.com/go/compute v1.23.0/go.mod h1:4tCnrn48xsqlwSAiLf1HXMQk8CONslYbdiEZc9FEIbM= cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= @@ -126,8 +126,9 @@ github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 h1:fAjc9m62+UWV/WA github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13/go.mod h1:SqUrOPUnsFjfmXRMNPybcSiG0BgUW2AuFH8PAnS2iTw= github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= @@ -163,8 +164,8 @@ github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KE github.com/go-piv/piv-go v1.11.0 h1:5vAaCdRTFSIW4PeqMbnsDlUZ7odMYWnHBDGdmtU/Zhg= github.com/go-piv/piv-go v1.11.0/go.mod h1:NZ2zmjVkfFaL/CF8cVQ/pXdXtuj110zEKGdJM6fJZZM= github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= -github.com/go-sql-driver/mysql v1.7.0 h1:ueSltNNllEqE3qcWBTD0iQd3IpL/6U+mJxLkazJ7YPc= -github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= +github.com/go-sql-driver/mysql v1.7.1 h1:lUIinVbN1DY0xBg0eMOzmmtGoHwWBbvnWubQUrtU8EI= +github.com/go-sql-driver/mysql v1.7.1/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI= github.com/go-stack/stack v1.6.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= @@ -211,8 +212,8 @@ github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/certificate-transparency-go v1.0.21/go.mod h1:QeJfpSbVSfYc7RgB3gJFj9cbuQMMchQxrWXz8Ruopmg= -github.com/google/certificate-transparency-go v1.1.4 h1:hCyXHDbtqlr/lMXU0D4WgbalXL0Zk4dSWWMbPV8VrqY= -github.com/google/certificate-transparency-go v1.1.4/go.mod h1:D6lvbfwckhNrbM9WVl1EVeMOyzC19mpIjMOI4nxBHtQ= +github.com/google/certificate-transparency-go v1.1.6 h1:SW5K3sr7ptST/pIvNkSVWMiJqemRmkjJPPT0jzXdOOY= +github.com/google/certificate-transparency-go v1.1.6/go.mod h1:0OJjOsOk+wj6aYQgP7FU0ioQ0AJUmnWPFMqTjQeazPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -382,6 +383,7 @@ github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxv github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= @@ -391,8 +393,8 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+ github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2 h1:AqzbZs4ZoCBp+GtejcpCpcxM3zlSMx29dXbUSeVtJb8= github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= @@ -457,7 +459,6 @@ github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxzi github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/newrelic/go-agent/v3 v3.25.1 h1:Fa+4apO08bcGJk9aOB0TlnacAOrXS4FzMYJzoG0ihA8= github.com/newrelic/go-agent/v3 v3.25.1/go.mod h1:MANAXqchXM8ko+EXPZ+6mzX243/lehYwJWq8HOV2ytc= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= @@ -512,6 +513,7 @@ github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+Gx github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= github.com/rs/xid v1.5.0 h1:mKX4bl4iPYJtEIxp6CYiUuLQ/8DYMoz0PUdtGgMFRVc= github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= @@ -548,8 +550,8 @@ github.com/smallstep/go-attestation v0.4.4-0.20230627102604-cf579e53cbd2 h1:UIAS github.com/smallstep/go-attestation v0.4.4-0.20230627102604-cf579e53cbd2/go.mod h1:vNAduivU014fubg6ewygkAvQC0IQVXqdc8vaGl/0er4= github.com/smallstep/nosql v0.6.0 h1:ur7ysI8s9st0cMXnTvB8tA3+x5Eifmkb6hl4uqNV5jc= github.com/smallstep/nosql v0.6.0/go.mod h1:jOXwLtockXORUPPZ2MCUcIkGR6w0cN1QGZniY9DITQA= -github.com/smallstep/pkcs7 v0.0.0-20230302202335-4c094085c948 h1:/80FqDt6pzL9clNW8G2IsRAzKGNAuzsEs7g1Y5oaM/Y= -github.com/smallstep/pkcs7 v0.0.0-20230302202335-4c094085c948/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= +github.com/smallstep/pkcs7 v0.0.0-20230615175518-7ce6486b74eb h1:wWc8z37baPz2oyusY9BVuM+uPtq6XAOb7qSegevnRs0= +github.com/smallstep/pkcs7 v0.0.0-20230615175518-7ce6486b74eb/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= @@ -619,8 +621,8 @@ go.step.sm/cli-utils v0.8.0 h1:b/Tc1/m3YuQq+u3ghTFP7Dz5zUekZj6GUmd5pCvkEXQ= go.step.sm/cli-utils v0.8.0/go.mod h1:S77aISrC0pKuflqiDfxxJlUbiXcAanyJ4POOnzFSxD4= go.step.sm/crypto v0.35.1 h1:QAZZ7Q8xaM4TdungGSAYw/zxpyH4fMYTkfaXVV9H7pY= go.step.sm/crypto v0.35.1/go.mod h1:vn8Vkx/Mbqgoe7AG8btC0qZ995Udm3e+JySuDS1LCJA= -go.step.sm/linkedca v0.20.0 h1:bH41rvyDm3nSSJ5xgGsKUZOpzJcq5x2zacMIeqtq9oI= -go.step.sm/linkedca v0.20.0/go.mod h1:eybHw6ZTpuFmkUQnTBRWM2SPIGaP0VbYeo1bupfPT70= +go.step.sm/linkedca v0.20.1 h1:bHDn1+UG1NgRrERkWbbCiAIvv4lD5NOFaswPDTyO5vU= +go.step.sm/linkedca v0.20.1/go.mod h1:Vaq4+Umtjh7DLFI1KuIxeo598vfBzgSYZUjgVJ7Syxw= 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.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= @@ -765,8 +767,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.1.0 h1:xYY+Bajn2a7VBmTM5GikTmnK8ZuX8YgnQCqZpbBNtmA= -golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -805,12 +807,12 @@ google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5 h1:L6iMMGrtzgHsWofoFcihmDEMYeDR9KN/ThbPWGrh++g= -google.golang.org/genproto v0.0.0-20230803162519-f966b187b2e5/go.mod h1:oH/ZOT02u4kWEp7oYBGYFFkCdKS/uYR9Z7+0/xuuFp8= +google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb h1:XFBgcDwm7irdHTbz4Zk2h7Mh+eis4nfJEFQFYzJzuIA= +google.golang.org/genproto v0.0.0-20230913181813-007df8e322eb/go.mod h1:yZTlhN0tQnXo3h00fuXNCxJdLdIdnVFVBaRJ5LWBbw4= google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5 h1:nIgk/EEq3/YlnmVVXVnm14rC2oxgs1o0ong4sD/rd44= google.golang.org/genproto/googleapis/api v0.0.0-20230803162519-f966b187b2e5/go.mod h1:5DZzOUPCLYL3mNkQ0ms0F3EuUNZ7py1Bqeq6sxzI7/Q= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb h1:Isk1sSH7bovx8Rti2wZK0UZF6oraBDK74uoyLEEVFN0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230913181813-007df8e322eb/go.mod h1:+Bk1OCOj40wS2hwAMA+aCW9ypzm63QTBBHp6lQ3p+9M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13 h1:N3bU/SQDCDyD6R528GJ/PwW9KjYcJA3dgyH+MovAkIM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:KSqppvjFjtoCI+KGd4PELB0qLNxdJHRGqRI09mB6pQA= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= @@ -841,8 +843,8 @@ google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqw gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= diff --git a/scep/api/api.go b/scep/api/api.go index 60e5f710..614b5184 100644 --- a/scep/api/api.go +++ b/scep/api/api.go @@ -18,6 +18,7 @@ import ( "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/api/log" + "github.com/smallstep/certificates/authority" "github.com/smallstep/certificates/authority/provisioner" "github.com/smallstep/certificates/scep" ) @@ -208,7 +209,7 @@ func lookupProvisioner(next http.HandlerFunc) http.HandlerFunc { } ctx := r.Context() - auth := scep.MustFromContext(ctx) + auth := authority.MustFromContext(ctx) p, err := auth.LoadProvisionerByName(provisionerName) if err != nil { fail(w, err) @@ -221,7 +222,7 @@ func lookupProvisioner(next http.HandlerFunc) http.HandlerFunc { return } - ctx = context.WithValue(ctx, scep.ProvisionerContextKey, scep.Provisioner(prov)) + ctx = scep.NewProvisionerContext(ctx, scep.Provisioner(prov)) next(w, r.WithContext(ctx)) } } @@ -314,7 +315,7 @@ func PKIOperation(ctx context.Context, req request) (Response, error) { // a certificate exists; then it will use RenewalReq. Adding the challenge check here may be a small breaking change for clients. // We'll have to see how it works out. if msg.MessageType == microscep.PKCSReq || msg.MessageType == microscep.RenewalReq { - if err := auth.ValidateChallenge(ctx, challengePassword, transactionID); err != nil { + if err := auth.ValidateChallenge(ctx, csr, challengePassword, transactionID); err != nil { if errors.Is(err, provisioner.ErrSCEPChallengeInvalid) { return createFailureResponse(ctx, csr, msg, microscep.BadRequest, err) } @@ -332,9 +333,18 @@ func PKIOperation(ctx context.Context, req request) (Response, error) { certRep, err := auth.SignCSR(ctx, csr, msg) if err != nil { + if notifyErr := auth.NotifyFailure(ctx, csr, transactionID, 0, err.Error()); notifyErr != nil { + // TODO(hs): ignore this error case? It's not critical if the notification fails; but logging it might be good + _ = notifyErr + } return createFailureResponse(ctx, csr, msg, microscep.BadRequest, fmt.Errorf("error when signing new certificate: %w", err)) } + if notifyErr := auth.NotifySuccess(ctx, csr, certRep.Certificate, transactionID); notifyErr != nil { + // TODO(hs): ignore this error case? It's not critical if the notification fails; but logging it might be good + _ = notifyErr + } + res := Response{ Operation: opnPKIOperation, Data: certRep.Raw, diff --git a/scep/authority.go b/scep/authority.go index 23c28813..292c7004 100644 --- a/scep/authority.go +++ b/scep/authority.go @@ -2,10 +2,11 @@ package scep import ( "context" + "crypto" "crypto/x509" "errors" "fmt" - "net/url" + "sync" microx509util "github.com/micromdm/scep/v2/cryptoutil/x509util" microscep "github.com/micromdm/scep/v2/scep" @@ -18,12 +19,17 @@ import ( // Authority is the layer that handles all SCEP interactions. type Authority struct { - prefix string - dns string - intermediateCertificate *x509.Certificate - caCerts []*x509.Certificate // TODO(hs): change to use these instead of root and intermediate - service *Service - signAuth SignAuthority + signAuth SignAuthority + roots []*x509.Certificate + intermediates []*x509.Certificate + defaultSigner crypto.Signer + signerCertificate *x509.Certificate + defaultDecrypter crypto.Decrypter + decrypterCertificate *x509.Certificate + scepProvisionerNames []string + + provisionersMutex sync.RWMutex + encryptionAlgorithmMutex sync.Mutex } type authorityKey struct{} @@ -49,19 +55,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 type SignAuthority interface { Sign(cr *x509.CertificateRequest, opts provisioner.SignOptions, signOpts ...provisioner.SignOption) ([]*x509.Certificate, error) @@ -69,24 +62,67 @@ type SignAuthority interface { } // New returns a new Authority that implements the SCEP interface. -func New(signAuth SignAuthority, ops AuthorityOptions) (*Authority, error) { - authority := &Authority{ - prefix: ops.Prefix, - dns: ops.DNS, - signAuth: signAuth, +func New(signAuth SignAuthority, opts Options) (*Authority, error) { + if err := opts.Validate(); err != nil { + return nil, err + } + + return &Authority{ + signAuth: signAuth, // TODO: provide signAuth through context instead? + roots: opts.Roots, + intermediates: opts.Intermediates, + defaultSigner: opts.Signer, + signerCertificate: opts.SignerCert, + defaultDecrypter: opts.Decrypter, + decrypterCertificate: opts.SignerCert, // the intermediate signer cert is also the decrypter cert (if RSA) + scepProvisionerNames: opts.SCEPProvisionerNames, + }, 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 { + if a == nil { + return nil } - // 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 + a.provisionersMutex.RLock() + defer a.provisionersMutex.RUnlock() + + 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 authority, nil + 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) { + if a == nil { + return + } + + a.provisionersMutex.Lock() + defer a.provisionersMutex.Unlock() + + a.scepProvisionerNames = scepProvisionerNames } var ( @@ -108,87 +144,58 @@ func (a *Authority) LoadProvisionerByName(name string) (provisioner.Interface, e return a.signAuth.LoadProvisionerByName(name) } -// GetLinkExplicit returns the requested link from the directory. -func (a *Authority) GetLinkExplicit(provName string, abs bool, baseURL *url.URL, inputs ...string) string { - return a.getLinkExplicit(provName, abs, baseURL, inputs...) -} - -// getLinkExplicit returns an absolute or partial path to the given resource and a base -// URL dynamically obtained from the request for which the link is being calculated. -func (a *Authority) getLinkExplicit(provisionerName string, abs bool, baseURL *url.URL, _ ...string) string { - link := "/" + provisionerName - 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 -} - -// GetCACertificates returns the certificate (chain) for the CA -func (a *Authority) GetCACertificates(ctx context.Context) ([]*x509.Certificate, error) { - // TODO: this should return: 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 - // - // 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]) +// GetCACertificates returns the certificate (chain) for the CA. +// +// 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) + + // if a provisioner specific RSA decrypter is available, it is returned as + // the first certificate. + if decrypterCertificate, _ := p.GetDecrypter(); decrypterCertificate != nil { + certs = append(certs, decrypterCertificate) + } + + // the CA intermediate is added to the chain by default. It's possible to + // exclude it from being added through configuration. This can be useful in + // environments where the SCEP client doesn't select the right RSA decrypter + // certificate, resulting in the wrong recipient in the PKCS7 message. + if p.ShouldIncludeIntermediateInChain() || len(certs) == 0 { + // 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...) + } + + // the CA roots are added for completeness when configured to do so. Clients + // are responsible to select the right cert(s) to store and use. + if p.ShouldIncludeRootInChain() { + certs = append(certs, a.roots...) } return certs, nil } // 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) if err != nil { return fmt.Errorf("error parsing pkcs7 content: %w", err) } - envelope, err := p7c.Decrypt(a.intermediateCertificate, a.service.decrypter) + cert, decrypter, err := a.selectDecrypter(ctx) + if err != nil { + return fmt.Errorf("failed selecting decrypter: %w", err) + } + + envelope, err := p7c.Decrypt(cert, decrypter) if err != nil { return fmt.Errorf("error decrypting encrypted pkcs7 content: %w", err) } @@ -208,7 +215,10 @@ func (a *Authority) DecryptPKIEnvelope(_ context.Context, msg *PKIMessage) error if err != nil { return fmt.Errorf("parse CSR from pkiEnvelope: %w", err) } - // check for challengePassword + if err := csr.CheckSignature(); err != nil { + return fmt.Errorf("invalid CSR signature; %w", err) + } + // extract the challenge password cp, err := microx509util.ParseChallengePassword(msg.pkiEnvelope) if err != nil { return fmt.Errorf("parse challenge password in pkiEnvelope: %w", err) @@ -234,10 +244,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 // the implementation after the one in the ACME authority. Requires storage, etc. - p, err := provisionerFromContext(ctx) - if err != nil { - return nil, err - } + p := provisionerFromContext(ctx) // check if CSRReqMessage has already been decrypted if msg.CSRReqMessage.CSR == nil { @@ -307,20 +314,13 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m // and create a degenerate cert structure deg, err := microscep.DegenerateCertificates([]*x509.Certificate{cert}) if err != nil { - return nil, err + return nil, fmt.Errorf("failed generating degenerate certificate: %w", err) } - // apparently the pkcs7 library uses a global default setting for the content encryption - // algorithm to use when en- or decrypting data. We need to restore the current setting after - // the cryptographic operation, so that other usages of the library are not influenced by - // this call to Encrypt(). We are not required to use the same algorithm the SCEP client uses. - encryptionAlgorithmToRestore := pkcs7.ContentEncryptionAlgorithm - pkcs7.ContentEncryptionAlgorithm = p.GetContentEncryptionAlgorithm() - e7, err := pkcs7.Encrypt(deg, msg.P7.Certificates) + e7, err := a.encrypt(deg, msg.P7.Certificates, p.GetContentEncryptionAlgorithm()) if err != nil { - return nil, err + return nil, fmt.Errorf("failed encrypting degenerate certificate: %w", err) } - pkcs7.ContentEncryptionAlgorithm = encryptionAlgorithmToRestore // PKIMessageAttributes to be signed config := pkcs7.SignerInfoConfig{ @@ -358,10 +358,13 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m // as the first certificate in the array signedData.AddCertificate(cert) - authCert := a.intermediateCertificate + signerCert, signer, err := a.selectSigner(ctx) + if err != nil { + return nil, fmt.Errorf("failed selecting signer: %w", err) + } // sign the attributes - if err := signedData.AddSigner(authCert, a.service.signer, config); err != nil { + if err := signedData.AddSigner(signerCert, signer, config); err != nil { return nil, err } @@ -388,8 +391,30 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m return crepMsg, nil } +func (a *Authority) encrypt(content []byte, recipients []*x509.Certificate, algorithm int) ([]byte, error) { + // apparently the pkcs7 library uses a global default setting for the content encryption + // algorithm to use when en- or decrypting data. We need to restore the current setting after + // the cryptographic operation, so that other usages of the library are not influenced by + // this call to Encrypt(). We are not required to use the same algorithm the SCEP client uses. + a.encryptionAlgorithmMutex.Lock() + defer a.encryptionAlgorithmMutex.Unlock() + + encryptionAlgorithmToRestore := pkcs7.ContentEncryptionAlgorithm + defer func() { + pkcs7.ContentEncryptionAlgorithm = encryptionAlgorithmToRestore + }() + + pkcs7.ContentEncryptionAlgorithm = algorithm + e7, err := pkcs7.Encrypt(content, recipients) + if err != nil { + return nil, err + } + + return e7, nil +} + // CreateFailureResponse creates an appropriately signed reply for PKI operations -func (a *Authority) CreateFailureResponse(_ context.Context, _ *x509.CertificateRequest, msg *PKIMessage, info FailInfoName, infoText string) (*PKIMessage, error) { +func (a *Authority) CreateFailureResponse(ctx context.Context, _ *x509.CertificateRequest, msg *PKIMessage, info FailInfoName, infoText string) (*PKIMessage, error) { config := pkcs7.SignerInfoConfig{ ExtraSignedAttributes: []pkcs7.Attribute{ { @@ -428,8 +453,13 @@ func (a *Authority) CreateFailureResponse(_ context.Context, _ *x509.Certificate return nil, err } + signerCert, signer, err := a.selectSigner(ctx) + if err != nil { + return nil, fmt.Errorf("failed selecting signer: %w", err) + } + // sign the attributes - if err := signedData.AddSigner(a.intermediateCertificate, a.service.signer, config); err != nil { + if err := signedData.AddSigner(signerCert, signer, config); err != nil { return nil, err } @@ -457,10 +487,7 @@ func (a *Authority) CreateFailureResponse(_ context.Context, _ *x509.Certificate // GetCACaps returns the CA capabilities func (a *Authority) GetCACaps(ctx context.Context) []string { - p, err := provisionerFromContext(ctx) - if err != nil { - return defaultCapabilities - } + p := provisionerFromContext(ctx) caps := p.GetCapabilities() if len(caps) == 0 { @@ -476,10 +503,63 @@ func (a *Authority) GetCACaps(ctx context.Context) []string { return caps } -func (a *Authority) ValidateChallenge(ctx context.Context, challenge, transactionID string) error { - p, err := provisionerFromContext(ctx) - if err != nil { - return err +func (a *Authority) ValidateChallenge(ctx context.Context, csr *x509.CertificateRequest, challenge, transactionID string) error { + p := provisionerFromContext(ctx) + return p.ValidateChallenge(ctx, csr, challenge, transactionID) +} + +func (a *Authority) NotifySuccess(ctx context.Context, csr *x509.CertificateRequest, cert *x509.Certificate, transactionID string) error { + p := provisionerFromContext(ctx) + return p.NotifySuccess(ctx, csr, cert, transactionID) +} + +func (a *Authority) NotifyFailure(ctx context.Context, csr *x509.CertificateRequest, transactionID string, errorCode int, errorDescription string) error { + p := provisionerFromContext(ctx) + return p.NotifyFailure(ctx, csr, transactionID, errorCode, errorDescription) +} + +func (a *Authority) selectDecrypter(ctx context.Context) (cert *x509.Certificate, decrypter crypto.Decrypter, err error) { + p := provisionerFromContext(ctx) + cert, decrypter = p.GetDecrypter() + switch { + case cert != nil && decrypter != nil: + return + case cert == nil && decrypter != nil: + return nil, nil, fmt.Errorf("provisioner %q does not have a decrypter certificate available", p.GetName()) + case cert != nil && decrypter == nil: + return nil, nil, fmt.Errorf("provisioner %q does not have a decrypter available", p.GetName()) } - return p.ValidateChallenge(ctx, challenge, transactionID) + + cert, decrypter = a.decrypterCertificate, a.defaultDecrypter + switch { + case cert == nil && decrypter != nil: + return nil, nil, fmt.Errorf("provisioner %q does not have a default decrypter certificate available", p.GetName()) + case cert != nil && decrypter == nil: + return nil, nil, fmt.Errorf("provisioner %q does not have a default decrypter available", p.GetName()) + } + + return +} + +func (a *Authority) selectSigner(ctx context.Context) (cert *x509.Certificate, signer crypto.Signer, err error) { + p := provisionerFromContext(ctx) + cert, signer = p.GetSigner() + switch { + case cert != nil && signer != nil: + return + case cert == nil && signer != nil: + return nil, nil, fmt.Errorf("provisioner %q does not have a signer certificate available", p.GetName()) + case cert != nil && signer == nil: + return nil, nil, fmt.Errorf("provisioner %q does not have a signer available", p.GetName()) + } + + cert, signer = a.signerCertificate, a.defaultSigner + switch { + case cert == nil && signer != nil: + return nil, nil, fmt.Errorf("provisioner %q does not have a default signer certificate available", p.GetName()) + case cert != nil && signer == nil: + return nil, nil, fmt.Errorf("provisioner %q does not have a default signer available", p.GetName()) + } + + return } diff --git a/scep/authority_test.go b/scep/authority_test.go new file mode 100644 index 00000000..0aa81b49 --- /dev/null +++ b/scep/authority_test.go @@ -0,0 +1,73 @@ +package scep + +import ( + "crypto/x509" + "crypto/x509/pkix" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mozilla.org/pkcs7" + "go.step.sm/crypto/keyutil" + "go.step.sm/crypto/minica" + "go.step.sm/crypto/randutil" +) + +func generateContent(t *testing.T, size int) []byte { + t.Helper() + b, err := randutil.Bytes(size) + require.NoError(t, err) + return b +} + +func generateRecipients(t *testing.T) []*x509.Certificate { + ca, err := minica.New() + require.NoError(t, err) + s, err := keyutil.GenerateSigner("RSA", "", 2048) + require.NoError(t, err) + tmpl := &x509.Certificate{ + PublicKey: s.Public(), + Subject: pkix.Name{CommonName: "Test PKCS#7 Encryption"}, + } + cert, err := ca.Sign(tmpl) + require.NoError(t, err) + return []*x509.Certificate{cert} +} + +func TestAuthority_encrypt(t *testing.T) { + t.Parallel() + a := &Authority{} + recipients := generateRecipients(t) + type args struct { + content []byte + recipients []*x509.Certificate + algorithm int + } + tests := []struct { + name string + args args + wantErr bool + }{ + {"alg-0", args{generateContent(t, 32), recipients, pkcs7.EncryptionAlgorithmDESCBC}, false}, + {"alg-1", args{generateContent(t, 32), recipients, pkcs7.EncryptionAlgorithmAES128CBC}, false}, + {"alg-2", args{generateContent(t, 32), recipients, pkcs7.EncryptionAlgorithmAES256CBC}, false}, + {"alg-3", args{generateContent(t, 32), recipients, pkcs7.EncryptionAlgorithmAES128GCM}, false}, + {"alg-4", args{generateContent(t, 32), recipients, pkcs7.EncryptionAlgorithmAES256GCM}, false}, + {"alg-unknown", args{generateContent(t, 32), recipients, 42}, true}, + } + for _, tt := range tests { + tc := tt + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + got, err := a.encrypt(tc.args.content, tc.args.recipients, tc.args.algorithm) + if tc.wantErr { + assert.Error(t, err) + assert.Nil(t, got) + return + } + + assert.NoError(t, err) + assert.NotEmpty(t, got) + }) + } +} diff --git a/scep/common.go b/scep/common.go deleted file mode 100644 index 73b16ed4..00000000 --- a/scep/common.go +++ /dev/null @@ -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 -} diff --git a/scep/database.go b/scep/database.go deleted file mode 100644 index f73573fd..00000000 --- a/scep/database.go +++ /dev/null @@ -1,7 +0,0 @@ -package scep - -import "crypto/x509" - -type DB interface { - StoreCertificate(crt *x509.Certificate) error -} diff --git a/scep/options.go b/scep/options.go index 201f1beb..8bc30a61 100644 --- a/scep/options.go +++ b/scep/options.go @@ -4,65 +4,78 @@ import ( "crypto" "crypto/rsa" "crypto/x509" - - "github.com/pkg/errors" + "errors" ) type Options struct { - // CertificateChain is the issuer certificate, along with any other bundled certificates - // to be returned in the chain for consumers. Configured in the ca.json crt property. - CertificateChain []*x509.Certificate + // Roots contains the (federated) CA roots certificate(s) + Roots []*x509.Certificate `json:"-"` + // 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 crypto.Signer `json:"-"` // Decrypter decrypts encrypted SCEP messages. Configured in the ca.json key property. Decrypter crypto.Decrypter `json:"-"` + // DecrypterCert points to the certificate of the CA decrypter. + DecrypterCert *x509.Certificate `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. func (o *Options) Validate() error { - if o.CertificateChain == nil { - return errors.New("certificate chain not configured correctly") + switch { + 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 { - return errors.New("certificate chain should at least have one certificate") + // check if the signer (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. + 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 - // can be used with something different than RSA, but requires the encryption - // to be performed using the challenge password. 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 than RSA 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 - // 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") + // decrypter can be nil in case a signing only key is used; validation complete. + if o.Decrypter == nil { + return nil } + // 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 + // to try other algorithms in the future. decrypterPublicKey, ok := o.Decrypter.Public().(*rsa.PublicKey) 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. // In certnanny/sscep it's mentioned that the signing key can be different - // from the decrypting (and encrypting) key. Currently that's not supported. - if !decrypterPublicKey.Equal(intermediate.PublicKey) { + // from the decrypting (and encrypting) key. These options are only used and + // 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") } diff --git a/scep/provisioner.go b/scep/provisioner.go index 8120057e..3df4b367 100644 --- a/scep/provisioner.go +++ b/scep/provisioner.go @@ -2,20 +2,43 @@ package scep import ( "context" - "time" + "crypto" + "crypto/x509" "github.com/smallstep/certificates/authority/provisioner" ) -// Provisioner is an interface that implements a subset of the provisioner.Interface -- -// only those methods required by the SCEP api/authority. +// Provisioner is an interface that embeds the +// provisioner.Interface and adds some SCEP specific +// functions. type Provisioner interface { - AuthorizeSign(ctx context.Context, token string) ([]provisioner.SignOption, error) - GetName() string - DefaultTLSCertDuration() time.Duration + provisioner.Interface GetOptions() *provisioner.Options GetCapabilities() []string ShouldIncludeRootInChain() bool + ShouldIncludeIntermediateInChain() bool + GetDecrypter() (*x509.Certificate, crypto.Decrypter) + GetSigner() (*x509.Certificate, crypto.Signer) GetContentEncryptionAlgorithm() int - ValidateChallenge(ctx context.Context, challenge, transactionID string) error + ValidateChallenge(ctx context.Context, csr *x509.CertificateRequest, challenge, transactionID string) error + NotifySuccess(ctx context.Context, csr *x509.CertificateRequest, cert *x509.Certificate, transactionID string) error + NotifyFailure(ctx context.Context, csr *x509.CertificateRequest, transactionID string, errorCode int, errorDescription 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) } diff --git a/scep/service.go b/scep/service.go deleted file mode 100644 index 85f7c73f..00000000 --- a/scep/service.go +++ /dev/null @@ -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 -} diff --git a/webhook/types.go b/webhook/types.go index 9eda0578..2d7832b8 100644 --- a/webhook/types.go +++ b/webhook/types.go @@ -30,6 +30,7 @@ type X509Certificate struct { PublicKeyAlgorithm string `json:"publicKeyAlgorithm"` NotBefore time.Time `json:"notBefore"` NotAfter time.Time `json:"notAfter"` + Raw []byte `json:"raw"` } // SSHCertificateRequest is the certificate request sent to webhook servers for @@ -79,9 +80,11 @@ type RequestBody struct { X509Certificate *X509Certificate `json:"x509Certificate,omitempty"` SSHCertificateRequest *SSHCertificateRequest `json:"sshCertificateRequest,omitempty"` SSHCertificate *SSHCertificate `json:"sshCertificate,omitempty"` - // Only set for SCEP challenge validation requests - SCEPChallenge string `json:"scepChallenge,omitempty"` - SCEPTransactionID string `json:"scepTransactionID,omitempty"` + // Only set for SCEP webhook requests + SCEPChallenge string `json:"scepChallenge,omitempty"` + SCEPTransactionID string `json:"scepTransactionID,omitempty"` + SCEPErrorCode int `json:"scepErrorCode,omitempty"` + SCEPErrorDescription string `json:"scepErrorDescription,omitempty"` // Only set for X5C provisioners X5CCertificate *X5CCertificate `json:"x5cCertificate,omitempty"` // Set for X5C, AWS, GCP, and Azure provisioners