Skip to content

Commit

Permalink
Add AWS CA issuer
Browse files Browse the repository at this point in the history
Adds support for the AWS Certificate Manager Private
Certificate Authority.
  • Loading branch information
johanbrandhorst committed Jan 26, 2019
1 parent 0696bfc commit 075df4d
Show file tree
Hide file tree
Showing 103 changed files with 16,300 additions and 36 deletions.
40 changes: 36 additions & 4 deletions Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

9 changes: 9 additions & 0 deletions Gopkg.toml
Expand Up @@ -20,3 +20,12 @@ required = [
[[constraint]]
name = "github.com/cloudflare/cfssl"
version = "1.3.2"

[[constraint]]
name = "github.com/aws/aws-sdk-go-v2"
version = "0.7.0"

[[constraint]]
name = "github.com/matryer/moq"
# Pending https://github.com/matryer/moq/issues/86
revision = "c4521bcc9dbca426d823d54b775a85c30cc527db"
1 change: 1 addition & 0 deletions issuers/aws/.gitignore
@@ -0,0 +1 @@
aws_secret_test.go
178 changes: 178 additions & 0 deletions issuers/aws/aws.go
@@ -0,0 +1,178 @@
package aws

import (
"context"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/tls"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"errors"
"fmt"

"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/service/acmpca"
iface "github.com/aws/aws-sdk-go-v2/service/acmpca/acmpcaiface"

"github.com/johanbrandhorst/certify"
)

// Issuer implements the Issuer interface with a
// AWS Certificate Manager Private Certificate Authority backend.
//
// Client and CertificateAuthorityARN are required.
type Issuer struct {
// Client is a pre-created ACMPCA client. It can be created
// via, for example:
// conf, err := external.LoadDefaultAWSConfig()
// if err != nil {
// return nil, err
// }
// conf.Region = endpoints.EuWest2RegionID
// conf.Credentials = aws.NewStaticCredentialsProvider("YOURKEY", "YOURKEYSECRET", "")
// cli := acmpca.New(conf)
Client iface.ACMPCAAPI
// CertificateAuthorityARN specifies the ARN of a pre-created CA
// which will be used to issue the certificates.
CertificateAuthorityARN string

// TimeToLive configures the lifetime of certificates
// requested from the AWS CA, in number of days.
// If unset, defaults to 30 days.
TimeToLive int

caCert *x509.Certificate
signAlgo acmpca.SigningAlgorithm
}

// Issue issues a certificate from the configured AWS CA backend.
func (i Issuer) Issue(ctx context.Context, commonName string, conf *certify.CertConfig) (*tls.Certificate, error) {
if i.caCert == nil {
caReq := i.Client.GetCertificateAuthorityCertificateRequest(&acmpca.GetCertificateAuthorityCertificateInput{
CertificateAuthorityArn: aws.String(i.CertificateAuthorityARN),
})

caResp, err := caReq.Send()
if err != nil {
return nil, err
}

caBlock, _ := pem.Decode([]byte(*caResp.Certificate))
if caBlock == nil {
return nil, errors.New("could not parse AWS CA cert")
}

if caBlock.Type != "CERTIFICATE" {
return nil, errors.New("saw unexpected PEM Type while requesting AWS CA cert: " + caBlock.Type)
}

i.caCert, err = x509.ParseCertificate(caBlock.Bytes)
if err != nil {
return nil, err
}

switch i.caCert.SignatureAlgorithm {
case x509.SHA256WithRSA:
i.signAlgo = acmpca.SigningAlgorithmSha256withrsa
case x509.SHA384WithRSA:
i.signAlgo = acmpca.SigningAlgorithmSha384withrsa
case x509.SHA512WithRSA:
i.signAlgo = acmpca.SigningAlgorithmSha512withrsa
case x509.ECDSAWithSHA256:
i.signAlgo = acmpca.SigningAlgorithmSha256withecdsa
case x509.ECDSAWithSHA384:
i.signAlgo = acmpca.SigningAlgorithmSha384withecdsa
case x509.ECDSAWithSHA512:
i.signAlgo = acmpca.SigningAlgorithmSha512withecdsa
default:
return nil, fmt.Errorf("unsupported CA cert signing algorithm: %T", i.caCert.SignatureAlgorithm)
}
}

pk, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
return nil, err
}

keyBytes, err := x509.MarshalECPrivateKey(pk)
if err != nil {
return nil, err
}

keyPEM := pem.EncodeToMemory(&pem.Block{
Type: "EC PRIVATE KEY",
Bytes: keyBytes,
})

template := &x509.CertificateRequest{
SignatureAlgorithm: x509.ECDSAWithSHA256,
Subject: pkix.Name{
CommonName: commonName,
},
}

if conf != nil {
template.DNSNames = conf.SubjectAlternativeNames
template.IPAddresses = conf.IPSubjectAlternativeNames
}

csr, err := x509.CreateCertificateRequest(rand.Reader, template, pk)
if err != nil {
return nil, err
}

csrPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE REQUEST",
Bytes: csr,
})

// Default to 30 days if unset.
ttl := int64(30)
if i.TimeToLive > 0 {
ttl = int64(i.TimeToLive)
}

csrReq := i.Client.IssueCertificateRequest(&acmpca.IssueCertificateInput{
CertificateAuthorityArn: aws.String(i.CertificateAuthorityARN),
Csr: csrPEM,
SigningAlgorithm: i.signAlgo,
Validity: &acmpca.Validity{
Type: acmpca.ValidityPeriodTypeDays,
Value: aws.Int64(ttl),
},
})

csrResp, err := csrReq.Send()
if err != nil {
return nil, err
}

getReq := &acmpca.GetCertificateInput{
CertificateArn: csrResp.CertificateArn,
CertificateAuthorityArn: aws.String(i.CertificateAuthorityARN),
}
err = i.Client.WaitUntilCertificateIssuedWithContext(ctx, getReq)
if err != nil {
return nil, err
}

certReq := i.Client.GetCertificateRequest(getReq)

certResp, err := certReq.Send()
if err != nil {
return nil, err
}

caChainPEM := append([]byte(*certResp.Certificate), []byte(*certResp.CertificateChain)...)

tlsCert, err := tls.X509KeyPair(caChainPEM, keyPEM)
if err != nil {
return nil, err
}

// This can't error since it's called in tls.X509KeyPair above successfully
tlsCert.Leaf, _ = x509.ParseCertificate(tlsCert.Certificate[0])
return &tlsCert, nil
}
13 changes: 13 additions & 0 deletions issuers/aws/aws_suite_test.go
@@ -0,0 +1,13 @@
package aws_test

import (
"testing"

. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)

func TestAws(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Aws Suite")
}

0 comments on commit 075df4d

Please sign in to comment.