From f2663dd9d9bd3bc13296d7fd3fc30f4d3a0ae9fb Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 11 Nov 2024 18:19:28 -0800 Subject: [PATCH] Add data support on SCEPCHALLENGE webhooks This commit adds support for using template data from SCEPCHALLENGE webhooks. --- authority/provisioner/scep.go | 27 ++++++++++++++++++--------- authority/provisioner/sign_options.go | 25 +++++++++++++++++++++++++ scep/api/api.go | 7 +++++-- scep/authority.go | 11 +++++++++-- scep/provisioner.go | 2 +- 5 files changed, 58 insertions(+), 14 deletions(-) diff --git a/authority/provisioner/scep.go b/authority/provisioner/scep.go index 7213285c..0bdaf3e9 100644 --- a/authority/provisioner/scep.go +++ b/authority/provisioner/scep.go @@ -15,6 +15,7 @@ import ( "go.step.sm/crypto/kms" kmsapi "go.step.sm/crypto/kms/apiv1" + "go.step.sm/crypto/x509util" "go.step.sm/linkedca" "github.com/smallstep/certificates/webhook" @@ -145,25 +146,33 @@ 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, csr *x509.CertificateRequest, provisionerName, challenge, transactionID string) error { +func (c *challengeValidationController) Validate(ctx context.Context, csr *x509.CertificateRequest, provisionerName, challenge, transactionID string) ([]SignCSROption, error) { + var opts []SignCSROption + 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) + return nil, fmt.Errorf("failed creating new webhook request: %w", err) } req.ProvisionerName = provisionerName 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) + return nil, fmt.Errorf("failed executing webhook request: %w", err) } if resp.Allow { - return nil // return early when response is positive + opts = append(opts, TemplateDataModifierFunc(func(data x509util.TemplateData) { + data.SetWebhook(wh.Name, resp.Data) + })) } } - return ErrSCEPChallengeInvalid + if len(opts) == 0 { + return nil, ErrSCEPChallengeInvalid + } + + return opts, nil } type notificationController struct { @@ -440,18 +449,18 @@ 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, csr *x509.CertificateRequest, challenge, transactionID string) error { +func (s *SCEP) ValidateChallenge(ctx context.Context, csr *x509.CertificateRequest, challenge, transactionID string) ([]SignCSROption, error) { if s.challengeValidationController == nil { - return fmt.Errorf("provisioner %q wasn't initialized", s.Name) + return nil, fmt.Errorf("provisioner %q wasn't initialized", s.Name) } switch s.selectValidationMethod() { case validationMethodWebhook: return s.challengeValidationController.Validate(ctx, csr, s.Name, challenge, transactionID) default: if subtle.ConstantTimeCompare([]byte(s.ChallengePassword), []byte(challenge)) == 0 { - return errors.New("invalid challenge password provided") + return nil, errors.New("invalid challenge password provided") } - return nil + return []SignCSROption{}, nil } } diff --git a/authority/provisioner/sign_options.go b/authority/provisioner/sign_options.go index 8367d8d0..09d786b1 100644 --- a/authority/provisioner/sign_options.go +++ b/authority/provisioner/sign_options.go @@ -545,3 +545,28 @@ func (s csrFingerprintValidator) Valid(cr *x509.CertificateRequest) error { } return nil } + +// SignCSROption is the interface used to collect extra option in the SignCSR +// method of the SCEP authority. +type SignCSROption interface{} + +// TemplateDataModifier in an interface that allows to modify template data. +type TemplateDataModifier interface { + Modify(data x509util.TemplateData) +} + +type templateDataModifier struct { + fn func(x509util.TemplateData) +} + +func (t *templateDataModifier) Modify(data x509util.TemplateData) { + t.fn(data) +} + +// TemplateDataModifierFunc returns a TemplateDataModifier with the given +// function. +func TemplateDataModifierFunc(fn func(data x509util.TemplateData)) TemplateDataModifier { + return &templateDataModifier{ + fn: fn, + } +} diff --git a/scep/api/api.go b/scep/api/api.go index 687099c9..a5e24055 100644 --- a/scep/api/api.go +++ b/scep/api/api.go @@ -384,14 +384,17 @@ func PKIOperation(ctx context.Context, req request) (Response, error) { // even if using the renewal flow as described in the README.md. MicroMDM SCEP client also only does PKCSreq by default, unless // 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. + var signCSROpts []provisioner.SignCSROption if msg.MessageType == smallscep.PKCSReq || msg.MessageType == smallscep.RenewalReq { - if err := auth.ValidateChallenge(ctx, csr, challengePassword, transactionID); err != nil { + challengeOptions, err := auth.ValidateChallenge(ctx, csr, challengePassword, transactionID) + if err != nil { if errors.Is(err, provisioner.ErrSCEPChallengeInvalid) { return createFailureResponse(ctx, csr, msg, smallscep.BadRequest, err.Error(), err) } scepErr := errors.New("failed validating challenge password") return createFailureResponse(ctx, csr, msg, smallscep.BadRequest, scepErr.Error(), scepErr) } + signCSROpts = append(signCSROpts, challengeOptions...) } // TODO: authorize renewal: we can authorize renewals with the challenge password (if reusable secrets are used). @@ -402,7 +405,7 @@ func PKIOperation(ctx context.Context, req request) (Response, error) { // Authentication by the (self-signed) certificate with an optional challenge is required; supporting renewals incl. verification // of the client cert is not. - certRep, err := auth.SignCSR(ctx, csr, msg) + certRep, err := auth.SignCSR(ctx, csr, msg, signCSROpts...) 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 diff --git a/scep/authority.go b/scep/authority.go index 00c58d8d..256c540d 100644 --- a/scep/authority.go +++ b/scep/authority.go @@ -241,7 +241,7 @@ func (a *Authority) DecryptPKIEnvelope(ctx context.Context, msg *PKIMessage) err // SignCSR creates an x509.Certificate based on a CSR template and Cert Authority credentials // 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, signCSROpts ...provisioner.SignCSROption) (*PKIMessage, error) { // TODO: intermediate storage of the request? In SCEP it's possible to request a csr/certificate // to be signed, which can be performed asynchronously / out-of-band. In that case a client can // poll for the status. It seems to be similar as what can happen in ACME, so might want to model @@ -284,6 +284,13 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m CommonName: csr.Subject.CommonName, }) + // Apply CSR options. Currently only one option is defined. + for _, o := range signCSROpts { + if m, ok := o.(provisioner.TemplateDataModifier); ok { + m.Modify(data) + } + } + // Get authorizations from the SCEP provisioner. ctx = provisioner.NewContextWithMethod(ctx, provisioner.SignMethod) signOps, err := p.AuthorizeSign(ctx, "") @@ -506,7 +513,7 @@ func (a *Authority) GetCACaps(ctx context.Context) []string { return caps } -func (a *Authority) ValidateChallenge(ctx context.Context, csr *x509.CertificateRequest, challenge, transactionID string) error { +func (a *Authority) ValidateChallenge(ctx context.Context, csr *x509.CertificateRequest, challenge, transactionID string) ([]provisioner.SignCSROption, error) { p := provisionerFromContext(ctx) return p.ValidateChallenge(ctx, csr, challenge, transactionID) } diff --git a/scep/provisioner.go b/scep/provisioner.go index 3df4b367..35821d8c 100644 --- a/scep/provisioner.go +++ b/scep/provisioner.go @@ -20,7 +20,7 @@ type Provisioner interface { GetDecrypter() (*x509.Certificate, crypto.Decrypter) GetSigner() (*x509.Certificate, crypto.Signer) GetContentEncryptionAlgorithm() int - ValidateChallenge(ctx context.Context, csr *x509.CertificateRequest, challenge, transactionID string) error + ValidateChallenge(ctx context.Context, csr *x509.CertificateRequest, challenge, transactionID string) ([]provisioner.SignCSROption, 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 }