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) + } + }) + } +}