From 854a902fbe80b7e1301ee96dcca585bcf29646fa Mon Sep 17 00:00:00 2001 From: Fabien Dupont Date: Wed, 4 Feb 2026 18:20:31 +0100 Subject: [PATCH] pkg/crypto: add ECDSA P-256 key generation support Extends the crypto package to generate ECDSA P-256 key pairs in addition to existing RSA support, enabling OpenShift components to use modern elliptic curve cryptography. Adds: - KeyAlgorithm type for algorithm selection (RSA, ECDSA) - newECDSAKeyPair() and newECDSAKeyPairWithHash() functions using P-256 curve - newKeyPairWithAlgorithm() for unified key generation - signatureAlgorithmForKey() for automatic algorithm detection - CA.MakeServerCertWithAlgorithm() and CA.MakeServerCertForDurationWithAlgorithm() All existing APIs remain unchanged, preserving 100% backwards compatibility. New functionality is opt-in through *WithAlgorithm functions. ECDSA P-256 provides equivalent security to 3072-bit RSA with smaller keys (~87% smaller), faster operations, and better performance. This prepares OpenShift for modern TLS deployments and aligns with industry best practices. Test coverage includes: - Unit tests for key generation, signature algorithm detection, and encoding - Integration tests for RSA CA + ECDSA server and vice versa - Backwards compatibility tests verifying existing RSA functionality Co-authored-by: Claude Sonnet 4.5 Signed-off-by: Fabien Dupont --- pkg/crypto/crypto.go | 109 +++++++++++++++- pkg/crypto/crypto_test.go | 259 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 365 insertions(+), 3 deletions(-) diff --git a/pkg/crypto/crypto.go b/pkg/crypto/crypto.go index ca2806ecc6..0fa007548b 100644 --- a/pkg/crypto/crypto.go +++ b/pkg/crypto/crypto.go @@ -4,9 +4,11 @@ import ( "bytes" "crypto" "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/sha1" + "crypto/sha256" "crypto/tls" "crypto/x509" "crypto/x509/pkix" @@ -443,6 +445,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 @@ -796,19 +808,54 @@ 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) { + sigFn := func(template *x509.Certificate) error { + template.SignatureAlgorithm = signatureAlgorithmForKey(ca.Config.Key) + return nil + } + fns = append([]CertificateExtensionFunc{sigFn}, fns...) + 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) { + sigFn := func(template *x509.Certificate) error { + template.SignatureAlgorithm = signatureAlgorithmForKey(ca.Config.Key) + return nil + } + fns = append([]CertificateExtensionFunc{sigFn}, fns...) + 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) + 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, @@ -816,20 +863,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) + 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, @@ -1001,6 +1055,55 @@ 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 +} + +// newECDSAKeyPairWithHash generates a new ECDSA key pair and computes the public key hash +// Uses SHA256 for ECDSA keys (vs SHA1 for RSA) for consistency with modern standards +func newECDSAKeyPairWithHash() (crypto.PublicKey, crypto.PrivateKey, []byte, error) { + publicKey, privateKey, err := newECDSAKeyPair() + var publicKeyHash []byte + if err == nil { + hash := sha256.New() + // Marshal public key in uncompressed form for hashing + pubBytes := elliptic.Marshal(publicKey.Curve, publicKey.X, publicKey.Y) + hash.Write(pubBytes) + publicKeyHash = hash.Sum(nil) + } + return publicKey, privateKey, publicKeyHash, err +} + +// 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: + return nil, nil, nil, fmt.Errorf("unsupported key algorithm: %d", algo) + } +} + +// signatureAlgorithmForKey returns the appropriate x509.SignatureAlgorithm for the given private key +func signatureAlgorithmForKey(key crypto.PrivateKey) x509.SignatureAlgorithm { + switch key.(type) { + case *ecdsa.PrivateKey: + return x509.ECDSAWithSHA256 + case *rsa.PrivateKey: + return x509.SHA256WithRSA + default: + // Default to RSA for backwards compatibility with unknown key types + return x509.SHA256WithRSA + } +} + // 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 { return &x509.Certificate{ diff --git a/pkg/crypto/crypto_test.go b/pkg/crypto/crypto_test.go index f0158efa5f..5d7dd2ab9a 100644 --- a/pkg/crypto/crypto_test.go +++ b/pkg/crypto/crypto_test.go @@ -2,6 +2,9 @@ package crypto import ( "crypto" + "crypto/ecdsa" + "crypto/elliptic" + "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "fmt" @@ -549,3 +552,259 @@ func TestServerCertRegeneration(t *testing.T) { require.NotNil(t, serverCert) require.True(t, created) } + +// TestECDSAKeyGeneration tests basic ECDSA key pair generation +func TestECDSAKeyGeneration(t *testing.T) { + publicKey, privateKey, err := newECDSAKeyPair() + require.NoError(t, err, "ECDSA key generation should succeed") + require.NotNil(t, publicKey, "public key should not be nil") + require.NotNil(t, privateKey, "private key should not be nil") + + // Verify key type + require.IsType(t, &ecdsa.PublicKey{}, publicKey, "public key should be ECDSA") + require.IsType(t, &ecdsa.PrivateKey{}, privateKey, "private key should be ECDSA") + + // Verify curve is P-256 + require.Equal(t, elliptic.P256(), publicKey.Curve, "should use P-256 curve") + require.Equal(t, elliptic.P256(), privateKey.Curve, "should use P-256 curve") + + // Verify public key matches private key + require.True(t, publicKey.X.Cmp(privateKey.PublicKey.X) == 0, "public key X should match") + require.True(t, publicKey.Y.Cmp(privateKey.PublicKey.Y) == 0, "public key Y should match") +} + +// TestECDSAKeyPairWithHash tests ECDSA key generation with hash computation +func TestECDSAKeyPairWithHash(t *testing.T) { + publicKey, privateKey, hash, err := newECDSAKeyPairWithHash() + require.NoError(t, err, "ECDSA key generation with hash should succeed") + require.NotNil(t, publicKey, "public key should not be nil") + require.NotNil(t, privateKey, "private key should not be nil") + require.NotNil(t, hash, "hash should not be nil") + + // Verify hash is SHA256 length (32 bytes) + require.Equal(t, 32, len(hash), "hash should be SHA256 (32 bytes)") + + // Hash should be deterministic for same public key + _, _, hash2, err := newECDSAKeyPairWithHash() + require.NoError(t, err) + // Different keys should produce different hashes + require.NotEqual(t, hash, hash2, "different keys should produce different hashes") +} + +// TestSignatureAlgorithmForKey tests signature algorithm detection +func TestSignatureAlgorithmForKey(t *testing.T) { + tests := []struct { + name string + keyGen func() interface{} + expectedSigAlg x509.SignatureAlgorithm + }{ + { + name: "RSA key", + keyGen: func() interface{} { + _, privateKey, _ := newRSAKeyPair() + return privateKey + }, + expectedSigAlg: x509.SHA256WithRSA, + }, + { + name: "ECDSA key", + keyGen: func() interface{} { + _, privateKey, _ := newECDSAKeyPair() + return privateKey + }, + expectedSigAlg: x509.ECDSAWithSHA256, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + key := tt.keyGen() + sigAlg := signatureAlgorithmForKey(key) + require.Equal(t, tt.expectedSigAlg, sigAlg, "signature algorithm should match key type") + }) + } +} + +// TestServerCertWithECDSA tests ECDSA server certificate generation +func TestServerCertWithECDSA(t *testing.T) { + // Create RSA CA (existing pattern) + caPublicKey, caPrivateKey, caPublicKeyHash, err := newKeyPairWithHash() + require.NoError(t, err) + + caTemplate := newSigningCertificateTemplate(pkix.Name{CommonName: "test-ca"}, DefaultCACertificateLifetimeDuration, time.Now) + caTemplate.SubjectKeyId = caPublicKeyHash + caTemplate.AuthorityKeyId = caPublicKeyHash + caTemplate.SignatureAlgorithm = x509.SHA256WithRSA + caCert, err := signCertificate(caTemplate, caPublicKey, caTemplate, caPrivateKey) + require.NoError(t, err) + + ca := &CA{ + Config: &TLSCertificateConfig{ + Certs: []*x509.Certificate{caCert}, + Key: caPrivateKey, + }, + SerialGenerator: &RandomSerialGenerator{}, + } + + // Test ECDSA server certificate generation + hostnames := sets.New("test.example.com", "localhost") + serverCert, err := ca.MakeServerCertWithAlgorithm(hostnames, time.Hour*24*365, AlgorithmECDSA) + require.NoError(t, err, "ECDSA server cert generation should succeed") + require.NotNil(t, serverCert, "server cert should not be nil") + + // Verify the certificate uses ECDSA key + require.IsType(t, &ecdsa.PrivateKey{}, serverCert.Key, "server cert should use ECDSA key") + + // Verify signature algorithm matches CA (RSA CA signs with RSA) + require.Equal(t, x509.SHA256WithRSA, serverCert.Certs[0].SignatureAlgorithm, "cert signature should match CA's key type") + + // Verify public key type + pubKey, ok := serverCert.Certs[0].PublicKey.(*ecdsa.PublicKey) + require.True(t, ok, "certificate public key should be ECDSA") + require.Equal(t, elliptic.P256(), pubKey.Curve, "should use P-256 curve") + + // Verify hostnames are present + require.Contains(t, serverCert.Certs[0].DNSNames, "test.example.com", "should contain hostname") + require.Contains(t, serverCert.Certs[0].DNSNames, "localhost", "should contain hostname") +} + +// TestServerCertWithRSA tests that RSA still works (backwards compatibility) +func TestServerCertWithRSA(t *testing.T) { + // Create RSA CA + caPublicKey, caPrivateKey, caPublicKeyHash, err := newKeyPairWithHash() + require.NoError(t, err) + + caTemplate := newSigningCertificateTemplate(pkix.Name{CommonName: "test-ca"}, DefaultCACertificateLifetimeDuration, time.Now) + caTemplate.SubjectKeyId = caPublicKeyHash + caTemplate.AuthorityKeyId = caPublicKeyHash + caTemplate.SignatureAlgorithm = x509.SHA256WithRSA + caCert, err := signCertificate(caTemplate, caPublicKey, caTemplate, caPrivateKey) + require.NoError(t, err) + + ca := &CA{ + Config: &TLSCertificateConfig{ + Certs: []*x509.Certificate{caCert}, + Key: caPrivateKey, + }, + SerialGenerator: &RandomSerialGenerator{}, + } + + // Test RSA server certificate generation + hostnames := sets.New("test.example.com") + serverCert, err := ca.MakeServerCertWithAlgorithm(hostnames, time.Hour*24*365, AlgorithmRSA) + require.NoError(t, err, "RSA server cert generation should succeed") + require.NotNil(t, serverCert, "server cert should not be nil") + + // Verify the certificate uses RSA + require.IsType(t, &rsa.PrivateKey{}, serverCert.Key, "server cert should use RSA key") + + // Verify signature algorithm + require.Equal(t, x509.SHA256WithRSA, serverCert.Certs[0].SignatureAlgorithm, "cert should use SHA256WithRSA") +} + +// TestMixedCAAndServerAlgorithms tests RSA CA signing ECDSA cert and vice versa +func TestMixedCAAndServerAlgorithms(t *testing.T) { + tests := []struct { + name string + caAlgorithm KeyAlgorithm + serverAlgorithm KeyAlgorithm + }{ + { + name: "RSA CA with ECDSA server", + caAlgorithm: AlgorithmRSA, + serverAlgorithm: AlgorithmECDSA, + }, + { + name: "ECDSA CA with RSA server", + caAlgorithm: AlgorithmECDSA, + serverAlgorithm: AlgorithmRSA, + }, + { + name: "ECDSA CA with ECDSA server", + caAlgorithm: AlgorithmECDSA, + serverAlgorithm: AlgorithmECDSA, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Generate CA with specified algorithm + caPublicKey, caPrivateKey, caPublicKeyHash, err := newKeyPairWithAlgorithm(tt.caAlgorithm) + require.NoError(t, err) + + caTemplate := newSigningCertificateTemplate(pkix.Name{CommonName: "test-ca"}, DefaultCACertificateLifetimeDuration, time.Now) + caTemplate.SubjectKeyId = caPublicKeyHash + caTemplate.AuthorityKeyId = caPublicKeyHash + caTemplate.SignatureAlgorithm = signatureAlgorithmForKey(caPrivateKey) + caCert, err := signCertificate(caTemplate, caPublicKey, caTemplate, caPrivateKey) + require.NoError(t, err) + + ca := &CA{ + Config: &TLSCertificateConfig{ + Certs: []*x509.Certificate{caCert}, + Key: caPrivateKey, + }, + SerialGenerator: &RandomSerialGenerator{}, + } + + // Generate server cert with specified algorithm + hostnames := sets.New("test.example.com") + serverCert, err := ca.MakeServerCertWithAlgorithm(hostnames, time.Hour*24*365, tt.serverAlgorithm) + require.NoError(t, err, "server cert generation should succeed") + require.NotNil(t, serverCert, "server cert should not be nil") + + // Verify certificate chain + require.Equal(t, 2, len(serverCert.Certs), "should have server cert + CA cert") + + // The server cert's signature algorithm should match the CA's key type + expectedServerSigAlg := signatureAlgorithmForKey(caPrivateKey) + require.Equal(t, expectedServerSigAlg, serverCert.Certs[0].SignatureAlgorithm) + }) + } +} + +// TestECDSACertificateEncoding tests that ECDSA certificates can be PEM encoded +func TestECDSACertificateEncoding(t *testing.T) { + // Generate ECDSA key pair + _, privateKey, err := newECDSAKeyPair() + require.NoError(t, err) + + // Test encoding (should use existing EncodeKey function) + pemBytes, err := EncodeKey(privateKey) + require.NoError(t, err, "encoding ECDSA key should succeed") + require.NotNil(t, pemBytes, "PEM bytes should not be nil") + require.Contains(t, string(pemBytes), "BEGIN EC PRIVATE KEY", "should contain EC PRIVATE KEY header") +} + +// TestNewKeyPairWithAlgorithm tests the algorithm selection function +func TestNewKeyPairWithAlgorithm(t *testing.T) { + tests := []struct { + name string + algorithm KeyAlgorithm + expectedType interface{} + }{ + { + name: "RSA algorithm", + algorithm: AlgorithmRSA, + expectedType: &rsa.PrivateKey{}, + }, + { + name: "ECDSA algorithm", + algorithm: AlgorithmECDSA, + expectedType: &ecdsa.PrivateKey{}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + publicKey, privateKey, hash, err := newKeyPairWithAlgorithm(tt.algorithm) + require.NoError(t, err, "key generation should succeed") + require.NotNil(t, publicKey, "public key should not be nil") + require.NotNil(t, privateKey, "private key should not be nil") + require.NotNil(t, hash, "hash should not be nil") + + // Verify key type + require.IsType(t, tt.expectedType, privateKey, "private key type should match") + }) + } +}