Skip to content

Commit

Permalink
Add support for use templates in validities
Browse files Browse the repository at this point in the history
This commit allows setting the validity bounds on X.509 and SSH
certificates using templates.
  • Loading branch information
maraino committed Jun 28, 2024
1 parent 5a67b5f commit dfd1fab
Show file tree
Hide file tree
Showing 8 changed files with 129 additions and 24 deletions.
9 changes: 5 additions & 4 deletions sshutil/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"crypto/rand"
"encoding/binary"
"encoding/json"
"time"

"github.com/pkg/errors"
"go.step.sm/crypto/randutil"
Expand All @@ -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"`
Expand Down Expand Up @@ -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: uint64(c.ValidAfter.Unix()),
ValidBefore: uint64(c.ValidBefore.Unix()),
Permissions: ssh.Permissions{
CriticalOptions: c.CriticalOptions,
Extensions: c.Extensions,
Expand Down
86 changes: 66 additions & 20 deletions sshutil/certificate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ import (
"io"
"reflect"
"testing"
"time"

"github.com/stretchr/testify/assert"
"golang.org/x/crypto/ssh"
)

Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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": "",
Expand All @@ -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,
Expand All @@ -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,
Expand All @@ -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",
Expand All @@ -174,15 +177,47 @@ 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,
Serial: 0,
Type: HostCert,
KeyID: "foo.internal",
Principals: nil,
ValidAfter: 0,
ValidBefore: 0,
ValidAfter: time.Time{},
ValidBefore: time.Time{},
CriticalOptions: nil,
Extensions: nil,
Reserved: nil,
Expand All @@ -203,6 +238,16 @@ 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, time.Second) {
tt.want.ValidAfter = got.ValidAfter
}
if assert.WithinDuration(t, tt.want.ValidBefore, got.ValidBefore, time.Second) {
tt.want.ValidBefore = got.ValidBefore
}

}
assert.Equal(t, tt.want, got)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewCertificate() = %v, want %v", got, tt.want)
}
Expand All @@ -212,6 +257,7 @@ func TestNewCertificate(t *testing.T) {

func TestCertificate_GetCertificate(t *testing.T) {
key := mustGeneratePublicKey(t)
now := time.Now()

type fields struct {
Nonce []byte
Expand All @@ -220,8 +266,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
Expand All @@ -240,8 +286,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"),
Expand All @@ -254,8 +300,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"},
Expand All @@ -269,8 +315,8 @@ func TestCertificate_GetCertificate(t *testing.T) {
Type: HostCert,
KeyID: "key-id",
Principals: []string{"foo.internal", "bar.internal"},
ValidAfter: 1111,
ValidBefore: 2222,
ValidAfter: now,
ValidBefore: now.Add(time.Hour),
CriticalOptions: map[string]string{"foo": "bar"},
Extensions: nil,
Reserved: []byte("reserved"),
Expand All @@ -283,8 +329,8 @@ func TestCertificate_GetCertificate(t *testing.T) {
CertType: ssh.HostCert,
KeyId: "key-id",
ValidPrincipals: []string{"foo.internal", "bar.internal"},
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: nil,
Expand Down
8 changes: 8 additions & 0 deletions sshutil/testdata/date.tpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"type": "{{ .Type }}",
"keyId": "{{ .KeyID }}",
"principals": {{ toJson .Principals }},
"extensions": {{ toJson .Extensions }},
"validAfter": {{ now | toJson }},
"validBefore": {{ now | dateModify .Webhooks.Test.validity | toJson }}
}
7 changes: 7 additions & 0 deletions x509util/certificate.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"crypto/rand"
"crypto/x509"
"encoding/json"
"time"

"github.com/pkg/errors"
)
Expand All @@ -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"`
Expand Down Expand Up @@ -165,6 +168,10 @@ func (c *Certificate) GetCertificate() *x509.Certificate {
e.Set(cert)
}

// Validity bpunds.
cert.NotBefore = c.NotBefore
cert.NotAfter = c.NotAfter

// Others.
c.SerialNumber.Set(cert)
c.SignatureAlgorithm.Set(cert)
Expand Down
28 changes: 28 additions & 0 deletions x509util/certificate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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,
Expand All @@ -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}),
Expand Down Expand Up @@ -285,6 +296,7 @@ func TestNewCertificate(t *testing.T) {
t.Errorf("NewCertificate() error = %v, wantErr %v", err, tt.wantErr)
return
}
assert.Equal(t, got, tt.want)
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewCertificate() = %v, want %v", got, tt.want)
}
Expand All @@ -309,6 +321,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": {{ now | toJson }},
"notAfter": {{ now | dateModify "24h" | toJson }},
{{- if typeIs "*rsa.PublicKey" .Insecure.CR.PublicKey }}
"keyUsage": ["keyEncipherment", "digitalSignature"],
{{- else }}
Expand Down Expand Up @@ -339,6 +353,7 @@ func TestNewCertificateTemplate(t *testing.T) {
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)

Expand All @@ -354,6 +369,9 @@ func TestNewCertificateTemplate(t *testing.T) {
},
}, crt.Subject)

assert.WithinDuration(t, now, crt.NotBefore, time.Second)
assert.WithinDuration(t, now.Add(24*time.Hour), crt.NotAfter, time.Second)

// Create expected SAN extension
var rawValues []asn1.RawValue
for _, san := range []SubjectAlternativeName{
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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}),
Expand Down
Loading

0 comments on commit dfd1fab

Please sign in to comment.