Skip to content

Commit

Permalink
Asynchronously issue certificates
Browse files Browse the repository at this point in the history
Signed-off-by: Alex Richman <[email protected]>
  • Loading branch information
ARichman555 committed Sep 20, 2024
1 parent 1b9aff7 commit d03dde8
Show file tree
Hide file tree
Showing 4 changed files with 244 additions and 37 deletions.
32 changes: 18 additions & 14 deletions pkg/aws/pca.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
injections "github.com/cert-manager/aws-privateca-issuer/pkg/api/injections"
cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
"github.com/go-logr/logr"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)

Expand All @@ -42,7 +43,8 @@ var collection = new(sync.Map)

// GenericProvisioner abstracts over the Provisioner type for mocking purposes
type GenericProvisioner interface {
Sign(ctx context.Context, cr *cmapi.CertificateRequest, log logr.Logger) ([]byte, []byte, error)
Get(ctx context.Context, cr *cmapi.CertificateRequest, certArn string, log logr.Logger) ([]byte, []byte, error)
Sign(ctx context.Context, cr *cmapi.CertificateRequest, log logr.Logger) error
}

// acmPCAClient abstracts over the methods used from acmpca.Client
Expand Down Expand Up @@ -94,10 +96,10 @@ func idempotencyToken(cr *cmapi.CertificateRequest) string {
}

// Sign takes a certificate request and signs it using PCA
func (p *PCAProvisioner) Sign(ctx context.Context, cr *cmapi.CertificateRequest, log logr.Logger) ([]byte, []byte, error) {
func (p *PCAProvisioner) Sign(ctx context.Context, cr *cmapi.CertificateRequest, log logr.Logger) error {
block, _ := pem.Decode(cr.Spec.Request)
if block == nil {
return nil, nil, fmt.Errorf("failed to decode CSR")
return fmt.Errorf("failed to decode CSR")
}

validityExpiration := int64(p.now().Unix()) + DEFAULT_DURATION
Expand All @@ -112,7 +114,7 @@ func (p *PCAProvisioner) Sign(ctx context.Context, cr *cmapi.CertificateRequest,

err := getSigningAlgorithm(ctx, p)
if err != nil {
return nil, nil, err
return err
}

issueParams := acmpca.IssueCertificateInput{
Expand All @@ -130,20 +132,20 @@ func (p *PCAProvisioner) Sign(ctx context.Context, cr *cmapi.CertificateRequest,
issueOutput, err := p.pcaClient.IssueCertificate(ctx, &issueParams)

if err != nil {
return nil, nil, err
return err
}

getParams := acmpca.GetCertificateInput{
CertificateArn: aws.String(*issueOutput.CertificateArn),
CertificateAuthorityArn: aws.String(p.arn),
}
metav1.SetMetaDataAnnotation(&cr.ObjectMeta, "aws-privateca-issuer/certificate-arn", *issueOutput.CertificateArn)

log.Info("Created certificate with arn: " + *issueOutput.CertificateArn)
log.Info("Issued certificate with arn: " + *issueOutput.CertificateArn)

waiter := acmpca.NewCertificateIssuedWaiter(p.pcaClient)
err = waiter.Wait(ctx, &getParams, 5*time.Minute)
if err != nil {
return nil, nil, err
return nil
}

func (p *PCAProvisioner) Get(ctx context.Context, cr *cmapi.CertificateRequest, certArn string, log logr.Logger) ([]byte, []byte, error) {
getParams := acmpca.GetCertificateInput{
CertificateArn: aws.String(certArn),
CertificateAuthorityArn: aws.String(p.arn),
}

getOutput, err := p.pcaClient.GetCertificate(ctx, &getParams)
Expand All @@ -159,6 +161,8 @@ func (p *PCAProvisioner) Sign(ctx context.Context, cr *cmapi.CertificateRequest,
}
certPem = append(certPem, chainIntCAs...)

log.Info("Created certificate with arn: ")

return certPem, rootCA, nil
}

Expand Down
60 changes: 56 additions & 4 deletions pkg/aws/pca_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,10 @@ func (m *errorACMPCAClient) IssueCertificate(_ context.Context, input *acmpca.Is
return nil, errors.New("Cannot issue certificate")
}

func (m *errorACMPCAClient) GetCertificate(_ context.Context, input *acmpca.GetCertificateInput, _ ...func(*acmpca.Options)) (*acmpca.GetCertificateOutput, error) {
return nil, errors.New("Cannot get certificate")
}

type workingACMPCAClient struct {
acmPCAClient
issueCertInput *acmpca.IssueCertificateInput
Expand Down Expand Up @@ -334,7 +338,7 @@ func TestIdempotencyToken(t *testing.T) {
}
}

func TestPCASign(t *testing.T) {
func TestPCAGet(t *testing.T) {
type testCase struct {
provisioner PCAProvisioner
expectFailure bool
Expand All @@ -349,7 +353,7 @@ func TestPCASign(t *testing.T) {
expectedChain: string([]byte(root + "\n")),
expectedCert: string([]byte(cert + "\n" + intermediate + "\n")),
},
"failure-error-issueCertificate": {
"failure-error-getCertificate": {
provisioner: PCAProvisioner{arn: arn, pcaClient: &errorACMPCAClient{}},
expectFailure: true,
},
Expand All @@ -369,7 +373,8 @@ func TestPCASign(t *testing.T) {
},
}

leaf, chain, err := tc.provisioner.Sign(context.TODO(), cr, logr.Discard())
leaf, chain, err := tc.provisioner.Get(context.TODO(), cr, certArn, logr.Discard())

if tc.expectFailure && err == nil {
fmt.Print(err.Error())
assert.Fail(t, "Expected an error but received none")
Expand All @@ -383,6 +388,53 @@ func TestPCASign(t *testing.T) {
}
}

func TestPCASign(t *testing.T) {
type testCase struct {
provisioner PCAProvisioner
expectFailure bool
expectedCertArn string
}

tests := map[string]testCase{
"success": {
provisioner: PCAProvisioner{arn: arn, pcaClient: &workingACMPCAClient{}},
expectFailure: false,
expectedCertArn: "arn",
},
"failure-error-issueCertificate": {
provisioner: PCAProvisioner{arn: arn, pcaClient: &errorACMPCAClient{}},
expectFailure: true,
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
key, _ := rsa.GenerateKey(rand.Reader, 2048)
csrBytes, _ := x509.CreateCertificateRequest(rand.Reader, &template, key)

cr := &v1.CertificateRequest{
Spec: v1.CertificateRequestSpec{
Request: pem.EncodeToMemory(&pem.Block{
Bytes: csrBytes,
Type: "CERTIFICATE REQUEST",
}),
},
}

err := tc.provisioner.Sign(context.TODO(), cr, logr.Discard())

if tc.expectFailure && err == nil {
fmt.Print(err.Error())
assert.Fail(t, "Expected an error but received none")
}

if tc.expectedCertArn != "" {
assert.Equal(t, cr.ObjectMeta.GetAnnotations()["aws-privateca-issuer/certificate-arn"], tc.expectedCertArn)
}
})
}
}

func TestPCASignValidity(t *testing.T) {
now := time.Now()
client := &workingACMPCAClient{}
Expand Down Expand Up @@ -432,7 +484,7 @@ func TestPCASignValidity(t *testing.T) {
},
}

_, _, _ = provisioner.Sign(context.TODO(), cr, logr.Discard())
_ = provisioner.Sign(context.TODO(), cr, logr.Discard())
got := client.issueCertInput
if got == nil {
assert.Fail(t, "Expected certificate input, got none")
Expand Down
32 changes: 25 additions & 7 deletions pkg/controllers/certificaterequest_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ package controllers

import (
"context"
"errors"
"fmt"

acmpcatypes "github.com/aws/aws-sdk-go-v2/service/acmpca/types"
"github.com/cert-manager/aws-privateca-issuer/pkg/aws"
"github.com/cert-manager/aws-privateca-issuer/pkg/util"
cmapi "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1"
"k8s.io/apimachinery/pkg/api/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/tools/record"

Expand Down Expand Up @@ -65,7 +67,7 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R
log := r.Log.WithValues("certificaterequest", req.NamespacedName)
cr := new(cmapi.CertificateRequest)
if err := r.Client.Get(ctx, req.NamespacedName, cr); err != nil {
if errors.IsNotFound(err) {
if apierrors.IsNotFound(err) {
return ctrl.Result{}, client.IgnoreNotFound(err)
}

Expand Down Expand Up @@ -161,14 +163,31 @@ func (r *CertificateRequestReconciler) Reconcile(ctx context.Context, req ctrl.R
return ctrl.Result{}, err
}

pem, ca, err := provisioner.Sign(ctx, cr, log)
certArn, exists := cr.ObjectMeta.GetAnnotations()["aws-privateca-issuer/certificate-arn"]
if !exists {
err := provisioner.Sign(ctx, cr, log)
if err != nil {
log.Error(err, "failed to request certificate from PCA")
return ctrl.Result{}, r.setStatus(ctx, cr, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonFailed, "failed to request certificate from PCA: "+err.Error())
}

return ctrl.Result{Requeue: true}, r.Client.Update(ctx, cr)
}

pem, ca, err := provisioner.Get(ctx, cr, certArn, log)
if err != nil {
log.Error(err, "failed to request certificate from PCA")
return ctrl.Result{}, r.setStatus(ctx, cr, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonFailed, "failed to request certificate from PCA: "+err.Error())
var errorType *acmpcatypes.RequestInProgressException
if errors.As(err, &errorType) {
log.Info("certificate is still issuing")
return ctrl.Result{Requeue: true}, r.setStatus(ctx, cr, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonPending, "waiting for certificate to be issued")
}

log.Error(err, "failed to issue certificate from PCA")
return ctrl.Result{}, r.setStatus(ctx, cr, cmmeta.ConditionFalse, cmapi.CertificateRequestReasonFailed, "failed to issue certificate from PCA: "+err.Error())
}

cr.Status.Certificate = pem
cr.Status.CA = ca

return ctrl.Result{}, r.setStatus(ctx, cr, cmmeta.ConditionTrue, cmapi.CertificateRequestReasonIssued, "certificate issued")
}

Expand Down Expand Up @@ -197,6 +216,5 @@ func (r *CertificateRequestReconciler) setStatus(ctx context.Context, cr *cmapi.
eventType = core.EventTypeWarning
}
r.Recorder.Event(cr, eventType, reason, completeMessage)

return r.Client.Status().Update(ctx, cr)
}
Loading

0 comments on commit d03dde8

Please sign in to comment.