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..ea7b9d8b7 100644 --- a/pkg/controller/servingcert/controller/secret_creating_controller.go +++ b/pkg/controller/servingcert/controller/secret_creating_controller.go @@ -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) @@ -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..cecd819fd 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" @@ -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 { @@ -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) + } + }) + } +} diff --git a/pkg/operator/config.go b/pkg/operator/config.go index 9d54f1664..fc47c204a 100644 --- a/pkg/operator/config.go +++ b/pkg/operator/config.go @@ -2,7 +2,10 @@ package operator import ( "encoding/json" + "strings" "time" + + "github.com/openshift/library-go/pkg/crypto" ) type unsupportedServiceCAConfig struct { @@ -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 { @@ -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, diff --git a/pkg/operator/rotate.go b/pkg/operator/rotate.go index 2c951e8d8..7a842d2ad 100644 --- a/pkg/operator/rotate.go +++ b/pkg/operator/rotate.go @@ -1,6 +1,8 @@ package operator import ( + stdcrypto "crypto" + "crypto/ecdsa" "crypto/rsa" "crypto/x509" "fmt" @@ -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 } @@ -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 { return nil, err } @@ -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) } @@ -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 { @@ -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 { diff --git a/pkg/operator/sync_common.go b/pkg/operator/sync_common.go index 1d4b2c2dd..1aaf48ddf 100644 --- a/pkg/operator/sync_common.go +++ b/pkg/operator/sync_common.go @@ -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 { @@ -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 } diff --git a/pkg/operator/sync_common_test.go b/pkg/operator/sync_common_test.go index 8f4393122..6d15e517a 100644 --- a/pkg/operator/sync_common_test.go +++ b/pkg/operator/sync_common_test.go @@ -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] diff --git a/test/e2e/e2e.go b/test/e2e/e2e.go index e6adf2e2b..868cc5ba6 100644 --- a/test/e2e/e2e.go +++ b/test/e2e/e2e.go @@ -9,6 +9,7 @@ import ( "math/rand" "os" "reflect" + "strings" "testing" "time" @@ -94,6 +95,24 @@ var _ = g.Describe("[sig-service-ca] service-ca-operator", func() { }) }) + g.Context("serving-cert-annotation-ecdsa", func() { + g.It("[Operator][Serial] should provision ECDSA certificates for services", func() { + testServingCertAnnotationWithAlgorithm(g.GinkgoTB(), "ecdsa") + }) + }) + + g.Context("serving-cert-annotation-invalid-algorithm", func() { + g.It("[Operator][Serial] should reject invalid algorithm annotation", func() { + testServingCertAnnotationInvalidAlgorithm(g.GinkgoTB()) + }) + }) + + g.Context("serving-cert-secret-regeneration-ecdsa", func() { + g.It("[Operator][Serial] should regenerate deleted ECDSA serving cert secrets", func() { + testServingCertSecretDeleteDataWithAlgorithm(g.GinkgoTB()) + }) + }) + g.Context("metrics", func() { g.It("[Operator][Serial] should collect metrics from the operator", func() { testMetricsCollection(g.GinkgoTB()) @@ -147,6 +166,220 @@ func testServingCertAnnotation(t testing.TB, headless bool) { } } +// testServingCertAnnotationWithAlgorithm checks that services with the +// serving-cert annotation and a key algorithm annotation get TLS certificates +// provisioned with the specified algorithm. +func testServingCertAnnotationWithAlgorithm(t testing.TB, algorithm string) { + adminClient, err := getKubeClient() + if err != nil { + t.Fatalf("error getting kube client: %v", err) + } + + ns, cleanup, err := createTestNamespace(t, adminClient, "test-"+randSeq(5)) + if err != nil { + t.Fatalf("could not create test namespace: %v", err) + } + defer cleanup() + + testServiceName := "test-service-" + randSeq(5) + testSecretName := "test-secret-" + randSeq(5) + + err = createServingCertAnnotatedServiceWithAlgorithm(adminClient, testSecretName, testServiceName, ns.Name, algorithm) + if err != nil { + t.Fatalf("error creating annotated service: %v", err) + } + + err = pollForServiceServingSecret(adminClient, testSecretName, ns.Name) + if err != nil { + t.Fatalf("error fetching created serving cert secret: %v", err) + } + + certBytes, is509, err := checkServiceServingCertSecretData(adminClient, testSecretName, ns.Name) + if err != nil { + t.Fatalf("error when checking serving cert secret: %v", err) + } + if !is509 { + t.Fatalf("TLSCertKey not valid pem bytes") + } + + block, _ := pem.Decode(certBytes) + if block == nil { + t.Fatalf("failed to decode PEM block from cert") + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatalf("failed to parse certificate: %v", err) + } + var expected x509.PublicKeyAlgorithm + switch { + case strings.EqualFold(algorithm, "ecdsa"): + expected = x509.ECDSA + case strings.EqualFold(algorithm, "rsa"): + expected = x509.RSA + default: + t.Fatalf("unsupported algorithm for test expectation: %q", algorithm) + } + if cert.PublicKeyAlgorithm != expected { + t.Fatalf("expected %v public key algorithm, got %v", expected, cert.PublicKeyAlgorithm) + } + if len(cert.DNSNames) == 0 { + t.Fatalf("expected DNS names in certificate, got none") + } +} + +// testServingCertAnnotationInvalidAlgorithm checks that an invalid algorithm +// annotation causes an error annotation on the service and the secret is not created. +func testServingCertAnnotationInvalidAlgorithm(t testing.TB) { + adminClient, err := getKubeClient() + if err != nil { + t.Fatalf("error getting kube client: %v", err) + } + + ns, cleanup, err := createTestNamespace(t, adminClient, "test-"+randSeq(5)) + if err != nil { + t.Fatalf("could not create test namespace: %v", err) + } + defer cleanup() + + testServiceName := "test-service-" + randSeq(5) + testSecretName := "test-secret-" + randSeq(5) + + err = createServingCertAnnotatedServiceWithAlgorithm(adminClient, testSecretName, testServiceName, ns.Name, "invalid-algo") + if err != nil { + t.Fatalf("error creating annotated service: %v", err) + } + + // Poll for the error annotation on the service + err = wait.PollImmediate(time.Second, pollTimeout, func() (bool, error) { + svc, err := adminClient.CoreV1().Services(ns.Name).Get(context.TODO(), testServiceName, metav1.GetOptions{}) + if err != nil { + return false, err + } + errAnnotation := svc.Annotations[api.ServingCertErrorAnnotation] + return len(errAnnotation) > 0, nil + }) + if err != nil { + t.Fatalf("error waiting for error annotation on service: %v", err) + } + + // Verify the secret is not created over a short stabilization window. + err = wait.PollImmediate(time.Second, 10*time.Second, func() (bool, error) { + _, err := adminClient.CoreV1().Secrets(ns.Name).Get(context.TODO(), testSecretName, metav1.GetOptions{}) + if errors.IsNotFound(err) { + return false, nil + } + if err != nil { + return false, err + } + return false, fmt.Errorf("secret %s/%s was unexpectedly created", ns.Name, testSecretName) + }) + if err != nil && err != wait.ErrWaitTimeout { + t.Fatalf("invalid algorithm should not produce a secret: %v", err) + } +} + +// testServingCertSecretDeleteDataWithAlgorithm checks that corrupting the +// tls.crt data of an ECDSA serving cert secret triggers regeneration and +// the regenerated cert is still ECDSA. +func testServingCertSecretDeleteDataWithAlgorithm(t testing.TB) { + adminClient, err := getKubeClient() + if err != nil { + t.Fatalf("error getting kube client: %v", err) + } + + ns, cleanup, err := createTestNamespace(t, adminClient, "test-"+randSeq(5)) + if err != nil { + t.Fatalf("could not create test namespace: %v", err) + } + defer cleanup() + + testServiceName := "test-service-" + randSeq(5) + testSecretName := "test-secret-" + randSeq(5) + + err = createServingCertAnnotatedServiceWithAlgorithm(adminClient, testSecretName, testServiceName, ns.Name, "ecdsa") + if err != nil { + t.Fatalf("error creating annotated service: %v", err) + } + + err = pollForServiceServingSecret(adminClient, testSecretName, ns.Name) + if err != nil { + t.Fatalf("error fetching created serving cert secret: %v", err) + } + + certBytes, is509, err := checkServiceServingCertSecretData(adminClient, testSecretName, ns.Name) + if err != nil { + t.Fatalf("error when checking serving cert secret: %v", err) + } + if !is509 { + t.Fatalf("TLSCertKey not valid pem bytes") + } + + // Verify initial cert is ECDSA + block, _ := pem.Decode(certBytes) + if block == nil { + t.Fatalf("failed to decode PEM block from cert") + } + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatalf("failed to parse certificate: %v", err) + } + if cert.PublicKeyAlgorithm != x509.ECDSA { + t.Fatalf("expected ECDSA public key algorithm, got %v", cert.PublicKeyAlgorithm) + } + + // Corrupt the tls.crt data to trigger regeneration + err = editServingSecretDataGinkgo(t, adminClient, testSecretName, ns.Name, "tls.crt") + if err != nil { + t.Fatalf("error editing serving cert secret: %v", err) + } + + // Verify regenerated cert is still ECDSA + regeneratedBytes, is509, err := checkServiceServingCertSecretData(adminClient, testSecretName, ns.Name) + if err != nil { + t.Fatalf("error when checking regenerated serving cert secret: %v", err) + } + if !is509 { + t.Fatalf("regenerated TLSCertKey not valid pem bytes") + } + + block, _ = pem.Decode(regeneratedBytes) + if block == nil { + t.Fatalf("failed to decode PEM block from regenerated cert") + } + cert, err = x509.ParseCertificate(block.Bytes) + if err != nil { + t.Fatalf("failed to parse regenerated certificate: %v", err) + } + if cert.PublicKeyAlgorithm != x509.ECDSA { + t.Fatalf("expected ECDSA public key algorithm after regeneration, got %v", cert.PublicKeyAlgorithm) + } +} + +// createServingCertAnnotatedServiceWithAlgorithm creates a service with the +// serving-cert annotation and a key algorithm annotation. +func createServingCertAnnotatedServiceWithAlgorithm(client *kubernetes.Clientset, secretName, serviceName, namespace, algorithm string) error { + service := &v1.Service{ + TypeMeta: metav1.TypeMeta{}, + ObjectMeta: metav1.ObjectMeta{ + Name: serviceName, + Annotations: map[string]string{ + api.ServingCertSecretAnnotation: secretName, + api.ServingCertKeyAlgorithmAnnotation: algorithm, + }, + }, + Spec: v1.ServiceSpec{ + Ports: []v1.ServicePort{ + { + Name: "tests", + Port: 8443, + }, + }, + }, + } + _, err := client.CoreV1().Services(namespace).Create(context.TODO(), service, metav1.CreateOptions{}) + return err +} + // getKubeClient returns a Kubernetes client for e2e tests. // It uses /tmp/admin.conf (placed by ci-operator) or KUBECONFIG env. func getKubeClient() (*kubernetes.Clientset, error) { diff --git a/test/e2e/e2e_test.go b/test/e2e/e2e_test.go index 4fe9d75e6..dd7df6861 100644 --- a/test/e2e/e2e_test.go +++ b/test/e2e/e2e_test.go @@ -318,7 +318,7 @@ func forceUnsupportedServiceCAConfigRotation(t *testing.T, config *rest.Config, break } } - rawUnsupportedServiceCAConfig, err := operator.RawUnsupportedServiceCAConfig(forceRotationReason, validityDuration) + rawUnsupportedServiceCAConfig, err := operator.RawUnsupportedServiceCAConfig(forceRotationReason, validityDuration, "") if err != nil { t.Fatalf("failed to create raw unsupported config overrides: %v", err) } diff --git a/vendor/github.com/openshift/library-go/pkg/crypto/crypto.go b/vendor/github.com/openshift/library-go/pkg/crypto/crypto.go index 69bac6637..119e70bac 100644 --- a/vendor/github.com/openshift/library-go/pkg/crypto/crypto.go +++ b/vendor/github.com/openshift/library-go/pkg/crypto/crypto.go @@ -4,12 +4,14 @@ import ( "bytes" "crypto" "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/sha1" "crypto/tls" "crypto/x509" "crypto/x509/pkix" + "encoding/asn1" "encoding/pem" "errors" "fmt" @@ -30,6 +32,8 @@ import ( "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/client-go/util/cert" + + configv1 "github.com/openshift/api/config/v1" ) // TLS versions that are known to golang. Go 1.13 adds support for @@ -110,15 +114,6 @@ func DefaultTLSVersion() uint16 { return tls.VersionTLS12 } -// ciphersTLS13 copies golang 1.13 implementation, where TLS1.3 suites are not -// configurable (cipherSuites field is ignored for TLS1.3 flows and all of the -// below three - and none other - are used) -var ciphersTLS13 = map[string]uint16{ - "TLS_AES_128_GCM_SHA256": tls.TLS_AES_128_GCM_SHA256, - "TLS_AES_256_GCM_SHA384": tls.TLS_AES_256_GCM_SHA384, - "TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256, -} - var ciphers = map[string]uint16{ "TLS_RSA_WITH_RC4_128_SHA": tls.TLS_RSA_WITH_RC4_128_SHA, "TLS_RSA_WITH_3DES_EDE_CBC_SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, @@ -144,10 +139,17 @@ var ciphers = map[string]uint16{ "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, + "TLS_AES_128_GCM_SHA256": tls.TLS_AES_128_GCM_SHA256, + "TLS_AES_256_GCM_SHA384": tls.TLS_AES_256_GCM_SHA384, + "TLS_CHACHA20_POLY1305_SHA256": tls.TLS_CHACHA20_POLY1305_SHA256, } // openSSLToIANACiphersMap maps OpenSSL cipher suite names to IANA names -// ref: https://www.iana.org/assignments/tls-parameters/tls-parameters.xml +// Ref: https://www.iana.org/assignments/tls-parameters/tls-parameters.xml +// This must hold a 1:1 mapping for each OpenSSL cipher defined in openshift/api TLSSecurityProfiles, +// so it can be used to translate OpenSSL ciphers to IANA ciphers, which is what go's crypto/tls understands. +// Ciphers in this map must also be compatible with go's crypto/tls ciphers: +// https://github.com/golang/go/blob/d4febb45179fa99ee1d5783bcb693ed7ba14115c/src/crypto/tls/cipher_suites.go#L682-L724 var openSSLToIANACiphersMap = map[string]string{ // TLS 1.3 ciphers - not configurable in go 1.13, all of them are used in TLSv1.3 flows "TLS_AES_128_GCM_SHA256": "TLS_AES_128_GCM_SHA256", // 0x13,0x01 @@ -167,6 +169,21 @@ var openSSLToIANACiphersMap = map[string]string{ "AES256-GCM-SHA384": "TLS_RSA_WITH_AES_256_GCM_SHA384", // 0x00,0x9D "AES128-SHA256": "TLS_RSA_WITH_AES_128_CBC_SHA256", // 0x00,0x3C + // Go's crypto/tls does not support CBC mode and DHE ciphers, so we don't want to include them here. + // See: + // - https://github.com/golang/go/issues/26652 + // - https://github.com/golang/go/issues/7758 + // - https://redhat-internal.slack.com/archives/C098FU5MRAB/p1770309657097269 + // + // "ECDHE-ECDSA-AES256-SHA384": "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384", // 0xC0,0x24 + // "ECDHE-RSA-AES256-SHA384": "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", // 0xC0,0x28 + // "AES256-SHA256": "TLS_RSA_WITH_AES_256_CBC_SHA256", // 0x00,0x3D + // "DHE-RSA-AES128-GCM-SHA256": "TLS_DHE_RSA_WITH_AES_128_GCM_SHA256", // 0x00,0x9E + // "DHE-RSA-AES256-GCM-SHA384": "TLS_DHE_RSA_WITH_AES_256_GCM_SHA384", // 0x00,0x9F + // "DHE-RSA-CHACHA20-POLY1305": "TLS_DHE_RSA_WITH_CHACHA20_POLY1305_SHA256", // 0xCC,0xAA + // "DHE-RSA-AES128-SHA256": "TLS_DHE_RSA_WITH_AES_128_CBC_SHA256", // 0x00,0x67 + // "DHE-RSA-AES256-SHA256": "TLS_DHE_RSA_WITH_AES_256_CBC_SHA256", // 0x00,0x6B + // TLS 1 "ECDHE-ECDSA-AES128-SHA": "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA", // 0xC0,0x09 "ECDHE-RSA-AES128-SHA": "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", // 0xC0,0x13 @@ -174,9 +191,10 @@ var openSSLToIANACiphersMap = map[string]string{ "ECDHE-RSA-AES256-SHA": "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA", // 0xC0,0x14 // SSL 3 - "AES128-SHA": "TLS_RSA_WITH_AES_128_CBC_SHA", // 0x00,0x2F - "AES256-SHA": "TLS_RSA_WITH_AES_256_CBC_SHA", // 0x00,0x35 - "DES-CBC3-SHA": "TLS_RSA_WITH_3DES_EDE_CBC_SHA", // 0x00,0x0A + "AES128-SHA": "TLS_RSA_WITH_AES_128_CBC_SHA", // 0x00,0x2F + "AES256-SHA": "TLS_RSA_WITH_AES_256_CBC_SHA", // 0x00,0x35 + "DES-CBC3-SHA": "TLS_RSA_WITH_3DES_EDE_CBC_SHA", // 0x00,0x0A + "ECDHE-RSA-DES-CBC3-SHA": "TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA", // 0xC0,0x12 } // CipherSuitesToNamesOrDie given a list of cipher suites as ints, return their readable names @@ -223,10 +241,6 @@ func CipherSuite(cipherName string) (uint16, error) { return cipher, nil } - if _, ok := ciphersTLS13[cipherName]; ok { - return 0, fmt.Errorf("all golang TLSv1.3 ciphers are always used for TLSv1.3 flows") - } - return 0, fmt.Errorf("unknown cipher name %q", cipherName) } @@ -252,35 +266,47 @@ func ValidCipherSuites() []string { sort.Strings(validCipherSuites) return validCipherSuites } + +// DefaultTLSProfileType is the intermediate profile type. +const DefaultTLSProfileType = configv1.TLSProfileIntermediateType + +// DefaultCiphers returns the default cipher suites for TLS connections. +// +// RECOMMENDATION: Instead of relying on this function directly, consumers should respect +// TLSSecurityProfile settings from one of the OpenShift API configuration resources: +// - For API servers: Use apiserver.config.openshift.io/cluster Spec.TLSSecurityProfile +// - For ingress controllers: Use operator.openshift.io/v1 IngressController Spec.TLSSecurityProfile +// - For kubelet: Use machineconfiguration.openshift.io/v1 KubeletConfig Spec.TLSSecurityProfile +// +// These API resources allow cluster administrators to choose between Old, Intermediate, +// Modern, or Custom TLS profiles. Components should observe these settings. func DefaultCiphers() []uint16 { - // HTTP/2 mandates TLS 1.2 or higher with an AEAD cipher - // suite (GCM, Poly1305) and ephemeral key exchange (ECDHE, DHE) for - // perfect forward secrecy. Servers may provide additional cipher - // suites for backwards compatibility with HTTP/1.1 clients. - // See RFC7540, section 9.2 (Use of TLS Features) and Appendix A - // (TLS 1.2 Cipher Suite Black List). + // Aligned with intermediate profile of the 5.7 version of the Mozilla Server + // Side TLS guidelines found at: https://ssl-config.mozilla.org/guidelines/5.7.json + // + // Latest guidelines: https://ssl-config.mozilla.org/guidelines/latest.json + // + // This profile provides strong security with wide compatibility. + // It requires TLS 1.2+ and uses only AEAD cipher suites (GCM, ChaCha20-Poly1305) + // with ECDHE key exchange for perfect forward secrecy. + // + // All CBC-mode ciphers have been removed due to padding oracle vulnerabilities. + // All RSA key exchange ciphers have been removed due to lack of perfect forward secrecy. + // + // HTTP/2 compliance: All ciphers are compliant with RFC7540, section 9.2. return []uint16{ + // TLS 1.2 cipher suites with ECDHE + AEAD tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256, tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256, - tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, // required by http/2 + tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256, // required by HTTP/2 tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384, tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384, - tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256, // forbidden by http/2, not flagged by http2isBadCipher() in go1.8 - tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256, // forbidden by http/2, not flagged by http2isBadCipher() in go1.8 - tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA, // forbidden by http/2 - tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA, // forbidden by http/2 - tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA, // forbidden by http/2 - tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA, // forbidden by http/2 - tls.TLS_RSA_WITH_AES_128_GCM_SHA256, // forbidden by http/2 - tls.TLS_RSA_WITH_AES_256_GCM_SHA384, // forbidden by http/2 - // the next one is in the intermediate suite, but go1.8 http2isBadCipher() complains when it is included at the recommended index - // because it comes after ciphers forbidden by the http/2 spec - // tls.TLS_RSA_WITH_AES_128_CBC_SHA256, - // tls.TLS_ECDHE_RSA_WITH_3DES_EDE_CBC_SHA, // forbidden by http/2, disabled to mitigate SWEET32 attack - // tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA, // forbidden by http/2, disabled to mitigate SWEET32 attack - tls.TLS_RSA_WITH_AES_128_CBC_SHA, // forbidden by http/2 - tls.TLS_RSA_WITH_AES_256_CBC_SHA, // forbidden by http/2 + + // TLS 1.3 cipher suites (negotiated automatically, not configurable) + tls.TLS_AES_128_GCM_SHA256, + tls.TLS_AES_256_GCM_SHA384, + tls.TLS_CHACHA20_POLY1305_SHA256, } } @@ -439,6 +465,16 @@ const ( keyBits = 2048 ) +// KeyAlgorithm represents the type of key pair to generate +type KeyAlgorithm int + +const ( + // AlgorithmRSA generates 2048-bit RSA key pairs (default for backwards compatibility) + AlgorithmRSA KeyAlgorithm = iota + // AlgorithmECDSA generates P-256 ECDSA key pairs + AlgorithmECDSA +) + type CA struct { Config *TLSCertificateConfig @@ -636,35 +672,40 @@ func MakeSelfSignedCAConfig(name string, lifetime time.Duration) (*TLSCertificat func MakeSelfSignedCAConfigForSubject(subject pkix.Name, lifetime time.Duration) (*TLSCertificateConfig, error) { if lifetime <= 0 { lifetime = DefaultCACertificateLifetimeDuration - fmt.Fprintf(os.Stderr, "Validity period of the certificate for %q is unset, resetting to %d years!\n", subject.CommonName, lifetime) + fmt.Fprintf(os.Stderr, "Validity period of the certificate for %q is unset, resetting to %s!\n", subject.CommonName, lifetime.String()) } if lifetime > DefaultCACertificateLifetimeDuration { warnAboutCertificateLifeTime(subject.CommonName, DefaultCACertificateLifetimeDuration) } - return makeSelfSignedCAConfigForSubjectAndDuration(subject, time.Now, lifetime) + return makeSelfSignedCAConfigForSubjectAndDuration(subject, time.Now, lifetime, AlgorithmRSA) } func MakeSelfSignedCAConfigForDuration(name string, caLifetime time.Duration) (*TLSCertificateConfig, error) { subject := pkix.Name{CommonName: name} - return makeSelfSignedCAConfigForSubjectAndDuration(subject, time.Now, caLifetime) + return makeSelfSignedCAConfigForSubjectAndDuration(subject, time.Now, caLifetime, AlgorithmRSA) +} + +func MakeSelfSignedCAConfigForDurationWithAlgorithm(name string, caLifetime time.Duration, algorithm KeyAlgorithm) (*TLSCertificateConfig, error) { + subject := pkix.Name{CommonName: name} + return makeSelfSignedCAConfigForSubjectAndDuration(subject, time.Now, caLifetime, algorithm) } func UnsafeMakeSelfSignedCAConfigForDurationAtTime(name string, currentTime func() time.Time, caLifetime time.Duration) (*TLSCertificateConfig, error) { subject := pkix.Name{CommonName: name} - return makeSelfSignedCAConfigForSubjectAndDuration(subject, currentTime, caLifetime) + return makeSelfSignedCAConfigForSubjectAndDuration(subject, currentTime, caLifetime, AlgorithmRSA) } -func makeSelfSignedCAConfigForSubjectAndDuration(subject pkix.Name, currentTime func() time.Time, caLifetime time.Duration) (*TLSCertificateConfig, error) { +func makeSelfSignedCAConfigForSubjectAndDuration(subject pkix.Name, currentTime func() time.Time, caLifetime time.Duration, algorithm KeyAlgorithm) (*TLSCertificateConfig, error) { // Create CA cert - rootcaPublicKey, rootcaPrivateKey, publicKeyHash, err := newKeyPairWithHash() + rootcaPublicKey, rootcaPrivateKey, publicKeyHash, err := newKeyPairWithAlgorithm(algorithm) if err != nil { return nil, err } // AuthorityKeyId and SubjectKeyId should match for a self-signed CA authorityKeyId := publicKeyHash subjectKeyId := publicKeyHash - rootcaTemplate := newSigningCertificateTemplateForDuration(subject, caLifetime, currentTime, authorityKeyId, subjectKeyId) + rootcaTemplate := newSigningCertificateTemplateForDuration(subject, caLifetime, currentTime, authorityKeyId, subjectKeyId, algorithm) rootcaCert, err := signCertificate(rootcaTemplate, rootcaPublicKey, rootcaTemplate, rootcaPrivateKey) if err != nil { return nil, err @@ -677,14 +718,22 @@ func makeSelfSignedCAConfigForSubjectAndDuration(subject pkix.Name, currentTime } func MakeCAConfigForDuration(name string, caLifetime time.Duration, issuer *CA) (*TLSCertificateConfig, error) { + return makeCAConfigForDuration(name, caLifetime, issuer, AlgorithmRSA) +} + +func MakeCAConfigForDurationWithAlgorithm(name string, caLifetime time.Duration, issuer *CA, algorithm KeyAlgorithm) (*TLSCertificateConfig, error) { + return makeCAConfigForDuration(name, caLifetime, issuer, algorithm) +} + +func makeCAConfigForDuration(name string, caLifetime time.Duration, issuer *CA, algorithm KeyAlgorithm) (*TLSCertificateConfig, error) { // Create CA cert - signerPublicKey, signerPrivateKey, publicKeyHash, err := newKeyPairWithHash() + signerPublicKey, signerPrivateKey, publicKeyHash, err := newKeyPairWithAlgorithm(algorithm) if err != nil { return nil, err } authorityKeyId := issuer.Config.Certs[0].SubjectKeyId subjectKeyId := publicKeyHash - signerTemplate := newSigningCertificateTemplateForDuration(pkix.Name{CommonName: name}, caLifetime, time.Now, authorityKeyId, subjectKeyId) + signerTemplate := newSigningCertificateTemplateForDuration(pkix.Name{CommonName: name}, caLifetime, time.Now, authorityKeyId, subjectKeyId, algorithm) signerCert, err := issuer.SignCertificate(signerTemplate, signerPublicKey) if err != nil { return nil, err @@ -792,19 +841,44 @@ func (ca *CA) MakeAndWriteServerCert(certFile, keyFile string, hostnames sets.Se type CertificateExtensionFunc func(*x509.Certificate) error func (ca *CA) MakeServerCert(hostnames sets.Set[string], lifetime time.Duration, fns ...CertificateExtensionFunc) (*TLSCertificateConfig, error) { - serverPublicKey, serverPrivateKey, publicKeyHash, _ := newKeyPairWithHash() + return ca.makeServerCert(hostnames, lifetime, AlgorithmRSA, fns...) +} + +func (ca *CA) MakeServerCertForDuration(hostnames sets.Set[string], lifetime time.Duration, fns ...CertificateExtensionFunc) (*TLSCertificateConfig, error) { + return ca.makeServerCertForDuration(hostnames, lifetime, AlgorithmRSA, fns...) +} + +// MakeServerCertWithAlgorithm creates a server certificate with the specified key algorithm +func (ca *CA) MakeServerCertWithAlgorithm(hostnames sets.Set[string], lifetime time.Duration, algorithm KeyAlgorithm, fns ...CertificateExtensionFunc) (*TLSCertificateConfig, error) { + return ca.makeServerCert(hostnames, lifetime, algorithm, fns...) +} + +// MakeServerCertForDurationWithAlgorithm creates a server certificate with specified duration and algorithm +func (ca *CA) MakeServerCertForDurationWithAlgorithm(hostnames sets.Set[string], lifetime time.Duration, algorithm KeyAlgorithm, fns ...CertificateExtensionFunc) (*TLSCertificateConfig, error) { + return ca.makeServerCertForDuration(hostnames, lifetime, algorithm, fns...) +} + +func (ca *CA) makeServerCert(hostnames sets.Set[string], lifetime time.Duration, algorithm KeyAlgorithm, fns ...CertificateExtensionFunc) (*TLSCertificateConfig, error) { + serverPublicKey, serverPrivateKey, publicKeyHash, err := newKeyPairWithAlgorithm(algorithm) + if err != nil { + return nil, err + } + authorityKeyId := ca.Config.Certs[0].SubjectKeyId subjectKeyId := publicKeyHash - serverTemplate := newServerCertificateTemplate(pkix.Name{CommonName: sets.List(hostnames)[0]}, sets.List(hostnames), lifetime, time.Now, authorityKeyId, subjectKeyId) + serverTemplate := newServerCertificateTemplate(pkix.Name{CommonName: sets.List(hostnames)[0]}, sets.List(hostnames), lifetime, time.Now, authorityKeyId, subjectKeyId, algorithm) + for _, fn := range fns { if err := fn(serverTemplate); err != nil { return nil, err } } + serverCrt, err := ca.SignCertificate(serverTemplate, serverPublicKey) if err != nil { return nil, err } + server := &TLSCertificateConfig{ Certs: append([]*x509.Certificate{serverCrt}, ca.Config.Certs...), Key: serverPrivateKey, @@ -812,20 +886,27 @@ func (ca *CA) MakeServerCert(hostnames sets.Set[string], lifetime time.Duration, return server, nil } -func (ca *CA) MakeServerCertForDuration(hostnames sets.Set[string], lifetime time.Duration, fns ...CertificateExtensionFunc) (*TLSCertificateConfig, error) { - serverPublicKey, serverPrivateKey, publicKeyHash, _ := newKeyPairWithHash() +func (ca *CA) makeServerCertForDuration(hostnames sets.Set[string], lifetime time.Duration, algorithm KeyAlgorithm, fns ...CertificateExtensionFunc) (*TLSCertificateConfig, error) { + serverPublicKey, serverPrivateKey, publicKeyHash, err := newKeyPairWithAlgorithm(algorithm) + if err != nil { + return nil, err + } + authorityKeyId := ca.Config.Certs[0].SubjectKeyId subjectKeyId := publicKeyHash - serverTemplate := newServerCertificateTemplateForDuration(pkix.Name{CommonName: sets.List(hostnames)[0]}, sets.List(hostnames), lifetime, time.Now, authorityKeyId, subjectKeyId) + serverTemplate := newServerCertificateTemplateForDuration(pkix.Name{CommonName: sets.List(hostnames)[0]}, sets.List(hostnames), lifetime, time.Now, authorityKeyId, subjectKeyId, algorithm) + for _, fn := range fns { if err := fn(serverTemplate); err != nil { return nil, err } } + serverCrt, err := ca.SignCertificate(serverTemplate, serverPublicKey) if err != nil { return nil, err } + server := &TLSCertificateConfig{ Certs: append([]*x509.Certificate{serverCrt}, ca.Config.Certs...), Key: serverPrivateKey, @@ -997,13 +1078,75 @@ func newRSAKeyPair() (*rsa.PublicKey, *rsa.PrivateKey, error) { return &privateKey.PublicKey, privateKey, nil } +// newECDSAKeyPair generates a new P-256 ECDSA key pair +func newECDSAKeyPair() (*ecdsa.PublicKey, *ecdsa.PrivateKey, error) { + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + if err != nil { + return nil, nil, err + } + return &privateKey.PublicKey, privateKey, nil +} + +// subjectPublicKeyInfo mirrors the ASN.1 SubjectPublicKeyInfo structure from RFC 5280 Section 4.1. +// It is used to extract the subjectPublicKey BIT STRING for hashing per Section 4.2.1.2. +type subjectPublicKeyInfo struct { + Algorithm pkix.AlgorithmIdentifier + PublicKey asn1.BitString +} + +// newECDSAKeyPairWithHash generates a new ECDSA key pair and computes the public key hash. +// Uses SHA-1 over the subjectPublicKey BIT STRING per RFC 5280 Section 4.2.1.2, +// matching the RSA convention. +func newECDSAKeyPairWithHash() (crypto.PublicKey, crypto.PrivateKey, []byte, error) { + publicKey, privateKey, err := newECDSAKeyPair() + if err != nil { + return nil, nil, nil, err + } + pubDER, err := x509.MarshalPKIXPublicKey(publicKey) + if err != nil { + return nil, nil, nil, err + } + var spki subjectPublicKeyInfo + if _, err := asn1.Unmarshal(pubDER, &spki); err != nil { + return nil, nil, nil, err + } + hash := sha1.New() + hash.Write(spki.PublicKey.Bytes) + publicKeyHash := hash.Sum(nil) + return publicKey, privateKey, publicKeyHash, nil +} + +// newKeyPairWithAlgorithm generates a new key pair using the specified algorithm +func newKeyPairWithAlgorithm(algo KeyAlgorithm) (crypto.PublicKey, crypto.PrivateKey, []byte, error) { + switch algo { + case AlgorithmECDSA: + return newECDSAKeyPairWithHash() + case AlgorithmRSA: + return newKeyPairWithHash() + default: + // This can only be reached if a new KeyAlgorithm constant is added + // to the const block above without a corresponding case here. + return nil, nil, nil, fmt.Errorf("unsupported key algorithm: %d", algo) + } +} + +// baseKeyUsageForAlgorithm returns the appropriate KeyUsage for the given algorithm. +// RSA keys use KeyEncipherment (for RSA key transport in TLS) + DigitalSignature. +// ECDSA keys use only DigitalSignature per RFC 5480 Section 3. +func baseKeyUsageForAlgorithm(algorithm KeyAlgorithm) x509.KeyUsage { + switch algorithm { + case AlgorithmECDSA: + return x509.KeyUsageDigitalSignature + default: + return x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature + } +} + // Can be used for CA or intermediate signing certs -func newSigningCertificateTemplateForDuration(subject pkix.Name, caLifetime time.Duration, currentTime func() time.Time, authorityKeyId, subjectKeyId []byte) *x509.Certificate { +func newSigningCertificateTemplateForDuration(subject pkix.Name, caLifetime time.Duration, currentTime func() time.Time, authorityKeyId, subjectKeyId []byte, algorithm KeyAlgorithm) *x509.Certificate { return &x509.Certificate{ Subject: subject, - SignatureAlgorithm: x509.SHA256WithRSA, - NotBefore: currentTime().Add(-1 * time.Second), NotAfter: currentTime().Add(caLifetime), @@ -1012,7 +1155,7 @@ func newSigningCertificateTemplateForDuration(subject pkix.Name, caLifetime time // signing certificate is ever rotated. SerialNumber: big.NewInt(randomSerialNumber()), - KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + KeyUsage: baseKeyUsageForAlgorithm(algorithm) | x509.KeyUsageCertSign, BasicConstraintsValid: true, IsCA: true, @@ -1022,31 +1165,29 @@ func newSigningCertificateTemplateForDuration(subject pkix.Name, caLifetime time } // Can be used for ListenAndServeTLS -func newServerCertificateTemplate(subject pkix.Name, hosts []string, lifetime time.Duration, currentTime func() time.Time, authorityKeyId, subjectKeyId []byte) *x509.Certificate { +func newServerCertificateTemplate(subject pkix.Name, hosts []string, lifetime time.Duration, currentTime func() time.Time, authorityKeyId, subjectKeyId []byte, algorithm KeyAlgorithm) *x509.Certificate { if lifetime <= 0 { lifetime = DefaultCertificateLifetimeDuration - fmt.Fprintf(os.Stderr, "Validity period of the certificate for %q is unset, resetting to %d years!\n", subject.CommonName, lifetime) + fmt.Fprintf(os.Stderr, "Validity period of the certificate for %q is unset, resetting to %s!\n", subject.CommonName, lifetime.String()) } if lifetime > DefaultCertificateLifetimeDuration { warnAboutCertificateLifeTime(subject.CommonName, DefaultCertificateLifetimeDuration) } - return newServerCertificateTemplateForDuration(subject, hosts, lifetime, currentTime, authorityKeyId, subjectKeyId) + return newServerCertificateTemplateForDuration(subject, hosts, lifetime, currentTime, authorityKeyId, subjectKeyId, algorithm) } // Can be used for ListenAndServeTLS -func newServerCertificateTemplateForDuration(subject pkix.Name, hosts []string, lifetime time.Duration, currentTime func() time.Time, authorityKeyId, subjectKeyId []byte) *x509.Certificate { +func newServerCertificateTemplateForDuration(subject pkix.Name, hosts []string, lifetime time.Duration, currentTime func() time.Time, authorityKeyId, subjectKeyId []byte, algorithm KeyAlgorithm) *x509.Certificate { template := &x509.Certificate{ Subject: subject, - SignatureAlgorithm: x509.SHA256WithRSA, - NotBefore: currentTime().Add(-1 * time.Second), NotAfter: currentTime().Add(lifetime), SerialNumber: big.NewInt(1), - KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + KeyUsage: baseKeyUsageForAlgorithm(algorithm), ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, BasicConstraintsValid: true, @@ -1112,7 +1253,7 @@ func CertsFromPEM(pemCerts []byte) ([]*x509.Certificate, error) { func NewClientCertificateTemplate(subject pkix.Name, lifetime time.Duration, currentTime func() time.Time) *x509.Certificate { if lifetime <= 0 { lifetime = DefaultCertificateLifetimeDuration - fmt.Fprintf(os.Stderr, "Validity period of the certificate for %q is unset, resetting to %d years!\n", subject.CommonName, lifetime) + fmt.Fprintf(os.Stderr, "Validity period of the certificate for %q is unset, resetting to %s!\n", subject.CommonName, lifetime.String()) } if lifetime > DefaultCertificateLifetimeDuration { @@ -1127,8 +1268,6 @@ func NewClientCertificateTemplateForDuration(subject pkix.Name, lifetime time.Du return &x509.Certificate{ Subject: subject, - SignatureAlgorithm: x509.SHA256WithRSA, - NotBefore: currentTime().Add(-1 * time.Second), NotAfter: currentTime().Add(lifetime), SerialNumber: big.NewInt(1),