diff --git a/README.md b/README.md index bce1a232..aa9f9573 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,8 @@ Currently, the `cert-controller-manager` supports certificate authorities via: - [Using `commonName` and optional `dnsNames`](#using-commonname-and-optional-dnsnames) - [Follow CNAME](#follow-cname) - [Preferred Chain](#preferred-chain) + - [Secret Labels](#secret-labels) + - [Specifying private key algorithm and size](#specifying-private-key-algorithm-and-size) - [Using a certificate signing request (CSR)](#using-a-certificate-signing-request-csr) - [Creating JKS or PKCS#12 keystores](#creating-jks-or-pkcs12-keystores) - [Requesting a Certificate for Ingress](#requesting-a-certificate-for-ingress) @@ -352,6 +354,33 @@ spec: In this case the secret `my-secret` will contains the labels. +### Specifying private key algorithm and size + +By default, the certificate uses `RSA` with a key size of 2048 bits for the private key. +Add the `privateKey` section to specify private key algorithm and/or size. + +Example: + +```yaml +apiVersion: cert.gardener.cloud/v1alpha1 +kind: Certificate +metadata: + name: cert-ecdsa + namespace: default +spec: + commonName: my-service.example-domain.com + secretName: my-secret + privateKey: + algorithm: ECDSA + size: 384 +``` + +Allowed values for `spec.privateKey.algorithm` are `RSA` and `ECDSA`. +For `RSA`, the allowed key sizes are `2048`, `3072`, and `4096`. If the size field is not specified, +`2048` is used by default. +For `ECDSA`, the allowed key sizes are `256` and `384`. If the size field is not specified, +`256` is used by default. + ### Using a certificate signing request (CSR) You can provide a complete CSR in PEM format (and encoded as Base64). @@ -474,6 +503,8 @@ See also [examples/40-ingress-echoheaders.yaml](./examples/40-ingress-echoheader #cert.gardener.cloud/secret-labels: "key1=value1,key2=value2" # optional labels for the certificate secret #cert.gardener.cloud/issuer: issuer-name # optional to specify custom issuer (use namespace/name for shoot issuers) #cert.gardener.cloud/preferred-chain: "chain name" # optional to specify preferred-chain (value is the Subject Common Name of the root issuer) + #cert.gardener.cloud/private-key-algorithm: ECDSA # optional to specify algorithm for private key, allowed values are 'RSA' or 'ECDSA' + #cert.gardener.cloud/private-key-size: "384" # optional to specify size of private key, allowed values for RSA are "2048", "3072", "4096" and for ECDSA "256" and "384" spec: tls: - hosts: @@ -526,6 +557,8 @@ metadata: #cert.gardener.cloud/secret-labels: "key1=value1,key2=value2" # optional labels for the certificate secret #cert.gardener.cloud/issuer: issuer-name # optional to specify custom issuer (use namespace/name for shoot issuers) #cert.gardener.cloud/preferred-chain: "chain name" # optional to specify preferred-chain (value is the Subject Common Name of the root issuer) + #cert.gardener.cloud/private-key-algorithm: ECDSA # optional to specify algorithm for private key, allowed values are 'RSA' or 'ECDSA' + #cert.gardener.cloud/private-key-size: "384" # optional to specify size of private key, allowed values for RSA are "2048", "3072", "4096" and for ECDSA "256" and "384" dns.gardener.cloud/ttl: "600" name: test-service namespace: default diff --git a/VERSION b/VERSION index d718103d..7be8dd91 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -v0.12.2-dev \ No newline at end of file +v0.13.0-dev diff --git a/charts/cert-management/templates/cert.gardener.cloud_certificates.yaml b/charts/cert-management/templates/cert.gardener.cloud_certificates.yaml index cf76f79e..aa729a42 100644 --- a/charts/cert-management/templates/cert.gardener.cloud_certificates.yaml +++ b/charts/cert-management/templates/cert.gardener.cloud_certificates.yaml @@ -182,6 +182,37 @@ spec: chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.' type: string + privateKey: + description: Private key options. These include the key algorithm + and size. + properties: + algorithm: + description: "Algorithm is the private key algorithm of the corresponding + private key for this certificate. \n If provided, allowed values + are either `RSA` or `ECDSA`. If `algorithm` is specified and + `size` is not provided, key size of 2048 will be used for `RSA` + key algorithm and key size of 256 will be used for `ECDSA` key + algorithm." + enum: + - RSA + - ECDSA + type: string + size: + description: "Size is the key bit size of the corresponding private + key for this certificate. \n If `algorithm` is set to `RSA`, + valid values are `2048`, `3072` or `4096`, and will default + to `2048` if not specified. If `algorithm` is set to `ECDSA`, + valid values are `256` or `384`, and will default to `256` if + not specified. No other values are allowed." + enum: + - 256 + - 384 + - 2048 + - 3072 + - 4096 + format: int32 + type: integer + type: object renew: description: Renew triggers a renewal if set to true type: boolean diff --git a/charts/cert-management/values.yaml b/charts/cert-management/values.yaml index 12475991..1448e9da 100644 --- a/charts/cert-management/values.yaml +++ b/charts/cert-management/values.yaml @@ -9,7 +9,7 @@ replicaCount: 1 image: repository: europe-docker.pkg.dev/gardener-project/public/cert-controller-manager - tag: v0.12.2-master + tag: v0.13.0-master pullPolicy: IfNotPresent resources: diff --git a/examples/30-cert-simple.yaml b/examples/30-cert-simple.yaml index f8edcba9..448c0af7 100644 --- a/examples/30-cert-simple.yaml +++ b/examples/30-cert-simple.yaml @@ -32,7 +32,12 @@ spec: # either '_acme-challenge.cert1.mydomain.com' or '_acme-challenge.cert1.my-other-domain.com'. # For example: If a CNAME record exists '_acme-challenge.cert1.mydomain.com' => '_acme-challenge.writable.domain.com', # the DNS challenge will be written to '_acme-challenge.writable.domain.com'. - #followCNAME: true + # followCNAME: true # Optionally specify the preferred certificate chain: if the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used. - #preferredChain: "ISRG Root X1" \ No newline at end of file + # preferredChain: "ISRG Root X1" + + # Optionally specify algorithm and key size for private key + # privateKey: + # algorithm: ECDSA + # size: 384 \ No newline at end of file diff --git a/examples/40-ingress-echoheaders.yaml b/examples/40-ingress-echoheaders.yaml index 5a84c7ee..8ab3a2fd 100644 --- a/examples/40-ingress-echoheaders.yaml +++ b/examples/40-ingress-echoheaders.yaml @@ -16,6 +16,8 @@ metadata: #cert.gardener.cloud/secret-labels: "key1=value1,key2=value2" # optional labels for the certificate secret #cert.gardener.cloud/issuer: issuer-name # optional to specify custom issuer (use namespace/name for shoot issuers) #cert.gardener.cloud/preferred-chain: "chain name" # optional to specify preferred-chain (value is the Subject Common Name of the root issuer) + #cert.gardener.cloud/private-key-algorithm: ECDSA # optional to specify algorithm for private key, allowed values are 'RSA' or 'ECDSA' + #cert.gardener.cloud/private-key-size: "384" # optional to specify size of private key, allowed values for RSA are "2048", "3072", "4096" and for ECDSA "256" and "384" spec: tls: - hosts: diff --git a/examples/40-service-loadbalancer.yaml b/examples/40-service-loadbalancer.yaml index 61301ca0..47fef3d4 100644 --- a/examples/40-service-loadbalancer.yaml +++ b/examples/40-service-loadbalancer.yaml @@ -16,6 +16,8 @@ metadata: #cert.gardener.cloud/secret-labels: "key1=value1,key2=value2" # optional labels for the certificate secret #cert.gardener.cloud/issuer: issuer-name # optional to specify custom issuer (use namespace/name for shoot issuers) #cert.gardener.cloud/preferred-chain: "chain name" # optional to specify preferred-chain (value is the Subject Common Name of the root issuer) + #cert.gardener.cloud/private-key-algorithm: ECDSA # optional to specify algorithm for private key, allowed values are 'RSA' or 'ECDSA' + #cert.gardener.cloud/private-key-size: "384" # optional to specify size of private key, allowed values for RSA are "2048", "3072", "4096" and for ECDSA "256" and "384" name: test-service namespace: default spec: diff --git a/pkg/apis/cert/crds/cert.gardener.cloud_certificates.yaml b/pkg/apis/cert/crds/cert.gardener.cloud_certificates.yaml index 6d49a62f..93df5ccb 100644 --- a/pkg/apis/cert/crds/cert.gardener.cloud_certificates.yaml +++ b/pkg/apis/cert/crds/cert.gardener.cloud_certificates.yaml @@ -177,6 +177,37 @@ spec: chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.' type: string + privateKey: + description: Private key options. These include the key algorithm + and size. + properties: + algorithm: + description: "Algorithm is the private key algorithm of the corresponding + private key for this certificate. \n If provided, allowed values + are either `RSA` or `ECDSA`. If `algorithm` is specified and + `size` is not provided, key size of 2048 will be used for `RSA` + key algorithm and key size of 256 will be used for `ECDSA` key + algorithm." + enum: + - RSA + - ECDSA + type: string + size: + description: "Size is the key bit size of the corresponding private + key for this certificate. \n If `algorithm` is set to `RSA`, + valid values are `2048`, `3072` or `4096`, and will default + to `2048` if not specified. If `algorithm` is set to `ECDSA`, + valid values are `256` or `384`, and will default to `256` if + not specified. No other values are allowed." + enum: + - 256 + - 384 + - 2048 + - 3072 + - 4096 + format: int32 + type: integer + type: object renew: description: Renew triggers a renewal if set to true type: boolean diff --git a/pkg/apis/cert/crds/zz_generated_crds.go b/pkg/apis/cert/crds/zz_generated_crds.go index 1e373987..3fe02856 100644 --- a/pkg/apis/cert/crds/zz_generated_crds.go +++ b/pkg/apis/cert/crds/zz_generated_crds.go @@ -477,6 +477,37 @@ spec: chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used.' type: string + privateKey: + description: Private key options. These include the key algorithm + and size. + properties: + algorithm: + description: "Algorithm is the private key algorithm of the corresponding + private key for this certificate. \n If provided, allowed values + are either ` + "`" + `RSA` + "`" + ` or ` + "`" + `ECDSA` + "`" + `. If ` + "`" + `algorithm` + "`" + ` is specified and + ` + "`" + `size` + "`" + ` is not provided, key size of 2048 will be used for ` + "`" + `RSA` + "`" + ` + key algorithm and key size of 256 will be used for ` + "`" + `ECDSA` + "`" + ` key + algorithm." + enum: + - RSA + - ECDSA + type: string + size: + description: "Size is the key bit size of the corresponding private + key for this certificate. \n If ` + "`" + `algorithm` + "`" + ` is set to ` + "`" + `RSA` + "`" + `, + valid values are ` + "`" + `2048` + "`" + `, ` + "`" + `3072` + "`" + ` or ` + "`" + `4096` + "`" + `, and will default + to ` + "`" + `2048` + "`" + ` if not specified. If ` + "`" + `algorithm` + "`" + ` is set to ` + "`" + `ECDSA` + "`" + `, + valid values are ` + "`" + `256` + "`" + ` or ` + "`" + `384` + "`" + `, and will default to ` + "`" + `256` + "`" + ` if + not specified. No other values are allowed." + enum: + - 256 + - 384 + - 2048 + - 3072 + - 4096 + format: int32 + type: integer + type: object renew: description: Renew triggers a renewal if set to true type: boolean diff --git a/pkg/apis/cert/v1alpha1/types.go b/pkg/apis/cert/v1alpha1/types.go index 6d296bd9..6b63c019 100644 --- a/pkg/apis/cert/v1alpha1/types.go +++ b/pkg/apis/cert/v1alpha1/types.go @@ -82,6 +82,9 @@ type CertificateSpec struct { // PreferredChain allows to specify the preferred certificate chain: if the CA offers multiple certificate chains, prefer the chain with an issuer matching this Subject Common Name. If no match, the default offered chain will be used. // +optional PreferredChain *string `json:"preferredChain,omitempty"` + // Private key options. These include the key algorithm and size. + // +optional + PrivateKey *CertificatePrivateKey `json:"privateKey,omitempty"` } // IssuerRef is the reference of the issuer by name. @@ -93,6 +96,47 @@ type IssuerRef struct { Namespace string `json:"namespace,omitempty"` } +// PrivateKeyAlgorithm is the type for the algorithm. +// +kubebuilder:validation:Enum=RSA;ECDSA +type PrivateKeyAlgorithm string + +const ( + // RSAKeyAlgorithm is the value to use the RSA algorithm for the private key. + RSAKeyAlgorithm PrivateKeyAlgorithm = "RSA" + + // ECDSAKeyAlgorithm is the value to use the ECDSA algorithm for the private key. + ECDSAKeyAlgorithm PrivateKeyAlgorithm = "ECDSA" +) + +// PrivateKeySize is the size for the algorithm. +// +kubebuilder:validation:Enum=256;384;2048;3072;4096 +type PrivateKeySize int32 + +// CertificatePrivateKey contains configuration options for private keys +// used by the Certificate controller. +// These include the key algorithm and size. +type CertificatePrivateKey struct { + // Algorithm is the private key algorithm of the corresponding private key + // for this certificate. + // + // If provided, allowed values are either `RSA` or `ECDSA`. + // If `algorithm` is specified and `size` is not provided, + // key size of 2048 will be used for `RSA` key algorithm and + // key size of 256 will be used for `ECDSA` key algorithm. + // +optional + Algorithm *PrivateKeyAlgorithm `json:"algorithm,omitempty"` + + // Size is the key bit size of the corresponding private key for this certificate. + // + // If `algorithm` is set to `RSA`, valid values are `2048`, `3072` or `4096`, + // and will default to `2048` if not specified. + // If `algorithm` is set to `ECDSA`, valid values are `256` or `384`, + // and will default to `256` if not specified. + // No other values are allowed. + // +optional + Size *PrivateKeySize `json:"size,omitempty"` +} + // BackOffState stores the status for exponential back off on repeated cert request failure type BackOffState struct { // ObservedGeneration is the observed generation the BackOffState is assigned to @@ -158,7 +202,7 @@ type QualifiedIssuerRef struct { Namespace string `json:"namespace"` } -// IsDefaultCluster returns true if the reference is on the default cluster +// IsDefaultCluster returns true if the reference is on the default cluster. func (r QualifiedIssuerRef) IsDefaultCluster() bool { return r.Cluster == "default" } diff --git a/pkg/apis/cert/v1alpha1/zz_generated.deepcopy.go b/pkg/apis/cert/v1alpha1/zz_generated.deepcopy.go index 63872c50..347a04f7 100644 --- a/pkg/apis/cert/v1alpha1/zz_generated.deepcopy.go +++ b/pkg/apis/cert/v1alpha1/zz_generated.deepcopy.go @@ -203,6 +203,32 @@ func (in *CertificateList) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *CertificatePrivateKey) DeepCopyInto(out *CertificatePrivateKey) { + *out = *in + if in.Algorithm != nil { + in, out := &in.Algorithm, &out.Algorithm + *out = new(PrivateKeyAlgorithm) + **out = **in + } + if in.Size != nil { + in, out := &in.Size, &out.Size + *out = new(PrivateKeySize) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new CertificatePrivateKey. +func (in *CertificatePrivateKey) DeepCopy() *CertificatePrivateKey { + if in == nil { + return nil + } + out := new(CertificatePrivateKey) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *CertificateRef) DeepCopyInto(out *CertificateRef) { *out = *in @@ -422,6 +448,11 @@ func (in *CertificateSpec) DeepCopyInto(out *CertificateSpec) { *out = new(string) **out = **in } + if in.PrivateKey != nil { + in, out := &in.PrivateKey, &out.PrivateKey + *out = new(CertificatePrivateKey) + (*in).DeepCopyInto(*out) + } return } diff --git a/pkg/cert/legobridge/certificate.go b/pkg/cert/legobridge/certificate.go index a5dced30..d03faca3 100644 --- a/pkg/cert/legobridge/certificate.go +++ b/pkg/cert/legobridge/certificate.go @@ -14,8 +14,11 @@ import ( "sync" "time" + api "github.com/gardener/cert-management/pkg/apis/cert/v1alpha1" "github.com/gardener/cert-management/pkg/cert/metrics" "github.com/gardener/cert-management/pkg/cert/utils" + "github.com/go-acme/lego/v4/certcrypto" + "k8s.io/utils/ptr" "github.com/go-acme/lego/v4/certificate" "github.com/go-acme/lego/v4/challenge/dns01" @@ -59,6 +62,8 @@ type ObtainInput struct { AlwaysDeactivateAuthorizations bool // PreferredChain PreferredChain string + // KeyType represents the algo and size to use for the private key (only used if CSR is not set). + KeyType certcrypto.KeyType } // DNSControllerSettings are the settings for the DNSController. @@ -93,6 +98,8 @@ type ObtainOutput struct { DNSNames []string // CSR is the copy from the input. CSR []byte + // KeyType is the copy from the input. + KeyType certcrypto.KeyType // Renew is the flag if this was a renew request. Renew bool // Err contains the obtain request error. @@ -126,17 +133,85 @@ func NewObtainer() Obtainer { return &obtainer{pendingDomains: map[string]time.Time{}} } -func obtainForDomains(client *lego.Client, domains []string, deactivateAuthz bool, preferredChain string) (*certificate.Resource, error) { +func obtainForDomains(client *lego.Client, domains []string, input ObtainInput) (*certificate.Resource, error) { + privateKey, err := certcrypto.GeneratePrivateKey(input.KeyType) + if err != nil { + return nil, err + } request := certificate.ObtainRequest{ Domains: domains, Bundle: true, - AlwaysDeactivateAuthorizations: deactivateAuthz, - PreferredChain: preferredChain, + AlwaysDeactivateAuthorizations: input.AlwaysDeactivateAuthorizations, + PreferredChain: input.PreferredChain, + PrivateKey: privateKey, } return client.Certificate.Obtain(request) } -func obtainForCSR(client *lego.Client, csr []byte, deactivateAuthz bool, preferredChain string) (*certificate.Resource, error) { +// ToKeyType extracts the key type from the private key spec. +func ToKeyType(privateKeySpec *api.CertificatePrivateKey) (certcrypto.KeyType, error) { + keyType := certcrypto.RSA2048 + if privateKeySpec != nil { + algorithm := api.RSAKeyAlgorithm + if privateKeySpec.Algorithm != nil && *privateKeySpec.Algorithm != "" { + algorithm = *privateKeySpec.Algorithm + } + size := 0 + if privateKeySpec.Size != nil && *privateKeySpec.Size != 0 { + size = int(*privateKeySpec.Size) + } + switch algorithm { + case api.RSAKeyAlgorithm: + switch size { + case 0, 2048: + keyType = certcrypto.RSA2048 + case 3072: + keyType = certcrypto.RSA3072 + case 4096: + keyType = certcrypto.RSA4096 + default: + return "", fmt.Errorf("invalid key size for RSA: %d (allowed values are 2048, 3072, and 4096)", size) + } + case api.ECDSAKeyAlgorithm: + switch size { + case 0, 256: + keyType = certcrypto.EC256 + case 384: + keyType = certcrypto.EC384 + default: + return "", fmt.Errorf("invalid key size for ECDSA: %d (allowed values are 256 and 384)", size) + } + default: + return "", fmt.Errorf("invalid private key algorithm %s (allowed values are '%s' and '%s')", + algorithm, api.RSAKeyAlgorithm, api.ECDSAKeyAlgorithm) + } + } + return keyType, nil +} + +// FromKeyType converts key type back to a private key spec. +func FromKeyType(keyType certcrypto.KeyType) *api.CertificatePrivateKey { + switch keyType { + case certcrypto.RSA2048: + return newCertificatePrivateKey(api.RSAKeyAlgorithm, 2048) + case certcrypto.RSA3072: + return newCertificatePrivateKey(api.RSAKeyAlgorithm, 3072) + case certcrypto.RSA4096: + return newCertificatePrivateKey(api.RSAKeyAlgorithm, 4096) + case certcrypto.EC256: + return newCertificatePrivateKey(api.ECDSAKeyAlgorithm, 256) + case certcrypto.EC384: + return newCertificatePrivateKey(api.ECDSAKeyAlgorithm, 384) + default: + return nil + } +} + +func newCertificatePrivateKey(algorithm api.PrivateKeyAlgorithm, size api.PrivateKeySize) *api.CertificatePrivateKey { + return &api.CertificatePrivateKey{Algorithm: ptr.To(algorithm), Size: ptr.To(size)} +} + +func obtainForCSR(client *lego.Client, csr []byte, input ObtainInput) (*certificate.Resource, error) { cert, err := extractCertificateRequest(csr) if err != nil { return nil, err @@ -144,8 +219,8 @@ func obtainForCSR(client *lego.Client, csr []byte, deactivateAuthz bool, preferr return client.Certificate.ObtainForCSR(certificate.ObtainForCSRRequest{ CSR: cert, Bundle: true, - PreferredChain: preferredChain, - AlwaysDeactivateAuthorizations: deactivateAuthz, + AlwaysDeactivateAuthorizations: input.AlwaysDeactivateAuthorizations, + PreferredChain: input.PreferredChain, }) } @@ -195,7 +270,7 @@ func (o *obtainer) Obtain(input ObtainInput) error { } } -// Obtain starts the async obtain request. +// ObtainACME starts the async obtain request. func (o *obtainer) ObtainACME(input ObtainInput) error { err := o.setPending(input) if err != nil { @@ -246,9 +321,9 @@ func (o *obtainer) ObtainACME(input ObtainInput) error { if input.CommonName != nil { domains = append([]string{*input.CommonName}, domains...) } - certificates, err = obtainForDomains(client, domains, input.AlwaysDeactivateAuthorizations, input.PreferredChain) + certificates, err = obtainForDomains(client, domains, input) } else { - certificates, err = obtainForCSR(client, input.CSR, input.AlwaysDeactivateAuthorizations, input.PreferredChain) + certificates, err = obtainForCSR(client, input.CSR, input) } } count := provider.GetChallengesCount() @@ -258,6 +333,7 @@ func (o *obtainer) ObtainACME(input ObtainInput) error { IssuerInfo: utils.NewACMEIssuerInfo(input.IssuerKey), CommonName: input.CommonName, DNSNames: input.DNSNames, + KeyType: input.KeyType, CSR: input.CSR, Renew: input.RenewCert != nil, Err: niceError(err, provider.GetPendingTXTRecordError()), diff --git a/pkg/cert/legobridge/certificate_test.go b/pkg/cert/legobridge/certificate_test.go new file mode 100644 index 00000000..fc7b252f --- /dev/null +++ b/pkg/cert/legobridge/certificate_test.go @@ -0,0 +1,45 @@ +/* + * SPDX-FileCopyrightText: 2024 SAP SE or an SAP affiliate company and Gardener contributors + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package legobridge + +import ( + api "github.com/gardener/cert-management/pkg/apis/cert/v1alpha1" + + "github.com/go-acme/lego/v4/certcrypto" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = DescribeTable("KeyType conversion", + func(keyType certcrypto.KeyType, algorithm api.PrivateKeyAlgorithm, size int) { + var key *api.CertificatePrivateKey + if size >= 0 { + key = newCertificatePrivateKey(algorithm, api.PrivateKeySize(size)) + } + actualKeyType, err := ToKeyType(key) + if keyType == "" { + Expect(err).To(HaveOccurred()) + } else { + Expect(err).ToNot(HaveOccurred()) + Expect(actualKeyType).To(Equal(keyType)) + actualKeyType, err = ToKeyType(FromKeyType(keyType)) + Expect(err).ToNot(HaveOccurred()) + Expect(actualKeyType).To(Equal(keyType)) + } + }, + Entry("default", certcrypto.RSA2048, api.PrivateKeyAlgorithm(""), -1), + Entry("empty", certcrypto.RSA2048, api.PrivateKeyAlgorithm(""), 0), + Entry("RSA from empty config", certcrypto.RSA2048, api.RSAKeyAlgorithm, 0), + Entry("RSA2048", certcrypto.RSA2048, api.RSAKeyAlgorithm, 2048), + Entry("RSA3072", certcrypto.RSA3072, api.RSAKeyAlgorithm, 3072), + Entry("RSA4096", certcrypto.RSA4096, api.RSAKeyAlgorithm, 4096), + Entry("ECDSA with default size", certcrypto.EC256, api.ECDSAKeyAlgorithm, 0), + Entry("EC256", certcrypto.EC256, api.ECDSAKeyAlgorithm, 256), + Entry("EC384", certcrypto.EC384, api.ECDSAKeyAlgorithm, 384), + Entry("RSA with wrong size", certcrypto.KeyType(""), api.RSAKeyAlgorithm, 8192), + Entry("ECDSA with wrong size", certcrypto.KeyType(""), api.ECDSAKeyAlgorithm, 511), +) diff --git a/pkg/cert/source/controller.go b/pkg/cert/source/controller.go index 55ca4eda..8f711271 100644 --- a/pkg/cert/source/controller.go +++ b/pkg/cert/source/controller.go @@ -45,6 +45,21 @@ const ( // AnnotPreferredChain is the annotation for the certificate preferred chain AnnotPreferredChain = "cert.gardener.cloud/preferred-chain" + // AnnotPrivateKeyAlgorithm is the annotation key to set the PrivateKeyAlgorithm for a Certificate. + // If PrivateKeyAlgorithm is specified and `size` is not provided, + // key size of 256 will be used for `ECDSA` key algorithm and + // key size of 2048 will be used for `RSA` key algorithm. + // If unset an algorithm `RSA` will be used. + AnnotPrivateKeyAlgorithm = "cert.gardener.cloud/private-key-algorithm" + + // AnnotPrivateKeySize is the annotation key to set the size of the private key for a Certificate. + // If PrivateKeyAlgorithm is set to `RSA`, valid values are `2048`, `3072`, or `4096`, + // and will default to `2048` if not specified. + // If PrivateKeyAlgorithm is set to `ECDSA`, valid values are `256` or `384`, + // and will default to `256` if not specified. + // No other values are allowed. + AnnotPrivateKeySize = "cert.gardener.cloud/private-key-size" + // OptClass is the cert-class command line option OptClass = "cert-class" // OptTargetclass is the target-cert-class command line option diff --git a/pkg/cert/source/defaults.go b/pkg/cert/source/defaults.go index a21410fa..7ff6bb24 100644 --- a/pkg/cert/source/defaults.go +++ b/pkg/cert/source/defaults.go @@ -199,13 +199,23 @@ func (s *DefaultCertSource) GetCertsInfo(logger logger.LogContext, obj resources } preferredChain, _ := resources.GetAnnotation(obj.Data(), AnnotPreferredChain) + algorithm, _ := resources.GetAnnotation(obj.Data(), AnnotPrivateKeyAlgorithm) + keySize := 0 + if keySizeStr, ok := resources.GetAnnotation(obj.Data(), AnnotPrivateKeySize); ok { + if value, err := strconv.Atoi(keySizeStr); err == nil { + keySize = value + } + } + info.Certs[secretName] = CertInfo{ - SecretName: secretName, - Domains: annotatedDomains, - IssuerName: issuer, - FollowCNAME: followCNAME, - SecretLabels: ExtractSecretLabels(obj), - PreferredChain: preferredChain, + SecretName: secretName, + Domains: annotatedDomains, + IssuerName: issuer, + FollowCNAME: followCNAME, + SecretLabels: ExtractSecretLabels(obj), + PreferredChain: preferredChain, + PrivateKeyAlgorithm: algorithm, + PrivateKeySize: keySize, } return info, nil } diff --git a/pkg/cert/source/interface.go b/pkg/cert/source/interface.go index 49ac2e69..59129d55 100644 --- a/pkg/cert/source/interface.go +++ b/pkg/cert/source/interface.go @@ -20,12 +20,14 @@ import ( // CertInfo contains basic certificate data. type CertInfo struct { - SecretName string - Domains []string - IssuerName *string - FollowCNAME bool - SecretLabels map[string]string - PreferredChain string + SecretName string + Domains []string + IssuerName *string + FollowCNAME bool + SecretLabels map[string]string + PreferredChain string + PrivateKeyAlgorithm string + PrivateKeySize int } // CertsInfo contains a map of CertInfo. diff --git a/pkg/cert/source/reconciler.go b/pkg/cert/source/reconciler.go index 29e72fe4..7af8f742 100644 --- a/pkg/cert/source/reconciler.go +++ b/pkg/cert/source/reconciler.go @@ -14,6 +14,7 @@ import ( core "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/utils/ptr" "github.com/gardener/controller-manager-library/pkg/controllermanager/controller" "github.com/gardener/controller-manager-library/pkg/controllermanager/controller/reconcile" @@ -350,6 +351,8 @@ func (r *sourceReconciler) createEntryFor(logger logger.LogContext, obj resource cert.Spec.PreferredChain = &info.PreferredChain } + cert.Spec.PrivateKey = createPrivateKey(info.PrivateKeyAlgorithm, info.PrivateKeySize) + e, _ := r.SlaveResoures()[0].Wrap(cert) err := r.Slaves().CreateSlave(obj, e) @@ -451,6 +454,13 @@ func (r *sourceReconciler) updateEntry(logger logger.LogContext, info CertInfo, } mod.Modify(true) } + + newPrivateKey := createPrivateKey(info.PrivateKeyAlgorithm, info.PrivateKeySize) + if !reflect.DeepEqual(spec.PrivateKey, newPrivateKey) { + spec.PrivateKey = newPrivateKey + mod.Modify(true) + } + if mod.IsModified() { logger.Infof("update certificate object %s", obj.ObjectName()) } @@ -458,3 +468,17 @@ func (r *sourceReconciler) updateEntry(logger logger.LogContext, info CertInfo, } return obj.Modify(f) } + +func createPrivateKey(algorithm string, size int) *api.CertificatePrivateKey { + if algorithm == "" && size == 0 { + return nil + } + obj := &api.CertificatePrivateKey{} + if algorithm != "" { + obj.Algorithm = ptr.To(api.PrivateKeyAlgorithm(algorithm)) + } + if size != 0 { + obj.Size = ptr.To(api.PrivateKeySize(size)) + } + return obj +} diff --git a/pkg/controller/issuer/certificate/backupsecret.go b/pkg/controller/issuer/certificate/backupsecret.go index 805dcf7b..9c190abe 100644 --- a/pkg/controller/issuer/certificate/backupsecret.go +++ b/pkg/controller/issuer/certificate/backupsecret.go @@ -92,28 +92,6 @@ func BackupSecret( }, true, nil } -// FindAllCertificateSecretsByOldHashLabel get all certificate secrets by the old certificate hash -func FindAllCertificateSecretsByOldHashLabel(res resources.Interface, hashKey, altHashKey string) ([]resources.Object, error) { - opts := metav1.ListOptions{ - LabelSelector: fmt.Sprintf("%s=%s", LabelCertificateOldHashKey, hashKey), - } - objs, err := res.List(opts) - if err != nil { - return nil, err - } - opts = metav1.ListOptions{ - LabelSelector: fmt.Sprintf("%s=%s", LabelCertificateOldHashKey, altHashKey), - } - objs2, err := res.List(opts) - if err != nil { - return nil, err - } - if len(objs2) > 0 { - objs = append(objs, objs2...) - } - return objs, nil -} - // FindAllCertificateSecretsByNewHashLabel get all certificate secrets by the certificate hash func FindAllCertificateSecretsByNewHashLabel(res resources.Interface, hashKey string) ([]resources.Object, error) { opts := metav1.ListOptions{ diff --git a/pkg/controller/issuer/certificate/reconciler.go b/pkg/controller/issuer/certificate/reconciler.go index 33bacde7..d7b3b2b0 100644 --- a/pkg/controller/issuer/certificate/reconciler.go +++ b/pkg/controller/issuer/certificate/reconciler.go @@ -9,11 +9,13 @@ package certificate import ( "crypto/sha256" "crypto/x509" + "errors" "fmt" "reflect" "strings" "time" + "github.com/go-acme/lego/v4/certcrypto" "github.com/go-acme/lego/v4/certificate" corev1 "k8s.io/api/core/v1" apierrrors "k8s.io/apimachinery/pkg/api/errors" @@ -23,7 +25,7 @@ import ( "github.com/gardener/controller-manager-library/pkg/controllermanager/cluster" "github.com/gardener/controller-manager-library/pkg/controllermanager/controller" "github.com/gardener/controller-manager-library/pkg/controllermanager/controller/reconcile" - "github.com/gardener/controller-manager-library/pkg/errors" + cmlerrors "github.com/gardener/controller-manager-library/pkg/errors" "github.com/gardener/controller-manager-library/pkg/logger" "github.com/gardener/controller-manager-library/pkg/resources" cmlutils "github.com/gardener/controller-manager-library/pkg/utils" @@ -39,8 +41,6 @@ import ( ) const ( - // LabelCertificateOldHashKey is the old label for the certificate hash - LabelCertificateOldHashKey = api.GroupName + "/certificate-hash" // LabelCertificateNewHashKey is the new label for the certificate hash LabelCertificateNewHashKey = api.GroupName + "/hash" // LabelCertificateKey is the label for marking secrets created for a certificate @@ -234,7 +234,7 @@ func (r *certReconciler) reconcileCert(logctx logger.LogContext, obj resources.O if secretRef != nil { secret, err = r.loadSecret(secretRef) if err != nil { - if !apierrrors.IsNotFound(errors.Cause(err)) { + if !apierrrors.IsNotFound(cmlerrors.Cause(err)) { return r.failed(logctx, obj, api.StateError, err) } // ignore if SecretRef is specified but not existing @@ -252,21 +252,6 @@ func (r *certReconciler) reconcileCert(logctx logger.LogContext, obj resources.O return *status } return r.checkForRenewAndSucceeded(logctx, obj, secret) - } else if storedOldHash := cert.Labels[LabelCertificateOldHashKey]; storedOldHash != "" { - specOldHash := r.buildSpecOldHash(&cert.Spec, issuerKey, false) - specAltHash := r.buildSpecOldHash(&cert.Spec, issuerKey, true) - if specOldHash != storedOldHash && specAltHash != storedOldHash { - return r.removeStoredHashKeyAndRepeat(logctx, obj) - } - specNewHash := r.buildSpecNewHash(&cert.Spec, issuerKey) - return r.migrateSpecHash(logctx, obj, specOldHash, specAltHash, specNewHash) - } else if storedOldHash := secret.Labels[LabelCertificateOldHashKey]; storedOldHash != "" { - specOldHash := r.buildSpecOldHash(&cert.Spec, issuerKey, false) - specAltHash := r.buildSpecOldHash(&cert.Spec, issuerKey, true) - if specOldHash == storedOldHash || specAltHash == storedOldHash { - specNewHash := r.buildSpecNewHash(&cert.Spec, issuerKey) - return r.migrateSpecHash(logctx, obj, specOldHash, specAltHash, specNewHash) - } } // corner case: existing secret but no stored hash, check if renewal is overdue @@ -309,6 +294,7 @@ func (r *certReconciler) handleObtainOutput(logctx logger.LogContext, obj resour DNSNames: result.DNSNames, CSR: result.CSR, IssuerRef: &api.IssuerRef{Name: result.IssuerInfo.Key().Name(), Namespace: result.IssuerInfo.Key().Namespace()}, + PrivateKey: legobridge.FromKeyType(result.KeyType), } issuerKey := r.support.IssuerClusterObjectKey(cert.Namespace, spec) specHash := r.buildSpecNewHash(spec, issuerKey) @@ -478,16 +464,31 @@ func (r *certReconciler) obtainCertificateAndPendingACME(logctx logger.LogContex if cert.Spec.PreferredChain != nil { preferredChain = *cert.Spec.PreferredChain } - input := legobridge.ObtainInput{User: reguser, DNSSettings: dnsSettings, IssuerKey: issuerKey, - CommonName: cert.Spec.CommonName, DNSNames: cert.Spec.DNSNames, CSR: cert.Spec.CSR, - TargetClass: targetDNSClass, Callback: callback, RequestName: objectName, RenewCert: renewCert, - AlwaysDeactivateAuthorizations: r.alwaysDeactivateAuthorizations, PreferredChain: preferredChain, + keyType, err := legobridge.ToKeyType(cert.Spec.PrivateKey) + if err != nil { + return r.failed(logctx, obj, api.StateError, fmt.Errorf("invalid private key configuration: %w", err)) + } + input := legobridge.ObtainInput{ + User: reguser, + DNSSettings: dnsSettings, + IssuerKey: issuerKey, + CommonName: cert.Spec.CommonName, + DNSNames: cert.Spec.DNSNames, + CSR: cert.Spec.CSR, + TargetClass: targetDNSClass, + Callback: callback, + RequestName: objectName, + RenewCert: renewCert, + AlwaysDeactivateAuthorizations: r.alwaysDeactivateAuthorizations, + PreferredChain: preferredChain, + KeyType: keyType, } err = r.obtainer.Obtain(input) if err != nil { - switch err.(type) { - case *legobridge.ConcurrentObtainError: + var concurrentObtainError *legobridge.ConcurrentObtainError + switch { + case errors.As(err, &concurrentObtainError): return r.delay(logctx, obj, api.StatePending, err) default: return r.failed(logctx, obj, api.StateError, fmt.Errorf("preparing obtaining certificates failed: %w", err)) @@ -573,8 +574,9 @@ func (r *certReconciler) obtainCertificateCA(logctx logger.LogContext, obj resou err = r.obtainer.Obtain(input) if err != nil { - switch err.(type) { - case *legobridge.ConcurrentObtainError: + var concurrentObtainError *legobridge.ConcurrentObtainError + switch { + case errors.As(err, &concurrentObtainError): return r.delay(logctx, obj, api.StatePending, err) default: return r.failed(logctx, obj, api.StateError, fmt.Errorf("preparing obtaining certificates failed: %w", err)) @@ -592,7 +594,7 @@ func (r *certReconciler) validateDomainsAndCsr(spec *api.CertificateSpec, issuer return err } - names := sets.String{} + names := sets.Set[string]{} for _, name := range domainsToValidate { if names.Has(name) { return fmt.Errorf("duplicate domain: %s", name) @@ -740,32 +742,6 @@ func (r *certReconciler) updateForRenewalAndRepeat(logctx logger.LogContext, obj return nil } -func (r *certReconciler) buildSpecOldHash(spec *api.CertificateSpec, issuerKey utils.IssuerKey, useAltHash bool) string { - h := sha256.New224() - if spec.CommonName != nil { - h.Write([]byte(*spec.CommonName)) - h.Write([]byte{0}) - for _, domain := range spec.DNSNames { - h.Write([]byte(domain)) - h.Write([]byte{0}) - } - } - if spec.CSR != nil { - h.Write([]byte{0}) - h.Write(spec.CSR) - h.Write([]byte{0}) - } - var hash string - if !useAltHash { - hash = r.support.GetIssuerSecretHash(issuerKey) - } else { - hash = r.support.GetAltIssuerSecretHash(issuerKey) - } - h.Write([]byte(hash)) - h.Write([]byte{0}) - return fmt.Sprintf("%x", h.Sum(nil)) -} - func (r *certReconciler) buildSpecNewHash(spec *api.CertificateSpec, issuerKey utils.IssuerKey) string { h := sha256.New224() if spec.CommonName != nil { @@ -783,6 +759,10 @@ func (r *certReconciler) buildSpecNewHash(spec *api.CertificateSpec, issuerKey u } h.Write([]byte(issuerKey.String())) h.Write([]byte{0}) + if keyType, err := legobridge.ToKeyType(spec.PrivateKey); err == nil && keyType != certcrypto.RSA2048 { + h.Write([]byte(keyType)) + h.Write([]byte{0}) + } return fmt.Sprintf("%x", h.Sum(nil)) } @@ -832,17 +812,6 @@ func (r *certReconciler) findSecretByHashLabel(namespace string, spec *api.Certi if err != nil { return nil, "", nil } - updateLabels := false - if len(objs) == 0 { - // for migration v0.7.x to v0.8.x only - updateLabels = true - oldHash := r.buildSpecOldHash(spec, issuerKey, false) - altHash := r.buildSpecOldHash(spec, issuerKey, true) - objs, err = FindAllCertificateSecretsByOldHashLabel(r.certSecretResources, oldHash, altHash) - if err != nil { - return nil, "", nil - } - } secretRef, _ := r.determineSecretRef(namespace, spec) var best resources.Object @@ -864,12 +833,6 @@ func (r *certReconciler) findSecretByHashLabel(namespace string, spec *api.Certi secretRef != nil && bestNotAfter.Equal(cert.NotAfter) && obj.GetName() == secretRef.Name && core.NormalizeNamespace(obj.GetNamespace()) == core.NormalizeNamespace(secretRef.Namespace) { best = obj bestNotAfter = cert.NotAfter - - // for migration v0.7.x to v0.8.x only - if updateLabels { - obj.SetLabels(resources.AddLabel(obj.GetLabels(), LabelCertificateNewHashKey, specHash)) - _ = obj.Update() - } } } } @@ -894,8 +857,7 @@ func (r *certReconciler) copySecretIfNeeded(logctx logger.LogContext, issuerInfo } certificates := legobridge.SecretDataToCertificates(secret.Data) - var requestedAt *time.Time = ExtractRequestedAtFromAnnotation(secret) - + requestedAt := ExtractRequestedAtFromAnnotation(secret) return r.writeCertificateSecret(logctx, issuerInfo, objectMeta, certificates, specHash, specSecretRef, requestedAt, spec.Keystores, spec.SecretLabels) } @@ -997,7 +959,6 @@ func (r *certReconciler) updateSecretRefAndSucceeded(logctx logger.LogContext, o func (r *certReconciler) removeStoredHashKeyAndRepeat(logctx logger.LogContext, obj resources.Object) reconcile.Status { c := obj.Data().(*api.Certificate) - delete(c.Labels, LabelCertificateOldHashKey) delete(c.Labels, LabelCertificateNewHashKey) obj2, err := r.certResources.Update(c) if err != nil { @@ -1006,40 +967,6 @@ func (r *certReconciler) removeStoredHashKeyAndRepeat(logctx logger.LogContext, return r.repeat(logctx, obj2) } -func (r *certReconciler) migrateSpecHash(logctx logger.LogContext, obj resources.Object, specOldHash, specAltHash, specNewHash string) reconcile.Status { - logctx.Infof("migrating spec hash") - - crt := obj.Data().(*api.Certificate) - - objs, err := FindAllCertificateSecretsByOldHashLabel(r.certSecretResources, specOldHash, specAltHash) - if err != nil { - return r.failed(logctx, obj, api.StateError, fmt.Errorf("find all certificates by old hash failed: %w", err)) - } - - for _, secret := range objs { - if _, ok := resources.GetLabel(secret.Data(), LabelCertificateNewHashKey); !ok { - secret.SetLabels(resources.AddLabel(secret.GetLabels(), LabelCertificateNewHashKey, specNewHash)) - err = secret.Update() - if err != nil { - logctx.Warnf("updating label for certificate secret %s failed: %w", secret.ObjectName(), err) - } - } - } - - if crt.Labels == nil { - crt.Labels = map[string]string{} - } - crt.Labels[LabelCertificateNewHashKey] = specNewHash - _, err = r.certResources.Update(crt) - if err != nil { - return r.failed(logctx, obj, api.StateError, fmt.Errorf("updating certificate resource failed: %w", err)) - } - - time.Sleep(500 * time.Millisecond) - - return reconcile.Succeeded(logctx) -} - func (r *certReconciler) prepareUpdateStatus(obj resources.Object, state string, msg *string, mode backoffMode) (*resources.ModificationState, *api.CertificateStatus) { crt := obj.Data().(*api.Certificate) status := &crt.Status @@ -1106,6 +1033,10 @@ func (r *certReconciler) prepareUpdateStatus(obj resources.Object, state string, func (r *certReconciler) updateReadyCondition(mod *resources.ModificationState, oldConditions []metav1.Condition, state string, msg *string, observedGeneration int64) []metav1.Condition { + if state == "" { + // ignore intermediate state + return oldConditions + } oldReadyCondition := &metav1.Condition{ Type: api.CertificateConditionReady, LastTransitionTime: metav1.NewTime(time.Now()), @@ -1160,7 +1091,8 @@ func (r *certReconciler) failedStop(logctx logger.LogContext, obj resources.Obje func (r *certReconciler) status(logctx logger.LogContext, obj resources.Object, state string, err error, stop bool) reconcile.Status { msg := err.Error() - rerr, isRecoverable := err.(*core.RecoverableError) + var rerr *core.RecoverableError + isRecoverable := errors.As(err, &rerr) backoffMode := boNone if !isRecoverable { if stop { diff --git a/pkg/controller/source/ingress/handler.go b/pkg/controller/source/ingress/handler.go index 49aa96dc..b6bdf7e3 100644 --- a/pkg/controller/source/ingress/handler.go +++ b/pkg/controller/source/ingress/handler.go @@ -68,6 +68,13 @@ func (s *CIngressSource) GetCertsInfo(logger logger.LogContext, obj resources.Ob } preferredChain, _ := resources.GetAnnotation(obj.Data(), source.AnnotPreferredChain) + algorithm, _ := resources.GetAnnotation(obj.Data(), source.AnnotPrivateKeyAlgorithm) + keySize := 0 + if keySizeStr, ok := resources.GetAnnotation(obj.Data(), source.AnnotPrivateKeySize); ok { + if value, err := strconv.Atoi(keySizeStr); err == nil { + keySize = value + } + } cn, _ := resources.GetAnnotation(obj.Data(), source.AnnotCommonName) cn = strings.TrimSpace(cn) @@ -97,12 +104,14 @@ func (s *CIngressSource) GetCertsInfo(logger logger.LogContext, obj resources.Ob domains = mergeCommonName(cn, tls.Hosts) } info.Certs[tls.SecretName] = source.CertInfo{ - SecretName: tls.SecretName, - Domains: domains, - IssuerName: issuer, - FollowCNAME: followCNAME, - SecretLabels: source.ExtractSecretLabels(obj), - PreferredChain: preferredChain, + SecretName: tls.SecretName, + Domains: domains, + IssuerName: issuer, + FollowCNAME: followCNAME, + SecretLabels: source.ExtractSecretLabels(obj), + PreferredChain: preferredChain, + PrivateKeyAlgorithm: algorithm, + PrivateKeySize: keySize, } } return info, err diff --git a/test/functional/basics.go b/test/functional/basics.go index 17413ab3..56c63cb1 100644 --- a/test/functional/basics.go +++ b/test/functional/basics.go @@ -8,6 +8,7 @@ package functional import ( "context" + "crypto/x509" "time" . "github.com/onsi/ginkgo/v2" @@ -154,6 +155,62 @@ spec: secretName: cert5-secret issuerRef: name: {{.Name}} +--- +apiVersion: cert.gardener.cloud/v1alpha1 +kind: Certificate +metadata: + name: cert6 + namespace: {{.Namespace}} +spec: + commonName: cert6.{{.Domain}} + secretName: cert6-secret + issuerRef: + name: {{.Name}} + privateKey: + algorithm: RSA + size: 3072 +--- +apiVersion: cert.gardener.cloud/v1alpha1 +kind: Certificate +metadata: + name: cert7 + namespace: {{.Namespace}} +spec: + commonName: cert7.{{.Domain}} + secretName: cert7-secret + issuerRef: + name: {{.Name}} + privateKey: + algorithm: RSA + size: 4096 +--- +apiVersion: cert.gardener.cloud/v1alpha1 +kind: Certificate +metadata: + name: cert8 + namespace: {{.Namespace}} +spec: + commonName: cert8.{{.Domain}} + secretName: cert8-secret + issuerRef: + name: {{.Name}} + privateKey: + algorithm: ECDSA + size: 256 +--- +apiVersion: cert.gardener.cloud/v1alpha1 +kind: Certificate +metadata: + name: cert9 + namespace: {{.Namespace}} +spec: + commonName: cert9.{{.Domain}} + secretName: cert9-secret + issuerRef: + name: {{.Name}} + privateKey: + algorithm: ECDSA + size: 384 ` var revoke2Template = ` @@ -239,7 +296,7 @@ func functestbasics(cfg *config.Config, iss *config.IssuerConfig) { Ω(err).ShouldNot(HaveOccurred()) entryNames := []string{} - for _, name := range []string{"1", "2", "2b", "3", "5"} { + for _, name := range []string{"1", "2", "2b", "3", "5", "6", "7", "8", "9"} { entryNames = append(entryNames, entryName(iss, name)) } err = u.AwaitCertReady(entryNames...) @@ -297,8 +354,42 @@ func functestbasics(cfg *config.Config, iss *config.IssuerConfig) { "expirationDate": HavePrefix("20"), }), }), + entryName(iss, "6"): MatchKeys(IgnoreExtras, Keys{ + "status": MatchKeys(IgnoreExtras, Keys{ + "commonName": Equal(dnsName(iss, "cert6")), + "state": Equal("Ready"), + "expirationDate": HavePrefix("20"), + }), + }), + entryName(iss, "7"): MatchKeys(IgnoreExtras, Keys{ + "status": MatchKeys(IgnoreExtras, Keys{ + "commonName": Equal(dnsName(iss, "cert7")), + "state": Equal("Ready"), + "expirationDate": HavePrefix("20"), + }), + }), + entryName(iss, "8"): MatchKeys(IgnoreExtras, Keys{ + "status": MatchKeys(IgnoreExtras, Keys{ + "commonName": Equal(dnsName(iss, "cert8")), + "state": Equal("Ready"), + "expirationDate": HavePrefix("20"), + }), + }), + entryName(iss, "9"): MatchKeys(IgnoreExtras, Keys{ + "status": MatchKeys(IgnoreExtras, Keys{ + "commonName": Equal(dnsName(iss, "cert9")), + "state": Equal("Ready"), + "expirationDate": HavePrefix("20"), + }), + }), })) + Ω(u.CheckCertificatePrivateKey("cert3-secret", x509.RSA, 2048)).ShouldNot(HaveOccurred()) + Ω(u.CheckCertificatePrivateKey("cert6-secret", x509.RSA, 3072)).ShouldNot(HaveOccurred()) + Ω(u.CheckCertificatePrivateKey("cert7-secret", x509.RSA, 4096)).ShouldNot(HaveOccurred()) + Ω(u.CheckCertificatePrivateKey("cert8-secret", x509.ECDSA, 256)).ShouldNot(HaveOccurred()) + Ω(u.CheckCertificatePrivateKey("cert9-secret", x509.ECDSA, 384)).ShouldNot(HaveOccurred()) + By("check keystores in cert3", func() { secret, err := u.KubectlGetSecret("cert3-secret") Ω(err).ShouldNot(HaveOccurred()) diff --git a/test/functional/config/config.go b/test/functional/config/config.go index 032cb285..3273c8b2 100644 --- a/test/functional/config/config.go +++ b/test/functional/config/config.go @@ -8,7 +8,6 @@ package config import ( "fmt" - "io/ioutil" "os" "text/template" @@ -155,7 +154,7 @@ func (c *Config) postProcess() error { func (p *IssuerConfig) CreateTempManifest(name, templateContent string) (string, error) { tmpl, err := template.New(name).Parse(templateContent) - f, err := ioutil.TempFile("", fmt.Sprintf("%s-*.yaml", p.Name)) + f, err := os.CreateTemp("", fmt.Sprintf("%s-*.yaml", p.Name)) if err != nil { return "", err } diff --git a/test/functional/config/utils.go b/test/functional/config/utils.go index df519407..d8e8eb3d 100644 --- a/test/functional/config/utils.go +++ b/test/functional/config/utils.go @@ -7,12 +7,16 @@ package config import ( + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" "encoding/json" "fmt" "os/exec" "strings" "time" + "github.com/gardener/cert-management/pkg/cert/legobridge" "github.com/onsi/gomega" corev1 "k8s.io/api/core/v1" ) @@ -28,7 +32,7 @@ type TestUtils struct { func CreateDefaultTestUtils() *TestUtils { return &TestUtils{ - AwaitTimeout: 90 * time.Second, + AwaitTimeout: 120 * time.Second, PollingPeriod: 200 * time.Millisecond, Namespace: "default", Verbose: true, @@ -55,6 +59,36 @@ func (u *TestUtils) KubectlGetSecret(name string) (*corev1.Secret, error) { return secret, nil } +func (u *TestUtils) CheckCertificatePrivateKey(secretName string, algorithm x509.PublicKeyAlgorithm, keySize int) error { + secret, err := u.KubectlGetSecret(secretName) + if err != nil { + return err + } + cert, err := legobridge.DecodeCertificateFromSecretData(secret.Data) + if err != nil { + return err + } + if cert.PublicKeyAlgorithm != algorithm { + return fmt.Errorf("algorithm mismatch: %s != %s", cert.PublicKeyAlgorithm, algorithm) + } + + switch pub := cert.PublicKey.(type) { + case *rsa.PublicKey: + size := pub.N.BitLen() + if size != keySize { + return fmt.Errorf("key size mismatch: %d != %d", size, keySize) + } + case *ecdsa.PublicKey: + size := pub.Curve.Params().N.BitLen() + if size != keySize { + return fmt.Errorf("key size mismatch: %d != %d", size, keySize) + } + default: + return fmt.Errorf("unknown public key") + } + return nil +} + func (u *TestUtils) toItemMap(output string) (map[string]interface{}, error) { untyped := map[string]interface{}{} err := json.Unmarshal([]byte(output), &untyped)