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 @@ -151,7 +151,7 @@ func (sc *serviceServingCertController) generateCert(ctx context.Context, servic

secret := toBaseSecret(serviceCopy)
if err := toRequiredSecret(sc.dnsSuffix, sc.ca, sc.intermediateCACert, serviceCopy, secret, sc.certificateLifetime); err != nil {
return err
return sc.updateServiceFailure(ctx, serviceCopy, err)
}
setSecretOwnerDescription(secret, serviceCopy)

Expand Down 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 @@ -462,6 +463,24 @@ func TestServiceServingCertControllerSync(t *testing.T) {
secretAnnotations: map[string]string{},
secretData: []byte(testCertUnknownIssuer),
},
{
name: "invalid key algorithm annotation",
secretName: testSecretName,
serviceAnnotations: map[string]string{
api.ServingCertSecretAnnotation: testSecretName,
api.ServingCertKeyAlgorithmAnnotation: "invalid-algo",
},
expectedServiceAnnotations: map[string]string{
api.ServingCertSecretAnnotation: testSecretName,
api.ServingCertKeyAlgorithmAnnotation: "invalid-algo",
api.ServingCertErrorAnnotation: "invalid key algorithm \"invalid-algo\", must be 'rsa' or 'ecdsa'",
api.AlphaServingCertErrorAnnotation: "invalid key algorithm \"invalid-algo\", must be 'rsa' or 'ecdsa'",
api.ServingCertErrorNumAnnotation: "1",
api.AlphaServingCertErrorNumAnnotation: "1",
},
updateService: true,
updateSecret: false, // Secret should not be created
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -783,3 +802,126 @@ 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

caDir := t.TempDir()
ca, _, err := crypto.EnsureCA(
path.Join(caDir, "test-ca.crt"),
path.Join(caDir, "test-ca.key"),
path.Join(caDir, "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)
}
})
}
}
17 changes: 16 additions & 1 deletion pkg/operator/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@ package operator

import (
"encoding/json"
"strings"
"time"

"github.com/openshift/library-go/pkg/crypto"
)

type unsupportedServiceCAConfig struct {
Expand All @@ -19,6 +22,17 @@ type caConfig struct {
// months.
// +optional
ValidityDurationForTesting time.Duration `json:"validityDurationForTesting"`
// keyAlgorithm specifies the key algorithm to use for the CA
// (rsa or ecdsa). Defaults to RSA if unspecified.
// +optional
KeyAlgorithm string `json:"keyAlgorithm"`
}

func (c caConfig) cryptoKeyAlgorithm() crypto.KeyAlgorithm {
if strings.EqualFold(c.KeyAlgorithm, "ecdsa") {
return crypto.AlgorithmECDSA
}
return crypto.AlgorithmRSA
}

type forceRotationConfig struct {
Expand All @@ -42,10 +56,11 @@ func loadUnsupportedServiceCAConfig(raw []byte) (unsupportedServiceCAConfig, err
// RawUnsupportedServiceCAConfig returns the raw value of the operator
// field UnsupportedConfigOverrides for the given force rotation
// reason.
func RawUnsupportedServiceCAConfig(reason string, duration time.Duration) ([]byte, error) {
func RawUnsupportedServiceCAConfig(reason string, duration time.Duration, algorithm string) ([]byte, error) {
config := &unsupportedServiceCAConfig{
CAConfig: caConfig{
ValidityDurationForTesting: duration,
KeyAlgorithm: algorithm,
},
ForceRotation: forceRotationConfig{
Reason: reason,
Expand Down
36 changes: 25 additions & 11 deletions pkg/operator/rotate.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package operator

import (
stdcrypto "crypto"
"crypto/ecdsa"
"crypto/rsa"
"crypto/x509"
"fmt"
Expand Down Expand Up @@ -82,12 +84,7 @@ func maybeRotateSigningSecret(secret *corev1.Secret, currentCACert *x509.Certifi
return "", fmt.Errorf("failed to parse private key from PEM: %v", err)
}

rsaKey, ok := key.(*rsa.PrivateKey)
if !ok {
return "", fmt.Errorf("expected RSA private key, got %T", key)
}

signingCA, err := rotateSigningCA(currentCACert, rsaKey, minimumTrustDuration, signingCertificateLifetime)
signingCA, err := rotateSigningCA(currentCACert, key.(stdcrypto.PrivateKey), minimumTrustDuration, signingCertificateLifetime)
if err != nil {
return "", err
}
Expand All @@ -111,9 +108,14 @@ func maybeRotateSigningSecret(secret *corev1.Secret, currentCACert *x509.Certifi
// rotateSigningCA creates a new signing CA, bundle and intermediate CA that together can
// be used to ensure that serving certs generated both before and after rotation can be
// trusted by both refreshed and unrefreshed consumers.
func rotateSigningCA(currentCACert *x509.Certificate, currentKey *rsa.PrivateKey, minimumTrustDuration time.Duration, signingCertificateLifetime time.Duration) (*signingCA, error) {
// Generate a new signing cert
newCAConfig, err := crypto.MakeSelfSignedCAConfigForSubject(currentCACert.Subject, signingCertificateLifetime)
func rotateSigningCA(currentCACert *x509.Certificate, currentKey stdcrypto.PrivateKey, minimumTrustDuration time.Duration, signingCertificateLifetime time.Duration) (*signingCA, error) {
// Generate a new signing cert with the same algorithm as the current key
algorithm, err := algorithmFromKey(currentKey)
if err != nil {
return nil, err
}
newCAConfig, err := crypto.MakeSelfSignedCAConfigForDurationWithAlgorithm(
currentCACert.Subject.CommonName, signingCertificateLifetime, algorithm)
if err != nil {
Comment thread
coderabbitai[bot] marked this conversation as resolved.
return nil, err
}
Expand All @@ -131,7 +133,7 @@ func rotateSigningCA(currentCACert *x509.Certificate, currentKey *rsa.PrivateKey
// generated by the current CA for inclusion in the new CA bundle. This will ensure
// that clients with a post-rotation ca bundle will be able to trust pre-rotation
// serving certs.
currentCACertSignedByNewCA, err := createIntermediateCACert(currentCACert, newCACert, newCAConfig.Key.(*rsa.PrivateKey), currentCACertExpiry)
currentCACertSignedByNewCA, err := createIntermediateCACert(currentCACert, newCACert, newCAConfig.Key, currentCACertExpiry)
if err != nil {
return nil, fmt.Errorf("failed to create intermediate certificate signed by new ca: %v", err)
}
Expand Down Expand Up @@ -161,7 +163,7 @@ func rotateSigningCA(currentCACert *x509.Certificate, currentKey *rsa.PrivateKey
// createIntermediateCACert creates a new intermediate CA cert from a template provided by
// the target CA cert and issued by the signing cert. This ensures that certificates
// issued by the target CA can be trusted by clients that trust the signing CA.
func createIntermediateCACert(targetCACert, signingCACert *x509.Certificate, signingKey *rsa.PrivateKey, expiry *time.Time) (*x509.Certificate, error) {
func createIntermediateCACert(targetCACert, signingCACert *x509.Certificate, signingKey stdcrypto.PrivateKey, expiry *time.Time) (*x509.Certificate, error) {
// Copy the target cert to allow modification.
template, err := x509.ParseCertificate(targetCACert.Raw)
if err != nil {
Expand Down Expand Up @@ -196,6 +198,18 @@ func createIntermediateCACert(targetCACert, signingCACert *x509.Certificate, sig
return caCert, nil
}

// algorithmFromKey detects the key algorithm from a private key.
func algorithmFromKey(key stdcrypto.PrivateKey) (crypto.KeyAlgorithm, error) {
switch key.(type) {
case *rsa.PrivateKey:
return crypto.AlgorithmRSA, nil
case *ecdsa.PrivateKey:
return crypto.AlgorithmECDSA, nil
default:
return crypto.AlgorithmRSA, fmt.Errorf("unsupported private key type %T", key)
}
}

// forcedRotationRequired indicates whether the force rotation reason is not empty and
// does not match the annotation stored on the signing secret.
func forcedRotationRequired(secret *corev1.Secret, reason string) bool {
Expand Down
12 changes: 9 additions & 3 deletions pkg/operator/sync_common.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func (c *serviceCAOperator) manageSignerCA(ctx context.Context, rawUnsupportedSe
if existingCert == nil {
// Secret does not exist or lacks the expected cert.
validityDuration := serviceCAConfig.CAConfig.ValidityDurationForTesting
if err := initializeSigningSecret(secret, validityDuration, c.signingCertificateLifetime); err != nil {
if err := initializeSigningSecret(secret, validityDuration, c.signingCertificateLifetime, serviceCAConfig.CAConfig.cryptoKeyAlgorithm()); err != nil {
return false, err
}
} else {
Expand Down Expand Up @@ -142,11 +142,17 @@ func (c *serviceCAOperator) manageSignerCA(ctx context.Context, rawUnsupportedSe
// PEM-encoded certificate and private key of a new self-signed
// CA. The duration, if non-zero, will be used to set the
// expiry of the CA.
func initializeSigningSecret(secret *corev1.Secret, duration time.Duration, lifetime time.Duration) error {
func initializeSigningSecret(secret *corev1.Secret, duration time.Duration, lifetime time.Duration, algorithm crypto.KeyAlgorithm) error {
name := serviceServingCertSignerName()
klog.V(4).Infof("generating signing CA: %s", name)

ca, err := crypto.MakeSelfSignedCAConfig(name, lifetime)
var ca *crypto.TLSCertificateConfig
var err error
if algorithm == crypto.AlgorithmECDSA {
ca, err = crypto.MakeSelfSignedCAConfigForDurationWithAlgorithm(name, lifetime, algorithm)
} else {
ca, err = crypto.MakeSelfSignedCAConfig(name, lifetime)
}
if err != nil {
return err
}
Expand Down
4 changes: 3 additions & 1 deletion pkg/operator/sync_common_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ func TestInitializeSigningSecret(t *testing.T) {
t.Run(testName, func(t *testing.T) {
now := time.Now()
secret := &corev1.Secret{}
initializeSigningSecret(secret, 0, tc.duration)
if err := initializeSigningSecret(secret, 0, tc.duration, crypto.AlgorithmRSA); err != nil {
t.Fatalf("initializeSigningSecret failed: %v", err)
}

// Check that the initialized key pair is valid
rawCert := secret.Data[corev1.TLSCertKey]
Expand Down
Loading