diff --git a/authority/admin/api/webhook.go b/authority/admin/api/webhook.go index f73f6806..3939d55e 100644 --- a/authority/admin/api/webhook.go +++ b/authority/admin/api/webhook.go @@ -57,9 +57,9 @@ func validateWebhook(webhook *linkedca.Webhook) error { // kind switch webhook.Kind { - case linkedca.Webhook_ENRICHING, linkedca.Webhook_AUTHORIZING: + case linkedca.Webhook_ENRICHING, linkedca.Webhook_AUTHORIZING, linkedca.Webhook_SCEPCHALLENGE: default: - return admin.NewError(admin.ErrorBadRequestType, "webhook kind is invalid") + return admin.NewError(admin.ErrorBadRequestType, "webhook kind %q is invalid", webhook.Kind) } return nil diff --git a/authority/admin/api/webhook_test.go b/authority/admin/api/webhook_test.go index baac2c11..0fb199f0 100644 --- a/authority/admin/api/webhook_test.go +++ b/authority/admin/api/webhook_test.go @@ -180,6 +180,26 @@ func TestWebhookAdminResponder_CreateProvisionerWebhook(t *testing.T) { statusCode: 400, } }, + "fail/unsupported-webhook-kind": func(t *testing.T) test { + prov := &linkedca.Provisioner{ + Name: "provName", + } + ctx := linkedca.NewContextWithProvisioner(context.Background(), prov) + adminErr := admin.NewError(admin.ErrorBadRequestType, `(line 5:13): invalid value for enum type: "UNSUPPORTED"`) + adminErr.Message = `(line 5:13): invalid value for enum type: "UNSUPPORTED"` + body := []byte(` + { + "name": "metadata", + "url": "https://example.com", + "kind": "UNSUPPORTED", + }`) + return test{ + ctx: ctx, + body: body, + err: adminErr, + statusCode: 400, + } + }, "fail/auth.UpdateProvisioner-error": func(t *testing.T) test { adm := &linkedca.Admin{ Subject: "step", diff --git a/authority/authority.go b/authority/authority.go index 7904a7ea..ae85c018 100644 --- a/authority/authority.go +++ b/authority/authority.go @@ -545,50 +545,6 @@ func (a *Authority) init() error { tmplVars.SSH.UserFederatedKeys = append(tmplVars.SSH.UserFederatedKeys, a.sshCAUserFederatedCerts...) } - // 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") - } - } - - // 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 - - // 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, - }) - 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 - } - } - - a.scepService, err = scep.NewService(ctx, options) - if err != nil { - return err - } - - // TODO: mimick the x509CAService GetCertificateAuthority here too? - } - if a.config.AuthorityConfig.EnableAdmin { // Initialize step-ca Admin Database if it's not already initialized using // WithAdminDB. @@ -684,6 +640,50 @@ 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") + } + } + + // 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 + + // 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, + }) + 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 + } + } + + a.scepService, err = scep.NewService(ctx, options) + if err != nil { + return err + } + + // TODO: mimick the x509CAService GetCertificateAuthority here too? + } + // Load X509 constraints engine. // // This is currently only available in CA mode. diff --git a/authority/provisioner/scep.go b/authority/provisioner/scep.go index 0f27b206..c20f9bf1 100644 --- a/authority/provisioner/scep.go +++ b/authority/provisioner/scep.go @@ -2,10 +2,16 @@ package provisioner import ( "context" + "crypto/subtle" + "fmt" + "net/http" "time" "github.com/pkg/errors" + "go.step.sm/linkedca" + + "github.com/smallstep/certificates/webhook" ) // SCEP is the SCEP provisioner type, an entity that can authorize the @@ -35,6 +41,7 @@ type SCEP struct { ctl *Controller secretChallengePassword string encryptionAlgorithm int + challengeValidationController *challengeValidationController } // GetID returns the provisioner unique identifier. @@ -82,6 +89,67 @@ func (s *SCEP) DefaultTLSCertDuration() time.Duration { return s.ctl.Claimer.DefaultTLSCertDuration() } +type challengeValidationController struct { + client *http.Client + webhooks []*Webhook +} + +// newChallengeValidationController creates a new challengeValidationController +// that performs challenge validation through webhooks. +func newChallengeValidationController(client *http.Client, webhooks []*Webhook) *challengeValidationController { + scepHooks := []*Webhook{} + for _, wh := range webhooks { + if wh.Kind != linkedca.Webhook_SCEPCHALLENGE.String() { + continue + } + if !isCertTypeOK(wh) { + continue + } + scepHooks = append(scepHooks, wh) + } + return &challengeValidationController{ + client: client, + webhooks: scepHooks, + } +} + +var ( + ErrSCEPChallengeInvalid = errors.New("webhook server did not allow request") +) + +// Validate executes zero or more configured webhooks to +// validate the SCEP challenge. If at least one of them indicates +// the challenge value is accepted, validation succeeds. In +// 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 { + for _, wh := range c.webhooks { + req := &webhook.RequestBody{ + SCEPChallenge: challenge, + 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) + } + if resp.Allow { + return nil // return early when response is positive + } + } + + return ErrSCEPChallengeInvalid +} + +// isCertTypeOK returns whether or not the webhook can be used +// with the SCEP challenge validation webhook controller. +func isCertTypeOK(wh *Webhook) bool { + if wh.CertType == linkedca.Webhook_ALL.String() || wh.CertType == "" { + return true + } + return linkedca.Webhook_X509.String() == wh.CertType +} + // Init initializes and validates the fields of a SCEP type. func (s *SCEP) Init(config Config) (err error) { switch { @@ -109,6 +177,11 @@ func (s *SCEP) Init(config Config) (err error) { return errors.New("only encryption algorithm identifiers from 0 to 4 are valid") } + s.challengeValidationController = newChallengeValidationController( + config.WebhookClient, + s.GetOptions().GetWebhooks(), + ) + // TODO: add other, SCEP specific, options? s.ctl, err = NewController(s, s.Claims, config, s.Options) @@ -156,3 +229,43 @@ func (s *SCEP) ShouldIncludeRootInChain() bool { func (s *SCEP) GetContentEncryptionAlgorithm() int { return s.encryptionAlgorithm } + +// 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 { + 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) + default: + if subtle.ConstantTimeCompare([]byte(s.secretChallengePassword), []byte(challenge)) == 0 { + return errors.New("invalid challenge password provided") + } + return nil + } +} + +type validationMethod string + +const ( + validationMethodNone validationMethod = "none" + validationMethodStatic validationMethod = "static" + validationMethodWebhook validationMethod = "webhook" +) + +// selectValidationMethod returns the method to validate SCEP +// challenges. If a webhook is configured with kind `SCEPCHALLENGE`, +// the webhook method will be used. If a challenge password is set, +// the static method is used. It will default to the `none` method. +func (s *SCEP) selectValidationMethod() validationMethod { + if len(s.challengeValidationController.webhooks) > 0 { + return validationMethodWebhook + } + if s.secretChallengePassword != "" { + return validationMethodStatic + } + return validationMethodNone +} diff --git a/authority/provisioner/scep_test.go b/authority/provisioner/scep_test.go new file mode 100644 index 00000000..acf047fb --- /dev/null +++ b/authority/provisioner/scep_test.go @@ -0,0 +1,342 @@ +package provisioner + +import ( + "context" + "encoding/json" + "errors" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "go.step.sm/linkedca" +) + +func Test_challengeValidationController_Validate(t *testing.T) { + type request struct { + Challenge string `json:"scepChallenge"` + TransactionID string `json:"scepTransactionID"` + } + type response struct { + Allow bool `json:"allow"` + } + nokServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + req := &request{} + err := json.NewDecoder(r.Body).Decode(req) + require.NoError(t, err) + assert.Equal(t, "not-allowed", req.Challenge) + assert.Equal(t, "transaction-1", req.TransactionID) + b, err := json.Marshal(response{Allow: false}) + require.NoError(t, err) + w.WriteHeader(200) + w.Write(b) + })) + okServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + req := &request{} + err := json.NewDecoder(r.Body).Decode(req) + require.NoError(t, err) + assert.Equal(t, "challenge", req.Challenge) + assert.Equal(t, "transaction-1", req.TransactionID) + b, err := json.Marshal(response{Allow: true}) + require.NoError(t, err) + w.WriteHeader(200) + w.Write(b) + })) + type fields struct { + client *http.Client + webhooks []*Webhook + } + type args struct { + challenge string + transactionID string + } + tests := []struct { + name string + fields fields + args args + server *httptest.Server + expErr error + }{ + { + name: "fail/no-webhook", + fields: fields{http.DefaultClient, nil}, + args: args{"no-webhook", "transaction-1"}, + expErr: errors.New("webhook server did not allow request"), + }, + { + name: "fail/wrong-cert-type", + fields: fields{http.DefaultClient, []*Webhook{ + { + Kind: linkedca.Webhook_SCEPCHALLENGE.String(), + CertType: linkedca.Webhook_SSH.String(), + }, + }}, + args: args{"wrong-cert-type", "transaction-1"}, + expErr: errors.New("webhook server did not allow request"), + }, + { + name: "fail/wrong-secret-value", + fields: fields{http.DefaultClient, []*Webhook{ + { + ID: "webhook-id-1", + Name: "webhook-name-1", + Secret: "{{}}", + Kind: linkedca.Webhook_SCEPCHALLENGE.String(), + CertType: linkedca.Webhook_X509.String(), + URL: okServer.URL, + }, + }}, + args: args{ + challenge: "wrong-secret-value", + transactionID: "transaction-1", + }, + expErr: errors.New("failed executing webhook request: illegal base64 data at input byte 0"), + }, + { + name: "fail/not-allowed", + fields: fields{http.DefaultClient, []*Webhook{ + { + ID: "webhook-id-1", + Name: "webhook-name-1", + Secret: "MTIzNAo=", + Kind: linkedca.Webhook_SCEPCHALLENGE.String(), + CertType: linkedca.Webhook_X509.String(), + URL: nokServer.URL, + }, + }}, + args: args{ + challenge: "not-allowed", + transactionID: "transaction-1", + }, + server: nokServer, + expErr: errors.New("webhook server did not allow request"), + }, + { + name: "ok", + fields: fields{http.DefaultClient, []*Webhook{ + { + ID: "webhook-id-1", + Name: "webhook-name-1", + Secret: "MTIzNAo=", + Kind: linkedca.Webhook_SCEPCHALLENGE.String(), + CertType: linkedca.Webhook_X509.String(), + URL: okServer.URL, + }, + }}, + args: args{ + challenge: "challenge", + transactionID: "transaction-1", + }, + server: okServer, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c := newChallengeValidationController(tt.fields.client, tt.fields.webhooks) + + if tt.server != nil { + defer tt.server.Close() + } + + ctx := context.Background() + err := c.Validate(ctx, tt.args.challenge, tt.args.transactionID) + + if tt.expErr != nil { + assert.EqualError(t, err, tt.expErr.Error()) + return + } + + assert.NoError(t, err) + }) + } +} + +func TestController_isCertTypeOK(t *testing.T) { + assert.True(t, isCertTypeOK(&Webhook{CertType: linkedca.Webhook_X509.String()})) + assert.True(t, isCertTypeOK(&Webhook{CertType: linkedca.Webhook_ALL.String()})) + assert.True(t, isCertTypeOK(&Webhook{CertType: ""})) + assert.False(t, isCertTypeOK(&Webhook{CertType: linkedca.Webhook_SSH.String()})) +} + +func Test_selectValidationMethod(t *testing.T) { + tests := []struct { + name string + p *SCEP + want validationMethod + }{ + {"webhooks", &SCEP{ + Name: "SCEP", + Type: "SCEP", + Options: &Options{ + Webhooks: []*Webhook{ + { + Kind: linkedca.Webhook_SCEPCHALLENGE.String(), + }, + }, + }, + }, "webhook"}, + {"challenge", &SCEP{ + Name: "SCEP", + Type: "SCEP", + ChallengePassword: "pass", + }, "static"}, + {"challenge-with-different-webhook", &SCEP{ + Name: "SCEP", + Type: "SCEP", + Options: &Options{ + Webhooks: []*Webhook{ + { + Kind: linkedca.Webhook_AUTHORIZING.String(), + }, + }, + }, + ChallengePassword: "pass", + }, "static"}, + {"none", &SCEP{ + Name: "SCEP", + Type: "SCEP", + }, "none"}, + {"none-with-different-webhook", &SCEP{ + Name: "SCEP", + Type: "SCEP", + Options: &Options{ + Webhooks: []*Webhook{ + { + Kind: linkedca.Webhook_AUTHORIZING.String(), + }, + }, + }, + }, "none"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := tt.p.Init(Config{Claims: globalProvisionerClaims}) + require.NoError(t, err) + got := tt.p.selectValidationMethod() + assert.Equal(t, tt.want, got) + }) + } +} + +func TestSCEP_ValidateChallenge(t *testing.T) { + type request struct { + Challenge string `json:"scepChallenge"` + TransactionID string `json:"scepTransactionID"` + } + type response struct { + Allow bool `json:"allow"` + } + okServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + req := &request{} + err := json.NewDecoder(r.Body).Decode(req) + require.NoError(t, err) + assert.Equal(t, "webhook-challenge", req.Challenge) + assert.Equal(t, "webhook-transaction-1", req.TransactionID) + b, err := json.Marshal(response{Allow: true}) + require.NoError(t, err) + w.WriteHeader(200) + w.Write(b) + })) + type args struct { + challenge string + transactionID string + } + tests := []struct { + name string + p *SCEP + server *httptest.Server + args args + expErr error + }{ + {"ok/webhooks", &SCEP{ + Name: "SCEP", + Type: "SCEP", + Options: &Options{ + Webhooks: []*Webhook{ + { + ID: "webhook-id-1", + Name: "webhook-name-1", + Secret: "MTIzNAo=", + Kind: linkedca.Webhook_SCEPCHALLENGE.String(), + CertType: linkedca.Webhook_X509.String(), + URL: okServer.URL, + }, + }, + }, + }, okServer, args{"webhook-challenge", "webhook-transaction-1"}, + nil, + }, + {"fail/webhooks-secret-configuration", &SCEP{ + Name: "SCEP", + Type: "SCEP", + Options: &Options{ + Webhooks: []*Webhook{ + { + ID: "webhook-id-1", + Name: "webhook-name-1", + Secret: "{{}}", + Kind: linkedca.Webhook_SCEPCHALLENGE.String(), + CertType: linkedca.Webhook_X509.String(), + URL: okServer.URL, + }, + }, + }, + }, nil, args{"webhook-challenge", "webhook-transaction-1"}, + errors.New("failed executing webhook request: illegal base64 data at input byte 0"), + }, + {"ok/static-challenge", &SCEP{ + Name: "SCEP", + Type: "SCEP", + Options: &Options{}, + ChallengePassword: "secret-static-challenge", + }, nil, args{"secret-static-challenge", "static-transaction-1"}, + nil, + }, + {"fail/wrong-static-challenge", &SCEP{ + Name: "SCEP", + Type: "SCEP", + Options: &Options{}, + ChallengePassword: "secret-static-challenge", + }, nil, args{"the-wrong-challenge-secret", "static-transaction-1"}, + errors.New("invalid challenge password provided"), + }, + {"ok/no-challenge", &SCEP{ + Name: "SCEP", + Type: "SCEP", + Options: &Options{}, + ChallengePassword: "", + }, nil, args{"", "static-transaction-1"}, + nil, + }, + {"fail/no-challenge-but-provided", &SCEP{ + Name: "SCEP", + Type: "SCEP", + Options: &Options{}, + ChallengePassword: "", + }, nil, args{"a-challenge-value", "static-transaction-1"}, + errors.New("invalid challenge password provided"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + if tt.server != nil { + defer tt.server.Close() + } + + err := tt.p.Init(Config{Claims: globalProvisionerClaims, WebhookClient: http.DefaultClient}) + require.NoError(t, err) + ctx := context.Background() + + err = tt.p.ValidateChallenge(ctx, tt.args.challenge, tt.args.transactionID) + if tt.expErr != nil { + assert.EqualError(t, err, tt.expErr.Error()) + return + } + + assert.NoError(t, err) + }) + } +} diff --git a/authority/provisioner/webhook.go b/authority/provisioner/webhook.go index ea02da35..cb15547d 100644 --- a/authority/provisioner/webhook.go +++ b/authority/provisioner/webhook.go @@ -107,6 +107,13 @@ type Webhook struct { } func (w *Webhook) Do(client *http.Client, reqBody *webhook.RequestBody, data any) (*webhook.ResponseBody, error) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + + return w.DoWithContext(ctx, client, reqBody, data) +} + +func (w *Webhook) DoWithContext(ctx context.Context, client *http.Client, reqBody *webhook.RequestBody, data any) (*webhook.ResponseBody, error) { tmpl, err := template.New("url").Funcs(templates.StepFuncMap()).Parse(w.URL) if err != nil { return nil, err @@ -129,8 +136,6 @@ func (w *Webhook) Do(client *http.Client, reqBody *webhook.RequestBody, data any reqBody.Token = tmpl[sshutil.TokenKey] } */ - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() reqBody.Timestamp = time.Now() diff --git a/scep/api/api.go b/scep/api/api.go index 346b9c75..98da818b 100644 --- a/scep/api/api.go +++ b/scep/api/api.go @@ -305,6 +305,8 @@ func PKIOperation(ctx context.Context, req request) (Response, error) { // NOTE: at this point we have sufficient information for returning nicely signed CertReps csr := msg.CSRReqMessage.CSR + transactionID := string(msg.TransactionID) + challengePassword := msg.CSRReqMessage.ChallengePassword // NOTE: we're blocking the RenewalReq if the challenge does not match, because otherwise we don't have any authentication. // The macOS SCEP client performs renewals using PKCSreq. The CertNanny SCEP client will use PKCSreq with challenge too, it seems, @@ -312,13 +314,11 @@ 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 { - challengeMatches, err := auth.MatchChallengePassword(ctx, msg.CSRReqMessage.ChallengePassword) - if err != nil { - return createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("error when checking password")) - } - if !challengeMatches { - // TODO: can this be returned safely to the client? In the end, if the password was correct, that gains a bit of info too. - return createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("wrong password provided")) + if err := auth.ValidateChallenge(ctx, challengePassword, transactionID); err != nil { + if errors.Is(err, provisioner.ErrSCEPChallengeInvalid) { + return createFailureResponse(ctx, csr, msg, microscep.BadRequest, err) + } + return createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("failed validating challenge password")) } } diff --git a/scep/authority.go b/scep/authority.go index 585b937e..8ba9c9c9 100644 --- a/scep/authority.go +++ b/scep/authority.go @@ -2,7 +2,6 @@ package scep import ( "context" - "crypto/subtle" "crypto/x509" "errors" "fmt" @@ -456,24 +455,6 @@ func (a *Authority) CreateFailureResponse(ctx context.Context, csr *x509.Certifi return crepMsg, nil } -// MatchChallengePassword verifies a SCEP challenge password -func (a *Authority) MatchChallengePassword(ctx context.Context, password string) (bool, error) { - p, err := provisionerFromContext(ctx) - if err != nil { - return false, err - } - - if subtle.ConstantTimeCompare([]byte(p.GetChallengePassword()), []byte(password)) == 1 { - return true, nil - } - - // TODO: support dynamic challenges, i.e. a list of challenges instead of one? - // That's probably a bit harder to configure, though; likely requires some data store - // that can be interacted with more easily, via some internal API, for example. - - return false, nil -} - // GetCACaps returns the CA capabilities func (a *Authority) GetCACaps(ctx context.Context) []string { p, err := provisionerFromContext(ctx) @@ -494,3 +475,11 @@ 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 + } + return p.ValidateChallenge(ctx, challenge, transactionID) +} diff --git a/scep/provisioner.go b/scep/provisioner.go index 679c6353..8120057e 100644 --- a/scep/provisioner.go +++ b/scep/provisioner.go @@ -14,8 +14,8 @@ type Provisioner interface { GetName() string DefaultTLSCertDuration() time.Duration GetOptions() *provisioner.Options - GetChallengePassword() string GetCapabilities() []string ShouldIncludeRootInChain() bool GetContentEncryptionAlgorithm() int + ValidateChallenge(ctx context.Context, challenge, transactionID string) error } diff --git a/webhook/types.go b/webhook/types.go index 19624f5c..9605742a 100644 --- a/webhook/types.go +++ b/webhook/types.go @@ -68,4 +68,7 @@ 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"` }