diff --git a/pkg/kubeapiserver/options/authentication.go b/pkg/kubeapiserver/options/authentication.go index 11501a2f3cb..1e83d015559 100644 --- a/pkg/kubeapiserver/options/authentication.go +++ b/pkg/kubeapiserver/options/authentication.go @@ -40,6 +40,7 @@ import ( "k8s.io/apiserver/pkg/server/egressselector" genericoptions "k8s.io/apiserver/pkg/server/options" utilfeature "k8s.io/apiserver/pkg/util/feature" + "k8s.io/apiserver/plugin/pkg/authenticator/token/oidc" "k8s.io/client-go/informers" "k8s.io/client-go/kubernetes" v1listers "k8s.io/client-go/listers/core/v1" @@ -461,6 +462,9 @@ func (o *BuiltInAuthenticationOptions) ToAuthenticationConfig() (kubeauthenticat if ret.AuthenticationConfig, err = loadAuthenticationConfig(o.AuthenticationConfigFile); err != nil { return kubeauthenticator.Config{}, err } + // all known signing algs are allowed when using authentication config + // TODO: what we really want to express is 'any alg is fine as long it matches a public key' + ret.OIDCSigningAlgs = oidc.AllValidSigningAlgorithms() } else if o.OIDC != nil && len(o.OIDC.IssuerURL) > 0 && len(o.OIDC.ClientID) > 0 { usernamePrefix := o.OIDC.UsernamePrefix diff --git a/pkg/kubeapiserver/options/authentication_test.go b/pkg/kubeapiserver/options/authentication_test.go index eed32f1cab8..c2b06c3950e 100644 --- a/pkg/kubeapiserver/options/authentication_test.go +++ b/pkg/kubeapiserver/options/authentication_test.go @@ -446,6 +446,8 @@ func TestBuiltInAuthenticationOptionsAddFlags(t *testing.T) { } func TestToAuthenticationConfig_OIDC(t *testing.T) { + defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)() + testCases := []struct { name string args []string @@ -640,6 +642,43 @@ func TestToAuthenticationConfig_OIDC(t *testing.T) { OIDCSigningAlgs: []string{"RS256"}, }, }, + { + name: "basic authentication configuration", + args: []string{ + "--authentication-config=" + writeTempFile(t, ` +apiVersion: apiserver.config.k8s.io/v1alpha1 +kind: AuthenticationConfiguration +jwt: +- issuer: + url: https://test-issuer + audiences: [ "🐼" ] + claimMappings: + username: + claim: sub + prefix: "" +`), + }, + expectConfig: kubeauthenticator.Config{ + TokenSuccessCacheTTL: 10 * time.Second, + AuthenticationConfig: &apiserver.AuthenticationConfiguration{ + JWT: []apiserver.JWTAuthenticator{ + { + Issuer: apiserver.Issuer{ + URL: "https://test-issuer", + Audiences: []string{"🐼"}, + }, + ClaimMappings: apiserver.ClaimMappings{ + Username: apiserver.PrefixedClaimOrExpression{ + Claim: "sub", + Prefix: pointer.String(""), + }, + }, + }, + }, + }, + OIDCSigningAlgs: []string{"ES256", "ES384", "ES512", "PS256", "PS384", "PS512", "RS256", "RS384", "RS512"}, + }, + }, } for _, testcase := range testCases { diff --git a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go index 26e6c7335d6..79ef5c92076 100644 --- a/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go +++ b/staging/src/k8s.io/apiserver/plugin/pkg/authenticator/token/oidc/oidc.go @@ -204,8 +204,11 @@ func (a *Authenticator) Close() { a.cancel() } -// whitelist of signing algorithms to ensure users don't mistakenly pass something -// goofy. +func AllValidSigningAlgorithms() []string { + return sets.List(sets.KeySet(allowedSigningAlgs)) +} + +// allowlist of signing algorithms to ensure users don't mistakenly pass something goofy. var allowedSigningAlgs = map[string]bool{ oidc.RS256: true, oidc.RS384: true, diff --git a/test/integration/apiserver/oidc/oidc_test.go b/test/integration/apiserver/oidc/oidc_test.go index a681ccc2ba3..ea6da90cad9 100644 --- a/test/integration/apiserver/oidc/oidc_test.go +++ b/test/integration/apiserver/oidc/oidc_test.go @@ -18,6 +18,8 @@ package oidc import ( "context" + "crypto/ecdsa" + "crypto/elliptic" "crypto/rand" "crypto/rsa" "crypto/tls" @@ -35,11 +37,13 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gopkg.in/square/go-jose.v2" authenticationv1 "k8s.io/api/authentication/v1" rbacv1 "k8s.io/api/rbac/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + utilrand "k8s.io/apimachinery/pkg/util/rand" "k8s.io/apiserver/pkg/features" utilfeature "k8s.io/apiserver/pkg/util/feature" "k8s.io/client-go/kubernetes" @@ -116,29 +120,10 @@ func TestStructuredAuthenticationConfig(t *testing.T) { } func runTests(t *testing.T, useAuthenticationConfig bool) { - var tests = []struct { - name string - configureInfrastructure func(t *testing.T, fn authenticationConfigFunc) ( - oidcServer *utilsoidc.TestServer, - apiServer *kubeapiserverapptesting.TestServer, - signingPrivateKey *rsa.PrivateKey, - caCertContent []byte, - caFilePath string, - ) - configureOIDCServerBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) - configureClient func( - t *testing.T, - restCfg *rest.Config, - caCert []byte, - certPath, - oidcServerURL, - oidcServerTokenURL string, - ) kubernetes.Interface - assertErrFn func(t *testing.T, errorToCheck error) - }{ + var tests = []singleTest[*rsa.PrivateKey, *rsa.PublicKey]{ { name: "ID token is ok", - configureInfrastructure: configureTestInfrastructure, + configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey], configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { idTokenLifetime := time.Second * 1200 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT( @@ -161,7 +146,7 @@ func runTests(t *testing.T, useAuthenticationConfig bool) { }, { name: "ID token is expired", - configureInfrastructure: configureTestInfrastructure, + configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey], configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { configureOIDCServerToReturnExpiredIDToken(t, 2, oidcServer, signingPrivateKey) }, @@ -172,7 +157,7 @@ func runTests(t *testing.T, useAuthenticationConfig bool) { }, { name: "wrong client ID", - configureInfrastructure: configureTestInfrastructure, + configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey], configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, _ *rsa.PrivateKey) { oidcServer.TokenHandler().EXPECT().Token().Times(2).Return(utilsoidc.Token{}, utilsoidc.ErrBadClientID) }, @@ -189,7 +174,7 @@ func runTests(t *testing.T, useAuthenticationConfig bool) { }, { name: "client has wrong CA", - configureInfrastructure: configureTestInfrastructure, + configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey], configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, _ *rsa.PrivateKey) {}, configureClient: func(t *testing.T, restCfg *rest.Config, caCert []byte, _, oidcServerURL, oidcServerTokenURL string) kubernetes.Interface { tempDir := t.TempDir() @@ -207,7 +192,7 @@ func runTests(t *testing.T, useAuthenticationConfig bool) { }, { name: "refresh flow does not return ID Token", - configureInfrastructure: configureTestInfrastructure, + configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey], configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { configureOIDCServerToReturnExpiredIDToken(t, 1, oidcServer, signingPrivateKey) oidcServer.TokenHandler().EXPECT().Token().Times(1).Return(utilsoidc.Token{ @@ -230,7 +215,7 @@ func runTests(t *testing.T, useAuthenticationConfig bool) { }, { name: "ID token signature can not be verified due to wrong JWKs", - configureInfrastructure: func(t *testing.T, fn authenticationConfigFunc) ( + configureInfrastructure: func(t *testing.T, fn authenticationConfigFunc, keyFunc func(t *testing.T) (*rsa.PrivateKey, *rsa.PublicKey)) ( oidcServer *utilsoidc.TestServer, apiServer *kubeapiserverapptesting.TestServer, signingPrivateKey *rsa.PrivateKey, @@ -239,8 +224,7 @@ func runTests(t *testing.T, useAuthenticationConfig bool) { ) { caCertContent, _, caFilePath, caKeyFilePath := generateCert(t) - signingPrivateKey, wantErr := rsa.GenerateKey(rand.Reader, rsaKeyBitSize) - require.NoError(t, wantErr) + signingPrivateKey, _ = keyFunc(t) oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath) @@ -260,16 +244,16 @@ jwt: claim: sub prefix: %s `, oidcServer.URL(), defaultOIDCClientID, indentCertificateAuthority(string(caCertContent)), defaultOIDCUsernamePrefix) - apiServer = startTestAPIServerForOIDC(t, "", "", "", authenticationConfig) + apiServer = startTestAPIServerForOIDC(t, "", "", "", authenticationConfig, &signingPrivateKey.PublicKey) } else { - apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath, "") + apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath, "", &signingPrivateKey.PublicKey) } adminClient := kubernetes.NewForConfigOrDie(apiServer.ClientConfig) configureRBAC(t, adminClient, defaultRole, defaultRoleBinding) - anotherSigningPrivateKey, wantErr := rsa.GenerateKey(rand.Reader, rsaKeyBitSize) - require.NoError(t, wantErr) + anotherSigningPrivateKey, _ := keyFunc(t) + oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, &anotherSigningPrivateKey.PublicKey)) return oidcServer, apiServer, signingPrivateKey, caCertContent, caFilePath @@ -296,11 +280,69 @@ jwt: } for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fn := func(t *testing.T, issuerURL, caCert string) string { return "" } - if useAuthenticationConfig { - fn = func(t *testing.T, issuerURL, caCert string) string { - return fmt.Sprintf(` + t.Run(tt.name, singleTestRunner(useAuthenticationConfig, rsaGenerateKey, tt)) + } + + for _, tt := range []singleTest[*ecdsa.PrivateKey, *ecdsa.PublicKey]{ + { + name: "ID token is ok", + configureInfrastructure: configureTestInfrastructure[*ecdsa.PrivateKey, *ecdsa.PublicKey], + configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *ecdsa.PrivateKey) { + idTokenLifetime := time.Second * 1200 + oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT( + t, + signingPrivateKey, + map[string]interface{}{ + "iss": oidcServer.URL(), + "sub": defaultOIDCClaimedUsername, + "aud": defaultOIDCClientID, + "exp": time.Now().Add(idTokenLifetime).Unix(), + }, + defaultStubAccessToken, + defaultStubRefreshToken, + )) + }, + configureClient: configureClientFetchingOIDCCredentials, + assertErrFn: func(t *testing.T, errorToCheck error) { + assert.NoError(t, errorToCheck) + }, + }, + } { + t.Run(tt.name, singleTestRunner(useAuthenticationConfig, ecdsaGenerateKey, tt)) + } +} + +type singleTest[K utilsoidc.JosePrivateKey, L utilsoidc.JosePublicKey] struct { + name string + configureInfrastructure func(t *testing.T, fn authenticationConfigFunc, keyFunc func(t *testing.T) (K, L)) ( + oidcServer *utilsoidc.TestServer, + apiServer *kubeapiserverapptesting.TestServer, + signingPrivateKey K, + caCertContent []byte, + caFilePath string, + ) + configureOIDCServerBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey K) + configureClient func( + t *testing.T, + restCfg *rest.Config, + caCert []byte, + certPath, + oidcServerURL, + oidcServerTokenURL string, + ) kubernetes.Interface + assertErrFn func(t *testing.T, errorToCheck error) +} + +func singleTestRunner[K utilsoidc.JosePrivateKey, L utilsoidc.JosePublicKey]( + useAuthenticationConfig bool, + keyFunc func(t *testing.T) (K, L), + tt singleTest[K, L], +) func(t *testing.T) { + return func(t *testing.T) { + fn := func(t *testing.T, issuerURL, caCert string) string { return "" } + if useAuthenticationConfig { + fn = func(t *testing.T, issuerURL, caCert string) string { + return fmt.Sprintf(` apiVersion: apiserver.config.k8s.io/v1alpha1 kind: AuthenticationConfiguration jwt: @@ -315,31 +357,32 @@ jwt: claim: sub prefix: %s `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert), defaultOIDCUsernamePrefix) - } } - oidcServer, apiServer, signingPrivateKey, caCert, certPath := tt.configureInfrastructure(t, fn) + } + oidcServer, apiServer, signingPrivateKey, caCert, certPath := tt.configureInfrastructure(t, fn, keyFunc) - tt.configureOIDCServerBehaviour(t, oidcServer, signingPrivateKey) + tt.configureOIDCServerBehaviour(t, oidcServer, signingPrivateKey) - tokenURL, err := oidcServer.TokenURL() - require.NoError(t, err) + tokenURL, err := oidcServer.TokenURL() + require.NoError(t, err) - client := tt.configureClient(t, apiServer.ClientConfig, caCert, certPath, oidcServer.URL(), tokenURL) + client := tt.configureClient(t, apiServer.ClientConfig, caCert, certPath, oidcServer.URL(), tokenURL) - ctx := testContext(t) - _, err = client.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{}) + ctx := testContext(t) + _, err = client.CoreV1().Pods(defaultNamespace).List(ctx, metav1.ListOptions{}) - tt.assertErrFn(t, err) - }) + tt.assertErrFn(t, err) } } func TestUpdatingRefreshTokenInCaseOfExpiredIDToken(t *testing.T) { - var tests = []struct { + type testRun[K utilsoidc.JosePrivateKey] struct { name string - configureUpdatingTokenBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) + configureUpdatingTokenBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey K) assertErrFn func(t *testing.T, errorToCheck error) - }{ + } + + var tests = []testRun[*rsa.PrivateKey]{ { name: "cache returns stale client if refresh token is not updated in config", configureUpdatingTokenBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { @@ -369,7 +412,7 @@ func TestUpdatingRefreshTokenInCaseOfExpiredIDToken(t *testing.T) { }, } - oidcServer, apiServer, signingPrivateKey, caCert, certPath := configureTestInfrastructure(t, func(t *testing.T, _, _ string) string { return "" }) + oidcServer, apiServer, signingPrivateKey, caCert, certPath := configureTestInfrastructure(t, func(t *testing.T, _, _ string) string { return "" }, rsaGenerateKey) tokenURL, err := oidcServer.TokenURL() require.NoError(t, err) @@ -399,17 +442,17 @@ func TestUpdatingRefreshTokenInCaseOfExpiredIDToken(t *testing.T) { func TestStructuredAuthenticationConfigCEL(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthenticationConfiguration, true)() - tests := []struct { + type testRun[K utilsoidc.JosePrivateKey, L utilsoidc.JosePublicKey] struct { name string authConfigFn authenticationConfigFunc - configureInfrastructure func(t *testing.T, fn authenticationConfigFunc) ( + configureInfrastructure func(t *testing.T, fn authenticationConfigFunc, keyFunc func(t *testing.T) (K, L)) ( oidcServer *utilsoidc.TestServer, apiServer *kubeapiserverapptesting.TestServer, signingPrivateKey *rsa.PrivateKey, caCertContent []byte, caFilePath string, ) - configureOIDCServerBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) + configureOIDCServerBehaviour func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey K) configureClient func( t *testing.T, restCfg *rest.Config, @@ -420,7 +463,9 @@ func TestStructuredAuthenticationConfigCEL(t *testing.T) { ) kubernetes.Interface assertErrFn func(t *testing.T, errorToCheck error) wantUser *authenticationv1.UserInfo - }{ + } + + tests := []testRun[*rsa.PrivateKey, *rsa.PublicKey]{ { name: "username CEL expression is ok", authConfigFn: func(t *testing.T, issuerURL, caCert string) string { @@ -441,7 +486,7 @@ jwt: expression: "'k8s-' + claims.sub" `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert)) }, - configureInfrastructure: configureTestInfrastructure, + configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey], configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { idTokenLifetime := time.Second * 1200 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT( @@ -488,7 +533,7 @@ jwt: expression: '(claims.roles.split(",") + claims.other_roles.split(",")).map(role, "prefix:" + role)' `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert)) }, - configureInfrastructure: configureTestInfrastructure, + configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey], configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { idTokenLifetime := time.Second * 1200 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT( @@ -538,7 +583,7 @@ jwt: message: "the hd claim must be set to example.com" `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert)) }, - configureInfrastructure: configureTestInfrastructure, + configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey], configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { idTokenLifetime := time.Second * 1200 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT( @@ -588,7 +633,7 @@ jwt: message: "example.org/foo must be bar and example.org/baz must be qux" `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert)) }, - configureInfrastructure: configureTestInfrastructure, + configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey], configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { idTokenLifetime := time.Second * 1200 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT( @@ -640,7 +685,7 @@ jwt: expression: "claims.uid" `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert)) }, - configureInfrastructure: configureTestInfrastructure, + configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey], configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { idTokenLifetime := time.Second * 1200 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT( @@ -692,7 +737,7 @@ jwt: message: "groups cannot used reserved system: prefix" `, issuerURL, defaultOIDCClientID, indentCertificateAuthority(caCert)) }, - configureInfrastructure: configureTestInfrastructure, + configureInfrastructure: configureTestInfrastructure[*rsa.PrivateKey, *rsa.PublicKey], configureOIDCServerBehaviour: func(t *testing.T, oidcServer *utilsoidc.TestServer, signingPrivateKey *rsa.PrivateKey) { idTokenLifetime := time.Second * 1200 oidcServer.TokenHandler().EXPECT().Token().Times(1).DoAndReturn(utilsoidc.TokenHandlerBehaviorReturningPredefinedJWT( @@ -720,7 +765,7 @@ jwt: for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - oidcServer, apiServer, signingPrivateKey, caCert, certPath := tt.configureInfrastructure(t, tt.authConfigFn) + oidcServer, apiServer, signingPrivateKey, caCert, certPath := tt.configureInfrastructure(t, tt.authConfigFn, rsaGenerateKey) tt.configureOIDCServerBehaviour(t, oidcServer, signingPrivateKey) @@ -743,10 +788,28 @@ jwt: } } -func configureTestInfrastructure(t *testing.T, fn authenticationConfigFunc) ( +func rsaGenerateKey(t *testing.T) (*rsa.PrivateKey, *rsa.PublicKey) { + t.Helper() + + privateKey, err := rsa.GenerateKey(rand.Reader, rsaKeyBitSize) + require.NoError(t, err) + + return privateKey, &privateKey.PublicKey +} + +func ecdsaGenerateKey(t *testing.T) (*ecdsa.PrivateKey, *ecdsa.PublicKey) { + t.Helper() + + privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) + require.NoError(t, err) + + return privateKey, &privateKey.PublicKey +} + +func configureTestInfrastructure[K utilsoidc.JosePrivateKey, L utilsoidc.JosePublicKey](t *testing.T, fn authenticationConfigFunc, keyFunc func(t *testing.T) (K, L)) ( oidcServer *utilsoidc.TestServer, apiServer *kubeapiserverapptesting.TestServer, - signingPrivateKey *rsa.PrivateKey, + signingPrivateKey K, caCertContent []byte, caFilePath string, ) { @@ -754,19 +817,18 @@ func configureTestInfrastructure(t *testing.T, fn authenticationConfigFunc) ( caCertContent, _, caFilePath, caKeyFilePath := generateCert(t) - signingPrivateKey, err := rsa.GenerateKey(rand.Reader, rsaKeyBitSize) - require.NoError(t, err) + signingPrivateKey, publicKey := keyFunc(t) oidcServer = utilsoidc.BuildAndRunTestServer(t, caFilePath, caKeyFilePath) authenticationConfig := fn(t, oidcServer.URL(), string(caCertContent)) if len(authenticationConfig) > 0 { - apiServer = startTestAPIServerForOIDC(t, "", "", "", authenticationConfig) + apiServer = startTestAPIServerForOIDC(t, "", "", "", authenticationConfig, publicKey) } else { - apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath, "") + apiServer = startTestAPIServerForOIDC(t, oidcServer.URL(), defaultOIDCClientID, caFilePath, "", publicKey) } - oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, &signingPrivateKey.PublicKey)) + oidcServer.JwksHandler().EXPECT().KeySet().AnyTimes().DoAndReturn(utilsoidc.DefaultJwksHandlerBehavior(t, publicKey)) adminClient := kubernetes.NewForConfigOrDie(apiServer.ClientConfig) configureRBAC(t, adminClient, defaultRole, defaultRoleBinding) @@ -815,7 +877,7 @@ func configureClientConfigForOIDC(t *testing.T, config *rest.Config, clientID, c return cfg } -func startTestAPIServerForOIDC(t *testing.T, oidcURL, oidcClientID, oidcCAFilePath, authenticationConfigYAML string) *kubeapiserverapptesting.TestServer { +func startTestAPIServerForOIDC[L utilsoidc.JosePublicKey](t *testing.T, oidcURL, oidcClientID, oidcCAFilePath, authenticationConfigYAML string, publicKey L) *kubeapiserverapptesting.TestServer { t.Helper() var customFlags []string @@ -828,6 +890,7 @@ func startTestAPIServerForOIDC(t *testing.T, oidcURL, oidcClientID, oidcCAFilePa fmt.Sprintf("--oidc-ca-file=%s", oidcCAFilePath), fmt.Sprintf("--oidc-username-prefix=%s", defaultOIDCUsernamePrefix), } + customFlags = append(customFlags, maybeSetSigningAlgs(publicKey)...) } customFlags = append(customFlags, "--authorization-mode=RBAC") @@ -844,6 +907,18 @@ func startTestAPIServerForOIDC(t *testing.T, oidcURL, oidcClientID, oidcCAFilePa return &server } +func maybeSetSigningAlgs[K utilsoidc.JoseKey](key K) []string { + alg := utilsoidc.GetSignatureAlgorithm(key) + if alg == jose.RS256 && randomBool() { + return nil // check the default case of RS256 by not always setting the flag + } + return []string{ + fmt.Sprintf("--oidc-signing-algs=%s", alg), // all other algs need to be manually set + } +} + +func randomBool() bool { return utilrand.Int()%2 == 1 } + func fetchOIDCCredentials(t *testing.T, oidcTokenURL string, caCertContent []byte) (idToken, refreshToken string) { t.Helper() diff --git a/test/utils/oidc/testserver.go b/test/utils/oidc/testserver.go index 8eb1167accb..6d425b426db 100644 --- a/test/utils/oidc/testserver.go +++ b/test/utils/oidc/testserver.go @@ -18,6 +18,7 @@ package oidc import ( "crypto" + "crypto/ecdsa" "crypto/rsa" "crypto/tls" "encoding/hex" @@ -170,17 +171,21 @@ func BuildAndRunTestServer(t *testing.T, caPath, caKeyPath string) *TestServer { return oidcServer } +type JosePrivateKey interface { + *rsa.PrivateKey | *ecdsa.PrivateKey +} + // TokenHandlerBehaviorReturningPredefinedJWT describes the scenario when signed JWT token is being created. // This behavior should being applied to the MockTokenHandler. -func TokenHandlerBehaviorReturningPredefinedJWT( +func TokenHandlerBehaviorReturningPredefinedJWT[K JosePrivateKey]( t *testing.T, - rsaPrivateKey *rsa.PrivateKey, + privateKey K, claims map[string]interface{}, accessToken, refreshToken string, ) func() (Token, error) { t.Helper() return func() (Token, error) { - signer, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.RS256, Key: rsaPrivateKey}, nil) + signer, err := jose.NewSigner(jose.SigningKey{Algorithm: GetSignatureAlgorithm(privateKey), Key: privateKey}, nil) require.NoError(t, err) payloadJSON, err := json.Marshal(claims) @@ -199,13 +204,17 @@ func TokenHandlerBehaviorReturningPredefinedJWT( } } +type JosePublicKey interface { + *rsa.PublicKey | *ecdsa.PublicKey +} + // DefaultJwksHandlerBehavior describes the scenario when JSON Web Key Set token is being returned. // This behavior should being applied to the MockJWKsHandler. -func DefaultJwksHandlerBehavior(t *testing.T, verificationPublicKey *rsa.PublicKey) func() jose.JSONWebKeySet { +func DefaultJwksHandlerBehavior[K JosePublicKey](t *testing.T, verificationPublicKey K) func() jose.JSONWebKeySet { t.Helper() return func() jose.JSONWebKeySet { - key := jose.JSONWebKey{Key: verificationPublicKey, Use: "sig", Algorithm: string(jose.RS256)} + key := jose.JSONWebKey{Key: verificationPublicKey, Use: "sig", Algorithm: string(GetSignatureAlgorithm(verificationPublicKey))} thumbprint, err := key.Thumbprint(crypto.SHA256) require.NoError(t, err) @@ -216,3 +225,16 @@ func DefaultJwksHandlerBehavior(t *testing.T, verificationPublicKey *rsa.PublicK } } } + +type JoseKey interface{ JosePrivateKey | JosePublicKey } + +func GetSignatureAlgorithm[K JoseKey](key K) jose.SignatureAlgorithm { + switch any(key).(type) { + case *rsa.PrivateKey, *rsa.PublicKey: + return jose.RS256 + case *ecdsa.PrivateKey, *ecdsa.PublicKey: + return jose.ES256 + default: + panic("unknown key type") // should be impossible + } +}