mirror of
				https://github.com/optim-enterprises-bv/kubernetes.git
				synced 2025-10-31 02:08:13 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			1613 lines
		
	
	
		
			61 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			1613 lines
		
	
	
		
			61 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| /*
 | |
| Copyright 2020 The Kubernetes Authors.
 | |
| 
 | |
| Licensed under the Apache License, Version 2.0 (the "License");
 | |
| you may not use this file except in compliance with the License.
 | |
| You may obtain a copy of the License at
 | |
| 
 | |
|     http://www.apache.org/licenses/LICENSE-2.0
 | |
| 
 | |
| Unless required by applicable law or agreed to in writing, software
 | |
| distributed under the License is distributed on an "AS IS" BASIS,
 | |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | |
| See the License for the specific language governing permissions and
 | |
| limitations under the License.
 | |
| */
 | |
| 
 | |
| package validation
 | |
| 
 | |
| import (
 | |
| 	"crypto/ed25519"
 | |
| 	"crypto/rand"
 | |
| 	"crypto/x509"
 | |
| 	"crypto/x509/pkix"
 | |
| 	"encoding/pem"
 | |
| 	"fmt"
 | |
| 	"math/big"
 | |
| 	mathrand "math/rand"
 | |
| 	"reflect"
 | |
| 	"regexp"
 | |
| 	"strings"
 | |
| 	"testing"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/google/go-cmp/cmp"
 | |
| 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 | |
| 	"k8s.io/apimachinery/pkg/util/sets"
 | |
| 	"k8s.io/apimachinery/pkg/util/validation/field"
 | |
| 	"k8s.io/client-go/util/certificate/csr"
 | |
| 	capi "k8s.io/kubernetes/pkg/apis/certificates"
 | |
| 	"k8s.io/kubernetes/pkg/apis/core"
 | |
| 	"k8s.io/utils/pointer"
 | |
| )
 | |
| 
 | |
| var (
 | |
| 	validObjectMeta = metav1.ObjectMeta{Name: "testcsr"}
 | |
| 	validSignerName = "example.com/valid-name"
 | |
| 	validUsages     = []capi.KeyUsage{capi.UsageKeyEncipherment}
 | |
| )
 | |
| 
 | |
| func TestValidateCertificateSigningRequestCreate(t *testing.T) {
 | |
| 	specPath := field.NewPath("spec")
 | |
| 	// maxLengthSignerName is a signerName that is of maximum length, utilising
 | |
| 	// the max length specifications defined in validation.go.
 | |
| 	// It is of the form <fqdn(253)>/<resource-namespace(63)>.<resource-name(253)>
 | |
| 	maxLengthFQDN := fmt.Sprintf("%s.%s.%s.%s", repeatString("a", 63), repeatString("a", 63), repeatString("a", 63), repeatString("a", 61))
 | |
| 	maxLengthSignerName := fmt.Sprintf("%s/%s.%s", maxLengthFQDN, repeatString("a", 63), repeatString("a", 253))
 | |
| 	tests := map[string]struct {
 | |
| 		csr  capi.CertificateSigningRequest
 | |
| 		errs field.ErrorList
 | |
| 	}{
 | |
| 		"CSR with empty request data should fail": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:     validUsages,
 | |
| 					SignerName: validSignerName,
 | |
| 				},
 | |
| 			},
 | |
| 			errs: field.ErrorList{
 | |
| 				field.Invalid(specPath.Child("request"), []byte(nil), "PEM block type must be CERTIFICATE REQUEST"),
 | |
| 			},
 | |
| 		},
 | |
| 		"CSR with invalid request data should fail": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:     validUsages,
 | |
| 					SignerName: validSignerName,
 | |
| 					Request:    []byte("invalid data"),
 | |
| 				},
 | |
| 			},
 | |
| 			errs: field.ErrorList{
 | |
| 				field.Invalid(specPath.Child("request"), []byte("invalid data"), "PEM block type must be CERTIFICATE REQUEST"),
 | |
| 			},
 | |
| 		},
 | |
| 		"CSR with no usages should fail": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					SignerName: validSignerName,
 | |
| 					Request:    newCSRPEM(t),
 | |
| 				},
 | |
| 			},
 | |
| 			errs: field.ErrorList{
 | |
| 				field.Required(specPath.Child("usages"), ""),
 | |
| 			},
 | |
| 		},
 | |
| 		"CSR with no signerName set should fail": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:  validUsages,
 | |
| 					Request: newCSRPEM(t),
 | |
| 				},
 | |
| 			},
 | |
| 			errs: field.ErrorList{
 | |
| 				field.Required(specPath.Child("signerName"), ""),
 | |
| 			},
 | |
| 		},
 | |
| 		"signerName contains no '/'": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:     validUsages,
 | |
| 					Request:    newCSRPEM(t),
 | |
| 					SignerName: "an-invalid-signer-name",
 | |
| 				},
 | |
| 			},
 | |
| 			errs: field.ErrorList{
 | |
| 				field.Invalid(specPath.Child("signerName"), "an-invalid-signer-name", "must be a fully qualified domain and path of the form 'example.com/signer-name'"),
 | |
| 			},
 | |
| 		},
 | |
| 		"signerName contains two '/'": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:     validUsages,
 | |
| 					Request:    newCSRPEM(t),
 | |
| 					SignerName: "an-invalid-signer-name.com/something/else",
 | |
| 				},
 | |
| 			},
 | |
| 			errs: field.ErrorList{
 | |
| 				field.Invalid(specPath.Child("signerName"), "an-invalid-signer-name.com/something/else", "must be a fully qualified domain and path of the form 'example.com/signer-name'"),
 | |
| 			},
 | |
| 		},
 | |
| 		"signerName domain component is not fully qualified": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:     validUsages,
 | |
| 					Request:    newCSRPEM(t),
 | |
| 					SignerName: "example/some-signer-name",
 | |
| 				},
 | |
| 			},
 | |
| 			errs: field.ErrorList{
 | |
| 				field.Invalid(specPath.Child("signerName"), "example", "should be a domain with at least two segments separated by dots"),
 | |
| 			},
 | |
| 		},
 | |
| 		"signerName path component is empty": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:     validUsages,
 | |
| 					Request:    newCSRPEM(t),
 | |
| 					SignerName: "example.com/",
 | |
| 				},
 | |
| 			},
 | |
| 			errs: field.ErrorList{
 | |
| 				field.Invalid(specPath.Child("signerName"), "", `validating label "": a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`),
 | |
| 			},
 | |
| 		},
 | |
| 		"signerName path component ends with a symbol": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:     validUsages,
 | |
| 					Request:    newCSRPEM(t),
 | |
| 					SignerName: "example.com/something-",
 | |
| 				},
 | |
| 			},
 | |
| 			errs: field.ErrorList{
 | |
| 				field.Invalid(specPath.Child("signerName"), "something-", `validating label "something-": a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`),
 | |
| 			},
 | |
| 		},
 | |
| 		"signerName path component is a symbol": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:     validUsages,
 | |
| 					Request:    newCSRPEM(t),
 | |
| 					SignerName: "example.com/-",
 | |
| 				},
 | |
| 			},
 | |
| 			errs: field.ErrorList{
 | |
| 				field.Invalid(specPath.Child("signerName"), "-", `validating label "-": a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`),
 | |
| 			},
 | |
| 		},
 | |
| 		"signerName path component contains no '.' but is valid": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:     validUsages,
 | |
| 					Request:    newCSRPEM(t),
 | |
| 					SignerName: "example.com/some-signer-name",
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		"signerName with a total length greater than 571 characters should be rejected": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:  validUsages,
 | |
| 					Request: newCSRPEM(t),
 | |
| 					// this string is longer than the max signerName limit (635 chars)
 | |
| 					SignerName: maxLengthSignerName + ".toolong",
 | |
| 				},
 | |
| 			},
 | |
| 			errs: field.ErrorList{
 | |
| 				field.TooLong(specPath.Child("signerName"), "" /*unused*/, len(maxLengthSignerName)),
 | |
| 			},
 | |
| 		},
 | |
| 		"signerName with a fqdn greater than 253 characters should be rejected": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:  validUsages,
 | |
| 					Request: newCSRPEM(t),
 | |
| 					// this string is longer than the max signerName limit (635 chars)
 | |
| 					SignerName: fmt.Sprintf("%s.extra/valid-path", maxLengthFQDN),
 | |
| 				},
 | |
| 			},
 | |
| 			errs: field.ErrorList{
 | |
| 				field.TooLong(specPath.Child("signerName"), "" /*unused*/, len(maxLengthFQDN)),
 | |
| 			},
 | |
| 		},
 | |
| 		"signerName can have a longer path if the domain component is less than the max length": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:     validUsages,
 | |
| 					Request:    newCSRPEM(t),
 | |
| 					SignerName: fmt.Sprintf("abc.io/%s.%s", repeatString("a", 253), repeatString("a", 253)),
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		"signerName with a domain label greater than 63 characters will fail": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:     validUsages,
 | |
| 					Request:    newCSRPEM(t),
 | |
| 					SignerName: fmt.Sprintf("%s.example.io/valid-path", repeatString("a", 66)),
 | |
| 				},
 | |
| 			},
 | |
| 			errs: field.ErrorList{
 | |
| 				field.Invalid(specPath.Child("signerName"), fmt.Sprintf("%s.example.io", repeatString("a", 66)), fmt.Sprintf(`validating label "%s": must be no more than 63 characters`, repeatString("a", 66))),
 | |
| 			},
 | |
| 		},
 | |
| 		"signerName of max length in format <fully-qualified-domain-name>/<resource-namespace>.<resource-name> is valid": {
 | |
| 			// ensure signerName is of the form domain.com/something and up to 571 characters.
 | |
| 			// This length and format is specified to accommodate signerNames like:
 | |
| 			// <fqdn>/<resource-namespace>.<resource-name>.
 | |
| 			// The max length of a FQDN is 253 characters (DNS1123Subdomain max length)
 | |
| 			// The max length of a namespace name is 63 characters (DNS1123Label max length)
 | |
| 			// The max length of a resource name is 253 characters (DNS1123Subdomain max length)
 | |
| 			// We then add an additional 2 characters to account for the one '.' and one '/'.
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:     validUsages,
 | |
| 					Request:    newCSRPEM(t),
 | |
| 					SignerName: maxLengthSignerName,
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		"negative duration": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:            validUsages,
 | |
| 					Request:           newCSRPEM(t),
 | |
| 					SignerName:        validSignerName,
 | |
| 					ExpirationSeconds: pointer.Int32(-1),
 | |
| 				},
 | |
| 			},
 | |
| 			errs: field.ErrorList{
 | |
| 				field.Invalid(specPath.Child("expirationSeconds"), int32(-1), "may not specify a duration less than 600 seconds (10 minutes)"),
 | |
| 			},
 | |
| 		},
 | |
| 		"zero duration": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:            validUsages,
 | |
| 					Request:           newCSRPEM(t),
 | |
| 					SignerName:        validSignerName,
 | |
| 					ExpirationSeconds: pointer.Int32(0),
 | |
| 				},
 | |
| 			},
 | |
| 			errs: field.ErrorList{
 | |
| 				field.Invalid(specPath.Child("expirationSeconds"), int32(0), "may not specify a duration less than 600 seconds (10 minutes)"),
 | |
| 			},
 | |
| 		},
 | |
| 		"one duration": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:            validUsages,
 | |
| 					Request:           newCSRPEM(t),
 | |
| 					SignerName:        validSignerName,
 | |
| 					ExpirationSeconds: pointer.Int32(1),
 | |
| 				},
 | |
| 			},
 | |
| 			errs: field.ErrorList{
 | |
| 				field.Invalid(specPath.Child("expirationSeconds"), int32(1), "may not specify a duration less than 600 seconds (10 minutes)"),
 | |
| 			},
 | |
| 		},
 | |
| 		"too short duration": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:            validUsages,
 | |
| 					Request:           newCSRPEM(t),
 | |
| 					SignerName:        validSignerName,
 | |
| 					ExpirationSeconds: csr.DurationToExpirationSeconds(time.Minute),
 | |
| 				},
 | |
| 			},
 | |
| 			errs: field.ErrorList{
 | |
| 				field.Invalid(specPath.Child("expirationSeconds"), *csr.DurationToExpirationSeconds(time.Minute), "may not specify a duration less than 600 seconds (10 minutes)"),
 | |
| 			},
 | |
| 		},
 | |
| 		"valid duration": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:            validUsages,
 | |
| 					Request:           newCSRPEM(t),
 | |
| 					SignerName:        validSignerName,
 | |
| 					ExpirationSeconds: csr.DurationToExpirationSeconds(10 * time.Minute),
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		"missing usages": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:     []capi.KeyUsage{},
 | |
| 					Request:    newCSRPEM(t),
 | |
| 					SignerName: validSignerName,
 | |
| 				},
 | |
| 			},
 | |
| 			errs: field.ErrorList{
 | |
| 				field.Required(specPath.Child("usages"), ""),
 | |
| 			},
 | |
| 		},
 | |
| 		"unknown and duplicate usages": {
 | |
| 			csr: capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: capi.CertificateSigningRequestSpec{
 | |
| 					Usages:     []capi.KeyUsage{"unknown", "unknown"},
 | |
| 					Request:    newCSRPEM(t),
 | |
| 					SignerName: validSignerName,
 | |
| 				},
 | |
| 			},
 | |
| 			errs: field.ErrorList{
 | |
| 				field.NotSupported(specPath.Child("usages").Index(0), capi.KeyUsage("unknown"), allValidUsages.List()),
 | |
| 				field.NotSupported(specPath.Child("usages").Index(1), capi.KeyUsage("unknown"), allValidUsages.List()),
 | |
| 				field.Duplicate(specPath.Child("usages").Index(1), capi.KeyUsage("unknown")),
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 	for name, test := range tests {
 | |
| 		t.Run(name, func(t *testing.T) {
 | |
| 			el := ValidateCertificateSigningRequestCreate(&test.csr)
 | |
| 			if !reflect.DeepEqual(el, test.errs) {
 | |
| 				t.Errorf("returned and expected errors did not match - expected\n%v\nbut got\n%v", test.errs.ToAggregate(), el.ToAggregate())
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func repeatString(s string, num int) string {
 | |
| 	l := make([]string, num)
 | |
| 	for i := 0; i < num; i++ {
 | |
| 		l[i] = s
 | |
| 	}
 | |
| 	return strings.Join(l, "")
 | |
| }
 | |
| 
 | |
| func newCSRPEM(t *testing.T) []byte {
 | |
| 	template := &x509.CertificateRequest{
 | |
| 		Subject: pkix.Name{
 | |
| 			Organization: []string{"testing-org"},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	_, key, err := ed25519.GenerateKey(rand.Reader)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	csrDER, err := x509.CreateCertificateRequest(rand.Reader, template, key)
 | |
| 	if err != nil {
 | |
| 		t.Fatal(err)
 | |
| 	}
 | |
| 
 | |
| 	csrPemBlock := &pem.Block{
 | |
| 		Type:  "CERTIFICATE REQUEST",
 | |
| 		Bytes: csrDER,
 | |
| 	}
 | |
| 
 | |
| 	p := pem.EncodeToMemory(csrPemBlock)
 | |
| 	if p == nil {
 | |
| 		t.Fatal("invalid pem block")
 | |
| 	}
 | |
| 
 | |
| 	return p
 | |
| }
 | |
| 
 | |
| func Test_getValidationOptions(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name   string
 | |
| 		newCSR *capi.CertificateSigningRequest
 | |
| 		oldCSR *capi.CertificateSigningRequest
 | |
| 		want   certificateValidationOptions
 | |
| 	}{{
 | |
| 		name:   "strict create",
 | |
| 		oldCSR: nil,
 | |
| 		want:   certificateValidationOptions{},
 | |
| 	}, {
 | |
| 		name:   "strict update",
 | |
| 		oldCSR: &capi.CertificateSigningRequest{},
 | |
| 		want:   certificateValidationOptions{},
 | |
| 	}, {
 | |
| 		name: "compatible update, approved+denied",
 | |
| 		oldCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved}, {Type: capi.CertificateDenied}},
 | |
| 		}},
 | |
| 		want: certificateValidationOptions{
 | |
| 			allowBothApprovedAndDenied: true,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name:   "compatible update, legacy signerName",
 | |
| 		oldCSR: &capi.CertificateSigningRequest{Spec: capi.CertificateSigningRequestSpec{SignerName: capi.LegacyUnknownSignerName}},
 | |
| 		want: certificateValidationOptions{
 | |
| 			allowLegacySignerName: true,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name: "compatible update, duplicate condition types",
 | |
| 		oldCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved}, {Type: capi.CertificateApproved}},
 | |
| 		}},
 | |
| 		want: certificateValidationOptions{
 | |
| 			allowDuplicateConditionTypes: true,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name: "compatible update, empty condition types",
 | |
| 		oldCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{}},
 | |
| 		}},
 | |
| 		want: certificateValidationOptions{
 | |
| 			allowEmptyConditionType: true,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name: "compatible update, no diff to certificate",
 | |
| 		newCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{
 | |
| 			Certificate: validCertificate,
 | |
| 		}},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{
 | |
| 			Certificate: validCertificate,
 | |
| 		}},
 | |
| 		want: certificateValidationOptions{
 | |
| 			allowArbitraryCertificate: true,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name: "compatible update, existing invalid certificate",
 | |
| 		newCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{
 | |
| 			Certificate: []byte(`new - no PEM blocks`),
 | |
| 		}},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{Status: capi.CertificateSigningRequestStatus{
 | |
| 			Certificate: []byte(`old - no PEM blocks`),
 | |
| 		}},
 | |
| 		want: certificateValidationOptions{
 | |
| 			allowArbitraryCertificate: true,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name:   "compatible update, existing unknown usages",
 | |
| 		oldCSR: &capi.CertificateSigningRequest{Spec: capi.CertificateSigningRequestSpec{Usages: []capi.KeyUsage{"unknown"}}},
 | |
| 		want: certificateValidationOptions{
 | |
| 			allowUnknownUsages: true,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name:   "compatible update, existing duplicate usages",
 | |
| 		oldCSR: &capi.CertificateSigningRequest{Spec: capi.CertificateSigningRequestSpec{Usages: []capi.KeyUsage{"any", "any"}}},
 | |
| 		want: certificateValidationOptions{
 | |
| 			allowDuplicateUsages: true,
 | |
| 		},
 | |
| 	}}
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			if got := getValidationOptions(tt.newCSR, tt.oldCSR); !reflect.DeepEqual(got, tt.want) {
 | |
| 				t.Errorf("got  %#v\nwant %#v", got, tt.want)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestValidateCertificateSigningRequestUpdate(t *testing.T) {
 | |
| 	validUpdateMeta := validObjectMeta
 | |
| 	validUpdateMeta.ResourceVersion = "1"
 | |
| 
 | |
| 	validUpdateMetaWithFinalizers := validUpdateMeta
 | |
| 	validUpdateMetaWithFinalizers.Finalizers = []string{"foo"}
 | |
| 
 | |
| 	validSpec := capi.CertificateSigningRequestSpec{
 | |
| 		Usages:     validUsages,
 | |
| 		Request:    newCSRPEM(t),
 | |
| 		SignerName: "example.com/something",
 | |
| 	}
 | |
| 
 | |
| 	tests := []struct {
 | |
| 		name   string
 | |
| 		newCSR *capi.CertificateSigningRequest
 | |
| 		oldCSR *capi.CertificateSigningRequest
 | |
| 		errs   []string
 | |
| 	}{{
 | |
| 		name:   "no-op",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
 | |
| 	}, {
 | |
| 		name:   "finalizer change with invalid status",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{Certificate: invalidCertificateNoPEM}},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{Certificate: invalidCertificateNoPEM}},
 | |
| 	}, {
 | |
| 		name: "add Approved condition",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
 | |
| 		}},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
 | |
| 		errs: []string{
 | |
| 			`status.conditions: Forbidden: updates may not add a condition of type "Approved"`,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name:   "remove Approved condition",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
 | |
| 		}},
 | |
| 		errs: []string{
 | |
| 			`status.conditions: Forbidden: updates may not remove a condition of type "Approved"`,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name: "add Denied condition",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}},
 | |
| 		}},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
 | |
| 		errs: []string{
 | |
| 			`status.conditions: Forbidden: updates may not add a condition of type "Denied"`,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name:   "remove Denied condition",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}},
 | |
| 		}},
 | |
| 		errs: []string{
 | |
| 			`status.conditions: Forbidden: updates may not remove a condition of type "Denied"`,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name: "add Failed condition",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}},
 | |
| 		}},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
 | |
| 		errs:   []string{},
 | |
| 	}, {
 | |
| 		name:   "remove Failed condition",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}},
 | |
| 		}},
 | |
| 		errs: []string{
 | |
| 			`status.conditions: Forbidden: updates may not remove a condition of type "Failed"`,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name: "set certificate",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Certificate: validCertificate,
 | |
| 		}},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
 | |
| 		errs: []string{
 | |
| 			`status.certificate: Forbidden: updates may not set certificate content`,
 | |
| 		},
 | |
| 	}}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			gotErrs := sets.NewString()
 | |
| 			for _, err := range ValidateCertificateSigningRequestUpdate(tt.newCSR, tt.oldCSR) {
 | |
| 				gotErrs.Insert(err.Error())
 | |
| 			}
 | |
| 			wantErrs := sets.NewString(tt.errs...)
 | |
| 			for _, missing := range wantErrs.Difference(gotErrs).List() {
 | |
| 				t.Errorf("missing expected error: %s", missing)
 | |
| 			}
 | |
| 			for _, unexpected := range gotErrs.Difference(wantErrs).List() {
 | |
| 				t.Errorf("unexpected error: %s", unexpected)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestValidateCertificateSigningRequestStatusUpdate(t *testing.T) {
 | |
| 	validUpdateMeta := validObjectMeta
 | |
| 	validUpdateMeta.ResourceVersion = "1"
 | |
| 
 | |
| 	validUpdateMetaWithFinalizers := validUpdateMeta
 | |
| 	validUpdateMetaWithFinalizers.Finalizers = []string{"foo"}
 | |
| 
 | |
| 	validSpec := capi.CertificateSigningRequestSpec{
 | |
| 		Usages:     validUsages,
 | |
| 		Request:    newCSRPEM(t),
 | |
| 		SignerName: "example.com/something",
 | |
| 	}
 | |
| 
 | |
| 	tests := []struct {
 | |
| 		name   string
 | |
| 		newCSR *capi.CertificateSigningRequest
 | |
| 		oldCSR *capi.CertificateSigningRequest
 | |
| 		errs   []string
 | |
| 	}{{
 | |
| 		name:   "no-op",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
 | |
| 	}, {
 | |
| 		name:   "finalizer change with invalid status",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{Certificate: invalidCertificateNoPEM}},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{Certificate: invalidCertificateNoPEM}},
 | |
| 	}, {
 | |
| 		name: "finalizer change with duplicate and unknown usages",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: capi.CertificateSigningRequestSpec{
 | |
| 			Usages:     []capi.KeyUsage{"unknown", "unknown"},
 | |
| 			Request:    newCSRPEM(t),
 | |
| 			SignerName: validSignerName,
 | |
| 		}},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: capi.CertificateSigningRequestSpec{
 | |
| 			Usages:     []capi.KeyUsage{"unknown", "unknown"},
 | |
| 			Request:    newCSRPEM(t),
 | |
| 			SignerName: validSignerName,
 | |
| 		}},
 | |
| 	}, {
 | |
| 		name: "add Approved condition",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
 | |
| 		}},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
 | |
| 		errs: []string{
 | |
| 			`status.conditions: Forbidden: updates may not add a condition of type "Approved"`,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name:   "remove Approved condition",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
 | |
| 		}},
 | |
| 		errs: []string{
 | |
| 			`status.conditions: Forbidden: updates may not remove a condition of type "Approved"`,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name: "add Denied condition",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}},
 | |
| 		}},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
 | |
| 		errs: []string{
 | |
| 			`status.conditions: Forbidden: updates may not add a condition of type "Denied"`,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name:   "remove Denied condition",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}},
 | |
| 		}},
 | |
| 		errs: []string{
 | |
| 			`status.conditions: Forbidden: updates may not remove a condition of type "Denied"`,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name: "add Failed condition",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}},
 | |
| 		}},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
 | |
| 		errs:   []string{},
 | |
| 	}, {
 | |
| 		name:   "remove Failed condition",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}},
 | |
| 		}},
 | |
| 		errs: []string{
 | |
| 			`status.conditions: Forbidden: updates may not remove a condition of type "Failed"`,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name: "set valid certificate",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Certificate: validCertificate,
 | |
| 		}},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
 | |
| 		errs:   []string{},
 | |
| 	}, {
 | |
| 		name: "set invalid certificate",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Certificate: invalidCertificateNoPEM,
 | |
| 		}},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
 | |
| 		errs: []string{
 | |
| 			`status.certificate: Invalid value: "<certificate data>": must contain at least one CERTIFICATE PEM block`,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name: "reset certificate",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Certificate: invalidCertificateNonCertificatePEM,
 | |
| 		}},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Certificate: invalidCertificateNoPEM,
 | |
| 		}},
 | |
| 		errs: []string{
 | |
| 			`status.certificate: Forbidden: updates may not modify existing certificate content`,
 | |
| 		},
 | |
| 	}}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			gotErrs := sets.NewString()
 | |
| 			for _, err := range ValidateCertificateSigningRequestStatusUpdate(tt.newCSR, tt.oldCSR) {
 | |
| 				gotErrs.Insert(err.Error())
 | |
| 			}
 | |
| 			wantErrs := sets.NewString(tt.errs...)
 | |
| 			for _, missing := range wantErrs.Difference(gotErrs).List() {
 | |
| 				t.Errorf("missing expected error: %s", missing)
 | |
| 			}
 | |
| 			for _, unexpected := range gotErrs.Difference(wantErrs).List() {
 | |
| 				t.Errorf("unexpected error: %s", unexpected)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestValidateCertificateSigningRequestApprovalUpdate(t *testing.T) {
 | |
| 	validUpdateMeta := validObjectMeta
 | |
| 	validUpdateMeta.ResourceVersion = "1"
 | |
| 
 | |
| 	validUpdateMetaWithFinalizers := validUpdateMeta
 | |
| 	validUpdateMetaWithFinalizers.Finalizers = []string{"foo"}
 | |
| 
 | |
| 	validSpec := capi.CertificateSigningRequestSpec{
 | |
| 		Usages:     validUsages,
 | |
| 		Request:    newCSRPEM(t),
 | |
| 		SignerName: "example.com/something",
 | |
| 	}
 | |
| 
 | |
| 	tests := []struct {
 | |
| 		name   string
 | |
| 		newCSR *capi.CertificateSigningRequest
 | |
| 		oldCSR *capi.CertificateSigningRequest
 | |
| 		errs   []string
 | |
| 	}{{
 | |
| 		name:   "no-op",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
 | |
| 	}, {
 | |
| 		name:   "finalizer change with invalid certificate",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{Certificate: invalidCertificateNoPEM}},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{Certificate: invalidCertificateNoPEM}},
 | |
| 	}, {
 | |
| 		name: "add Approved condition",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
 | |
| 		}},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
 | |
| 	}, {
 | |
| 		name:   "remove Approved condition",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
 | |
| 		}},
 | |
| 		errs: []string{
 | |
| 			`status.conditions: Forbidden: updates may not remove a condition of type "Approved"`,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name: "add Denied condition",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}},
 | |
| 		}},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
 | |
| 	}, {
 | |
| 		name:   "remove Denied condition",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}},
 | |
| 		}},
 | |
| 		errs: []string{
 | |
| 			`status.conditions: Forbidden: updates may not remove a condition of type "Denied"`,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name: "add Failed condition",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}},
 | |
| 		}},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
 | |
| 		errs:   []string{},
 | |
| 	}, {
 | |
| 		name:   "remove Failed condition",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}},
 | |
| 		}},
 | |
| 		errs: []string{
 | |
| 			`status.conditions: Forbidden: updates may not remove a condition of type "Failed"`,
 | |
| 		},
 | |
| 	}, {
 | |
| 		name: "set certificate",
 | |
| 		newCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMeta, Spec: validSpec, Status: capi.CertificateSigningRequestStatus{
 | |
| 			Certificate: validCertificate,
 | |
| 		}},
 | |
| 		oldCSR: &capi.CertificateSigningRequest{ObjectMeta: validUpdateMetaWithFinalizers, Spec: validSpec},
 | |
| 		errs: []string{
 | |
| 			`status.certificate: Forbidden: updates may not set certificate content`,
 | |
| 		},
 | |
| 	}}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			gotErrs := sets.NewString()
 | |
| 			for _, err := range ValidateCertificateSigningRequestApprovalUpdate(tt.newCSR, tt.oldCSR) {
 | |
| 				gotErrs.Insert(err.Error())
 | |
| 			}
 | |
| 			wantErrs := sets.NewString(tt.errs...)
 | |
| 			for _, missing := range wantErrs.Difference(gotErrs).List() {
 | |
| 				t.Errorf("missing expected error: %s", missing)
 | |
| 			}
 | |
| 			for _, unexpected := range gotErrs.Difference(wantErrs).List() {
 | |
| 				t.Errorf("unexpected error: %s", unexpected)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Test_validateCertificateSigningRequestOptions verifies validation options are effective in tolerating specific aspects of CSRs
 | |
| func Test_validateCertificateSigningRequestOptions(t *testing.T) {
 | |
| 	validSpec := capi.CertificateSigningRequestSpec{
 | |
| 		Usages:     validUsages,
 | |
| 		Request:    newCSRPEM(t),
 | |
| 		SignerName: "example.com/something",
 | |
| 	}
 | |
| 
 | |
| 	tests := []struct {
 | |
| 		// testcase name
 | |
| 		name string
 | |
| 
 | |
| 		// csr being validated
 | |
| 		csr *capi.CertificateSigningRequest
 | |
| 
 | |
| 		// options that allow the csr to pass validation
 | |
| 		lenientOpts certificateValidationOptions
 | |
| 
 | |
| 		// regexes matching expected errors when validating strictly
 | |
| 		strictRegexes []regexp.Regexp
 | |
| 
 | |
| 		// expected errors (after filtering out errors matched by strictRegexes) when validating strictly
 | |
| 		strictErrs []string
 | |
| 	}{
 | |
| 		// valid strict cases
 | |
| 		{
 | |
| 			name: "no status",
 | |
| 			csr:  &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec},
 | |
| 		}, {
 | |
| 			name: "approved condition",
 | |
| 			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
 | |
| 				Status: capi.CertificateSigningRequestStatus{
 | |
| 					Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
 | |
| 				},
 | |
| 			},
 | |
| 		}, {
 | |
| 			name: "denied condition",
 | |
| 			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
 | |
| 				Status: capi.CertificateSigningRequestStatus{
 | |
| 					Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateDenied, Status: core.ConditionTrue}},
 | |
| 				},
 | |
| 			},
 | |
| 		}, {
 | |
| 			name: "failed condition",
 | |
| 			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
 | |
| 				Status: capi.CertificateSigningRequestStatus{
 | |
| 					Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateFailed, Status: core.ConditionTrue}},
 | |
| 				},
 | |
| 			},
 | |
| 		}, {
 | |
| 			name: "approved+issued",
 | |
| 			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
 | |
| 				Status: capi.CertificateSigningRequestStatus{
 | |
| 					Conditions:  []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
 | |
| 					Certificate: validCertificate,
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 
 | |
| 		// legacy signer
 | |
| 		{
 | |
| 			name: "legacy signer",
 | |
| 			csr: &capi.CertificateSigningRequest{
 | |
| 				ObjectMeta: validObjectMeta,
 | |
| 				Spec: func() capi.CertificateSigningRequestSpec {
 | |
| 					specCopy := validSpec
 | |
| 					specCopy.SignerName = capi.LegacyUnknownSignerName
 | |
| 					return specCopy
 | |
| 				}(),
 | |
| 			},
 | |
| 			lenientOpts: certificateValidationOptions{allowLegacySignerName: true},
 | |
| 			strictErrs:  []string{`spec.signerName: Invalid value: "kubernetes.io/legacy-unknown": the legacy signerName is not allowed via this API version`},
 | |
| 		},
 | |
| 
 | |
| 		// invalid condition cases
 | |
| 		{
 | |
| 			name: "empty condition type",
 | |
| 			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
 | |
| 				Status: capi.CertificateSigningRequestStatus{
 | |
| 					Conditions: []capi.CertificateSigningRequestCondition{{Status: core.ConditionTrue}},
 | |
| 				},
 | |
| 			},
 | |
| 			lenientOpts: certificateValidationOptions{allowEmptyConditionType: true},
 | |
| 			strictErrs:  []string{`status.conditions[0].type: Required value`},
 | |
| 		}, {
 | |
| 			name: "approved and denied",
 | |
| 			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
 | |
| 				Status: capi.CertificateSigningRequestStatus{
 | |
| 					Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}, {Type: capi.CertificateDenied, Status: core.ConditionTrue}},
 | |
| 				},
 | |
| 			},
 | |
| 			lenientOpts: certificateValidationOptions{allowBothApprovedAndDenied: true},
 | |
| 			strictErrs:  []string{`status.conditions[1].type: Invalid value: "Denied": Approved and Denied conditions are mutually exclusive`},
 | |
| 		}, {
 | |
| 			name: "duplicate condition",
 | |
| 			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
 | |
| 				Status: capi.CertificateSigningRequestStatus{
 | |
| 					Conditions: []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}, {Type: capi.CertificateApproved, Status: core.ConditionTrue}},
 | |
| 				},
 | |
| 			},
 | |
| 			lenientOpts: certificateValidationOptions{allowDuplicateConditionTypes: true},
 | |
| 			strictErrs:  []string{`status.conditions[1].type: Duplicate value: "Approved"`},
 | |
| 		},
 | |
| 
 | |
| 		// invalid allowArbitraryCertificate cases
 | |
| 		{
 | |
| 			name: "status.certificate, no PEM",
 | |
| 			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
 | |
| 				Status: capi.CertificateSigningRequestStatus{
 | |
| 					Conditions:  []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
 | |
| 					Certificate: invalidCertificateNoPEM,
 | |
| 				},
 | |
| 			},
 | |
| 			lenientOpts: certificateValidationOptions{allowArbitraryCertificate: true},
 | |
| 			strictErrs:  []string{`status.certificate: Invalid value: "<certificate data>": must contain at least one CERTIFICATE PEM block`},
 | |
| 		}, {
 | |
| 			name: "status.certificate, non-CERTIFICATE PEM",
 | |
| 			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
 | |
| 				Status: capi.CertificateSigningRequestStatus{
 | |
| 					Conditions:  []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
 | |
| 					Certificate: invalidCertificateNonCertificatePEM,
 | |
| 				},
 | |
| 			},
 | |
| 			lenientOpts: certificateValidationOptions{allowArbitraryCertificate: true},
 | |
| 			strictErrs:  []string{`status.certificate: Invalid value: "<certificate data>": only CERTIFICATE PEM blocks are allowed, found "CERTIFICATE1"`},
 | |
| 		}, {
 | |
| 			name: "status.certificate, PEM headers",
 | |
| 			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
 | |
| 				Status: capi.CertificateSigningRequestStatus{
 | |
| 					Conditions:  []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
 | |
| 					Certificate: invalidCertificatePEMHeaders,
 | |
| 				},
 | |
| 			},
 | |
| 			lenientOpts: certificateValidationOptions{allowArbitraryCertificate: true},
 | |
| 			strictErrs:  []string{`status.certificate: Invalid value: "<certificate data>": no PEM block headers are permitted`},
 | |
| 		}, {
 | |
| 			name: "status.certificate, non-base64 PEM",
 | |
| 			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
 | |
| 				Status: capi.CertificateSigningRequestStatus{
 | |
| 					Conditions:  []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
 | |
| 					Certificate: invalidCertificateNonBase64PEM,
 | |
| 				},
 | |
| 			},
 | |
| 			lenientOpts: certificateValidationOptions{allowArbitraryCertificate: true},
 | |
| 			strictErrs:  []string{`status.certificate: Invalid value: "<certificate data>": must contain at least one CERTIFICATE PEM block`},
 | |
| 		}, {
 | |
| 			name: "status.certificate, empty PEM block",
 | |
| 			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
 | |
| 				Status: capi.CertificateSigningRequestStatus{
 | |
| 					Conditions:  []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
 | |
| 					Certificate: invalidCertificateEmptyPEM,
 | |
| 				},
 | |
| 			},
 | |
| 			lenientOpts: certificateValidationOptions{allowArbitraryCertificate: true},
 | |
| 			strictErrs:  []string{`status.certificate: Invalid value: "<certificate data>": found CERTIFICATE PEM block containing 0 certificates`},
 | |
| 		}, {
 | |
| 			name: "status.certificate, non-ASN1 data",
 | |
| 			csr: &capi.CertificateSigningRequest{ObjectMeta: validObjectMeta, Spec: validSpec,
 | |
| 				Status: capi.CertificateSigningRequestStatus{
 | |
| 					Conditions:  []capi.CertificateSigningRequestCondition{{Type: capi.CertificateApproved, Status: core.ConditionTrue}},
 | |
| 					Certificate: invalidCertificateNonASN1Data,
 | |
| 				},
 | |
| 			},
 | |
| 			lenientOpts:   certificateValidationOptions{allowArbitraryCertificate: true},
 | |
| 			strictRegexes: []regexp.Regexp{*regexp.MustCompile(`status.certificate: Invalid value: "\<certificate data\>": (asn1: structure error: sequence tag mismatch|x509: invalid RDNSequence)`)},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			// make sure the lenient options validate with no errors
 | |
| 			for _, err := range validateCertificateSigningRequest(tt.csr, tt.lenientOpts) {
 | |
| 				t.Errorf("unexpected error with lenient options: %s", err.Error())
 | |
| 			}
 | |
| 
 | |
| 			// make sure the strict options produce the expected errors
 | |
| 			gotErrs := sets.NewString()
 | |
| 			for _, err := range validateCertificateSigningRequest(tt.csr, certificateValidationOptions{}) {
 | |
| 				gotErrs.Insert(err.Error())
 | |
| 			}
 | |
| 
 | |
| 			// filter errors matching strictRegexes and ensure every strictRegex matches at least one error
 | |
| 			for _, expectedRegex := range tt.strictRegexes {
 | |
| 				matched := false
 | |
| 				for _, err := range gotErrs.List() {
 | |
| 					if expectedRegex.MatchString(err) {
 | |
| 						gotErrs.Delete(err)
 | |
| 						matched = true
 | |
| 					}
 | |
| 				}
 | |
| 				if !matched {
 | |
| 					t.Errorf("missing expected error matching: %s", expectedRegex.String())
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			wantErrs := sets.NewString(tt.strictErrs...)
 | |
| 			for _, missing := range wantErrs.Difference(gotErrs).List() {
 | |
| 				t.Errorf("missing expected strict error: %s", missing)
 | |
| 			}
 | |
| 			for _, unexpected := range gotErrs.Difference(wantErrs).List() {
 | |
| 				t.Errorf("unexpected errors: %s", unexpected)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func mustMakeCertificate(t *testing.T, template *x509.Certificate) []byte {
 | |
| 	gen := mathrand.New(mathrand.NewSource(12345))
 | |
| 
 | |
| 	pub, priv, err := ed25519.GenerateKey(gen)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("Error while generating key: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	cert, err := x509.CreateCertificate(gen, template, template, pub, priv)
 | |
| 	if err != nil {
 | |
| 		t.Fatalf("Error while making certificate: %v", err)
 | |
| 	}
 | |
| 
 | |
| 	return cert
 | |
| }
 | |
| 
 | |
| func mustMakePEMBlock(blockType string, headers map[string]string, data []byte) string {
 | |
| 	return string(pem.EncodeToMemory(&pem.Block{
 | |
| 		Type:    blockType,
 | |
| 		Headers: headers,
 | |
| 		Bytes:   data,
 | |
| 	}))
 | |
| }
 | |
| 
 | |
| func TestValidateClusterTrustBundle(t *testing.T) {
 | |
| 	goodCert1 := mustMakeCertificate(t, &x509.Certificate{
 | |
| 		SerialNumber: big.NewInt(0),
 | |
| 		Subject: pkix.Name{
 | |
| 			CommonName: "root1",
 | |
| 		},
 | |
| 		IsCA:                  true,
 | |
| 		BasicConstraintsValid: true,
 | |
| 	})
 | |
| 
 | |
| 	goodCert2 := mustMakeCertificate(t, &x509.Certificate{
 | |
| 		SerialNumber: big.NewInt(0),
 | |
| 		Subject: pkix.Name{
 | |
| 			CommonName: "root2",
 | |
| 		},
 | |
| 		IsCA:                  true,
 | |
| 		BasicConstraintsValid: true,
 | |
| 	})
 | |
| 
 | |
| 	badNotCACert := mustMakeCertificate(t, &x509.Certificate{
 | |
| 		SerialNumber: big.NewInt(0),
 | |
| 		Subject: pkix.Name{
 | |
| 			CommonName: "root3",
 | |
| 		},
 | |
| 	})
 | |
| 
 | |
| 	goodCert1Block := string(mustMakePEMBlock("CERTIFICATE", nil, goodCert1))
 | |
| 	goodCert2Block := string(mustMakePEMBlock("CERTIFICATE", nil, goodCert2))
 | |
| 
 | |
| 	goodCert1AlternateBlock := strings.ReplaceAll(goodCert1Block, "\n", "\n\t\n")
 | |
| 
 | |
| 	badNotCACertBlock := string(mustMakePEMBlock("CERTIFICATE", nil, badNotCACert))
 | |
| 
 | |
| 	badBlockHeadersBlock := string(mustMakePEMBlock("CERTIFICATE", map[string]string{"key": "value"}, goodCert1))
 | |
| 	badBlockTypeBlock := string(mustMakePEMBlock("NOTACERTIFICATE", nil, goodCert1))
 | |
| 	badNonParseableBlock := string(mustMakePEMBlock("CERTIFICATE", nil, []byte("this is not a certificate")))
 | |
| 
 | |
| 	badTooBigBundle := ""
 | |
| 	for i := 0; i < (core.MaxSecretSize/len(goodCert1Block))+1; i++ {
 | |
| 		badTooBigBundle += goodCert1Block + "\n"
 | |
| 	}
 | |
| 
 | |
| 	testCases := []struct {
 | |
| 		description string
 | |
| 		bundle      *capi.ClusterTrustBundle
 | |
| 		opts        ValidateClusterTrustBundleOptions
 | |
| 		wantErrors  field.ErrorList
 | |
| 	}{
 | |
| 		{
 | |
| 			description: "valid, no signer name",
 | |
| 			bundle: &capi.ClusterTrustBundle{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "foo",
 | |
| 				},
 | |
| 				Spec: capi.ClusterTrustBundleSpec{
 | |
| 					TrustBundle: goodCert1Block,
 | |
| 				},
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			description: "invalid, too big",
 | |
| 			bundle: &capi.ClusterTrustBundle{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "foo",
 | |
| 				},
 | |
| 				Spec: capi.ClusterTrustBundleSpec{
 | |
| 					TrustBundle: badTooBigBundle,
 | |
| 				},
 | |
| 			},
 | |
| 			wantErrors: field.ErrorList{
 | |
| 				field.TooLong(field.NewPath("spec", "trustBundle"), "" /*unused*/, core.MaxSecretSize),
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			description: "invalid, no signer name, invalid name",
 | |
| 			bundle: &capi.ClusterTrustBundle{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "k8s.io:bar:foo",
 | |
| 				},
 | |
| 				Spec: capi.ClusterTrustBundleSpec{
 | |
| 					TrustBundle: goodCert1Block,
 | |
| 				},
 | |
| 			},
 | |
| 			wantErrors: field.ErrorList{
 | |
| 				field.Invalid(field.NewPath("metadata", "name"), "k8s.io:bar:foo", "ClusterTrustBundle without signer name must not have \":\" in its name"),
 | |
| 			},
 | |
| 		}, {
 | |
| 			description: "valid, with signer name",
 | |
| 			bundle: &capi.ClusterTrustBundle{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "k8s.io:foo:bar",
 | |
| 				},
 | |
| 				Spec: capi.ClusterTrustBundleSpec{
 | |
| 					SignerName:  "k8s.io/foo",
 | |
| 					TrustBundle: goodCert1Block,
 | |
| 				},
 | |
| 			},
 | |
| 		}, {
 | |
| 			description: "invalid, with signer name, missing name prefix",
 | |
| 			bundle: &capi.ClusterTrustBundle{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "look-ma-no-prefix",
 | |
| 				},
 | |
| 				Spec: capi.ClusterTrustBundleSpec{
 | |
| 					SignerName:  "k8s.io/foo",
 | |
| 					TrustBundle: goodCert1Block,
 | |
| 				},
 | |
| 			},
 | |
| 			wantErrors: field.ErrorList{
 | |
| 				field.Invalid(field.NewPath("metadata", "name"), "look-ma-no-prefix", "ClusterTrustBundle for signerName k8s.io/foo must be named with prefix k8s.io:foo:"),
 | |
| 			},
 | |
| 		}, {
 | |
| 			description: "invalid, with signer name, empty name suffix",
 | |
| 			bundle: &capi.ClusterTrustBundle{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "k8s.io:foo:",
 | |
| 				},
 | |
| 				Spec: capi.ClusterTrustBundleSpec{
 | |
| 					SignerName:  "k8s.io/foo",
 | |
| 					TrustBundle: goodCert1Block,
 | |
| 				},
 | |
| 			},
 | |
| 			wantErrors: field.ErrorList{
 | |
| 				field.Invalid(field.NewPath("metadata", "name"), "k8s.io:foo:", `a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`),
 | |
| 			},
 | |
| 		}, {
 | |
| 			description: "invalid, with signer name, bad name suffix",
 | |
| 			bundle: &capi.ClusterTrustBundle{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "k8s.io:foo:123notvalidDNSSubdomain",
 | |
| 				},
 | |
| 				Spec: capi.ClusterTrustBundleSpec{
 | |
| 					SignerName:  "k8s.io/foo",
 | |
| 					TrustBundle: goodCert1Block,
 | |
| 				},
 | |
| 			},
 | |
| 			wantErrors: field.ErrorList{
 | |
| 				field.Invalid(field.NewPath("metadata", "name"), "k8s.io:foo:123notvalidDNSSubdomain", `a lowercase RFC 1123 subdomain must consist of lower case alphanumeric characters, '-' or '.', and must start and end with an alphanumeric character (e.g. 'example.com', regex used for validation is '[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*')`),
 | |
| 			},
 | |
| 		}, {
 | |
| 			description: "valid, with signer name, with inter-block garbage",
 | |
| 			bundle: &capi.ClusterTrustBundle{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "k8s.io:foo:abc",
 | |
| 				},
 | |
| 				Spec: capi.ClusterTrustBundleSpec{
 | |
| 					SignerName:  "k8s.io/foo",
 | |
| 					TrustBundle: "garbage\n" + goodCert1Block + "\ngarbage\n" + goodCert2Block,
 | |
| 				},
 | |
| 			},
 | |
| 		}, {
 | |
| 			description: "invalid, no signer name, no trust anchors",
 | |
| 			bundle: &capi.ClusterTrustBundle{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "foo",
 | |
| 				},
 | |
| 				Spec: capi.ClusterTrustBundleSpec{},
 | |
| 			},
 | |
| 			wantErrors: field.ErrorList{
 | |
| 				field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"),
 | |
| 			},
 | |
| 		}, {
 | |
| 			description: "invalid, no trust anchors",
 | |
| 			bundle: &capi.ClusterTrustBundle{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "k8s.io:foo:abc",
 | |
| 				},
 | |
| 				Spec: capi.ClusterTrustBundleSpec{
 | |
| 					SignerName: "k8s.io/foo",
 | |
| 				},
 | |
| 			},
 | |
| 			wantErrors: field.ErrorList{
 | |
| 				field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"),
 | |
| 			},
 | |
| 		}, {
 | |
| 			description: "invalid, bad signer name",
 | |
| 			bundle: &capi.ClusterTrustBundle{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "invalid:foo",
 | |
| 				},
 | |
| 				Spec: capi.ClusterTrustBundleSpec{
 | |
| 					SignerName:  "invalid",
 | |
| 					TrustBundle: goodCert1Block,
 | |
| 				},
 | |
| 			},
 | |
| 			wantErrors: field.ErrorList{
 | |
| 				field.Invalid(field.NewPath("spec", "signerName"), "invalid", "must be a fully qualified domain and path of the form 'example.com/signer-name'"),
 | |
| 			},
 | |
| 		}, {
 | |
| 			description: "invalid, no blocks",
 | |
| 			bundle: &capi.ClusterTrustBundle{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "foo",
 | |
| 				},
 | |
| 				Spec: capi.ClusterTrustBundleSpec{
 | |
| 					TrustBundle: "non block garbage",
 | |
| 				},
 | |
| 			},
 | |
| 			wantErrors: field.ErrorList{
 | |
| 				field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"),
 | |
| 			},
 | |
| 		}, {
 | |
| 			description: "invalid, bad block type",
 | |
| 			bundle: &capi.ClusterTrustBundle{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "foo",
 | |
| 				},
 | |
| 				Spec: capi.ClusterTrustBundleSpec{
 | |
| 					TrustBundle: goodCert1Block + "\n" + badBlockTypeBlock,
 | |
| 				},
 | |
| 			},
 | |
| 			wantErrors: field.ErrorList{
 | |
| 				field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "entry 1 has bad block type: NOTACERTIFICATE"),
 | |
| 			},
 | |
| 		}, {
 | |
| 			description: "invalid, block with headers",
 | |
| 			bundle: &capi.ClusterTrustBundle{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "foo",
 | |
| 				},
 | |
| 				Spec: capi.ClusterTrustBundleSpec{
 | |
| 					TrustBundle: goodCert1Block + "\n" + badBlockHeadersBlock,
 | |
| 				},
 | |
| 			},
 | |
| 			wantErrors: field.ErrorList{
 | |
| 				field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "entry 1 has PEM block headers"),
 | |
| 			},
 | |
| 		}, {
 | |
| 			description: "invalid, cert is not a CA cert",
 | |
| 			bundle: &capi.ClusterTrustBundle{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "foo",
 | |
| 				},
 | |
| 				Spec: capi.ClusterTrustBundleSpec{
 | |
| 					TrustBundle: badNotCACertBlock,
 | |
| 				},
 | |
| 			},
 | |
| 			wantErrors: field.ErrorList{
 | |
| 				field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "entry 0 does not have the CA bit set"),
 | |
| 			},
 | |
| 		}, {
 | |
| 			description: "invalid, duplicated blocks",
 | |
| 			bundle: &capi.ClusterTrustBundle{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "foo",
 | |
| 				},
 | |
| 				Spec: capi.ClusterTrustBundleSpec{
 | |
| 					TrustBundle: goodCert1Block + "\n" + goodCert1AlternateBlock,
 | |
| 				},
 | |
| 			},
 | |
| 			wantErrors: field.ErrorList{
 | |
| 				field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "duplicate trust anchor (indices [0 1])"),
 | |
| 			},
 | |
| 		}, {
 | |
| 			description: "invalid, non-certificate entry",
 | |
| 			bundle: &capi.ClusterTrustBundle{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "foo",
 | |
| 				},
 | |
| 				Spec: capi.ClusterTrustBundleSpec{
 | |
| 					TrustBundle: goodCert1Block + "\n" + badNonParseableBlock,
 | |
| 				},
 | |
| 			},
 | |
| 			wantErrors: field.ErrorList{
 | |
| 				field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "entry 1 does not parse as X.509"),
 | |
| 			},
 | |
| 		}, {
 | |
| 			description: "allow any old garbage in the PEM field if we suppress parsing",
 | |
| 			bundle: &capi.ClusterTrustBundle{
 | |
| 				ObjectMeta: metav1.ObjectMeta{
 | |
| 					Name: "foo",
 | |
| 				},
 | |
| 				Spec: capi.ClusterTrustBundleSpec{
 | |
| 					TrustBundle: "garbage",
 | |
| 				},
 | |
| 			},
 | |
| 			opts: ValidateClusterTrustBundleOptions{
 | |
| 				SuppressBundleParsing: true,
 | |
| 			},
 | |
| 		}}
 | |
| 	for _, tc := range testCases {
 | |
| 		t.Run(tc.description, func(t *testing.T) {
 | |
| 			gotErrors := ValidateClusterTrustBundle(tc.bundle, tc.opts)
 | |
| 			if diff := cmp.Diff(gotErrors, tc.wantErrors); diff != "" {
 | |
| 				t.Fatalf("Unexpected error output from Validate; diff (-got +want)\n%s", diff)
 | |
| 			}
 | |
| 
 | |
| 			// When there are no changes to the object,
 | |
| 			// ValidateClusterTrustBundleUpdate should not report errors about
 | |
| 			// the TrustBundle field.
 | |
| 			tc.bundle.ObjectMeta.ResourceVersion = "1"
 | |
| 			newBundle := tc.bundle.DeepCopy()
 | |
| 			newBundle.ObjectMeta.ResourceVersion = "2"
 | |
| 			gotErrors = ValidateClusterTrustBundleUpdate(newBundle, tc.bundle)
 | |
| 
 | |
| 			var filteredWantErrors field.ErrorList
 | |
| 			for _, err := range tc.wantErrors {
 | |
| 				if err.Field != "spec.trustBundle" {
 | |
| 					filteredWantErrors = append(filteredWantErrors, err)
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if diff := cmp.Diff(gotErrors, filteredWantErrors); diff != "" {
 | |
| 				t.Fatalf("Unexpected error output from ValidateUpdate; diff (-got +want)\n%s", diff)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestValidateClusterTrustBundleUpdate(t *testing.T) {
 | |
| 	goodCert1 := mustMakeCertificate(t, &x509.Certificate{
 | |
| 		SerialNumber: big.NewInt(0),
 | |
| 		Subject: pkix.Name{
 | |
| 			CommonName: "root1",
 | |
| 		},
 | |
| 		IsCA:                  true,
 | |
| 		BasicConstraintsValid: true,
 | |
| 	})
 | |
| 
 | |
| 	goodCert2 := mustMakeCertificate(t, &x509.Certificate{
 | |
| 		SerialNumber: big.NewInt(0),
 | |
| 		Subject: pkix.Name{
 | |
| 			CommonName: "root2",
 | |
| 		},
 | |
| 		IsCA:                  true,
 | |
| 		BasicConstraintsValid: true,
 | |
| 	})
 | |
| 
 | |
| 	goodCert1Block := string(mustMakePEMBlock("CERTIFICATE", nil, goodCert1))
 | |
| 	goodCert2Block := string(mustMakePEMBlock("CERTIFICATE", nil, goodCert2))
 | |
| 
 | |
| 	testCases := []struct {
 | |
| 		description          string
 | |
| 		oldBundle, newBundle *capi.ClusterTrustBundle
 | |
| 		wantErrors           field.ErrorList
 | |
| 	}{{
 | |
| 		description: "changing signer name disallowed",
 | |
| 		oldBundle: &capi.ClusterTrustBundle{
 | |
| 			ObjectMeta: metav1.ObjectMeta{
 | |
| 				Name: "k8s.io:foo:bar",
 | |
| 			},
 | |
| 			Spec: capi.ClusterTrustBundleSpec{
 | |
| 				SignerName:  "k8s.io/foo",
 | |
| 				TrustBundle: goodCert1Block,
 | |
| 			},
 | |
| 		},
 | |
| 		newBundle: &capi.ClusterTrustBundle{
 | |
| 			ObjectMeta: metav1.ObjectMeta{
 | |
| 				Name: "k8s.io:foo:bar",
 | |
| 			},
 | |
| 			Spec: capi.ClusterTrustBundleSpec{
 | |
| 				SignerName:  "k8s.io/bar",
 | |
| 				TrustBundle: goodCert1Block,
 | |
| 			},
 | |
| 		},
 | |
| 		wantErrors: field.ErrorList{
 | |
| 			field.Invalid(field.NewPath("metadata", "name"), "k8s.io:foo:bar", "ClusterTrustBundle for signerName k8s.io/bar must be named with prefix k8s.io:bar:"),
 | |
| 			field.Invalid(field.NewPath("spec", "signerName"), "k8s.io/bar", "field is immutable"),
 | |
| 		},
 | |
| 	}, {
 | |
| 		description: "adding certificate allowed",
 | |
| 		oldBundle: &capi.ClusterTrustBundle{
 | |
| 			ObjectMeta: metav1.ObjectMeta{
 | |
| 				Name: "k8s.io:foo:bar",
 | |
| 			},
 | |
| 			Spec: capi.ClusterTrustBundleSpec{
 | |
| 				SignerName:  "k8s.io/foo",
 | |
| 				TrustBundle: goodCert1Block,
 | |
| 			},
 | |
| 		},
 | |
| 		newBundle: &capi.ClusterTrustBundle{
 | |
| 			ObjectMeta: metav1.ObjectMeta{
 | |
| 				Name: "k8s.io:foo:bar",
 | |
| 			},
 | |
| 			Spec: capi.ClusterTrustBundleSpec{
 | |
| 				SignerName:  "k8s.io/foo",
 | |
| 				TrustBundle: goodCert1Block + "\n" + goodCert2Block,
 | |
| 			},
 | |
| 		},
 | |
| 	}, {
 | |
| 		description: "emptying trustBundle disallowed",
 | |
| 		oldBundle: &capi.ClusterTrustBundle{
 | |
| 			ObjectMeta: metav1.ObjectMeta{
 | |
| 				Name: "k8s.io:foo:bar",
 | |
| 			},
 | |
| 			Spec: capi.ClusterTrustBundleSpec{
 | |
| 				SignerName:  "k8s.io/foo",
 | |
| 				TrustBundle: goodCert1Block,
 | |
| 			},
 | |
| 		},
 | |
| 		newBundle: &capi.ClusterTrustBundle{
 | |
| 			ObjectMeta: metav1.ObjectMeta{
 | |
| 				Name: "k8s.io:foo:bar",
 | |
| 			},
 | |
| 			Spec: capi.ClusterTrustBundleSpec{
 | |
| 				SignerName:  "k8s.io/foo",
 | |
| 				TrustBundle: "",
 | |
| 			},
 | |
| 		},
 | |
| 		wantErrors: field.ErrorList{
 | |
| 			field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"),
 | |
| 		},
 | |
| 	}, {
 | |
| 		description: "emptying trustBundle (replace with non-block garbage) disallowed",
 | |
| 		oldBundle: &capi.ClusterTrustBundle{
 | |
| 			ObjectMeta: metav1.ObjectMeta{
 | |
| 				Name: "k8s.io:foo:bar",
 | |
| 			},
 | |
| 			Spec: capi.ClusterTrustBundleSpec{
 | |
| 				SignerName:  "k8s.io/foo",
 | |
| 				TrustBundle: goodCert1Block,
 | |
| 			},
 | |
| 		},
 | |
| 		newBundle: &capi.ClusterTrustBundle{
 | |
| 			ObjectMeta: metav1.ObjectMeta{
 | |
| 				Name: "k8s.io:foo:bar",
 | |
| 			},
 | |
| 			Spec: capi.ClusterTrustBundleSpec{
 | |
| 				SignerName:  "k8s.io/foo",
 | |
| 				TrustBundle: "non block garbage",
 | |
| 			},
 | |
| 		},
 | |
| 		wantErrors: field.ErrorList{
 | |
| 			field.Invalid(field.NewPath("spec", "trustBundle"), "<value omitted>", "at least one trust anchor must be provided"),
 | |
| 		},
 | |
| 	}}
 | |
| 
 | |
| 	for _, tc := range testCases {
 | |
| 		t.Run(tc.description, func(t *testing.T) {
 | |
| 			tc.oldBundle.ObjectMeta.ResourceVersion = "1"
 | |
| 			tc.newBundle.ObjectMeta.ResourceVersion = "2"
 | |
| 			gotErrors := ValidateClusterTrustBundleUpdate(tc.newBundle, tc.oldBundle)
 | |
| 			if diff := cmp.Diff(gotErrors, tc.wantErrors); diff != "" {
 | |
| 				t.Errorf("Unexpected error output from ValidateUpdate; diff (-got +want)\n%s", diff)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| var (
 | |
| 	validCertificate = []byte(`
 | |
| Leading non-PEM content
 | |
| -----BEGIN CERTIFICATE-----
 | |
| MIIBqDCCAU2gAwIBAgIUfbqeieihh/oERbfvRm38XvS/xHAwCgYIKoZIzj0EAwIw
 | |
| GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMCAXDTE2MTAxMTA1MDYwMFoYDzIx
 | |
| MTYwOTE3MDUwNjAwWjAUMRIwEAYDVQQDEwlNeSBDbGllbnQwWTATBgcqhkjOPQIB
 | |
| BggqhkjOPQMBBwNCAARv6N4R/sjMR65iMFGNLN1GC/vd7WhDW6J4X/iAjkRLLnNb
 | |
| KbRG/AtOUZ+7upJ3BWIRKYbOabbQGQe2BbKFiap4o3UwczAOBgNVHQ8BAf8EBAMC
 | |
| BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
 | |
| K/pZOWpNcYai6eHFpmJEeFpeQlEwHwYDVR0jBBgwFoAUX6nQlxjfWnP6aM1meO/Q
 | |
| a6b3a9kwCgYIKoZIzj0EAwIDSQAwRgIhAIWTKw/sjJITqeuNzJDAKU4xo1zL+xJ5
 | |
| MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1dveps=
 | |
| -----END CERTIFICATE-----
 | |
| Intermediate non-PEM content
 | |
| -----BEGIN CERTIFICATE-----
 | |
| MIIBqDCCAU6gAwIBAgIUfqZtjoFgczZ+oQZbEC/BDSS2J6wwCgYIKoZIzj0EAwIw
 | |
| EjEQMA4GA1UEAxMHUm9vdC1DQTAgFw0xNjEwMTEwNTA2MDBaGA8yMTE2MDkxNzA1
 | |
| MDYwMFowGjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMFkwEwYHKoZIzj0CAQYI
 | |
| KoZIzj0DAQcDQgAEyWHEMMCctJg8Xa5YWLqaCPbk3MjB+uvXac42JM9pj4k9jedD
 | |
| kpUJRkWIPzgJI8Zk/3cSzluUTixP6JBSDKtwwaN4MHYwDgYDVR0PAQH/BAQDAgGm
 | |
| MBMGA1UdJQQMMAoGCCsGAQUFBwMCMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYE
 | |
| FF+p0JcY31pz+mjNZnjv0Gum92vZMB8GA1UdIwQYMBaAFB7P6+i4/pfNjqZgJv/b
 | |
| dgA7Fe4tMAoGCCqGSM49BAMCA0gAMEUCIQCTT1YWQZaAqfQ2oBxzOkJE2BqLFxhz
 | |
| 3smQlrZ5gCHddwIgcvT7puhYOzAgcvMn9+SZ1JOyZ7edODjshCVCRnuHK2c=
 | |
| -----END CERTIFICATE-----
 | |
| Trailing non-PEM content
 | |
| `)
 | |
| 
 | |
| 	invalidCertificateNoPEM = []byte(`no PEM content`)
 | |
| 
 | |
| 	invalidCertificateNonCertificatePEM = []byte(`
 | |
| Leading non-PEM content
 | |
| -----BEGIN CERTIFICATE1-----
 | |
| MIIBqDCCAU2gAwIBAgIUfbqeieihh/oERbfvRm38XvS/xHAwCgYIKoZIzj0EAwIw
 | |
| GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMCAXDTE2MTAxMTA1MDYwMFoYDzIx
 | |
| MTYwOTE3MDUwNjAwWjAUMRIwEAYDVQQDEwlNeSBDbGllbnQwWTATBgcqhkjOPQIB
 | |
| BggqhkjOPQMBBwNCAARv6N4R/sjMR65iMFGNLN1GC/vd7WhDW6J4X/iAjkRLLnNb
 | |
| KbRG/AtOUZ+7upJ3BWIRKYbOabbQGQe2BbKFiap4o3UwczAOBgNVHQ8BAf8EBAMC
 | |
| BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
 | |
| K/pZOWpNcYai6eHFpmJEeFpeQlEwHwYDVR0jBBgwFoAUX6nQlxjfWnP6aM1meO/Q
 | |
| a6b3a9kwCgYIKoZIzj0EAwIDSQAwRgIhAIWTKw/sjJITqeuNzJDAKU4xo1zL+xJ5
 | |
| MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1dveps=
 | |
| -----END CERTIFICATE1-----
 | |
| Trailing non-PEM content
 | |
| `)
 | |
| 
 | |
| 	invalidCertificatePEMHeaders = []byte(`
 | |
| Leading non-PEM content
 | |
| -----BEGIN CERTIFICATE-----
 | |
| Some-Header: Some-Value
 | |
| MIIBqDCCAU2gAwIBAgIUfbqeieihh/oERbfvRm38XvS/xHAwCgYIKoZIzj0EAwIw
 | |
| GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMCAXDTE2MTAxMTA1MDYwMFoYDzIx
 | |
| MTYwOTE3MDUwNjAwWjAUMRIwEAYDVQQDEwlNeSBDbGllbnQwWTATBgcqhkjOPQIB
 | |
| BggqhkjOPQMBBwNCAARv6N4R/sjMR65iMFGNLN1GC/vd7WhDW6J4X/iAjkRLLnNb
 | |
| KbRG/AtOUZ+7upJ3BWIRKYbOabbQGQe2BbKFiap4o3UwczAOBgNVHQ8BAf8EBAMC
 | |
| BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
 | |
| K/pZOWpNcYai6eHFpmJEeFpeQlEwHwYDVR0jBBgwFoAUX6nQlxjfWnP6aM1meO/Q
 | |
| a6b3a9kwCgYIKoZIzj0EAwIDSQAwRgIhAIWTKw/sjJITqeuNzJDAKU4xo1zL+xJ5
 | |
| MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1dveps=
 | |
| -----END CERTIFICATE-----
 | |
| Trailing non-PEM content
 | |
| `)
 | |
| 
 | |
| 	invalidCertificateNonBase64PEM = []byte(`
 | |
| Leading non-PEM content
 | |
| -----BEGIN CERTIFICATE-----
 | |
| MIIBqDCCAU2gAwIBAgIUfbqeieihh/oERbfvRm38XvS/xHAwCgYIKoZIzj0EAwIw
 | |
| GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMCAXDTE2MTAxMTA1MDYwMFoYDzIx
 | |
| MTYwOTE3MDUwNjAwWjAUMRIwEAYDVQQDEwlNeSBDbGllbnQwWTATBgcqhkjOPQIB
 | |
| BggqhkjOPQMBBwNCAARv6N4R/sjMR65iMFGNLN1GC/vd7WhDW6J4X/iAjkRLLnNb
 | |
| KbRG/AtOUZ+7upJ3BWIRKYbOabbQGQe2BbKFiap4o3UwczAOBgNVHQ8BAf8EBAMC
 | |
| BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
 | |
| K/pZOWpNcYai6eHFpmJEeFpeQlEwHwYDVR0jBBgwFoAUX6nQlxjfWnP6aM1meO/Q
 | |
| a6b3a9kwCgYIKoZIzj0EAwIDSQAwRgIhAIWTKw/sjJITqeuNzJDAKU4xo1zL+xJ5
 | |
| MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1d?????????
 | |
| -----END CERTIFICATE-----
 | |
| Trailing non-PEM content
 | |
| `)
 | |
| 
 | |
| 	invalidCertificateEmptyPEM = []byte(`
 | |
| Leading non-PEM content
 | |
| -----BEGIN CERTIFICATE-----
 | |
| -----END CERTIFICATE-----
 | |
| Trailing non-PEM content
 | |
| `)
 | |
| 
 | |
| 	// first character is invalid
 | |
| 	invalidCertificateNonASN1Data = []byte(`
 | |
| Leading non-PEM content
 | |
| -----BEGIN CERTIFICATE-----
 | |
| MIIBqDCCAU2gAwIBAgIUfbqeieihh/oERbfvRm38XvS/xHAwCgYIKoZIzj0EAwIw
 | |
| GjEYMBYGA1UEAxMPSW50ZXJtZWRpYXRlLUNBMCAXDTE2MTAxMTA1MDYwMFoYDzIx
 | |
| MTYwOTE3MDUwNjAwWjAUNRIwEAYDVQQDEwlNeSBDbGllbnQwWTATBgcqhkjOPQIB
 | |
| BggqhkjOPQMBBwNCAARv6N4R/sjMR65iMFGNLN1GC/vd7WhDW6J4X/iAjkRLLnNb
 | |
| KbRG/AtOUZ+7upJ3BWIRKYbOabbQGQe2BbKFiap4o3UwczAOBgNVHQ8BAf8EBAMC
 | |
| BaAwEwYDVR0lBAwwCgYIKwYBBQUHAwIwDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQU
 | |
| K/pZOWpNcYai6eHFpmJEeFpeQlEwHwYDVR0jBBgwFoAUX6nQlxjfWnP6aM1meO/Q
 | |
| a6b3a9kwCgYIKoZIzj0EAwIDSQAwRgIhAIWTKw/sjJITqeuNzJDAKU4xo1zL+xJ5
 | |
| MnVCuBwfwDXCAiEAw/1TA+CjPq9JC5ek1ifR0FybTURjeQqYkKpve1dveps=
 | |
| -----END CERTIFICATE-----
 | |
| Trailing non-PEM content
 | |
| `)
 | |
| )
 | 
