Repository: smallstep/crypto
Pull request #302 (2023-08-03 21:39:32)

Template funcs
### Description

This commit adds functions that return a map of the functions used by templates. `step-ca` currently fails to create a provisioner with a template using the new functions because the validation of the template fails.


Patch:
diff --git a/internal/templates/validate.go b/internal/templates/validate.go
index f8b5e7b..af0b25e 100644
--- a/internal/templates/validate.go
+++ b/internal/templates/validate.go
@@ -12,15 +12,11 @@ import (
 // results in invalid JSON, the template is invalid. When the template
 // is valid, it can be used safely. A valid template can still result
 // in invalid JSON when non-empty template data is provided.
-func ValidateTemplate(data []byte) error {
+func ValidateTemplate(data []byte, funcMap template.FuncMap) error {
 	if len(data) == 0 {
 		return nil
 	}
 
-	// get the default supported functions
-	var failMessage string
-	funcMap := GetFuncMap(&failMessage)
-
 	// prepare the template with our template functions
 	_, err := template.New("template").Funcs(funcMap).Parse(string(data))
 	if err != nil {
diff --git a/kms/capi/capi_no_windows.go b/kms/capi/capi_no_windows.go
index 17939af..092c88e 100644
--- a/kms/capi/capi_no_windows.go
+++ b/kms/capi/capi_no_windows.go
@@ -5,6 +5,7 @@ package capi
 
 import (
 	"context"
+
 	"github.com/pkg/errors"
 	"go.step.sm/crypto/kms/apiv1"
 )
diff --git a/kms/capi/ncrypt_windows.go b/kms/capi/ncrypt_windows.go
index ccde89a..49d4dc5 100644
--- a/kms/capi/ncrypt_windows.go
+++ b/kms/capi/ncrypt_windows.go
@@ -11,8 +11,9 @@ import (
 	"encoding/binary"
 	"errors"
 	"fmt"
-	"golang.org/x/sys/windows"
 	"unsafe"
+
+	"golang.org/x/sys/windows"
 )
 
 const (
@@ -152,7 +153,7 @@ type BCRYPT_PKCS1_PADDING_INFO struct {
 	pszAlgID *uint16
 }
 
-//CRYPTOAPI_BLOB -- https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/aa381414(v=vs.85)
+// CRYPTOAPI_BLOB -- https://learn.microsoft.com/en-us/previous-versions/windows/desktop/legacy/aa381414(v=vs.85)
 type CRYPTOAPI_BLOB struct {
 	len  uint32
 	data uintptr
diff --git a/kms/tpmkms/no_tpmkms.go b/kms/tpmkms/no_tpmkms.go
index 2a84569..f80e22c 100644
--- a/kms/tpmkms/no_tpmkms.go
+++ b/kms/tpmkms/no_tpmkms.go
@@ -3,6 +3,15 @@
 
 package tpmkms
 
+import (
+	"context"
+	"os"
+	"path/filepath"
+
+	"github.com/pkg/errors"
+	"go.step.sm/crypto/kms/apiv1"
+)
+
 func init() {
 	apiv1.Register(apiv1.TPMKMS, func(ctx context.Context, opts apiv1.Options) (apiv1.KeyManager, error) {
 		name := filepath.Base(os.Args[0])
diff --git a/sshutil/options.go b/sshutil/options.go
index 144e43d..8b937bd 100644
--- a/sshutil/options.go
+++ b/sshutil/options.go
@@ -28,13 +28,24 @@ func (o *Options) apply(cr CertificateRequest, opts []Option) (*Options, error)
 // Option is the type used as a variadic argument in NewCertificate.
 type Option func(cr CertificateRequest, o *Options) error
 
+// GetFuncMap returns the list of functions used by the templates. It will
+// return all the functions supported by "sprig.TxtFuncMap()" but exclude "env"
+// and "expandenv", removed to avoid the leak of information.
+func GetFuncMap() template.FuncMap {
+	return getFuncMap(new(TemplateError))
+}
+
+func getFuncMap(err *TemplateError) template.FuncMap {
+	return templates.GetFuncMap(&err.Message)
+}
+
 // WithTemplate is an options that executes the given template text with the
 // given data.
 func WithTemplate(text string, data TemplateData) Option {
 	return func(cr CertificateRequest, o *Options) error {
 		terr := new(TemplateError)
-		funcMap := templates.GetFuncMap(&terr.Message)
-
+		funcMap := getFuncMap(terr)
+		// Parse template
 		tmpl, err := template.New("template").Funcs(funcMap).Parse(text)
 		if err != nil {
 			return errors.Wrapf(err, "error parsing template")
diff --git a/sshutil/templates.go b/sshutil/templates.go
index de1f898..623a4bb 100644
--- a/sshutil/templates.go
+++ b/sshutil/templates.go
@@ -32,7 +32,7 @@ func (e *TemplateError) Error() string {
 
 // ValidateTemplate validates a text template.
 func ValidateTemplate(text []byte) error {
-	return templates.ValidateTemplate(text)
+	return templates.ValidateTemplate(text, GetFuncMap())
 }
 
 // ValidateTemplateData validates that template data is
diff --git a/x509util/options.go b/x509util/options.go
index fff689d..c56bba6 100644
--- a/x509util/options.go
+++ b/x509util/options.go
@@ -33,18 +33,39 @@ func (o *Options) apply(cr *x509.CertificateRequest, opts []Option) (*Options, e
 // Option is the type used as a variadic argument in NewCertificate.
 type Option func(cr *x509.CertificateRequest, o *Options) error
 
+// GetFuncMap returns the list of functions used by the templates. It will
+// return all the functions supported by "sprig.TxtFuncMap()" but exclude "env"
+// and "expandenv", removed to avoid the leak of information. It will also add
+// the following functions to encode data using ASN.1:
+//
+//   - asn1Enc: encodes the given string to ASN.1. By default, it will use the
+//     PrintableString format but it can be change using the suffix ":<format>".
+//     Supported formats are: "printable", "utf8", "ia5", "numeric", "int", "oid",
+//     "utc", "generalized", and "raw".
+//   - asn1Marshal: encodes the given string with the given params using Go's
+//     asn1.MarshalWithParams.
+//   - asn1Seq: encodes a sequence of the given ASN.1 data.
+//   - asn1Set: encodes a set of the given ASN.1 data.
+func GetFuncMap() template.FuncMap {
+	return getFuncMap(new(TemplateError))
+}
+
+func getFuncMap(err *TemplateError) template.FuncMap {
+	funcMap := templates.GetFuncMap(&err.Message)
+	// asn1 methods
+	funcMap["asn1Enc"] = asn1Encode
+	funcMap["asn1Marshal"] = asn1Marshal
+	funcMap["asn1Seq"] = asn1Sequence
+	funcMap["asn1Set"] = asn1Set
+	return funcMap
+}
+
 // WithTemplate is an options that executes the given template text with the
 // given data.
 func WithTemplate(text string, data TemplateData) Option {
 	return func(cr *x509.CertificateRequest, o *Options) error {
 		terr := new(TemplateError)
-		funcMap := templates.GetFuncMap(&terr.Message)
-		// asn1 methods
-		funcMap["asn1Enc"] = asn1Encode
-		funcMap["asn1Marshal"] = asn1Marshal
-		funcMap["asn1Seq"] = asn1Sequence
-		funcMap["asn1Set"] = asn1Set
-
+		funcMap := getFuncMap(terr)
 		// Parse template
 		tmpl, err := template.New("template").Funcs(funcMap).Parse(text)
 		if err != nil {
diff --git a/x509util/templates.go b/x509util/templates.go
index 3199d22..ff6ab0b 100644
--- a/x509util/templates.go
+++ b/x509util/templates.go
@@ -33,7 +33,7 @@ func (e *TemplateError) Error() string {
 
 // ValidateTemplate validates a text template.
 func ValidateTemplate(text []byte) error {
-	return templates.ValidateTemplate(text)
+	return templates.ValidateTemplate(text, GetFuncMap())
 }
 
 // ValidateTemplateData validates that template data is


Test patch:
diff --git a/internal/templates/validate_test.go b/internal/templates/validate_test.go
index da7c767..5b960b0 100644
--- a/internal/templates/validate_test.go
+++ b/internal/templates/validate_test.go
@@ -191,7 +191,8 @@ func TestValidateTemplate(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			err := ValidateTemplate(tt.data)
+			var failMessage string
+			err := ValidateTemplate(tt.data, GetFuncMap(&failMessage))
 			if tt.err != nil {
 				assert.Error(t, err)
 				assert.EqualError(t, err, tt.err.Error())
diff --git a/sshutil/options_test.go b/sshutil/options_test.go
index 7e90995..35f481e 100644
--- a/sshutil/options_test.go
+++ b/sshutil/options_test.go
@@ -7,6 +7,23 @@ import (
 	"testing"
 )
 
+func TestGetFuncMap(t *testing.T) {
+	ok := []string{"fail", "contains", "split"}
+	fail := []string{"env", "expandenv"}
+
+	funcMap := GetFuncMap()
+	for _, name := range ok {
+		if _, ok := funcMap[name]; !ok {
+			t.Errorf("GetFuncMap() does not contain the function %s", name)
+		}
+	}
+	for _, name := range fail {
+		if _, ok := funcMap[name]; ok {
+			t.Errorf("GetFuncMap() contains the function %s", name)
+		}
+	}
+}
+
 func TestWithTemplate(t *testing.T) {
 	key := mustGeneratePublicKey(t)
 	cr := CertificateRequest{
diff --git a/x509util/options_test.go b/x509util/options_test.go
index f22db79..c7b6dc0 100644
--- a/x509util/options_test.go
+++ b/x509util/options_test.go
@@ -41,6 +41,26 @@ func createRSACertificateRequest(t *testing.T, bits int, commonName string, sans
 	return cr, priv
 }
 
+func TestGetFuncMap(t *testing.T) {
+	ok := []string{
+		"fail", "contains", "split", // generic sprig functions
+		"asn1Enc", "asn1Marshal", "asn1Seq", "asn1Set", // custom functions
+	}
+	fail := []string{"env", "expandenv"}
+
+	funcMap := GetFuncMap()
+	for _, name := range ok {
+		if _, ok := funcMap[name]; !ok {
+			t.Errorf("GetFuncMap() does not contain the function %s", name)
+		}
+	}
+	for _, name := range fail {
+		if _, ok := funcMap[name]; ok {
+			t.Errorf("GetFuncMap() contains the function %s", name)
+		}
+	}
+}
+
 func TestWithTemplate(t *testing.T) {
 	cr, _ := createCertificateRequest(t, "foo", []string{"foo.com", "foo@foo.com", "::1", "https://foo.com"})
 	crRSA, _ := createRSACertificateRequest(t, 2048, "foo", []string{"foo.com", "foo@foo.com", "::1", "https://foo.com"})