From 13b704aeed747845d9fd991af47ea4b38329a062 Mon Sep 17 00:00:00 2001 From: Mariano Cano Date: Mon, 13 Jul 2020 12:47:26 -0700 Subject: [PATCH] Add template support for AWS provisioner. --- authority/provisioner/aws.go | 34 ++++++++++++++++++++++--------- authority/provisioner/options.go | 18 +++++++++++----- x509util/marshal_utils.go | 9 ++++++++ x509util/options.go | 2 ++ x509util/templates.go | 35 ++++++++++++++++++++++++++++++++ 5 files changed, 83 insertions(+), 15 deletions(-) diff --git a/authority/provisioner/aws.go b/authority/provisioner/aws.go index e426564f..a2461630 100644 --- a/authority/provisioner/aws.go +++ b/authority/provisioner/aws.go @@ -17,6 +17,7 @@ import ( "github.com/pkg/errors" "github.com/smallstep/certificates/errs" + "github.com/smallstep/certificates/x509util" "github.com/smallstep/cli/jose" ) @@ -125,13 +126,14 @@ type awsInstanceIdentityDocument struct { // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/instance-identity-documents.html type AWS struct { *base - Type string `json:"type"` - Name string `json:"name"` - Accounts []string `json:"accounts"` - DisableCustomSANs bool `json:"disableCustomSANs"` - DisableTrustOnFirstUse bool `json:"disableTrustOnFirstUse"` - InstanceAge Duration `json:"instanceAge,omitempty"` - Claims *Claims `json:"claims,omitempty"` + Type string `json:"type"` + Name string `json:"name"` + Accounts []string `json:"accounts"` + DisableCustomSANs bool `json:"disableCustomSANs"` + DisableTrustOnFirstUse bool `json:"disableTrustOnFirstUse"` + InstanceAge Duration `json:"instanceAge,omitempty"` + Claims *Claims `json:"claims,omitempty"` + Options *ProvisionerOptions `json:"options,omitempty"` claimer *Claimer config *awsConfig audiences Audiences @@ -276,14 +278,20 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er } doc := payload.document + + // Template options + data := x509util.NewTemplateData() + data.SetCommonName(payload.Claims.Subject) + // Enforce known CN and default DNS and IP if configured. // By default we'll accept the CN and SANs in the CSR. // There's no way to trust them other than TOFU. var so []SignOption if p.DisableCustomSANs { - so = append(so, dnsNamesValidator([]string{ - fmt.Sprintf("ip-%s.%s.compute.internal", strings.Replace(doc.PrivateIP, ".", "-", -1), doc.Region), - })) + dnsName := fmt.Sprintf("ip-%s.%s.compute.internal", strings.Replace(doc.PrivateIP, ".", "-", -1), doc.Region) + data.SetSANs([]string{dnsName, doc.PrivateIP}) + + so = append(so, dnsNamesValidator([]string{dnsName})) so = append(so, ipAddressesValidator([]net.IP{ net.ParseIP(doc.PrivateIP), })) @@ -291,7 +299,13 @@ func (p *AWS) AuthorizeSign(ctx context.Context, token string) ([]SignOption, er so = append(so, urisValidator(nil)) } + templateOptions, err := CustomTemplateOptions(p.Options, data, x509util.DefaultIIDLeafTemplate) + if err != nil { + return nil, errs.Wrap(http.StatusInternalServerError, err, "aws.AuthorizeSign") + } + return append(so, + templateOptions, // modifiers / withOptions newProvisionerExtensionOption(TypeAWS, p.Name, doc.AccountID, "InstanceID", doc.InstanceID), profileDefaultDuration(p.claimer.DefaultTLSCertDuration()), diff --git a/authority/provisioner/options.go b/authority/provisioner/options.go index 1f007f32..300055e1 100644 --- a/authority/provisioner/options.go +++ b/authority/provisioner/options.go @@ -21,14 +21,23 @@ func (fn certificateOptionsFunc) Options(so Options) []x509util.Option { type ProvisionerOptions struct { Template string `json:"template"` - TemplateFile string `json:"templateFile` + TemplateFile string `json:"templateFile"` TemplateData json.RawMessage `json:"templateData"` } -// TemplateOptions generate a CertificateOptions with the template and data +// TemplateOptions generates a CertificateOptions with the template and data // defined in the ProvisionerOptions, the provisioner generated data, and the -// user data provided in the request. +// user data provided in the request. If no template has been provided, +// x509util.DefaultLeafTemplate will be used. func TemplateOptions(o *ProvisionerOptions, data x509util.TemplateData) (CertificateOptions, error) { + return CustomTemplateOptions(o, data, x509util.DefaultLeafTemplate) +} + +// CustomTemplateOptions generates a CertificateOptions with the template, data +// defined in the ProvisionerOptions, the provisioner generated data and the +// user data provided in the request. If no template has been provided in the +// ProvisionerOptions, the given template will be used. +func CustomTemplateOptions(o *ProvisionerOptions, data x509util.TemplateData, defaultTemplate string) (CertificateOptions, error) { if o != nil { if data == nil { data = x509util.NewTemplateData() @@ -40,14 +49,13 @@ func TemplateOptions(o *ProvisionerOptions, data x509util.TemplateData) (Certifi return nil, errors.Wrap(err, "error unmarshaling template data") } } - } return certificateOptionsFunc(func(so Options) []x509util.Option { // We're not provided user data without custom templates. if o == nil || (o.Template == "" && o.TemplateFile == "") { return []x509util.Option{ - x509util.WithTemplate(x509util.DefaultLeafTemplate, data), + x509util.WithTemplate(defaultTemplate, data), } } diff --git a/x509util/marshal_utils.go b/x509util/marshal_utils.go index 7f3bb4af..2195900b 100644 --- a/x509util/marshal_utils.go +++ b/x509util/marshal_utils.go @@ -82,6 +82,15 @@ func (m *MultiIPNet) UnmarshalJSON(data []byte) error { // into a []*url.URL. type MultiURL []*url.URL +// MarshalJSON implements the json.Marshaler interface for MultiURL. +func (m MultiURL) MarshalJSON() ([]byte, error) { + urls := make([]string, len(m)) + for i, u := range m { + urls[i] = u.String() + } + return json.Marshal(urls) +} + // UnmarshalJSON implements the json.Unmarshaler interface for MultiURL. func (m *MultiURL) UnmarshalJSON(data []byte) error { ms, err := unmarshalMultiString(data) diff --git a/x509util/options.go b/x509util/options.go index 9a061bed..cd261f2b 100644 --- a/x509util/options.go +++ b/x509util/options.go @@ -3,6 +3,7 @@ package x509util import ( "bytes" "crypto/x509" + "fmt" "io/ioutil" "text/template" @@ -42,6 +43,7 @@ func WithTemplate(text string, data TemplateData) Option { if err := tmpl.Execute(buf, data); err != nil { return errors.Wrapf(err, "error executing template") } + fmt.Println(buf.String()) o.CertBuffer = buf return nil } diff --git a/x509util/templates.go b/x509util/templates.go index f3e2bcd7..bcbb7059 100644 --- a/x509util/templates.go +++ b/x509util/templates.go @@ -41,6 +41,12 @@ func (t TemplateData) SetSubject(v Subject) { t[SubjectKey] = v } +func (t TemplateData) SetCommonName(cn string) { + s, _ := t[SubjectKey].(Subject) + s.CommonName = cn + t[SubjectKey] = s +} + func (t TemplateData) SetSANs(sans []string) { t[SANsKey] = CreateSANs(sans) } @@ -53,6 +59,9 @@ func (t TemplateData) SetCertificateRequest(cr *x509.CertificateRequest) { t[CertificateRequestKey] = newCertificateRequest(cr) } +// DefaultLeafTemplate is the default templated used to generate a leaf +// certificate. The keyUsage "keyEncipherment" is special and it will be only +// used for RSA keys. const DefaultLeafTemplate = `{ "subject": {{ toJson .Subject }}, "sans": {{ toJson .SANs }}, @@ -60,6 +69,30 @@ const DefaultLeafTemplate = `{ "extKeyUsage": ["serverAuth", "clientAuth"] }` +// DefaultIIDLeafTemplate is the template used by default on instance identity +// provisioners like AWS, GCP or Azure. By default, those provisioners allow the +// SANs provided in the certificate request, but the option `DisableCustomSANs` +// can be provided to force only the verified domains, if the option is true +// `.SANs` will be set with the verified domains. +// +// The keyUsage "keyEncipherment" is special and it will be only used for RSA +// keys. +const DefaultIIDLeafTemplate = `{ + "subject": {{ toJson .Subject }}, + {{- if .SANs }} + "sans": {{ toJson .SANs }}, + {{- else }} + "dnsNames": {{ toJson .CR.DNSNames }}, + "emailAddresses": {{ toJson .CR.EmailAddresses }}, + "ipAddresses": {{ toJson .CR.IPAddresses }}, + "uris": {{ toJson .CR.URIs }}, + {{- end }} + "keyUsage": ["keyEncipherment", "digitalSignature"], + "extKeyUsage": ["serverAuth", "clientAuth"] +}` + +// DefaultIntermediateTemplate is a template that can be used to generate an +// intermediate certificate. const DefaultIntermediateTemplate = `{ "subject": {{ toJson .Subject }}, "keyUsage": ["certSign", "crlSign"], @@ -69,6 +102,8 @@ const DefaultIntermediateTemplate = `{ } }` +// DefaultRootTemplate is a template that can be used to generate a root +// certificate. const DefaultRootTemplate = `{ "subject": {{ toJson .Subject }}, "issuer": {{ toJson .Subject }},