diff --git a/go.mod b/go.mod index a469dcb6..17fcec58 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( go.mozilla.org/pkcs7 v0.0.0-20210826202110-33d05740a352 go.step.sm/cli-utils v0.7.6 go.step.sm/crypto v0.29.3 - go.step.sm/linkedca v0.19.0 + go.step.sm/linkedca v0.19.1-0.20230428150007-f95d2903af82 golang.org/x/crypto v0.8.0 golang.org/x/exp v0.0.0-20230310171629-522b1b587ee0 golang.org/x/net v0.9.0 @@ -106,8 +106,6 @@ require ( github.com/jackc/pgx/v4 v4.18.0 // indirect github.com/jmespath/go-jmespath v0.4.0 // indirect github.com/klauspost/compress v1.15.11 // indirect - github.com/kr/pretty v0.3.1 // indirect - github.com/kr/text v0.2.0 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/manifoldco/promptui v0.9.0 // indirect github.com/mattn/go-colorable v0.1.8 // indirect @@ -121,10 +119,8 @@ require ( github.com/peterbourgon/diskv/v3 v3.0.1 // indirect github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rogpeppe/go-internal v1.9.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/ryanuber/go-glob v1.0.0 // indirect - github.com/ryboe/q v1.0.19 // indirect github.com/schollz/jsonstore v1.1.0 // indirect github.com/shopspring/decimal v1.2.0 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect diff --git a/go.sum b/go.sum index f91d5a9c..1aa1170d 100644 --- a/go.sum +++ b/go.sum @@ -657,8 +657,6 @@ github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFB 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.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 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= @@ -796,7 +794,6 @@ github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0 github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= @@ -847,8 +844,6 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So github.com/rogpeppe/fastuuid v1.1.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= 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/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= github.com/rs/cors v1.8.0/go.mod h1:EBwu+T5AvHOcXwvZIkQFjUN6s8Czyqw12GL/Y0tUyRM= github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= @@ -864,8 +859,6 @@ github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= -github.com/ryboe/q v1.0.19 h1:1dO1anK4gorZRpXBD/edBZkMxIC1tFIwN03nfyOV13A= -github.com/ryboe/q v1.0.19/go.mod h1:IoEB3Q2/p6n1qbhIQVuNyakxtnV4rNJ/XJPK+jsEa0M= github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sassoftware/go-rpmutils v0.0.0-20190420191620-a8f1baeba37b/go.mod h1:am+Fp8Bt506lA3Rk3QCmSqmYmLMnPDhdDUcosQCAx+I= github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= @@ -1039,8 +1032,8 @@ go.step.sm/cli-utils v0.7.6 h1:YkpLVrepmy2c5+eaz/wduiGxlgrRx3YdAStE37if25g= go.step.sm/cli-utils v0.7.6/go.mod h1:j+FxFZ2gbWkAJl0eded/rksuxmNqWpmyxbkXcukGJaY= go.step.sm/crypto v0.29.3 h1:lFCsFQQGic1VZIa0B/87iMCDy67+LW8eEl119GTyeWI= go.step.sm/crypto v0.29.3/go.mod h1:0lYeIyQMJbFJ27L4BOGaq2gnuTgOShf+Ju/cTsMULq4= -go.step.sm/linkedca v0.19.0 h1:xuagkR35wrJI2gnu6FAM+q3VmjwsHScvGcJsfZ0GdsI= -go.step.sm/linkedca v0.19.0/go.mod h1:b7vWPrHfYLEOTSUZitFEcztVCpTc+ileIN85CwEAluM= +go.step.sm/linkedca v0.19.1-0.20230428150007-f95d2903af82 h1:oQtwNr4cxHxyrJaqYlI6DfhtVfkoVjsRZlUm0XYhec8= +go.step.sm/linkedca v0.19.1-0.20230428150007-f95d2903af82/go.mod h1:vPV2ad3LFQJmV7XWt87VlnJSs6UOqgsbVGVWe3veEmI= 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= diff --git a/scep/api/api.go b/scep/api/api.go index 66118388..9e659887 100644 --- a/scep/api/api.go +++ b/scep/api/api.go @@ -14,8 +14,8 @@ import ( "github.com/go-chi/chi" microscep "github.com/micromdm/scep/v2/scep" - "github.com/ryboe/q" "go.mozilla.org/pkcs7" + "go.step.sm/linkedca" "github.com/smallstep/certificates/api" "github.com/smallstep/certificates/api/log" @@ -313,12 +313,16 @@ func PKIOperation(ctx context.Context, req request) (Response, error) { return Response{}, err } - _ = prov - q.Q(prov) - - // TODO(hs): set the checking method based on what's configured in provisioner. Or try something dynamic. const checkMethodWebhook string = "webhook" - checkMethod := checkMethodWebhook + checkMethod := "" + for _, wh := range prov.GetOptions().GetWebhooks() { + // if there's at least one webhook for validating SCEP challenges, the + // webhook will be used to perform challenge validation. + if wh.Kind == linkedca.Webhook_SCEPCHALLENGE.String() { + checkMethod = checkMethodWebhook + break + } + } // 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, @@ -332,28 +336,13 @@ func PKIOperation(ctx context.Context, req request) (Response, error) { // failure too. switch checkMethod { case checkMethodWebhook: - // TODO(hs): implement webhook call: needs endpoint, auth, request body - // TODO(hs): integrate this with the existing webhook implementation by extending it - fmt.Println("test") - q.Q("HERE") - q.Q(msg.CSRReqMessage) - opts := []webhook.ControllerOption{ - webhook.WithURL("http://127.0.0.1:8081/scepvalidate"), - webhook.WithBearerToken("fake-token"), - } - c, err := webhook.New(opts...) + c, err := webhook.New(prov.GetOptions().GetWebhooks()) if err != nil { return createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("failed creating SCEP validation webhook controller")) } - q.Q(c) - ok, err := c.Validate(msg.CSRReqMessage.ChallengePassword) - if err != nil { - q.Q(err) + if err := c.Validate(msg.CSRReqMessage.ChallengePassword); err != nil { return createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("failed validating challenge password")) } - if !ok { - return createFailureResponse(ctx, csr, msg, microscep.BadRequest, errors.New("wrong challenge password provided")) - } default: challengeMatches, err := auth.MatchChallengePassword(ctx, msg.CSRReqMessage.ChallengePassword) if err != nil { diff --git a/scep/api/webhook/options.go b/scep/api/webhook/options.go deleted file mode 100644 index ce809cb4..00000000 --- a/scep/api/webhook/options.go +++ /dev/null @@ -1,24 +0,0 @@ -package webhook - -type ControllerOption func(*Controller) error - -func WithURL(url string) ControllerOption { - return func(c *Controller) error { - c.webhook.URL = url - return nil - } -} - -func WithBearerToken(token string) ControllerOption { - return func(c *Controller) error { - c.webhook.BearerToken = token - return nil - } -} - -func WithDisableTLSClientAuth(enabled bool) ControllerOption { - return func(c *Controller) error { - c.webhook.DisableTLSClientAuth = enabled - return nil - } -} diff --git a/scep/api/webhook/webhook.go b/scep/api/webhook/webhook.go index d3474c14..63fdd533 100644 --- a/scep/api/webhook/webhook.go +++ b/scep/api/webhook/webhook.go @@ -1,161 +1,51 @@ package webhook import ( - "bytes" - "context" - "crypto/hmac" - "crypto/sha256" - "encoding/base64" - "encoding/hex" - "encoding/json" - "errors" - "fmt" - "log" "net/http" - "time" - "github.com/ryboe/q" + "go.step.sm/linkedca" + + "github.com/smallstep/certificates/authority/provisioner" + "github.com/smallstep/certificates/webhook" ) type Controller struct { - client *http.Client - webhook *Webhook + client *http.Client + webhooks []*provisioner.Webhook } -func New(options ...ControllerOption) (*Controller, error) { - c := &Controller{ - client: http.DefaultClient, - webhook: &Webhook{}, +func New(webhooks []*provisioner.Webhook) (*Controller, error) { + return &Controller{ + client: http.DefaultClient, + webhooks: webhooks, + }, nil +} + +func (c *Controller) Validate(challenge string) error { + if c == nil { + return nil } - for _, apply := range options { - if err := apply(c); err != nil { - return nil, err + for _, wh := range c.webhooks { + if wh.Kind != linkedca.Webhook_SCEPCHALLENGE.String() { + continue } - } - return c, nil -} - -func (c *Controller) Validate(challenge string) (bool, error) { - req := &Request{ - Challenge: challenge, - } - client := c.client - if client == nil { - client = http.DefaultClient - } - resp, err := c.webhook.Do(client, req) - if err != nil { - q.Q(err) - return false, fmt.Errorf("failed performing webhook request: %w", err) - } - - if resp == nil { - return false, nil - } - - return true, nil -} - -type Webhook struct { - URL string - DisableTLSClientAuth bool - Secret string - BearerToken string - BasicAuth struct { - Username string - Password string - } -} - -func (w *Webhook) Do(client *http.Client, req *Request) (*Response, error) { - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) - defer cancel() - - reqBytes, err := json.Marshal(req) - if err != nil { - return nil, err - } - - retries := 1 -retry: - - r, err := http.NewRequestWithContext(ctx, "POST", w.URL, bytes.NewReader(reqBytes)) - if err != nil { - return nil, err - } - - if w.Secret != "" { - secret, err := base64.StdEncoding.DecodeString(w.Secret) + if !c.isCertTypeOK(wh) { + continue + } + req := &webhook.RequestBody{ + SCEPChallenge: challenge, + } + resp, err := wh.Do(c.client, req, nil) // TODO(hs): support templated URL? if err != nil { - return nil, err + return err } - sig := hmac.New(sha256.New, secret).Sum(reqBytes) - r.Header.Set("X-Smallstep-Signature", hex.EncodeToString(sig)) - //req.Header.Set("X-Smallstep-Webhook-ID", w.ID) - } - - if w.BearerToken != "" { - r.Header.Set("Authorization", fmt.Sprintf("Bearer %s", w.BearerToken)) - } else if w.BasicAuth.Username != "" || w.BasicAuth.Password != "" { - r.SetBasicAuth(w.BasicAuth.Username, w.BasicAuth.Password) - } - if w.DisableTLSClientAuth { - transport, ok := client.Transport.(*http.Transport) - if !ok { - return nil, errors.New("client transport is not a *http.Transport") - } - transport = transport.Clone() - tlsConfig := transport.TLSClientConfig.Clone() - tlsConfig.GetClientCertificate = nil - tlsConfig.Certificates = nil - transport.TLSClientConfig = tlsConfig - client = &http.Client{ - Transport: transport, + if !resp.Allow { + return provisioner.ErrWebhookDenied } } - - resp, err := client.Do(r) - if err != nil { - if errors.Is(err, context.DeadlineExceeded) { - return nil, err - } else if retries > 0 { - retries-- - time.Sleep(time.Second) - goto retry - } - return nil, err - } - defer func() { - if err := resp.Body.Close(); err != nil { - // TODO: return this error instead of (just) logging? - log.Printf("failed to close body of response from %s", w.URL) - } - }() - - if resp.StatusCode >= 500 && retries > 0 { - retries-- - time.Sleep(time.Second) - goto retry - } - if resp.StatusCode >= 400 { - return nil, fmt.Errorf("webhook server responded with %d", resp.StatusCode) - } - - respBody := &Response{} - // TODO: decide on the JSON structure for the response (if any); HTTP status code may be enough. - // if err := json.NewDecoder(resp.Body).Decode(respBody); err != nil { - // return nil, err - // } - - return respBody, nil + return nil } -type Request struct { - Challenge string `json:"challenge"` -} - -type Response struct { - // TODO: define expected response format? Or do we consider 200 OK enough? - Allow bool `json:"allow"` +func (c *Controller) isCertTypeOK(wh *provisioner.Webhook) bool { + return linkedca.Webhook_X509.String() == wh.CertType } diff --git a/scep/authority.go b/scep/authority.go index 9bfa20b8..c1304bb7 100644 --- a/scep/authority.go +++ b/scep/authority.go @@ -284,6 +284,7 @@ func (a *Authority) SignCSR(ctx context.Context, csr *x509.CertificateRequest, m // Unlike most of the provisioners, scep's AuthorizeSign method doesn't // define the templates, and the template data used in WebHooks is not // available. + // TODO(hs): pass in challenge password to this webhook controller too? for _, signOp := range signOps { if wc, ok := signOp.(*provisioner.WebhookController); ok { wc.TemplateData = data diff --git a/webhook/types.go b/webhook/types.go index 19624f5c..a1e10efe 100644 --- a/webhook/types.go +++ b/webhook/types.go @@ -68,4 +68,6 @@ type RequestBody struct { X509Certificate *X509Certificate `json:"x509Certificate,omitempty"` SSHCertificateRequest *SSHCertificateRequest `json:"sshCertificateRequest,omitempty"` SSHCertificate *SSHCertificate `json:"sshCertificate,omitempty"` + // Only set for SCEP requests + SCEPChallenge string `json:"scepChallenge,omitempty"` }