From 7b058d5bfe128ce8151c791ddd06c7166d63f575 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 support for certificates and CAs Extends the crypto package to generate ECDSA P-256 key pairs and certificates in addition to existing RSA support, enabling OpenShift components to use modern elliptic curve cryptography. Adds: - KeyAlgorithm type for algorithm selection (RSA, ECDSA) - ECDSA P-256 key pair generation with proper SubjectKeyIdentifier hashing - MakeServerCertWithAlgorithm() and MakeServerCertForDurationWithAlgorithm() - MakeSelfSignedCAConfigForDurationWithAlgorithm() for ECDSA root CAs - MakeCAConfigForDurationWithAlgorithm() for ECDSA intermediate CAs Removes hardcoded SignatureAlgorithm from certificate templates, letting x509.CreateCertificate infer the correct algorithm from the signing key. This is backward compatible and enables correct cross-algorithm signing (e.g. RSA CA signing ECDSA leaf certs). All existing APIs remain unchanged, preserving backwards compatibility. New functionality is opt-in through *WithAlgorithm variants. Co-authored-by: Claude Sonnet 4.5 Co-Authored-By: Claude Opus 4.6 (1M context) Signed-off-by: Fabien Dupont --- pkg/crypto/crypto.go | 165 ++++++++++++--- pkg/crypto/crypto_test.go | 428 +++++++++++++++++++++++++++++++++++++- 2 files changed, 564 insertions(+), 29 deletions(-) diff --git a/pkg/crypto/crypto.go b/pkg/crypto/crypto.go index be0337b900..119e70bacf 100644 --- a/pkg/crypto/crypto.go +++ b/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" @@ -463,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 @@ -666,29 +678,34 @@ func MakeSelfSignedCAConfigForSubject(subject pkix.Name, lifetime time.Duration) 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 @@ -701,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 @@ -816,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, @@ -836,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, @@ -1021,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), @@ -1036,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, @@ -1046,7 +1165,7 @@ 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 %s!\n", subject.CommonName, lifetime.String()) @@ -1056,21 +1175,19 @@ func newServerCertificateTemplate(subject pkix.Name, hosts []string, lifetime ti 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, @@ -1151,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), diff --git a/pkg/crypto/crypto_test.go b/pkg/crypto/crypto_test.go index cac73f952a..8c32447d47 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" @@ -20,6 +23,21 @@ import ( configv1 "github.com/openshift/api/config/v1" ) +// signatureAlgorithmForKey returns the appropriate x509.SignatureAlgorithm for the given private key. +// Used in tests to assert that x509.CreateCertificate infers the correct signature algorithm. +func signatureAlgorithmForKey(t testing.TB, key crypto.PrivateKey) x509.SignatureAlgorithm { + t.Helper() + switch key.(type) { + case *ecdsa.PrivateKey: + return x509.ECDSAWithSHA256 + case *rsa.PrivateKey: + return x509.SHA256WithRSA + default: + t.Fatalf("unrecognized private key type: %T", key) + return 0 + } +} + const certificateLifetime = time.Hour * 24 * 365 * 2 func TestDefaultCipherSuite(t *testing.T) { @@ -141,7 +159,7 @@ func TestCrypto(t *testing.T) { } // Can be used for CA or intermediate signing certs -func newSigningCertificateTemplate(subject pkix.Name, lifetime time.Duration, currentTime func() time.Time) *x509.Certificate { +func newSigningCertificateTemplate(subject pkix.Name, lifetime time.Duration, currentTime func() time.Time, algorithm ...KeyAlgorithm) *x509.Certificate { if lifetime <= 0 { lifetime = DefaultCACertificateLifetimeDuration fmt.Fprintf(os.Stderr, "Validity period of the certificate for %q is unset, resetting to %s!\n", subject.CommonName, lifetime.String()) @@ -151,7 +169,11 @@ func newSigningCertificateTemplate(subject pkix.Name, lifetime time.Duration, cu warnAboutCertificateLifeTime(subject.CommonName, DefaultCACertificateLifetimeDuration) } - return newSigningCertificateTemplateForDuration(subject, lifetime, currentTime, nil, nil) + algo := AlgorithmRSA + if len(algorithm) > 0 { + algo = algorithm[0] + } + return newSigningCertificateTemplateForDuration(subject, lifetime, currentTime, nil, nil, algo) } func buildCA(t *testing.T) (crypto.PrivateKey, *x509.Certificate) { @@ -189,7 +211,7 @@ func buildServer(t *testing.T, signingKey crypto.PrivateKey, signingCrt *x509.Ce t.Fatalf("Unexpected error: %#v", err) } hosts := []string{"127.0.0.1", "localhost", "www.example.com"} - serverTemplate := newServerCertificateTemplate(pkix.Name{CommonName: "Server"}, hosts, certificateLifetime, time.Now, nil, nil) + serverTemplate := newServerCertificateTemplate(pkix.Name{CommonName: "Server"}, hosts, certificateLifetime, time.Now, nil, nil, AlgorithmRSA) serverCrt, err := signCertificate(serverTemplate, serverPublicKey, signingCrt, signingKey) if err != nil { t.Fatalf("Unexpected error: %#v", err) @@ -237,7 +259,7 @@ func TestRandomSerialGenerator(t *testing.T) { generator := &RandomSerialGenerator{} hostnames := []string{"foo", "bar"} - template := newServerCertificateTemplate(pkix.Name{CommonName: hostnames[0]}, hostnames, certificateLifetime, time.Now, nil, nil) + template := newServerCertificateTemplate(pkix.Name{CommonName: hostnames[0]}, hostnames, certificateLifetime, time.Now, nil, nil, AlgorithmRSA) if _, err := generator.Next(template); err != nil { t.Fatalf("unexpected error: %v", err) } @@ -313,6 +335,7 @@ func TestValidityPeriodOfServerCertificate(t *testing.T) { currentFakeTime, nil, nil, + AlgorithmRSA, ) expirationDate := cert.NotAfter expectedExpirationDate := currentTime.Add(test.realDuration) @@ -573,3 +596,400 @@ func TestTLSProfileCipherSuitesHaveMappings(t *testing.T) { strings.Join(missingMappings, "\n")) } } + +// 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 SHA-1 length (20 bytes), matching RSA convention and RFC 5280 + require.Equal(t, 20, len(hash), "hash should be SHA-1 (20 bytes)") + + // Different keys should produce different hashes + _, _, hash2, err := newECDSAKeyPairWithHash() + require.NoError(t, err) + 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() any + expectedSigAlg x509.SignatureAlgorithm + }{ + { + name: "RSA key", + keyGen: func() any { + _, privateKey, _ := newRSAKeyPair() + return privateKey + }, + expectedSigAlg: x509.SHA256WithRSA, + }, + { + name: "ECDSA key", + keyGen: func() any { + _, privateKey, _ := newECDSAKeyPair() + return privateKey + }, + expectedSigAlg: x509.ECDSAWithSHA256, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + key := tt.keyGen() + sigAlg := signatureAlgorithmForKey(t, 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 ECDSA cert does not have KeyEncipherment (RFC 5480 Section 3) + require.Equal(t, x509.KeyUsageDigitalSignature, serverCert.Certs[0].KeyUsage, "ECDSA cert should only have DigitalSignature") + + // 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, tt.caAlgorithm) + caTemplate.SubjectKeyId = caPublicKeyHash + caTemplate.AuthorityKeyId = caPublicKeyHash + 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(t, 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 any + }{ + { + 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") + }) + } + + t.Run("unsupported algorithm", func(t *testing.T) { + _, _, _, err := newKeyPairWithAlgorithm(KeyAlgorithm(99)) + require.Error(t, err, "unsupported algorithm should return an error") + require.Contains(t, err.Error(), "unsupported key algorithm") + }) +} + +// TestServerCertForDurationWithAlgorithm tests the ForDuration+WithAlgorithm path +// used in production (certrotation/target.go) +func TestServerCertForDurationWithAlgorithm(t *testing.T) { + caConfig, err := MakeSelfSignedCAConfigForDuration("test-ca", DefaultCACertificateLifetimeDuration) + require.NoError(t, err) + + ca := &CA{ + Config: caConfig, + SerialGenerator: &RandomSerialGenerator{}, + } + + hostnames := sets.New("test.example.com", "localhost") + + t.Run("ECDSA leaf from RSA CA", func(t *testing.T) { + serverCert, err := ca.MakeServerCertForDurationWithAlgorithm(hostnames, time.Hour*24*365, AlgorithmECDSA) + require.NoError(t, err) + require.IsType(t, &ecdsa.PrivateKey{}, serverCert.Key) + require.Equal(t, x509.SHA256WithRSA, serverCert.Certs[0].SignatureAlgorithm, + "cert signed by RSA CA should use SHA256WithRSA") + require.Contains(t, serverCert.Certs[0].DNSNames, "test.example.com") + }) + + t.Run("RSA leaf from RSA CA", func(t *testing.T) { + serverCert, err := ca.MakeServerCertForDurationWithAlgorithm(hostnames, time.Hour*24*365, AlgorithmRSA) + require.NoError(t, err) + require.IsType(t, &rsa.PrivateKey{}, serverCert.Key) + require.Equal(t, x509.SHA256WithRSA, serverCert.Certs[0].SignatureAlgorithm) + }) +} + +// TestMakeSelfSignedCAConfigForDurationWithAlgorithm tests ECDSA CA creation +func TestMakeSelfSignedCAConfigForDurationWithAlgorithm(t *testing.T) { + tests := []struct { + name string + algorithm KeyAlgorithm + expectedKeyTyp any + expectedSigAlg x509.SignatureAlgorithm + expectedKeyUsage x509.KeyUsage + }{ + { + name: "RSA CA", + algorithm: AlgorithmRSA, + expectedKeyTyp: &rsa.PrivateKey{}, + expectedSigAlg: x509.SHA256WithRSA, + expectedKeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + }, + { + name: "ECDSA CA", + algorithm: AlgorithmECDSA, + expectedKeyTyp: &ecdsa.PrivateKey{}, + expectedSigAlg: x509.ECDSAWithSHA256, + expectedKeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + caConfig, err := MakeSelfSignedCAConfigForDurationWithAlgorithm("test-ca", DefaultCACertificateLifetimeDuration, tt.algorithm) + require.NoError(t, err) + require.NotNil(t, caConfig) + require.Len(t, caConfig.Certs, 1) + require.IsType(t, tt.expectedKeyTyp, caConfig.Key) + + caCert := caConfig.Certs[0] + require.True(t, caCert.IsCA, "certificate should be a CA") + require.Equal(t, "test-ca", caCert.Subject.CommonName) + require.Equal(t, tt.expectedSigAlg, caCert.SignatureAlgorithm) + require.Equal(t, tt.expectedKeyUsage, caCert.KeyUsage, "KeyUsage should match algorithm") + require.Equal(t, caCert.SubjectKeyId, caCert.AuthorityKeyId, + "self-signed CA should have matching SubjectKeyId and AuthorityKeyId") + }) + } +} + +// TestMakeCAConfigForDurationWithAlgorithm tests ECDSA intermediate CA creation +func TestMakeCAConfigForDurationWithAlgorithm(t *testing.T) { + tests := []struct { + name string + rootAlgorithm KeyAlgorithm + intermediateAlgo KeyAlgorithm + expectedKeyTyp any + expectedRootSig x509.SignatureAlgorithm + }{ + { + name: "RSA root, ECDSA intermediate", + rootAlgorithm: AlgorithmRSA, + intermediateAlgo: AlgorithmECDSA, + expectedKeyTyp: &ecdsa.PrivateKey{}, + expectedRootSig: x509.SHA256WithRSA, + }, + { + name: "ECDSA root, RSA intermediate", + rootAlgorithm: AlgorithmECDSA, + intermediateAlgo: AlgorithmRSA, + expectedKeyTyp: &rsa.PrivateKey{}, + expectedRootSig: x509.ECDSAWithSHA256, + }, + { + name: "ECDSA root, ECDSA intermediate", + rootAlgorithm: AlgorithmECDSA, + intermediateAlgo: AlgorithmECDSA, + expectedKeyTyp: &ecdsa.PrivateKey{}, + expectedRootSig: x509.ECDSAWithSHA256, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + rootConfig, err := MakeSelfSignedCAConfigForDurationWithAlgorithm("root-ca", DefaultCACertificateLifetimeDuration, tt.rootAlgorithm) + require.NoError(t, err) + + rootCA := &CA{ + Config: rootConfig, + SerialGenerator: &RandomSerialGenerator{}, + } + + intConfig, err := MakeCAConfigForDurationWithAlgorithm("intermediate-ca", DefaultCACertificateLifetimeDuration, rootCA, tt.intermediateAlgo) + require.NoError(t, err) + require.NotNil(t, intConfig) + require.IsType(t, tt.expectedKeyTyp, intConfig.Key) + + // Intermediate cert bundle should contain intermediate + root + require.Len(t, intConfig.Certs, 2) + intCert := intConfig.Certs[0] + require.True(t, intCert.IsCA, "intermediate should be a CA") + require.Equal(t, "intermediate-ca", intCert.Subject.CommonName) + require.Equal(t, tt.expectedRootSig, intCert.SignatureAlgorithm, + "intermediate cert signature should match root CA key type") + require.Equal(t, rootConfig.Certs[0].SubjectKeyId, intCert.AuthorityKeyId, + "intermediate AuthorityKeyId should match root SubjectKeyId") + }) + } +}