Merge pull request #2597 from meyskens/emailsans

Add Email SANs
This commit is contained in:
jetstack-bot 2020-03-03 16:31:56 +00:00 committed by GitHub
commit 00b101de76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 186 additions and 18 deletions

View File

@ -96,6 +96,12 @@ spec:
duration:
description: Certificate default Duration
type: string
emailSANs:
description: EmailSANs is a list of Email Subject Alternative Names
to be set on this Certificate.
type: array
items:
type: string
ipAddresses:
description: IPAddresses is a list of IP addresses to be used on the
Certificate
@ -337,6 +343,12 @@ spec:
duration:
description: Certificate default Duration
type: string
emailSANs:
description: EmailSANs is a list of Email Subject Alternative Names
to be set on this Certificate.
type: array
items:
type: string
ipAddresses:
description: IPAddresses is a list of IP addresses to be used on the
Certificate

View File

@ -302,6 +302,12 @@ spec:
duration:
description: Certificate default Duration
type: string
emailSANs:
description: EmailSANs is a list of Email Subject Alternative Names
to be set on this Certificate.
type: array
items:
type: string
ipAddresses:
description: IPAddresses is a list of IP addresses to be used on the
Certificate
@ -543,6 +549,12 @@ spec:
duration:
description: Certificate default Duration
type: string
emailSANs:
description: EmailSANs is a list of Email Subject Alternative Names
to be set on this Certificate.
type: array
items:
type: string
ipAddresses:
description: IPAddresses is a list of IP addresses to be used on the
Certificate

View File

@ -109,6 +109,11 @@ type CertificateSpec struct {
// +optional
URISANs []string `json:"uriSANs,omitempty"`
// EmailSANs is a list of Email Subject Alternative Names to be set on this
// Certificate.
// +optional
EmailSANs []string `json:"emailSANs,omitempty"`
// SecretName is the name of the secret resource to store this secret in
SecretName string `json:"secretName"`

View File

@ -312,6 +312,11 @@ func (in *CertificateSpec) DeepCopyInto(out *CertificateSpec) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.EmailSANs != nil {
in, out := &in.EmailSANs, &out.EmailSANs
*out = make([]string, len(*in))
copy(*out, *in)
}
out.IssuerRef = in.IssuerRef
if in.Usages != nil {
in, out := &in.Usages, &out.Usages

View File

@ -105,6 +105,11 @@ type CertificateSpec struct {
// +optional
URISANs []string `json:"uriSANs,omitempty"`
// EmailSANs is a list of Email Subject Alternative Names to be set on this
// Certificate.
// +optional
EmailSANs []string `json:"emailSANs,omitempty"`
// SecretName is the name of the secret resource to store this secret in
SecretName string `json:"secretName"`

View File

@ -307,6 +307,11 @@ func (in *CertificateSpec) DeepCopyInto(out *CertificateSpec) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.EmailSANs != nil {
in, out := &in.EmailSANs, &out.EmailSANs
*out = make([]string, len(*in))
copy(*out, *in)
}
out.IssuerRef = in.IssuerRef
if in.Usages != nil {
in, out := &in.Usages, &out.Usages

View File

@ -333,6 +333,12 @@ func (c *controller) certificateRequiresIssuance(ctx context.Context, log logr.L
return true
}
// validate the email addressed are correct
if !util.EqualUnsorted(cert.EmailAddresses, crt.Spec.EmailSANs) {
log.Info("certificate email addresses are not as expected, re-issuing")
return true
}
if c.certificateNeedsRenew(ctx, cert, crt) {
log.Info("certificate requires renewal, re-issuing")
return true

View File

@ -86,6 +86,10 @@ type CertificateSpec struct {
// Certificate.
URISANs []string
// EmailSANs is a list of Email Subject Alternative Names to be set on this
// Certificate.
EmailSANs []string
// SecretName is the name of the secret resource to store this secret in
SecretName string

View File

@ -605,6 +605,7 @@ func autoConvert_v1alpha2_CertificateSpec_To_certmanager_CertificateSpec(in *v1a
out.DNSNames = *(*[]string)(unsafe.Pointer(&in.DNSNames))
out.IPAddresses = *(*[]string)(unsafe.Pointer(&in.IPAddresses))
out.URISANs = *(*[]string)(unsafe.Pointer(&in.URISANs))
out.EmailSANs = *(*[]string)(unsafe.Pointer(&in.EmailSANs))
out.SecretName = in.SecretName
// TODO: Inefficient conversion - can we improve it?
if err := s.Convert(&in.IssuerRef, &out.IssuerRef, 0); err != nil {
@ -634,6 +635,7 @@ func autoConvert_certmanager_CertificateSpec_To_v1alpha2_CertificateSpec(in *cer
out.DNSNames = *(*[]string)(unsafe.Pointer(&in.DNSNames))
out.IPAddresses = *(*[]string)(unsafe.Pointer(&in.IPAddresses))
out.URISANs = *(*[]string)(unsafe.Pointer(&in.URISANs))
out.EmailSANs = *(*[]string)(unsafe.Pointer(&in.EmailSANs))
out.SecretName = in.SecretName
// TODO: Inefficient conversion - can we improve it?
if err := s.Convert(&in.IssuerRef, &out.IssuerRef, 0); err != nil {

View File

@ -576,6 +576,7 @@ func autoConvert_v1alpha3_CertificateSpec_To_certmanager_CertificateSpec(in *v1a
out.DNSNames = *(*[]string)(unsafe.Pointer(&in.DNSNames))
out.IPAddresses = *(*[]string)(unsafe.Pointer(&in.IPAddresses))
out.URISANs = *(*[]string)(unsafe.Pointer(&in.URISANs))
out.EmailSANs = *(*[]string)(unsafe.Pointer(&in.EmailSANs))
out.SecretName = in.SecretName
// TODO: Inefficient conversion - can we improve it?
if err := s.Convert(&in.IssuerRef, &out.IssuerRef, 0); err != nil {
@ -602,6 +603,7 @@ func autoConvert_certmanager_CertificateSpec_To_v1alpha3_CertificateSpec(in *cer
out.DNSNames = *(*[]string)(unsafe.Pointer(&in.DNSNames))
out.IPAddresses = *(*[]string)(unsafe.Pointer(&in.IPAddresses))
out.URISANs = *(*[]string)(unsafe.Pointer(&in.URISANs))
out.EmailSANs = *(*[]string)(unsafe.Pointer(&in.EmailSANs))
out.SecretName = in.SecretName
// TODO: Inefficient conversion - can we improve it?
if err := s.Convert(&in.IssuerRef, &out.IssuerRef, 0); err != nil {

View File

@ -19,6 +19,7 @@ package validation
import (
"fmt"
"net"
"net/mail"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
@ -39,9 +40,8 @@ func ValidateCertificateSpec(crt *cmapi.CertificateSpec, fldPath *field.Path) fi
el = append(el, validateIssuerRef(crt.IssuerRef, fldPath)...)
if len(crt.CommonName) == 0 && len(crt.DNSNames) == 0 && len(crt.URISANs) == 0 {
el = append(el, field.Required(fldPath.Child("commonName", "dnsNames", "uriSANs"),
"at least one of commonName, dnsNames, or uriSANs must be set"))
if len(crt.CommonName) == 0 && len(crt.DNSNames) == 0 && len(crt.URISANs) == 0 && len(crt.EmailSANs) == 0 {
el = append(el, field.Invalid(fldPath, "", "at least one of commonName, dnsNames, uriSANs or emailSANs must be set"))
}
// if a common name has been specified, ensure it is no longer than 64 chars
@ -52,6 +52,11 @@ func ValidateCertificateSpec(crt *cmapi.CertificateSpec, fldPath *field.Path) fi
if len(crt.IPAddresses) > 0 {
el = append(el, validateIPAddresses(crt, fldPath)...)
}
if len(crt.EmailSANs) > 0 {
el = append(el, validateEmailAddresses(crt, fldPath)...)
}
switch crt.KeyAlgorithm {
case cmapi.KeyAlgorithm(""):
case cmapi.RSAKeyAlgorithm:
@ -113,6 +118,24 @@ func validateIPAddresses(a *cmapi.CertificateSpec, fldPath *field.Path) field.Er
return el
}
func validateEmailAddresses(a *cmapi.CertificateSpec, fldPath *field.Path) field.ErrorList {
if len(a.EmailSANs) <= 0 {
return nil
}
el := field.ErrorList{}
for i, d := range a.EmailSANs {
e, err := mail.ParseAddress(d)
if err != nil {
el = append(el, field.Invalid(fldPath.Child("emailSANs").Index(i), d, fmt.Sprintf("invalid email address: %s", err)))
} else if e.Address != d {
// Go accepts email names as per RFC 5322 (name <email>)
// This checks if the supplied value only contains the email address and nothing else
el = append(el, field.Invalid(fldPath.Child("emailSANs").Index(i), d, "invalid email address: make sure the supplied value only contains the email address itself"))
}
}
return el
}
func validateUsages(a *cmapi.CertificateSpec, fldPath *field.Path) field.ErrorList {
el := field.ErrorList{}
for i, u := range a.Usages {

View File

@ -125,7 +125,7 @@ func TestValidateCertificate(t *testing.T) {
},
},
errs: []*field.Error{
field.Required(fldPath.Child("commonName", "dnsNames", "uriSANs"), "at least one of commonName, dnsNames, or uriSANs must be set"),
field.Invalid(fldPath, "", "at least one of commonName, dnsNames, uriSANs or emailSANs must be set"),
},
},
"certificate with no issuerRef": {
@ -413,6 +413,51 @@ func TestValidateCertificate(t *testing.T) {
},
},
},
"valid certificate with only email SAN": {
cfg: &cmapi.Certificate{
Spec: cmapi.CertificateSpec{
EmailSANs: []string{"alice@example.com"},
SecretName: "abc",
IssuerRef: validIssuerRef,
},
},
},
"invalid certificate with incorrect email": {
cfg: &cmapi.Certificate{
Spec: cmapi.CertificateSpec{
EmailSANs: []string{"aliceexample.com"},
SecretName: "abc",
IssuerRef: validIssuerRef,
},
},
errs: []*field.Error{
field.Invalid(fldPath.Child("emailSANs").Index(0), "aliceexample.com", "invalid email address: mail: missing '@' or angle-addr"),
},
},
"invalid certificate with email formatted with name": {
cfg: &cmapi.Certificate{
Spec: cmapi.CertificateSpec{
EmailSANs: []string{"Alice <alice@example.com>"},
SecretName: "abc",
IssuerRef: validIssuerRef,
},
},
errs: []*field.Error{
field.Invalid(fldPath.Child("emailSANs").Index(0), "Alice <alice@example.com>", "invalid email address: make sure the supplied value only contains the email address itself"),
},
},
"invalid certificate with email formatted with mailto": {
cfg: &cmapi.Certificate{
Spec: cmapi.CertificateSpec{
EmailSANs: []string{"mailto:alice@example.com"},
SecretName: "abc",
IssuerRef: validIssuerRef,
},
},
errs: []*field.Error{
field.Invalid(fldPath.Child("emailSANs").Index(0), "mailto:alice@example.com", "invalid email address: mail: expected comma"),
},
},
}
for n, s := range scenarios {
t.Run(n, func(t *testing.T) {

View File

@ -307,6 +307,11 @@ func (in *CertificateSpec) DeepCopyInto(out *CertificateSpec) {
*out = make([]string, len(*in))
copy(*out, *in)
}
if in.EmailSANs != nil {
in, out := &in.EmailSANs, &out.EmailSANs
*out = make([]string, len(*in))
copy(*out, *in)
}
out.IssuerRef = in.IssuerRef
if in.Usages != nil {
in, out := &in.Usages, &out.Usages

View File

@ -188,8 +188,8 @@ func GenerateCSR(crt *v1alpha2.Certificate) (*x509.CertificateRequest, error) {
return nil, err
}
if len(commonName) == 0 && len(dnsNames) == 0 && len(uriNames) == 0 {
return nil, fmt.Errorf("no common name, DNS name, or URI SAN specified on certificate")
if len(commonName) == 0 && len(dnsNames) == 0 && len(uriNames) == 0 && len(crt.Spec.EmailSANs) == 0 {
return nil, fmt.Errorf("no common name, DNS name, URI SAN, or Email SAN specified on certificate")
}
pubKeyAlgo, sigAlgo, err := SignatureAlgorithm(crt)
@ -212,9 +212,10 @@ func GenerateCSR(crt *v1alpha2.Certificate) (*x509.CertificateRequest, error) {
SerialNumber: subject.SerialNumber,
CommonName: commonName,
},
DNSNames: dnsNames,
IPAddresses: iPAddresses,
URIs: uriNames,
DNSNames: dnsNames,
IPAddresses: iPAddresses,
URIs: uriNames,
EmailAddresses: crt.Spec.EmailSANs,
// TODO: work out how best to handle extensions/key usages here
ExtraExtensions: []pkix.Extension{},
}, nil
@ -271,10 +272,11 @@ func GenerateTemplate(crt *v1alpha2.Certificate) (*x509.Certificate, error) {
NotBefore: time.Now(),
NotAfter: time.Now().Add(certDuration),
// see http://golang.org/pkg/crypto/x509/#KeyUsage
KeyUsage: keyUsages,
ExtKeyUsage: extKeyUsages,
DNSNames: dnsNames,
IPAddresses: ipAddresses,
KeyUsage: keyUsages,
ExtKeyUsage: extKeyUsages,
DNSNames: dnsNames,
IPAddresses: ipAddresses,
EmailAddresses: crt.Spec.EmailSANs,
}, nil
}
@ -328,11 +330,12 @@ func GenerateTemplateFromCSRPEMWithUsages(csrPEM []byte, duration time.Duration,
NotBefore: time.Now(),
NotAfter: time.Now().Add(duration),
// see http://golang.org/pkg/crypto/x509/#KeyUsage
KeyUsage: keyUsage,
ExtKeyUsage: extKeyUsage,
DNSNames: csr.DNSNames,
IPAddresses: csr.IPAddresses,
URIs: csr.URIs,
KeyUsage: keyUsage,
ExtKeyUsage: extKeyUsage,
DNSNames: csr.DNSNames,
IPAddresses: csr.IPAddresses,
EmailAddresses: csr.EmailAddresses,
URIs: csr.URIs,
}, nil
}

View File

@ -213,6 +213,10 @@ func (h *Helper) ValidateIssuedCertificate(certificate *cmapi.Certificate, rootC
apiutil.ExtKeyUsageStrings(certificateExtKeyUsages), apiutil.ExtKeyUsageStrings(cert.ExtKeyUsage))
}
if !util.EqualUnsorted(cert.EmailAddresses, certificate.Spec.EmailSANs) {
return nil, fmt.Errorf("certificate doesn't contain Email SANs: exp=%v got=%v", certificate.Spec.EmailSANs, cert.EmailAddresses)
}
var dnsName string
if len(expectedDNSNames) > 0 {
dnsName = expectedDNSNames[0]

View File

@ -54,6 +54,7 @@ func runACMEIssuerTests(eab *cmacme.ACMEExternalAccountBinding) {
certificates.URISANsFeature,
certificates.CommonNameFeature,
certificates.KeyUsagesFeature,
certificates.EmailSANsFeature,
)
// unsupportedDNS01Features is a list of features that are not supported by the ACME
@ -64,6 +65,7 @@ func runACMEIssuerTests(eab *cmacme.ACMEExternalAccountBinding) {
certificates.URISANsFeature,
certificates.CommonNameFeature,
certificates.KeyUsagesFeature,
certificates.EmailSANsFeature,
)
provisionerHTTP01 := &acmeIssuerProvisioner{

View File

@ -103,6 +103,10 @@ const (
// that includes a URISANs. ACME providers do not support this.
URISANsFeature Feature = "URISANs"
// EmailSANs denotes whether to the target issuer is able to sign a certificate
// that includes a EmailSANs.
EmailSANsFeature Feature = "EmailSANs"
// CommonName denotes whether the target issuer is able to sign certificates
// with a distinct CommonName. This is useful for issuers such as ACME
// providers that ignore, or otherwise have special requirements for the

View File

@ -242,6 +242,30 @@ func (s *Suite) Define() {
Expect(err).NotTo(HaveOccurred())
})
It("should issue a certificate that defines an Email Address", func() {
s.checkFeatures(EmailSANsFeature)
testCertificate := &cmapi.Certificate{
ObjectMeta: metav1.ObjectMeta{
Name: "testcert",
Namespace: f.Namespace.Name,
},
Spec: cmapi.CertificateSpec{
SecretName: "testcert-tls",
EmailSANs: []string{"alice@example.com"},
IssuerRef: issuerRef,
},
}
By("Creating a Certificate")
err := f.CRClient.Create(ctx, testCertificate)
Expect(err).NotTo(HaveOccurred())
By("Waiting for the Certificate to be issued...")
err = f.Helper().WaitCertificateIssuedValid(f.Namespace.Name, "testcert", time.Minute*5)
Expect(err).NotTo(HaveOccurred())
})
It("should issue a certificate that defines a CommonName and URI SAN", func() {
s.checkFeatures(URISANsFeature)
s.checkFeatures(CommonNameFeature)