From 38f3f5505ab4db7f682d1735eca44c10e61b7387 Mon Sep 17 00:00:00 2001 From: Michael Smith Date: Thu, 23 Jul 2020 16:45:55 -0700 Subject: [PATCH] Generate CA and certificates from CA Adds `TLSCACert`, `TLSCertFromCA`, and `TLSKeyFromCA` to generate a named CA cert/key pair and access the cert, as well as generate cert/key pairs from that CA as needed (based on the combination of CA name, cert name, and common name). Useful when you need a separate CA certificate, such as working around https://github.com/openssl/openssl/issues/1418. Fixes #844. --- pkg/template/static_context.go | 92 ++++++++++++++++++++++++++++- pkg/template/static_context_test.go | 82 +++++++++++++++++++++++++ 2 files changed, 173 insertions(+), 1 deletion(-) diff --git a/pkg/template/static_context.go b/pkg/template/static_context.go index 50374d83b0..5ce81fd86b 100644 --- a/pkg/template/static_context.go +++ b/pkg/template/static_context.go @@ -51,6 +51,7 @@ type TLSPair struct { } var tlsMap = map[string]TLSPair{} +var caMap = map[string]TLSPair{} func (ctx StaticCtx) FuncMap() template.FuncMap { funcMap := sprig.TxtFuncMap() @@ -82,6 +83,10 @@ func (ctx StaticCtx) FuncMap() template.FuncMap { funcMap["TLSCert"] = ctx.tlsCert funcMap["TLSKey"] = ctx.tlsKey + funcMap["TLSCACert"] = ctx.tlsCaCert + funcMap["TLSCertFromCA"] = ctx.tlsCertFromCa + funcMap["TLSKeyFromCA"] = ctx.tlsKeyFromCa + funcMap["IsKurl"] = ctx.isKurl funcMap["Distribution"] = ctx.distribution funcMap["NodeCount"] = ctx.nodeCount @@ -390,14 +395,99 @@ func (ctx StaticCtx) tlsKey(certName string, args ...interface{}) string { return p.Key } +func (ctx StaticCtx) tlsCaCert(caName string, args ...interface{}) string { + cap, ok := caMap[caName] + if !ok { + if len(args) != 1 { + return "" + } + + daysValid, ok := args[0].(int) + if !ok { + return "" + } + + cap = genCa(caName, daysValid) + caMap[caName] = cap + } + + return cap.Cert +} + +func (ctx StaticCtx) tlsCertFromCa(caName, certName, cn string, ips []interface{}, alternateDNS []interface{}, daysValid int) string { + key := fmt.Sprintf("%s:%s:%s", caName, certName, cn) + if p, ok := tlsMap[key]; ok { + return p.Cert + } + + p := genSignedCert(caName, cn, ips, alternateDNS, daysValid) + tlsMap[key] = p + return p.Cert +} + +func (ctx StaticCtx) tlsKeyFromCa(caName, certName, cn string, args ...interface{}) string { + key := fmt.Sprintf("%s:%s:%s", caName, certName, cn) + if p, ok := tlsMap[key]; ok { + return p.Key + } + + if len(args) != 3 { + return "" + } + + ips, ok := args[0].([]interface{}) + if !ok { + return "" + } + + alternateDNS, ok := args[1].([]interface{}) + if !ok { + return "" + } + + daysValid, ok := args[2].(int) + if !ok { + return "" + } + + p := genSignedCert(caName, cn, ips, alternateDNS, daysValid) + tlsMap[key] = p + return p.Key +} + +func genCa(cn string, daysValid int) TLSPair { + tmplate := `cert: {{ $i := genCA %q %d }}{{ $i.Cert | b64enc }} +key: {{ $i.Key | b64enc }}` + return genCertAndKey(cn, fmt.Sprintf(tmplate, cn, daysValid)) +} + +func genSignedCert(ca, cn string, ips []interface{}, alternateDNS []interface{}, daysValid int) TLSPair { + tmplate := `cert: {{ $ca := buildCustomCert %q %q }}{{ $i := genSignedCert %q %s %s %d $ca }}{{ $i.Cert | b64enc }} +key: {{ $i.Key | b64enc }}` + + cap, ok := caMap[ca] + if !ok { + cap = genCa(ca, daysValid) + caMap[ca] = cap + } + + caCert := base64.StdEncoding.EncodeToString([]byte(cap.Cert)) + caKey := base64.StdEncoding.EncodeToString([]byte(cap.Key)) + ipList := arrayToTemplateList(ips) + nameList := arrayToTemplateList(alternateDNS) + return genCertAndKey(cn, fmt.Sprintf(tmplate, caCert, caKey, cn, ipList, nameList, daysValid)) +} + func genSelfSignedCert(cn string, ips []interface{}, alternateDNS []interface{}, daysValid int) TLSPair { tmplate := `cert: {{ $i := genSelfSignedCert %q %s %s %d }}{{ $i.Cert | b64enc }} key: {{ $i.Key | b64enc }}` ipList := arrayToTemplateList(ips) nameList := arrayToTemplateList(alternateDNS) - templated := fmt.Sprintf(tmplate, cn, ipList, nameList, daysValid) + return genCertAndKey(cn, fmt.Sprintf(tmplate, cn, ipList, nameList, daysValid)) +} +func genCertAndKey(cn, templated string) TLSPair { parsed, err := template.New("cn").Funcs(sprig.GenericFuncMap()).Parse(templated) if err != nil { fmt.Printf("Failed to evaluate cert template: %v\n", err) diff --git a/pkg/template/static_context_test.go b/pkg/template/static_context_test.go index 883570d077..d07bc69f6b 100644 --- a/pkg/template/static_context_test.go +++ b/pkg/template/static_context_test.go @@ -1,6 +1,9 @@ package template import ( + "crypto/x509" + "encoding/pem" + "fmt" "testing" "github.com/stretchr/testify/require" @@ -77,3 +80,82 @@ func TestSprigRandom(t *testing.T) { req.NoError(err) req.Len(randAlphaNum, 50) } + +func TestTlsCaCert(t *testing.T) { + scopetest := scopeagent.StartTest(t) + defer scopetest.End() + req := require.New(t) + + builder := Builder{} + builder.AddCtx(StaticCtx{}) + + caCert, err := builder.String(`{{repl TLSCACert "my-ca" 365}}`) + req.NoError(err) + + cert, err := getCert(caCert) + req.NoError(err) + req.NotZero(cert.KeyUsage & x509.KeyUsageCertSign) + + expected := caMap["my-ca"] + req.Equal(expected.Cert, caCert) + delete(caMap, "my-ca") +} + +func TestTlsCertFromCa(t *testing.T) { + scopetest := scopeagent.StartTest(t) + defer scopetest.End() + req := require.New(t) + + builder := Builder{} + builder.AddCtx(StaticCtx{}) + + cert, err := builder.String(`{{repl TLSCertFromCA "my-ca" "my-cert" "mine.example.com" nil nil 365}}`) + req.NoError(err) + + certObj, err := getCert(cert) + req.NoError(err) + req.Equal("CN=mine.example.com", certObj.Subject.String()) + req.Equal("CN=my-ca", certObj.Issuer.String()) + + expected := tlsMap["my-ca:my-cert:mine.example.com"] + req.Equal("mine.example.com", expected.Cn) + req.Equal(expected.Cert, cert) + + _, err = builder.String(`{{repl TLSKeyFromCA "my-ca" "my-cert" "mine.example.com"}}`) + req.NoError(err) + delete(tlsMap, "my-ca:my-cert:mine.example.com") +} + +func TestTlsKeyFromCa(t *testing.T) { + scopetest := scopeagent.StartTest(t) + defer scopetest.End() + req := require.New(t) + + builder := Builder{} + builder.AddCtx(StaticCtx{}) + + _, err := builder.String(`{{repl TLSKeyFromCA "my-ca" "my-cert" "mine.example.com" nil nil 365}}`) + req.NoError(err) + + cert, err := builder.String(`{{repl TLSCertFromCA "my-ca" "my-cert" "mine.example.com" nil nil 365}}`) + req.NoError(err) + + certObj, err := getCert(cert) + req.NoError(err) + req.Equal("CN=mine.example.com", certObj.Subject.String()) + req.Equal("CN=my-ca", certObj.Issuer.String()) + + expected := tlsMap["my-ca:my-cert:mine.example.com"] + req.Equal("mine.example.com", expected.Cn) + req.Equal(expected.Cert, cert) + delete(tlsMap, "my-ca:my-cert:mine.example.com") +} + +func getCert(s string) (*x509.Certificate, error) { + block, _ := pem.Decode([]byte(s)) + if block == nil { + return nil, fmt.Errorf("failed to decode PEM: %s", s) + } + + return x509.ParseCertificate(block.Bytes) +}