commit
00b101de76
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"`
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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"`
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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{
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user