Repository: smallstep/crypto
Pull request #534 (2024-06-28 02:12:25)

Add support validities in templates
### Description

This commit allows setting the validity bounds on X.509 and SSH certificates using templates. 

It also adds the template method `toTime`, which makes it easier to format a timestamp to RFC3339. This format is compatible with Go's `time.Time` when `json.Unmarshal` is used. There are similar methods to do this in the sprig library, but they require you to specify the format string. For example, these methods will return a valid string that can be unmarshaled to `time.Time`:

```go
{{ date "2006-01-02T15:04:05Z07:00" .Token.nbf | toJson }}
{{ dateInZone "2006-01-02T15:04:05Z07:00" .Token.nbf "UTC" | toJson }}
{{ toTime .Token.nbf | toJson }}
```



Patch:
diff --git a/internal/templates/funcmap.go b/internal/templates/funcmap.go
index 01b6866..76dd6e7 100644
--- a/internal/templates/funcmap.go
+++ b/internal/templates/funcmap.go
@@ -3,14 +3,19 @@ package templates
 import (
 	"errors"
 	"text/template"
+	"time"
 
 	"github.com/Masterminds/sprig/v3"
+	"go.step.sm/crypto/jose"
 )
 
-// GetFuncMap returns the list of functions provided by sprig. It changes the
-// function "fail" to set the given string, this way we can report template
-// errors directly to the template without having the wrapper that text/template
-// adds.
+// GetFuncMap returns the list of functions provided by sprig. It adds the
+// function "toTime" and changes the function "fail".
+//
+// The "toTime" function receives a time or a Unix epoch and formats it to
+// RFC3339 in UTC. The "fail" function sets the provided message, so that
+// template errors are reported directly to the template without having the
+// wrapper that text/template adds.
 //
 // sprig "env" and "expandenv" functions are removed to avoid the leak of
 // information.
@@ -22,5 +27,31 @@ func GetFuncMap(failMessage *string) template.FuncMap {
 		*failMessage = msg
 		return "", errors.New(msg)
 	}
+	m["toTime"] = toTime
 	return m
 }
+
+func toTime(v any) string {
+	var t time.Time
+	switch date := v.(type) {
+	case time.Time:
+		t = date
+	case *time.Time:
+		t = *date
+	case int64:
+		t = time.Unix(date, 0)
+	case float64: // from json
+		t = time.Unix(int64(date), 0)
+	case int:
+		t = time.Unix(int64(date), 0)
+	case int32:
+		t = time.Unix(int64(date), 0)
+	case jose.NumericDate:
+		t = date.Time()
+	case *jose.NumericDate:
+		t = date.Time()
+	default:
+		t = time.Now()
+	}
+	return t.UTC().Format(time.RFC3339)
+}
diff --git a/sshutil/certificate.go b/sshutil/certificate.go
index 1bc59fb..a2367dd 100644
--- a/sshutil/certificate.go
+++ b/sshutil/certificate.go
@@ -6,6 +6,7 @@ import (
 	"crypto/rand"
 	"encoding/binary"
 	"encoding/json"
+	"time"
 
 	"github.com/pkg/errors"
 	"go.step.sm/crypto/randutil"
@@ -20,8 +21,8 @@ type Certificate struct {
 	Type            CertType          `json:"type"`
 	KeyID           string            `json:"keyId"`
 	Principals      []string          `json:"principals"`
-	ValidAfter      uint64            `json:"-"`
-	ValidBefore     uint64            `json:"-"`
+	ValidAfter      time.Time         `json:"validAfter"`
+	ValidBefore     time.Time         `json:"validBefore"`
 	CriticalOptions map[string]string `json:"criticalOptions"`
 	Extensions      map[string]string `json:"extensions"`
 	Reserved        []byte            `json:"reserved"`
@@ -62,8 +63,8 @@ func (c *Certificate) GetCertificate() *ssh.Certificate {
 		CertType:        uint32(c.Type),
 		KeyId:           c.KeyID,
 		ValidPrincipals: c.Principals,
-		ValidAfter:      c.ValidAfter,
-		ValidBefore:     c.ValidBefore,
+		ValidAfter:      toValidity(c.ValidAfter),
+		ValidBefore:     toValidity(c.ValidBefore),
 		Permissions: ssh.Permissions{
 			CriticalOptions: c.CriticalOptions,
 			Extensions:      c.Extensions,
@@ -124,3 +125,10 @@ func CreateCertificate(cert *ssh.Certificate, signer ssh.Signer) (*ssh.Certifica
 
 	return cert, nil
 }
+
+func toValidity(t time.Time) uint64 {
+	if t.IsZero() {
+		return 0
+	}
+	return uint64(t.Unix())
+}
diff --git a/x509util/certificate.go b/x509util/certificate.go
index 918e28d..69eb537 100644
--- a/x509util/certificate.go
+++ b/x509util/certificate.go
@@ -7,6 +7,7 @@ import (
 	"crypto/rand"
 	"crypto/x509"
 	"encoding/json"
+	"time"
 
 	"github.com/pkg/errors"
 )
@@ -23,6 +24,8 @@ type Certificate struct {
 	IPAddresses           MultiIP                  `json:"ipAddresses"`
 	URIs                  MultiURL                 `json:"uris"`
 	SANs                  []SubjectAlternativeName `json:"sans"`
+	NotBefore             time.Time                `json:"notBefore"`
+	NotAfter              time.Time                `json:"notAfter"`
 	Extensions            []Extension              `json:"extensions"`
 	KeyUsage              KeyUsage                 `json:"keyUsage"`
 	ExtKeyUsage           ExtKeyUsage              `json:"extKeyUsage"`
@@ -165,6 +168,10 @@ func (c *Certificate) GetCertificate() *x509.Certificate {
 		e.Set(cert)
 	}
 
+	// Validity bounds.
+	cert.NotBefore = c.NotBefore
+	cert.NotAfter = c.NotAfter
+
 	// Others.
 	c.SerialNumber.Set(cert)
 	c.SignatureAlgorithm.Set(cert)


Test patch:
diff --git a/internal/templates/funcmap_test.go b/internal/templates/funcmap_test.go
index 155aed1..924b306 100644
--- a/internal/templates/funcmap_test.go
+++ b/internal/templates/funcmap_test.go
@@ -3,6 +3,11 @@ package templates
 import (
 	"errors"
 	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"go.step.sm/crypto/jose"
 )
 
 func Test_GetFuncMap_fail(t *testing.T) {
@@ -20,3 +25,51 @@ func Test_GetFuncMap_fail(t *testing.T) {
 		t.Errorf("fail() message = \"%s\", want \"the fail message\"", failMesage)
 	}
 }
+
+func TestGetFuncMap_toTime(t *testing.T) {
+	now := time.Now()
+	numericDate := jose.NewNumericDate(now)
+	expected := now.UTC().Format(time.RFC3339)
+	loc, err := time.LoadLocation("America/Los_Angeles")
+	require.NoError(t, err)
+
+	type args struct {
+		v any
+	}
+	tests := []struct {
+		name string
+		args args
+		want string
+	}{
+		{"time", args{now}, expected},
+		{"time pointer", args{&now}, expected},
+		{"time UTC", args{now.UTC()}, expected},
+		{"time with location", args{now.In(loc)}, expected},
+		{"unix", args{now.Unix()}, expected},
+		{"unix int", args{int(now.Unix())}, expected},
+		{"unix int32", args{int32(now.Unix())}, expected},
+		{"unix float64", args{float64(now.Unix())}, expected},
+		{"unix float64", args{float64(now.Unix()) + 0.999}, expected},
+		{"jose.NumericDate", args{*numericDate}, expected},
+		{"jose.NumericDate pointer", args{numericDate}, expected},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var failMesage string
+			fns := GetFuncMap(&failMesage)
+			fn := fns["toTime"].(func(any) string)
+			assert.Equal(t, tt.want, fn(tt.args.v))
+		})
+	}
+
+	t.Run("default", func(t *testing.T) {
+		var failMesage string
+		fns := GetFuncMap(&failMesage)
+		fn := fns["toTime"].(func(any) string)
+		want := time.Now()
+		got, err := time.Parse(time.RFC3339, fn(nil))
+		require.NoError(t, err)
+		assert.WithinDuration(t, want, got, time.Second)
+		assert.Equal(t, time.UTC, got.Location())
+	})
+}
diff --git a/sshutil/certificate_test.go b/sshutil/certificate_test.go
index 7f67d68..be6cf9c 100644
--- a/sshutil/certificate_test.go
+++ b/sshutil/certificate_test.go
@@ -10,7 +10,9 @@ import (
 	"io"
 	"reflect"
 	"testing"
+	"time"
 
+	"github.com/stretchr/testify/assert"
 	"golang.org/x/crypto/ssh"
 )
 
@@ -71,6 +73,7 @@ func mustGeneratePublicKey(t *testing.T) ssh.PublicKey {
 }
 
 func TestNewCertificate(t *testing.T) {
+	now := time.Now().Truncate(time.Second)
 	key := mustGeneratePublicKey(t)
 	cr := CertificateRequest{
 		Key: key,
@@ -100,8 +103,8 @@ func TestNewCertificate(t *testing.T) {
 			Type:            UserCert,
 			KeyID:           "jane@doe.com",
 			Principals:      []string{"jane"},
-			ValidAfter:      0,
-			ValidBefore:     0,
+			ValidAfter:      time.Time{},
+			ValidBefore:     time.Time{},
 			CriticalOptions: nil,
 			Extensions: map[string]string{
 				"permit-X11-forwarding":   "",
@@ -121,8 +124,8 @@ func TestNewCertificate(t *testing.T) {
 			Type:            HostCert,
 			KeyID:           "foobar",
 			Principals:      []string{"foo.internal", "bar.internal"},
-			ValidAfter:      0,
-			ValidBefore:     0,
+			ValidAfter:      time.Time{},
+			ValidBefore:     time.Time{},
 			CriticalOptions: nil,
 			Extensions:      nil,
 			Reserved:        nil,
@@ -136,8 +139,8 @@ func TestNewCertificate(t *testing.T) {
 			Type:            HostCert,
 			KeyID:           `foobar", "criticalOptions": {"foo": "bar"},"foo":"`,
 			Principals:      []string{"foo.internal", "bar.internal"},
-			ValidAfter:      0,
-			ValidBefore:     0,
+			ValidAfter:      time.Time{},
+			ValidBefore:     time.Time{},
 			CriticalOptions: nil,
 			Extensions:      nil,
 			Reserved:        nil,
@@ -159,8 +162,8 @@ func TestNewCertificate(t *testing.T) {
 			Type:            UserCert,
 			KeyID:           "john@doe.com",
 			Principals:      []string{"john", "john@doe.com"},
-			ValidAfter:      0,
-			ValidBefore:     0,
+			ValidAfter:      time.Time{},
+			ValidBefore:     time.Time{},
 			CriticalOptions: nil,
 			Extensions: map[string]string{
 				"login@github.com":        "john",
@@ -174,6 +177,38 @@ func TestNewCertificate(t *testing.T) {
 			SignatureKey: nil,
 			Signature:    nil,
 		}, false},
+		{"file with dates", args{cr, []Option{WithTemplateFile("./testdata/date.tpl", TemplateData{
+			TypeKey:       UserCert,
+			KeyIDKey:      "john@doe.com",
+			PrincipalsKey: []string{"john", "john@doe.com"},
+			ExtensionsKey: DefaultExtensions(UserCert),
+			InsecureKey: TemplateData{
+				"User": map[string]interface{}{"username": "john"},
+			},
+			WebhooksKey: TemplateData{
+				"Test": map[string]interface{}{"validity": "16h"},
+			},
+		})}}, &Certificate{
+			Nonce:           nil,
+			Key:             key,
+			Serial:          0,
+			Type:            UserCert,
+			KeyID:           "john@doe.com",
+			Principals:      []string{"john", "john@doe.com"},
+			ValidAfter:      now,
+			ValidBefore:     now.Add(16 * time.Hour),
+			CriticalOptions: nil,
+			Extensions: map[string]string{
+				"permit-X11-forwarding":   "",
+				"permit-agent-forwarding": "",
+				"permit-port-forwarding":  "",
+				"permit-pty":              "",
+				"permit-user-rc":          "",
+			},
+			Reserved:     nil,
+			SignatureKey: nil,
+			Signature:    nil,
+		}, false},
 		{"base64", args{cr, []Option{WithTemplateBase64(base64.StdEncoding.EncodeToString([]byte(DefaultTemplate)), CreateTemplateData(HostCert, "foo.internal", nil))}}, &Certificate{
 			Nonce:           nil,
 			Key:             key,
@@ -181,8 +216,8 @@ func TestNewCertificate(t *testing.T) {
 			Type:            HostCert,
 			KeyID:           "foo.internal",
 			Principals:      nil,
-			ValidAfter:      0,
-			ValidBefore:     0,
+			ValidAfter:      time.Time{},
+			ValidBefore:     time.Time{},
 			CriticalOptions: nil,
 			Extensions:      nil,
 			Reserved:        nil,
@@ -203,6 +238,15 @@ func TestNewCertificate(t *testing.T) {
 				t.Errorf("NewCertificate() error = %v, wantErr %v", err, tt.wantErr)
 				return
 			}
+			if got != nil && tt.want != nil {
+				if assert.WithinDuration(t, tt.want.ValidAfter, got.ValidAfter, 2*time.Second) {
+					tt.want.ValidAfter = got.ValidAfter
+				}
+				if assert.WithinDuration(t, tt.want.ValidBefore, got.ValidBefore, 2*time.Second) {
+					tt.want.ValidBefore = got.ValidBefore
+				}
+
+			}
 			if !reflect.DeepEqual(got, tt.want) {
 				t.Errorf("NewCertificate() = %v, want %v", got, tt.want)
 			}
@@ -212,6 +256,7 @@ func TestNewCertificate(t *testing.T) {
 
 func TestCertificate_GetCertificate(t *testing.T) {
 	key := mustGeneratePublicKey(t)
+	now := time.Now()
 
 	type fields struct {
 		Nonce           []byte
@@ -220,8 +265,8 @@ func TestCertificate_GetCertificate(t *testing.T) {
 		Type            CertType
 		KeyID           string
 		Principals      []string
-		ValidAfter      uint64
-		ValidBefore     uint64
+		ValidAfter      time.Time
+		ValidBefore     time.Time
 		CriticalOptions map[string]string
 		Extensions      map[string]string
 		Reserved        []byte
@@ -240,8 +285,8 @@ func TestCertificate_GetCertificate(t *testing.T) {
 			Type:            UserCert,
 			KeyID:           "key-id",
 			Principals:      []string{"john"},
-			ValidAfter:      1111,
-			ValidBefore:     2222,
+			ValidAfter:      now,
+			ValidBefore:     now.Add(time.Hour),
 			CriticalOptions: map[string]string{"foo": "bar"},
 			Extensions:      map[string]string{"login@github.com": "john"},
 			Reserved:        []byte("reserved"),
@@ -254,8 +299,8 @@ func TestCertificate_GetCertificate(t *testing.T) {
 			CertType:        ssh.UserCert,
 			KeyId:           "key-id",
 			ValidPrincipals: []string{"john"},
-			ValidAfter:      1111,
-			ValidBefore:     2222,
+			ValidAfter:      uint64(now.Unix()),
+			ValidBefore:     uint64(now.Add(time.Hour).Unix()),
 			Permissions: ssh.Permissions{
 				CriticalOptions: map[string]string{"foo": "bar"},
 				Extensions:      map[string]string{"login@github.com": "john"},
@@ -269,8 +314,8 @@ func TestCertificate_GetCertificate(t *testing.T) {
 			Type:            HostCert,
 			KeyID:           "key-id",
 			Principals:      []string{"foo.internal", "bar.internal"},
-			ValidAfter:      1111,
-			ValidBefore:     2222,
+			ValidAfter:      time.Time{},
+			ValidBefore:     time.Time{},
 			CriticalOptions: map[string]string{"foo": "bar"},
 			Extensions:      nil,
 			Reserved:        []byte("reserved"),
@@ -283,8 +328,8 @@ func TestCertificate_GetCertificate(t *testing.T) {
 			CertType:        ssh.HostCert,
 			KeyId:           "key-id",
 			ValidPrincipals: []string{"foo.internal", "bar.internal"},
-			ValidAfter:      1111,
-			ValidBefore:     2222,
+			ValidAfter:      0,
+			ValidBefore:     0,
 			Permissions: ssh.Permissions{
 				CriticalOptions: map[string]string{"foo": "bar"},
 				Extensions:      nil,
diff --git a/sshutil/testdata/date.tpl b/sshutil/testdata/date.tpl
new file mode 100644
index 0000000..5070d7b
--- /dev/null
+++ b/sshutil/testdata/date.tpl
@@ -0,0 +1,8 @@
+{
+	"type": "{{ .Type }}",
+	"keyId": "{{ .KeyID }}",
+	"principals": {{ toJson .Principals }},
+	"extensions": {{ toJson .Extensions }},
+	"validAfter": {{ now | toJson }},
+	"validBefore": {{ now | dateModify .Webhooks.Test.validity | toJson }}
+}
\ No newline at end of file
diff --git a/x509util/certificate_test.go b/x509util/certificate_test.go
index 9e111eb..5fc6b83 100644
--- a/x509util/certificate_test.go
+++ b/x509util/certificate_test.go
@@ -112,6 +112,7 @@ func (b *badSigner) Sign(_ io.Reader, digest []byte, opts crypto.SignerOpts) ([]
 }
 
 func TestNewCertificate(t *testing.T) {
+	now := time.Now().UTC().Truncate(time.Second)
 	cr, priv := createCertificateRequest(t, "commonName", []string{"foo.com", "root@foo.com"})
 	crBadSignateure, _ := createCertificateRequest(t, "fail", []string{"foo.com"})
 	crBadSignateure.PublicKey = priv.Public()
@@ -195,12 +196,20 @@ func TestNewCertificate(t *testing.T) {
 			TokenKey: map[string]interface{}{
 				"iss": "https://iss",
 				"sub": "sub",
+				"nbf": now.Unix(),
+			},
+			WebhooksKey: map[string]interface{}{
+				"Test": map[string]interface{}{
+					"notAfter": now.Add(10 * time.Hour).Format(time.RFC3339),
+				},
 			},
 		})}}, &Certificate{
 			Subject:        Subject{CommonName: "commonName"},
 			SANs:           []SubjectAlternativeName{{Type: DNSType, Value: "foo.com"}},
 			EmailAddresses: []string{"root@foo.com"},
 			URIs:           []*url.URL{{Scheme: "https", Host: "iss", Fragment: "sub"}},
+			NotBefore:      now,
+			NotAfter:       now.Add(10 * time.Hour),
 			KeyUsage:       KeyUsage(x509.KeyUsageDigitalSignature),
 			ExtKeyUsage: ExtKeyUsage([]x509.ExtKeyUsage{
 				x509.ExtKeyUsageServerAuth,
@@ -219,6 +228,8 @@ func TestNewCertificate(t *testing.T) {
 			EmailAddresses:        []string{"jane@doe.com"},
 			URIs:                  []*url.URL{{Scheme: "https", Host: "doe.com"}},
 			SANs:                  []SubjectAlternativeName{{Type: DNSType, Value: "www.doe.com"}},
+			NotBefore:             time.Unix(1234567890, 0).UTC(),
+			NotAfter:              time.Unix(1234654290, 0).UTC(),
 			Extensions:            []Extension{{ID: []int{1, 2, 3, 4}, Critical: true, Value: []byte("extension")}},
 			KeyUsage:              KeyUsage(x509.KeyUsageDigitalSignature),
 			ExtKeyUsage:           ExtKeyUsage([]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}),
@@ -309,6 +320,8 @@ func TestNewCertificateTemplate(t *testing.T) {
 		(dict "type" "userPrincipalName" "value" .Token.upn)
 		(dict "type" "1.2.3.4" "value" (printf "int:%s" .Insecure.User.id))
 	) | toJson }},
+	"notBefore": "{{ .Token.nbf | toTime }}",
+	"notAfter": {{ now | dateModify "24h" | toJson }},
 	{{- if typeIs "*rsa.PublicKey" .Insecure.CR.PublicKey }}
 		"keyUsage": ["keyEncipherment", "digitalSignature"],
 	{{- else }}
@@ -334,11 +347,13 @@ func TestNewCertificateTemplate(t *testing.T) {
 	data.SetToken(map[string]any{
 		"upn": "foo@upn.com",
 		"pi":  "0123456789",
+		"nbf": time.Now().Unix(),
 	})
 
 	iss, issPriv := createIssuerCertificate(t, "issuer")
 	cr, priv := createCertificateRequest(t, "commonName", sans)
 
+	now := time.Now().Truncate(time.Second)
 	cert, err := NewCertificate(cr, WithTemplate(tpl, data))
 	require.NoError(t, err)
 
@@ -354,6 +369,9 @@ func TestNewCertificateTemplate(t *testing.T) {
 		},
 	}, crt.Subject)
 
+	assert.WithinDuration(t, now, crt.NotBefore, 2*time.Second)
+	assert.WithinDuration(t, now.Add(24*time.Hour), crt.NotAfter, 2*time.Second)
+
 	// Create expected SAN extension
 	var rawValues []asn1.RawValue
 	for _, san := range []SubjectAlternativeName{
@@ -415,6 +433,7 @@ func TestNewCertificateTemplate(t *testing.T) {
 }
 
 func TestNewCertificateFromX509(t *testing.T) {
+	now := time.Now().UTC().Truncate(time.Second)
 	priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
 	require.NoError(t, err)
 	template := &x509.Certificate{ // similar template as the certificate request for TestNewCertificate
@@ -500,11 +519,18 @@ func TestNewCertificateFromX509(t *testing.T) {
 				"iss": "https://iss",
 				"sub": "sub",
 			},
+			WebhooksKey: map[string]interface{}{
+				"Test": map[string]interface{}{
+					"notAfter": now.Add(10 * time.Hour).Format(time.RFC3339),
+				},
+			},
 		})}}, &Certificate{
 			Subject:        Subject{CommonName: "commonName"},
 			SANs:           []SubjectAlternativeName{{Type: DNSType, Value: "foo.com"}},
 			EmailAddresses: []string{"root@foo.com"},
 			URIs:           []*url.URL{{Scheme: "https", Host: "iss", Fragment: "sub"}},
+			NotBefore:      now,
+			NotAfter:       now.Add(10 * time.Hour),
 			KeyUsage:       KeyUsage(x509.KeyUsageDigitalSignature),
 			ExtKeyUsage: ExtKeyUsage([]x509.ExtKeyUsage{
 				x509.ExtKeyUsageServerAuth,
@@ -523,6 +549,8 @@ func TestNewCertificateFromX509(t *testing.T) {
 			EmailAddresses:        []string{"jane@doe.com"},
 			URIs:                  []*url.URL{{Scheme: "https", Host: "doe.com"}},
 			SANs:                  []SubjectAlternativeName{{Type: DNSType, Value: "www.doe.com"}},
+			NotBefore:             time.Unix(1234567890, 0).UTC(),
+			NotAfter:              time.Unix(1234654290, 0).UTC(),
 			Extensions:            []Extension{{ID: []int{1, 2, 3, 4}, Critical: true, Value: []byte("extension")}},
 			KeyUsage:              KeyUsage(x509.KeyUsageDigitalSignature),
 			ExtKeyUsage:           ExtKeyUsage([]x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}),
@@ -572,6 +600,7 @@ func TestNewCertificateFromX509(t *testing.T) {
 }
 
 func TestCertificate_GetCertificate(t *testing.T) {
+	now := time.Now()
 	type fields struct {
 		Version               int
 		Subject               Subject
@@ -582,6 +611,8 @@ func TestCertificate_GetCertificate(t *testing.T) {
 		IPAddresses           MultiIP
 		URIs                  MultiURL
 		SANs                  []SubjectAlternativeName
+		NotBefore             time.Time
+		NotAfter              time.Time
 		Extensions            []Extension
 		KeyUsage              KeyUsage
 		ExtKeyUsage           ExtKeyUsage
@@ -618,6 +649,8 @@ func TestCertificate_GetCertificate(t *testing.T) {
 				{Type: EmailType, Value: "admin@foo.com"},
 				{Type: URIType, Value: "mailto:admin@foo.com"},
 			},
+			NotBefore:  now,
+			NotAfter:   time.Time{},
 			Extensions: []Extension{{ID: []int{1, 2, 3, 4}, Critical: true, Value: []byte("custom extension")}},
 			KeyUsage:   KeyUsage(x509.KeyUsageDigitalSignature),
 			ExtKeyUsage: ExtKeyUsage([]x509.ExtKeyUsage{
@@ -641,6 +674,8 @@ func TestCertificate_GetCertificate(t *testing.T) {
 			Subject:         pkix.Name{CommonName: "commonName", Organization: []string{"smallstep"}},
 			Issuer:          pkix.Name{},
 			SerialNumber:    big.NewInt(123),
+			NotBefore:       now,
+			NotAfter:        time.Time{},
 			DNSNames:        []string{"foo.bar", "www.foo.bar"},
 			EmailAddresses:  []string{"root@foo.com", "admin@foo.com"},
 			IPAddresses:     []net.IP{net.ParseIP("::1"), net.ParseIP("127.0.0.1")},
@@ -680,6 +715,8 @@ func TestCertificate_GetCertificate(t *testing.T) {
 				IPAddresses:           tt.fields.IPAddresses,
 				URIs:                  tt.fields.URIs,
 				SANs:                  tt.fields.SANs,
+				NotBefore:             tt.fields.NotBefore,
+				NotAfter:              tt.fields.NotAfter,
 				Extensions:            tt.fields.Extensions,
 				KeyUsage:              tt.fields.KeyUsage,
 				ExtKeyUsage:           tt.fields.ExtKeyUsage,
diff --git a/x509util/options_test.go b/x509util/options_test.go
index 067d132..80a284f 100644
--- a/x509util/options_test.go
+++ b/x509util/options_test.go
@@ -249,6 +249,7 @@ func TestWithTemplateFile(t *testing.T) {
 	rsa2048, _ := createRSACertificateRequest(t, 2048, "foo", []string{"foo.com", "foo@foo.com", "::1", "https://foo.com"})
 	rsa3072, _ := createRSACertificateRequest(t, 3072, "foo", []string{"foo.com", "foo@foo.com", "::1", "https://foo.com"})
 
+	now := time.Now().UTC().Truncate(time.Second)
 	data := TemplateData{
 		SANsKey: []SubjectAlternativeName{
 			{Type: "dns", Value: "foo.com"},
@@ -259,6 +260,12 @@ func TestWithTemplateFile(t *testing.T) {
 		TokenKey: map[string]interface{}{
 			"iss": "https://iss",
 			"sub": "sub",
+			"nbf": now.Unix(),
+		},
+		WebhooksKey: map[string]interface{}{
+			"Test": map[string]interface{}{
+				"notAfter": now.Add(10 * time.Hour).Format(time.RFC3339),
+			},
 		},
 	}
 	type args struct {
@@ -278,6 +285,8 @@ func TestWithTemplateFile(t *testing.T) {
 	"sans": [{"type":"dns","value":"foo.com"},{"type":"email","value":"root@foo.com"},{"type":"ip","value":"127.0.0.1"},{"type":"uri","value":"uri:foo:bar"}],
 	"emailAddresses": ["foo@foo.com"],
 	"uris": "https://iss#sub",
+	"notBefore": "` + now.Format(time.RFC3339) + `",
+	"notAfter": "` + now.Add(10*time.Hour).Format(time.RFC3339) + `",
 	"keyUsage": ["digitalSignature"],
 	"extKeyUsage": ["serverAuth", "clientAuth"]
 }`),
@@ -288,6 +297,8 @@ func TestWithTemplateFile(t *testing.T) {
 	"sans": [{"type":"dns","value":"foo.com"},{"type":"email","value":"root@foo.com"},{"type":"ip","value":"127.0.0.1"},{"type":"uri","value":"uri:foo:bar"}],
 	"emailAddresses": ["foo@foo.com"],
 	"uris": "https://iss#sub",
+	"notBefore": "` + now.Format(time.RFC3339) + `",
+	"notAfter": "` + now.Add(10*time.Hour).Format(time.RFC3339) + `",
 	"keyUsage": ["keyEncipherment", "digitalSignature"],
 	"extKeyUsage": ["serverAuth", "clientAuth"]
 }`),
diff --git a/x509util/testdata/example.tpl b/x509util/testdata/example.tpl
index 286edb5..81c0637 100644
--- a/x509util/testdata/example.tpl
+++ b/x509util/testdata/example.tpl
@@ -7,6 +7,8 @@
 {{- if .Token }}
 	"uris": "{{ .Token.iss }}#{{ .Token.sub }}",
 {{- end }}
+	"notBefore": "{{ dateInZone "2006-01-02T15:04:05Z07:00" .Token.nbf "UTC" }}",
+	"notAfter": "{{ .Webhooks.Test.notAfter }}",
 {{- if typeIs "*rsa.PublicKey" .Insecure.CR.PublicKey }}
 	{{- if lt .Insecure.CR.PublicKey.Size 384 }}
 		{{ fail "Key length must be at least 3072 bits" }}
diff --git a/x509util/testdata/fullsimple.tpl b/x509util/testdata/fullsimple.tpl
index 92b02f7..1a879de 100644
--- a/x509util/testdata/fullsimple.tpl
+++ b/x509util/testdata/fullsimple.tpl
@@ -8,6 +8,8 @@
     "ipAddresses": "127.0.0.1",
     "uris": "https://doe.com",
     "sans": [{"type":"dns", "value":"www.doe.com"}],
+    "notBefore": "2009-02-13T23:31:30Z",
+    "notAfter": "2009-02-14T23:31:30Z",
     "extensions": [{"id":"1.2.3.4","critical":true,"value":"ZXh0ZW5zaW9u"}],
     "keyUsage": ["digitalSignature"],
     "extKeyUsage": ["serverAuth"],