From dd9888037864a3527adef6df2fbc07975937f6c9 Mon Sep 17 00:00:00 2001 From: Fabien Dupont Date: Wed, 4 Feb 2026 18:55:48 +0100 Subject: [PATCH] Add ECDSA certificate generation support via annotation Adds support for generating ECDSA P-256 certificates by specifying the key algorithm via the new service annotation: service.beta.openshift.io/serving-cert-key-algorithm: ecdsa When the annotation is not specified or set to "rsa", the operator generates RSA certificates for full backwards compatibility. This enables services to opt into modern elliptic curve cryptography, which provides equivalent security to 3072-bit RSA with significantly smaller keys (~87% smaller) and better performance. Implementation: - Added ServingCertKeyAlgorithmAnnotation constant in api.go - Modified MakeServingCert() to check annotation and select algorithm - Uses library-go's new MakeServerCertWithAlgorithm() API - Validates annotation values (rsa, ecdsa) with helpful error messages - Case-insensitive algorithm matching Testing: - Added TestECDSACertificateGeneration with 5 test cases - Verifies RSA (default and explicit), ECDSA, and invalid inputs - All existing tests pass with no regressions Depends on: openshift/library-go#2116 Co-authored-by: Claude Sonnet 4.5 Signed-off-by: Fabien Dupont --- pkg/controller/api/api.go | 3 + .../controller/secret_creating_controller.go | 19 ++- .../secret_creating_controller_test.go | 117 ++++++++++++++++++ 3 files changed, 138 insertions(+), 1 deletion(-) diff --git a/pkg/controller/api/api.go b/pkg/controller/api/api.go index 5aedd4fff..674545ee7 100644 --- a/pkg/controller/api/api.go +++ b/pkg/controller/api/api.go @@ -51,6 +51,9 @@ const ( // regeneration ServingCertCreatedByAnnotation = "service.beta.openshift.io/serving-cert-signed-by" AlphaServingCertCreatedByAnnotation = "service.alpha.openshift.io/serving-cert-signed-by" + // ServingCertKeyAlgorithmAnnotation specifies the key algorithm to use (rsa or ecdsa). + // If not specified, defaults to RSA for backwards compatibility. + ServingCertKeyAlgorithmAnnotation = "service.beta.openshift.io/serving-cert-key-algorithm" // ServingCertErrorAnnotation stores the error that caused cert generation failures. ServingCertErrorAnnotation = "service.beta.openshift.io/serving-cert-generation-error" AlphaServingCertErrorAnnotation = "service.alpha.openshift.io/serving-cert-generation-error" diff --git a/pkg/controller/servingcert/controller/secret_creating_controller.go b/pkg/controller/servingcert/controller/secret_creating_controller.go index abe30d4a2..c71280621 100644 --- a/pkg/controller/servingcert/controller/secret_creating_controller.go +++ b/pkg/controller/servingcert/controller/secret_creating_controller.go @@ -381,9 +381,26 @@ func certSubjectsForService(service *corev1.Service, dnsSuffix string) sets.Set[ func MakeServingCert(dnsSuffix string, ca *crypto.CA, intermediateCACert *x509.Certificate, service *corev1.Service, lifetime time.Duration) (*crypto.TLSCertificateConfig, error) { subjects := certSubjectsForService(service, dnsSuffix) - servingCert, err := ca.MakeServerCert( + + // Check for key algorithm annotation + algorithm := crypto.AlgorithmRSA // Default to RSA for backwards compatibility + if service.Annotations != nil { + if algoStr := service.Annotations[api.ServingCertKeyAlgorithmAnnotation]; algoStr != "" { + switch strings.ToLower(algoStr) { + case "ecdsa": + algorithm = crypto.AlgorithmECDSA + case "rsa": + algorithm = crypto.AlgorithmRSA + default: + return nil, fmt.Errorf("invalid key algorithm %q, must be 'rsa' or 'ecdsa'", algoStr) + } + } + } + + servingCert, err := ca.MakeServerCertWithAlgorithm( subjects, lifetime, + algorithm, cryptoextensions.ServiceServerCertificateExtensionV1(service.UID), ) if err != nil { diff --git a/pkg/controller/servingcert/controller/secret_creating_controller_test.go b/pkg/controller/servingcert/controller/secret_creating_controller_test.go index 2dfe1cd1c..db29a1c93 100644 --- a/pkg/controller/servingcert/controller/secret_creating_controller_test.go +++ b/pkg/controller/servingcert/controller/secret_creating_controller_test.go @@ -10,6 +10,7 @@ import ( "reflect" "strconv" "testing" + "time" corev1 "k8s.io/api/core/v1" kapierrors "k8s.io/apimachinery/pkg/api/errors" @@ -783,3 +784,119 @@ func newTestSyncContext(queueKey string) factory.SyncContext { eventRecorder: events.NewInMemoryRecorder("test", clock.RealClock{}), } } + +// TestECDSACertificateGeneration tests that ECDSA certificates can be generated via annotation +func TestECDSACertificateGeneration(t *testing.T) { + dnsSuffix := "cluster.local" + caLifetime := 365 * 24 * time.Hour + certLifetime := 180 * 24 * time.Hour + + ca, _, err := crypto.EnsureCA("/tmp/test-ca.crt", "/tmp/test-ca.key", "/tmp/test-ca.serial", signerName, caLifetime) + if err != nil { + t.Fatalf("failed to create CA: %v", err) + } + + tests := []struct { + name string + algorithmAnnotation string + expectedKeyType string + expectError bool + }{ + { + name: "RSA certificate (default)", + algorithmAnnotation: "", + expectedKeyType: "RSA", + expectError: false, + }, + { + name: "RSA certificate (explicit)", + algorithmAnnotation: "rsa", + expectedKeyType: "RSA", + expectError: false, + }, + { + name: "ECDSA certificate", + algorithmAnnotation: "ecdsa", + expectedKeyType: "ECDSA", + expectError: false, + }, + { + name: "ECDSA certificate (case insensitive)", + algorithmAnnotation: "ECDSA", + expectedKeyType: "ECDSA", + expectError: false, + }, + { + name: "Invalid algorithm", + algorithmAnnotation: "invalid", + expectedKeyType: "", + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + service := &corev1.Service{ + ObjectMeta: metav1.ObjectMeta{ + Name: testServiceName, + Namespace: testNamespace, + UID: testServiceUID, + Annotations: map[string]string{ + api.ServingCertSecretAnnotation: testSecretName, + }, + }, + } + + if tt.algorithmAnnotation != "" { + service.Annotations[api.ServingCertKeyAlgorithmAnnotation] = tt.algorithmAnnotation + } + + servingCert, err := MakeServingCert(dnsSuffix, ca, nil, service, certLifetime) + + if tt.expectError { + if err == nil { + t.Errorf("expected error, got none") + } + return + } + + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Verify certificate was generated + if servingCert == nil { + t.Fatal("servingCert is nil") + } + + if len(servingCert.Certs) == 0 { + t.Fatal("no certificates generated") + } + + // Verify key type + cert := servingCert.Certs[0] + switch tt.expectedKeyType { + case "RSA": + if cert.PublicKeyAlgorithm != x509.RSA { + t.Errorf("expected RSA public key algorithm, got %v", cert.PublicKeyAlgorithm) + } + case "ECDSA": + if cert.PublicKeyAlgorithm != x509.ECDSA { + t.Errorf("expected ECDSA public key algorithm, got %v", cert.PublicKeyAlgorithm) + } + } + + // Verify certificate subjects include service name + foundServiceName := false + for _, dnsName := range cert.DNSNames { + if dnsName == fmt.Sprintf("%s.%s.svc", testServiceName, testNamespace) { + foundServiceName = true + break + } + } + if !foundServiceName { + t.Errorf("certificate DNS names %v do not include expected service name", cert.DNSNames) + } + }) + } +}