ingress subject annotations & helper tests

Signed-off-by: ctrought <65360454+ctrought@users.noreply.github.com>
This commit is contained in:
ctrought 2022-04-08 22:20:29 -04:00
parent 89ae7238be
commit d9a8047f9c
8 changed files with 268 additions and 59 deletions

View File

@ -287,7 +287,10 @@ func SecretTemplateMismatchesSecretManagedFields(fieldManager string) Func {
}
}
baseAnnotations := internalcertificates.AnnotationsForCertificateSecret(input.Certificate, x509cert)
baseAnnotations, err := internalcertificates.AnnotationsForCertificateSecret(input.Certificate, x509cert)
if err != nil {
return InvalidCertificate, fmt.Sprintf("Failed getting secret annotations: %v", err), true
}
managedLabels, managedAnnotations := sets.NewString(), sets.NewString()

View File

@ -19,7 +19,9 @@ package certificates
import (
"bytes"
"crypto/x509"
"encoding/csv"
"encoding/pem"
"fmt"
"strings"
apiutil "github.com/cert-manager/cert-manager/pkg/api/util"
@ -32,7 +34,7 @@ import (
// information about the Issuer and Certificate.
// If the X.509 certificate is not-nil, additional annotations will be added
// relating to its Common Name and Subject Alternative Names.
func AnnotationsForCertificateSecret(crt *cmapi.Certificate, certificate *x509.Certificate) map[string]string {
func AnnotationsForCertificateSecret(crt *cmapi.Certificate, certificate *x509.Certificate) (map[string]string, error) {
annotations := make(map[string]string)
annotations[cmapi.CertificateNameKey] = crt.Name
@ -42,13 +44,44 @@ func AnnotationsForCertificateSecret(crt *cmapi.Certificate, certificate *x509.C
// Only add certificate data if certificate is non-nil.
if certificate != nil {
var err error
annotations[cmapi.CommonNameAnnotationKey] = certificate.Subject.CommonName
annotations[cmapi.AltNamesAnnotationKey] = strings.Join(certificate.DNSNames, ",")
annotations[cmapi.IPSANAnnotationKey] = strings.Join(utilpki.IPAddressesToString(certificate.IPAddresses), ",")
annotations[cmapi.URISANAnnotationKey] = strings.Join(utilpki.URLsToString(certificate.URIs), ",")
annotations[cmapi.EmailsAnnotationKey] = strings.Join(certificate.EmailAddresses, ",")
annotations[cmapi.SubjectSerialNumberAnnotationKey] = utilpki.SerialNumberToString(certificate.SerialNumber)
var errList []error
annotations[cmapi.SubjectOrganizationsAnnotationKey], err = joinWithEscapeCSV(certificate.Subject.Organization)
errList = append(errList, err)
annotations[cmapi.SubjectOrganizationalUnitsAnnotationKey], err = joinWithEscapeCSV(certificate.Subject.OrganizationalUnit)
errList = append(errList, err)
annotations[cmapi.SubjectCountriesAnnotationKey], err = joinWithEscapeCSV(certificate.Subject.Country)
errList = append(errList, err)
annotations[cmapi.SubjectProvincesAnnotationKey], err = joinWithEscapeCSV(certificate.Subject.Province)
errList = append(errList, err)
annotations[cmapi.SubjectLocalitiesAnnotationKey], err = joinWithEscapeCSV(certificate.Subject.Locality)
errList = append(errList, err)
annotations[cmapi.SubjectPostalCodesAnnotationKey], err = joinWithEscapeCSV(certificate.Subject.PostalCode)
errList = append(errList, err)
annotations[cmapi.SubjectStreetAddressesAnnotationKey], err = joinWithEscapeCSV(certificate.Subject.StreetAddress)
errList = append(errList, err)
// return first error
for _, v := range errList {
if v != nil {
return nil, err
}
}
}
return annotations
return annotations, nil
}
// OutputFormatDER returns the byte slice of the private key in DER format. To
@ -64,3 +97,21 @@ func OutputFormatDER(privateKey []byte) []byte {
func OutputFormatCombinedPEM(privateKey, certificate []byte) []byte {
return bytes.Join([][]byte{privateKey, certificate}, []byte("\n"))
}
// joinWithEscapeCSV returns the given list as a single line of CSV that
// is escaped with quotes if necessary
func joinWithEscapeCSV(in []string) (string, error) {
b := new(bytes.Buffer)
writer := csv.NewWriter(b)
writer.Write(in)
writer.Flush()
if err := writer.Error(); err != nil {
return "", fmt.Errorf("failed to write %q as CSV: %w", in, err)
}
s := b.String()
// CSV writer adds a trailing new line, we need to clean it up
s = strings.TrimSuffix(s, "\n")
return s, nil
}

View File

@ -49,21 +49,39 @@ func Test_AnnotationsForCertificateSecret(t *testing.T) {
),
certificate: &x509.Certificate{
Subject: pkix.Name{
CommonName: "cert-manager",
CommonName: "cert-manager",
Organization: []string{"Example Organization 1", "Example Organization 2"},
OrganizationalUnit: []string{"Example Organizational Unit 1", "Example Organizational Unit 2"},
Country: []string{"Country 1", "Country 2"},
Province: []string{"Province 1", "Province 2"},
Locality: []string{"City 1", "City 2"},
StreetAddress: []string{"1725 Slough Avenue, Suite 200, Scranton Business Park", "123 Example St"},
PostalCode: []string{"55555", "12345"},
SerialNumber: "12345678",
},
DNSNames: []string{"example.com", "cert-manager.io"},
IPAddresses: []net.IP{{1, 1, 1, 1}, {1, 2, 3, 4}},
URIs: urls,
DNSNames: []string{"example.com", "cert-manager.io"},
IPAddresses: []net.IP{{1, 1, 1, 1}, {1, 2, 3, 4}},
URIs: urls,
EmailAddresses: []string{"test1@example.com", "test2@cert-manager.io"},
},
expAnnotations: map[string]string{
"cert-manager.io/certificate-name": "test-certificate",
"cert-manager.io/issuer-name": "another-test-issuer",
"cert-manager.io/issuer-kind": "GoogleCASIssuer",
"cert-manager.io/issuer-group": "my-group.hello.world",
"cert-manager.io/common-name": "cert-manager",
"cert-manager.io/alt-names": "example.com,cert-manager.io",
"cert-manager.io/ip-sans": "1.1.1.1,1.2.3.4",
"cert-manager.io/uri-sans": "spiffe.io//cert-manager.io/test,spiffe.io//hello.world",
"cert-manager.io/certificate-name": "test-certificate",
"cert-manager.io/issuer-name": "another-test-issuer",
"cert-manager.io/issuer-kind": "GoogleCASIssuer",
"cert-manager.io/issuer-group": "my-group.hello.world",
"cert-manager.io/common-name": "cert-manager",
"cert-manager.io/alt-names": "example.com,cert-manager.io",
"cert-manager.io/ip-sans": "1.1.1.1,1.2.3.4",
"cert-manager.io/uri-sans": "spiffe.io//cert-manager.io/test,spiffe.io//hello.world",
"cert-manager.io/email-sans": "test1@example.com,test2@cert-manager.io",
"cert-manager.io/subject-organizations": "Example Organization 1,Example Organization 2",
"cert-manager.io/subject-organizationalunits": "Example Organizational Unit 1,Example Organizational Unit 2",
"cert-manager.io/subject-countries": "Country 1,Country 2",
"cert-manager.io/subject-provinces": "Province 1,Province 2",
"cert-manager.io/subject-localities": "City 1,City 2",
"cert-manager.io/subject-streetaddresses": "\"1725 Slough Avenue, Suite 200, Scranton Business Park\",123 Example St",
"cert-manager.io/subject-postalcodes": "55555,12345",
"cert-manager.io/subject-serialnumber": "12345678",
},
},
"if pass non-nil certificate with only CommonName, expect all Annotations to be present": {
@ -76,14 +94,23 @@ func Test_AnnotationsForCertificateSecret(t *testing.T) {
},
},
expAnnotations: map[string]string{
"cert-manager.io/certificate-name": "test-certificate",
"cert-manager.io/issuer-name": "another-test-issuer",
"cert-manager.io/issuer-kind": "GoogleCASIssuer",
"cert-manager.io/issuer-group": "my-group.hello.world",
"cert-manager.io/common-name": "cert-manager",
"cert-manager.io/alt-names": "",
"cert-manager.io/ip-sans": "",
"cert-manager.io/uri-sans": "",
"cert-manager.io/certificate-name": "test-certificate",
"cert-manager.io/issuer-name": "another-test-issuer",
"cert-manager.io/issuer-kind": "GoogleCASIssuer",
"cert-manager.io/issuer-group": "my-group.hello.world",
"cert-manager.io/common-name": "cert-manager",
"cert-manager.io/alt-names": "",
"cert-manager.io/ip-sans": "",
"cert-manager.io/uri-sans": "",
"cert-manager.io/email-sans": "",
"cert-manager.io/subject-organizations": "",
"cert-manager.io/subject-organizationalunits": "",
"cert-manager.io/subject-countries": "",
"cert-manager.io/subject-provinces": "",
"cert-manager.io/subject-localities": "",
"cert-manager.io/subject-streetaddresses": "",
"cert-manager.io/subject-postalcodes": "",
"cert-manager.io/subject-serialnumber": "",
},
},
"if pass non-nil certificate with only IP Addresses, expect all Annotations to be present": {
@ -94,14 +121,23 @@ func Test_AnnotationsForCertificateSecret(t *testing.T) {
IPAddresses: []net.IP{{1, 1, 1, 1}, {1, 2, 3, 4}},
},
expAnnotations: map[string]string{
"cert-manager.io/certificate-name": "test-certificate",
"cert-manager.io/issuer-name": "another-test-issuer",
"cert-manager.io/issuer-kind": "GoogleCASIssuer",
"cert-manager.io/issuer-group": "my-group.hello.world",
"cert-manager.io/common-name": "",
"cert-manager.io/alt-names": "",
"cert-manager.io/ip-sans": "1.1.1.1,1.2.3.4",
"cert-manager.io/uri-sans": "",
"cert-manager.io/certificate-name": "test-certificate",
"cert-manager.io/issuer-name": "another-test-issuer",
"cert-manager.io/issuer-kind": "GoogleCASIssuer",
"cert-manager.io/issuer-group": "my-group.hello.world",
"cert-manager.io/common-name": "",
"cert-manager.io/alt-names": "",
"cert-manager.io/ip-sans": "1.1.1.1,1.2.3.4",
"cert-manager.io/uri-sans": "",
"cert-manager.io/email-sans": "",
"cert-manager.io/subject-organizations": "",
"cert-manager.io/subject-organizationalunits": "",
"cert-manager.io/subject-countries": "",
"cert-manager.io/subject-provinces": "",
"cert-manager.io/subject-localities": "",
"cert-manager.io/subject-streetaddresses": "",
"cert-manager.io/subject-postalcodes": "",
"cert-manager.io/subject-serialnumber": "",
},
},
"if pass non-nil certificate with only URI SANs, expect all Annotations to be present": {
@ -112,14 +148,23 @@ func Test_AnnotationsForCertificateSecret(t *testing.T) {
URIs: urls,
},
expAnnotations: map[string]string{
"cert-manager.io/certificate-name": "test-certificate",
"cert-manager.io/issuer-name": "another-test-issuer",
"cert-manager.io/issuer-kind": "GoogleCASIssuer",
"cert-manager.io/issuer-group": "my-group.hello.world",
"cert-manager.io/common-name": "",
"cert-manager.io/alt-names": "",
"cert-manager.io/ip-sans": "",
"cert-manager.io/uri-sans": "spiffe.io//cert-manager.io/test,spiffe.io//hello.world",
"cert-manager.io/certificate-name": "test-certificate",
"cert-manager.io/issuer-name": "another-test-issuer",
"cert-manager.io/issuer-kind": "GoogleCASIssuer",
"cert-manager.io/issuer-group": "my-group.hello.world",
"cert-manager.io/common-name": "",
"cert-manager.io/alt-names": "",
"cert-manager.io/ip-sans": "",
"cert-manager.io/uri-sans": "spiffe.io//cert-manager.io/test,spiffe.io//hello.world",
"cert-manager.io/email-sans": "",
"cert-manager.io/subject-organizations": "",
"cert-manager.io/subject-organizationalunits": "",
"cert-manager.io/subject-countries": "",
"cert-manager.io/subject-provinces": "",
"cert-manager.io/subject-localities": "",
"cert-manager.io/subject-streetaddresses": "",
"cert-manager.io/subject-postalcodes": "",
"cert-manager.io/subject-serialnumber": "",
},
},
"if pass non-nil certificate with only DNS names, expect all Annotations to be present": {
@ -130,14 +175,23 @@ func Test_AnnotationsForCertificateSecret(t *testing.T) {
DNSNames: []string{"example.com", "cert-manager.io"},
},
expAnnotations: map[string]string{
"cert-manager.io/certificate-name": "test-certificate",
"cert-manager.io/issuer-name": "another-test-issuer",
"cert-manager.io/issuer-kind": "GoogleCASIssuer",
"cert-manager.io/issuer-group": "my-group.hello.world",
"cert-manager.io/common-name": "",
"cert-manager.io/alt-names": "example.com,cert-manager.io",
"cert-manager.io/ip-sans": "",
"cert-manager.io/uri-sans": "",
"cert-manager.io/certificate-name": "test-certificate",
"cert-manager.io/issuer-name": "another-test-issuer",
"cert-manager.io/issuer-kind": "GoogleCASIssuer",
"cert-manager.io/issuer-group": "my-group.hello.world",
"cert-manager.io/common-name": "",
"cert-manager.io/alt-names": "example.com,cert-manager.io",
"cert-manager.io/ip-sans": "",
"cert-manager.io/uri-sans": "",
"cert-manager.io/email-sans": "",
"cert-manager.io/subject-organizations": "",
"cert-manager.io/subject-organizationalunits": "",
"cert-manager.io/subject-countries": "",
"cert-manager.io/subject-provinces": "",
"cert-manager.io/subject-localities": "",
"cert-manager.io/subject-streetaddresses": "",
"cert-manager.io/subject-postalcodes": "",
"cert-manager.io/subject-serialnumber": "",
},
},
"if no certificate data, then expect no X.509 related annotations": {
@ -156,8 +210,9 @@ func Test_AnnotationsForCertificateSecret(t *testing.T) {
for name, test := range tests {
t.Run(name, func(t *testing.T) {
gotAnnotations := AnnotationsForCertificateSecret(test.crt, test.certificate)
gotAnnotations, err := AnnotationsForCertificateSecret(test.crt, test.certificate)
assert.Equal(t, test.expAnnotations, gotAnnotations)
assert.Equal(t, nil, err)
})
}
}

View File

@ -36,6 +36,33 @@ const (
// Annotation key for certificate renewBefore.
RenewBeforeAnnotationKey = "cert-manager.io/renew-before"
// Annotation key for emails subjectAltNames.
EmailsAnnotationKey = "cert-manager.io/email-sans"
// Annotation key for subject organization.
SubjectOrganizationsAnnotationKey = "cert-manager.io/subject-organizations"
// Annotation key for subject organizational units.
SubjectOrganizationalUnitsAnnotationKey = "cert-manager.io/subject-organizationalunits"
// Annotation key for subject organizational units.
SubjectCountriesAnnotationKey = "cert-manager.io/subject-countries"
// Annotation key for subject provinces.
SubjectProvincesAnnotationKey = "cert-manager.io/subject-provinces"
// Annotation key for subject localities.
SubjectLocalitiesAnnotationKey = "cert-manager.io/subject-localities"
// Annotation key for subject provinces.
SubjectStreetAddressesAnnotationKey = "cert-manager.io/subject-streetaddresses"
// Annotation key for subject postal codes.
SubjectPostalCodesAnnotationKey = "cert-manager.io/subject-postalcodes"
// Annotation key for subject serial number.
SubjectSerialNumberAnnotationKey = "cert-manager.io/subject-serialnumber"
// Annotation key for certificate key usages.
UsagesAnnotationKey = "cert-manager.io/usages"

View File

@ -75,35 +75,66 @@ func translateAnnotations(crt *cmapi.Certificate, ingLikeAnnotations map[string]
subject := &cmapi.X509Subject{}
if organizations, found := ingLikeAnnotations[cmapi.SubjectOrganizationsAnnotationKey]; found {
subject.Organizations = strings.Split(organizations, ",")
organizations, err := splitWithEscapeCSV(organizations)
subject.Organizations = organizations
if err != nil {
return fmt.Errorf("%w %q: %v", errInvalidIngressAnnotation, cmapi.SubjectOrganizationsAnnotationKey, err)
}
}
if organizationalUnits, found := ingLikeAnnotations[cmapi.SubjectOrganizationalUnitsAnnotationKey]; found {
subject.OrganizationalUnits = strings.Split(organizationalUnits, ",")
organizationalUnits, err := splitWithEscapeCSV(organizationalUnits)
subject.OrganizationalUnits = organizationalUnits
if err != nil {
return fmt.Errorf("%w %q: %v", errInvalidIngressAnnotation, cmapi.SubjectOrganizationsAnnotationKey, err)
}
}
if countries, found := ingLikeAnnotations[cmapi.SubjectCountriesAnnotationKey]; found {
subject.Countries = strings.Split(countries, ",")
countries, err := splitWithEscapeCSV(countries)
subject.Countries = countries
if err != nil {
return fmt.Errorf("%w %q: %v", errInvalidIngressAnnotation, cmapi.SubjectCountriesAnnotationKey, err)
}
}
if provinces, found := ingLikeAnnotations[cmapi.SubjectProvincesAnnotationKey]; found {
subject.Provinces = strings.Split(provinces, ",")
provinces, err := splitWithEscapeCSV(provinces)
subject.Provinces = provinces
if err != nil {
return fmt.Errorf("%w %q: %v", errInvalidIngressAnnotation, cmapi.SubjectProvincesAnnotationKey, err)
}
}
if localities, found := ingLikeAnnotations[cmapi.SubjectLocalitiesAnnotationKey]; found {
subject.Localities = strings.Split(localities, ",")
localities, err := splitWithEscapeCSV(localities)
subject.Localities = localities
if err != nil {
return fmt.Errorf("%w %q: %v", errInvalidIngressAnnotation, cmapi.SubjectLocalitiesAnnotationKey, err)
}
}
if postalCodes, found := ingLikeAnnotations[cmapi.SubjectPostalCodesAnnotationKey]; found {
subject.PostalCodes = strings.Split(postalCodes, ",")
postalCodes, err := splitWithEscapeCSV(postalCodes)
subject.PostalCodes = postalCodes
if err != nil {
return fmt.Errorf("%w %q: %v", errInvalidIngressAnnotation, cmapi.SubjectPostalCodesAnnotationKey, err)
}
}
if streetAddresses, found := ingLikeAnnotations[cmapi.SubjectStreetAddressesAnnotationKey]; found {
addresses, err := splitWithEscapeCSV(streetAddresses)
streetAddresses, err := splitWithEscapeCSV(streetAddresses)
subject.StreetAddresses = streetAddresses
if err != nil {
return fmt.Errorf("%w %q: %v", errInvalidIngressAnnotation, cmapi.SubjectStreetAddressesAnnotationKey, err)
}
subject.StreetAddresses = addresses
}
if serialNumber, found := ingLikeAnnotations[cmapi.SubjectSerialNumberAnnotationKey]; found {

View File

@ -17,7 +17,11 @@ limitations under the License.
package shimhelper
import (
"bytes"
"encoding/csv"
"errors"
"fmt"
"strings"
"testing"
"time"
@ -51,7 +55,7 @@ func Test_translateAnnotations(t *testing.T) {
cmapi.SubjectCountriesAnnotationKey: "Country",
cmapi.SubjectProvincesAnnotationKey: "Province",
cmapi.SubjectLocalitiesAnnotationKey: "City",
cmapi.SubjectStreetAddressesAnnotationKey: "\"1725 Slough Avenue, Suite 200, Scranton Business Park\"",
cmapi.SubjectStreetAddressesAnnotationKey: "\"1725 Slough Avenue, Suite 200, Scranton Business Park\",\"1800 Slough Avenue, Suite 200, Scranton Business Park\"",
cmapi.SubjectPostalCodesAnnotationKey: "ABC123",
cmapi.SubjectSerialNumberAnnotationKey: "123456",
cmapi.DurationAnnotationKey: "168h", // 1 week
@ -70,6 +74,15 @@ func Test_translateAnnotations(t *testing.T) {
a.Equal(&metav1.Duration{Duration: time.Hour * 24}, crt.Spec.RenewBefore)
a.Equal([]cmapi.KeyUsage{cmapi.UsageServerAuth, cmapi.UsageSigning}, crt.Spec.Usages)
a.Equal(pointer.Int32(7), crt.Spec.RevisionHistoryLimit)
a.Equal("123456", crt.Spec.Subject.SerialNumber)
splitAddresses, splitErr := splitWithEscapeCSV("\"1725 Slough Avenue, Suite 200, Scranton Business Park\",\"1800 Slough Avenue, Suite 200, Scranton Business Park\"")
a.Equal(nil, splitErr)
a.Equal(splitAddresses, crt.Spec.Subject.StreetAddresses)
joinedAddresses, joinErr := joinWithEscapeCSV(crt.Spec.Subject.StreetAddresses)
a.Equal(nil, joinErr)
a.Equal("\"1725 Slough Avenue, Suite 200, Scranton Business Park\",\"1800 Slough Avenue, Suite 200, Scranton Business Park\"", joinedAddresses)
},
},
"success rsa private key algorithm": {
@ -137,7 +150,6 @@ func Test_translateAnnotations(t *testing.T) {
a.Equal(cmapi.Ed25519KeyAlgorithm, crt.Spec.PrivateKey.Algorithm)
a.Equal(cmapi.PKCS8, crt.Spec.PrivateKey.Encoding)
a.Equal(cmapi.RotationPolicyAlways, crt.Spec.PrivateKey.RotationPolicy)
a.Equal([]string{"1725 Slough Avenue, Suite 200, Scranton Business Park"}, crt.Spec.Subject.StreetAddresses)
},
},
"nil annotations": {
@ -296,3 +308,21 @@ func assertErrorIs(t *testing.T, err, target error) {
assert.Truef(t, errors.Is(err, target), "unexpected error type. err: %v, target: %v", err, target)
}
}
// joinWithEscapeCSV returns the given list as a single line of CSV that
// is escaped with quotes if necessary
func joinWithEscapeCSV(in []string) (string, error) {
b := new(bytes.Buffer)
writer := csv.NewWriter(b)
writer.Write(in)
writer.Flush()
if err := writer.Error(); err != nil {
return "", fmt.Errorf("failed to write %q as CSV: %w", in, err)
}
s := b.String()
// CSV writer adds a trailing new line, we need to clean it up
s = strings.TrimSuffix(s, "\n")
return s, nil
}

View File

@ -160,7 +160,12 @@ func (s *SecretsManager) setValues(crt *cmapi.Certificate, secret *corev1.Secret
}
}
secret.Annotations = certificates.AnnotationsForCertificateSecret(crt, certificate)
var err error
secret.Annotations, err = certificates.AnnotationsForCertificateSecret(crt, certificate)
if err != nil {
return err
}
if secret.Labels == nil {
secret.Labels = make(map[string]string)
}

View File

@ -110,6 +110,13 @@ func URLsToString(uris []*url.URL) []string {
return uriStrs
}
func SerialNumberToString(sn *big.Int) string {
if sn == nil {
return ""
}
return sn.String()
}
func removeDuplicates(in []string) []string {
var found []string
Outer: