Integrate the SCEP webhook with the existing webhook logic

This commit is contained in:
Herman Slatman 2023-04-28 17:15:05 +02:00
parent 05f7ab979f
commit 27cdcaf5ee
No known key found for this signature in database
GPG key ID: F4D8A44EA0A75A4F
7 changed files with 50 additions and 203 deletions

6
go.mod
View file

@ -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

11
go.sum
View file

@ -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=

View file

@ -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 {

View file

@ -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
}
}

View file

@ -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
}

View file

@ -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

View file

@ -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"`
}