Skip to content

Commit 5d49e82

Browse files
committed
pki: add unit tests for configurable PKI feature
Add tests covering PKI defaults auto-population, validation integration, feature gate enforcement, and cross-algorithm certificate signing. Key additions: - Test SetInstallConfigDefaults auto-populates PKI defaults (RSA 2048) - Test ValidateInstallConfig catches invalid PKI configs with field paths - Test ConfigurablePKI feature gate with TechPreview and CustomNoUpgrade - Test SelfSignedCertKey.Generate() with non-nil PKI profiles - Test ECDSA CA signing RSA leaf certificate with chain verification Assisted-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f28c415 commit 5d49e82

4 files changed

Lines changed: 305 additions & 0 deletions

File tree

pkg/asset/tls/certkey_test.go

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@ package tls
22

33
import (
44
"context"
5+
"crypto/ecdsa"
6+
"crypto/rsa"
57
"crypto/x509"
68
"crypto/x509/pkix"
79
"net"
810
"testing"
911

1012
"github.com/stretchr/testify/assert"
13+
14+
configv1alpha1 "github.com/openshift/api/config/v1alpha1"
1115
)
1216

1317
func TestSignedCertKeyGenerate(t *testing.T) {
@@ -96,3 +100,139 @@ func TestSignedCertKeyGenerate(t *testing.T) {
96100
})
97101
}
98102
}
103+
104+
func TestSelfSignedCertKeyGenerateWithPKIProfile(t *testing.T) {
105+
cases := []struct {
106+
name string
107+
pkiProfile *configv1alpha1.PKIProfile
108+
expectKeyType interface{}
109+
expectPubKeyAlg x509.PublicKeyAlgorithm
110+
}{
111+
{
112+
name: "RSA 4096 via defaults",
113+
pkiProfile: &configv1alpha1.PKIProfile{
114+
Defaults: configv1alpha1.DefaultCertificateConfig{
115+
Key: configv1alpha1.KeyConfig{
116+
Algorithm: configv1alpha1.KeyAlgorithmRSA,
117+
RSA: configv1alpha1.RSAKeyConfig{KeySize: 4096},
118+
},
119+
},
120+
},
121+
expectKeyType: &rsa.PrivateKey{},
122+
expectPubKeyAlg: x509.RSA,
123+
},
124+
{
125+
name: "ECDSA P384 via defaults",
126+
pkiProfile: &configv1alpha1.PKIProfile{
127+
Defaults: configv1alpha1.DefaultCertificateConfig{
128+
Key: configv1alpha1.KeyConfig{
129+
Algorithm: configv1alpha1.KeyAlgorithmECDSA,
130+
ECDSA: configv1alpha1.ECDSAKeyConfig{Curve: configv1alpha1.ECDSACurveP384},
131+
},
132+
},
133+
},
134+
expectKeyType: &ecdsa.PrivateKey{},
135+
expectPubKeyAlg: x509.ECDSA,
136+
},
137+
{
138+
name: "ECDSA P384 via signerCertificates override",
139+
pkiProfile: &configv1alpha1.PKIProfile{
140+
Defaults: configv1alpha1.DefaultCertificateConfig{
141+
Key: configv1alpha1.KeyConfig{
142+
Algorithm: configv1alpha1.KeyAlgorithmRSA,
143+
RSA: configv1alpha1.RSAKeyConfig{KeySize: 2048},
144+
},
145+
},
146+
SignerCertificates: configv1alpha1.CertificateConfig{
147+
Key: configv1alpha1.KeyConfig{
148+
Algorithm: configv1alpha1.KeyAlgorithmECDSA,
149+
ECDSA: configv1alpha1.ECDSAKeyConfig{Curve: configv1alpha1.ECDSACurveP384},
150+
},
151+
},
152+
},
153+
expectKeyType: &ecdsa.PrivateKey{},
154+
expectPubKeyAlg: x509.ECDSA,
155+
},
156+
}
157+
158+
for _, tc := range cases {
159+
t.Run(tc.name, func(t *testing.T) {
160+
cfg := &CertCfg{
161+
Subject: pkix.Name{CommonName: "test-pki-ca", OrganizationalUnit: []string{"openshift"}},
162+
Validity: ValidityTenYears(),
163+
IsCA: true,
164+
}
165+
166+
ca := &SelfSignedCertKey{}
167+
err := ca.Generate(context.Background(), cfg, "test-pki-ca", tc.pkiProfile)
168+
assert.NoError(t, err)
169+
170+
key, err := PemToPrivateKey(ca.Key())
171+
assert.NoError(t, err)
172+
assert.IsType(t, tc.expectKeyType, key)
173+
174+
cert, err := PemToCertificate(ca.Cert())
175+
assert.NoError(t, err)
176+
assert.Equal(t, tc.expectPubKeyAlg, cert.PublicKeyAlgorithm)
177+
assert.True(t, cert.IsCA)
178+
})
179+
}
180+
}
181+
182+
func TestCrossAlgorithmCertificateSigning(t *testing.T) {
183+
// Generate ECDSA P384 CA
184+
ecdsaProfile := &configv1alpha1.PKIProfile{
185+
SignerCertificates: configv1alpha1.CertificateConfig{
186+
Key: configv1alpha1.KeyConfig{
187+
Algorithm: configv1alpha1.KeyAlgorithmECDSA,
188+
ECDSA: configv1alpha1.ECDSAKeyConfig{Curve: configv1alpha1.ECDSACurveP384},
189+
},
190+
},
191+
}
192+
rootCA := &SelfSignedCertKey{}
193+
rootCACfg := &CertCfg{
194+
Subject: pkix.Name{CommonName: "ecdsa-ca", OrganizationalUnit: []string{"openshift"}},
195+
Validity: ValidityTenYears(),
196+
IsCA: true,
197+
}
198+
err := rootCA.Generate(context.Background(), rootCACfg, "ecdsa-ca", ecdsaProfile)
199+
assert.NoError(t, err)
200+
201+
// Verify CA key is ECDSA
202+
caKey, err := PemToPrivateKey(rootCA.Key())
203+
assert.NoError(t, err)
204+
assert.IsType(t, &ecdsa.PrivateKey{}, caKey)
205+
206+
// Generate RSA leaf signed by ECDSA CA
207+
leafCfg := &CertCfg{
208+
Subject: pkix.Name{CommonName: "leaf-cert", OrganizationalUnit: []string{"openshift"}},
209+
KeyUsages: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
210+
Validity: ValidityTenYears(),
211+
DNSNames: []string{"test.openshift.io"},
212+
}
213+
certKey := &SignedCertKey{}
214+
err = certKey.Generate(context.Background(), leafCfg, rootCA, "cross-algo-leaf", DoNotAppendParent)
215+
assert.NoError(t, err)
216+
217+
// Verify leaf key is RSA (SignedCertKey always generates RSA leaf keys)
218+
leafKey, err := PemToPrivateKey(certKey.Key())
219+
assert.NoError(t, err)
220+
assert.IsType(t, &rsa.PrivateKey{}, leafKey)
221+
222+
// Verify the leaf cert was signed by the ECDSA CA
223+
leafCert, err := PemToCertificate(certKey.Cert())
224+
assert.NoError(t, err)
225+
assert.Equal(t, x509.ECDSAWithSHA384, leafCert.SignatureAlgorithm)
226+
227+
// Verify cert chain: leaf validates against CA
228+
caCert, err := PemToCertificate(rootCA.Cert())
229+
assert.NoError(t, err)
230+
certPool := x509.NewCertPool()
231+
certPool.AddCert(caCert)
232+
_, err = leafCert.Verify(x509.VerifyOptions{
233+
Roots: certPool,
234+
DNSName: "test.openshift.io",
235+
KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny},
236+
})
237+
assert.NoError(t, err, "leaf cert should validate against ECDSA CA")
238+
}

pkg/types/defaults/installconfig_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55

66
"github.com/stretchr/testify/assert"
77

8+
configv1alpha1 "github.com/openshift/api/config/v1alpha1"
89
"github.com/openshift/installer/pkg/ipnet"
910
"github.com/openshift/installer/pkg/types"
1011
"github.com/openshift/installer/pkg/types/aws"
@@ -285,6 +286,80 @@ func TestSetInstallConfigDefaults(t *testing.T) {
285286
return c
286287
}(),
287288
},
289+
{
290+
name: "PKI present with empty defaults - auto-populates RSA 2048",
291+
config: &types.InstallConfig{
292+
PKI: &configv1alpha1.PKIProfile{},
293+
},
294+
expected: func() *types.InstallConfig {
295+
c := defaultInstallConfig()
296+
c.PKI = &configv1alpha1.PKIProfile{
297+
Defaults: configv1alpha1.DefaultCertificateConfig{
298+
Key: configv1alpha1.KeyConfig{
299+
Algorithm: configv1alpha1.KeyAlgorithmRSA,
300+
RSA: configv1alpha1.RSAKeyConfig{KeySize: 2048},
301+
},
302+
},
303+
}
304+
return c
305+
}(),
306+
},
307+
{
308+
name: "PKI present with algorithm already set - not overwritten",
309+
config: &types.InstallConfig{
310+
PKI: &configv1alpha1.PKIProfile{
311+
Defaults: configv1alpha1.DefaultCertificateConfig{
312+
Key: configv1alpha1.KeyConfig{
313+
Algorithm: configv1alpha1.KeyAlgorithmECDSA,
314+
ECDSA: configv1alpha1.ECDSAKeyConfig{Curve: configv1alpha1.ECDSACurveP384},
315+
},
316+
},
317+
},
318+
},
319+
expected: func() *types.InstallConfig {
320+
c := defaultInstallConfig()
321+
c.PKI = &configv1alpha1.PKIProfile{
322+
Defaults: configv1alpha1.DefaultCertificateConfig{
323+
Key: configv1alpha1.KeyConfig{
324+
Algorithm: configv1alpha1.KeyAlgorithmECDSA,
325+
ECDSA: configv1alpha1.ECDSAKeyConfig{Curve: configv1alpha1.ECDSACurveP384},
326+
},
327+
},
328+
}
329+
return c
330+
}(),
331+
},
332+
{
333+
name: "PKI with only signerCertificates - defaults auto-populated",
334+
config: &types.InstallConfig{
335+
PKI: &configv1alpha1.PKIProfile{
336+
SignerCertificates: configv1alpha1.CertificateConfig{
337+
Key: configv1alpha1.KeyConfig{
338+
Algorithm: configv1alpha1.KeyAlgorithmECDSA,
339+
ECDSA: configv1alpha1.ECDSAKeyConfig{Curve: configv1alpha1.ECDSACurveP256},
340+
},
341+
},
342+
},
343+
},
344+
expected: func() *types.InstallConfig {
345+
c := defaultInstallConfig()
346+
c.PKI = &configv1alpha1.PKIProfile{
347+
Defaults: configv1alpha1.DefaultCertificateConfig{
348+
Key: configv1alpha1.KeyConfig{
349+
Algorithm: configv1alpha1.KeyAlgorithmRSA,
350+
RSA: configv1alpha1.RSAKeyConfig{KeySize: 2048},
351+
},
352+
},
353+
SignerCertificates: configv1alpha1.CertificateConfig{
354+
Key: configv1alpha1.KeyConfig{
355+
Algorithm: configv1alpha1.KeyAlgorithmECDSA,
356+
ECDSA: configv1alpha1.ECDSAKeyConfig{Curve: configv1alpha1.ECDSACurveP256},
357+
},
358+
},
359+
}
360+
return c
361+
}(),
362+
},
288363
}
289364
for _, tc := range cases {
290365
t.Run(tc.name, func(t *testing.T) {

pkg/types/validation/featuregate_test.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
capz "sigs.k8s.io/cluster-api-provider-azure/api/v1beta1"
88

99
v1 "github.com/openshift/api/config/v1"
10+
configv1alpha1 "github.com/openshift/api/config/v1alpha1"
1011
"github.com/openshift/installer/pkg/types"
1112
"github.com/openshift/installer/pkg/types/azure"
1213
"github.com/openshift/installer/pkg/types/dns"
@@ -194,6 +195,41 @@ func TestFeatureGates(t *testing.T) {
194195
return c
195196
}(),
196197
},
198+
{
199+
name: "PKI config present without feature gate - error",
200+
installConfig: func() *types.InstallConfig {
201+
c := validInstallConfig()
202+
c.PKI = &configv1alpha1.PKIProfile{}
203+
return c
204+
}(),
205+
expected: `^pki: Forbidden: this field is protected by the ConfigurablePKI feature gate which must be enabled through either the TechPreviewNoUpgrade or CustomNoUpgrade feature set$`,
206+
},
207+
{
208+
name: "PKI config present with TechPreviewNoUpgrade - passes",
209+
installConfig: func() *types.InstallConfig {
210+
c := validInstallConfig()
211+
c.FeatureSet = v1.TechPreviewNoUpgrade
212+
c.PKI = &configv1alpha1.PKIProfile{}
213+
return c
214+
}(),
215+
},
216+
{
217+
name: "PKI config present with CustomNoUpgrade and ConfigurablePKI - passes",
218+
installConfig: func() *types.InstallConfig {
219+
c := validInstallConfig()
220+
c.FeatureSet = v1.CustomNoUpgrade
221+
c.FeatureGates = []string{"ConfigurablePKI=true"}
222+
c.PKI = &configv1alpha1.PKIProfile{}
223+
return c
224+
}(),
225+
},
226+
{
227+
name: "PKI nil without feature gate - no error",
228+
installConfig: func() *types.InstallConfig {
229+
c := validInstallConfig()
230+
return c
231+
}(),
232+
},
197233
{
198234
name: "OKD featureset requires SCOS-compiled installer",
199235
installConfig: func() *types.InstallConfig {

pkg/types/validation/installconfig_test.go

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
utilsslice "k8s.io/utils/strings/slices"
1414

1515
configv1 "github.com/openshift/api/config/v1"
16+
configv1alpha1 "github.com/openshift/api/config/v1alpha1"
1617
operv1 "github.com/openshift/api/operator/v1"
1718
"github.com/openshift/installer/pkg/ipnet"
1819
"github.com/openshift/installer/pkg/types"
@@ -3043,6 +3044,59 @@ func TestValidateInstallConfig(t *testing.T) {
30433044
}(),
30443045
expectedError: "Unsupported OS Image Stream. Supported values are: rhel-9, rhel-10",
30453046
},
3047+
{
3048+
name: "valid PKI with signer certificates",
3049+
installConfig: func() *types.InstallConfig {
3050+
c := validInstallConfig()
3051+
c.FeatureSet = configv1.TechPreviewNoUpgrade
3052+
c.PKI = &configv1alpha1.PKIProfile{
3053+
Defaults: configv1alpha1.DefaultCertificateConfig{
3054+
Key: configv1alpha1.KeyConfig{
3055+
Algorithm: configv1alpha1.KeyAlgorithmRSA,
3056+
RSA: configv1alpha1.RSAKeyConfig{KeySize: 4096},
3057+
},
3058+
},
3059+
SignerCertificates: configv1alpha1.CertificateConfig{
3060+
Key: configv1alpha1.KeyConfig{
3061+
Algorithm: configv1alpha1.KeyAlgorithmECDSA,
3062+
ECDSA: configv1alpha1.ECDSAKeyConfig{Curve: configv1alpha1.ECDSACurveP384},
3063+
},
3064+
},
3065+
}
3066+
return c
3067+
}(),
3068+
},
3069+
{
3070+
name: "invalid PKI signer with unsupported algorithm",
3071+
installConfig: func() *types.InstallConfig {
3072+
c := validInstallConfig()
3073+
c.PKI = &configv1alpha1.PKIProfile{
3074+
SignerCertificates: configv1alpha1.CertificateConfig{
3075+
Key: configv1alpha1.KeyConfig{
3076+
Algorithm: "EdDSA",
3077+
},
3078+
},
3079+
}
3080+
return c
3081+
}(),
3082+
expectedError: `pki\.signerCertificates\.key\.algorithm: Unsupported value: "EdDSA"`,
3083+
},
3084+
{
3085+
name: "invalid PKI signer with bad RSA key size",
3086+
installConfig: func() *types.InstallConfig {
3087+
c := validInstallConfig()
3088+
c.PKI = &configv1alpha1.PKIProfile{
3089+
SignerCertificates: configv1alpha1.CertificateConfig{
3090+
Key: configv1alpha1.KeyConfig{
3091+
Algorithm: configv1alpha1.KeyAlgorithmRSA,
3092+
RSA: configv1alpha1.RSAKeyConfig{KeySize: 1024},
3093+
},
3094+
},
3095+
}
3096+
return c
3097+
}(),
3098+
expectedError: `pki\.signerCertificates\.key\.rsa\.keySize: Invalid value: 1024`,
3099+
},
30463100
}
30473101
for _, tc := range cases {
30483102
t.Run(tc.name, func(t *testing.T) {

0 commit comments

Comments
 (0)