Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions pkg/controller/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"reflect"
"strconv"
"testing"
"time"

corev1 "k8s.io/api/core/v1"
kapierrors "k8s.io/apimachinery/pkg/api/errors"
Expand Down Expand Up @@ -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)
}
})
}
}