Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add SCEP issuance notification webhook #1544

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 1 addition & 3 deletions authority/admin/api/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
81 changes: 80 additions & 1 deletion authority/provisioner/scep.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ type SCEP struct {
ctl *Controller
encryptionAlgorithm int
challengeValidationController *challengeValidationController
notificationController *notificationController
keyManager kmsapi.KeyManager
decrypter crypto.Decrypter
decrypterCertificate *x509.Certificate
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 (
Expand Down
9 changes: 9 additions & 0 deletions scep/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
10 changes: 10 additions & 0 deletions scep/authority.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
2 changes: 2 additions & 0 deletions scep/provisioner.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 6 additions & 3 deletions webhook/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down