diff --git a/pkg/asset/imagebased/configimage/ingressoperatorsigner.go b/pkg/asset/imagebased/configimage/ingressoperatorsigner.go index 3cca119b3a4..48184aa37fa 100644 --- a/pkg/asset/imagebased/configimage/ingressoperatorsigner.go +++ b/pkg/asset/imagebased/configimage/ingressoperatorsigner.go @@ -59,7 +59,10 @@ func (a *IngressOperatorSignerCertKey) Generate(ctx context.Context, dependencie return err } - a.KeyRaw = tls.PrivateKeyToPem(key) + a.KeyRaw, err = tls.PrivateKeyToPem(key) + if err != nil { + return fmt.Errorf("failed to encode private key to PEM: %w", err) + } a.CertRaw = tls.CertToPem(crt) return nil diff --git a/pkg/asset/tls/adminkubeconfig.go b/pkg/asset/tls/adminkubeconfig.go index f4ec8a7bf65..17bd5f6525e 100644 --- a/pkg/asset/tls/adminkubeconfig.go +++ b/pkg/asset/tls/adminkubeconfig.go @@ -23,13 +23,13 @@ func (c *AdminKubeConfigSignerCertKey) Dependencies() []asset.Asset { // Generate generates the root-ca key and cert pair. func (c *AdminKubeConfigSignerCertKey) Generate(ctx context.Context, parents asset.Parents) error { cfg := &CertCfg{ - Subject: pkix.Name{CommonName: "admin-kubeconfig-signer", OrganizationalUnit: []string{"openshift"}}, - KeyUsages: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - Validity: ValidityTenYears(), - IsCA: true, + Subject: pkix.Name{CommonName: "admin-kubeconfig-signer", OrganizationalUnit: []string{"openshift"}}, + // KeyUsages is set by GenerateSelfSignedCertificate based on the key algorithm. + Validity: ValidityTenYears(), + IsCA: true, } - return c.SelfSignedCertKey.Generate(ctx, cfg, "admin-kubeconfig-signer") + return c.SelfSignedCertKey.Generate(ctx, cfg, "admin-kubeconfig-signer", nil) } // Load reads the asset files from disk. diff --git a/pkg/asset/tls/aggregator.go b/pkg/asset/tls/aggregator.go index 6088b6785b1..082f23d7266 100644 --- a/pkg/asset/tls/aggregator.go +++ b/pkg/asset/tls/aggregator.go @@ -32,13 +32,13 @@ func (a *AggregatorCA) Generate(ctx context.Context, dependencies asset.Parents) dependencies.Get(installConfig) cfg := &CertCfg{ - Subject: pkix.Name{CommonName: "aggregator", OrganizationalUnit: []string{"bootkube"}}, - KeyUsages: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - Validity: ValidityOneDay(installConfig), - IsCA: true, + Subject: pkix.Name{CommonName: "aggregator", OrganizationalUnit: []string{"bootkube"}}, + // KeyUsages is set by GenerateSelfSignedCertificate based on the key algorithm. + Validity: ValidityOneDay(installConfig), + IsCA: true, } - return a.SelfSignedCertKey.Generate(ctx, cfg, "aggregator-ca") + return a.SelfSignedCertKey.Generate(ctx, cfg, "aggregator-ca", nil) } // Name returns the human-friendly name of the asset. @@ -102,13 +102,13 @@ func (c *AggregatorSignerCertKey) Generate(ctx context.Context, parents asset.Pa installConfig := &installconfig.InstallConfig{} parents.Get(installConfig) cfg := &CertCfg{ - Subject: pkix.Name{CommonName: "aggregator-signer", OrganizationalUnit: []string{"openshift"}}, - KeyUsages: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - Validity: ValidityOneDay(installConfig), - IsCA: true, + Subject: pkix.Name{CommonName: "aggregator-signer", OrganizationalUnit: []string{"openshift"}}, + // KeyUsages is set by GenerateSelfSignedCertificate based on the key algorithm. + Validity: ValidityOneDay(installConfig), + IsCA: true, } - return c.SelfSignedCertKey.Generate(ctx, cfg, "aggregator-signer") + return c.SelfSignedCertKey.Generate(ctx, cfg, "aggregator-signer", nil) } // Name returns the human-friendly name of the asset. diff --git a/pkg/asset/tls/apiserver.go b/pkg/asset/tls/apiserver.go index 8eb94e9b871..9078514c0cb 100644 --- a/pkg/asset/tls/apiserver.go +++ b/pkg/asset/tls/apiserver.go @@ -29,13 +29,13 @@ func (c *KubeAPIServerToKubeletSignerCertKey) Generate(ctx context.Context, pare installConfig := &installconfig.InstallConfig{} parents.Get(installConfig) cfg := &CertCfg{ - Subject: pkix.Name{CommonName: "kube-apiserver-to-kubelet-signer", OrganizationalUnit: []string{"openshift"}}, - KeyUsages: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - Validity: ValidityOneYear(installConfig), - IsCA: true, + Subject: pkix.Name{CommonName: "kube-apiserver-to-kubelet-signer", OrganizationalUnit: []string{"openshift"}}, + // KeyUsages is set by GenerateSelfSignedCertificate based on the key algorithm. + Validity: ValidityOneYear(installConfig), + IsCA: true, } - return c.SelfSignedCertKey.Generate(ctx, cfg, "kube-apiserver-to-kubelet-signer") + return c.SelfSignedCertKey.Generate(ctx, cfg, "kube-apiserver-to-kubelet-signer", nil) } // Name returns the human-friendly name of the asset. @@ -124,13 +124,13 @@ func (c *KubeAPIServerLocalhostSignerCertKey) Dependencies() []asset.Asset { // Generate generates the root-ca key and cert pair. func (c *KubeAPIServerLocalhostSignerCertKey) Generate(ctx context.Context, parents asset.Parents) error { cfg := &CertCfg{ - Subject: pkix.Name{CommonName: "kube-apiserver-localhost-signer", OrganizationalUnit: []string{"openshift"}}, - KeyUsages: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - Validity: ValidityTenYears(), - IsCA: true, + Subject: pkix.Name{CommonName: "kube-apiserver-localhost-signer", OrganizationalUnit: []string{"openshift"}}, + // KeyUsages is set by GenerateSelfSignedCertificate based on the key algorithm. + Validity: ValidityTenYears(), + IsCA: true, } - return c.SelfSignedCertKey.Generate(ctx, cfg, "kube-apiserver-localhost-signer") + return c.SelfSignedCertKey.Generate(ctx, cfg, "kube-apiserver-localhost-signer", nil) } // Load reads the asset files from disk. @@ -228,13 +228,13 @@ func (c *KubeAPIServerServiceNetworkSignerCertKey) Dependencies() []asset.Asset // Generate generates the root-ca key and cert pair. func (c *KubeAPIServerServiceNetworkSignerCertKey) Generate(ctx context.Context, parents asset.Parents) error { cfg := &CertCfg{ - Subject: pkix.Name{CommonName: "kube-apiserver-service-network-signer", OrganizationalUnit: []string{"openshift"}}, - KeyUsages: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - Validity: ValidityTenYears(), - IsCA: true, + Subject: pkix.Name{CommonName: "kube-apiserver-service-network-signer", OrganizationalUnit: []string{"openshift"}}, + // KeyUsages is set by GenerateSelfSignedCertificate based on the key algorithm. + Validity: ValidityTenYears(), + IsCA: true, } - return c.SelfSignedCertKey.Generate(ctx, cfg, "kube-apiserver-service-network-signer") + return c.SelfSignedCertKey.Generate(ctx, cfg, "kube-apiserver-service-network-signer", nil) } // Load reads the asset files from disk. @@ -341,13 +341,13 @@ func (c *KubeAPIServerLBSignerCertKey) Dependencies() []asset.Asset { // Generate generates the root-ca key and cert pair. func (c *KubeAPIServerLBSignerCertKey) Generate(ctx context.Context, parents asset.Parents) error { cfg := &CertCfg{ - Subject: pkix.Name{CommonName: "kube-apiserver-lb-signer", OrganizationalUnit: []string{"openshift"}}, - KeyUsages: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - Validity: ValidityTenYears(), - IsCA: true, + Subject: pkix.Name{CommonName: "kube-apiserver-lb-signer", OrganizationalUnit: []string{"openshift"}}, + // KeyUsages is set by GenerateSelfSignedCertificate based on the key algorithm. + Validity: ValidityTenYears(), + IsCA: true, } - return c.SelfSignedCertKey.Generate(ctx, cfg, "kube-apiserver-lb-signer") + return c.SelfSignedCertKey.Generate(ctx, cfg, "kube-apiserver-lb-signer", nil) } // Load reads the asset files from disk. diff --git a/pkg/asset/tls/boundsasigningkey.go b/pkg/asset/tls/boundsasigningkey.go index 1f88f2f295b..d14ae648bcd 100644 --- a/pkg/asset/tls/boundsasigningkey.go +++ b/pkg/asset/tls/boundsasigningkey.go @@ -2,6 +2,8 @@ package tls import ( "context" + "crypto/rsa" + "fmt" "os" "github.com/pkg/errors" @@ -50,10 +52,14 @@ func (sk *BoundSASigningKey) Load(f asset.FileFetcher) (bool, error) { return false, err } - rsaKey, err := PemToPrivateKey(keyFile.Data) + key, err := PemToPrivateKey(keyFile.Data) if err != nil { - logrus.Debugf("Failed to load rsa.PrivateKey from file: %s", err) - return false, errors.Wrap(err, "failed to load rsa.PrivateKey from the file") + logrus.Debugf("Failed to load private key from file: %s", err) + return false, fmt.Errorf("failed to load private key from the file: %w", err) + } + rsaKey, ok := key.(*rsa.PrivateKey) + if !ok { + return false, fmt.Errorf("bound service account signing key must be RSA") } pubData, err := PublicKeyToPem(&rsaKey.PublicKey) if err != nil { diff --git a/pkg/asset/tls/certkey.go b/pkg/asset/tls/certkey.go index 489ee158a05..f51d93ad1ca 100644 --- a/pkg/asset/tls/certkey.go +++ b/pkg/asset/tls/certkey.go @@ -3,14 +3,13 @@ package tls import ( "bytes" "context" - "crypto/rsa" - "crypto/x509" + "fmt" "os" - "github.com/pkg/errors" "github.com/sirupsen/logrus" "github.com/openshift/installer/pkg/asset" + "github.com/openshift/installer/pkg/types" ) // CertInterface contains cert. @@ -128,29 +127,28 @@ func (c *SignedCertKey) Generate(_ context.Context, filenameBase string, appendParent AppendParentChoice, ) error { - var key *rsa.PrivateKey - var crt *x509.Certificate - var err error - caKey, err := PemToPrivateKey(parentCA.Key()) if err != nil { - logrus.Debugf("Failed to parse RSA private key: %s", err) - return errors.Wrap(err, "failed to parse rsa private key") + logrus.Debugf("Failed to parse private key: %s", err) + return fmt.Errorf("failed to parse private key: %w", err) } caCert, err := PemToCertificate(parentCA.Cert()) if err != nil { logrus.Debugf("Failed to parse x509 certificate: %s", err) - return errors.Wrap(err, "failed to parse x509 certificate") + return fmt.Errorf("failed to parse x509 certificate: %w", err) } - key, crt, err = GenerateSignedCertificate(caKey, caCert, cfg) + key, crt, err := GenerateSignedCertificate(caKey, caCert, cfg) if err != nil { logrus.Debugf("Failed to generate signed cert/key pair: %s", err) - return errors.Wrap(err, "failed to generate signed cert/key pair") + return fmt.Errorf("failed to generate signed cert/key pair: %w", err) } - c.KeyRaw = PrivateKeyToPem(key) + c.KeyRaw, err = PrivateKeyToPem(key) + if err != nil { + return fmt.Errorf("failed to encode private key to PEM: %w", err) + } c.CertRaw = CertToPem(crt) if appendParent { @@ -167,17 +165,23 @@ type SelfSignedCertKey struct { CertKey } -// Generate generates a cert/key pair signed by the specified parent CA. +// Generate generates a self-signed cert/key pair using the specified PKI profile. func (c *SelfSignedCertKey) Generate(_ context.Context, cfg *CertCfg, filenameBase string, + pkiConfig *types.PKIConfig, ) error { - key, crt, err := GenerateSelfSignedCertificate(cfg) + params := PKIConfigToKeyParams(pkiConfig) + + key, crt, err := GenerateSelfSignedCertificate(cfg, params) if err != nil { - return errors.Wrap(err, "failed to generate self-signed cert/key pair") + return fmt.Errorf("failed to generate self-signed cert/key pair: %w", err) } - c.KeyRaw = PrivateKeyToPem(key) + c.KeyRaw, err = PrivateKeyToPem(key) + if err != nil { + return fmt.Errorf("failed to encode private key to PEM: %w", err) + } c.CertRaw = CertToPem(crt) c.generateFiles(filenameBase) @@ -192,29 +196,28 @@ func RegenerateSignedCertKey( parentCA CertKeyInterface, appendParent AppendParentChoice, ) ([]byte, []byte, error) { - var key *rsa.PrivateKey - var crt *x509.Certificate - var err error - caKey, err := PemToPrivateKey(parentCA.Key()) if err != nil { - logrus.Debugf("Failed to parse RSA private key: %s", err) - return nil, nil, errors.Wrap(err, "failed to parse rsa private key") + logrus.Debugf("Failed to parse private key: %s", err) + return nil, nil, fmt.Errorf("failed to parse private key: %w", err) } caCert, err := PemToCertificate(parentCA.Cert()) if err != nil { logrus.Debugf("Failed to parse x509 certificate: %s", err) - return nil, nil, errors.Wrap(err, "failed to parse x509 certificate") + return nil, nil, fmt.Errorf("failed to parse x509 certificate: %w", err) } - key, crt, err = GenerateSignedCertificate(caKey, caCert, cfg) - if err != nil { - logrus.Debugf("Failed to generate signed cert/key pair: %s", err) - return nil, nil, errors.Wrap(err, "failed to generate signed cert/key pair") + key, crt, generateErr := GenerateSignedCertificate(caKey, caCert, cfg) + if generateErr != nil { + logrus.Debugf("Failed to generate signed cert/key pair: %s", generateErr) + return nil, nil, fmt.Errorf("failed to generate signed cert/key pair: %w", generateErr) } - keyRaw := PrivateKeyToPem(key) + keyRaw, err := PrivateKeyToPem(key) + if err != nil { + return nil, nil, fmt.Errorf("failed to encode private key to PEM: %w", err) + } certRaw := CertToPem(crt) if appendParent { diff --git a/pkg/asset/tls/certkey_test.go b/pkg/asset/tls/certkey_test.go index c8801c7e3d9..4ceee86a07f 100644 --- a/pkg/asset/tls/certkey_test.go +++ b/pkg/asset/tls/certkey_test.go @@ -2,12 +2,16 @@ package tls import ( "context" + "crypto/ecdsa" + "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "net" "testing" "github.com/stretchr/testify/assert" + + "github.com/openshift/installer/pkg/types" ) func TestSignedCertKeyGenerate(t *testing.T) { @@ -46,8 +50,13 @@ func TestSignedCertKeyGenerate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - rootCA := &RootCA{} - err := rootCA.Generate(context.Background(), nil) + rootCA := &SelfSignedCertKey{} + rootCACfg := &CertCfg{ + Subject: pkix.Name{CommonName: "test-root-ca", OrganizationalUnit: []string{"openshift"}}, + Validity: ValidityTenYears(), + IsCA: true, + } + err := rootCA.Generate(t.Context(), rootCACfg, "test-root-ca", nil) assert.NoError(t, err, "failed to generate root CA") certKey := &SignedCertKey{} @@ -90,3 +99,127 @@ func TestSignedCertKeyGenerate(t *testing.T) { }) } } + +func TestSelfSignedCertKeyGenerateWithPKIConfig(t *testing.T) { + cases := []struct { + name string + pkiConfig *types.PKIConfig + expectKeyType interface{} + expectPubKeyAlg x509.PublicKeyAlgorithm + }{ + { + name: "RSA 4096", + pkiConfig: &types.PKIConfig{ + SignerCertificates: types.CertificateConfig{ + Key: types.KeyConfig{ + Algorithm: types.KeyAlgorithmRSA, + RSA: &types.RSAKeyConfig{KeySize: 4096}, + }, + }, + }, + expectKeyType: &rsa.PrivateKey{}, + expectPubKeyAlg: x509.RSA, + }, + { + name: "ECDSA P384", + pkiConfig: &types.PKIConfig{ + SignerCertificates: types.CertificateConfig{ + Key: types.KeyConfig{ + Algorithm: types.KeyAlgorithmECDSA, + ECDSA: &types.ECDSAKeyConfig{Curve: types.ECDSACurveP384}, + }, + }, + }, + expectKeyType: &ecdsa.PrivateKey{}, + expectPubKeyAlg: x509.ECDSA, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + cfg := &CertCfg{ + Subject: pkix.Name{CommonName: "test-pki-ca", OrganizationalUnit: []string{"openshift"}}, + Validity: ValidityTenYears(), + IsCA: true, + } + + ca := &SelfSignedCertKey{} + err := ca.Generate(t.Context(), cfg, "test-pki-ca", tc.pkiConfig) + assert.NoError(t, err) + + key, err := PemToPrivateKey(ca.Key()) + assert.NoError(t, err) + assert.IsType(t, tc.expectKeyType, key) + + switch k := key.(type) { + case *rsa.PrivateKey: + assert.Equal(t, 4096, k.N.BitLen()) + case *ecdsa.PrivateKey: + assert.Equal(t, "P-384", k.Curve.Params().Name) + } + + cert, err := PemToCertificate(ca.Cert()) + assert.NoError(t, err) + assert.Equal(t, tc.expectPubKeyAlg, cert.PublicKeyAlgorithm) + assert.True(t, cert.IsCA) + }) + } +} + +func TestCrossAlgorithmCertificateSigning(t *testing.T) { + // Generate ECDSA P384 CA + ecdsaPKI := &types.PKIConfig{ + SignerCertificates: types.CertificateConfig{ + Key: types.KeyConfig{ + Algorithm: types.KeyAlgorithmECDSA, + ECDSA: &types.ECDSAKeyConfig{Curve: types.ECDSACurveP384}, + }, + }, + } + rootCA := &SelfSignedCertKey{} + rootCACfg := &CertCfg{ + Subject: pkix.Name{CommonName: "ecdsa-ca", OrganizationalUnit: []string{"openshift"}}, + Validity: ValidityTenYears(), + IsCA: true, + } + err := rootCA.Generate(t.Context(), rootCACfg, "ecdsa-ca", ecdsaPKI) + assert.NoError(t, err) + + // Verify CA key is ECDSA + caKey, err := PemToPrivateKey(rootCA.Key()) + assert.NoError(t, err) + assert.IsType(t, &ecdsa.PrivateKey{}, caKey) + + // Generate RSA leaf signed by ECDSA CA + leafCfg := &CertCfg{ + Subject: pkix.Name{CommonName: "leaf-cert", OrganizationalUnit: []string{"openshift"}}, + KeyUsages: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, + Validity: ValidityTenYears(), + DNSNames: []string{"test.openshift.io"}, + } + certKey := &SignedCertKey{} + err = certKey.Generate(t.Context(), leafCfg, rootCA, "cross-algo-leaf", DoNotAppendParent) + assert.NoError(t, err) + + // Verify leaf key is RSA (SignedCertKey always generates RSA leaf keys) + leafKey, err := PemToPrivateKey(certKey.Key()) + assert.NoError(t, err) + assert.IsType(t, &rsa.PrivateKey{}, leafKey) + + // Verify the leaf cert was signed by the ECDSA CA + leafCert, err := PemToCertificate(certKey.Cert()) + assert.NoError(t, err) + assert.Equal(t, x509.ECDSAWithSHA384, leafCert.SignatureAlgorithm) + + // Verify cert chain: leaf validates against CA + caCert, err := PemToCertificate(rootCA.Cert()) + assert.NoError(t, err) + certPool := x509.NewCertPool() + certPool.AddCert(caCert) + _, err = leafCert.Verify(x509.VerifyOptions{ + Roots: certPool, + DNSName: "test.openshift.io", + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + }) + assert.NoError(t, err, "leaf cert should validate against ECDSA CA") +} diff --git a/pkg/asset/tls/ironictls.go b/pkg/asset/tls/ironictls.go index 7ac8d6094d9..eef4697df92 100644 --- a/pkg/asset/tls/ironictls.go +++ b/pkg/asset/tls/ironictls.go @@ -56,7 +56,7 @@ func (a *IronicTLSCert) Generate(ctx context.Context, dependencies asset.Parents cfg.DNSNames = []string{hostname} logrus.Debugf("Generating TLS certificate for ironic (virtual media)") - return a.SelfSignedCertKey.Generate(ctx, cfg, "ironic/tls") + return a.SelfSignedCertKey.Generate(ctx, cfg, "ironic/tls", nil) } // Name returns the human-friendly name of the asset. diff --git a/pkg/asset/tls/keypair.go b/pkg/asset/tls/keypair.go index b8348b1d765..0b9109692af 100644 --- a/pkg/asset/tls/keypair.go +++ b/pkg/asset/tls/keypair.go @@ -2,8 +2,7 @@ package tls import ( "context" - - "github.com/pkg/errors" + "fmt" "github.com/openshift/installer/pkg/asset" ) @@ -27,15 +26,18 @@ type KeyPair struct { func (k *KeyPair) Generate(_ context.Context, filenameBase string) error { key, err := PrivateKey() if err != nil { - return errors.Wrap(err, "failed to generate private key") + return fmt.Errorf("failed to generate private key: %w", err) } pubkeyData, err := PublicKeyToPem(&key.PublicKey) if err != nil { - return errors.Wrap(err, "failed to get public key data from private key") + return fmt.Errorf("failed to get public key data from private key: %w", err) } - k.Pvt = PrivateKeyToPem(key) + k.Pvt, err = PrivateKeyToPem(key) + if err != nil { + return fmt.Errorf("failed to encode private key to PEM: %w", err) + } k.Pub = pubkeyData k.FileList = []*asset.File{ diff --git a/pkg/asset/tls/kubecontrolplane.go b/pkg/asset/tls/kubecontrolplane.go index 59735d3d6a0..5834ffdb83d 100644 --- a/pkg/asset/tls/kubecontrolplane.go +++ b/pkg/asset/tls/kubecontrolplane.go @@ -26,13 +26,13 @@ func (c *KubeControlPlaneSignerCertKey) Generate(ctx context.Context, parents as installConfig := &installconfig.InstallConfig{} parents.Get(installConfig) cfg := &CertCfg{ - Subject: pkix.Name{CommonName: "kube-control-plane-signer", OrganizationalUnit: []string{"openshift"}}, - KeyUsages: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - Validity: ValidityOneYear(installConfig), - IsCA: true, + Subject: pkix.Name{CommonName: "kube-control-plane-signer", OrganizationalUnit: []string{"openshift"}}, + // KeyUsages is set by GenerateSelfSignedCertificate based on the key algorithm. + Validity: ValidityOneYear(installConfig), + IsCA: true, } - return c.SelfSignedCertKey.Generate(ctx, cfg, "kube-control-plane-signer") + return c.SelfSignedCertKey.Generate(ctx, cfg, "kube-control-plane-signer", nil) } // Name returns the human-friendly name of the asset. diff --git a/pkg/asset/tls/kubelet.go b/pkg/asset/tls/kubelet.go index bfec9000020..acdcaec249e 100644 --- a/pkg/asset/tls/kubelet.go +++ b/pkg/asset/tls/kubelet.go @@ -26,13 +26,13 @@ func (c *KubeletCSRSignerCertKey) Generate(ctx context.Context, parents asset.Pa installConfig := &installconfig.InstallConfig{} parents.Get(installConfig) cfg := &CertCfg{ - Subject: pkix.Name{CommonName: "kubelet-signer", OrganizationalUnit: []string{"openshift"}}, - KeyUsages: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - Validity: ValidityOneDay(installConfig), - IsCA: true, + Subject: pkix.Name{CommonName: "kubelet-signer", OrganizationalUnit: []string{"openshift"}}, + // KeyUsages is set by GenerateSelfSignedCertificate based on the key algorithm. + Validity: ValidityOneDay(installConfig), + IsCA: true, } - return c.SelfSignedCertKey.Generate(ctx, cfg, "kubelet-signer") + return c.SelfSignedCertKey.Generate(ctx, cfg, "kubelet-signer", nil) } // Name returns the human-friendly name of the asset. @@ -116,13 +116,13 @@ func (c *KubeletBootstrapCertSigner) Dependencies() []asset.Asset { // Generate generates the root-ca key and cert pair. func (c *KubeletBootstrapCertSigner) Generate(ctx context.Context, parents asset.Parents) error { cfg := &CertCfg{ - Subject: pkix.Name{CommonName: "kubelet-bootstrap-kubeconfig-signer", OrganizationalUnit: []string{"openshift"}}, - KeyUsages: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - Validity: ValidityTenYears(), - IsCA: true, + Subject: pkix.Name{CommonName: "kubelet-bootstrap-kubeconfig-signer", OrganizationalUnit: []string{"openshift"}}, + // KeyUsages is set by GenerateSelfSignedCertificate based on the key algorithm. + Validity: ValidityTenYears(), + IsCA: true, } - return c.SelfSignedCertKey.Generate(ctx, cfg, "kubelet-bootstrap-kubeconfig-signer") + return c.SelfSignedCertKey.Generate(ctx, cfg, "kubelet-bootstrap-kubeconfig-signer", nil) } // Name returns the human-friendly name of the asset. diff --git a/pkg/asset/tls/root.go b/pkg/asset/tls/root.go index 6529cdc7890..f64d0651b81 100644 --- a/pkg/asset/tls/root.go +++ b/pkg/asset/tls/root.go @@ -2,7 +2,6 @@ package tls import ( "context" - "crypto/x509" "crypto/x509/pkix" "github.com/openshift/installer/pkg/asset" @@ -30,13 +29,13 @@ func (c *RootCA) Dependencies() []asset.Asset { // Generate generates the MCS/Ignition CA. func (c *RootCA) Generate(ctx context.Context, parents asset.Parents) error { cfg := &CertCfg{ - Subject: pkix.Name{CommonName: "root-ca", OrganizationalUnit: []string{"openshift"}}, - KeyUsages: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, - Validity: ValidityTenYears(), - IsCA: true, + Subject: pkix.Name{CommonName: "root-ca", OrganizationalUnit: []string{"openshift"}}, + // KeyUsages is set by GenerateSelfSignedCertificate based on the key algorithm. + Validity: ValidityTenYears(), + IsCA: true, } - return c.SelfSignedCertKey.Generate(ctx, cfg, "root-ca") + return c.SelfSignedCertKey.Generate(ctx, cfg, "root-ca", nil) } // Name returns the human-friendly name of the asset. diff --git a/pkg/asset/tls/tls.go b/pkg/asset/tls/tls.go index f027bcd7bbe..a9700e8a696 100644 --- a/pkg/asset/tls/tls.go +++ b/pkg/asset/tls/tls.go @@ -10,6 +10,7 @@ import ( "crypto/x509" "crypto/x509/pkix" "encoding/asn1" + "fmt" "math" "math/big" "net" @@ -20,12 +21,111 @@ import ( features "github.com/openshift/api/features" "github.com/openshift/installer/pkg/asset/installconfig" + "github.com/openshift/installer/pkg/types" ) const ( - keySize = 2048 + // DefaultRSAKeySize is the default RSA key size used when PKI config is not specified. + DefaultRSAKeySize int32 = 2048 + // DefaultECDSACurve is the default ECDSA curve used when PKI config is not specified. + DefaultECDSACurve = types.ECDSACurveP384 + // DefaultKeyAlgorithm is the default key algorithm used when PKI config is not specified. + DefaultKeyAlgorithm = types.KeyAlgorithmRSA ) +// PrivateKeyParams specifies parameters for private key generation. +type PrivateKeyParams struct { + Algorithm types.KeyAlgorithm + RSAKeySize int32 + ECDSACurve types.ECDSACurve +} + +// PKIConfigToKeyParams converts PKI config to key generation parameters. +// If pkiConfig is nil, returns default RSA 2048 parameters. +func PKIConfigToKeyParams(pkiConfig *types.PKIConfig) PrivateKeyParams { + if pkiConfig == nil { + return PrivateKeyParams{ + Algorithm: DefaultKeyAlgorithm, + RSAKeySize: DefaultRSAKeySize, + } + } + + keyConfig := pkiConfig.SignerCertificates.Key + params := PrivateKeyParams{ + Algorithm: keyConfig.Algorithm, + } + + switch keyConfig.Algorithm { + case types.KeyAlgorithmRSA: + params.RSAKeySize = DefaultRSAKeySize + if keyConfig.RSA != nil { + params.RSAKeySize = keyConfig.RSA.KeySize + } + case types.KeyAlgorithmECDSA: + params.ECDSACurve = DefaultECDSACurve + if keyConfig.ECDSA != nil { + params.ECDSACurve = keyConfig.ECDSA.Curve + } + } + + return params +} + +// GeneratePrivateKeyWithParams generates a private key with the specified parameters. +func GeneratePrivateKeyWithParams(params PrivateKeyParams) (crypto.PrivateKey, error) { + switch params.Algorithm { + case types.KeyAlgorithmRSA: + return GenerateRSAPrivateKey(params.RSAKeySize) + case types.KeyAlgorithmECDSA: + return GenerateECDSAPrivateKey(params.ECDSACurve) + default: + return nil, fmt.Errorf("unsupported algorithm: %s", params.Algorithm) + } +} + +// GenerateRSAPrivateKey generates an RSA private key with the specified size. +func GenerateRSAPrivateKey(keySize int32) (*rsa.PrivateKey, error) { + rsaKey, err := rsa.GenerateKey(rand.Reader, int(keySize)) + if err != nil { + return nil, fmt.Errorf("error generating RSA private key: %w", err) + } + return rsaKey, nil +} + +// GenerateECDSAPrivateKey generates an ECDSA private key with the specified curve. +func GenerateECDSAPrivateKey(curve types.ECDSACurve) (*ecdsa.PrivateKey, error) { + var c elliptic.Curve + + switch curve { + case types.ECDSACurveP256: + c = elliptic.P256() + case types.ECDSACurveP384: + c = elliptic.P384() + case types.ECDSACurveP521: + c = elliptic.P521() + default: + return nil, fmt.Errorf("unsupported ECDSA curve: %s", curve) + } + + ecdsaKey, err := ecdsa.GenerateKey(c, rand.Reader) + if err != nil { + return nil, fmt.Errorf("error generating ECDSA private key: %w", err) + } + return ecdsaKey, nil +} + +// keyUsageForAlgorithm returns appropriate x509.KeyUsage flags for the given algorithm. +// ECDSA keys can only perform digital signatures — they cannot perform key encipherment. +// RSA keys support both digital signatures and key encipherment. +func keyUsageForAlgorithm(algorithm types.KeyAlgorithm) x509.KeyUsage { + switch algorithm { + case types.KeyAlgorithmECDSA: + return x509.KeyUsageDigitalSignature + default: // RSA + return x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature + } +} + // CertCfg contains all needed fields to configure a new certificate type CertCfg struct { DNSNames []string @@ -43,18 +143,13 @@ type rsaPublicKey struct { E int } -// PrivateKey generates an RSA Private key and returns the value +// PrivateKey generates an RSA private key with default parameters (for leaf certs). func PrivateKey() (*rsa.PrivateKey, error) { - rsaKey, err := rsa.GenerateKey(rand.Reader, keySize) - if err != nil { - return nil, errors.Wrap(err, "error generating RSA private key") - } - - return rsaKey, nil + return GenerateRSAPrivateKey(DefaultRSAKeySize) } // SelfSignedCertificate creates a self signed certificate -func SelfSignedCertificate(cfg *CertCfg, key *rsa.PrivateKey) (*x509.Certificate, error) { +func SelfSignedCertificate(cfg *CertCfg, key crypto.Signer) (*x509.Certificate, error) { serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64)) if err != nil { return nil, err @@ -90,7 +185,7 @@ func SignedCertificate( csr *x509.CertificateRequest, key *rsa.PrivateKey, caCert *x509.Certificate, - caKey *rsa.PrivateKey, + caKey crypto.PrivateKey, ) (*x509.Certificate, error) { serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64)) if err != nil { @@ -144,7 +239,7 @@ func generateSubjectKeyID(pub crypto.PublicKey) ([]byte, error) { } // GenerateSignedCertificate generate a key and cert defined by CertCfg and signed by CA. -func GenerateSignedCertificate(caKey *rsa.PrivateKey, caCert *x509.Certificate, +func GenerateSignedCertificate(caKey crypto.PrivateKey, caCert *x509.Certificate, cfg *CertCfg) (*rsa.PrivateKey, *x509.Certificate, error) { // create a private key @@ -175,17 +270,27 @@ func GenerateSignedCertificate(caKey *rsa.PrivateKey, caCert *x509.Certificate, return key, cert, nil } -// GenerateSelfSignedCertificate generates a key/cert pair defined by CertCfg. -func GenerateSelfSignedCertificate(cfg *CertCfg) (*rsa.PrivateKey, *x509.Certificate, error) { - key, err := PrivateKey() +// GenerateSelfSignedCertificate generates a key/cert pair defined by CertCfg +// using the specified key parameters. cfg.KeyUsages is ignored — KeyUsage is +// set based on the algorithm (ECDSA gets DigitalSignature only; RSA gets +// DigitalSignature|KeyEncipherment) plus CertSign for CAs. +func GenerateSelfSignedCertificate(cfg *CertCfg, params PrivateKeyParams) (crypto.PrivateKey, *x509.Certificate, error) { + key, err := GeneratePrivateKeyWithParams(params) if err != nil { - logrus.Debugf("Failed to generate a private key: %s", err) return nil, nil, errors.Wrap(err, "failed to generate private key") } - crt, err := SelfSignedCertificate(cfg, key) + // Set KeyUsage based on algorithm — ECDSA keys cannot perform key encipherment + adjustedCfg := *cfg + baseUsage := keyUsageForAlgorithm(params.Algorithm) + if cfg.IsCA { + adjustedCfg.KeyUsages = baseUsage | x509.KeyUsageCertSign + } else { + adjustedCfg.KeyUsages = baseUsage + } + + crt, err := SelfSignedCertificate(&adjustedCfg, key.(crypto.Signer)) if err != nil { - logrus.Debugf("Failed to create self-signed certificate: %s", err) return nil, nil, errors.Wrap(err, "failed to create self-signed certificate") } return key, crt, nil diff --git a/pkg/asset/tls/tls_test.go b/pkg/asset/tls/tls_test.go index 116cf046fb3..64c770a59e2 100644 --- a/pkg/asset/tls/tls_test.go +++ b/pkg/asset/tls/tls_test.go @@ -1,11 +1,18 @@ package tls import ( + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" + "crypto/rsa" "crypto/x509" "crypto/x509/pkix" "testing" "time" + + "github.com/stretchr/testify/assert" + + "github.com/openshift/installer/pkg/types" ) func TestSelfSignedCertificate(t *testing.T) { @@ -105,3 +112,205 @@ func TestSignedCertificate(t *testing.T) { } } } + +func TestGenerateRSAPrivateKey(t *testing.T) { + cases := []struct { + name string + keySize int32 + }{ + {"RSA 2048", 2048}, + {"RSA 4096", 4096}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + key, err := GenerateRSAPrivateKey(tc.keySize) + assert.NoError(t, err) + assert.IsType(t, &rsa.PrivateKey{}, key) + assert.Equal(t, int(tc.keySize), key.N.BitLen()) + }) + } +} + +func TestGenerateECDSAPrivateKey(t *testing.T) { + cases := []struct { + name string + curve types.ECDSACurve + expected elliptic.Curve + expectErr bool + }{ + {"P256", types.ECDSACurveP256, elliptic.P256(), false}, + {"P384", types.ECDSACurveP384, elliptic.P384(), false}, + {"P521", types.ECDSACurveP521, elliptic.P521(), false}, + {"invalid", "P224", nil, true}, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + key, err := GenerateECDSAPrivateKey(tc.curve) + if tc.expectErr { + assert.Error(t, err) + return + } + assert.NoError(t, err) + assert.IsType(t, &ecdsa.PrivateKey{}, key) + assert.Equal(t, tc.expected, key.Curve) + }) + } +} + +func TestGenerateSelfSignedCertificateWithParams(t *testing.T) { + cases := []struct { + name string + params PrivateKeyParams + expectKeyType interface{} + expectPubKeyAlg x509.PublicKeyAlgorithm + }{ + { + name: "RSA 4096 CA", + params: PrivateKeyParams{ + Algorithm: types.KeyAlgorithmRSA, + RSAKeySize: 4096, + }, + expectKeyType: &rsa.PrivateKey{}, + expectPubKeyAlg: x509.RSA, + }, + { + name: "ECDSA P384 CA", + params: PrivateKeyParams{ + Algorithm: types.KeyAlgorithmECDSA, + ECDSACurve: types.ECDSACurveP384, + }, + expectKeyType: &ecdsa.PrivateKey{}, + expectPubKeyAlg: x509.ECDSA, + }, + { + name: "RSA 2048 CA (default)", + params: PrivateKeyParams{ + Algorithm: types.KeyAlgorithmRSA, + RSAKeySize: 2048, + }, + expectKeyType: &rsa.PrivateKey{}, + expectPubKeyAlg: x509.RSA, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + cfg := &CertCfg{ + Subject: pkix.Name{CommonName: "test-ca", OrganizationalUnit: []string{"openshift"}}, + Validity: time.Hour, + IsCA: true, + } + key, cert, err := GenerateSelfSignedCertificate(cfg, tc.params) + assert.NoError(t, err) + assert.IsType(t, tc.expectKeyType, key) + + switch k := key.(type) { + case *rsa.PrivateKey: + assert.Equal(t, int(tc.params.RSAKeySize), k.N.BitLen()) + case *ecdsa.PrivateKey: + assert.Equal(t, "P-384", k.Curve.Params().Name) + } + + assert.Equal(t, tc.expectPubKeyAlg, cert.PublicKeyAlgorithm) + assert.True(t, cert.IsCA) + }) + } +} + +func TestKeyUsageForAlgorithm(t *testing.T) { + cases := []struct { + name string + params PrivateKeyParams + isCA bool + wantUsage x509.KeyUsage + notUsage x509.KeyUsage + }{ + { + name: "RSA signer", + params: PrivateKeyParams{Algorithm: types.KeyAlgorithmRSA, RSAKeySize: 2048}, + isCA: true, + wantUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign, + }, + { + name: "ECDSA signer", + params: PrivateKeyParams{Algorithm: types.KeyAlgorithmECDSA, ECDSACurve: types.ECDSACurveP256}, + isCA: true, + wantUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, + notUsage: x509.KeyUsageKeyEncipherment, + }, + { + name: "RSA non-CA", + params: PrivateKeyParams{Algorithm: types.KeyAlgorithmRSA, RSAKeySize: 2048}, + isCA: false, + wantUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + notUsage: x509.KeyUsageCertSign, + }, + { + name: "ECDSA non-CA", + params: PrivateKeyParams{Algorithm: types.KeyAlgorithmECDSA, ECDSACurve: types.ECDSACurveP384}, + isCA: false, + wantUsage: x509.KeyUsageDigitalSignature, + notUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageCertSign, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + cfg := &CertCfg{ + Subject: pkix.Name{CommonName: "test", OrganizationalUnit: []string{"openshift"}}, + Validity: time.Hour, + IsCA: tc.isCA, + } + _, cert, err := GenerateSelfSignedCertificate(cfg, tc.params) + assert.NoError(t, err) + assert.Equal(t, tc.wantUsage, cert.KeyUsage, "KeyUsage mismatch") + if tc.notUsage != 0 { + assert.Zero(t, cert.KeyUsage&tc.notUsage, "unexpected KeyUsage bits set") + } + }) + } +} + +func TestSignatureAlgorithmAutoDetection(t *testing.T) { + cases := []struct { + name string + params PrivateKeyParams + expected x509.SignatureAlgorithm + }{ + { + name: "RSA", + params: PrivateKeyParams{Algorithm: types.KeyAlgorithmRSA, RSAKeySize: 2048}, + expected: x509.SHA256WithRSA, + }, + { + name: "ECDSA P256", + params: PrivateKeyParams{Algorithm: types.KeyAlgorithmECDSA, ECDSACurve: types.ECDSACurveP256}, + expected: x509.ECDSAWithSHA256, + }, + { + name: "ECDSA P384", + params: PrivateKeyParams{Algorithm: types.KeyAlgorithmECDSA, ECDSACurve: types.ECDSACurveP384}, + expected: x509.ECDSAWithSHA384, + }, + { + name: "ECDSA P521", + params: PrivateKeyParams{Algorithm: types.KeyAlgorithmECDSA, ECDSACurve: types.ECDSACurveP521}, + expected: x509.ECDSAWithSHA512, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + cfg := &CertCfg{ + Subject: pkix.Name{CommonName: "test-sig", OrganizationalUnit: []string{"openshift"}}, + Validity: time.Hour, + IsCA: true, + } + _, cert, err := GenerateSelfSignedCertificate(cfg, tc.params) + assert.NoError(t, err) + assert.Equal(t, tc.expected, cert.SignatureAlgorithm) + }) + } +} diff --git a/pkg/asset/tls/utils.go b/pkg/asset/tls/utils.go index dd60f187b97..ff11207288f 100644 --- a/pkg/asset/tls/utils.go +++ b/pkg/asset/tls/utils.go @@ -1,24 +1,27 @@ package tls import ( + "crypto" + "crypto/ecdsa" "crypto/rsa" "crypto/x509" "encoding/pem" + "fmt" "github.com/pkg/errors" "github.com/sirupsen/logrus" ) -// PrivateKeyToPem converts an rsa.PrivateKey object to pem string -func PrivateKeyToPem(key *rsa.PrivateKey) []byte { - keyInBytes := x509.MarshalPKCS1PrivateKey(key) - keyinPem := pem.EncodeToMemory( - &pem.Block{ - Type: "RSA PRIVATE KEY", - Bytes: keyInBytes, - }, - ) - return keyinPem +// PrivateKeyToPem converts a private key to PKCS#8 PEM format. +func PrivateKeyToPem(key crypto.PrivateKey) ([]byte, error) { + bytes, err := x509.MarshalPKCS8PrivateKey(key) + if err != nil { + return nil, fmt.Errorf("failed to marshal private key: %w", err) + } + return pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: bytes, + }), nil } // CertToPem converts an x509.Certificate object to a pem string @@ -59,13 +62,32 @@ func PublicKeyToPem(key *rsa.PublicKey) ([]byte, error) { return keyinPem, nil } -// PemToPrivateKey converts a data block to rsa.PrivateKey. -func PemToPrivateKey(data []byte) (*rsa.PrivateKey, error) { +// PemToPrivateKey converts a PEM data block to a private key (RSA or ECDSA). +func PemToPrivateKey(data []byte) (crypto.PrivateKey, error) { block, _ := pem.Decode(data) if block == nil { return nil, errors.Errorf("could not find a PEM block in the private key") } - return x509.ParsePKCS1PrivateKey(block.Bytes) + + switch block.Type { + case "RSA PRIVATE KEY": + return x509.ParsePKCS1PrivateKey(block.Bytes) + case "EC PRIVATE KEY": + return x509.ParseECPrivateKey(block.Bytes) + case "PRIVATE KEY": + key, err := x509.ParsePKCS8PrivateKey(block.Bytes) + if err != nil { + return nil, err + } + switch key.(type) { + case *rsa.PrivateKey, *ecdsa.PrivateKey: + return key, nil + default: + return nil, fmt.Errorf("unsupported PKCS#8 key type: %T", key) + } + default: + return nil, fmt.Errorf("unsupported PEM block type: %s", block.Type) + } } // PemToPublicKey converts a data block to rsa.PublicKey. diff --git a/pkg/asset/tls/utils_test.go b/pkg/asset/tls/utils_test.go new file mode 100644 index 00000000000..8eb13c9ccef --- /dev/null +++ b/pkg/asset/tls/utils_test.go @@ -0,0 +1,110 @@ +package tls + +import ( + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/openshift/installer/pkg/types" +) + +func TestPrivateKeyToPemRoundtrip(t *testing.T) { + cases := []struct { + name string + genFunc func() (interface{}, error) + expectType interface{} + }{ + { + name: "RSA key", + genFunc: func() (interface{}, error) { + return GenerateRSAPrivateKey(2048) + }, + expectType: &rsa.PrivateKey{}, + }, + { + name: "ECDSA P256 key", + genFunc: func() (interface{}, error) { + return GenerateECDSAPrivateKey(types.ECDSACurveP256) + }, + expectType: &ecdsa.PrivateKey{}, + }, + { + name: "ECDSA P384 key", + genFunc: func() (interface{}, error) { + return GenerateECDSAPrivateKey(types.ECDSACurveP384) + }, + expectType: &ecdsa.PrivateKey{}, + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + key, err := tc.genFunc() + assert.NoError(t, err) + + pemBytes, err := PrivateKeyToPem(key) + assert.NoError(t, err) + assert.NotEmpty(t, pemBytes) + + decoded, err := PemToPrivateKey(pemBytes) + assert.NoError(t, err) + assert.IsType(t, tc.expectType, decoded) + }) + } +} + +func TestPemToPrivateKeyFormats(t *testing.T) { + t.Run("invalid PEM", func(t *testing.T) { + _, err := PemToPrivateKey([]byte("not a PEM")) + assert.Error(t, err) + }) + + t.Run("empty data", func(t *testing.T) { + _, err := PemToPrivateKey([]byte{}) + assert.Error(t, err) + }) + + t.Run("RSA PEM block", func(t *testing.T) { + key, err := GenerateRSAPrivateKey(2048) + assert.NoError(t, err) + pemBytes, pemErr := PrivateKeyToPem(key) + assert.NoError(t, pemErr) + + decoded, err := PemToPrivateKey(pemBytes) + assert.NoError(t, err) + _, ok := decoded.(*rsa.PrivateKey) + assert.True(t, ok, "expected *rsa.PrivateKey") + }) + + t.Run("EC PEM block", func(t *testing.T) { + key, err := GenerateECDSAPrivateKey(types.ECDSACurveP256) + assert.NoError(t, err) + pemBytes, pemErr := PrivateKeyToPem(key) + assert.NoError(t, pemErr) + + decoded, err := PemToPrivateKey(pemBytes) + assert.NoError(t, err) + _, ok := decoded.(*ecdsa.PrivateKey) + assert.True(t, ok, "expected *ecdsa.PrivateKey") + }) + + t.Run("PKCS#8 PEM block", func(t *testing.T) { + key, err := GenerateRSAPrivateKey(2048) + assert.NoError(t, err) + pkcs8Bytes, err := x509.MarshalPKCS8PrivateKey(key) + assert.NoError(t, err) + pemBytes := pem.EncodeToMemory(&pem.Block{ + Type: "PRIVATE KEY", + Bytes: pkcs8Bytes, + }) + + decoded, err := PemToPrivateKey(pemBytes) + assert.NoError(t, err) + _, ok := decoded.(*rsa.PrivateKey) + assert.True(t, ok, "expected *rsa.PrivateKey") + }) +}