Integrate the SCEP webhook with the existing webhook logic
This commit is contained in:
parent
05f7ab979f
commit
27cdcaf5ee
7 changed files with 50 additions and 203 deletions
6
go.mod
6
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
|
||||
|
|
11
go.sum
11
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=
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"`
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue