Merge pull request #1544 from smallstep/herman/scep-notifying-webhook

Add SCEP issuance notification webhook
pull/1414/head
Herman Slatman 8 months ago committed by GitHub
commit 0d66158bf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -56,9 +56,7 @@ func validateWebhook(webhook *linkedca.Webhook) error {
}
// kind
switch webhook.Kind {
case linkedca.Webhook_ENRICHING, linkedca.Webhook_AUTHORIZING, linkedca.Webhook_SCEPCHALLENGE:
default:
if _, ok := linkedca.Webhook_Kind_name[int32(webhook.Kind)]; !ok || webhook.Kind == linkedca.Webhook_NO_KIND {
return admin.NewError(admin.ErrorBadRequestType, "webhook kind %q is invalid", webhook.Kind)
}

@ -58,6 +58,7 @@ type SCEP struct {
ctl *Controller
encryptionAlgorithm int
challengeValidationController *challengeValidationController
notificationController *notificationController
keyManager kmsapi.KeyManager
decrypter crypto.Decrypter
decrypterCertificate *x509.Certificate
@ -135,7 +136,8 @@ func newChallengeValidationController(client *http.Client, webhooks []*Webhook)
}
var (
ErrSCEPChallengeInvalid = errors.New("webhook server did not allow request")
ErrSCEPChallengeInvalid = errors.New("webhook server did not allow request")
ErrSCEPNotificationFailed = errors.New("scep notification failed")
)
// Validate executes zero or more configured webhooks to
@ -164,6 +166,63 @@ func (c *challengeValidationController) Validate(ctx context.Context, csr *x509.
return ErrSCEPChallengeInvalid
}
type notificationController struct {
client *http.Client
webhooks []*Webhook
}
// newNotificationController creates a new notificationController
// that performs SCEP notifications through webhooks.
func newNotificationController(client *http.Client, webhooks []*Webhook) *notificationController {
scepHooks := []*Webhook{}
for _, wh := range webhooks {
if wh.Kind != linkedca.Webhook_NOTIFYING.String() {
continue
}
if !isCertTypeOK(wh) {
continue
}
scepHooks = append(scepHooks, wh)
}
return &notificationController{
client: client,
webhooks: scepHooks,
}
}
func (c *notificationController) Success(ctx context.Context, csr *x509.CertificateRequest, cert *x509.Certificate, transactionID string) error {
for _, wh := range c.webhooks {
req, err := webhook.NewRequestBody(webhook.WithX509CertificateRequest(csr), webhook.WithX509Certificate(nil, cert)) // TODO(hs): pass in the x509util.Certifiate too?
if err != nil {
return fmt.Errorf("failed creating new webhook request: %w", err)
}
req.X509Certificate.Raw = cert.Raw // adding the full certificate DER bytes
req.SCEPTransactionID = transactionID
if _, err = wh.DoWithContext(ctx, c.client, req, nil); err != nil {
return fmt.Errorf("failed executing webhook request: %w: %w", ErrSCEPNotificationFailed, err)
}
}
return nil
}
func (c *notificationController) Failure(ctx context.Context, csr *x509.CertificateRequest, transactionID string, errorCode int, errorDescription string) error {
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)
}
req.SCEPTransactionID = transactionID
req.SCEPErrorCode = errorCode
req.SCEPErrorDescription = errorDescription
if _, err = wh.DoWithContext(ctx, c.client, req, nil); err != nil {
return fmt.Errorf("failed executing webhook request: %w: %w", ErrSCEPNotificationFailed, err)
}
}
return nil
}
// isCertTypeOK returns whether or not the webhook can be used
// with the SCEP challenge validation webhook controller.
func isCertTypeOK(wh *Webhook) bool {
@ -202,6 +261,12 @@ func (s *SCEP) Init(config Config) (err error) {
s.GetOptions().GetWebhooks(),
)
// Prepare the SCEP notification controller
s.notificationController = newNotificationController(
config.WebhookClient,
s.GetOptions().GetWebhooks(),
)
// parse the decrypter key PEM contents if available
if decryptionKeyPEM := s.DecrypterKeyPEM; len(decryptionKeyPEM) > 0 {
// try reading the PEM for validation
@ -383,6 +448,20 @@ func (s *SCEP) ValidateChallenge(ctx context.Context, csr *x509.CertificateReque
}
}
func (s *SCEP) NotifySuccess(ctx context.Context, csr *x509.CertificateRequest, cert *x509.Certificate, transactionID string) error {
if s.notificationController == nil {
return fmt.Errorf("provisioner %q wasn't initialized", s.Name)
}
return s.notificationController.Success(ctx, csr, cert, transactionID)
}
func (s *SCEP) NotifyFailure(ctx context.Context, csr *x509.CertificateRequest, transactionID string, errorCode int, errorDescription string) error {
if s.notificationController == nil {
return fmt.Errorf("provisioner %q wasn't initialized", s.Name)
}
return s.notificationController.Failure(ctx, csr, transactionID, errorCode, errorDescription)
}
type validationMethod string
const (

@ -333,9 +333,18 @@ func PKIOperation(ctx context.Context, req request) (Response, error) {
certRep, err := auth.SignCSR(ctx, csr, msg)
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
_ = notifyErr
}
return createFailureResponse(ctx, csr, msg, microscep.BadRequest, fmt.Errorf("error when signing new certificate: %w", err))
}
if notifyErr := auth.NotifySuccess(ctx, csr, certRep.Certificate, transactionID); notifyErr != nil {
// TODO(hs): ignore this error case? It's not critical if the notification fails; but logging it might be good
_ = notifyErr
}
res := Response{
Operation: opnPKIOperation,
Data: certRep.Raw,

@ -508,6 +508,16 @@ func (a *Authority) ValidateChallenge(ctx context.Context, csr *x509.Certificate
return p.ValidateChallenge(ctx, csr, challenge, transactionID)
}
func (a *Authority) NotifySuccess(ctx context.Context, csr *x509.CertificateRequest, cert *x509.Certificate, transactionID string) error {
p := provisionerFromContext(ctx)
return p.NotifySuccess(ctx, csr, cert, transactionID)
}
func (a *Authority) NotifyFailure(ctx context.Context, csr *x509.CertificateRequest, transactionID string, errorCode int, errorDescription string) error {
p := provisionerFromContext(ctx)
return p.NotifyFailure(ctx, csr, transactionID, errorCode, errorDescription)
}
func (a *Authority) selectDecrypter(ctx context.Context) (cert *x509.Certificate, decrypter crypto.Decrypter, err error) {
p := provisionerFromContext(ctx)
cert, decrypter = p.GetDecrypter()

@ -21,6 +21,8 @@ type Provisioner interface {
GetSigner() (*x509.Certificate, crypto.Signer)
GetContentEncryptionAlgorithm() int
ValidateChallenge(ctx context.Context, csr *x509.CertificateRequest, challenge, transactionID string) 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
}
// provisionerKey is the key type for storing and searching a

@ -30,6 +30,7 @@ type X509Certificate struct {
PublicKeyAlgorithm string `json:"publicKeyAlgorithm"`
NotBefore time.Time `json:"notBefore"`
NotAfter time.Time `json:"notAfter"`
Raw []byte `json:"raw"`
}
// SSHCertificateRequest is the certificate request sent to webhook servers for
@ -79,9 +80,11 @@ 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"`
// Only set for SCEP webhook requests
SCEPChallenge string `json:"scepChallenge,omitempty"`
SCEPTransactionID string `json:"scepTransactionID,omitempty"`
SCEPErrorCode int `json:"scepErrorCode,omitempty"`
SCEPErrorDescription string `json:"scepErrorDescription,omitempty"`
// Only set for X5C provisioners
X5CCertificate *X5CCertificate `json:"x5cCertificate,omitempty"`
// Set for X5C, AWS, GCP, and Azure provisioners

Loading…
Cancel
Save