diff --git a/internal/controller/certificates/policies/checks.go b/internal/controller/certificates/policies/checks.go index 52d47d1ab..0236e87ba 100644 --- a/internal/controller/certificates/policies/checks.go +++ b/internal/controller/certificates/policies/checks.go @@ -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() diff --git a/internal/controller/certificates/secrets.go b/internal/controller/certificates/secrets.go index 0a401c2a5..45021c90b 100644 --- a/internal/controller/certificates/secrets.go +++ b/internal/controller/certificates/secrets.go @@ -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 +} diff --git a/internal/controller/certificates/secrets_test.go b/internal/controller/certificates/secrets_test.go index 7ac3d5919..183dc923f 100644 --- a/internal/controller/certificates/secrets_test.go +++ b/internal/controller/certificates/secrets_test.go @@ -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) }) } } diff --git a/pkg/apis/certmanager/v1/types.go b/pkg/apis/certmanager/v1/types.go index a3fa3ae35..f4a8b1661 100644 --- a/pkg/apis/certmanager/v1/types.go +++ b/pkg/apis/certmanager/v1/types.go @@ -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" diff --git a/pkg/controller/certificate-shim/helper.go b/pkg/controller/certificate-shim/helper.go index 86fd26593..797938e99 100644 --- a/pkg/controller/certificate-shim/helper.go +++ b/pkg/controller/certificate-shim/helper.go @@ -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 { diff --git a/pkg/controller/certificate-shim/helper_test.go b/pkg/controller/certificate-shim/helper_test.go index 424217cdb..ee8cba1de 100644 --- a/pkg/controller/certificate-shim/helper_test.go +++ b/pkg/controller/certificate-shim/helper_test.go @@ -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 +} diff --git a/pkg/controller/certificates/issuing/internal/secret.go b/pkg/controller/certificates/issuing/internal/secret.go index c145dcb07..7c29162b1 100644 --- a/pkg/controller/certificates/issuing/internal/secret.go +++ b/pkg/controller/certificates/issuing/internal/secret.go @@ -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) } diff --git a/pkg/util/pki/csr.go b/pkg/util/pki/csr.go index 2afe88e76..95d9039df 100644 --- a/pkg/util/pki/csr.go +++ b/pkg/util/pki/csr.go @@ -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: