diff --git a/docs/content/en/configuration/identity-providers/open-id-connect.md b/docs/content/en/configuration/identity-providers/open-id-connect.md index 6ac8d1249..05e550e2c 100644 --- a/docs/content/en/configuration/identity-providers/open-id-connect.md +++ b/docs/content/en/configuration/identity-providers/open-id-connect.md @@ -28,12 +28,12 @@ More information about the beta can be found in the [roadmap](../../roadmap/acti ## Configuration -{{< config-alert-example >}} +The following snippet provides a sample-configuration for the OIDC identity provider explaining each field in detail. ```yaml identity_providers: oidc: - hmac_secret: 'this_is_a_secret_abc123abc123abc' + hmac_secret: this_is_a_secret_abc123abc123abc issuer_certificate_chain: | -----BEGIN CERTIFICATE----- MIIC5jCCAc6gAwIBAgIRAK4Sj7FiN6PXo/urPfO4E7owDQYJKoZIhvcNAQELBQAw @@ -101,54 +101,59 @@ identity_providers: 27GoE2i5mh6Yez6VAYbUuns3FcwIsMyWLq043Tu2DNkx9ijOOAuQzw^invalid.. DO NOT USE== -----END RSA PRIVATE KEY----- - access_token_lifespan: '1h' - authorize_code_lifespan: '1m' - id_token_lifespan: '1h' - refresh_token_lifespan: '90m' + issuer_jwks: + - key_id: '' + algorithm: 'RS256' + key: | + + certificate_chain: | + + access_token_lifespan: 1h + authorize_code_lifespan: 1m + id_token_lifespan: 1h + refresh_token_lifespan: 90m enable_client_debug_messages: false - enforce_pkce: 'public_clients_only' + enforce_pkce: public_clients_only cors: endpoints: - - 'authorization' - - 'token' - - 'revocation' - - 'introspection' + - authorization + - token + - revocation + - introspection allowed_origins: - - 'https://example.com' + - https://example.com allowed_origins_from_client_redirect_uris: false clients: - - id: 'myapp' - description: 'My Application' + - id: myapp + description: My Application secret: '$pbkdf2-sha512$310000$c8p78n7pUMln0jzvd4aK4Q$JNRBzwAo0ek5qKn50cFzzvE9RXV88h1wJn5KGiHrD0YKtZaR/nCb2CJPOsKaPK0hjf.9yHxzQGZziziccp6Yng' # The digest of 'insecure_secret'. sector_identifier: '' public: false - authorization_policy: 'two_factor' - consent_mode: 'explicit' - pre_configured_consent_duration: '1w' + authorization_policy: two_factor + consent_mode: explicit + pre_configured_consent_duration: 1w audience: [] scopes: - - 'openid' - - 'groups' - - 'email' - - 'profile' + - openid + - groups + - email + - profile redirect_uris: - - 'https://oidc.example.com:8080/oauth2/callback' + - https://oidc.example.com:8080/oauth2/callback grant_types: - - 'refresh_token' - - 'authorization_code' + - refresh_token + - authorization_code response_types: - - 'code' + - code response_modes: - - 'form_post' - - 'query' - - 'fragment' - userinfo_signing_algorithm: 'none' + - form_post + - query + - fragment + userinfo_signing_algorithm: none ``` ## Options -This section describes the individual configuration options. - ### hmac_secret {{< confkey type="string" required="yes" >}} @@ -163,23 +168,6 @@ It's __strongly recommended__ this is a [Random Alphanumeric String](../../reference/guides/generating-secure-values.md#generating-a-random-alphanumeric-string) with 64 or more characters. -### issuer_certificate_chain - -{{< confkey type="string" required="no" >}} - -The certificate chain/bundle to be used with the [issuer_private_key](#issuer_private_key) DER base64 ([RFC4648]) -encoded PEM format used to sign/encrypt the [OpenID Connect 1.0] [JWT]'s. When configured it enables the [x5c] and [x5t] -JSON key's in the JWKs [Discoverable Endpoint](../../integration/openid-connect/introduction.md#discoverable-endpoints) -as per [RFC7517]. - -[RFC7517]: https://datatracker.ietf.org/doc/html/rfc7517 -[x5c]: https://datatracker.ietf.org/doc/html/rfc7517#section-4.7 -[x5t]: https://datatracker.ietf.org/doc/html/rfc7517#section-4.8 - -The first certificate in the chain must have the public key for the [issuer_private_key](#issuerprivatekey), each -certificate in the chain must be valid for the current date, and each certificate in the chain should be signed by the -certificate immediately following it if present. - ### issuer_private_key {{< confkey type="string" required="yes" >}} @@ -199,13 +187,85 @@ The private key *__MUST__*: If the [issuer_certificate_chain](#issuercertificatechain) is provided the private key must include matching public key data for the first certificate in the chain. +### issuer_certificate_chain + +{{< confkey type="string" required="no" >}} + +The certificate chain/bundle to be used with the [issuer_private_key](#issuer_private_key) DER base64 ([RFC4648]) +encoded PEM format used to sign/encrypt the [OpenID Connect 1.0] [JWT]'s. When configured it enables the [x5c] and [x5t] +JSON key's in the JWKs [Discoverable Endpoint](../../integration/openid-connect/introduction.md#discoverable-endpoints) +as per [RFC7517]. + +[RFC7517]: https://datatracker.ietf.org/doc/html/rfc7517 +[x5c]: https://datatracker.ietf.org/doc/html/rfc7517#section-4.7 +[x5t]: https://datatracker.ietf.org/doc/html/rfc7517#section-4.8 + +The first certificate in the chain must have the public key for the [issuer_private_key](#issuerprivatekey), each +certificate in the chain must be valid for the current date, and each certificate in the chain should be signed by the +certificate immediately following it if present. + +### issuer_jwks + +{{< confkey type="list(object" required="no" >}} + +The list of JWKS instead of or in addition to the [issuer_private_key](#issuerprivatekey) and +[issuer_certificate_chain](#issuercertificatechain). Can also accept ECDSA Private Key's and Certificates. + +#### key_id + +{{< confkey type="string" default="" required="no" >}} + +Completely optional, and generally discouraged unless there is a collision between the automatically generated key id's. +If provided must be a unique string with 7 or less alphanumeric characters. + +This value is the first 7 characters of the public key thumbprint (SHA1) encoded into hexadecimal. + +#### algorithm + +{{< confkey type="string" required="no" >}} + +The algorithm for this key. This value must be unique. It's automatically detected based on the type of key. + +#### key + +{{< confkey type="string" required="yes" >}} + +The private key associated with this key entry. + +The private key used to sign/encrypt the [OpenID Connect 1.0] issued [JWT]'s. The key must be generated by the administrator +and can be done by following the +[Generating an RSA Keypair](../../reference/guides/generating-secure-values.md#generating-an-rsa-keypair) guide. + +The private key *__MUST__*: +* Be a PEM block encoded in the DER base64 format ([RFC4648]). +* Be one of: + * An RSA key with a key size of at least 2048 bits. + * An ECDSA private key with one of the P-256, P-384, or P-521 elliptical curves. + +If the [certificate_chain](#certificatechain) is provided the private key must include matching public +key data for the first certificate in the chain. + +#### certificate_chain + +{{< confkey type="string" required="no" >}} + +The certificate chain/bundle to be used with the [key](#key) DER base64 ([RFC4648]) +encoded PEM format used to sign/encrypt the [OpenID Connect 1.0] [JWT]'s. When configured it enables the [x5c] and [x5t] +JSON key's in the JWKs [Discoverable Endpoint](../../integration/openid-connect/introduction.md#discoverable-endpoints) +as per [RFC7517]. + +[RFC7517]: https://datatracker.ietf.org/doc/html/rfc7517 +[x5c]: https://datatracker.ietf.org/doc/html/rfc7517#section-4.7 +[x5t]: https://datatracker.ietf.org/doc/html/rfc7517#section-4.8 + +The first certificate in the chain must have the public key for the [key](#key), each certificate in the chain must be +valid for the current date, and each certificate in the chain should be signed by the certificate immediately following +it if present. + ### access_token_lifespan {{< confkey type="duration" default="1h" required="no" >}} -*__Reference Note:__ This configuration option uses the [duration common syntax](../prologue/common.md#duration). -Please see the [documentation](../prologue/common.md#duration) on this format for more information.* - The maximum lifetime of an access token. It's generally recommended keeping this short similar to the default. For more information read these docs about [token lifespan]. @@ -213,9 +273,6 @@ For more information read these docs about [token lifespan]. {{< confkey type="duration" default="1m" required="no" >}} -*__Reference Note:__ This configuration option uses the [duration common syntax](../prologue/common.md#duration). -Please see the [documentation](../prologue/common.md#duration) on this format for more information.* - The maximum lifetime of an authorize code. This can be rather short, as the authorize code should only be needed to obtain the other token types. For more information read these docs about [token lifespan]. @@ -223,18 +280,12 @@ obtain the other token types. For more information read these docs about [token {{< confkey type="duration" default="1h" required="no" >}} -*__Reference Note:__ This configuration option uses the [duration common syntax](../prologue/common.md#duration). -Please see the [documentation](../prologue/common.md#duration) on this format for more information.* - The maximum lifetime of an ID token. For more information read these docs about [token lifespan]. ### refresh_token_lifespan {{< confkey type="string" default="90m" required="no" >}} -*__Reference Note:__ This configuration option uses the [duration common syntax](../prologue/common.md#duration). -Please see the [documentation](../prologue/common.md#duration) on this format for more information.* - The maximum lifetime of a refresh token. The refresh token can be used to obtain new refresh tokens as well as access tokens or id tokens with an up-to-date expiration. For more information read these docs about [token lifespan]. @@ -300,9 +351,6 @@ When enabled all authorization requests must use the [Pushed Authorization Reque {{< confkey type="duration" default="5m" required="no" >}} -*__Reference Note:__ This configuration option uses the [duration common syntax](../prologue/common.md#duration). -Please see the [documentation](../prologue/common.md#duration) on this format for more information.* - The maximum amount of time between the [Pushed Authorization Requests] flow being initiated and the generated `request_uri` being utilized by a client. @@ -590,8 +638,8 @@ Configures the consent mode. The following table describes the different modes: {{< confkey type="duration" default="1w" required="no" >}} -*__Reference Note:__ This configuration option uses the [duration common syntax](../prologue/common.md#duration). -Please see the [documentation](../prologue/common.md#duration) on this format for more information.* +*__Note:__ This setting uses the [duration notation format](../prologue/common.md#duration-notation-format). Please see +the [common options](../prologue/common.md#duration-notation-format) documentation for information on this format.* Specifying this in the configuration without a consent [consent_mode] enables the `pre-configured` mode. If this is specified as well as the [consent_mode] then it only has an effect if the [consent_mode] is `pre-configured` or `auto`. diff --git a/docs/content/en/integration/openid-connect/introduction.md b/docs/content/en/integration/openid-connect/introduction.md index 043124f99..e1568acdd 100644 --- a/docs/content/en/integration/openid-connect/introduction.md +++ b/docs/content/en/integration/openid-connect/introduction.md @@ -101,6 +101,39 @@ This scope includes the profile information the authentication backend reports a | preferred_username | string | username | The username the user used to login with | | name | string | display_name | The users display name | +## Signing and Encryption Algorithms + +[OpenID Connect 1.0] and OAuth 2.0 support a wide variety of signature and encryption algorithms. Authelia supports +a subset of these. + +### Response Object + +Authelia's response objects can have the following signature algorithms: + +| Algorithm | Key Type | Hashing Algorithm | Use | JWK Default Conditions | Notes | +|:---------:|:-----------:|:-----------------:|:---------:|:--------------------------------------------:|:----------------------------------------------------:| +| RS256 | RSA | SHA-256 | Signature | RSA Private Key without a specific algorithm | Requires an RSA Private Key with 2048 bits or more | +| RS384 | RSA | SHA-384 | Signature | N/A | Requires an RSA Private Key with 2048 bits or more | +| RS512 | RSA | SHA-512 | Signature | N/A | Requires an RSA Private Key with 2048 bits or more | +| ES256 | ECDSA P-256 | SHA-256 | Signature | ECDSA Private Key with the P-256 curve | | +| ES384 | ECDSA P-384 | SHA-384 | Signature | ECDSA Private Key with the P-384 curve | | +| ES512 | ECDSA P-521 | SHA-512 | Signature | ECDSA Private Key with the P-521 curve | Requires an ECDSA Private Key with 2048 bits or more | +| PS256 | RSA (MGF1) | SHA-256 | Signature | N/A | Requires an RSA Private Key with 2048 bits or more | +| PS384 | RSA (MGF1) | SHA-384 | Signature | N/A | Requires an RSA Private Key with 2048 bits or more | +| PS512 | RSA (MGF1) | SHA-512 | Signature | N/A | Requires an RSA Private Key with 2048 bits or more | + +### Request Object + + +| Algorithm | Key Type | Hashing Algorithm | Use | Notes | +|:---------:|:------------------:|:-----------------:|:---------:|:--------------------------------------------------:| +| none | None | None | N/A | N/A | +| HS256 | HMAC Shared Secret | SHA-256 | Signature | [Client Authentication Method] `client_secret_jwt` | +| HS384 | HMAC Shared Secret | SHA-384 | Signature | [Client Authentication Method] `client_secret_jwt` | +| HS512 | HMAC Shared Secret | SHA-512 | Signature | [Client Authentication Method] `client_secret_jwt` | + +[Client Authentication Method]: #client-authentication-method + ## Parameters The following section describes advanced parameters which can be used in various endpoints as well as their related diff --git a/internal/commands/context.go b/internal/commands/context.go index 64282acb6..65b5250f1 100644 --- a/internal/commands/context.go +++ b/internal/commands/context.go @@ -162,9 +162,7 @@ func (ctx *CmdCtx) LoadProviders() (warns, errs []error) { ctx.providers.Notifier = notification.NewFileNotifier(*ctx.config.Notifier.FileSystem) } - if ctx.providers.OpenIDConnect, err = oidc.NewOpenIDConnectProvider(ctx.config.IdentityProviders.OIDC, ctx.providers.StorageProvider, ctx.providers.Templates); err != nil { - errs = append(errs, err) - } + ctx.providers.OpenIDConnect = oidc.NewOpenIDConnectProvider(ctx.config.IdentityProviders.OIDC, ctx.providers.StorageProvider, ctx.providers.Templates) if ctx.config.Telemetry.Metrics.Enabled { ctx.providers.Metrics = metrics.NewPrometheus() diff --git a/internal/commands/crypto.go b/internal/commands/crypto.go index 6df2a8e20..eb225886e 100644 --- a/internal/commands/crypto.go +++ b/internal/commands/crypto.go @@ -335,7 +335,7 @@ func (ctx *CmdCtx) CryptoCertificateRequestRunE(cmd *cobra.Command, _ []string) return err } - if err = utils.WriteCertificateBytesToPEM(csrPath, true, csr); err != nil { + if err = utils.WriteCertificateBytesAsPEMToPath(csrPath, true, csr); err != nil { return err } @@ -430,7 +430,7 @@ func (ctx *CmdCtx) CryptoCertificateGenerateRunE(cmd *cobra.Command, _ []string, return err } - if err = utils.WriteCertificateBytesToPEM(certificatePath, false, certificate); err != nil { + if err = utils.WriteCertificateBytesAsPEMToPath(certificatePath, false, certificate); err != nil { return err } diff --git a/internal/commands/crypto_helper.go b/internal/commands/crypto_helper.go index fd8f999f9..a4dc508af 100644 --- a/internal/commands/crypto_helper.go +++ b/internal/commands/crypto_helper.go @@ -198,7 +198,7 @@ func cryptoGenerateCertificateBundlesFromCmd(cmd *cobra.Command, b *strings.Buil b.WriteString(fmt.Sprintf("\tCertificate (chain): %s\n", pathPEM)) - if err = utils.WritePEM(pathPEM, blocks...); err != nil { + if err = utils.WritePEMBlocksToPath(pathPEM, blocks...); err != nil { return err } } @@ -220,7 +220,7 @@ func cryptoGenerateCertificateBundlesFromCmd(cmd *cobra.Command, b *strings.Buil b.WriteString(fmt.Sprintf("\tCertificate (priv-chain): %s\n", pathPEM)) - if err = utils.WritePEM(pathPEM, blocks...); err != nil { + if err = utils.WritePEMBlocksToPath(pathPEM, blocks...); err != nil { return err } } diff --git a/internal/configuration/decode_hooks.go b/internal/configuration/decode_hooks.go index c9d4be4f5..2bcfb614c 100644 --- a/internal/configuration/decode_hooks.go +++ b/internal/configuration/decode_hooks.go @@ -513,6 +513,30 @@ func StringToCryptoPrivateKeyHookFunc() mapstructure.DecodeHookFuncType { } } +// StringToCryptographicKeyHookFunc decodes strings to schema.CryptographicKey's. +func StringToCryptographicKeyHookFunc() mapstructure.DecodeHookFuncType { + return func(f reflect.Type, t reflect.Type, data any) (value any, err error) { + if f.Kind() != reflect.String { + return data, nil + } + + field, _ := reflect.TypeOf(schema.JWK{}).FieldByName("Key") + expectedType := field.Type + + if t != expectedType { + return data, nil + } + + dataStr := data.(string) + + if value, err = utils.ParseX509FromPEM([]byte(dataStr)); err != nil { + return nil, fmt.Errorf(errFmtDecodeHookCouldNotParseBasic, "", expectedType, err) + } + + return value, nil + } +} + // StringToPrivateKeyHookFunc decodes strings to rsa.PrivateKey's. func StringToPrivateKeyHookFunc() mapstructure.DecodeHookFuncType { return func(f reflect.Type, t reflect.Type, data any) (value any, err error) { diff --git a/internal/configuration/koanf_callbacks_test.go b/internal/configuration/koanf_callbacks_test.go index 039f18637..5ea5ec914 100644 --- a/internal/configuration/koanf_callbacks_test.go +++ b/internal/configuration/koanf_callbacks_test.go @@ -21,6 +21,7 @@ func TestKoanfEnvironmentCallback(t *testing.T) { keyMap := map[string]string{ DefaultEnvPrefix + "KEY_EXAMPLE_UNDERSCORE": "key.example_underscore", } + ignoredKeys := []string{DefaultEnvPrefix + "SOME_SECRET"} callback := koanfEnvironmentCallback(keyMap, ignoredKeys, DefaultEnvPrefix, DefaultEnvDelimiter) diff --git a/internal/configuration/provider.go b/internal/configuration/provider.go index cd3294972..b6dcaffff 100644 --- a/internal/configuration/provider.go +++ b/internal/configuration/provider.go @@ -65,6 +65,7 @@ func unmarshal(ko *koanf.Koanf, val *schema.StructValidator, path string, o any) StringToX509CertificateChainHookFunc(), StringToPrivateKeyHookFunc(), StringToCryptoPrivateKeyHookFunc(), + StringToCryptographicKeyHookFunc(), StringToTLSVersionHookFunc(), StringToPasswordDigestHookFunc(), ToTimeDurationHookFunc(), diff --git a/internal/configuration/provider_test.go b/internal/configuration/provider_test.go index 27f363d4f..bae07d5d5 100644 --- a/internal/configuration/provider_test.go +++ b/internal/configuration/provider_test.go @@ -245,6 +245,37 @@ func TestShouldLoadURLList(t *testing.T) { assert.Equal(t, "https://example.com", config.IdentityProviders.OIDC.CORS.AllowedOrigins[1].String()) } +/* +func TestShouldLoadNewOIDCConfig(t *testing.T) { + val := schema.NewStructValidator() + _, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config_oidc_modern.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...) + + assert.NoError(t, err) + + assert.Len(t, val.Errors(), 0) + assert.Len(t, val.Warnings(), 0) + + val.Clear() + + validator.ValidateIdentityProviders(&config.IdentityProviders, val) + + assert.Len(t, val.Errors(), 0) + + assert.Len(t, config.IdentityProviders.OIDC.IssuerJWKS.Keys, 2) + assert.Equal(t, "keya", config.IdentityProviders.OIDC.IssuerJWKS.DefaultKeyID) + + assert.Equal(t, oidc.KeyUseSignature, config.IdentityProviders.OIDC.IssuerJWKS.Keys["keya"].Use) + assert.Equal(t, oidc.SigningAlgRSAUsingSHA256, config.IdentityProviders.OIDC.IssuerJWKS.Keys["keya"].Algorithm) + + assert.Equal(t, oidc.KeyUseSignature, config.IdentityProviders.OIDC.IssuerJWKS.Keys["ec521"].Use) + assert.Equal(t, oidc.SigningAlgECDSAUsingP521AndSHA512, config.IdentityProviders.OIDC.IssuerJWKS.Keys["ec521"].Algorithm) + + assert.Contains(t, config.IdentityProviders.OIDC.Discovery.RegisteredJWKSigningAlgs, oidc.SigningAlgRSAUsingSHA256) + assert.Contains(t, config.IdentityProviders.OIDC.Discovery.RegisteredJWKSigningAlgs, oidc.SigningAlgECDSAUsingP521AndSHA512) +}. + +*/ + func TestShouldConfigureConsent(t *testing.T) { val := schema.NewStructValidator() keys, config, err := Load(val, NewDefaultSources([]string{"./test_resources/config_oidc.yml"}, DefaultEnvPrefix, DefaultEnvDelimiter)...) diff --git a/internal/configuration/schema/identity_providers.go b/internal/configuration/schema/identity_providers.go index a89f923ad..e3713718e 100644 --- a/internal/configuration/schema/identity_providers.go +++ b/internal/configuration/schema/identity_providers.go @@ -17,6 +17,8 @@ type OpenIDConnectConfiguration struct { IssuerCertificateChain X509CertificateChain `koanf:"issuer_certificate_chain"` IssuerPrivateKey *rsa.PrivateKey `koanf:"issuer_private_key"` + IssuerJWKS []JWK `koanf:"issuer_jwks"` + AccessTokenLifespan time.Duration `koanf:"access_token_lifespan"` AuthorizeCodeLifespan time.Duration `koanf:"authorize_code_lifespan"` IDTokenLifespan time.Duration `koanf:"id_token_lifespan"` @@ -32,6 +34,13 @@ type OpenIDConnectConfiguration struct { PAR OpenIDConnectPARConfiguration `koanf:"pushed_authorizations"` Clients []OpenIDConnectClientConfiguration `koanf:"clients"` + + Discovery OpenIDConnectDiscovery // MetaData value. Not configurable by users. +} + +type OpenIDConnectDiscovery struct { + DefaultKeyID string + RegisteredJWKSigningAlgs []string } // OpenIDConnectPARConfiguration represents an OpenID Connect PAR config. @@ -67,13 +76,15 @@ type OpenIDConnectClientConfiguration struct { TokenEndpointAuthMethod string `koanf:"token_endpoint_auth_method"` TokenEndpointAuthSigningAlg string `koanf:"token_endpoint_auth_signing_alg"` + IDTokenSigningAlg string `koanf:"id_token_signing_alg"` + Policy string `koanf:"authorization_policy"` EnforcePAR bool `koanf:"enforce_par"` EnforcePKCE bool `koanf:"enforce_pkce"` - PKCEChallengeMethod string `koanf:"pkce_challenge_method"` - UserinfoSigningAlgorithm string `koanf:"userinfo_signing_algorithm"` + PKCEChallengeMethod string `koanf:"pkce_challenge_method"` + UserinfoSigningAlg string `koanf:"userinfo_signing_algorithm"` ConsentMode string `koanf:"consent_mode"` ConsentPreConfiguredDuration *time.Duration `koanf:"pre_configured_consent_duration"` @@ -97,7 +108,8 @@ var DefaultOpenIDConnectClientConfiguration = OpenIDConnectClientConfiguration{ ResponseTypes: []string{"code"}, ResponseModes: []string{"form_post"}, - UserinfoSigningAlgorithm: "none", + IDTokenSigningAlg: "RS256", + UserinfoSigningAlg: "none", ConsentMode: "auto", ConsentPreConfiguredDuration: &defaultOIDCClientConsentPreConfiguredDuration, } diff --git a/internal/configuration/schema/keys.go b/internal/configuration/schema/keys.go index ec7947e1a..1473226c0 100644 --- a/internal/configuration/schema/keys.go +++ b/internal/configuration/schema/keys.go @@ -20,6 +20,12 @@ var Keys = []string{ "identity_providers.oidc.hmac_secret", "identity_providers.oidc.issuer_certificate_chain", "identity_providers.oidc.issuer_private_key", + "identity_providers.oidc.issuer_jwks", + "identity_providers.oidc.issuer_jwks[].key_id", + "identity_providers.oidc.issuer_jwks[]", + "identity_providers.oidc.issuer_jwks[].algorithm", + "identity_providers.oidc.issuer_jwks[].key", + "identity_providers.oidc.issuer_jwks[].certificate_chain", "identity_providers.oidc.access_token_lifespan", "identity_providers.oidc.authorize_code_lifespan", "identity_providers.oidc.id_token_lifespan", @@ -47,6 +53,7 @@ var Keys = []string{ "identity_providers.oidc.clients[].response_modes", "identity_providers.oidc.clients[].token_endpoint_auth_method", "identity_providers.oidc.clients[].token_endpoint_auth_signing_alg", + "identity_providers.oidc.clients[].id_token_signing_alg", "identity_providers.oidc.clients[].authorization_policy", "identity_providers.oidc.clients[].enforce_par", "identity_providers.oidc.clients[].enforce_pkce", @@ -54,6 +61,7 @@ var Keys = []string{ "identity_providers.oidc.clients[].userinfo_signing_algorithm", "identity_providers.oidc.clients[].consent_mode", "identity_providers.oidc.clients[].pre_configured_consent_duration", + "identity_providers.oidc", "authentication_backend.password_reset.disable", "authentication_backend.password_reset.custom_url", "authentication_backend.refresh_interval", diff --git a/internal/configuration/schema/shared.go b/internal/configuration/schema/shared.go index 13d56b037..d244b13a7 100644 --- a/internal/configuration/schema/shared.go +++ b/internal/configuration/schema/shared.go @@ -34,3 +34,12 @@ type ServerBuffers struct { Read int `koanf:"read"` Write int `koanf:"write"` } + +// JWK represents a JWK. +type JWK struct { + KeyID string `koanf:"key_id"` + Use string + Algorithm string `koanf:"algorithm"` + Key CryptographicKey `koanf:"key"` + CertificateChain X509CertificateChain `koanf:"certificate_chain"` +} diff --git a/internal/configuration/schema/types.go b/internal/configuration/schema/types.go index bc5bd988c..6a938d781 100644 --- a/internal/configuration/schema/types.go +++ b/internal/configuration/schema/types.go @@ -1,6 +1,7 @@ package schema import ( + "bytes" "crypto" "crypto/ecdsa" "crypto/ed25519" @@ -101,6 +102,10 @@ func NewX509CertificateChain(in string) (chain *X509CertificateChain, err error) return chain, nil } +func NewX509CertificateChainFromCerts(in []*x509.Certificate) (chain X509CertificateChain) { + return X509CertificateChain{certs: in} +} + // NewTLSVersion returns a new TLSVersion given a string. func NewTLSVersion(input string) (version *TLSVersion, err error) { switch strings.ReplaceAll(strings.ToUpper(input), " ", "") { @@ -166,6 +171,9 @@ type CryptographicPrivateKey interface { Equal(x crypto.PrivateKey) bool } +// CryptographicKey represents an artificial cryptographic public or private key. +type CryptographicKey any + // X509CertificateChain is a helper struct that holds a list of *x509.Certificate's. type X509CertificateChain struct { certs []*x509.Certificate @@ -277,6 +285,24 @@ func (c *X509CertificateChain) Leaf() (leaf *x509.Certificate) { return c.certs[0] } +// EncodePEM encodes the entire chain as PEM bytes. +func (c *X509CertificateChain) EncodePEM() (encoded []byte, err error) { + buf := &bytes.Buffer{} + + for _, cert := range c.certs { + block := pem.Block{ + Type: blockCERTIFICATE, + Bytes: cert.Raw, + } + + if err = pem.Encode(buf, &block); err != nil { + return nil, err + } + } + + return buf.Bytes(), nil +} + // Validate the X509CertificateChain ensuring the certificates were provided in the correct order // (with nth being signed by the nth+1), and that all of the certificates are valid based on the current time. func (c *X509CertificateChain) Validate() (err error) { diff --git a/internal/configuration/test_resources/config_oidc_modern.yml b/internal/configuration/test_resources/config_oidc_modern.yml new file mode 100644 index 000000000..be63faba5 --- /dev/null +++ b/internal/configuration/test_resources/config_oidc_modern.yml @@ -0,0 +1,211 @@ +--- +default_redirection_url: https://home.example.com:8080/ + +server: + host: 127.0.0.1 + port: 9091 + +log: + level: debug + +totp: + issuer: authelia.com + +duo_api: + hostname: api-123456789.example.com + integration_key: ABCDEF + +authentication_backend: + ldap: + url: ldap://127.0.0.1 + base_dn: dc=example,dc=com + username_attribute: uid + additional_users_dn: ou=users + users_filter: (&({username_attribute}={input})(objectCategory=person)(objectClass=user)) + additional_groups_dn: ou=groups + groups_filter: (&(member={dn})(objectClass=groupOfNames)) + group_name_attribute: cn + mail_attribute: mail + user: cn=admin,dc=example,dc=com + +access_control: + default_policy: deny + + rules: + # Rules applied to everyone + - domain: public.example.com + policy: bypass + + - domain: secure.example.com + policy: one_factor + # Network based rule, if not provided any network matches. + networks: + - 192.168.1.0/24 + - domain: secure.example.com + policy: two_factor + + - domain: [singlefactor.example.com, onefactor.example.com] + policy: one_factor + + # Rules applied to 'admins' group + - domain: "mx2.mail.example.com" + subject: "group:admins" + policy: deny + - domain: "*.example.com" + subject: "group:admins" + policy: two_factor + + # Rules applied to 'dev' group + - domain: dev.example.com + resources: + - "^/groups/dev/.*$" + subject: "group:dev" + policy: two_factor + + # Rules applied to user 'john' + - domain: dev.example.com + resources: + - "^/users/john/.*$" + subject: "user:john" + policy: two_factor + + # Rules applied to 'dev' group and user 'john' + - domain: dev.example.com + resources: + - "^/deny-all.*$" + subject: ["group:dev", "user:john"] + policy: deny + + # Rules applied to user 'harry' + - domain: dev.example.com + resources: + - "^/users/harry/.*$" + subject: "user:harry" + policy: two_factor + + # Rules applied to user 'bob' + - domain: "*.mail.example.com" + subject: "user:bob" + policy: two_factor + - domain: "dev.example.com" + resources: + - "^/users/bob/.*$" + subject: "user:bob" + policy: two_factor + +session: + name: authelia_session + expiration: 3600000 # 1 hour + inactivity: 300000 # 5 minutes + domain: example.com + redis: + host: 127.0.0.1 + port: 6379 + high_availability: + sentinel_name: test + +regulation: + max_retries: 3 + find_time: 120 + ban_time: 300 + +storage: + mysql: + host: 127.0.0.1 + port: 3306 + database: authelia + username: authelia + +notifier: + smtp: + username: test + host: 127.0.0.1 + port: 1025 + sender: admin@example.com + disable_require_tls: true + +identity_providers: + oidc: + hmac_secret: 1nb2j3kh1b23kjh1b23jh1b23j1h2b3 + issuer_jwks: + keys: + keya: + key: | + -----BEGIN RSA PRIVATE KEY----- + MIIEpQIBAAKCAQEAs5BZdREjkceDvty5c+qBski4XXiMubVyGFLazoNumhMbgjA7 + DoCLglCrIRcYd4Wn4CEe4KJzghIdfijDCIQ6V+wrm/KR3iMvBdkPYVC7vGXYY5kx + WtAT5qejuxKXHQK2/jWSO6tOxFRGxA/nE4A9cm8FH6/lfz05ci1h63gAOVQpkvcj + JMHlGqTHt93HOTBVFQpi9zpTdDKQx3yq4ttfh49vUwWBsXe640Y+soaGjTMJS8IT + kGktwOxwRfGJ1PRHVF9FnRm7nhf53hFat/k3mbbyV8rnlmVLPqZ+KIqH5/rjYh3K + Rr71WAptFnHtoiT2SNfwDh+8iqo3QlWtW24iAwIDAQABAoIBAQCLKVkbMEA3z79b + 4SZdHqaLbG5uCmpN1sBo93WaTSQfhqVwHT73u0njoe8ugv60SsJTIngSsfQBH1b6 + Gk8kv42T7HXTs4e299+Oka2oxu/oT6oHbodgkRiLTurGpd61XhBCLXR6iAZQg9wg + QQ7d/yogEMiQyTp8hQ+LXH6iBetugW0l0Uz2pbJNi4c4qqXm5BYoQaJ0RjqQI5C5 + 5ZPiX/1yn3bWbJRhSK5FfnEdO/3LclfQMvMOaipH4CXOjYcFSxVP1vVL6Jli92+j + tVApsnSZiEZ3kB4jRqDZnV9xQhfTXVyfopCNL1a3LkbE171GStd5eib7ESydTik7 + DhFqTdpZAoGBAOgEPAmFKi9z40umcN27+dd6CAXfjy2dqkuM8hGshQiGnH9bAAl+ + hhr3u5AvbxZ/qsD0KAAEReD3cY0OFTPfl2qD0nsleqUt6N6gM+j6596jVNnOlW9A + 0y0Lssobh8DrvXoDTBCL1wdcQXknoyUm8lkpVhSm6PmLFAS2lnBFJBJvAoGBAMYg + FGeSQ0kRWmmVnfibcHqHerD5FK6AvciYg/ycjKUA9Fde0gon1v/M1TWAhT6mqjjv + uSX2c3eEpzAjoJxd5bPgxfEWIM6ygn1N5fka1re5d5pHOAMJB9AAz3A3PZJP8ANt + ES51dO0rYAuKdBuza9eSyj9/JPDh7QdE47ZvP6OtAoGBAI4aAddm2uaDWOP9hcUY + mzXRBNbsDJpIpYNuSNhwTG5jW7hYuNYXyvT7Y8I0expRiPhy0YjpFQ9rHf3hcTT7 + LZbMM/6+frZqPuUTQ5ffDGJ8sLxR3Y5tKqm9L3y/jc6n073GBTFhJIragzM8Bpz7 + lJTtT06Ix8oG13Tni44pmqU7AoGBAJPS56aHSNDBs9XHnkAZqgiiAPb+QWIaCIAc + 242lOIL8fVKbGtgc9ZuSNxpeNAyUybkFk/0xLuHkBeIzEujYXkSh1s6UlhHiut3H + O2lrjv0x0n032iDZogyeLigp7zS1k/zaadFiLcWvcU/rE8p/Sl1j1qcdtHBOAU5F + Jim+Q5tZAoGAbNaUs05FPastfdmZCp5Pj1sbnRZdwUZH1AeZuhuSfbS6aA/wB3VA + i/LTu4Z3iR0M0p8HeNy/YBKl0MrRc0nE/UkV66kOQH3az/N39i2KsTBzMsvlByWd + ofwOgCjgkIviNDIXikLBEWZFwr/4mQkN91YoFY37pteS3sVYQpVbQeA= + -----END RSA PRIVATE KEY----- + certificate_chain: | + -----BEGIN CERTIFICATE----- + MIIC5jCCAc6gAwIBAgIRAMMH7qhte0VDXdnbHMy43dUwDQYJKoZIhvcNAQELBQAw + EzERMA8GA1UEChMIQXV0aGVsaWEwHhcNMjMwNDE2MTEyMjU0WhcNMjQwNDE1MTEy + MjU0WjATMREwDwYDVQQKEwhBdXRoZWxpYTCCASIwDQYJKoZIhvcNAQEBBQADggEP + ADCCAQoCggEBALOQWXURI5HHg77cuXPqgbJIuF14jLm1chhS2s6DbpoTG4IwOw6A + i4JQqyEXGHeFp+AhHuCic4ISHX4owwiEOlfsK5vykd4jLwXZD2FQu7xl2GOZMVrQ + E+ano7sSlx0Ctv41kjurTsRURsQP5xOAPXJvBR+v5X89OXItYet4ADlUKZL3IyTB + 5Rqkx7fdxzkwVRUKYvc6U3QykMd8quLbX4ePb1MFgbF3uuNGPrKGho0zCUvCE5Bp + LcDscEXxidT0R1RfRZ0Zu54X+d4RWrf5N5m28lfK55ZlSz6mfiiKh+f642Idyka+ + 9VgKbRZx7aIk9kjX8A4fvIqqN0JVrVtuIgMCAwEAAaM1MDMwDgYDVR0PAQH/BAQD + AgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcN + AQELBQADggEBAKT2CQNx/JlutnsMHYBoOLfr8Vpz3/PTx0rAgxptgp1CDkTBxIah + 7rmGuHX0qDKbqINr4DmFhkKeIPSMux11xzW6MW83ZN8WZCTkAiPPIGTNGWccJONl + lpX5dIPdpXGoXA7T31Gto3FPmsgxQ2jK/mpok20J6EFkkpUuXxSxjwO1zd56iMxQ + FuSLo8J/uI/T4aj7Wrk6fFI5z7gjP8BjVFAsYkTYUhkLatnbuQstx+R2p7hjv50G + weBOw5YW8JWLRRJ2A5FBJsZekiNdpIa+CmH7v0SICAHaTKdR3RVgQfa8zvbzW/d8 + qXsSjjBkmEkKUFoFi/fxTQuqseQC0h+P5N8= + -----END CERTIFICATE----- + ec521: + key: | + -----BEGIN EC PRIVATE KEY----- + MIHcAgEBBEIA5s1+7OZClSDG3Ro7FFJjR2J6cBFimlR/sNcZ4ljFjDaef4vNw2DU + Eq1x5gkj888I1/BXV+/KVc+dtYDGKeGSxvagBwYFK4EEACOhgYkDgYYABADXFb5h + KymYeOH8Em1VJvOsc9mUi6Gr0AAiseu5G0HofN+GzxD7GBDAE9plkRhd8QfmuwZy + S0rUlTXhvZMARuujnABxJ7FnPp81osndv/vk9ujUTZsK0UPaLJ189NuR6VwImUQK + c/xWqI9AC99VchRw6fw7smpn6lCmVkHNRJFL1Bs4iA== + -----END EC PRIVATE KEY----- + certificate_chain: | + -----BEGIN CERTIFICATE----- + MIIB4jCCAUOgAwIBAgIRAKhDsoaXc69n4uH0CB31XfswCgYIKoZIzj0EAwIwEzER + MA8GA1UEChMIQXV0aGVsaWEwHhcNMjMwNDE2MTEyMTQxWhcNMjQwNDE1MTEyMTQx + WjATMREwDwYDVQQKEwhBdXRoZWxpYTCBmzAQBgcqhkjOPQIBBgUrgQQAIwOBhgAE + ANcVvmErKZh44fwSbVUm86xz2ZSLoavQACKx67kbQeh834bPEPsYEMAT2mWRGF3x + B+a7BnJLStSVNeG9kwBG66OcAHEnsWc+nzWiyd2/++T26NRNmwrRQ9osnXz025Hp + XAiZRApz/Faoj0AL31VyFHDp/DuyamfqUKZWQc1EkUvUGziIozUwMzAOBgNVHQ8B + Af8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADAKBggq + hkjOPQQDAgOBjAAwgYgCQgCBXWYXKn7aANz5JC8sXLJSOkNMF6vbd/T96KoLSBT+ + +l6KKwKg5evolAKync1ksGAzNidFKqjhwG4BZj10YnowPQJCAWjb/3KeRN1RmGOc + abG/6Y0USqC3LUb/ZrtVwRYvQYZYi1R7OJx7cTJcVy1yBwfJa5IcPJwjEiQI3CXV + 3AGOvhz+ + -----END CERTIFICATE----- + cors: + allowed_origins: + - https://google.com + - https://example.com + clients: + - id: abc + secret: '123' + consent_mode: explicit +... diff --git a/internal/configuration/test_resources/crypto/ECDSA_P224.crt b/internal/configuration/test_resources/crypto/ECDSA_P224.crt new file mode 100644 index 000000000..42baa6c8a --- /dev/null +++ b/internal/configuration/test_resources/crypto/ECDSA_P224.crt @@ -0,0 +1,9 @@ +-----BEGIN CERTIFICATE----- +MIIBRzCB9qADAgECAhB51uvUDHkaxlSEs8cgoYBRMAoGCCqGSM49BAMCMBMxETAP +BgNVBAoTCEF1dGhlbGlhMCAXDTIzMDQxNzEzMTIwMloYDzIxMDAwMTAxMDAwMDAw +WjATMREwDwYDVQQKEwhBdXRoZWxpYTBOMBAGByqGSM49AgEGBSuBBAAhAzoABJa4 +oEFZqEbmsnKWXEfNWTiqyEq6YiWbVFIH/PGijaRmsYpKC2UBGsscN4DziAUHBlqX +KLA/lsRjozUwMzAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEw +DAYDVR0TAQH/BAIwADAKBggqhkjOPQQDAgNAADA9Ah0Aq03epx31NN1fTorB/rrz +Muu9Taw8YxaZxvjLaQIcbNHGY5bFYxi04ahvN1rYi2sJEn66SaWut+lBIw== +-----END CERTIFICATE----- diff --git a/internal/configuration/test_resources/crypto/ECDSA_P224.pem b/internal/configuration/test_resources/crypto/ECDSA_P224.pem new file mode 100644 index 000000000..4f6ee7d08 --- /dev/null +++ b/internal/configuration/test_resources/crypto/ECDSA_P224.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MGgCAQEEHM0126u3fW5scirH39HU9FgPTZOHPxg2NgbSQQ+gBwYFK4EEACGhPAM6 +AASWuKBBWahG5rJyllxHzVk4qshKumIlm1RSB/zxoo2kZrGKSgtlARrLHDeA84gF +BwZalyiwP5bEYw== +-----END EC PRIVATE KEY----- diff --git a/internal/configuration/test_resources/crypto/ECDSA_P256.crt b/internal/configuration/test_resources/crypto/ECDSA_P256.crt new file mode 100644 index 000000000..2f0748e0a --- /dev/null +++ b/internal/configuration/test_resources/crypto/ECDSA_P256.crt @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBWzCCAQKgAwIBAgIRANqK3vKflYMr/2HGVd3aOR0wCgYIKoZIzj0EAwIwEzER +MA8GA1UEChMIQXV0aGVsaWEwIBcNMjMwNDE3MTMxNjI5WhgPMjEwMDAxMDEwMDAw +MDBaMBMxETAPBgNVBAoTCEF1dGhlbGlhMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD +QgAEnnBdDSXbTgHtrc5vcJ2xz6qyGXM8PJgENjgQgn5WFVQCSZnKp08+mzeDiHrM +67KmISfxSAjoeCJV+dP6JfxIVqM1MDMwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQM +MAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwCgYIKoZIzj0EAwIDRwAwRAIgOo+m +1yQsTmqOaKak9MY2q7CdBI9Di8vPK/sE/x5JIPYCIA/lyI/sG1EEdLT8g3M4Joc3 +VK7cBHjmftnZL6kiS+Dn +-----END CERTIFICATE----- diff --git a/internal/configuration/test_resources/crypto/ECDSA_P256.pem b/internal/configuration/test_resources/crypto/ECDSA_P256.pem new file mode 100644 index 000000000..04eae9328 --- /dev/null +++ b/internal/configuration/test_resources/crypto/ECDSA_P256.pem @@ -0,0 +1,5 @@ +-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIHL87FDsqijXFhRJ5VgYiOz2ko6xxP7aP7i4v3Eowf4KoAoGCCqGSM49 +AwEHoUQDQgAEnnBdDSXbTgHtrc5vcJ2xz6qyGXM8PJgENjgQgn5WFVQCSZnKp08+ +mzeDiHrM67KmISfxSAjoeCJV+dP6JfxIVg== +-----END EC PRIVATE KEY----- diff --git a/internal/configuration/test_resources/crypto/ECDSA_P256_PKCS8.crt b/internal/configuration/test_resources/crypto/ECDSA_P256_PKCS8.crt new file mode 100644 index 000000000..5e806c256 --- /dev/null +++ b/internal/configuration/test_resources/crypto/ECDSA_P256_PKCS8.crt @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MIIBXTCCAQKgAwIBAgIRAPtITpvhty9gwPvrPO1J8GYwCgYIKoZIzj0EAwIwEzER +MA8GA1UEChMIQXV0aGVsaWEwIBcNMjMwNDE4MDU0NTM5WhgPMjEwMDAxMDEwMDAw +MDBaMBMxETAPBgNVBAoTCEF1dGhlbGlhMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcD +QgAEW3aPMQmzoU84DWr4UbbH8tWPMCuzLC44450JvNa8ChDto0ST+koT1Xtq75cu +JSAlxn3QeMWZ0pjlvt/woj4Y/qM1MDMwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQM +MAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwCgYIKoZIzj0EAwIDSQAwRgIhAOI6 +pgq2zjf5wr1bW21HXsUmwrbyfiCz5vSlAk76QgkRAiEA7Txu5kjdEhnFUw3ORQIF +enG1sLX3iZOljfTsHTG1kug= +-----END CERTIFICATE----- diff --git a/internal/configuration/test_resources/crypto/ECDSA_P256_PKCS8.pem b/internal/configuration/test_resources/crypto/ECDSA_P256_PKCS8.pem new file mode 100644 index 000000000..f52702212 --- /dev/null +++ b/internal/configuration/test_resources/crypto/ECDSA_P256_PKCS8.pem @@ -0,0 +1,5 @@ +-----BEGIN PRIVATE KEY----- +MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgKwZ0IZaf/E54bRh6 +b9AEwZJ368O9uoJaJ4tloCjWuDOhRANCAARbdo8xCbOhTzgNavhRtsfy1Y8wK7Ms +LjjjnQm81rwKEO2jRJP6ShPVe2rvly4lICXGfdB4xZnSmOW+3/CiPhj+ +-----END PRIVATE KEY----- diff --git a/internal/configuration/test_resources/crypto/ECDSA_P384.crt b/internal/configuration/test_resources/crypto/ECDSA_P384.crt new file mode 100644 index 000000000..c34b14c83 --- /dev/null +++ b/internal/configuration/test_resources/crypto/ECDSA_P384.crt @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBmDCCAR6gAwIBAgIQWFgOoTSBNa4F1A+Uk5fBhTAKBggqhkjOPQQDAjATMREw +DwYDVQQKEwhBdXRoZWxpYTAgFw0yMzA0MTcxMzE1MDBaGA8yMTAwMDEwMTAwMDAw +MFowEzERMA8GA1UEChMIQXV0aGVsaWEwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAARq +Fk2dSauZd2mW0ZXuxZ0k2a5PInZOs3wbzjJr67RPzmPMNGt5dVHtbOTLr9MAcm21 +E6/4CLQZ+wMq4Zxuhoa02VN4lQBFOzWFPwVTa0lcOUCkJ7E7JWXiZjX80ROyqDOj +NTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMB +Af8EAjAAMAoGCCqGSM49BAMCA2gAMGUCMQCJHEN22ouKJr0usue9/bUsJltPrgSW +v7NjjQ9hY96JAwBQpTxX6EksQdnl44Q/LLACMHBZn3weWvq8frMOAmAvOomMsnMp +H7tweTJNXh4V8XdtR2GGxAAYbq/ShyxrpQ6LVA== +-----END CERTIFICATE----- diff --git a/internal/configuration/test_resources/crypto/ECDSA_P384.pem b/internal/configuration/test_resources/crypto/ECDSA_P384.pem new file mode 100644 index 000000000..cecf7b648 --- /dev/null +++ b/internal/configuration/test_resources/crypto/ECDSA_P384.pem @@ -0,0 +1,6 @@ +-----BEGIN EC PRIVATE KEY----- +MIGkAgEBBDBd2neGG9Ax14sDR0V0TYSXIBxNWZwYr7OAFd57MRUZ/+BkHvQEMOoV +umd/tOgGjEagBwYFK4EEACKhZANiAARqFk2dSauZd2mW0ZXuxZ0k2a5PInZOs3wb +zjJr67RPzmPMNGt5dVHtbOTLr9MAcm21E6/4CLQZ+wMq4Zxuhoa02VN4lQBFOzWF +PwVTa0lcOUCkJ7E7JWXiZjX80ROyqDM= +-----END EC PRIVATE KEY----- diff --git a/internal/configuration/test_resources/crypto/ECDSA_P384_PKCS8.crt b/internal/configuration/test_resources/crypto/ECDSA_P384_PKCS8.crt new file mode 100644 index 000000000..8b5ddeafa --- /dev/null +++ b/internal/configuration/test_resources/crypto/ECDSA_P384_PKCS8.crt @@ -0,0 +1,11 @@ +-----BEGIN CERTIFICATE----- +MIIBmDCCAR6gAwIBAgIQfhi2ThPJEF21jbDOB9z24TAKBggqhkjOPQQDAjATMREw +DwYDVQQKEwhBdXRoZWxpYTAgFw0yMzA0MTgwNTQ0NTRaGA8yMTAwMDEwMTAwMDAw +MFowEzERMA8GA1UEChMIQXV0aGVsaWEwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAATV +Q1Vm2hMPPv0zUA9+jphIUa4pxviHz0wrk2GlpwBvzP/tbF1aRY7MRH+8d/JIKF7p +9wuVfOYB0mE7/fpzI33baVb8Js35IRax8EIRxsDvVevE5kcheddIGIyJ0FC3yNyj +NTAzMA4GA1UdDwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMB +Af8EAjAAMAoGCCqGSM49BAMCA2gAMGUCMQCT6hC9cNxVkvJb1ddNOg1E4TUg3veD +pnLvWm9iifH5dbep1Vk8idK8XvKyKfyM5F8CMF7MTdv21GjKF3Fl01zORgE9cxlo +zLNWQbB/rLpgGRkB2Emd/dCZUQwgD9iDd2eifQ== +-----END CERTIFICATE----- diff --git a/internal/configuration/test_resources/crypto/ECDSA_P384_PKCS8.pem b/internal/configuration/test_resources/crypto/ECDSA_P384_PKCS8.pem new file mode 100644 index 000000000..7e0e43db6 --- /dev/null +++ b/internal/configuration/test_resources/crypto/ECDSA_P384_PKCS8.pem @@ -0,0 +1,6 @@ +-----BEGIN PRIVATE KEY----- +MIG2AgEAMBAGByqGSM49AgEGBSuBBAAiBIGeMIGbAgEBBDDQSUy5MggN3tt+A4RV +lCcLGFICUBO27VexEuZuY75e+xYRUeDISXSlnqwQVa42Qk+hZANiAATVQ1Vm2hMP +Pv0zUA9+jphIUa4pxviHz0wrk2GlpwBvzP/tbF1aRY7MRH+8d/JIKF7p9wuVfOYB +0mE7/fpzI33baVb8Js35IRax8EIRxsDvVevE5kcheddIGIyJ0FC3yNw= +-----END PRIVATE KEY----- diff --git a/internal/configuration/test_resources/crypto/ECDSA_P521.crt b/internal/configuration/test_resources/crypto/ECDSA_P521.crt new file mode 100644 index 000000000..cdb6e4fe2 --- /dev/null +++ b/internal/configuration/test_resources/crypto/ECDSA_P521.crt @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB5DCCAUWgAwIBAgIRAIpQUsZLrSAJ7+PY4U0MIaYwCgYIKoZIzj0EAwIwEzER +MA8GA1UEChMIQXV0aGVsaWEwIBcNMjMwNDE3MTMxNTM2WhgPMjEwMDAxMDEwMDAw +MDBaMBMxETAPBgNVBAoTCEF1dGhlbGlhMIGbMBAGByqGSM49AgEGBSuBBAAjA4GG +AAQAO8GuJvWACDYuO1ZhMdbrINK8AM8B2xFn5nSvAHAgYolyXz8yxLjmFT1/ifQZ +QjnocX4j/zOGIt1f1OXQvPSRaiQAzWlFIejCKChBK0hiDqfTyzDgrJGiCobL1bgr +yxO3oDg70YeN3mr0OkMvdrIBjpGpGkt5AX6XyaIau9ogZJz6gyOjNTAzMA4GA1Ud +DwEB/wQEAwIFoDATBgNVHSUEDDAKBggrBgEFBQcDATAMBgNVHRMBAf8EAjAAMAoG +CCqGSM49BAMCA4GMADCBiAJCAKaUek+/zGw7Tt3L5rqQhXFzWI4mfci+jD99/JHY +UT/FX2Co/tjEcyty46mMWsw6E7q6XCJ6gx38SCWe7wRdMXQMAkIA6rlkntqo6r+j +PoTtPVmbkFFc5ficw+xlmhuKblKrq+u8sbnm+J62C8pXuzSc8dtKEe0+oORD5HH9 +YGuoIKNL2vg= +-----END CERTIFICATE----- diff --git a/internal/configuration/test_resources/crypto/ECDSA_P521.pem b/internal/configuration/test_resources/crypto/ECDSA_P521.pem new file mode 100644 index 000000000..ca28662dc --- /dev/null +++ b/internal/configuration/test_resources/crypto/ECDSA_P521.pem @@ -0,0 +1,7 @@ +-----BEGIN EC PRIVATE KEY----- +MIHcAgEBBEIAe0mKO82UiFUDM3M3CgyEKkXuXnt0m2DAnW3Yf2nadim00n/XsGw7 ++ID6Zz5Xhazpx7WFNNhtrjbNQOKbsQNndPugBwYFK4EEACOhgYkDgYYABAA7wa4m +9YAINi47VmEx1usg0rwAzwHbEWfmdK8AcCBiiXJfPzLEuOYVPX+J9BlCOehxfiP/ +M4Yi3V/U5dC89JFqJADNaUUh6MIoKEErSGIOp9PLMOCskaIKhsvVuCvLE7egODvR +h43eavQ6Qy92sgGOkakaS3kBfpfJohq72iBknPqDIw== +-----END EC PRIVATE KEY----- diff --git a/internal/configuration/test_resources/crypto/ECDSA_P521_PKCS8.crt b/internal/configuration/test_resources/crypto/ECDSA_P521_PKCS8.crt new file mode 100644 index 000000000..64591376c --- /dev/null +++ b/internal/configuration/test_resources/crypto/ECDSA_P521_PKCS8.crt @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB4zCCAUSgAwIBAgIQLyw4qjgh0+UtQ4W4Apd+7zAKBggqhkjOPQQDAjATMREw +DwYDVQQKEwhBdXRoZWxpYTAgFw0yMzA0MTgwNTM4NTdaGA8yMTAwMDEwMTAwMDAw +MFowEzERMA8GA1UEChMIQXV0aGVsaWEwgZswEAYHKoZIzj0CAQYFK4EEACMDgYYA +BABUNIHCT8zGTz/gYHp5vA5DvYr1CbUuncCF2uP+Yoy56p06hD4oQKgo9K9gK03q +DCZTs0rAamxgO+PgZW3D1VZlDQAehJniclY/0Fpsc+qMrfZ5O269fTvwutUR7L/S +LXifAaW8lHcwYjgpEsaPdZUyxzKZW//usTrCHhkwSy7LXZzQ56M1MDMwDgYDVR0P +AQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwCgYI +KoZIzj0EAwIDgYwAMIGIAkIBznnm//FcYd+WRw19hRvyC9XCjmHW+M11B3kqpApv +iHueaoBIPvSZueS9fjjsNZG8qpLRVgWfVVPV8+L01z9DWrUCQgD/0sIlL4Cf5W4L +JasSbXARkm+jBj+QH5ZKA0hgpoL32I98TIEUATWSBvygXigHaD7ZEZEbxLyCL32a +uL3TmO+2wA== +-----END CERTIFICATE----- diff --git a/internal/configuration/test_resources/crypto/ECDSA_P521_PKCS8.pem b/internal/configuration/test_resources/crypto/ECDSA_P521_PKCS8.pem new file mode 100644 index 000000000..abc4a567b --- /dev/null +++ b/internal/configuration/test_resources/crypto/ECDSA_P521_PKCS8.pem @@ -0,0 +1,8 @@ +-----BEGIN PRIVATE KEY----- +MIHuAgEAMBAGByqGSM49AgEGBSuBBAAjBIHWMIHTAgEBBEIB1bhgd0P7DBnlzyL3 +S2YcOXhRxq4KkcTcw3zEX+jn6XG2cyh16u9gLv9PJ6fHLVdFHQxrVGC6/fxH9p9S +B6B57/OhgYkDgYYABABUNIHCT8zGTz/gYHp5vA5DvYr1CbUuncCF2uP+Yoy56p06 +hD4oQKgo9K9gK03qDCZTs0rAamxgO+PgZW3D1VZlDQAehJniclY/0Fpsc+qMrfZ5 +O269fTvwutUR7L/SLXifAaW8lHcwYjgpEsaPdZUyxzKZW//usTrCHhkwSy7LXZzQ +5w== +-----END PRIVATE KEY----- diff --git a/internal/configuration/test_resources/crypto/ED25519_PKCS8.crt b/internal/configuration/test_resources/crypto/ED25519_PKCS8.crt new file mode 100644 index 000000000..a636982f1 --- /dev/null +++ b/internal/configuration/test_resources/crypto/ED25519_PKCS8.crt @@ -0,0 +1,8 @@ +-----BEGIN CERTIFICATE----- +MIIBGjCBzaADAgECAhAiL/zLZb4EevlhMjiuV5DZMAUGAytlcDATMREwDwYDVQQK +EwhBdXRoZWxpYTAgFw0yMzA0MTgwNTQ4NDNaGA8yMTAwMDEwMTAwMDAwMFowEzER +MA8GA1UEChMIQXV0aGVsaWEwKjAFBgMrZXADIQBjrmjS0+DbAzaJWN+8USL8V1qU +smG9mWH96wuA6NPA4aM1MDMwDgYDVR0PAQH/BAQDAgWgMBMGA1UdJQQMMAoGCCsG +AQUFBwMBMAwGA1UdEwEB/wQCMAAwBQYDK2VwA0EAMuUxDvbVjJwCYhal6H1pZxOh +lJd16Aj4C7j7qHLwmWWwREkCoLK/Su1b3982OuCWrOMYMxEx4yNdwsnrKpNTBg== +-----END CERTIFICATE----- diff --git a/internal/configuration/test_resources/crypto/ED25519_PKCS8.pem b/internal/configuration/test_resources/crypto/ED25519_PKCS8.pem new file mode 100644 index 000000000..38a7a17b4 --- /dev/null +++ b/internal/configuration/test_resources/crypto/ED25519_PKCS8.pem @@ -0,0 +1,3 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VwBCIEIG1eby8XtRD2+eZMPFi2jvztZbBSr3wcYEeEl6Sj8k6N +-----END PRIVATE KEY----- diff --git a/internal/configuration/test_resources/crypto/RSA_1024.crt b/internal/configuration/test_resources/crypto/RSA_1024.crt new file mode 100644 index 000000000..993eb4d50 --- /dev/null +++ b/internal/configuration/test_resources/crypto/RSA_1024.crt @@ -0,0 +1,13 @@ +-----BEGIN CERTIFICATE----- +MIIB4zCCAUygAwIBAgIRANSysyC3vJlv86Ttmi8M97owDQYJKoZIhvcNAQELBQAw +EzERMA8GA1UEChMIQXV0aGVsaWEwIBcNMjMwNDE3MTM0MTM3WhgPMjEwMDAxMDEw +MDAwMDBaMBMxETAPBgNVBAoTCEF1dGhlbGlhMIGfMA0GCSqGSIb3DQEBAQUAA4GN +ADCBiQKBgQC4ntJ/qcs9yBQihZkrF5v2Pdp6Rr8uNc4GDjuOsVGUohpwcjVobAuj +AuvCG646cnekbkJOm1bY+38F+nfWJ7ny9RYMp1ng6xWR6vpzZiPyJI89FQU3gd8f +WDI5Xn2ZvrSqfgEJhXMAWn7EPgUajlbLoPzYFCKSChIpR9umk5DBnQIDAQABozUw +MzAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/ +BAIwADANBgkqhkiG9w0BAQsFAAOBgQBjpYkj+iE9XoA0q8Iq8+CYwlRwQ76jHKgy +z+0JCJE10ysuDPqRJEGJR3vfOs6VyNTGcvdCemPkTEYYAikaT4ydRNqIwefuHlx0 +7Abr/GUkZpRdTNfitAZbN4HpHpxZhx/A4yNutwGLiZSzqsn1r1VxTymSkNLa680X +84rsVRZppA== +-----END CERTIFICATE----- diff --git a/internal/configuration/test_resources/crypto/RSA_1024.pem b/internal/configuration/test_resources/crypto/RSA_1024.pem new file mode 100644 index 000000000..40fa6603f --- /dev/null +++ b/internal/configuration/test_resources/crypto/RSA_1024.pem @@ -0,0 +1,15 @@ +-----BEGIN RSA PRIVATE KEY----- +MIICWwIBAAKBgQC4ntJ/qcs9yBQihZkrF5v2Pdp6Rr8uNc4GDjuOsVGUohpwcjVo +bAujAuvCG646cnekbkJOm1bY+38F+nfWJ7ny9RYMp1ng6xWR6vpzZiPyJI89FQU3 +gd8fWDI5Xn2ZvrSqfgEJhXMAWn7EPgUajlbLoPzYFCKSChIpR9umk5DBnQIDAQAB +AoGAHNF93jus5An1SqY8EIPw7nEdR3T/psDzVfKmzVFUgLUFF4RcXd5vupRcJMKZ +Ybo4fsxPQWHyHpCzdUVxq1YsKkK5qaAGjUfyHKP9yS/ZTKzA4BQJ+mOxagdZ79PB +dxrxtRWz58x++537TNGAUNziG7zaLOmdwlqul4bYjHt5FCkCQQDhPwFMbrpjT4oM +fDuy1bWS4t3X4VVBZfEQT3WeLu4qHzCnBbEszL/q3bXtKlbiqcWRwhrOvCqkmY8v +XBAb21yTAkEA0dPVHCcgXKWIytz7DOGEcBoO8ANw/918VoA9LW560pL5B1qzYAl+ +7Ecl6zoZLJPVY1BQ+HVE5tLih84hmlp3DwJAZnHQdmHaFfcEE3+ha0n1plPWkCwl +KXRi+ocZOJOhsLi02RImrfiFxR2Hc9GQ6NBMUmnU5XgBcRGCZQjbLsBLTwJAEct7 +SVXwIqtPPJUdHWyKxM8Q8T35eVmZT+S0S4QRGoaoY/1HNR/ZCcTG7HoS5HrtH+0R +0OBxJXpBB+9tXh/J9QJAJDVE0lcJWKeUl/W3P/pzCXZHdUtqoYUFuuuJyFnAAFFU +CKi6wGKnfsc0v01tVpooyThJ+4Z9eTotNGp6ke1tTA== +-----END RSA PRIVATE KEY----- diff --git a/internal/configuration/test_resources/crypto/RSA_2048.crt b/internal/configuration/test_resources/crypto/RSA_2048.crt new file mode 100644 index 000000000..f2eb46b6b --- /dev/null +++ b/internal/configuration/test_resources/crypto/RSA_2048.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC6DCCAdCgAwIBAgIRAKtA7n5MZorcd0TquNdXF74wDQYJKoZIhvcNAQELBQAw +EzERMA8GA1UEChMIQXV0aGVsaWEwIBcNMjMwNDE3MTM0NDI4WhgPMjEwMDAxMDEw +MDAwMDBaMBMxETAPBgNVBAoTCEF1dGhlbGlhMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEAugGNhIscNEpZtxdrTuRAyeGjSERuue56uF0lMDvh8YDi0alQ +y4q80FmIwfP9lWmji9IkMvf1iD0M2I55WBPrL9ddqtpdkIfLgz6eX791LKtF9n8u +PwuX2jUDtWdJrMlOIJ8wCcTjyCFzjTFujAtXGffjWt4tlKCWZZUkJqmyfBiQIag6 +ZFb1S6VXFXFpWTOIc41X2VBmzpSLnfqEDqgp/KMDja1tDYAhFh3IAFSzBpXqUGT4 +cH2wJUcrngdaiHR/2ToJRu66jK8akB35gjmKiF/Tp9pUI7/rBgPeFWDCZuuZa3k3 +brfjJkynQAoCNajfxy8cglCxAuG+jWubFDPGewIDAQABozUwMzAOBgNVHQ8BAf8E +BAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADANBgkqhkiG +9w0BAQsFAAOCAQEAoIXTZcKC13KW1GNhzx9ECFTs/gatfjkYONRz+M2wjpVGHzsn +JUPXhoT1SL9WdWYXVVCUXrQge9n9n6IccDjwddnoWL2JMXnNd2PAJLgwE2Xfd40o +U1CLTwvsVNCXjoQLyjkg+SbWGqApVS3oj+A8RTtSBbztP+CoOqbyD3Roo1sFHeE8 +PXYboT5bIIaU7DaxhItGHwVDLLOSD72FP/5i+ZmFse2EzUUdyi6d4FSjk7pZCX1T +/2w/bqk3zRemBqDwTnH+sMhPUPvcOg6AIR5YdjWYSDz45sDdgBpUZYYTPfSz2nUL +PJwsB/gk0asMwSYprat6sJ0X3xrtg1ak3a7LkQ== +-----END CERTIFICATE----- diff --git a/internal/configuration/test_resources/crypto/RSA_2048.pem b/internal/configuration/test_resources/crypto/RSA_2048.pem new file mode 100644 index 000000000..f901b0568 --- /dev/null +++ b/internal/configuration/test_resources/crypto/RSA_2048.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEAugGNhIscNEpZtxdrTuRAyeGjSERuue56uF0lMDvh8YDi0alQ +y4q80FmIwfP9lWmji9IkMvf1iD0M2I55WBPrL9ddqtpdkIfLgz6eX791LKtF9n8u +PwuX2jUDtWdJrMlOIJ8wCcTjyCFzjTFujAtXGffjWt4tlKCWZZUkJqmyfBiQIag6 +ZFb1S6VXFXFpWTOIc41X2VBmzpSLnfqEDqgp/KMDja1tDYAhFh3IAFSzBpXqUGT4 +cH2wJUcrngdaiHR/2ToJRu66jK8akB35gjmKiF/Tp9pUI7/rBgPeFWDCZuuZa3k3 +brfjJkynQAoCNajfxy8cglCxAuG+jWubFDPGewIDAQABAoIBAD7IkWUAs4du5TNo +wz7AyqGZ+MxG1P0LYv7h6dCLFeu3blgIh438iVjmL8QPwDNzkdF7H97YVVckDDb4 +eDrjlknyrtohlN1ZCLeHJlv5OurV8OqP6SM8nYf4xwSvFW4uEKHwOX3CqIP/zooE ++mRo24CXbHVacxYs0jb9jVNDikxaRh57aL9wqvg8/SUhe2T2VXzTO9z0ZYNuIyF/ +EkOLkRM1FY7o3zHuYeRN2Y/QbS7yOT4aIodHBtwvObWs8g6Sq8cSbss72x22fY8J +uPwDASy93EOfn7phx9rGq6ioy5XpZtiGHPs+dThftBoOUCq3x2PxdVjGRwnZ1LRd +kEhWm3kCgYEA8LelRt6go8tqUGj82Y+RkKlbviO7+P8Hol1B1fV+wIlLK6CRm6Mb +3PB7fRdBsxcuBFf8WfVdfIkSHEi2uG+Ehb54AySTdsgRYHSQtSYMvKyKxHcsEJ6x +5uNK425N6wVS86MGoIK5li6jpVwdnA8bE6cvEMBRMXlTTIzvwM1+AV8CgYEAxdCw +qqaS6aYBH5Ol0hUi3wW6HaLUOVGksrn3ZTfAGsgoX/mgF8qinz1NmuSzKoevvVK9 +q9TC+GDrVNQcD7s2kQQD3Ni8Jb3ok360HcqRN+n8O5+v/t7OjHSsqSRmnQX/lvWO ++65r442ziHXrhFT2CwmY3zBHXe2+MBrGHmrgRGUCgYEAkRoSbdrrOHD44Am5SSfq +1inQnJgLyjdpEa1nbyLxyfu4rU64FvpGZHMt7SSkvODfI00qV8u5E8XIffYy9pB6 +cOh0jWhx36sQFnWNeTS7fsv/RhiUHlya3pPqY5ftLhtiemyuJPlIB8iLarVRP+43 +IyynCVD0YH9DACUArNbx+r8CgYBpd9EZy2I9DPNAYLpifj5vZmBK+MvqG6uSVzCe +WNEl9l4AfdlrlfCKsma0FQepv1pluL3D5dZmE1aljcnAYXLAcsGUeEIoZU6hhUaH +M7+lbi27pHJzk1vQ60w7ilrjkZUqaZZofiCr3JtCQIznq1zbmaxWIymJ3P4wK7ZB +9X3JOQKBgQDHPBrRVP5Od7dd/C+yi7p2CujuLbV4vKczpNf7Kj+OwQZtnZxjzi95 +ObquHDkrz5+OhkvZwKNKO89UQzvhT+7gpQpZJ/gdVl1oNuZ5gAsJ4aDWX+NZ00Z0 +Nkb2HpYR8xlnt6rFV465SYun1CIz5h1sUD1T75LDK340SrMbQq2Ilw== +-----END RSA PRIVATE KEY----- diff --git a/internal/configuration/test_resources/crypto/RSA_2048_PAIR_PRIVATE.pem b/internal/configuration/test_resources/crypto/RSA_2048_PAIR_PRIVATE.pem new file mode 100644 index 000000000..8618ed9d9 --- /dev/null +++ b/internal/configuration/test_resources/crypto/RSA_2048_PAIR_PRIVATE.pem @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAwwT+q3AlxKeidlD706fdK9G5d+R0vK8D+eThGULFyzQfC4A3 +zmG92yWtrLcux1jsFBunjHCU7CnJTMazgMSl9K0oDIv5vZoIau+aq80Y+jP3CfyR +guqzQRwVdLZ3Z2rgFISsdhBTFXAyPpaZUfPjqlCXb5DMLoWwva8feA9PD9YOsuhV +A2aWbBYdYL0XlFP+dOCGbhB6Y+Kr3OXhXIYnFbX1bf8Jr1y+bvyf4B3zasl7awRx +lRn1kI2zpNIWFL1Srpj9NlwkcsW2ne03+JwgJtfaCRBTkZMOh1sgGC4Xjby7KrBY +21YLkpHhDJadg9NEdhD+St4MfdAQKkG3e/UWcQIDAQABAoIBABfi1bpzyvxyN9Dc +DGwZJFrInjnUDoRJv2ftI7DvX8CKyr6i3rL1f8aGr+X2rdEW0BuKY5Qs+eCPIau4 +rqW38EeuqbgXsOgLJLrMTBp8zXFfygM8Hyp0yq3P3cTk0G0nRvjcYy82wqZejpjh +4zeJcroakuHET23nTAV/nJAc9+cNbwwE41wIJ3t+xd/gECWc8LRWiwGjQJSaBVSK +D6/fKKqBMdk9QYeqM1Z/KGZW4PUw1IDgxbUXFWGAU5Vl1qYckDRVmVfrZOzyl1yM +nZ3cdTMACJ+i+T0SGUWlsZ3l8cmNryGnxE65tnQsRs6+r48/bwFh/2MgIDwy5Jle +IG36wMECgYEA12rREd1qAEDBVe9Vfz3RG//SedDhGtBcZiyZu6ndfioAW9glxRCD +j+ICiTVH6Uipz8FWIn8pHkr0uosgE1fDEW2LIH6/FAHalNhGSQc7omSfOrxgUf7H +fH/YS9sAErRDMhmPPdWj6pYO9MWG8FNiEsTspAEBLh8JFJnOzVkGv3kCgYEA58Ju +mnjj8wTkib4Tbi/CX0ZrnEiI0xAsLbAPzLhnhLvPvYvMnThSsRBbjg51fe6n/P9m +FgC1Bxd9kyY3MRRS7/7iPmMTQyOYTC83brBAFjCHiTVrQAyU1QsVhofCEsVFl8Iq +xSb9pO3A/HJCeKokxZ1WNTGbs3bylgK6XCX0eLkCgYAn1NJvsTcmcNLO5wAyFOYT +fUwXxi25XYmYQuryLkiMSYvjb6YcOB97fVjmsfloA02S4rbgjg62UTnLPGpj6Thi +gpTVH1qJgoY+O3dTjYjTUDO5Epfk0W1lceY2sHnk+3vpSZyY3GYAvFprnBKFYYWi +3tK6yIzFUGvHaWE5yxpaUQKBgAJ2/egkqv/1qFySqfA9D8slm5Vg7Buai2289p4N +xAQUX0Q7zWRKqg56Bw8+th8tv5mgOby4KzS1Gj/LY0DhX3Rv+IYDVbwcD82XnvZN +Z5EU2QwrhkV7HMYbRRJWNUkv0eqoRP4tyPrNEIVezbgszxFO+BC+w1IoTLItuz+p +o6z5AoGAeFRJrSzapMU9RSXdIxkoT6Hsalunbqw1yTedlCpqGggFujfX/GCToWB7 +PZeWUOWijK818+IR5YJbvFHx1q/4zf5CCfOHHp1TmmA9bR/622oQF6/jCrTnMTqK +0BpcWXXzh241EzE1rXpLHxnywCygxEGELRFsCR5YVFxdwPf1ZgM= +-----END RSA PRIVATE KEY----- diff --git a/internal/configuration/test_resources/crypto/RSA_2048_PAIR_PUBLIC.pem b/internal/configuration/test_resources/crypto/RSA_2048_PAIR_PUBLIC.pem new file mode 100644 index 000000000..84ffbac69 --- /dev/null +++ b/internal/configuration/test_resources/crypto/RSA_2048_PAIR_PUBLIC.pem @@ -0,0 +1,8 @@ +-----BEGIN RSA PUBLIC KEY----- +MIIBCgKCAQEAwwT+q3AlxKeidlD706fdK9G5d+R0vK8D+eThGULFyzQfC4A3zmG9 +2yWtrLcux1jsFBunjHCU7CnJTMazgMSl9K0oDIv5vZoIau+aq80Y+jP3CfyRguqz +QRwVdLZ3Z2rgFISsdhBTFXAyPpaZUfPjqlCXb5DMLoWwva8feA9PD9YOsuhVA2aW +bBYdYL0XlFP+dOCGbhB6Y+Kr3OXhXIYnFbX1bf8Jr1y+bvyf4B3zasl7awRxlRn1 +kI2zpNIWFL1Srpj9NlwkcsW2ne03+JwgJtfaCRBTkZMOh1sgGC4Xjby7KrBY21YL +kpHhDJadg9NEdhD+St4MfdAQKkG3e/UWcQIDAQAB +-----END RSA PUBLIC KEY----- diff --git a/internal/configuration/test_resources/crypto/RSA_2048_PKCS8.crt b/internal/configuration/test_resources/crypto/RSA_2048_PKCS8.crt new file mode 100644 index 000000000..afac8a846 --- /dev/null +++ b/internal/configuration/test_resources/crypto/RSA_2048_PKCS8.crt @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIC6DCCAdCgAwIBAgIRAIs/ORkJgOjf/C7gnnKyjicwDQYJKoZIhvcNAQELBQAw +EzERMA8GA1UEChMIQXV0aGVsaWEwIBcNMjMwNDE4MDU0NjUwWhgPMjEwMDAxMDEw +MDAwMDBaMBMxETAPBgNVBAoTCEF1dGhlbGlhMIIBIjANBgkqhkiG9w0BAQEFAAOC +AQ8AMIIBCgKCAQEA6ZPuDj+TItlSNL+rx4h2L/oxIVZzPVf/fD92b1zIILlMCAMh +MB1PbaUtxnFfkdT5OGnIQaF9aZtEk3Hf80YSVvOHSEhKKAtmN+Zd+kTvasJBZzHM +r82+ZhDKCAVkIj8+vlSSQ7plV8/m9EC4aUQE0qRCuTthqluLfLwJ+ceNnuTIuQnD +Qg10qG7+bycjPNFQuXDnU+voJ4PCQ5OVo1ZfN/u2KthtfenbqIx5Xi9JVFyA9Xik +ZDTm87OJahSi9bfbhi+3aJJOGpgOC3hEA+vNh0b6kbkbHXMaCCM26/3R/inECywd +r9FQqVyOm3eQIdPKNxwbqdMyWrgoQYSOvVliZwIDAQABozUwMzAOBgNVHQ8BAf8E +BAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0TAQH/BAIwADANBgkqhkiG +9w0BAQsFAAOCAQEAaDeYs35r0+bSjWQl4TlBdpcOBbceffQ4DY1OqXT7aogyGcr7 +O8wzvf4bEHAkzGo0GdvaBOp/rbJEQOmpXQaGiTg1V/5rFfi9RG/7NhOCmto8RSee +66pYYJPfUR7nv/EnfJExGCz+vhroaHlqRJJfXJjT8XCzG5KnbUm8PL5QG4GounzZ +u0+seG+lTMaxHNdLxZ9Zp5ZTbXZjQ9F5/bdvuKJnM9wD1EW6JJw05Dl1hjl2Ak0E +X34dUe0XNWirfpjntjd5mt+QpcLYJi5tqjiskrMzhoGFKh4g9lXQA3qLX2aaPAt4 +28ew+YkL7/qvP3MBE91bJ/IWtYX+3UCMke2+dw== +-----END CERTIFICATE----- diff --git a/internal/configuration/test_resources/crypto/RSA_2048_PKCS8.pem b/internal/configuration/test_resources/crypto/RSA_2048_PKCS8.pem new file mode 100644 index 000000000..c4d52da65 --- /dev/null +++ b/internal/configuration/test_resources/crypto/RSA_2048_PKCS8.pem @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDpk+4OP5Mi2VI0 +v6vHiHYv+jEhVnM9V/98P3ZvXMgguUwIAyEwHU9tpS3GcV+R1Pk4achBoX1pm0ST +cd/zRhJW84dISEooC2Y35l36RO9qwkFnMcyvzb5mEMoIBWQiPz6+VJJDumVXz+b0 +QLhpRATSpEK5O2GqW4t8vAn5x42e5Mi5CcNCDXSobv5vJyM80VC5cOdT6+gng8JD +k5WjVl83+7Yq2G196duojHleL0lUXID1eKRkNObzs4lqFKL1t9uGL7dokk4amA4L +eEQD682HRvqRuRsdcxoIIzbr/dH+KcQLLB2v0VCpXI6bd5Ah08o3HBup0zJauChB +hI69WWJnAgMBAAECggEASCEkbEX5m9NcbWmbFFzxklNChLb7kz/vZ2D5o94U9vYB +op/EyeTjOEq/3f34s0H/TApuisXhwpDuFlIeBDPpSeyeJBYewEr03+JFtxk+jcs7 +AzD/snJoj4Azw1JW37SEHaZkHIIc9YcAHQE3cVpN2vZanHTX7hGi+3vd4MJc22nX +8XD/KRrosulsaFbWho2H9K1g7p3BkRQASuNm6caGVM6uvon+GqrylFgGHYAgzFXW +5ZEbybLN3yjSHag3F6LKw1LLxZ1/amy2Zz8nt5DzPajJEATFyeMGVnGXV2gv0BT+ +2Xj6U46cvHIjQtfmWCm+vWTK9cexaAjgRNZhwMwOAQKBgQDtL9kJ3Pp41g90idiO +p2mNCKh5000b3W+7HPhi2MkuPEIwzotd4Yzxm6DYxTy1UlQ3v1TnCcxGxXHSqXRL +Iq07eK+KQjxfJmTJIc5BQgiGDczj4kxrPOjIj5uGwhgpMJRyquAN+opJjOMV+FIh +rQx5XQ7ZljtGvUvEMqrHPpdD5wKBgQD8GsyVV7CsqeSyEs3SubvggOAJXoivNdg0 +Mmy6DL2ctvMH9qp038V1gdgxIuZN7Iul1qLTDKhVeS7zg7PJ2T2/06XIM6bz8FPx +6E21QfFPVbQw73VQ/2uGzsZoLPBe5sVSz/GLVlgM3bJfTPadB92RRhWNO4HEz08x +KQBnPjkdgQKBgEu2UlnLqEiaTCSvO+mNlyvl76GzyZFzhg78mG01NkMECTz7MZGu +Rgd53kZT76URusBw2vFFN2f/7u2IGg9B6nppc992dT8KSnHJ0MUUBxSDozu7KRmy +P7yF2ueRXZUIZNqVoR/VMf94caS5t734N1smUW8zfYh/NIhUhB9F45NRAoGAT7Nt +QD2T1lJzwbReK6OaePRlX7DqR3IfYwkaBIuomlXgaYEbDI3+EBM3tPkSlEoXMBu2 +KEDVKwh/xm65tTOf6PhRbgSeYHp3H4BQqOArGOjAacQac7v8U4clhKPIbkhI09B3 +zZRDi/W+wZBEWwq0iov8nkTU1tKvd5w9y2YJioECgYEA4xyF1+QV4stitX59NsES +IG/PuqxjmGtvzOLgMhfDCm/yewtAWLqXNoIoL3ogbilC/n/J+70kKOjXRwAwXBae +7JBKm3hJQm9uDYG3VSVkR4+SWpZbvSNSBdndObE9Uj0kc9+FQh7DMggSgkCP6UUl +/oyZBYhR3oBq6TyHZSS03/g= +-----END PRIVATE KEY----- diff --git a/internal/configuration/test_resources/crypto/RSA_4096.crt b/internal/configuration/test_resources/crypto/RSA_4096.crt new file mode 100644 index 000000000..0a9d3ccd2 --- /dev/null +++ b/internal/configuration/test_resources/crypto/RSA_4096.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE5zCCAs+gAwIBAgIQFaK8pCAGUonNI4J2+aHgGzANBgkqhkiG9w0BAQsFADAT +MREwDwYDVQQKEwhBdXRoZWxpYTAgFw0yMzA0MTcxMzQ1MTBaGA8yMTAwMDEwMTAw +MDAwMFowEzERMA8GA1UEChMIQXV0aGVsaWEwggIiMA0GCSqGSIb3DQEBAQUAA4IC +DwAwggIKAoICAQCjdNqfv8DzXBfR4XXsskYFcUYSLx17BrQ1hQqyLCTaAluH80OE +7HyZbbklKyvw4ig5Rk8Slq1pv6JBK3dEsOWWth2BNZEQyyGXa/aa77Zj6FxdDRHb +H/retzJzxTaaiu+F50OU0PkE5clj/V5JsgKwm70GwOy6zLkGnlv0k0I8HzTYJd1u +9qfKLZpmkJ0oiR4TogEgZA+9atCxzAFbcrynEnStIrepxvad9oOlzFLxArFe5Ai8 +IsT3hgIo1SHFSVMhPdfsXVt1nIo5h2Ol2Ry932sIIDypNc70KsYgzQ4jC/6iRni4 +saKoUp9IIDCRl+zjlLM1csiufhzC8U0g+UvWFkzigTW4J+CneQk9nnb5BtfCAiir +6WjOicQJff9EuvQFYASljQypH8hunKcH9YWtT/DGRThpWRgDMMalHnEprC4uSrYy +1QajLCi0ncJIArW3SdyePc7tRebNIxY3/Phj5kMwfV+ypIso5nJyu41AQVaRT7U5 ++YHydvg3FGOa7JDUm1a27BQgpocz5yU5aazbffmPz/eRPqMa/YRsmzBLnuhkR2Fo +6aRoU0a3zQe9LcrP8gdxr2ZQqZYzdVJ2feywaeH6RN6jl3S2IlH/j71G0dyi3nSN +nC4pe7CHH/wtE6NYCzoPcpZIcDqWt1aCFKYCK1MacmWSycxzv0dzJo+CnwIDAQAB +ozUwMzAOBgNVHQ8BAf8EBAMCBaAwEwYDVR0lBAwwCgYIKwYBBQUHAwEwDAYDVR0T +AQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAL65NXfJDjTF/GeoB6s8V3fvuwviu +RVQZsbKO4i7nCgs7lFJbXDkW+ybXhq1fmOeCnD1BaF5wwBNB4rgb70PT2eGpKbC7 +7U4sgghU7CYSS5sc3dP2xAS1NaCrTAa3m8tlzPqhkR1VkZ9X16fG7kTp/CPXMKMT +zoBj+v3Iv3Gf+gSJu6a2PmvAPuV4w78PH9Lz2sZ7FkZYOYhzNgUS++vL2k53DIW9 +6mIqMhm6y9yzsxJx9FXKBWqNuqpL3Gp5KL0XWFy4JIbnpX1b0J4C8yAvF5QS8viO +3VFFcgGB5VWS6vMDAp/c6O+9Rzg0ZbfnLYAHxeSMGZ/Zkf/TnHNjKASFmEu912iH +c6ulT8hVxwTxi/P8eereFdsMib8Z0z871e/2KGZN9bwycVIsqZIllTM3vqcwc9wi +uu6eoScqx25qut2G7K2aQxtfPHmPfyh7/Ft3jZra3apzEuRE3KRBVWaSbDI2SjoP +LESJpBtFPnKOt2p9p/70iODv2lrfoMpj4eXXztJAJFUi4KkczomrU1WtJDc8J5Pp +9tBiNFR1bBKE4+9kwY+6x8LMJs94XjlbG7stoPki41qGR/8Th+n33GcIF4n9Up9l +2XR5/Iqewj2FJAkiYcalFasScU/hLTyjJzpYMOtAvVbBgvYm8IQ4Q5VBkQPPe6a4 +P+3smf8j9ywptqk= +-----END CERTIFICATE----- diff --git a/internal/configuration/test_resources/crypto/RSA_4096.pem b/internal/configuration/test_resources/crypto/RSA_4096.pem new file mode 100644 index 000000000..37cd7729f --- /dev/null +++ b/internal/configuration/test_resources/crypto/RSA_4096.pem @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIJJwIBAAKCAgEAo3Tan7/A81wX0eF17LJGBXFGEi8dewa0NYUKsiwk2gJbh/ND +hOx8mW25JSsr8OIoOUZPEpatab+iQSt3RLDllrYdgTWREMshl2v2mu+2Y+hcXQ0R +2x/63rcyc8U2morvhedDlND5BOXJY/1eSbICsJu9BsDsusy5Bp5b9JNCPB802CXd +bvanyi2aZpCdKIkeE6IBIGQPvWrQscwBW3K8pxJ0rSK3qcb2nfaDpcxS8QKxXuQI +vCLE94YCKNUhxUlTIT3X7F1bdZyKOYdjpdkcvd9rCCA8qTXO9CrGIM0OIwv+okZ4 +uLGiqFKfSCAwkZfs45SzNXLIrn4cwvFNIPlL1hZM4oE1uCfgp3kJPZ52+QbXwgIo +q+lozonECX3/RLr0BWAEpY0MqR/IbpynB/WFrU/wxkU4aVkYAzDGpR5xKawuLkq2 +MtUGoywotJ3CSAK1t0ncnj3O7UXmzSMWN/z4Y+ZDMH1fsqSLKOZycruNQEFWkU+1 +OfmB8nb4NxRjmuyQ1JtWtuwUIKaHM+clOWms2335j8/3kT6jGv2EbJswS57oZEdh +aOmkaFNGt80HvS3Kz/IHca9mUKmWM3VSdn3ssGnh+kTeo5d0tiJR/4+9RtHcot50 +jZwuKXuwhx/8LROjWAs6D3KWSHA6lrdWghSmAitTGnJlksnMc79HcyaPgp8CAwEA +AQKCAgAO31QBEwZwXiHAs/3x0mqylhLlFqpdBkghUoCdo4ya1XoUjZrIHmhb4XLm +Id52pW05gN8y9sjChXAy88x/UIUjSGC43/HaEFF3IJiokkULJBo7UTQdtvQxjYOm +qvwD5b5Tda5dfQIbYvkHAwewNuUtwo3ZbnZbrMLtCj2drERrif9Z52AVd5XevHV+ +/Yt/I7K74JKvqssP1gc1FjXNZ0wo+3HoSu9hIDxSNRrXXBbz3OXcl20ACT3Ys7XA +l1viQoCw1pqt4/StZ9ff0iTL80w9LnXjoGNEliPFbZrnYyD1KWM6yqSzUV5WaGYb +vuoMZUFll6MSquX9knX1etUkueofZE631DEtXagjdgoPwShxHzJM6dPqrelm79u0 +FWrptMCKActwGnFMOow8jDBnJ/ns7g95fKWthCZgyHhJHhs5EpNFEc1WTRuV+/Yb +564oJlgwTrSsNdBtdtmNLVFX2YqMXjeFkJuiJDFHEFpV8fMmHgVko0ihpFrohSza +ftfwCQKkd/L4huN4hMfZYwVd9rdeUqsJGXASnDu0f4Q1Bq6H9zQ//A+wBkh70Sq+ +2cYsW/GryI4h/Q5677xtKUA9IDMIz2opBBewjCJH+BRPUobcuyq/xWod217mo8gn +mHN5/8ysFPWJH42wL3aO20L0XUrps4n9ni5qW7sbiGg/VZCQyQKCAQEA2WpR4Csw +HOkhfnvay7NOEAiud/MLPLNPVzQioR+SdOqWuQ81Dx1iZf6ZkMJzKi7l/I9txG39 +QAh3aEUXStpjcmQygn+LS9zmZsaACnii/kBqNEJ7EVDkP/kL9qecgAUwSPB9xD8P +1SpcUVqcXfDpN9jQduLTHHcXgs5vUIUwFcC3OJKTMwWbSxMW2WmBsWrI/QqjprI3 +sFKHKOPs+RRALUF44L8jMnODzwbMEK1Bui+eFiNOI5QbU2cd/uVkKe+PZTMexziX +cBUa7gzrE2dDccLkVup8dRsXjZJlu2ET8VZYncLidjBzAiTy7E8hZwuHzzovvppr +c2PHvlX0JtrEvQKCAQEAwHcPEOCQP/cswnYgRSsI3uDCZN3PpiMQd1qKYiIQItKT +hBRPvYqzvDcsprSnqvcWqd6jlbvyhgZyi8xQt+Iyduib3scoOZ2w8P94B5YR3rT7 +u+BVd/A4iUxU0f8FfZ6sbwE5Q76XZdWwvFuy+lloM8y4BFe//VicA0JO7Rt4lFN2 +31WSGwa7nJuPbbIPCeY9hPQVkdgsVgSksW+TjuUxiRVUvcF60rAuErcDBnO4XcC6 +f/qGlLAUAzmQQqI0WpfldAFlYCrZi5UuJJaA7/w7h0a9MQmPx4UCcg4D1IZiyt0U +psueRzFBAndsgVLd+/v5t8Tkrz3M3/Dsw9yDg8RwiwKCAQB3gD7cjiB145YrZXxP +dpCzs3HiME6+4Hf9oIRgN3BSnxaVRUyOsEIDebuCm76dMwXqmhNlYmdOqNipEUDK +PdtnZrd0jxJLcnGZkAWUu9YrFdDKRLhMPkAXAZaXzmzw2Ok/TiBym47iRdRUSw+j +euVVcvCyR95tyO+9UCZTBcH2UuTiTX5nDu/ahfWLLrjAgcdTfmORHmgJnHL6AL2h +8oWL2m7MaYK5GlEam8vSZsi3w7CKzoEGgUO7xfPwxLkXa7tPjpeePPbP/mm86pDT +K3EguFS1iVE7NNbvU8ZjBermPeWbYSEEgYDVbuWvCZd8ghP1zS+s/keNNwz1C12V +da2pAoIBAFsBsSkM1oi4ivykuJucPsSMyL7DN6XaXLXjJR5D9xdQNRq2NAJvLI/q +Ev383G92CMxoDzgFOCdxswYxpVVd6vjZAqMzzux3iSxb0Fjd+DMzpvjumdttxn39 +jvoBOYpt1iFjFb3XyGUJx1k5jwbb8e7UdYrwJ0NXe+X6m7F4VOrmEIaIQt7urxXd +ZNO852mJ6jsM44okCsrdxTZ1iPN/oo2sfXaAn2AymIaW7SJG473JHSbYwnxaSgxA +Utt/MXxI6OGSq2nuuRFMiBYa6HsR7OAJbfpbCBaS6VYfFGaQ6PP91/8Ktxv4yUGu +UKtSEM9PFYR04KGQemjF1l7CzZkn8QMCggEAetZUlucrPeejwSaIJvxcyO59Vp73 +I5Yp6wnIQ3SZ3SIyx3GRJNU6uB8S6GR9Zu6CzdHSjuU9oBdmd4WdRle9H+8hooq0 +xWbtpZE90cXvx36Z1IqILSu1ZTJrdsCxTiU9vQmg23jRl5z8K2YnsN4ury7qiZBt +SPD051WfyTeyLG3A8gx9ugw3vyJwXgvE6d82BJvJoWS9IzuK7xGULD80zO/l18P+ +9ixzCh3rTi46FWESKu58HYNtWdTb0NolrqKexAx+IXhPBc8Zj7+Ip5u5s6XDV0Ek +tk96Zvf7GMFfPqRP6gidXeouyTu3pdcR7bCT9bVC0AcT/4n/L2D99ftFYQ== +-----END RSA PRIVATE KEY----- diff --git a/internal/configuration/validator/const.go b/internal/configuration/validator/const.go index f9933e372..ec29e700f 100644 --- a/internal/configuration/validator/const.go +++ b/internal/configuration/validator/const.go @@ -144,7 +144,7 @@ const ( const ( errFmtOIDCNoClientsConfigured = "identity_providers: oidc: option 'clients' must have one or " + "more clients configured" - errFmtOIDCNoPrivateKey = "identity_providers: oidc: option 'issuer_private_key' is required" + errFmtOIDCNoPrivateKey = "identity_providers: oidc: option 'issuer_private_key' or `issuer_jwks` is required" errFmtOIDCInvalidPrivateKeyBitSize = "identity_providers: oidc: option 'issuer_private_key' must be an RSA private key with %d bits or more but it only has %d bits" errFmtOIDCInvalidPrivateKeyMalformedMissingPublicKey = "identity_providers: oidc: option 'issuer_private_key' must be a valid RSA private key but the provided data is missing the public key bits" errFmtOIDCCertificateMismatch = "identity_providers: oidc: option 'issuer_private_key' does not appear to be the private key the certificate provided by option 'issuer_certificate_chain'" @@ -409,6 +409,7 @@ const ( attrOIDCRedirectURIs = "redirect_uris" attrOIDCTokenAuthMethod = "token_endpoint_auth_method" attrOIDCUsrSigAlg = "userinfo_signing_algorithm" + attrOIDCIDTokenSigAlg = "id_token_signing_alg" attrOIDCPKCEChallengeMethod = "pkce_challenge_method" ) @@ -416,7 +417,6 @@ var ( validOIDCCORSEndpoints = []string{oidc.EndpointAuthorization, oidc.EndpointPushedAuthorizationRequest, oidc.EndpointToken, oidc.EndpointIntrospection, oidc.EndpointRevocation, oidc.EndpointUserinfo} validOIDCClientScopes = []string{oidc.ScopeOpenID, oidc.ScopeEmail, oidc.ScopeProfile, oidc.ScopeGroups, oidc.ScopeOfflineAccess} - validOIDCClientUserinfoAlgorithms = []string{oidc.SigningAlgNone, oidc.SigningAlgRSAUsingSHA256} validOIDCClientConsentModes = []string{auto, oidc.ClientConsentModeImplicit.String(), oidc.ClientConsentModeExplicit.String(), oidc.ClientConsentModePreConfigured.String()} validOIDCClientResponseModes = []string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery, oidc.ResponseModeFragment} validOIDCClientResponseTypes = []string{oidc.ResponseTypeAuthorizationCodeFlow, oidc.ResponseTypeImplicitFlowIDToken, oidc.ResponseTypeImplicitFlowToken, oidc.ResponseTypeImplicitFlowBoth, oidc.ResponseTypeHybridFlowIDToken, oidc.ResponseTypeHybridFlowToken, oidc.ResponseTypeHybridFlowBoth} @@ -428,6 +428,7 @@ var ( validOIDCClientTokenEndpointAuthMethods = []string{oidc.ClientAuthMethodNone, oidc.ClientAuthMethodClientSecretPost, oidc.ClientAuthMethodClientSecretBasic, oidc.ClientAuthMethodClientSecretJWT} validOIDCClientTokenEndpointAuthMethodsConfidential = []string{oidc.ClientAuthMethodClientSecretPost, oidc.ClientAuthMethodClientSecretBasic} validOIDCClientTokenEndpointAuthSigAlgs = []string{oidc.SigningAlgHMACUsingSHA256, oidc.SigningAlgHMACUsingSHA384, oidc.SigningAlgHMACUsingSHA512} + validOIDCIssuerJWKSigningAlgs = []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA256, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgRSAUsingSHA384, oidc.SigningAlgRSAPSSUsingSHA384, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgRSAUsingSHA512, oidc.SigningAlgRSAPSSUsingSHA512, oidc.SigningAlgECDSAUsingP521AndSHA512} ) var ( diff --git a/internal/configuration/validator/identity_providers.go b/internal/configuration/validator/identity_providers.go index 21ba0aab1..a716c841f 100644 --- a/internal/configuration/validator/identity_providers.go +++ b/internal/configuration/validator/identity_providers.go @@ -1,12 +1,19 @@ package validator import ( + "crypto" + "crypto/ecdsa" + "crypto/ed25519" + "crypto/rsa" "fmt" "net/url" + "sort" "strconv" "strings" "time" + "gopkg.in/square/go-jose.v2" + "github.com/authelia/authelia/v4/internal/configuration/schema" "github.com/authelia/authelia/v4/internal/oidc" "github.com/authelia/authelia/v4/internal/utils" @@ -24,26 +31,9 @@ func validateOIDC(config *schema.OpenIDConnectConfiguration, val *schema.StructV setOIDCDefaults(config) - switch { - case config.IssuerPrivateKey == nil: - val.Push(fmt.Errorf(errFmtOIDCNoPrivateKey)) - default: - if config.IssuerCertificateChain.HasCertificates() { - if !config.IssuerCertificateChain.EqualKey(config.IssuerPrivateKey) { - val.Push(fmt.Errorf(errFmtOIDCCertificateMismatch)) - } + validateOIDCIssuer(config, val) - if err := config.IssuerCertificateChain.Validate(); err != nil { - val.Push(fmt.Errorf(errFmtOIDCCertificateChain, err)) - } - } - - if config.IssuerPrivateKey.PublicKey.N == nil { - val.Push(fmt.Errorf(errFmtOIDCInvalidPrivateKeyMalformedMissingPublicKey)) - } else if config.IssuerPrivateKey.Size()*8 < 2048 { - val.Push(fmt.Errorf(errFmtOIDCInvalidPrivateKeyBitSize, 2048, config.IssuerPrivateKey.Size()*8)) - } - } + sort.Sort(oidc.SortedSigningAlgs(config.Discovery.RegisteredJWKSigningAlgs)) if config.MinimumParameterEntropy != 0 && config.MinimumParameterEntropy < 8 { val.PushWarning(fmt.Errorf(errFmtOIDCServerInsecureParameterEntropy, config.MinimumParameterEntropy)) @@ -62,6 +52,163 @@ func validateOIDC(config *schema.OpenIDConnectConfiguration, val *schema.StructV } } +func validateOIDCIssuer(config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { + switch { + case config.IssuerPrivateKey != nil: + validateOIDCIssuerLegacy(config, val) + + fallthrough + case len(config.IssuerJWKS) != 0: + validateOIDCIssuerModern(config, val) + default: + val.Push(fmt.Errorf(errFmtOIDCNoPrivateKey)) + } +} + +func validateOIDCIssuerLegacy(config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { + j := &jose.JSONWebKey{Key: &config.IssuerPrivateKey.PublicKey} + + thumbprint, err := j.Thumbprint(crypto.SHA1) + if err != nil { + val.Push(fmt.Errorf("identity_providers: oidc: option 'issuer_private_key' failed to calculate thumbprint to configure key id value: %w", err)) + + return + } + + config.IssuerJWKS = append(config.IssuerJWKS, schema.JWK{ + KeyID: fmt.Sprintf("%x", thumbprint)[:6], + Algorithm: oidc.SigningAlgRSAUsingSHA256, + Use: oidc.KeyUseSignature, + Key: config.IssuerPrivateKey, + CertificateChain: config.IssuerCertificateChain, + }) +} + +//nolint:gocyclo // Refactor time permitting. +func validateOIDCIssuerModern(config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { + var ( + props *JWKProperties + err error + ) + + kids := make([]string, len(config.IssuerJWKS)) + + for i := 0; i < len(config.IssuerJWKS); i++ { + if key, ok := config.IssuerJWKS[i].Key.(*rsa.PrivateKey); ok && key.PublicKey.N == nil { + val.Push(fmt.Errorf("identity_providers: oidc: issuer_jwks: key #%d: option 'key' must be a valid RSA private key but the provided data is malformed as it's missing the public key bits", i+1)) + + continue + } + + switch n := len(config.IssuerJWKS[i].KeyID); { + case n == 0: + j := jose.JSONWebKey{} + + switch key := config.IssuerJWKS[i].Key.(type) { + case schema.CryptographicPrivateKey: + j.Key = key.Public() + case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey: + j.Key = key + default: + break + } + + if j.Key == nil { + break + } + + var thumbprint []byte + + if thumbprint, err = j.Thumbprint(crypto.SHA1); err != nil { + val.Push(fmt.Errorf("identity_providers: oidc: issuer_jwks: key #%d: option 'key' failed to calculate thumbprint to configure key id value: %w", i+1, err)) + + continue + } + + config.IssuerJWKS[i].KeyID = fmt.Sprintf("%x", thumbprint)[:6] + case n > 7: + val.Push(fmt.Errorf("identity_providers: oidc: issuer_jwks: key #%d with key id '%s': option `key_id`` must be 7 characters or less", i+1, config.IssuerJWKS[i].KeyID)) + } + + if config.IssuerJWKS[i].KeyID != "" && utils.IsStringInSlice(config.IssuerJWKS[i].KeyID, kids) { + val.Push(fmt.Errorf("identity_providers: oidc: issuer_jwks: key #%d with key id '%s': option 'key_id' must be unique", i+1, config.IssuerJWKS[i].KeyID)) + } + + kids[i] = config.IssuerJWKS[i].KeyID + + if !utils.IsStringAlphaNumeric(config.IssuerJWKS[i].KeyID) { + val.Push(fmt.Errorf("identity_providers: oidc: issuer_jwks: key #%d with key id '%s': option 'key_id' must only have alphanumeric characters", i+1, config.IssuerJWKS[i].KeyID)) + } + + if props, err = schemaJWKGetProperties(config.IssuerJWKS[i]); err != nil { + val.Push(fmt.Errorf("identity_providers: oidc: issuer_jwks: key #%d with key id '%s': option 'key' failed to get key properties: %w", i+1, config.IssuerJWKS[i].KeyID, err)) + + continue + } + + switch config.IssuerJWKS[i].Use { + case "": + config.IssuerJWKS[i].Use = props.Use + case oidc.KeyUseSignature: + break + default: + val.Push(fmt.Errorf("identity_providers: oidc: issuer_jwks: key #%d with key id '%s': option '%s' must be one of %s but it's configured as '%s'", i+1, config.IssuerJWKS[i].KeyID, "use", strJoinOr([]string{oidc.KeyUseSignature}), config.IssuerJWKS[i].Use)) + } + + switch { + case config.IssuerJWKS[i].Algorithm == "": + config.IssuerJWKS[i].Algorithm = props.Algorithm + case utils.IsStringInSlice(config.IssuerJWKS[i].Algorithm, validOIDCIssuerJWKSigningAlgs): + break + default: + val.Push(fmt.Errorf("identity_providers: oidc: issuer_jwks: key #%d with key id '%s': option '%s' must be one of %s but it's configured as '%s'", i+1, config.IssuerJWKS[i].KeyID, "algorithm", strJoinOr(validOIDCIssuerJWKSigningAlgs), config.IssuerJWKS[i].Algorithm)) + } + + if config.IssuerJWKS[i].Algorithm != "" { + if utils.IsStringInSlice(config.IssuerJWKS[i].Algorithm, config.Discovery.RegisteredJWKSigningAlgs) { + val.Push(fmt.Errorf("identity_providers: oidc: issuer_jwks: key #%d with key id '%s': option 'algorithm' must be unique but another key is using it", i+1, config.IssuerJWKS[i].KeyID)) + } else { + config.Discovery.RegisteredJWKSigningAlgs = append(config.Discovery.RegisteredJWKSigningAlgs, config.IssuerJWKS[i].Algorithm) + } + } + + if config.IssuerJWKS[i].Algorithm == oidc.SigningAlgRSAUsingSHA256 && config.Discovery.DefaultKeyID == "" { + config.Discovery.DefaultKeyID = config.IssuerJWKS[i].KeyID + } + + var checkEqualKey bool + + switch key := config.IssuerJWKS[i].Key.(type) { + case *rsa.PrivateKey: + checkEqualKey = true + + if key.Size() < 256 { + checkEqualKey = false + + val.Push(fmt.Errorf("identity_providers: oidc: issuer_jwks: key #%d with key id '%s': option 'key' is an RSA %d bit private key but it must be a RSA 2048 bit private key", i+1, config.IssuerJWKS[i].KeyID, key.Size()*8)) + } + case *ecdsa.PrivateKey: + checkEqualKey = true + default: + val.Push(fmt.Errorf("identity_providers: oidc: issuer_jwks: key #%d with key id '%s': option 'key' must be a *rsa.PrivateKey or *ecdsa.PrivateKey but it's a %T", i+1, config.IssuerJWKS[i].KeyID, key)) + } + + if config.IssuerJWKS[i].CertificateChain.HasCertificates() { + if checkEqualKey && !config.IssuerJWKS[i].CertificateChain.EqualKey(config.IssuerJWKS[i].Key) { + val.Push(fmt.Errorf("identity_providers: oidc: issuer_jwks: key #%d with key id '%s': option 'key' does not appear to be the private key the certificate provided by option 'certificate_chain'", i+1, config.IssuerJWKS[i].KeyID)) + } + + if err = config.IssuerJWKS[i].CertificateChain.Validate(); err != nil { + val.Push(fmt.Errorf("identity_providers: oidc: issuer_jwks: key #%d with key id '%s': option 'certificate_chain' produced an error during validation of the chain: %w", i+1, config.IssuerJWKS[i].KeyID, err)) + } + } + } + + if len(config.Discovery.RegisteredJWKSigningAlgs) != 0 && !utils.IsStringInSlice(oidc.SigningAlgRSAUsingSHA256, config.Discovery.RegisteredJWKSigningAlgs) { + val.Push(fmt.Errorf("identity_providers: oidc: issuer_jwks: keys: must at least have one key supporting the '%s' algorithm but only has %s", oidc.SigningAlgRSAUsingSHA256, strJoinAnd(config.Discovery.RegisteredJWKSigningAlgs))) + } +} + func setOIDCDefaults(config *schema.OpenIDConnectConfiguration) { if config.AccessTokenLifespan == time.Duration(0) { config.AccessTokenLifespan = schema.DefaultOpenIDConnectConfiguration.AccessTokenLifespan @@ -228,7 +375,7 @@ func validateOIDCClient(c int, config *schema.OpenIDConnectConfiguration, val *s validateOIDCClientRedirectURIs(c, config, val, errDeprecatedFunc) validateOIDCClientTokenEndpointAuth(c, config, val) - validateOIDDClientUserinfoAlgorithm(c, config, val) + validateOIDDClientSigningAlgs(c, config, val) validateOIDCClientSectorIdentifier(c, config, val) } @@ -516,13 +663,18 @@ func validateOIDCClientTokenEndpointAuth(c int, config *schema.OpenIDConnectConf } } -func validateOIDDClientUserinfoAlgorithm(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { - if config.Clients[c].UserinfoSigningAlgorithm == "" { - config.Clients[c].UserinfoSigningAlgorithm = schema.DefaultOpenIDConnectClientConfiguration.UserinfoSigningAlgorithm +func validateOIDDClientSigningAlgs(c int, config *schema.OpenIDConnectConfiguration, val *schema.StructValidator) { + if config.Clients[c].UserinfoSigningAlg == "" { + config.Clients[c].UserinfoSigningAlg = schema.DefaultOpenIDConnectClientConfiguration.UserinfoSigningAlg + } else if config.Clients[c].UserinfoSigningAlg != oidc.SigningAlgNone && !utils.IsStringInSlice(config.Clients[c].UserinfoSigningAlg, config.Discovery.RegisteredJWKSigningAlgs) { + val.Push(fmt.Errorf(errFmtOIDCClientInvalidValue, + config.Clients[c].ID, attrOIDCUsrSigAlg, strJoinOr(append(config.Discovery.RegisteredJWKSigningAlgs, oidc.SigningAlgNone)), config.Clients[c].UserinfoSigningAlg)) } - if !utils.IsStringInSlice(config.Clients[c].UserinfoSigningAlgorithm, validOIDCClientUserinfoAlgorithms) { + if config.Clients[c].IDTokenSigningAlg == "" { + config.Clients[c].IDTokenSigningAlg = schema.DefaultOpenIDConnectClientConfiguration.IDTokenSigningAlg + } else if !utils.IsStringInSlice(config.Clients[c].IDTokenSigningAlg, config.Discovery.RegisteredJWKSigningAlgs) { val.Push(fmt.Errorf(errFmtOIDCClientInvalidValue, - config.Clients[c].ID, attrOIDCUsrSigAlg, strJoinOr(validOIDCClientUserinfoAlgorithms), config.Clients[c].UserinfoSigningAlgorithm)) + config.Clients[c].ID, attrOIDCIDTokenSigAlg, strJoinOr(config.Discovery.RegisteredJWKSigningAlgs), config.Clients[c].IDTokenSigningAlg)) } } diff --git a/internal/configuration/validator/identity_providers_test.go b/internal/configuration/validator/identity_providers_test.go index edd438b32..93178a795 100644 --- a/internal/configuration/validator/identity_providers_test.go +++ b/internal/configuration/validator/identity_providers_test.go @@ -1,12 +1,15 @@ package validator import ( + "crypto/ecdsa" + "crypto/ed25519" "crypto/rsa" "crypto/x509" - "encoding/pem" "errors" "fmt" "net/url" + "os" + "strings" "testing" "time" @@ -30,7 +33,7 @@ func TestShouldRaiseErrorWhenInvalidOIDCServerConfiguration(t *testing.T) { require.Len(t, validator.Errors(), 2) - assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: option 'issuer_private_key' is required") + assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: option 'issuer_private_key' or `issuer_jwks` is required") assert.EqualError(t, validator.Errors()[1], "identity_providers: oidc: option 'clients' must have one or more clients configured") } @@ -39,14 +42,14 @@ func TestShouldNotRaiseErrorWhenCORSEndpointsValid(t *testing.T) { config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: MustParseRSAPrivateKey(testKey1), + IssuerPrivateKey: keyRSA2048, CORS: schema.OpenIDConnectCORSConfiguration{ Endpoints: []string{oidc.EndpointAuthorization, oidc.EndpointToken, oidc.EndpointIntrospection, oidc.EndpointRevocation, oidc.EndpointUserinfo}, }, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "example", - Secret: MustDecodeSecret("$plaintext$example"), + Secret: tOpenIDConnectPlainTextClientSecret, }, }, }, @@ -62,14 +65,14 @@ func TestShouldRaiseErrorWhenCORSEndpointsNotValid(t *testing.T) { config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: MustParseRSAPrivateKey(testKey1), + IssuerPrivateKey: keyRSA2048, CORS: schema.OpenIDConnectCORSConfiguration{ Endpoints: []string{oidc.EndpointAuthorization, oidc.EndpointToken, oidc.EndpointIntrospection, oidc.EndpointRevocation, oidc.EndpointUserinfo, "invalid_endpoint"}, }, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "example", - Secret: MustDecodeSecret("$plaintext$example"), + Secret: tOpenIDConnectPlainTextClientSecret, }, }, }, @@ -87,7 +90,7 @@ func TestShouldRaiseErrorWhenOIDCPKCEEnforceValueInvalid(t *testing.T) { config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: MustParseRSAPrivateKey(testKey1), + IssuerPrivateKey: keyRSA2048, EnforcePKCE: testInvalid, }, } @@ -106,7 +109,7 @@ func TestShouldRaiseErrorWhenOIDCCORSOriginsHasInvalidValues(t *testing.T) { config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: MustParseRSAPrivateKey(testKey1), + IssuerPrivateKey: keyRSA2048, CORS: schema.OpenIDConnectCORSConfiguration{ AllowedOrigins: utils.URLsFromStringSlice([]string{"https://example.com/", "https://site.example.com/subpath", "https://site.example.com?example=true", "*"}), AllowedOriginsFromClientRedirectURIs: true, @@ -114,7 +117,7 @@ func TestShouldRaiseErrorWhenOIDCCORSOriginsHasInvalidValues(t *testing.T) { Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "myclient", - Secret: MustDecodeSecret("$plaintext$jk12nb3klqwmnelqkwenm"), + Secret: tOpenIDConnectPlainTextClientSecret, Policy: "two_factor", RedirectURIs: []string{"https://example.com/oauth2_callback", "https://localhost:566/callback", "http://an.example.com/callback", "file://a/file"}, }, @@ -141,7 +144,7 @@ func TestShouldRaiseErrorWhenOIDCServerNoClients(t *testing.T) { config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: MustParseRSAPrivateKey(testKey1), + IssuerPrivateKey: keyRSA2048, }, } @@ -187,7 +190,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "client-1", - Secret: MustDecodeSecret("$plaintext$a-secret"), + Secret: tOpenIDConnectPlainTextClientSecret, Policy: "a-policy", RedirectURIs: []string{ "https://google.com", @@ -203,13 +206,13 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "client-x", - Secret: MustDecodeSecret("$plaintext$a-secret"), + Secret: tOpenIDConnectPlainTextClientSecret, Policy: policyTwoFactor, RedirectURIs: []string{}, }, { ID: "client-x", - Secret: MustDecodeSecret("$plaintext$a-secret"), + Secret: tOpenIDConnectPlainTextClientSecret, Policy: policyTwoFactor, RedirectURIs: []string{}, }, @@ -223,7 +226,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "client-check-uri-parse", - Secret: MustDecodeSecret("$plaintext$a-secret"), + Secret: tOpenIDConnectPlainTextClientSecret, Policy: policyTwoFactor, RedirectURIs: []string{ "http://abc@%two", @@ -239,7 +242,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "client-check-uri-abs", - Secret: MustDecodeSecret("$plaintext$a-secret"), + Secret: tOpenIDConnectPlainTextClientSecret, Policy: policyTwoFactor, RedirectURIs: []string{ "google.com", @@ -255,7 +258,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "client-valid-sector", - Secret: MustDecodeSecret("$plaintext$a-secret"), + Secret: tOpenIDConnectPlainTextClientSecret, Policy: policyTwoFactor, RedirectURIs: []string{ "https://google.com", @@ -269,7 +272,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "client-valid-sector", - Secret: MustDecodeSecret("$plaintext$a-secret"), + Secret: tOpenIDConnectPlainTextClientSecret, Policy: policyTwoFactor, RedirectURIs: []string{ "https://google.com", @@ -283,7 +286,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "client-invalid-sector", - Secret: MustDecodeSecret("$plaintext$a-secret"), + Secret: tOpenIDConnectPlainTextClientSecret, Policy: policyTwoFactor, RedirectURIs: []string{ "https://google.com", @@ -305,7 +308,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "client-invalid-sector", - Secret: MustDecodeSecret("$plaintext$a-secret"), + Secret: tOpenIDConnectPlainTextClientSecret, Policy: policyTwoFactor, RedirectURIs: []string{ "https://google.com", @@ -322,7 +325,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "client-bad-consent-mode", - Secret: MustDecodeSecret("$plaintext$a-secret"), + Secret: tOpenIDConnectPlainTextClientSecret, Policy: policyTwoFactor, RedirectURIs: []string{ "https://google.com", @@ -339,7 +342,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "client-bad-pkce-mode", - Secret: MustDecodeSecret("$plaintext$a-secret"), + Secret: tOpenIDConnectPlainTextClientSecret, Policy: policyTwoFactor, RedirectURIs: []string{ "https://google.com", @@ -356,7 +359,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "client-bad-pkce-mode-s256", - Secret: MustDecodeSecret("$plaintext$a-secret"), + Secret: tOpenIDConnectPlainTextClientSecret, Policy: policyTwoFactor, RedirectURIs: []string{ "https://google.com", @@ -376,7 +379,7 @@ func TestShouldRaiseErrorWhenOIDCServerClientBadValues(t *testing.T) { config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: MustParseRSAPrivateKey(testKey1), + IssuerPrivateKey: keyRSA2048, Clients: tc.Clients, }, } @@ -400,11 +403,11 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadScopes(t *testing.T) { config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: MustParseRSAPrivateKey(testKey1), + IssuerPrivateKey: keyRSA2048, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "good_id", - Secret: MustDecodeSecret("$plaintext$good_secret"), + Secret: tOpenIDConnectPlainTextClientSecret, Policy: "two_factor", Scopes: []string{"openid", "bad_scope"}, RedirectURIs: []string{ @@ -426,11 +429,11 @@ func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadGrantTypes(t *testing.T) config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: MustParseRSAPrivateKey(testKey1), + IssuerPrivateKey: keyRSA2048, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "good_id", - Secret: MustDecodeSecret(goodOpenIDConnectClientSecret), + Secret: tOpenIDConnectPBKDF2ClientSecret, Policy: "two_factor", GrantTypes: []string{"bad_grant_type"}, RedirectURIs: []string{ @@ -452,12 +455,12 @@ func TestShouldNotErrorOnCertificateValid(t *testing.T) { config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerCertificateChain: MustParseX509CertificateChain(testCert1), - IssuerPrivateKey: MustParseRSAPrivateKey(testKey1), + IssuerCertificateChain: certRSA2048, + IssuerPrivateKey: keyRSA2048, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "good_id", - Secret: MustDecodeSecret(goodOpenIDConnectClientSecret), + Secret: tOpenIDConnectPBKDF2ClientSecret, Policy: "two_factor", RedirectURIs: []string{ "https://google.com/callback", @@ -478,12 +481,12 @@ func TestShouldRaiseErrorOnCertificateNotValid(t *testing.T) { config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerCertificateChain: MustParseX509CertificateChain(testCert1), - IssuerPrivateKey: MustParseRSAPrivateKey(testKey2), + IssuerCertificateChain: certRSA2048, + IssuerPrivateKey: keyRSA4096, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "good_id", - Secret: MustDecodeSecret(goodOpenIDConnectClientSecret), + Secret: tOpenIDConnectPBKDF2ClientSecret, Policy: "two_factor", RedirectURIs: []string{ "https://google.com/callback", @@ -498,115 +501,7 @@ func TestShouldRaiseErrorOnCertificateNotValid(t *testing.T) { assert.Len(t, validator.Warnings(), 0) require.Len(t, validator.Errors(), 1) - assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: option 'issuer_private_key' does not appear to be the private key the certificate provided by option 'issuer_certificate_chain'") -} - -func TestShouldRaiseErrorOnKeySizeTooSmall(t *testing.T) { - validator := schema.NewStructValidator() - config := &schema.IdentityProvidersConfiguration{ - OIDC: &schema.OpenIDConnectConfiguration{ - HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: MustParseRSAPrivateKey(testKey3), - Clients: []schema.OpenIDConnectClientConfiguration{ - { - ID: "good_id", - Secret: MustDecodeSecret(goodOpenIDConnectClientSecret), - Policy: "two_factor", - RedirectURIs: []string{ - "https://google.com/callback", - }, - }, - }, - }, - } - - ValidateIdentityProviders(config, validator) - - assert.Len(t, validator.Warnings(), 0) - require.Len(t, validator.Errors(), 1) - - assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: option 'issuer_private_key' must be an RSA private key with 2048 bits or more but it only has 1024 bits") -} - -func TestShouldRaiseErrorOnKeyInvalidPublicKey(t *testing.T) { - validator := schema.NewStructValidator() - config := &schema.IdentityProvidersConfiguration{ - OIDC: &schema.OpenIDConnectConfiguration{ - HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: MustParseRSAPrivateKey(testKey3), - Clients: []schema.OpenIDConnectClientConfiguration{ - { - ID: "good_id", - Secret: MustDecodeSecret(goodOpenIDConnectClientSecret), - Policy: "two_factor", - RedirectURIs: []string{ - "https://google.com/callback", - }, - }, - }, - }, - } - - config.OIDC.IssuerPrivateKey.PublicKey.N = nil - - ValidateIdentityProviders(config, validator) - - assert.Len(t, validator.Warnings(), 0) - require.Len(t, validator.Errors(), 1) - - assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: option 'issuer_private_key' must be a valid RSA private key but the provided data is missing the public key bits") -} - -func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadResponseModes(t *testing.T) { - validator := schema.NewStructValidator() - config := &schema.IdentityProvidersConfiguration{ - OIDC: &schema.OpenIDConnectConfiguration{ - HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: MustParseRSAPrivateKey(testKey1), - Clients: []schema.OpenIDConnectClientConfiguration{ - { - ID: "good_id", - Secret: MustDecodeSecret("$plaintext$good_secret"), - Policy: "two_factor", - ResponseModes: []string{"bad_responsemode"}, - RedirectURIs: []string{ - "https://google.com/callback", - }, - }, - }, - }, - } - - ValidateIdentityProviders(config, validator) - - require.Len(t, validator.Errors(), 1) - assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: client 'good_id': option 'response_modes' must only have the values 'form_post', 'query', or 'fragment' but the values 'bad_responsemode' are present") -} - -func TestShouldRaiseErrorWhenOIDCClientConfiguredWithBadUserinfoAlg(t *testing.T) { - validator := schema.NewStructValidator() - config := &schema.IdentityProvidersConfiguration{ - OIDC: &schema.OpenIDConnectConfiguration{ - HMACSecret: "rLABDrx87et5KvRHVUgTm3pezWWd8LMN", - IssuerPrivateKey: MustParseRSAPrivateKey(testKey1), - Clients: []schema.OpenIDConnectClientConfiguration{ - { - ID: "good_id", - Secret: MustDecodeSecret("$plaintext$good_secret"), - Policy: "two_factor", - UserinfoSigningAlgorithm: "rs256", - RedirectURIs: []string{ - "https://google.com/callback", - }, - }, - }, - }, - } - - ValidateIdentityProviders(config, validator) - - require.Len(t, validator.Errors(), 1) - assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: client 'good_id': option 'userinfo_signing_algorithm' must be one of 'none' or 'RS256' but it's configured as 'rs256'") + assert.EqualError(t, validator.Errors()[0], "identity_providers: oidc: issuer_jwks: key #1 with key id '9c7423': option 'key' does not appear to be the private key the certificate provided by option 'certificate_chain'") } func TestValidateIdentityProvidersShouldRaiseWarningOnSecurityIssue(t *testing.T) { @@ -614,12 +509,12 @@ func TestValidateIdentityProvidersShouldRaiseWarningOnSecurityIssue(t *testing.T config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "abc", - IssuerPrivateKey: MustParseRSAPrivateKey(testKey1), + IssuerPrivateKey: keyRSA2048, MinimumParameterEntropy: 1, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "good_id", - Secret: MustDecodeSecret(goodOpenIDConnectClientSecret), + Secret: tOpenIDConnectPBKDF2ClientSecret, Policy: "two_factor", RedirectURIs: []string{ "https://google.com/callback", @@ -642,11 +537,11 @@ func TestValidateIdentityProvidersShouldRaiseErrorsOnInvalidClientTypes(t *testi config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "hmac1", - IssuerPrivateKey: MustParseRSAPrivateKey(testKey1), + IssuerPrivateKey: keyRSA2048, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "client-with-invalid-secret", - Secret: MustDecodeSecret("$plaintext$a-secret"), + Secret: tOpenIDConnectPlainTextClientSecret, Public: true, Policy: "two_factor", RedirectURIs: []string{ @@ -655,7 +550,7 @@ func TestValidateIdentityProvidersShouldRaiseErrorsOnInvalidClientTypes(t *testi }, { ID: "client-with-bad-redirect-uri", - Secret: MustDecodeSecret(goodOpenIDConnectClientSecret), + Secret: tOpenIDConnectPBKDF2ClientSecret, Public: false, Policy: "two_factor", RedirectURIs: []string{ @@ -680,7 +575,7 @@ func TestValidateIdentityProvidersShouldNotRaiseErrorsOnValidClientOptions(t *te config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "hmac1", - IssuerPrivateKey: MustParseRSAPrivateKey(testKey1), + IssuerPrivateKey: keyRSA2048, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "installed-app-client", @@ -739,11 +634,11 @@ func TestValidateIdentityProvidersShouldRaiseWarningOnPlainTextClients(t *testin config := &schema.IdentityProvidersConfiguration{ OIDC: &schema.OpenIDConnectConfiguration{ HMACSecret: "hmac1", - IssuerPrivateKey: MustParseRSAPrivateKey(testKey1), + IssuerPrivateKey: keyRSA2048, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "client-with-invalid-secret_standard", - Secret: MustDecodeSecret("$plaintext$a-secret"), + Secret: tOpenIDConnectPlainTextClientSecret, Policy: "two_factor", RedirectURIs: []string{ "https://localhost", @@ -1574,7 +1469,7 @@ func TestValidateOIDCClients(t *testing.T) { "ShouldSetDefaultUserInfoAlg", nil, func(t *testing.T, have *schema.OpenIDConnectConfiguration) { - assert.Equal(t, oidc.SigningAlgNone, have.Clients[0].UserinfoSigningAlgorithm) + assert.Equal(t, oidc.SigningAlgNone, have.Clients[0].UserinfoSigningAlg) }, tcv{ nil, @@ -1594,10 +1489,10 @@ func TestValidateOIDCClients(t *testing.T) { { "ShouldNotOverrideUserInfoAlg", func(have *schema.OpenIDConnectConfiguration) { - have.Clients[0].UserinfoSigningAlgorithm = oidc.SigningAlgRSAUsingSHA256 + have.Clients[0].UserinfoSigningAlg = oidc.SigningAlgRSAUsingSHA256 }, func(t *testing.T, have *schema.OpenIDConnectConfiguration) { - assert.Equal(t, oidc.SigningAlgRSAUsingSHA256, have.Clients[0].UserinfoSigningAlgorithm) + assert.Equal(t, oidc.SigningAlgRSAUsingSHA256, have.Clients[0].UserinfoSigningAlg) }, tcv{ nil, @@ -1617,10 +1512,10 @@ func TestValidateOIDCClients(t *testing.T) { { "ShouldRaiseErrorOnInvalidUserInfoAlg", func(have *schema.OpenIDConnectConfiguration) { - have.Clients[0].UserinfoSigningAlgorithm = "rs256" + have.Clients[0].UserinfoSigningAlg = "rs256" }, func(t *testing.T, have *schema.OpenIDConnectConfiguration) { - assert.Equal(t, "rs256", have.Clients[0].UserinfoSigningAlgorithm) + assert.Equal(t, "rs256", have.Clients[0].UserinfoSigningAlg) }, tcv{ nil, @@ -1636,7 +1531,7 @@ func TestValidateOIDCClients(t *testing.T) { }, nil, []string{ - "identity_providers: oidc: client 'test': option 'userinfo_signing_algorithm' must be one of 'none' or 'RS256' but it's configured as 'rs256'", + "identity_providers: oidc: client 'test': option 'userinfo_signing_algorithm' must be one of 'RS256' or 'none' but it's configured as 'rs256'", }, }, { @@ -1782,6 +1677,52 @@ func TestValidateOIDCClients(t *testing.T) { nil, nil, }, + { + "ShouldRaiseErrorOnIncorrectlyConfiguredTokenEndpointClientAuthMethodClientSecretJWT", + func(have *schema.OpenIDConnectConfiguration) { + have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT + have.Clients[0].Secret = tOpenIDConnectPBKDF2ClientSecret + }, + nil, + tcv{ + nil, + nil, + nil, + nil, + }, + tcv{ + []string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail}, + []string{oidc.ResponseTypeAuthorizationCodeFlow}, + []string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery}, + []string{oidc.GrantTypeAuthorizationCode}, + }, + nil, + []string{ + "identity_providers: oidc: client 'test': option 'secret' must be plaintext with option 'token_endpoint_auth_method' with a value of 'client_secret_jwt'", + }, + }, + { + "ShouldNotRaiseWarningOrErrorOnCorrectlyConfiguredTokenEndpointClientAuthMethodClientSecretJWT", + func(have *schema.OpenIDConnectConfiguration) { + have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT + have.Clients[0].Secret = tOpenIDConnectPlainTextClientSecret + }, + nil, + tcv{ + nil, + nil, + nil, + nil, + }, + tcv{ + []string{oidc.ScopeOpenID, oidc.ScopeGroups, oidc.ScopeProfile, oidc.ScopeEmail}, + []string{oidc.ResponseTypeAuthorizationCodeFlow}, + []string{oidc.ResponseModeFormPost, oidc.ResponseModeQuery}, + []string{oidc.GrantTypeAuthorizationCode}, + }, + nil, + nil, + }, { "ShouldRaiseErrorOnIncorrectlyConfiguredTokenEndpointClientAuthMethodClientSecretJWT", func(have *schema.OpenIDConnectConfiguration) { @@ -1832,7 +1773,7 @@ func TestValidateOIDCClients(t *testing.T) { "ShouldSetDefaultTokenEndpointAuthSigAlg", func(have *schema.OpenIDConnectConfiguration) { have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT - have.Clients[0].Secret = MustDecodeSecret("$plaintext$abc123") + have.Clients[0].Secret = tOpenIDConnectPlainTextClientSecret }, func(t *testing.T, have *schema.OpenIDConnectConfiguration) { assert.Equal(t, oidc.SigningAlgHMACUsingSHA256, have.Clients[0].TokenEndpointAuthSigningAlg) @@ -1884,11 +1825,11 @@ func TestValidateOIDCClients(t *testing.T) { "ShouldRaiseErrorOnInvalidTokenAuthAlgClientTypeConfidential", func(have *schema.OpenIDConnectConfiguration) { have.Clients[0].TokenEndpointAuthMethod = oidc.ClientAuthMethodClientSecretJWT - have.Clients[0].TokenEndpointAuthSigningAlg = "abc" - have.Clients[0].Secret = MustDecodeSecret("$plaintext$abc123") + have.Clients[0].TokenEndpointAuthSigningAlg = oidc.EndpointToken + have.Clients[0].Secret = tOpenIDConnectPlainTextClientSecret }, func(t *testing.T, have *schema.OpenIDConnectConfiguration) { - assert.Equal(t, "abc", have.Clients[0].TokenEndpointAuthSigningAlg) + assert.Equal(t, oidc.EndpointToken, have.Clients[0].TokenEndpointAuthSigningAlg) }, tcv{ nil, @@ -1914,10 +1855,13 @@ func TestValidateOIDCClients(t *testing.T) { for _, tc := range testCasses { t.Run(tc.name, func(t *testing.T) { have := &schema.OpenIDConnectConfiguration{ + Discovery: schema.OpenIDConnectDiscovery{ + RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, + }, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: "test", - Secret: MustDecodeSecret("$pbkdf2-sha512$310000$c8p78n7pUMln0jzvd4aK4Q$JNRBzwAo0ek5qKn50cFzzvE9RXV88h1wJn5KGiHrD0YKtZaR/nCb2CJPOsKaPK0hjf.9yHxzQGZziziccp6Yng"), + Secret: tOpenIDConnectPBKDF2ClientSecret, Scopes: tc.have.Scopes, ResponseModes: tc.have.ResponseModes, ResponseTypes: tc.have.ResponseTypes, @@ -2017,6 +1961,388 @@ func TestValidateOIDCClientTokenEndpointAuthMethod(t *testing.T) { } } +func TestValidateOIDCIssuer(t *testing.T) { + frankenchain := schema.NewX509CertificateChainFromCerts([]*x509.Certificate{certRSA2048.Leaf(), certRSA1024.Leaf()}) + frankenkey := &rsa.PrivateKey{} + + *frankenkey = *keyRSA2048 + + frankenkey.PublicKey.N = nil + + testCases := []struct { + name string + have schema.OpenIDConnectConfiguration + expected schema.OpenIDConnectConfiguration + errs []string + }{ + { + "ShouldMapLegacyConfiguration", + schema.OpenIDConnectConfiguration{ + IssuerPrivateKey: keyRSA2048, + }, + schema.OpenIDConnectConfiguration{ + IssuerPrivateKey: keyRSA2048, + IssuerJWKS: []schema.JWK{ + {KeyID: "e7dfdc", Key: keyRSA2048, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature}, + }, + Discovery: schema.OpenIDConnectDiscovery{ + DefaultKeyID: "e7dfdc", + RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, + }, + }, + nil, + }, + { + "ShouldSetDefaultKeyValues", + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA2048, CertificateChain: certRSA2048}, + {Key: keyECDSAP256, CertificateChain: certECDSAP256}, + {Key: keyECDSAP384, CertificateChain: certECDSAP384}, + {Key: keyECDSAP521, CertificateChain: certECDSAP521}, + }, + }, + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA2048, CertificateChain: certRSA2048, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "e7dfdc"}, + {Key: keyECDSAP256, CertificateChain: certECDSAP256, Algorithm: oidc.SigningAlgECDSAUsingP256AndSHA256, Use: oidc.KeyUseSignature, KeyID: "29b3f2"}, + {Key: keyECDSAP384, CertificateChain: certECDSAP384, Algorithm: oidc.SigningAlgECDSAUsingP384AndSHA384, Use: oidc.KeyUseSignature, KeyID: "e968b4"}, + {Key: keyECDSAP521, CertificateChain: certECDSAP521, Algorithm: oidc.SigningAlgECDSAUsingP521AndSHA512, Use: oidc.KeyUseSignature, KeyID: "6b20c3"}, + }, + Discovery: schema.OpenIDConnectDiscovery{ + DefaultKeyID: "e7dfdc", + RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgECDSAUsingP256AndSHA256, oidc.SigningAlgECDSAUsingP384AndSHA384, oidc.SigningAlgECDSAUsingP521AndSHA512}, + }, + }, + nil, + }, + { + "ShouldRaiseErrorsDuplicateRSA256Keys", + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA2048, CertificateChain: certRSA2048}, + {Key: keyRSA4096, CertificateChain: certRSA4096}, + }, + }, + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA2048, CertificateChain: certRSA2048, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "e7dfdc"}, + {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "9c7423"}, + }, + Discovery: schema.OpenIDConnectDiscovery{ + DefaultKeyID: "e7dfdc", + RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, + }, + }, + []string{ + "identity_providers: oidc: issuer_jwks: key #2 with key id '9c7423': option 'algorithm' must be unique but another key is using it", + }, + }, + { + "ShouldRaiseErrorsDuplicateRSA256Keys", + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA512}, + }, + }, + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA512, Use: oidc.KeyUseSignature, KeyID: "9c7423"}, + }, + Discovery: schema.OpenIDConnectDiscovery{ + DefaultKeyID: "", + RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA512}, + }, + }, + []string{ + "identity_providers: oidc: issuer_jwks: keys: must at least have one key supporting the 'RS256' algorithm but only has 'RS512'", + }, + }, + { + "ShouldRaiseErrorOnBadCurve", + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA4096, CertificateChain: certRSA4096}, + {Key: keyECDSAP224, CertificateChain: certECDSAP224}, + }, + }, + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "9c7423"}, + {Key: keyECDSAP224, CertificateChain: certECDSAP224}, + }, + Discovery: schema.OpenIDConnectDiscovery{ + DefaultKeyID: "9c7423", + RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, + }, + }, + []string{ + "identity_providers: oidc: issuer_jwks: key #2: option 'key' failed to calculate thumbprint to configure key id value: square/go-jose: unsupported/unknown elliptic curve", + }, + }, + { + "ShouldRaiseErrorOnBadRSAKey", + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA1024, CertificateChain: certRSA1024}, + }, + }, + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA1024, CertificateChain: certRSA1024, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "a9c018"}, + }, + Discovery: schema.OpenIDConnectDiscovery{ + DefaultKeyID: "a9c018", + RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, + }, + }, + []string{ + "identity_providers: oidc: issuer_jwks: key #1 with key id 'a9c018': option 'key' is an RSA 1024 bit private key but it must be a RSA 2048 bit private key", + }, + }, + { + "ShouldRaiseErrorOnBadAlg", + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: "invalid"}, + }, + }, + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: "invalid", Use: oidc.KeyUseSignature, KeyID: "9c7423"}, + }, + Discovery: schema.OpenIDConnectDiscovery{ + DefaultKeyID: "", + RegisteredJWKSigningAlgs: []string{"invalid"}, + }, + }, + []string{ + "identity_providers: oidc: issuer_jwks: key #1 with key id '9c7423': option 'algorithm' must be one of 'RS256', 'PS256', 'ES256', 'RS384', 'PS384', 'ES384', 'RS512', 'PS512', or 'ES512' but it's configured as 'invalid'", + "identity_providers: oidc: issuer_jwks: keys: must at least have one key supporting the 'RS256' algorithm but only has 'invalid'", + }, + }, + { + "ShouldRaiseErrorOnBadUse", + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA4096, CertificateChain: certRSA4096, Use: "invalid"}, + }, + }, + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: "invalid", KeyID: "9c7423"}, + }, + Discovery: schema.OpenIDConnectDiscovery{ + DefaultKeyID: "9c7423", + RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, + }, + }, + []string{ + "identity_providers: oidc: issuer_jwks: key #1 with key id '9c7423': option 'use' must be one of 'sig' but it's configured as 'invalid'", + }, + }, + { + "ShouldRaiseErrorOnBadKeyIDLength", + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA4096, CertificateChain: certRSA4096, KeyID: "thisistoolong"}, + }, + }, + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "thisistoolong"}, + }, + Discovery: schema.OpenIDConnectDiscovery{ + DefaultKeyID: "thisistoolong", + RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, + }, + }, + []string{ + "identity_providers: oidc: issuer_jwks: key #1 with key id 'thisistoolong': option `key_id`` must be 7 characters or less", + }, + }, + { + "ShouldRaiseErrorOnBadKeyIDCharacters", + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA4096, CertificateChain: certRSA4096, KeyID: "x@x"}, + }, + }, + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "x@x"}, + }, + Discovery: schema.OpenIDConnectDiscovery{ + DefaultKeyID: "x@x", + RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, + }, + }, + []string{ + "identity_providers: oidc: issuer_jwks: key #1 with key id 'x@x': option 'key_id' must only have alphanumeric characters", + }, + }, + { + "ShouldRaiseErrorOnBadKeyIDDuplicates", + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA4096, CertificateChain: certRSA4096, KeyID: "x"}, + {Key: keyRSA2048, CertificateChain: certRSA2048, Algorithm: oidc.SigningAlgRSAPSSUsingSHA256, KeyID: "x"}, + }, + }, + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA4096, CertificateChain: certRSA4096, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "x"}, + {Key: keyRSA2048, CertificateChain: certRSA2048, Algorithm: oidc.SigningAlgRSAPSSUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "x"}, + }, + Discovery: schema.OpenIDConnectDiscovery{ + DefaultKeyID: "x", + RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256, oidc.SigningAlgRSAPSSUsingSHA256}, + }, + }, + []string{ + "identity_providers: oidc: issuer_jwks: key #2 with key id 'x': option 'key_id' must be unique", + }, + }, + { + "ShouldRaiseErrorOnEd25519Keys", + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyEd2519, CertificateChain: certEd15519}, + }, + }, + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyEd2519, CertificateChain: certEd15519, KeyID: "d2dd94"}, + }, + Discovery: schema.OpenIDConnectDiscovery{ + DefaultKeyID: "", + RegisteredJWKSigningAlgs: []string(nil), + }, + }, + []string{ + "identity_providers: oidc: issuer_jwks: key #1 with key id 'd2dd94': option 'key' must be a *rsa.PrivateKey or *ecdsa.PrivateKey but it's a ed25519.PrivateKey", + }, + }, + { + "ShouldRaiseErrorOnCertificateAsKey", + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: publicRSA2048Pair}, + }, + }, + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: publicRSA2048Pair, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "904c62"}, + }, + Discovery: schema.OpenIDConnectDiscovery{ + DefaultKeyID: "904c62", + RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, + }, + }, + []string{ + "identity_providers: oidc: issuer_jwks: key #1 with key id '904c62': option 'key' must be a *rsa.PrivateKey or *ecdsa.PrivateKey but it's a *rsa.PublicKey", + }, + }, + { + "ShouldRaiseErrorOnInvalidChain", + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA2048, CertificateChain: frankenchain}, + }, + }, + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: keyRSA2048, CertificateChain: frankenchain, Algorithm: oidc.SigningAlgRSAUsingSHA256, Use: oidc.KeyUseSignature, KeyID: "e7dfdc"}, + }, + Discovery: schema.OpenIDConnectDiscovery{ + DefaultKeyID: "e7dfdc", + RegisteredJWKSigningAlgs: []string{oidc.SigningAlgRSAUsingSHA256}, + }, + }, + []string{ + "identity_providers: oidc: issuer_jwks: key #1 with key id 'e7dfdc': option 'certificate_chain' produced an error during validation of the chain: certificate #1 in chain is not signed properly by certificate #2 in chain: x509: invalid signature: parent certificate cannot sign this kind of certificate", + }, + }, + { + "ShouldRaiseErrorOnInvalidPrivateKeyN", + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: frankenkey}, + }, + }, + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: frankenkey}, + }, + Discovery: schema.OpenIDConnectDiscovery{ + DefaultKeyID: "", + RegisteredJWKSigningAlgs: []string(nil), + }, + }, + []string{ + "identity_providers: oidc: issuer_jwks: key #1: option 'key' must be a valid RSA private key but the provided data is malformed as it's missing the public key bits", + }, + }, + { + "ShouldRaiseErrorOnCertForKey", + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: certRSA2048}, + }, + }, + schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{ + {Key: certRSA2048}, + }, + Discovery: schema.OpenIDConnectDiscovery{ + DefaultKeyID: "", + RegisteredJWKSigningAlgs: []string(nil), + }, + }, + []string{ + "identity_providers: oidc: issuer_jwks: key #1 with key id '': option 'key' failed to get key properties: the key type 'schema.X509CertificateChain' is unknown or not valid for the configuration", + }, + }, + } + + var n int + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + val := schema.NewStructValidator() + + validateOIDCIssuer(&tc.have, val) + + assert.Equal(t, tc.expected.Discovery.DefaultKeyID, tc.have.Discovery.DefaultKeyID) + assert.Equal(t, tc.expected.Discovery.RegisteredJWKSigningAlgs, tc.have.Discovery.RegisteredJWKSigningAlgs) + assert.Equal(t, tc.expected.IssuerPrivateKey, tc.have.IssuerPrivateKey) + assert.Equal(t, tc.expected.IssuerCertificateChain, tc.have.IssuerCertificateChain) + + n = len(tc.expected.IssuerJWKS) + + require.Len(t, tc.have.IssuerJWKS, n) + + for i := 0; i < n; i++ { + t.Run(fmt.Sprintf("Key%d", i), func(t *testing.T) { + assert.Equal(t, tc.expected.IssuerJWKS[i].Algorithm, tc.have.IssuerJWKS[i].Algorithm) + assert.Equal(t, tc.expected.IssuerJWKS[i].Use, tc.have.IssuerJWKS[i].Use) + assert.Equal(t, tc.expected.IssuerJWKS[i].KeyID, tc.have.IssuerJWKS[i].KeyID) + assert.Equal(t, tc.expected.IssuerJWKS[i].Key, tc.have.IssuerJWKS[i].Key) + assert.Equal(t, tc.expected.IssuerJWKS[i].CertificateChain, tc.have.IssuerJWKS[i].CertificateChain) + }) + } + + n = len(tc.errs) + + require.Len(t, val.Errors(), n) + + for i := 0; i < n; i++ { + assert.EqualError(t, val.Errors()[i], tc.errs[i]) + } + }) + } +} + func MustDecodeSecret(value string) *schema.PasswordDigest { if secret, err := schema.DecodePasswordDigest(value); err != nil { panic(err) @@ -2025,128 +2351,153 @@ func MustDecodeSecret(value string) *schema.PasswordDigest { } } -func MustParseRSAPrivateKey(data string) *rsa.PrivateKey { - block, _ := pem.Decode([]byte(data)) - if block == nil || block.Bytes == nil || len(block.Bytes) == 0 { - panic("not pem encoded") +func MustLoadCrypto(alg, mod, ext string, extra ...string) any { + fparts := []string{alg, mod} + if len(extra) != 0 { + fparts = append(fparts, extra...) } - if block.Type != "RSA PRIVATE KEY" { - panic("not private key") - } + var ( + data []byte + decoded any + err error + ) - key, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { + if data, err = os.ReadFile(fmt.Sprintf(pathCrypto, strings.Join(fparts, "_"), ext)); err != nil { panic(err) } + if decoded, err = utils.ParseX509FromPEMRecursive(data); err != nil { + panic(err) + } + + return decoded +} + +func MustLoadCertificateChain(alg, op string) schema.X509CertificateChain { + decoded := MustLoadCrypto(alg, op, "crt") + + switch cert := decoded.(type) { + case *x509.Certificate: + return schema.NewX509CertificateChainFromCerts([]*x509.Certificate{cert}) + case []*x509.Certificate: + return schema.NewX509CertificateChainFromCerts(cert) + default: + panic(fmt.Errorf("the key was not a *x509.Certificate or []*x509.Certificate, it's a %T", cert)) + } +} + +func MustLoadCertificate(alg, op string) *x509.Certificate { + decoded := MustLoadCrypto(alg, op, "crt") + + cert, ok := decoded.(*x509.Certificate) + if !ok { + panic(fmt.Errorf("the key was not a *x509.Certificate, it's a %T", cert)) + } + + return cert +} + +func MustLoadEd15519PrivateKey(curve string, extra ...string) ed25519.PrivateKey { + decoded := MustLoadCrypto("ED25519", curve, "pem", extra...) + + key, ok := decoded.(ed25519.PrivateKey) + if !ok { + panic(fmt.Errorf("the key was not a ed25519.PrivateKey, it's a %T", key)) + } + return key } -func MustParseX509CertificateChain(data string) schema.X509CertificateChain { - chain, err := schema.NewX509CertificateChain(data) +func MustLoadECDSAPrivateKey(curve string, extra ...string) *ecdsa.PrivateKey { + decoded := MustLoadCrypto("ECDSA", curve, "pem", extra...) - if err != nil { - panic(err) + key, ok := decoded.(*ecdsa.PrivateKey) + if !ok { + panic(fmt.Errorf("the key was not a *ecdsa.PrivateKey, it's a %T", key)) } - return *chain + return key } -var ( - testCert1 = ` ------BEGIN CERTIFICATE----- -MIIC5jCCAc6gAwIBAgIRAJZ+6KrHw95zIDgm2arCTCgwDQYJKoZIhvcNAQELBQAw -EzERMA8GA1UEChMIQXV0aGVsaWEwHhcNMjIwOTA4MDIyNDQyWhcNMjMwOTA4MDIy -NDQyWjATMREwDwYDVQQKEwhBdXRoZWxpYTCCASIwDQYJKoZIhvcNAQEBBQADggEP -ADCCAQoCggEBAMAE7muDAJtLsV3WgOpjrZ1JD1RlhuSOa3V+4zo2NYFQSdZW18SZ -fYYgUwLOleEy3VQ3N9MEFh/rWNHYHdsBjDvz/Q1EzAlXqthGd0Sic/UDYtrahrko -jCSkZCQ5YVO9ivMRth6XdUlu7RHVYY3aSOWPx2wiw9cdN+e4p73W6KwyzT7ezbUD -0Nng0Z7CNQTLHv3LBsLUODc4aVOvp2B4aAaw6cn990buKMvUuo2ge9gh0c5gIOM5 -dU7xOGAt7RzwCIHnG4CGAWPFuuS215ZeelgQr/9/fhtzDqSuBZw5f10vXnAyBwei -vN6Kffj2RXB+koFwBguT84A6cfmxWllGNF0CAwEAAaM1MDMwDgYDVR0PAQH/BAQD -AgWgMBMGA1UdJQQMMAoGCCsGAQUFBwMBMAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcN -AQELBQADggEBAFvORjj7RGoIc3q0fv6QjuncZ0Mu1/24O0smCr6tq5d6RQBRpb1M -jEsbTMLZErrHbyw/DWC75eJhW6T+6HiVTo6brBXkmDL+QGkLgRNOkZla6cnmIpmL -bf9iPmmcThscQERgYZzNg19zqK8JAQU/6PgU/N6OXTL/mQQoB972ET9dUl7lGx1Q -2l8XBe8t4QTp4t1xd3c4azxWvFNpzWBjC5eBWiVHLJmFXr4xpcnPFYFETOkvEqwt -pMQ2x895BoLrep6b+g0xeF4pmmIQwA9KrUVr++gpYaRzytaOIYwcIPMzt9iLWKQe -6ZSOrTVi8pPugYXp+LhVk/WI7r8EWtyADu0= ------END CERTIFICATE-----` +func MustLoadRSAPublicKey(bits string, extra ...string) *rsa.PublicKey { + decoded := MustLoadCrypto("RSA", bits, "pem", extra...) - testKey1 = ` ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEAwATua4MAm0uxXdaA6mOtnUkPVGWG5I5rdX7jOjY1gVBJ1lbX -xJl9hiBTAs6V4TLdVDc30wQWH+tY0dgd2wGMO/P9DUTMCVeq2EZ3RKJz9QNi2tqG -uSiMJKRkJDlhU72K8xG2Hpd1SW7tEdVhjdpI5Y/HbCLD1x0357invdborDLNPt7N -tQPQ2eDRnsI1BMse/csGwtQ4NzhpU6+nYHhoBrDpyf33Ru4oy9S6jaB72CHRzmAg -4zl1TvE4YC3tHPAIgecbgIYBY8W65LbXll56WBCv/39+G3MOpK4FnDl/XS9ecDIH -B6K83op9+PZFcH6SgXAGC5PzgDpx+bFaWUY0XQIDAQABAoIBAQClcdpHcglMxOwe -kRpkWdwWAAQgUJXoSbnW86wu1NRHBfmInyyrrSBVN3aunXbQITZIQIdt3kB94haW -P6KBt5Svd2saSqOOjSWb0SMkVOCaQ/+h19VqpcASNj4+Y94y+8ZD5ofHVfJtghDr -Y7H5OhHDEZ3e0xlwODGaCyUkUY4KBv/oIlILoh4phbDYHkZH8AzDnEiyVE1JAWlN -voAQysgSU7eEnNCi1S07jl5bY+MD3XpJkAfQsJYhqYT/qetStZ12PuXjpbIr3y53 -qjCrKeWTyDN+gOznyIGuiR6nvXeQAw/o9hZiah4RuHXTPs/3GAcRXcuMR0pbgJ+B -yfX6eLK1AoGBAPKkJKPYJD2NHukAelNbT2OgeDIgJmfOIvLa73/x2hXvWwa4VwIC -POuKXtT/a02J4pYMGlaKXfHgLmaW2HPObOIjpxqgRIswsiKS1AbaDtkWnhaS1/SJ -oZ7Fk8DdX+1QT4J/fj/2uxRT0GhXdMxDpK7ekpmRE+APPCGhmOMgmWszAoGBAMqX -Ts1RdGWgDxLi15rDqdqRBARJG7Om/xC2voXVmbAb4Q+QoNrNeiIAM2usuhrVuj5V -c16m9fxswRNYqQBYyShDi5wp5a8UjfqDpzJdku2bmrBaL+XVq8PY+oTK6KS3ss8U -CGQ8P6Phz5JGavn/nDMRZ4EwEWqbEMUqJAJlpmIvAoGAQ9Wj8LJyn0qew6FAkaFL -dpzcPZdDZW35009l+a0RvWQnXJ+Yo5UglvEeRgoKY6kS0cQccOlKDl8QWdn+NZIW -WrqA8y6vOwKoKoZGBIxd7k8mb0UqXtFDf/HYtuis8tmrAN7H2vYNo0czUphwrNKU -bdcHwSsQFWns87IL3iO1AIUCgYBzmBX8jOePPN6c9hXzVoVKEshp8ZT+0uBilwLq -tk/07lNiYDGH5woy8E5mt62QtjaIbpVfgoCEwUEBWutDKWXNtYypVDabyWyhbhEu -abn2HX0L9smxqFNTcjCvKF/J7I74HQQUvVPKnIOlgMx1TOXBNcMLMXQekc/lz/+v -5nQjPQKBgQDjdJABeiy9tU0tzLWUVc5QoQKnlfSJoFLis46REb1yHwU9OjTc05Wx -5lAXdTjDmnelDdGWNWHjWOiKSkTxhvQD3jXriI5y8Sdxe3zS3ikYvbMbi97GJz0O -5oyNJo6/froW1dLkJJWR8hg2PQbtoOo6l9HHSd91BnJJ4qFbq9ZrXQ== ------END RSA PRIVATE KEY-----` + key, ok := decoded.(*rsa.PublicKey) + if !ok { + panic(fmt.Errorf("the key was not a *rsa.PublicKey, it's a %T", key)) + } - testKey2 = ` ------BEGIN RSA PRIVATE KEY----- -MIIEpAIBAAKCAQEA6z1LOg1ZCqb0lytXWZ+MRBpMHEXOoTOLYgfZXt1IYyE3Z758 -cyalk0NYQhY5cZDsXPYWPvAHiPMUxutWkoxFwby56S+AbIMa3/Is+ILrHRJs8Exn -ZkpyrYFxPX12app2kErdmAkHSx0Z5/kuXiz96PHs8S8/ZbyZolLHzdfLtSzjvRm5 -Zue5iFzsf19NJz5CIBfv8g5lRwtE8wNJoRSpn1xq7fqfuA0weDNFPzjlNWRLy6aa -rK7qJexRkmkCs4sLgyl+9NODYJpvmN8E1yhyC27E0joI6rBFVW7Ihv+cSPCdDzGp -EWe81x3AeqAa3mjVqkiq4u4Z2i8JDgBaPboqJwIDAQABAoIBAAFdLZ58jVOefDSU -L8F5R1rtvBs93GDa56f926jNJ6pLewLC+/2+757W+SAI+PRLntM7Kg3bXm/Q2QH+ -Q1Y+MflZmspbWCdI61L5GIGoYKyeers59i+FpvySj5GHtLQRiTZ0+Kv1AXHSDWBm -9XneUOqU3IbZe0ifu1RRno72/VtjkGXbW8Mkkw+ohyGbIeTx/0/JQ6sSNZTT3Vk7 -8i4IXptq3HSF0/vqZuah8rShoeNq72pD1YLM9YPdL5by1QkDLnqATDiCpLBTCaNV -I8sqYEun+HYbQzBj8ZACG2JVZpEEidONWQHw5BPWO95DSZYrVnEkuCqeH+u5vYt7 -CHuJ3AECgYEA+W3v5z+j91w1VPHS0VB3SCDMouycAMIUnJPAbt+0LPP0scUFsBGE -hPAKddC54pmMZRQ2KIwBKiyWfCrJ8Xz8Yogn7fJgmwTHidJBr2WQpIEkNGlK3Dzi -jXL2sh0yC7sHvn0DqiQ79l/e7yRbSnv2wrTJEczOOH2haD7/tBRyCYECgYEA8W+q -E9YyGvEltnPFaOxofNZ8LHVcZSsQI5b6fc0iE7fjxFqeXPXEwGSOTwqQLQRiHn9b -CfPmIG4Vhyq0otVmlPvUnfBZ2OK+tl5X2/mQFO3ROMdvpi0KYa994uqfJdSTaqLn -jjoKFB906UFHnDQDLZUNiV1WwnkTglgLc+xrd6cCgYEAqqthyv6NyBTM3Tm2gcio -Ra9Dtntl51LlXZnvwy3IkDXBCd6BHM9vuLKyxZiziGx+Vy90O1xI872cnot8sINQ -Am+dur/tAEVN72zxyv0Y8qb2yfH96iKy9gxi5s75TnOEQgAygLnYWaWR2lorKRUX -bHTdXBOiS58S0UzCFEslGIECgYBqkO4SKWYeTDhoKvuEj2yjRYyzlu28XeCWxOo1 -otiauX0YSyNBRt2cSgYiTzhKFng0m+QUJYp63/wymB/5C5Zmxi0XtWIDADpLhqLj -HmmBQ2Mo26alQ5YkffBju0mZyhVzaQop1eZi8WuKFV1FThPlB7hc3E0SM5zv2Grd -tQnOWwKBgQC40yZY0PcjuILhy+sIc0Wvh7LUA7taSdTye149kRvbvsCDN7Jh75lM -USjhLXY0Nld2zBm9r8wMb81mXH29uvD+tDqqsICvyuKlA/tyzXR+QTr7dCVKVwu0 -1YjCJ36UpTsLre2f8nOSLtNmRfDPtbOE2mkOoO9dD9UU0XZwnvn9xw== ------END RSA PRIVATE KEY-----` + return key +} - testKey3 = `-----BEGIN RSA PRIVATE KEY----- -MIICXgIBAAKBgQDBi7fdmUmlpWklpgAvNUdhDrpsDVqAHuEzVApK6f6ohYAi0/q2 -+YmOwyPKDSrOc6Sy1myJtV3FbZGvYaQhnokc4bnkS9DH0lY+6Hk2vKps5PrhRY/q -1EjnfwXvzhAzb25rGFwKcSvfvndMTVvxgqXVob+3pRt9maD6HFHAh2/NCQIDAQAB -AoGACT2bfLgJ3R/FomeHkLlxe//RBMGqdX2D8QhtKWB8qR0engsS6FOHrspAVjBE -v/Cjh2pXake/f2KY1w/JX1WLZEFXja2RFPeeDiiC/4S7pKCySUVeHO9rQ4SY5Frg -/s/QWWtmq7+1iu2DXhdGJA6fIurzSoDgUXo3NGFCYqIFaAECQQDUi9AAgEljmc2q -dAUQD0KNTcJFkpTafhfPiYc2GT1vS/bArtXRmvJmbIiRfVuGbM8z5ES7JGd5FyYL -i2WCCzUBAkEA6R14GVhN8NIPWEUrzjgOvjKlc2ZHskT3dYb3djpm69TK7GjLtHyq -qO7l4VJowsXI+o/6YucagF6+rH0O0VrwCQJBAM8twYDbi63knA8MrGqtFUg7haTf -bu1Tf84y1nOrQrEcMNg9E/sOuD2SicSXlwF/SrHgTgbFQ39LSzBxnm6WkgECQQCh -AQmB98tdGLggbyXiODV2h+Rd37aFGb0QHzerIIsVNtMwlPCcp733D4kWJqTUYWZ+ -KBL3XEahgs6Os5EYZ4aBAkEAjKE+2/nBYUdHVusjMXeNsE5rqwJND5zvYzmToG7+ -xhv4RUAe4dHL4IDQoQRjhr3Nw+JYvtzBx0Iq/178xMnGKg== ------END RSA PRIVATE KEY-----` +func MustLoadRSAPrivateKey(bits string, extra ...string) *rsa.PrivateKey { + decoded := MustLoadCrypto("RSA", bits, "pem", extra...) - goodOpenIDConnectClientSecret = "$pbkdf2-sha512$310000$c8p78n7pUMln0jzvd4aK4Q$JNRBzwAo0ek5qKn50cFzzvE9RXV88h1wJn5KGiHrD0YKtZaR/nCb2CJPOsKaPK0hjf.9yHxzQGZziziccp6Yng" //nolint:gosec + key, ok := decoded.(*rsa.PrivateKey) + if !ok { + panic(fmt.Errorf("the key was not a *rsa.PrivateKey, it's a %T", key)) + } + + return key +} + +const ( + pathCrypto = "../test_resources/crypto/%s.%s" ) + +//nolint:unused +var ( + tOpenIDConnectPBKDF2ClientSecret, tOpenIDConnectPlainTextClientSecret *schema.PasswordDigest + + // Standard RSA key pair. + publicRSA2048Pair *rsa.PublicKey + privateRSA2048Pair *rsa.PrivateKey + + // Standard RSA key / certificate pairs. + keyRSA1024, keyRSA2048, keyRSA2048PKCS8, keyRSA4096 *rsa.PrivateKey + certRSA1024, certRSA2048, certRSA4096 schema.X509CertificateChain + + // Standard ECDSA key / certificate pairs. + keyECDSAP224, keyECDSAP256, keyECDSAP384, keyECDSAP521 *ecdsa.PrivateKey + certECDSAP224, certECDSAP256, certECDSAP384, certECDSAP521 schema.X509CertificateChain + + // Standard ECDSA key / certificate pairs. + keyECDSAP256PKCS8, keyECDSAP384PKCS8, keyECDSAP521PKCS8 *ecdsa.PrivateKey + certECDSAP224PKCS8, certECDSAP256PKCS8, certECDSAP384PKCS8, certECDSAP521PKCS8 schema.X509CertificateChain + + // Ed15519 key / certificate pair. + keyEd2519 ed25519.PrivateKey + certEd15519 schema.X509CertificateChain +) + +func init() { + tOpenIDConnectPBKDF2ClientSecret = MustDecodeSecret("$pbkdf2-sha512$310000$c8p78n7pUMln0jzvd4aK4Q$JNRBzwAo0ek5qKn50cFzzvE9RXV88h1wJn5KGiHrD0YKtZaR/nCb2CJPOsKaPK0hjf.9yHxzQGZziziccp6Yng") + tOpenIDConnectPlainTextClientSecret = MustDecodeSecret("$plaintext$example") + + publicRSA2048Pair = MustLoadRSAPublicKey("2048", "PAIR", "PUBLIC") + privateRSA2048Pair = MustLoadRSAPrivateKey("2048", "PAIR", "PRIVATE") + + keyRSA1024 = MustLoadRSAPrivateKey("1024") + keyRSA2048 = MustLoadRSAPrivateKey("2048") + keyRSA4096 = MustLoadRSAPrivateKey("4096") + keyECDSAP224 = MustLoadECDSAPrivateKey("P224") + keyECDSAP256 = MustLoadECDSAPrivateKey("P256") + keyECDSAP384 = MustLoadECDSAPrivateKey("P384") + keyECDSAP521 = MustLoadECDSAPrivateKey("P521") + keyEd2519 = MustLoadEd15519PrivateKey("PKCS8") + + keyRSA2048PKCS8 = MustLoadRSAPrivateKey("2048", "PKCS8") + keyECDSAP256PKCS8 = MustLoadECDSAPrivateKey("P256", "PKCS8") + keyECDSAP384PKCS8 = MustLoadECDSAPrivateKey("P384", "PKCS8") + keyECDSAP521PKCS8 = MustLoadECDSAPrivateKey("P521", "PKCS8") + + certRSA1024 = MustLoadCertificateChain("RSA", "1024") + certRSA2048 = MustLoadCertificateChain("RSA", "2048") + certRSA4096 = MustLoadCertificateChain("RSA", "4096") + certECDSAP224 = MustLoadCertificateChain("ECDSA", "P224") + certECDSAP256 = MustLoadCertificateChain("ECDSA", "P256") + certECDSAP384 = MustLoadCertificateChain("ECDSA", "P384") + certECDSAP521 = MustLoadCertificateChain("ECDSA", "P521") + certEd15519 = MustLoadCertificateChain("ED25519", "PKCS8") +} diff --git a/internal/configuration/validator/util.go b/internal/configuration/validator/util.go index 59b9411b1..e40f9ec4f 100644 --- a/internal/configuration/validator/util.go +++ b/internal/configuration/validator/util.go @@ -1,10 +1,17 @@ package validator import ( + "crypto/ecdsa" + "crypto/ed25519" + "crypto/elliptic" + "crypto/rsa" + "fmt" "strings" "golang.org/x/net/publicsuffix" + "github.com/authelia/authelia/v4/internal/configuration/schema" + "github.com/authelia/authelia/v4/internal/oidc" "github.com/authelia/authelia/v4/internal/utils" ) @@ -107,3 +114,47 @@ func validateList(values, valid []string, chkDuplicate bool) (invalid, duplicate return } + +type JWKProperties struct { + Use string + Algorithm string + Bits int + Curve elliptic.Curve +} + +func schemaJWKGetProperties(jwk schema.JWK) (properties *JWKProperties, err error) { + switch key := jwk.Key.(type) { + case nil: + return nil, fmt.Errorf("private key is nil") + case ed25519.PrivateKey, ed25519.PublicKey: + return &JWKProperties{}, nil + case *rsa.PrivateKey: + return &JWKProperties{oidc.KeyUseSignature, oidc.SigningAlgRSAUsingSHA256, key.Size(), nil}, nil + case *rsa.PublicKey: + return &JWKProperties{oidc.KeyUseSignature, oidc.SigningAlgRSAUsingSHA256, key.Size(), nil}, nil + case *ecdsa.PublicKey: + switch key.Curve { + case elliptic.P256(): + return &JWKProperties{oidc.KeyUseSignature, oidc.SigningAlgECDSAUsingP256AndSHA256, -1, key.Curve}, nil + case elliptic.P384(): + return &JWKProperties{oidc.KeyUseSignature, oidc.SigningAlgECDSAUsingP384AndSHA384, -1, key.Curve}, nil + case elliptic.P521(): + return &JWKProperties{oidc.KeyUseSignature, oidc.SigningAlgECDSAUsingP521AndSHA512, -1, key.Curve}, nil + default: + return &JWKProperties{oidc.KeyUseSignature, "", -1, key.Curve}, nil + } + case *ecdsa.PrivateKey: + switch key.Curve { + case elliptic.P256(): + return &JWKProperties{oidc.KeyUseSignature, oidc.SigningAlgECDSAUsingP256AndSHA256, -1, key.Curve}, nil + case elliptic.P384(): + return &JWKProperties{oidc.KeyUseSignature, oidc.SigningAlgECDSAUsingP384AndSHA384, -1, key.Curve}, nil + case elliptic.P521(): + return &JWKProperties{oidc.KeyUseSignature, oidc.SigningAlgECDSAUsingP521AndSHA512, -1, key.Curve}, nil + default: + return &JWKProperties{oidc.KeyUseSignature, "", -1, key.Curve}, nil + } + default: + return nil, fmt.Errorf("the key type '%T' is unknown or not valid for the configuration", key) + } +} diff --git a/internal/handlers/handler_jwks.go b/internal/handlers/handler_jwks.go index 14f680711..ad0f8a43b 100644 --- a/internal/handlers/handler_jwks.go +++ b/internal/handlers/handler_jwks.go @@ -8,9 +8,9 @@ import ( // JSONWebKeySetGET returns the JSON Web Key Set. Used in OAuth 2.0 and OpenID Connect 1.0. func JSONWebKeySetGET(ctx *middlewares.AutheliaCtx) { - ctx.SetContentType("application/json") + ctx.SetContentTypeApplicationJSON() - if err := json.NewEncoder(ctx).Encode(ctx.Providers.OpenIDConnect.KeyManager.GetKeySet()); err != nil { + if err := json.NewEncoder(ctx).Encode(ctx.Providers.OpenIDConnect.KeyManager.Set(ctx)); err != nil { ctx.Error(err, "failed to serve json web key set") } } diff --git a/internal/handlers/handler_oidc_authorization.go b/internal/handlers/handler_oidc_authorization.go index 9924c5ec1..9d8ea4e4e 100644 --- a/internal/handlers/handler_oidc_authorization.go +++ b/internal/handlers/handler_oidc_authorization.go @@ -127,7 +127,7 @@ func OpenIDConnectAuthorization(ctx *middlewares.AutheliaCtx, rw http.ResponseWr ctx.Logger.Debugf("Authorization Request with id '%s' on client with id '%s' was successfully processed, proceeding to build Authorization Response", requester.GetID(), clientID) - session := oidc.NewSessionWithAuthorizeRequest(issuer, ctx.Providers.OpenIDConnect.KeyManager.GetActiveKeyID(), + session := oidc.NewSessionWithAuthorizeRequest(issuer, ctx.Providers.OpenIDConnect.KeyManager.GetKIDFromAlg(ctx, client.GetIDTokenSigningAlg()), userSession.Username, userSession.AuthenticationMethodRefs.MarshalRFC8176(), extraClaims, authTime, consent, requester) ctx.Logger.Tracef("Authorization Request with id '%s' on client with id '%s' creating session for Authorization Response for subject '%s' with username '%s' with claims: %+v", diff --git a/internal/handlers/handler_oidc_token_test.go b/internal/handlers/handler_oidc_token_test.go index 52aacbbd9..279cc4e3d 100644 --- a/internal/handlers/handler_oidc_token_test.go +++ b/internal/handlers/handler_oidc_token_test.go @@ -145,11 +145,10 @@ func (s *ClientAuthenticationStrategySuite) SetupTest() { s.ctrl = gomock.NewController(s.T()) s.store = mocks.NewMockStorage(s.ctrl) - var err error - secret := MustDecodeSecret("$plaintext$client-secret") - s.provider, err = oidc.NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ + s.provider = oidc.NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ + IssuerJWKS: []schema.JWK{}, IssuerCertificateChain: schema.X509CertificateChain{}, IssuerPrivateKey: MustParseRSAPrivateKey(exampleRSAPrivateKey), HMACSecret: "abc123", @@ -370,8 +369,6 @@ func (s *ClientAuthenticationStrategySuite) SetupTest() { }, }, }, s.store, nil) - - s.Require().NoError(err) } func (s *ClientAuthenticationStrategySuite) TestShouldValidateAssertionHS256() { diff --git a/internal/handlers/handler_oidc_userinfo.go b/internal/handlers/handler_oidc_userinfo.go index 33b827bbe..9e98a0f4a 100644 --- a/internal/handlers/handler_oidc_userinfo.go +++ b/internal/handlers/handler_oidc_userinfo.go @@ -99,8 +99,18 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, ctx.Logger.Tracef("UserInfo Response with id '%s' on client with id '%s' is being sent with the following claims: %+v", requester.GetID(), clientID, claims) - switch client.GetUserinfoSigningAlgorithm() { - case oidc.SigningAlgRSAUsingSHA256: + switch alg := client.GetUserinfoSigningAlg(); alg { + case oidc.SigningAlgNone, "": + ctx.Providers.OpenIDConnect.Write(rw, req, claims) + default: + var jwk *oidc.JWK + + if jwk = ctx.Providers.OpenIDConnect.KeyManager.GetByAlg(ctx, alg); jwk == nil { + ctx.Providers.OpenIDConnect.WriteError(rw, req, errors.WithStack(fosite.ErrServerError.WithHintf("Unsupported UserInfo signing algorithm '%s'.", alg))) + + return + } + var jti uuid.UUID if jti, err = uuid.NewRandom(); err != nil { @@ -114,11 +124,11 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, headers := &jwt.Headers{ Extra: map[string]any{ - oidc.JWTHeaderKeyIdentifier: ctx.Providers.OpenIDConnect.KeyManager.GetActiveKeyID(), + oidc.JWTHeaderKeyIdentifier: jwk.KeyID(), }, } - if token, _, err = ctx.Providers.OpenIDConnect.KeyManager.Strategy().Generate(req.Context(), claims, headers); err != nil { + if token, _, err = jwk.Strategy().Generate(req.Context(), claims, headers); err != nil { ctx.Providers.OpenIDConnect.WriteError(rw, req, err) return @@ -126,9 +136,5 @@ func OpenIDConnectUserinfo(ctx *middlewares.AutheliaCtx, rw http.ResponseWriter, rw.Header().Set(fasthttp.HeaderContentType, "application/jwt") _, _ = rw.Write([]byte(token)) - case oidc.SigningAlgNone, "": - ctx.Providers.OpenIDConnect.Write(rw, req, claims) - default: - ctx.Providers.OpenIDConnect.WriteError(rw, req, errors.WithStack(fosite.ErrServerError.WithHintf("Unsupported UserInfo signing algorithm '%s'.", client.GetUserinfoSigningAlgorithm()))) } } diff --git a/internal/oidc/client.go b/internal/oidc/client.go index 6a100cdbe..64d1f22b3 100644 --- a/internal/oidc/client.go +++ b/internal/oidc/client.go @@ -34,7 +34,8 @@ func NewClient(config schema.OpenIDConnectClientConfiguration) (client Client) { EnforcePAR: config.EnforcePAR, - UserinfoSigningAlgorithm: config.UserinfoSigningAlgorithm, + IDTokenSigningAlg: config.IDTokenSigningAlg, + UserinfoSigningAlg: config.UserinfoSigningAlg, Policy: authorization.NewLevel(config.Policy), @@ -131,13 +132,22 @@ func (c *BaseClient) GetResponseModes() []fosite.ResponseModeType { return c.ResponseModes } -// GetUserinfoSigningAlgorithm returns the UserinfoSigningAlgorithm. -func (c *BaseClient) GetUserinfoSigningAlgorithm() string { - if c.UserinfoSigningAlgorithm == "" { - c.UserinfoSigningAlgorithm = SigningAlgNone +// GetIDTokenSigningAlg returns the IDTokenSigningAlg. +func (c *BaseClient) GetIDTokenSigningAlg() (alg string) { + if c.IDTokenSigningAlg == "" { + c.IDTokenSigningAlg = SigningAlgRSAUsingSHA256 } - return c.UserinfoSigningAlgorithm + return c.IDTokenSigningAlg +} + +// GetUserinfoSigningAlg returns the UserinfoSigningAlg. +func (c *BaseClient) GetUserinfoSigningAlg() string { + if c.UserinfoSigningAlg == "" { + c.UserinfoSigningAlg = SigningAlgNone + } + + return c.UserinfoSigningAlg } // GetPAREnforcement returns EnforcePAR. @@ -295,7 +305,7 @@ func (c *FullClient) GetTokenEndpointAuthMethod() string { if c.Public { c.TokenEndpointAuthMethod = ClientAuthMethodNone } else { - c.TokenEndpointAuthMethod = ClientAuthMethodClientSecretPost + c.TokenEndpointAuthMethod = ClientAuthMethodClientSecretBasic } } diff --git a/internal/oidc/client_auth.go b/internal/oidc/client_auth.go index 1b76d9438..c852153bc 100644 --- a/internal/oidc/client_auth.go +++ b/internal/oidc/client_auth.go @@ -74,8 +74,8 @@ func (p *OpenIDConnectProvider) DefaultClientAuthenticationStrategy(ctx context. return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf("This requested OAuth 2.0 client only supports client authentication method '%s', however that method is not supported by this server.", oidcClient.GetTokenEndpointAuthMethod())) } - if oidcClient.GetTokenEndpointAuthSigningAlgorithm() != fmt.Sprintf("%s", t.Header[HeaderParameterAlgorithm]) { - return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf("The 'client_assertion' uses signing algorithm '%s' but the requested OAuth 2.0 Client enforces signing algorithm '%s'.", t.Header[HeaderParameterAlgorithm], oidcClient.GetTokenEndpointAuthSigningAlgorithm())) + if oidcClient.GetTokenEndpointAuthSigningAlgorithm() != fmt.Sprintf("%s", t.Header[JWTHeaderKeyAlgorithm]) { + return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf("The 'client_assertion' uses signing algorithm '%s' but the requested OAuth 2.0 Client enforces signing algorithm '%s'.", t.Header[JWTHeaderKeyAlgorithm], oidcClient.GetTokenEndpointAuthSigningAlgorithm())) } switch t.Method { @@ -94,7 +94,7 @@ func (p *OpenIDConnectProvider) DefaultClientAuthenticationStrategy(ctx context. return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHint("This client does not support authentication method 'client_secret_jwt' as the client secret is not in plaintext.")) default: - return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf("The 'client_assertion' request parameter uses unsupported signing algorithm '%s'.", t.Header[HeaderParameterAlgorithm])) + return nil, errorsx.WithStack(fosite.ErrInvalidClient.WithHintf("The 'client_assertion' request parameter uses unsupported signing algorithm '%s'.", t.Header[JWTHeaderKeyAlgorithm])) } }) diff --git a/internal/oidc/client_test.go b/internal/oidc/client_test.go index ece18b278..814c42a3c 100644 --- a/internal/oidc/client_test.go +++ b/internal/oidc/client_test.go @@ -3,6 +3,7 @@ package oidc import ( "fmt" "testing" + "time" "github.com/ory/fosite" "github.com/stretchr/testify/assert" @@ -26,8 +27,8 @@ func TestNewClient(t *testing.T) { bclient, ok := client.(*BaseClient) require.True(t, ok) - assert.Equal(t, "", bclient.UserinfoSigningAlgorithm) - assert.Equal(t, SigningAlgNone, client.GetUserinfoSigningAlgorithm()) + assert.Equal(t, "", bclient.UserinfoSigningAlg) + assert.Equal(t, SigningAlgNone, client.GetUserinfoSigningAlg()) _, ok = client.(*FullClient) assert.False(t, ok) @@ -51,7 +52,7 @@ func TestNewClient(t *testing.T) { assert.Equal(t, authorization.TwoFactor, client.GetAuthorizationPolicy()) config = schema.OpenIDConnectClientConfiguration{ - TokenEndpointAuthMethod: ClientAuthMethodClientSecretBasic, + TokenEndpointAuthMethod: ClientAuthMethodClientSecretPost, } client = NewClient(config) @@ -61,18 +62,51 @@ func TestNewClient(t *testing.T) { var niljwks *jose.JSONWebKeySet require.True(t, ok) - assert.Equal(t, "", fclient.UserinfoSigningAlgorithm) - assert.Equal(t, ClientAuthMethodClientSecretBasic, fclient.TokenEndpointAuthMethod) - assert.Equal(t, ClientAuthMethodClientSecretBasic, fclient.GetTokenEndpointAuthMethod()) - assert.Equal(t, SigningAlgNone, client.GetUserinfoSigningAlgorithm()) + + assert.Equal(t, "", fclient.UserinfoSigningAlg) + assert.Equal(t, SigningAlgNone, client.GetUserinfoSigningAlg()) + assert.Equal(t, SigningAlgNone, fclient.UserinfoSigningAlg) + + assert.Equal(t, "", fclient.IDTokenSigningAlg) + assert.Equal(t, SigningAlgRSAUsingSHA256, client.GetIDTokenSigningAlg()) + assert.Equal(t, SigningAlgRSAUsingSHA256, fclient.IDTokenSigningAlg) + + assert.Equal(t, ClientAuthMethodClientSecretPost, fclient.TokenEndpointAuthMethod) + assert.Equal(t, ClientAuthMethodClientSecretPost, fclient.GetTokenEndpointAuthMethod()) + assert.Equal(t, "", fclient.TokenEndpointAuthSigningAlgorithm) assert.Equal(t, SigningAlgRSAUsingSHA256, fclient.GetTokenEndpointAuthSigningAlgorithm()) + assert.Equal(t, SigningAlgRSAUsingSHA256, fclient.TokenEndpointAuthSigningAlgorithm) + assert.Equal(t, "", fclient.RequestObjectSigningAlgorithm) assert.Equal(t, "", fclient.GetRequestObjectSigningAlgorithm()) + + fclient.RequestObjectSigningAlgorithm = SigningAlgRSAUsingSHA256 + assert.Equal(t, SigningAlgRSAUsingSHA256, fclient.GetRequestObjectSigningAlgorithm()) + assert.Equal(t, "", fclient.JSONWebKeysURI) assert.Equal(t, "", fclient.GetJSONWebKeysURI()) + + fclient.JSONWebKeysURI = "https://example.com" + assert.Equal(t, "https://example.com", fclient.GetJSONWebKeysURI()) + assert.Equal(t, niljwks, fclient.JSONWebKeys) assert.Equal(t, niljwks, fclient.GetJSONWebKeys()) + + assert.Equal(t, ClientConsentMode(0), fclient.Consent.Mode) + assert.Equal(t, time.Second*0, fclient.Consent.Duration) + assert.Equal(t, ClientConsent{Mode: ClientConsentModeExplicit}, fclient.GetConsentPolicy()) + + fclient.TokenEndpointAuthMethod = "" + fclient.Public = false + assert.Equal(t, ClientAuthMethodClientSecretBasic, fclient.GetTokenEndpointAuthMethod()) + assert.Equal(t, ClientAuthMethodClientSecretBasic, fclient.TokenEndpointAuthMethod) + + fclient.TokenEndpointAuthMethod = "" + fclient.Public = true + assert.Equal(t, ClientAuthMethodNone, fclient.GetTokenEndpointAuthMethod()) + assert.Equal(t, ClientAuthMethodNone, fclient.TokenEndpointAuthMethod) + assert.Equal(t, []string(nil), fclient.RequestURIs) assert.Equal(t, []string(nil), fclient.GetRequestURIs()) } diff --git a/internal/oidc/const.go b/internal/oidc/const.go index 294ac4a1b..2143236af 100644 --- a/internal/oidc/const.go +++ b/internal/oidc/const.go @@ -134,10 +134,6 @@ const ( PKCEChallengeMethodSHA256 = "S256" ) -const ( - HeaderParameterAlgorithm = "alg" -) - const ( FormParameterClientID = "client_id" FormParameterClientSecret = "client_secret" @@ -170,6 +166,9 @@ const ( const ( // JWTHeaderKeyIdentifier is the JWT Header referencing the JWS Key Identifier used to sign a token. JWTHeaderKeyIdentifier = "kid" + + // JWTHeaderKeyAlgorithm is the JWT Header referencing the JWS Key algorithm used to sign a token. + JWTHeaderKeyAlgorithm = "alg" ) const ( diff --git a/internal/oidc/const_test.go b/internal/oidc/const_test.go index 0208df15f..73441b964 100644 --- a/internal/oidc/const_test.go +++ b/internal/oidc/const_test.go @@ -1,24 +1,29 @@ package oidc import ( + "crypto/ecdsa" + "crypto/ed25519" "crypto/rsa" "crypto/x509" - "encoding/pem" + "fmt" "net/url" + "os" + "strings" "github.com/authelia/authelia/v4/internal/configuration/schema" + "github.com/authelia/authelia/v4/internal/utils" ) const ( - myclient = "myclient" - myclientdesc = "My Client" - onefactor = "one_factor" - twofactor = "two_factor" - examplecom = "https://example.com" - examplecomsid = "example.com" - badsecret = "$plaintext$a_bad_secret" - badhmac = "asbdhaaskmdlkamdklasmdlkams" - exampleIssuerPrivateKey = "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEAvcMVMB2vEbqI6PlSNJ4HmUyMxBDJ5iY7FS+zDDAHOZBg9S3S\nKcAn1CZcnyL0VvJ7wcdhR6oTnOwR94eKvzUyJZ+GL2hTMm27dubEYsNdhoCl6N3X\nyEEohNfoxiiCYraVauX8X3M9jFzbEz9+pacaDbHB2syaJ1qFmMNR+HSu2jPzOo7M\nlqKIOgUzA0741MaYNt47AEVg4XU5ORLdolbAkItmYg1QbyFndg9H5IvwKkYaXTGE\nlgDBcPUC0yVjAC15Mguquq+jZeQay+6PSbHTD8PQMOkLjyChI2xEhVNbdCXe676R\ncMW2R/gjrcK23zmtmTWRfdC1iZLSlHO+bJj9vQIDAQABAoIBAEZvkP/JJOCJwqPn\nV3IcbmmilmV4bdi1vByDFgyiDyx4wOSA24+PubjvfFW9XcCgRPuKjDtTj/AhWBHv\nB7stfa2lZuNV7/u562mZArA+IAr62Zp0LdIxDV8x3T8gbjVB3HhPYbv0RJZDKTYd\nzV6jhfIrVu9mHpoY6ZnodhapCPYIyk/d49KBIHZuAc25CUjMXgTeaVtf0c996036\nUxW6ef33wAOJAvW0RCvbXAJfmBeEq2qQlkjTIlpYx71fhZWexHifi8Ouv3Zonc+1\n/P2Adq5uzYVBT92f9RKHg9QxxNzVrLjSMaxyvUtWQCAQfW0tFIRdqBGsHYsQrFtI\nF4yzv8ECgYEA7ntpyN9HD9Z9lYQzPCR73sFCLM+ID99aVij0wHuxK97bkSyyvkLd\n7MyTaym3lg1UEqWNWBCLvFULZx7F0Ah6qCzD4ymm3Bj/ADpWWPgljBI0AFml+HHs\nhcATmXUrj5QbLyhiP2gmJjajp1o/rgATx6ED66seSynD6JOH8wUhhZUCgYEAy7OA\n06PF8GfseNsTqlDjNF0K7lOqd21S0prdwrsJLiVzUlfMM25MLE0XLDUutCnRheeh\nIlcuDoBsVTxz6rkvFGD74N+pgXlN4CicsBq5ofK060PbqCQhSII3fmHobrZ9Cr75\nHmBjAxHx998SKaAAGbBbcYGUAp521i1pH5CEPYkCgYEAkUd1Zf0+2RMdZhwm6hh/\nrW+l1I6IoMK70YkZsLipccRNld7Y9LbfYwYtODcts6di9AkOVfueZJiaXbONZfIE\nZrb+jkAteh9wGL9xIrnohbABJcV3Kiaco84jInUSmGDtPokncOENfHIEuEpuSJ2b\nbx1TuhmAVuGWivR0+ULC7RECgYEAgS0cDRpWc9Xzh9Cl7+PLsXEvdWNpPsL9OsEq\n0Ep7z9+/+f/jZtoTRCS/BTHUpDvAuwHglT5j3p5iFMt5VuiIiovWLwynGYwrbnNS\nqfrIrYKUaH1n1oDS+oBZYLQGCe9/7EifAjxtjYzbvSyg//SPG7tSwfBCREbpZXj2\nqSWkNsECgYA/mCDzCTlrrWPuiepo6kTmN+4TnFA+hJI6NccDVQ+jvbqEdoJ4SW4L\nzqfZSZRFJMNpSgIqkQNRPJqMP0jQ5KRtJrjMWBnYxktwKz9fDg2R2MxdFgMF2LH2\nHEMMhFHlv8NDjVOXh1KwRoltNGVWYsSrD9wKU9GhRCEfmNCGrvBcEg==\n-----END RSA PRIVATE KEY-----" + pathCrypto = "../configuration/test_resources/crypto/%s.%s" + myclient = "myclient" + myclientdesc = "My Client" + onefactor = "one_factor" + twofactor = "two_factor" + examplecom = "https://example.com" + examplecomsid = "example.com" + badsecret = "$plaintext$a_bad_secret" + badhmac = "asbdhaaskmdlkamdklasmdlkams" ) func MustDecodeSecret(value string) *schema.PasswordDigest { @@ -37,20 +42,126 @@ func MustParseRequestURI(input string) *url.URL { } } -func MustParseRSAPrivateKey(data string) *rsa.PrivateKey { - block, _ := pem.Decode([]byte(data)) - if block == nil || block.Bytes == nil || len(block.Bytes) == 0 { - panic("not pem encoded") +func MustLoadCrypto(alg, mod, ext string, extra ...string) any { + fparts := []string{alg, mod} + if len(extra) != 0 { + fparts = append(fparts, extra...) } - if block.Type != "RSA PRIVATE KEY" { - panic("not private key") - } + var ( + data []byte + decoded any + err error + ) - key, err := x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { + if data, err = os.ReadFile(fmt.Sprintf(pathCrypto, strings.Join(fparts, "_"), ext)); err != nil { panic(err) } + if decoded, err = utils.ParseX509FromPEMRecursive(data); err != nil { + panic(err) + } + + return decoded +} + +func MustLoadCertificateChain(alg, op string) schema.X509CertificateChain { + decoded := MustLoadCrypto(alg, op, "crt") + + switch cert := decoded.(type) { + case *x509.Certificate: + return schema.NewX509CertificateChainFromCerts([]*x509.Certificate{cert}) + case []*x509.Certificate: + return schema.NewX509CertificateChainFromCerts(cert) + default: + panic(fmt.Errorf("the key was not a *x509.Certificate or []*x509.Certificate, it's a %T", cert)) + } +} + +func MustLoadCertificate(alg, op string) *x509.Certificate { + decoded := MustLoadCrypto(alg, op, "crt") + + cert, ok := decoded.(*x509.Certificate) + if !ok { + panic(fmt.Errorf("the key was not a *x509.Certificate, it's a %T", cert)) + } + + return cert +} + +func MustLoadEd15519PrivateKey(curve string, extra ...string) ed25519.PrivateKey { + decoded := MustLoadCrypto("ED25519", curve, "pem", extra...) + + key, ok := decoded.(ed25519.PrivateKey) + if !ok { + panic(fmt.Errorf("the key was not a ed25519.PrivateKey, it's a %T", key)) + } + return key } + +func MustLoadECDSAPrivateKey(curve string, extra ...string) *ecdsa.PrivateKey { + decoded := MustLoadCrypto("ECDSA", curve, "pem", extra...) + + key, ok := decoded.(*ecdsa.PrivateKey) + if !ok { + panic(fmt.Errorf("the key was not a *ecdsa.PrivateKey, it's a %T", key)) + } + + return key +} + +func MustLoadRSAPublicKey(bits string, extra ...string) *rsa.PublicKey { + decoded := MustLoadCrypto("RSA", bits, "pem", extra...) + + key, ok := decoded.(*rsa.PublicKey) + if !ok { + panic(fmt.Errorf("the key was not a *rsa.PublicKey, it's a %T", key)) + } + + return key +} + +func MustLoadRSAPrivateKey(bits string, extra ...string) *rsa.PrivateKey { + decoded := MustLoadCrypto("RSA", bits, "pem", extra...) + + key, ok := decoded.(*rsa.PrivateKey) + if !ok { + panic(fmt.Errorf("the key was not a *rsa.PrivateKey, it's a %T", key)) + } + + return key +} + +var ( + tOpenIDConnectPBKDF2ClientSecret, tOpenIDConnectPlainTextClientSecret *schema.PasswordDigest + + // Standard RSA key / certificate pairs. + keyRSA1024, keyRSA2048, keyRSA4096 *rsa.PrivateKey + certRSA1024, certRSA2048, certRSA4096 schema.X509CertificateChain + + // Standard ECDSA key / certificate pairs. + keyECDSAP224, keyECDSAP256, keyECDSAP384, keyECDSAP521 *ecdsa.PrivateKey + certECDSAP224, certECDSAP256, certECDSAP384, certECDSAP521 schema.X509CertificateChain +) + +func init() { + tOpenIDConnectPBKDF2ClientSecret = MustDecodeSecret("$pbkdf2-sha512$310000$c8p78n7pUMln0jzvd4aK4Q$JNRBzwAo0ek5qKn50cFzzvE9RXV88h1wJn5KGiHrD0YKtZaR/nCb2CJPOsKaPK0hjf.9yHxzQGZziziccp6Yng") + tOpenIDConnectPlainTextClientSecret = MustDecodeSecret("$plaintext$example") + + keyRSA1024 = MustLoadRSAPrivateKey("1024") + keyRSA2048 = MustLoadRSAPrivateKey("2048") + keyRSA4096 = MustLoadRSAPrivateKey("4096") + keyECDSAP224 = MustLoadECDSAPrivateKey("P224") + keyECDSAP256 = MustLoadECDSAPrivateKey("P256") + keyECDSAP384 = MustLoadECDSAPrivateKey("P384") + keyECDSAP521 = MustLoadECDSAPrivateKey("P521") + + certRSA1024 = MustLoadCertificateChain("RSA", "1024") + certRSA2048 = MustLoadCertificateChain("RSA", "2048") + certRSA4096 = MustLoadCertificateChain("RSA", "4096") + certECDSAP224 = MustLoadCertificateChain("ECDSA", "P224") + certECDSAP256 = MustLoadCertificateChain("ECDSA", "P256") + certECDSAP384 = MustLoadCertificateChain("ECDSA", "P384") + certECDSAP521 = MustLoadCertificateChain("ECDSA", "P521") +} diff --git a/internal/oidc/core_strategy_hmac_test.go b/internal/oidc/core_strategy_hmac_test.go index d390d4bdb..57dc6a038 100644 --- a/internal/oidc/core_strategy_hmac_test.go +++ b/internal/oidc/core_strategy_hmac_test.go @@ -1,13 +1,93 @@ package oidc import ( + "context" "fmt" + "regexp" "strings" "testing" + "time" + "github.com/ory/fosite" + "github.com/ory/fosite/token/hmac" "github.com/stretchr/testify/assert" ) +func TestHMACStrategy(t *testing.T) { + goodsecret := []byte("R7VCSUfnKc7Y5zE84q6GstYqfMGjL4wM") + secreta := []byte("a") + + config := &Config{ + TokenEntropy: 10, + GlobalSecret: secreta, + Lifespans: LifespanConfig{ + AccessToken: time.Hour, + RefreshToken: time.Hour, + AuthorizeCode: time.Minute, + }, + } + + strategy := &HMACCoreStrategy{ + Enigma: &hmac.HMACStrategy{Config: config}, + Config: config, + } + + var ( + token, signature string + err error + ) + + ctx := context.Background() + + token, signature, err = strategy.GenerateAuthorizeCode(ctx, &fosite.Request{}) + assert.EqualError(t, err, "secret for signing HMAC-SHA512/256 is expected to be 32 byte long, got 1 byte") + assert.Empty(t, token) + assert.Empty(t, signature) + + config.GlobalSecret = goodsecret + + token, signature, err = strategy.GenerateAuthorizeCode(ctx, &fosite.Request{}) + assert.NoError(t, err) + + assert.NotEmpty(t, token) + assert.NotEmpty(t, signature) + assert.Equal(t, signature, strategy.AuthorizeCodeSignature(ctx, token)) + assert.Regexp(t, regexp.MustCompile(`^authelia_ac_`), token) + + assert.NoError(t, strategy.ValidateAuthorizeCode(ctx, &fosite.Request{RequestedAt: time.Now(), Session: &fosite.DefaultSession{}}, token)) + assert.NoError(t, strategy.ValidateAuthorizeCode(ctx, &fosite.Request{RequestedAt: time.Now(), Session: &fosite.DefaultSession{}}, token)) + assert.EqualError(t, strategy.ValidateAuthorizeCode(ctx, &fosite.Request{RequestedAt: time.Now().Add(time.Hour * -2400), Session: &fosite.DefaultSession{}}, token), "invalid_token") + assert.NoError(t, strategy.ValidateAuthorizeCode(ctx, &fosite.Request{RequestedAt: time.Now().Add(time.Hour * -2400), Session: &fosite.DefaultSession{ExpiresAt: map[fosite.TokenType]time.Time{fosite.AuthorizeCode: time.Now().Add(100 * time.Hour)}}}, token)) + assert.EqualError(t, strategy.ValidateAuthorizeCode(ctx, &fosite.Request{RequestedAt: time.Now(), Session: &fosite.DefaultSession{ExpiresAt: map[fosite.TokenType]time.Time{fosite.AuthorizeCode: time.Now().Add(-100 * time.Second)}}}, token), "invalid_token") + + token, signature, err = strategy.GenerateRefreshToken(ctx, &fosite.Request{}) + assert.NoError(t, err) + + assert.NotEmpty(t, token) + assert.NotEmpty(t, signature) + assert.Equal(t, signature, strategy.RefreshTokenSignature(ctx, token)) + assert.Regexp(t, regexp.MustCompile(`^authelia_rt_`), token) + + assert.NoError(t, strategy.ValidateRefreshToken(ctx, &fosite.Request{RequestedAt: time.Now(), Session: &fosite.DefaultSession{}}, token)) + assert.NoError(t, strategy.ValidateRefreshToken(ctx, &fosite.Request{RequestedAt: time.Now(), Session: &fosite.DefaultSession{}}, token)) + assert.NoError(t, strategy.ValidateRefreshToken(ctx, &fosite.Request{RequestedAt: time.Now().Add(time.Hour * -2400), Session: &fosite.DefaultSession{ExpiresAt: map[fosite.TokenType]time.Time{fosite.RefreshToken: time.Now().Add(100 * time.Hour)}}}, token)) + assert.EqualError(t, strategy.ValidateRefreshToken(ctx, &fosite.Request{RequestedAt: time.Now(), Session: &fosite.DefaultSession{ExpiresAt: map[fosite.TokenType]time.Time{fosite.RefreshToken: time.Now().Add(-100 * time.Second)}}}, token), "invalid_token") + + token, signature, err = strategy.GenerateAccessToken(ctx, &fosite.Request{}) + assert.NoError(t, err) + + assert.NotEmpty(t, token) + assert.NotEmpty(t, signature) + assert.Equal(t, signature, strategy.AccessTokenSignature(ctx, token)) + assert.Regexp(t, regexp.MustCompile(`^authelia_at_`), token) + + assert.NoError(t, strategy.ValidateAccessToken(ctx, &fosite.Request{RequestedAt: time.Now(), Session: &fosite.DefaultSession{}}, token)) + assert.NoError(t, strategy.ValidateAccessToken(ctx, &fosite.Request{RequestedAt: time.Now(), Session: &fosite.DefaultSession{}}, token)) + assert.EqualError(t, strategy.ValidateAccessToken(ctx, &fosite.Request{RequestedAt: time.Now().Add(time.Hour * -2400), Session: &fosite.DefaultSession{}}, token), "invalid_token") + assert.NoError(t, strategy.ValidateAccessToken(ctx, &fosite.Request{RequestedAt: time.Now().Add(time.Hour * -2400), Session: &fosite.DefaultSession{ExpiresAt: map[fosite.TokenType]time.Time{fosite.AccessToken: time.Now().Add(100 * time.Hour)}}}, token)) + assert.EqualError(t, strategy.ValidateAccessToken(ctx, &fosite.Request{RequestedAt: time.Now(), Session: &fosite.DefaultSession{ExpiresAt: map[fosite.TokenType]time.Time{fosite.AccessToken: time.Now().Add(-100 * time.Second)}}}, token), "invalid_token") +} + func TestHMACCoreStrategy_TrimPrefix(t *testing.T) { testCases := []struct { name string diff --git a/internal/oidc/discovery.go b/internal/oidc/discovery.go index 7ce180fbf..5410e36ad 100644 --- a/internal/oidc/discovery.go +++ b/internal/oidc/discovery.go @@ -1,7 +1,10 @@ package oidc import ( + "sort" + "github.com/authelia/authelia/v4/internal/configuration/schema" + "github.com/authelia/authelia/v4/internal/utils" ) // NewOpenIDConnectWellKnownConfiguration generates a new OpenIDConnectWellKnownConfiguration. @@ -75,6 +78,21 @@ func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnectConfiguration CodeChallengeMethodsSupported: []string{ PKCEChallengeMethodSHA256, }, + RevocationEndpointAuthMethodsSupported: []string{ + ClientAuthMethodClientSecretBasic, + ClientAuthMethodClientSecretPost, + ClientAuthMethodClientSecretJWT, + ClientAuthMethodNone, + }, + RevocationEndpointAuthSigningAlgValuesSupported: []string{ + SigningAlgHMACUsingSHA256, + SigningAlgHMACUsingSHA384, + SigningAlgHMACUsingSHA512, + }, + IntrospectionEndpointAuthMethodsSupported: []string{ + ClientAuthMethodClientSecretBasic, + ClientAuthMethodNone, + }, }, OAuth2PushedAuthorizationDiscoveryOptions: &OAuth2PushedAuthorizationDiscoveryOptions{ RequirePushedAuthorizationRequests: c.PAR.Enforce, @@ -89,6 +107,10 @@ func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnectConfiguration SigningAlgNone, SigningAlgRSAUsingSHA256, }, + RequestObjectSigningAlgValuesSupported: []string{ + SigningAlgNone, + SigningAlgRSAUsingSHA256, + }, }, OpenIDConnectFrontChannelLogoutDiscoveryOptions: &OpenIDConnectFrontChannelLogoutDiscoveryOptions{}, OpenIDConnectBackChannelLogoutDiscoveryOptions: &OpenIDConnectBackChannelLogoutDiscoveryOptions{}, @@ -100,6 +122,26 @@ func NewOpenIDConnectWellKnownConfiguration(c *schema.OpenIDConnectConfiguration }, } + algs := make([]string, len(c.Discovery.RegisteredJWKSigningAlgs)) + + copy(algs, c.Discovery.RegisteredJWKSigningAlgs) + + for _, alg := range algs { + if !utils.IsStringInSlice(alg, config.IDTokenSigningAlgValuesSupported) { + config.IDTokenSigningAlgValuesSupported = append(config.IDTokenSigningAlgValuesSupported, alg) + } + + if !utils.IsStringInSlice(alg, config.UserinfoSigningAlgValuesSupported) { + config.UserinfoSigningAlgValuesSupported = append(config.UserinfoSigningAlgValuesSupported, alg) + } + } + + sort.Sort(SortedSigningAlgs(config.IDTokenSigningAlgValuesSupported)) + sort.Sort(SortedSigningAlgs(config.UserinfoSigningAlgValuesSupported)) + sort.Sort(SortedSigningAlgs(config.RequestObjectSigningAlgValuesSupported)) + sort.Sort(SortedSigningAlgs(config.RevocationEndpointAuthSigningAlgValuesSupported)) + sort.Sort(SortedSigningAlgs(config.TokenEndpointAuthSigningAlgValuesSupported)) + if c.EnablePKCEPlainChallenge { config.CodeChallengeMethodsSupported = append(config.CodeChallengeMethodsSupported, PKCEChallengeMethodPlain) } diff --git a/internal/oidc/discovery_test.go b/internal/oidc/discovery_test.go index 34ca1f732..b3d880f84 100644 --- a/internal/oidc/discovery_test.go +++ b/internal/oidc/discovery_test.go @@ -14,8 +14,9 @@ func TestNewOpenIDConnectWellKnownConfiguration(t *testing.T) { pkcePlainChallenge bool enforcePAR bool clients map[string]Client + discovery schema.OpenIDConnectDiscovery - expectCodeChallengeMethodsSupported, expectSubjectTypesSupported []string + expectCodeChallengeMethodsSupported, expectSubjectTypesSupported, expectedIDTokenSigAlgsSupported, expectedUserInfoSigAlgsSupported []string }{ { desc: "ShouldHaveChallengeMethodsS256ANDSubjectTypesSupportedPublic", @@ -23,6 +24,20 @@ func TestNewOpenIDConnectWellKnownConfiguration(t *testing.T) { clients: map[string]Client{"a": &BaseClient{}}, expectCodeChallengeMethodsSupported: []string{PKCEChallengeMethodSHA256}, expectSubjectTypesSupported: []string{SubjectTypePublic, SubjectTypePairwise}, + expectedIDTokenSigAlgsSupported: []string{SigningAlgRSAUsingSHA256}, + expectedUserInfoSigAlgsSupported: []string{SigningAlgRSAUsingSHA256, SigningAlgNone}, + }, + { + desc: "ShouldIncludDiscoveryInfo", + pkcePlainChallenge: false, + clients: map[string]Client{"a": &BaseClient{}}, + discovery: schema.OpenIDConnectDiscovery{ + RegisteredJWKSigningAlgs: []string{SigningAlgECDSAUsingP521AndSHA512}, + }, + expectCodeChallengeMethodsSupported: []string{PKCEChallengeMethodSHA256}, + expectSubjectTypesSupported: []string{SubjectTypePublic, SubjectTypePairwise}, + expectedIDTokenSigAlgsSupported: []string{SigningAlgRSAUsingSHA256, SigningAlgECDSAUsingP521AndSHA512}, + expectedUserInfoSigAlgsSupported: []string{SigningAlgRSAUsingSHA256, SigningAlgECDSAUsingP521AndSHA512, SigningAlgNone}, }, { desc: "ShouldHaveChallengeMethodsS256PlainANDSubjectTypesSupportedPublic", @@ -30,6 +45,8 @@ func TestNewOpenIDConnectWellKnownConfiguration(t *testing.T) { clients: map[string]Client{"a": &BaseClient{}}, expectCodeChallengeMethodsSupported: []string{PKCEChallengeMethodSHA256, PKCEChallengeMethodPlain}, expectSubjectTypesSupported: []string{SubjectTypePublic, SubjectTypePairwise}, + expectedIDTokenSigAlgsSupported: []string{SigningAlgRSAUsingSHA256}, + expectedUserInfoSigAlgsSupported: []string{SigningAlgRSAUsingSHA256, SigningAlgNone}, }, { desc: "ShouldHaveChallengeMethodsS256ANDSubjectTypesSupportedPublicPairwise", @@ -37,6 +54,8 @@ func TestNewOpenIDConnectWellKnownConfiguration(t *testing.T) { clients: map[string]Client{"a": &BaseClient{SectorIdentifier: "yes"}}, expectCodeChallengeMethodsSupported: []string{PKCEChallengeMethodSHA256}, expectSubjectTypesSupported: []string{SubjectTypePublic, SubjectTypePairwise}, + expectedIDTokenSigAlgsSupported: []string{SigningAlgRSAUsingSHA256}, + expectedUserInfoSigAlgsSupported: []string{SigningAlgRSAUsingSHA256, SigningAlgNone}, }, { desc: "ShouldHaveChallengeMethodsS256PlainANDSubjectTypesSupportedPublicPairwise", @@ -44,6 +63,8 @@ func TestNewOpenIDConnectWellKnownConfiguration(t *testing.T) { clients: map[string]Client{"a": &BaseClient{SectorIdentifier: "yes"}}, expectCodeChallengeMethodsSupported: []string{PKCEChallengeMethodSHA256, PKCEChallengeMethodPlain}, expectSubjectTypesSupported: []string{SubjectTypePublic, SubjectTypePairwise}, + expectedIDTokenSigAlgsSupported: []string{SigningAlgRSAUsingSHA256}, + expectedUserInfoSigAlgsSupported: []string{SigningAlgRSAUsingSHA256, SigningAlgNone}, }, { desc: "ShouldHaveTokenAuthMethodsNone", @@ -51,6 +72,8 @@ func TestNewOpenIDConnectWellKnownConfiguration(t *testing.T) { clients: map[string]Client{"a": &BaseClient{SectorIdentifier: "yes"}}, expectCodeChallengeMethodsSupported: []string{PKCEChallengeMethodSHA256, PKCEChallengeMethodPlain}, expectSubjectTypesSupported: []string{SubjectTypePublic, SubjectTypePairwise}, + expectedIDTokenSigAlgsSupported: []string{SigningAlgRSAUsingSHA256}, + expectedUserInfoSigAlgsSupported: []string{SigningAlgRSAUsingSHA256, SigningAlgNone}, }, { desc: "ShouldHaveTokenAuthMethodsNone", @@ -61,6 +84,20 @@ func TestNewOpenIDConnectWellKnownConfiguration(t *testing.T) { }, expectCodeChallengeMethodsSupported: []string{PKCEChallengeMethodSHA256, PKCEChallengeMethodPlain}, expectSubjectTypesSupported: []string{SubjectTypePublic, SubjectTypePairwise}, + expectedIDTokenSigAlgsSupported: []string{SigningAlgRSAUsingSHA256}, + expectedUserInfoSigAlgsSupported: []string{SigningAlgRSAUsingSHA256, SigningAlgNone}, + }, + { + desc: "ShouldHaveTokenAuthMethodsNone", + pkcePlainChallenge: true, + clients: map[string]Client{ + "a": &BaseClient{SectorIdentifier: "yes"}, + "b": &BaseClient{SectorIdentifier: "yes"}, + }, + expectCodeChallengeMethodsSupported: []string{PKCEChallengeMethodSHA256, PKCEChallengeMethodPlain}, + expectSubjectTypesSupported: []string{SubjectTypePublic, SubjectTypePairwise}, + expectedIDTokenSigAlgsSupported: []string{SigningAlgRSAUsingSHA256}, + expectedUserInfoSigAlgsSupported: []string{SigningAlgRSAUsingSHA256, SigningAlgNone}, }, } @@ -71,6 +108,7 @@ func TestNewOpenIDConnectWellKnownConfiguration(t *testing.T) { PAR: schema.OpenIDConnectPARConfiguration{ Enforce: tc.enforcePAR, }, + Discovery: tc.discovery, } actual := NewOpenIDConnectWellKnownConfiguration(&c) @@ -89,6 +127,9 @@ func TestNewOpenIDConnectWellKnownConfiguration(t *testing.T) { for _, subjectType := range actual.SubjectTypesSupported { assert.Contains(t, tc.expectSubjectTypesSupported, subjectType) } + + assert.Equal(t, tc.expectedUserInfoSigAlgsSupported, actual.UserinfoSigningAlgValuesSupported) + assert.Equal(t, tc.expectedIDTokenSigAlgsSupported, actual.IDTokenSigningAlgValuesSupported) }) } } diff --git a/internal/oidc/keys.go b/internal/oidc/keys.go index 3ca47d414..fa367fd41 100644 --- a/internal/oidc/keys.go +++ b/internal/oidc/keys.go @@ -3,181 +3,398 @@ package oidc import ( "context" "crypto" + "crypto/ecdsa" "crypto/rsa" "errors" "fmt" + "sort" "strings" - "github.com/ory/fosite/token/jwt" + "github.com/golang-jwt/jwt/v4" + fjwt "github.com/ory/fosite/token/jwt" + "github.com/ory/x/errorsx" + "gopkg.in/square/go-jose.v2" "github.com/authelia/authelia/v4/internal/configuration/schema" ) -// NewKeyManagerWithConfiguration when provided a schema.OpenIDConnectConfiguration creates a new KeyManager and adds an -// initial key to the manager. -func NewKeyManagerWithConfiguration(config *schema.OpenIDConnectConfiguration) (manager *KeyManager, err error) { - manager = NewKeyManager() +// NewKeyManager news up a KeyManager. +func NewKeyManager(config *schema.OpenIDConnectConfiguration) (manager *KeyManager) { + manager = &KeyManager{ + kids: map[string]*JWK{}, + algs: map[string]*JWK{}, + } - if _, err = manager.AddActiveJWK(config.IssuerCertificateChain, config.IssuerPrivateKey); err != nil { + for _, sjwk := range config.IssuerJWKS { + jwk := NewJWK(sjwk) + + manager.kids[sjwk.KeyID] = jwk + manager.algs[jwk.alg.Alg()] = jwk + + if jwk.kid == config.Discovery.DefaultKeyID { + manager.kid = jwk.kid + } + } + + return manager +} + +// The KeyManager type handles JWKs and signing operations. +type KeyManager struct { + kid string + kids map[string]*JWK + algs map[string]*JWK +} + +func (m *KeyManager) GetKIDFromAlgStrict(ctx context.Context, alg string) (kid string, err error) { + if jwks, ok := m.algs[alg]; ok { + return jwks.kid, nil + } + + return "", fmt.Errorf("alg not found") +} + +func (m *KeyManager) GetKIDFromAlg(ctx context.Context, alg string) string { + if jwks, ok := m.algs[alg]; ok { + return jwks.kid + } + + return m.kid +} + +func (m *KeyManager) GetByAlg(ctx context.Context, alg string) *JWK { + if jwk, ok := m.algs[alg]; ok { + return jwk + } + + return nil +} + +func (m *KeyManager) GetByKID(ctx context.Context, kid string) *JWK { + if kid == "" { + return m.kids[m.kid] + } + + if jwk, ok := m.kids[kid]; ok { + return jwk + } + + return nil +} + +func (m *KeyManager) GetByHeader(ctx context.Context, header fjwt.Mapper) (jwk *JWK, err error) { + var ( + kid string + ok bool + ) + + if header == nil { + return nil, fmt.Errorf("jwt header was nil") + } + + if kid, ok = header.Get(JWTHeaderKeyIdentifier).(string); !ok { + return nil, fmt.Errorf("jwt header did not have a kid") + } + + if jwk, ok = m.kids[kid]; !ok { + return nil, fmt.Errorf("jwt header '%s' with value '%s' does not match a managed jwk", JWTHeaderKeyIdentifier, kid) + } + + return jwk, nil +} + +func (m *KeyManager) GetByTokenString(ctx context.Context, tokenString string) (jwk *JWK, err error) { + var ( + token *jwt.Token + ) + + if token, _, err = jwt.NewParser().ParseUnverified(tokenString, jwt.MapClaims{}); err != nil { return nil, err } - return manager, nil + return m.GetByHeader(ctx, &fjwt.Headers{Extra: token.Header}) } -// NewKeyManager creates a new empty KeyManager. -func NewKeyManager() (manager *KeyManager) { - return &KeyManager{ - jwks: &jose.JSONWebKeySet{}, +func (m *KeyManager) Set(ctx context.Context) *jose.JSONWebKeySet { + keys := make([]jose.JSONWebKey, 0, len(m.kids)) + + for _, jwk := range m.kids { + keys = append(keys, jwk.JWK()) + } + + sort.Sort(SortedJSONWebKey(keys)) + + return &jose.JSONWebKeySet{ + Keys: keys, } } -// Strategy returns the fosite jwt.JWTStrategy. -func (m *KeyManager) Strategy() (strategy jwt.Signer) { - if m.jwk == nil { - return nil +func (m *KeyManager) Generate(ctx context.Context, claims fjwt.MapClaims, header fjwt.Mapper) (tokenString string, sig string, err error) { + var jwk *JWK + + if jwk, err = m.GetByHeader(ctx, header); err != nil { + return "", "", fmt.Errorf("error getting jwk from header: %w", err) } - return m.jwk.Strategy() + return jwk.Strategy().Generate(ctx, claims, header) } -// GetKeySet returns the joseJSONWebKeySet containing the rsa.PublicKey types. -func (m *KeyManager) GetKeySet() (jwks *jose.JSONWebKeySet) { - return m.jwks +func (m *KeyManager) Validate(ctx context.Context, tokenString string) (sig string, err error) { + var jwk *JWK + + if jwk, err = m.GetByTokenString(ctx, tokenString); err != nil { + return "", fmt.Errorf("error getting jwk from token string: %w", err) + } + + return jwk.Strategy().Validate(ctx, tokenString) } -// GetActiveJWK obtains the currently active jose.JSONWebKey. -func (m *KeyManager) GetActiveJWK() (jwk *jose.JSONWebKey, err error) { - if m.jwks == nil || m.jwk == nil { - return nil, errors.New("could not obtain the active JWK from an improperly configured key manager") - } - - jwks := m.jwks.Key(m.jwk.id) - - if len(jwks) == 1 { - return &jwks[0], nil - } - - if len(jwks) == 0 { - return nil, errors.New("could not find a key with the active key id") - } - - return nil, errors.New("multiple keys with the same key id") +func (m *KeyManager) Hash(ctx context.Context, in []byte) (sum []byte, err error) { + return m.GetByKID(ctx, "").Strategy().Hash(ctx, in) } -// GetActiveKeyID returns the key id of the currently active key. -func (m *KeyManager) GetActiveKeyID() (keyID string) { - if m.jwk == nil { - return "" +func (m *KeyManager) Decode(ctx context.Context, tokenString string) (token *fjwt.Token, err error) { + var jwk *JWK + + if jwk, err = m.GetByTokenString(ctx, tokenString); err != nil { + return nil, fmt.Errorf("error getting jwk from token string: %w", err) } - return m.jwk.id + return jwk.Strategy().Decode(ctx, tokenString) } -// GetActivePrivateKey returns the rsa.PrivateKey of the currently active key. -func (m *KeyManager) GetActivePrivateKey() (key *rsa.PrivateKey, err error) { - if m.jwk == nil { - return nil, errors.New("failed to retrieve active private key") +func (m *KeyManager) GetSignature(ctx context.Context, tokenString string) (sig string, err error) { + return getTokenSignature(tokenString) +} + +func (m *KeyManager) GetSigningMethodLength(ctx context.Context) (size int) { + return m.GetByKID(ctx, "").Strategy().GetSigningMethodLength(ctx) +} + +func NewJWK(s schema.JWK) (jwk *JWK) { + jwk = &JWK{ + kid: s.KeyID, + use: s.Use, + alg: jwt.GetSigningMethod(s.Algorithm), + key: s.Key.(schema.CryptographicPrivateKey), + + chain: s.CertificateChain, + thumbprint: s.CertificateChain.Thumbprint(crypto.SHA256), + thumbprintsha1: s.CertificateChain.Thumbprint(crypto.SHA1), } - return m.jwk.key, nil -} - -// AddActiveJWK is used to add a cert and key pair. -func (m *KeyManager) AddActiveJWK(chain schema.X509CertificateChain, key *rsa.PrivateKey) (jwk *JWK, err error) { - // TODO: Add a mutex when implementing key rotation to be utilized here and in methods which retrieve the JWK or JWKS. - if m.jwk, err = NewJWK(chain, key); err != nil { - return nil, err - } - - m.jwks.Keys = append(m.jwks.Keys, *m.jwk.JSONWebKey()) - - return m.jwk, nil -} - -// JWTStrategy is a decorator struct for the fosite jwt.JWTStrategy. -type JWTStrategy struct { - jwt.Signer - - id string -} - -// KeyID returns the key id. -func (s *JWTStrategy) KeyID() (id string) { - return s.id -} - -// GetPublicKeyID is a decorator func for the underlying fosite RS256JWTStrategy. -func (s *JWTStrategy) GetPublicKeyID(_ context.Context) (string, error) { - return s.id, nil -} - -// NewJWK creates a new JWK. -func NewJWK(chain schema.X509CertificateChain, key *rsa.PrivateKey) (j *JWK, err error) { - if key == nil { - return nil, fmt.Errorf("JWK is not properly initialized: missing key") - } - - j = &JWK{ - key: key, - chain: chain, - } - - jwk := &jose.JSONWebKey{ - Algorithm: SigningAlgRSAUsingSHA256, - Use: KeyUseSignature, - Key: &key.PublicKey, - } - - var thumbprint []byte - - if thumbprint, err = jwk.Thumbprint(crypto.SHA1); err != nil { - return nil, fmt.Errorf("failed to calculate SHA1 thumbprint for certificate: %w", err) - } - - j.id = strings.ToLower(fmt.Sprintf("%x", thumbprint)) - - if len(j.id) >= 7 { - j.id = j.id[:6] - } - - if len(j.id) >= 7 { - j.id = j.id[:6] - } - - return j, nil -} - -// JWK is a utility wrapper for JSON Web Key's. -type JWK struct { - id string - key *rsa.PrivateKey - chain schema.X509CertificateChain -} - -// Strategy returns the relevant jwt.JWTStrategy for this JWT. -func (j *JWK) Strategy() (strategy jwt.Signer) { - return &JWTStrategy{id: j.id, Signer: &jwt.DefaultSigner{GetPrivateKey: j.GetPrivateKey}} -} - -func (j *JWK) GetPrivateKey(ctx context.Context) (key any, err error) { - return j.key, nil -} - -// JSONWebKey returns the relevant *jose.JSONWebKey for this JWT. -func (j *JWK) JSONWebKey() (jwk *jose.JSONWebKey) { - jwk = &jose.JSONWebKey{ - Key: &j.key.PublicKey, - KeyID: j.id, - Algorithm: SigningAlgRSAUsingSHA256, - Use: KeyUseSignature, - Certificates: j.chain.Certificates(), - } - - if len(jwk.Certificates) != 0 { - jwk.CertificateThumbprintSHA1, jwk.CertificateThumbprintSHA256 = j.chain.Thumbprint(crypto.SHA1), j.chain.Thumbprint(crypto.SHA256) + switch jwk.alg { + case jwt.SigningMethodRS256, jwt.SigningMethodPS256, jwt.SigningMethodES256: + jwk.hash = crypto.SHA256 + case jwt.SigningMethodRS384, jwt.SigningMethodPS384, jwt.SigningMethodES384: + jwk.hash = crypto.SHA384 + case jwt.SigningMethodRS512, jwt.SigningMethodPS512, jwt.SigningMethodES512: + jwk.hash = crypto.SHA512 + default: + jwk.hash = crypto.SHA256 } return jwk } + +type JWK struct { + kid string + use string + alg jwt.SigningMethod + hash crypto.Hash + + key schema.CryptographicPrivateKey + chain schema.X509CertificateChain + thumbprintsha1 []byte + thumbprint []byte +} + +func (j *JWK) GetPrivateKey(ctx context.Context) (any, error) { + return j.PrivateJWK(), nil +} + +func (j *JWK) KeyID() string { + return j.kid +} + +func (j *JWK) PrivateJWK() (jwk *jose.JSONWebKey) { + return &jose.JSONWebKey{ + Key: j.key, + KeyID: j.kid, + Algorithm: j.alg.Alg(), + Use: j.use, + Certificates: j.chain.Certificates(), + CertificateThumbprintSHA1: j.thumbprintsha1, + CertificateThumbprintSHA256: j.thumbprint, + } +} + +func (j *JWK) JWK() (jwk jose.JSONWebKey) { + return j.PrivateJWK().Public() +} + +func (j *JWK) Strategy() (strategy fjwt.Signer) { + return &Signer{ + hash: j.hash, + alg: j.alg, + GetPrivateKey: j.GetPrivateKey, + } +} + +// Signer is responsible for generating and validating JWT challenges. +type Signer struct { + hash crypto.Hash + alg jwt.SigningMethod + + GetPrivateKey fjwt.GetPrivateKeyFunc +} + +func (j *Signer) GetPublicKey(ctx context.Context) (key crypto.PublicKey, err error) { + var k any + + if k, err = j.GetPrivateKey(ctx); err != nil { + return nil, err + } + + switch t := k.(type) { + case *jose.JSONWebKey: + return t.Public().Key, nil + case jose.OpaqueSigner: + return t.Public().Key, nil + case schema.CryptographicPrivateKey: + return t.Public(), nil + default: + return nil, errors.New("invalid private key type") + } +} + +// Generate generates a new authorize code or returns an error. set secret. +func (j *Signer) Generate(ctx context.Context, claims fjwt.MapClaims, header fjwt.Mapper) (tokenString string, sig string, err error) { + var key any + + if key, err = j.GetPrivateKey(ctx); err != nil { + return "", "", err + } + + switch t := key.(type) { + case *jose.JSONWebKey: + return generateToken(claims, header, j.alg, t.Key) + case jose.JSONWebKey: + return generateToken(claims, header, j.alg, t.Key) + case *rsa.PrivateKey, *ecdsa.PrivateKey: + return generateToken(claims, header, j.alg, t) + case jose.OpaqueSigner: + switch tt := t.Public().Key.(type) { + case *rsa.PrivateKey, *ecdsa.PrivateKey: + return generateToken(claims, header, j.alg, t) + default: + return "", "", fmt.Errorf("unsupported private / public key pairs: %T, %T", t, tt) + } + default: + return "", "", fmt.Errorf("unsupported private key type: %T", t) + } +} + +// Validate validates a token and returns its signature or an error if the token is not valid. +func (j *Signer) Validate(ctx context.Context, tokenString string) (sig string, err error) { + var ( + key crypto.PublicKey + ) + + if key, err = j.GetPublicKey(ctx); err != nil { + return "", err + } + + return validateToken(tokenString, key) +} + +// Decode will decode a JWT token. +func (j *Signer) Decode(ctx context.Context, tokenString string) (token *fjwt.Token, err error) { + var ( + key crypto.PublicKey + ) + + if key, err = j.GetPublicKey(ctx); err != nil { + return nil, err + } + + return decodeToken(tokenString, key) +} + +// GetSignature will return the signature of a token. +func (j *Signer) GetSignature(ctx context.Context, tokenString string) (sig string, err error) { + return getTokenSignature(tokenString) +} + +// Hash will return a given hash based on the byte input or an error upon fail. +func (j *Signer) Hash(ctx context.Context, in []byte) (sum []byte, err error) { + hash := j.hash.New() + + if _, err = hash.Write(in); err != nil { + return []byte{}, errorsx.WithStack(err) + } + + return hash.Sum([]byte{}), nil +} + +// GetSigningMethodLength will return the length of the signing method. +func (j *Signer) GetSigningMethodLength(ctx context.Context) (size int) { + return j.hash.Size() +} + +func generateToken(claims fjwt.MapClaims, header fjwt.Mapper, signingMethod jwt.SigningMethod, key any) (rawToken string, sig string, err error) { + if header == nil || claims == nil { + return "", "", errors.New("either claims or header is nil") + } + + token := jwt.NewWithClaims(signingMethod, claims) + + token.Header = assign(token.Header, header.ToMap()) + + if rawToken, err = token.SignedString(key); err != nil { + return "", "", err + } + + if sig, err = getTokenSignature(rawToken); err != nil { + return "", "", err + } + + return rawToken, sig, nil +} + +func decodeToken(tokenString string, key any) (token *fjwt.Token, err error) { + return fjwt.ParseWithClaims(tokenString, fjwt.MapClaims{}, func(*fjwt.Token) (any, error) { + return key, nil + }) +} + +func validateToken(tokenString string, key any) (sig string, err error) { + if _, err = decodeToken(tokenString, key); err != nil { + return "", err + } + + return getTokenSignature(tokenString) +} + +func getTokenSignature(tokenString string) (sig string, err error) { + parts := strings.Split(tokenString, ".") + + if len(parts) != 3 { + return "", errors.New("header, body and signature must all be set") + } + + return parts[2], nil +} + +func assign(a, b map[string]any) map[string]any { + for k, w := range b { + if _, ok := a[k]; ok { + continue + } + + a[k] = w + } + + return a +} diff --git a/internal/oidc/keys_test.go b/internal/oidc/keys_test.go index 0405227b3..70c86e418 100644 --- a/internal/oidc/keys_test.go +++ b/internal/oidc/keys_test.go @@ -1,48 +1,450 @@ package oidc import ( - "crypto" + "context" + "encoding/json" "fmt" - "strings" "testing" + fjwt "github.com/ory/fosite/token/jwt" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gopkg.in/square/go-jose.v2" "github.com/authelia/authelia/v4/internal/configuration/schema" ) -func TestKeyManager_AddActiveJWK(t *testing.T) { - manager := NewKeyManager() - assert.Nil(t, manager.jwk) - assert.Nil(t, manager.Strategy()) +func TestKeyManager(t *testing.T) { + config := &schema.OpenIDConnectConfiguration{ + Discovery: schema.OpenIDConnectDiscovery{ + DefaultKeyID: "kid-RS256-sig", + }, + IssuerJWKS: []schema.JWK{ + { + Use: KeyUseSignature, + Algorithm: SigningAlgRSAUsingSHA256, + Key: keyRSA2048, + CertificateChain: certRSA2048, + }, + { + Use: KeyUseSignature, + Algorithm: SigningAlgRSAUsingSHA384, + Key: keyRSA2048, + CertificateChain: certRSA2048, + }, + { + Use: KeyUseSignature, + Algorithm: SigningAlgRSAUsingSHA512, + Key: keyRSA4096, + CertificateChain: certRSA4096, + }, + { + Use: KeyUseSignature, + Algorithm: SigningAlgRSAPSSUsingSHA256, + Key: keyRSA2048, + CertificateChain: certRSA2048, + }, + { + Use: KeyUseSignature, + Algorithm: SigningAlgRSAPSSUsingSHA384, + Key: keyRSA2048, + CertificateChain: certRSA2048, + }, + { + Use: KeyUseSignature, + Algorithm: SigningAlgRSAPSSUsingSHA512, + Key: keyRSA4096, + CertificateChain: certRSA4096, + }, + { + Use: KeyUseSignature, + Algorithm: SigningAlgECDSAUsingP256AndSHA256, + Key: keyECDSAP256, + CertificateChain: certECDSAP256, + }, + { + Use: KeyUseSignature, + Algorithm: SigningAlgECDSAUsingP384AndSHA384, + Key: keyECDSAP384, + CertificateChain: certECDSAP384, + }, + { + Use: KeyUseSignature, + Algorithm: SigningAlgECDSAUsingP521AndSHA512, + Key: keyECDSAP521, + CertificateChain: certECDSAP521, + }, + }, + } - j, err := manager.AddActiveJWK(schema.X509CertificateChain{}, MustParseRSAPrivateKey(exampleIssuerPrivateKey)) - require.NoError(t, err) - require.NotNil(t, j) - require.NotNil(t, manager.jwk) - require.NotNil(t, manager.Strategy()) + for i, key := range config.IssuerJWKS { + config.IssuerJWKS[i].KeyID = fmt.Sprintf("kid-%s-%s", key.Algorithm, key.Use) + } - thumbprint, err := j.JSONWebKey().Thumbprint(crypto.SHA1) + manager := NewKeyManager(config) + + assert.NotNil(t, manager) + + assert.Len(t, manager.kids, len(config.IssuerJWKS)) + assert.Len(t, manager.algs, len(config.IssuerJWKS)) + + assert.Equal(t, "kid-RS256-sig", manager.kid) + + ctx := context.Background() + + var ( + jwk *JWK + err error + ) + + jwk = manager.GetByAlg(ctx, "notalg") + assert.Nil(t, jwk) + + jwk = manager.GetByKID(ctx, "notalg") + assert.Nil(t, jwk) + + jwk = manager.GetByKID(ctx, "") + assert.NotNil(t, jwk) + assert.Equal(t, config.Discovery.DefaultKeyID, jwk.KeyID()) + + jwk, err = manager.GetByHeader(ctx, &fjwt.Headers{Extra: map[string]any{JWTHeaderKeyIdentifier: "notalg"}}) + assert.EqualError(t, err, "jwt header 'kid' with value 'notalg' does not match a managed jwk") + assert.Nil(t, jwk) + + jwk, err = manager.GetByHeader(ctx, &fjwt.Headers{Extra: map[string]any{}}) + assert.EqualError(t, err, "jwt header did not have a kid") + assert.Nil(t, jwk) + + jwk, err = manager.GetByHeader(ctx, nil) + assert.EqualError(t, err, "jwt header was nil") + assert.Nil(t, jwk) + + kid, err := manager.GetKIDFromAlgStrict(ctx, "notalg") + assert.EqualError(t, err, "alg not found") + assert.Equal(t, "", kid) + + kid = manager.GetKIDFromAlg(ctx, "notalg") + assert.Equal(t, config.Discovery.DefaultKeyID, kid) + + set := manager.Set(ctx) + + assert.NotNil(t, set) + assert.Len(t, set.Keys, len(config.IssuerJWKS)) + + data, err := json.Marshal(&set) assert.NoError(t, err) + assert.NotNil(t, data) - kid := strings.ToLower(fmt.Sprintf("%x", thumbprint)[:6]) - assert.Equal(t, manager.jwk.id, kid) - assert.Equal(t, kid, j.JSONWebKey().KeyID) - assert.Len(t, manager.jwks.Keys, 1) + out := jose.JSONWebKeySet{} + assert.NoError(t, json.Unmarshal(data, &out)) + assert.Equal(t, *set, out) - keys := manager.jwks.Key(kid) - assert.Equal(t, keys[0].KeyID, kid) + for _, alg := range []string{SigningAlgRSAUsingSHA256, SigningAlgRSAUsingSHA384, SigningAlgRSAPSSUsingSHA512, SigningAlgRSAPSSUsingSHA256, SigningAlgRSAPSSUsingSHA384, SigningAlgRSAPSSUsingSHA512, SigningAlgECDSAUsingP256AndSHA256, SigningAlgECDSAUsingP384AndSHA384, SigningAlgECDSAUsingP521AndSHA512} { + t.Run(alg, func(t *testing.T) { + expectedKID := fmt.Sprintf("kid-%s-%s", alg, KeyUseSignature) - privKey, err := manager.GetActivePrivateKey() - assert.NoError(t, err) - assert.NotNil(t, privKey) + t.Run("ShouldGetCorrectKey", func(t *testing.T) { + jwk = manager.GetByKID(ctx, expectedKID) + assert.NotNil(t, jwk) + assert.Equal(t, expectedKID, jwk.KeyID()) - webKey, err := manager.GetActiveJWK() - assert.NoError(t, err) - assert.NotNil(t, webKey) + jwk = manager.GetByAlg(ctx, alg) + assert.NotNil(t, jwk) - keySet := manager.GetKeySet() - assert.NotNil(t, keySet) - assert.Equal(t, kid, manager.GetActiveKeyID()) + assert.Equal(t, alg, jwk.alg.Alg()) + assert.Equal(t, expectedKID, jwk.KeyID()) + + kid, err = manager.GetKIDFromAlgStrict(ctx, alg) + assert.NoError(t, err) + assert.Equal(t, expectedKID, kid) + + kid = manager.GetKIDFromAlg(ctx, alg) + assert.Equal(t, expectedKID, kid) + + jwk, err = manager.GetByHeader(ctx, &fjwt.Headers{Extra: map[string]any{JWTHeaderKeyIdentifier: expectedKID}}) + assert.NoError(t, err) + assert.NotNil(t, jwk) + + assert.Equal(t, expectedKID, jwk.KeyID()) + }) + + t.Run("ShouldUseCorrectSigner", func(t *testing.T) { + var tokenString, sig, sigb string + var token *fjwt.Token + + tokenString, sig, err = manager.Generate(ctx, fjwt.MapClaims{}, &fjwt.Headers{Extra: map[string]any{JWTHeaderKeyIdentifier: expectedKID}}) + assert.NoError(t, err) + + sigb, err = manager.GetSignature(ctx, tokenString) + assert.NoError(t, err) + assert.Equal(t, sig, sigb) + + sigb, err = manager.Validate(ctx, tokenString) + assert.NoError(t, err) + assert.Equal(t, sig, sigb) + + token, err = manager.Decode(ctx, tokenString) + assert.NoError(t, err) + assert.Equal(t, expectedKID, token.Header[JWTHeaderKeyIdentifier]) + + jwk, err = manager.GetByTokenString(ctx, tokenString) + + assert.NoError(t, err) + + sigb, err = jwk.Strategy().Validate(ctx, tokenString) + assert.NoError(t, err) + assert.Equal(t, sig, sigb) + }) + }) + } +} + +func TestJWKFunctionality(t *testing.T) { + testCases := []struct { + have schema.JWK + }{ + { + schema.JWK{ + KeyID: "rsa2048-rs256", + Use: KeyUseSignature, + Algorithm: SigningAlgRSAUsingSHA256, + Key: keyRSA2048, + CertificateChain: certRSA2048, + }, + }, + { + schema.JWK{ + KeyID: "rsa2048-rs384", + Use: KeyUseSignature, + Algorithm: SigningAlgRSAUsingSHA384, + Key: keyRSA2048, + CertificateChain: certRSA2048, + }, + }, + { + schema.JWK{ + KeyID: "rsa2048-rs512", + Use: KeyUseSignature, + Algorithm: SigningAlgRSAUsingSHA512, + Key: keyRSA2048, + CertificateChain: certRSA2048, + }, + }, + { + schema.JWK{ + KeyID: "rsa4096-rs256", + Use: KeyUseSignature, + Algorithm: SigningAlgRSAUsingSHA256, + Key: keyRSA4096, + CertificateChain: certRSA4096, + }, + }, + { + schema.JWK{ + KeyID: "rsa4096-rs384", + Use: KeyUseSignature, + Algorithm: SigningAlgRSAUsingSHA384, + Key: keyRSA4096, + CertificateChain: certRSA4096, + }, + }, + { + schema.JWK{ + KeyID: "rsa4096-rs512", + Use: KeyUseSignature, + Algorithm: SigningAlgRSAUsingSHA512, + Key: keyRSA4096, + CertificateChain: certRSA4096, + }, + }, + { + schema.JWK{ + KeyID: "rsa2048-rs256", + Use: KeyUseSignature, + Algorithm: SigningAlgRSAPSSUsingSHA256, + Key: keyRSA2048, + CertificateChain: certRSA2048, + }, + }, + { + schema.JWK{ + KeyID: "rsa2048-ps384", + Use: KeyUseSignature, + Algorithm: SigningAlgRSAPSSUsingSHA384, + Key: keyRSA2048, + CertificateChain: certRSA2048, + }, + }, + { + schema.JWK{ + KeyID: "rsa2048-ps512", + Use: KeyUseSignature, + Algorithm: SigningAlgRSAPSSUsingSHA512, + Key: keyRSA2048, + CertificateChain: certRSA2048, + }, + }, + { + schema.JWK{ + KeyID: "rsa4096-ps256", + Use: KeyUseSignature, + Algorithm: SigningAlgRSAPSSUsingSHA256, + Key: keyRSA4096, + CertificateChain: certRSA4096, + }, + }, + { + schema.JWK{ + KeyID: "rsa4096-ps384", + Use: KeyUseSignature, + Algorithm: SigningAlgRSAPSSUsingSHA384, + Key: keyRSA4096, + CertificateChain: certRSA4096, + }, + }, + { + schema.JWK{ + KeyID: "rsa4096-ps512", + Use: KeyUseSignature, + Algorithm: SigningAlgRSAPSSUsingSHA512, + Key: keyRSA4096, + CertificateChain: certRSA4096, + }, + }, + { + schema.JWK{ + KeyID: "ecdsaP256", + Use: KeyUseSignature, + Algorithm: SigningAlgECDSAUsingP256AndSHA256, + Key: keyECDSAP256, + CertificateChain: certECDSAP256, + }, + }, + { + schema.JWK{ + KeyID: "ecdsaP384", + Use: KeyUseSignature, + Algorithm: SigningAlgECDSAUsingP384AndSHA384, + Key: keyECDSAP384, + CertificateChain: certECDSAP384, + }, + }, + { + schema.JWK{ + KeyID: "ecdsaP521", + Use: KeyUseSignature, + Algorithm: SigningAlgECDSAUsingP521AndSHA512, + Key: keyECDSAP521, + CertificateChain: certECDSAP521, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.have.KeyID, func(t *testing.T) { + t.Run("Generating", func(t *testing.T) { + var ( + jwk *JWK + ) + + ctx := context.Background() + + jwk = NewJWK(tc.have) + + signer := jwk.Strategy() + + claims := fjwt.MapClaims{} + header := &fjwt.Headers{ + Extra: map[string]any{ + "kid": jwk.kid, + }, + } + + tokenString, sig, err := signer.Generate(ctx, claims, header) + + assert.NoError(t, err) + assert.NotEqual(t, "", tokenString) + assert.NotEqual(t, "", sig) + + sigd, err := signer.GetSignature(ctx, tokenString) + assert.NoError(t, err) + assert.Equal(t, sig, sigd) + + token, err := signer.Decode(ctx, tokenString) + assert.NoError(t, err) + assert.NotNil(t, token) + fmt.Println(tokenString) + + assert.True(t, token.Valid()) + assert.Equal(t, jwk.alg.Alg(), string(token.Method)) + + sigv, err := signer.Validate(ctx, tokenString) + assert.NoError(t, err) + assert.Equal(t, sig, sigv) + }) + + t.Run("Marshalling", func(t *testing.T) { + var ( + jwk *JWK + out jose.JSONWebKey + data []byte + err error + ) + + jwk = NewJWK(tc.have) + + strategy := jwk.Strategy() + + assert.NotNil(t, strategy) + + signer, ok := strategy.(*Signer) + + require.True(t, ok) + + assert.NotNil(t, signer) + + key, err := signer.GetPublicKey(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, key) + + key, err = jwk.GetPrivateKey(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, key) + + data, err = json.Marshal(jwk.JWK()) + + assert.NoError(t, err) + require.NotNil(t, data) + + assert.NoError(t, json.Unmarshal(data, &out)) + + assert.True(t, out.IsPublic()) + assert.Equal(t, tc.have.KeyID, out.KeyID) + assert.Equal(t, tc.have.KeyID, jwk.KeyID()) + assert.Equal(t, tc.have.Use, out.Use) + assert.Equal(t, tc.have.Algorithm, out.Algorithm) + assert.NotNil(t, out.Key) + assert.NotNil(t, out.Certificates) + assert.NotNil(t, out.CertificateThumbprintSHA1) + assert.NotNil(t, out.CertificateThumbprintSHA256) + assert.True(t, out.Valid()) + + data, err = json.Marshal(jwk.PrivateJWK()) + + assert.NoError(t, err) + require.NotNil(t, data) + assert.NoError(t, json.Unmarshal(data, &out)) + + assert.False(t, out.IsPublic()) + assert.Equal(t, tc.have.KeyID, out.KeyID) + assert.Equal(t, tc.have.Use, out.Use) + assert.Equal(t, tc.have.Algorithm, out.Algorithm) + assert.NotNil(t, out.Key) + assert.NotNil(t, out.Certificates) + assert.NotNil(t, out.CertificateThumbprintSHA1) + assert.NotNil(t, out.CertificateThumbprintSHA256) + assert.True(t, out.Valid()) + }) + }) + } } diff --git a/internal/oidc/provider.go b/internal/oidc/provider.go index 3238a1c83..1f7837989 100644 --- a/internal/oidc/provider.go +++ b/internal/oidc/provider.go @@ -13,34 +13,31 @@ import ( ) // NewOpenIDConnectProvider new-ups a OpenIDConnectProvider. -func NewOpenIDConnectProvider(config *schema.OpenIDConnectConfiguration, store storage.Provider, templates *templates.Provider) (provider *OpenIDConnectProvider, err error) { +func NewOpenIDConnectProvider(config *schema.OpenIDConnectConfiguration, store storage.Provider, templates *templates.Provider) (provider *OpenIDConnectProvider) { if config == nil { - return nil, nil + return nil } provider = &OpenIDConnectProvider{ JSONWriter: herodot.NewJSONWriter(nil), Store: NewStore(config, store), + KeyManager: NewKeyManager(config), Config: NewConfig(config, templates), } provider.OAuth2Provider = fosite.NewOAuth2Provider(provider.Store, provider.Config) - if provider.KeyManager, err = NewKeyManagerWithConfiguration(config); err != nil { - return nil, err - } - provider.Config.Strategy.OpenID = &openid.DefaultStrategy{ - Signer: provider.KeyManager.Strategy(), + Signer: provider.KeyManager, Config: provider.Config, } - provider.Config.LoadHandlers(provider.Store, provider.KeyManager.Strategy()) + provider.Config.LoadHandlers(provider.Store, provider.KeyManager) provider.Config.Strategy.ClientAuthentication = provider.DefaultClientAuthenticationStrategy provider.discovery = NewOpenIDConnectWellKnownConfiguration(config) - return provider, nil + return provider } // GetOAuth2WellKnownConfiguration returns the discovery document for the OAuth Configuration. diff --git a/internal/oidc/provider_test.go b/internal/oidc/provider_test.go index 51ed6e4e6..335b03ab9 100644 --- a/internal/oidc/provider_test.go +++ b/internal/oidc/provider_test.go @@ -1,6 +1,7 @@ package oidc import ( + "encoding/json" "net/url" "testing" @@ -11,16 +12,15 @@ import ( ) func TestOpenIDConnectProvider_NewOpenIDConnectProvider_NotConfigured(t *testing.T) { - provider, err := NewOpenIDConnectProvider(nil, nil, nil) + provider := NewOpenIDConnectProvider(nil, nil, nil) - assert.NoError(t, err) assert.Nil(t, provider) } func TestNewOpenIDConnectProvider_ShouldEnableOptionalDiscoveryValues(t *testing.T) { - provider, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ + provider := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, - IssuerPrivateKey: MustParseRSAPrivateKey(exampleIssuerPrivateKey), + IssuerPrivateKey: keyRSA2048, EnablePKCEPlainChallenge: true, HMACSecret: badhmac, Clients: []schema.OpenIDConnectClientConfiguration{ @@ -36,7 +36,7 @@ func TestNewOpenIDConnectProvider_ShouldEnableOptionalDiscoveryValues(t *testing }, }, nil, nil) - assert.NoError(t, err) + require.NotNil(t, provider) disco := provider.GetOpenIDConnectWellKnownConfiguration(examplecom) @@ -50,9 +50,9 @@ func TestNewOpenIDConnectProvider_ShouldEnableOptionalDiscoveryValues(t *testing } func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *testing.T) { - provider, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ + provider := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, - IssuerPrivateKey: MustParseRSAPrivateKey(exampleIssuerPrivateKey), + IssuerPrivateKey: keyRSA2048, HMACSecret: badhmac, Clients: []schema.OpenIDConnectClientConfiguration{ { @@ -86,13 +86,12 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GoodConfiguration(t *tes }, nil, nil) assert.NotNil(t, provider) - assert.NoError(t, err) } func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnownConfiguration(t *testing.T) { - provider, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ + provider := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, - IssuerPrivateKey: MustParseRSAPrivateKey(exampleIssuerPrivateKey), + IssuerPrivateKey: keyRSA2048, HMACSecret: "asbdhaaskmdlkamdklasmdlkams", Clients: []schema.OpenIDConnectClientConfiguration{ { @@ -106,7 +105,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow }, }, nil, nil) - assert.NoError(t, err) + require.NotNil(t, provider) disco := provider.GetOpenIDConnectWellKnownConfiguration(examplecom) @@ -153,19 +152,41 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodClientSecretJWT) assert.Contains(t, disco.TokenEndpointAuthMethodsSupported, ClientAuthMethodNone) + assert.Len(t, disco.RevocationEndpointAuthMethodsSupported, 4) + assert.Contains(t, disco.RevocationEndpointAuthMethodsSupported, ClientAuthMethodClientSecretBasic) + assert.Contains(t, disco.RevocationEndpointAuthMethodsSupported, ClientAuthMethodClientSecretPost) + assert.Contains(t, disco.RevocationEndpointAuthMethodsSupported, ClientAuthMethodClientSecretJWT) + assert.Contains(t, disco.RevocationEndpointAuthMethodsSupported, ClientAuthMethodNone) + + assert.Len(t, disco.IntrospectionEndpointAuthMethodsSupported, 2) + assert.Contains(t, disco.IntrospectionEndpointAuthMethodsSupported, ClientAuthMethodClientSecretBasic) + assert.Contains(t, disco.IntrospectionEndpointAuthMethodsSupported, ClientAuthMethodNone) + assert.Len(t, disco.GrantTypesSupported, 3) assert.Contains(t, disco.GrantTypesSupported, GrantTypeAuthorizationCode) assert.Contains(t, disco.GrantTypesSupported, GrantTypeRefreshToken) assert.Contains(t, disco.GrantTypesSupported, GrantTypeImplicit) + assert.Len(t, disco.RevocationEndpointAuthSigningAlgValuesSupported, 3) + assert.Equal(t, disco.RevocationEndpointAuthSigningAlgValuesSupported[0], SigningAlgHMACUsingSHA256) + assert.Equal(t, disco.RevocationEndpointAuthSigningAlgValuesSupported[1], SigningAlgHMACUsingSHA384) + assert.Equal(t, disco.RevocationEndpointAuthSigningAlgValuesSupported[2], SigningAlgHMACUsingSHA512) + + assert.Len(t, disco.TokenEndpointAuthSigningAlgValuesSupported, 3) + assert.Equal(t, disco.TokenEndpointAuthSigningAlgValuesSupported[0], SigningAlgHMACUsingSHA256) + assert.Equal(t, disco.TokenEndpointAuthSigningAlgValuesSupported[1], SigningAlgHMACUsingSHA384) + assert.Equal(t, disco.TokenEndpointAuthSigningAlgValuesSupported[2], SigningAlgHMACUsingSHA512) + assert.Len(t, disco.IDTokenSigningAlgValuesSupported, 1) assert.Contains(t, disco.IDTokenSigningAlgValuesSupported, SigningAlgRSAUsingSHA256) assert.Len(t, disco.UserinfoSigningAlgValuesSupported, 2) - assert.Contains(t, disco.UserinfoSigningAlgValuesSupported, SigningAlgRSAUsingSHA256) - assert.Contains(t, disco.UserinfoSigningAlgValuesSupported, SigningAlgNone) + assert.Equal(t, disco.UserinfoSigningAlgValuesSupported[0], SigningAlgRSAUsingSHA256) + assert.Equal(t, disco.UserinfoSigningAlgValuesSupported[1], SigningAlgNone) - assert.Len(t, disco.RequestObjectSigningAlgValuesSupported, 0) + require.Len(t, disco.RequestObjectSigningAlgValuesSupported, 2) + assert.Equal(t, SigningAlgRSAUsingSHA256, disco.RequestObjectSigningAlgValuesSupported[0]) + assert.Equal(t, SigningAlgNone, disco.RequestObjectSigningAlgValuesSupported[1]) assert.Len(t, disco.ClaimsSupported, 18) assert.Contains(t, disco.ClaimsSupported, ClaimAuthenticationMethodsReference) @@ -186,12 +207,16 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow assert.Contains(t, disco.ClaimsSupported, ClaimGroups) assert.Contains(t, disco.ClaimsSupported, ClaimPreferredUsername) assert.Contains(t, disco.ClaimsSupported, ClaimFullName) + + assert.Len(t, disco.PromptValuesSupported, 2) + assert.Contains(t, disco.PromptValuesSupported, PromptConsent) + assert.Contains(t, disco.PromptValuesSupported, PromptNone) } func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOAuth2WellKnownConfiguration(t *testing.T) { - provider, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ + provider := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, - IssuerPrivateKey: MustParseRSAPrivateKey(exampleIssuerPrivateKey), + IssuerPrivateKey: keyRSA2048, HMACSecret: "asbdhaaskmdlkamdklasmdlkams", Clients: []schema.OpenIDConnectClientConfiguration{ { @@ -205,7 +230,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOAuth2WellKnownConfig }, }, nil, nil) - assert.NoError(t, err) + require.NotNil(t, provider) disco := provider.GetOAuth2WellKnownConfiguration(examplecom) @@ -278,9 +303,9 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOAuth2WellKnownConfig } func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnownConfigurationWithPlainPKCE(t *testing.T) { - provider, err := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ + provider := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, - IssuerPrivateKey: MustParseRSAPrivateKey(exampleIssuerPrivateKey), + IssuerPrivateKey: keyRSA2048, HMACSecret: "asbdhaaskmdlkamdklasmdlkams", EnablePKCEPlainChallenge: true, Clients: []schema.OpenIDConnectClientConfiguration{ @@ -295,7 +320,7 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow }, }, nil, nil) - assert.NoError(t, err) + require.NotNil(t, provider) disco := provider.GetOpenIDConnectWellKnownConfiguration(examplecom) @@ -303,3 +328,44 @@ func TestOpenIDConnectProvider_NewOpenIDConnectProvider_GetOpenIDConnectWellKnow assert.Equal(t, PKCEChallengeMethodSHA256, disco.CodeChallengeMethodsSupported[0]) assert.Equal(t, PKCEChallengeMethodPlain, disco.CodeChallengeMethodsSupported[1]) } + +func TestNewOpenIDConnectProviderDiscovery(t *testing.T) { + provider := NewOpenIDConnectProvider(&schema.OpenIDConnectConfiguration{ + IssuerCertificateChain: schema.X509CertificateChain{}, + IssuerPrivateKey: keyRSA2048, + HMACSecret: "asbdhaaskmdlkamdklasmdlkams", + EnablePKCEPlainChallenge: true, + Clients: []schema.OpenIDConnectClientConfiguration{ + { + ID: "a-client", + Secret: MustDecodeSecret("$plaintext$a-client-secret"), + Policy: onefactor, + RedirectURIs: []string{ + "https://google.com", + }, + }, + }, + }, nil, nil) + + a := provider.GetOpenIDConnectWellKnownConfiguration("https://auth.example.com") + + data, err := json.Marshal(&a) + assert.NoError(t, err) + + b := OpenIDConnectWellKnownConfiguration{} + + assert.NoError(t, json.Unmarshal(data, &b)) + + assert.Equal(t, a, b) + + y := provider.GetOAuth2WellKnownConfiguration("https://auth.example.com") + + data, err = json.Marshal(&y) + assert.NoError(t, err) + + z := OAuth2WellKnownConfiguration{} + + assert.NoError(t, json.Unmarshal(data, &z)) + + assert.Equal(t, y, z) +} diff --git a/internal/oidc/store_test.go b/internal/oidc/store_test.go index 47e15c424..bdd5f4d69 100644 --- a/internal/oidc/store_test.go +++ b/internal/oidc/store_test.go @@ -15,7 +15,7 @@ import ( func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) { s := NewStore(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, - IssuerPrivateKey: MustParseRSAPrivateKey(exampleIssuerPrivateKey), + IssuerPrivateKey: keyRSA2048, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: myclient, @@ -47,7 +47,7 @@ func TestOpenIDConnectStore_GetClientPolicy(t *testing.T) { func TestOpenIDConnectStore_GetInternalClient(t *testing.T) { s := NewStore(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, - IssuerPrivateKey: MustParseRSAPrivateKey(exampleIssuerPrivateKey), + IssuerPrivateKey: keyRSA2048, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: myclient, @@ -82,7 +82,7 @@ func TestOpenIDConnectStore_GetInternalClient_ValidClient(t *testing.T) { s := NewStore(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, - IssuerPrivateKey: MustParseRSAPrivateKey(exampleIssuerPrivateKey), + IssuerPrivateKey: keyRSA2048, Clients: []schema.OpenIDConnectClientConfiguration{c1}, }, nil) @@ -110,7 +110,7 @@ func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) { s := NewStore(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, - IssuerPrivateKey: MustParseRSAPrivateKey(exampleIssuerPrivateKey), + IssuerPrivateKey: keyRSA2048, Clients: []schema.OpenIDConnectClientConfiguration{c1}, }, nil) @@ -122,7 +122,7 @@ func TestOpenIDConnectStore_GetInternalClient_InvalidClient(t *testing.T) { func TestOpenIDConnectStore_IsValidClientID(t *testing.T) { s := NewStore(&schema.OpenIDConnectConfiguration{ IssuerCertificateChain: schema.X509CertificateChain{}, - IssuerPrivateKey: MustParseRSAPrivateKey(exampleIssuerPrivateKey), + IssuerPrivateKey: keyRSA2048, Clients: []schema.OpenIDConnectClientConfiguration{ { ID: myclient, diff --git a/internal/oidc/types.go b/internal/oidc/types.go index 6346a3ae2..ecec14e5f 100644 --- a/internal/oidc/types.go +++ b/internal/oidc/types.go @@ -122,7 +122,8 @@ type BaseClient struct { ResponseTypes []string ResponseModes []fosite.ResponseModeType - UserinfoSigningAlgorithm string + IDTokenSigningAlg string + UserinfoSigningAlg string Policy authorization.Level @@ -150,7 +151,9 @@ type Client interface { GetSecret() algorithm.Digest GetSectorIdentifier() string GetConsentResponseBody(consent *model.OAuth2ConsentSession) ConsentGetResponseBody - GetUserinfoSigningAlgorithm() string + + GetUserinfoSigningAlg() string + GetIDTokenSigningAlg() string GetPAREnforcement() bool GetPKCEEnforcement() bool @@ -222,13 +225,6 @@ func (c ClientConsentMode) String() string { } } -// KeyManager keeps track of all of the active/inactive rsa keys and provides them to services requiring them. -// It additionally allows us to add keys for the purpose of key rotation in the future. -type KeyManager struct { - jwk *JWK - jwks *jose.JSONWebKeySet -} - // ConsentGetResponseBody schema of the response body of the consent GET endpoint. type ConsentGetResponseBody struct { ClientID string `json:"client_id"` diff --git a/internal/oidc/util.go b/internal/oidc/util.go index 2e7c14d37..6fc10cab1 100644 --- a/internal/oidc/util.go +++ b/internal/oidc/util.go @@ -4,9 +4,101 @@ import ( "strings" "github.com/ory/fosite" + "gopkg.in/square/go-jose.v2" ) // IsPushedAuthorizedRequest returns true if the requester has a PushedAuthorizationRequest redirect_uri value. func IsPushedAuthorizedRequest(r fosite.Requester, prefix string) bool { return strings.HasPrefix(r.GetRequestForm().Get(FormParameterRequestURI), prefix) } + +// SortedSigningAlgs is a sorting type which allows the use of sort.Sort to order a list of OAuth 2.0 Signing Algs. +// Sorting occurs in the order of from within the RFC's. +type SortedSigningAlgs []string + +func (algs SortedSigningAlgs) Len() int { + return len(algs) +} + +func (algs SortedSigningAlgs) Less(i, j int) bool { + return isSigningAlgLess(algs[i], algs[j]) +} + +func (algs SortedSigningAlgs) Swap(i, j int) { + algs[i], algs[j] = algs[j], algs[i] +} + +type SortedJSONWebKey []jose.JSONWebKey + +func (jwks SortedJSONWebKey) Len() int { + return len(jwks) +} + +func (jwks SortedJSONWebKey) Less(i, j int) bool { + if jwks[i].Algorithm == jwks[j].Algorithm { + return jwks[i].KeyID < jwks[j].KeyID + } + + return isSigningAlgLess(jwks[i].Algorithm, jwks[j].Algorithm) +} + +func (jwks SortedJSONWebKey) Swap(i, j int) { + jwks[i], jwks[j] = jwks[j], jwks[i] +} + +//nolint:gocyclo // Low importance func. +func isSigningAlgLess(i, j string) bool { + switch { + case i == j: + return false + case i == SigningAlgNone: + return false + case j == SigningAlgNone: + return true + default: + var ( + ip, jp string + it, jt bool + ) + + if len(i) > 2 { + it = true + ip = i[:2] + } + + if len(j) > 2 { + jt = true + jp = j[:2] + } + + switch { + case it && jt && ip == jp: + return i < j + case ip == SigningAlgPrefixHMAC: + return true + case jp == SigningAlgPrefixHMAC: + return false + case ip == SigningAlgPrefixRSAPSS: + return false + case jp == SigningAlgPrefixRSAPSS: + return true + case ip == SigningAlgPrefixRSA: + return true + case jp == SigningAlgPrefixRSA: + return false + case ip == SigningAlgPrefixECDSA: + return true + case jp == SigningAlgPrefixECDSA: + return false + default: + return false + } + } +} + +const ( + SigningAlgPrefixRSA = "RS" + SigningAlgPrefixHMAC = "HS" + SigningAlgPrefixRSAPSS = "PS" + SigningAlgPrefixECDSA = "ES" +) diff --git a/internal/utils/const.go b/internal/utils/const.go index eb53c2d93..f6f28a366 100644 --- a/internal/utils/const.go +++ b/internal/utils/const.go @@ -31,6 +31,7 @@ const ( BlockTypePKIXPublicKey = "PUBLIC KEY" BlockTypeCertificate = "CERTIFICATE" BlockTypeCertificateRequest = "CERTIFICATE REQUEST" + BlockTypeX509CRL = "X509 CRL" KeyAlgorithmRSA = "RSA" KeyAlgorithmECDSA = "ECDSA" diff --git a/internal/utils/crypto.go b/internal/utils/crypto.go index 3f177fb1e..d8afd2d4c 100644 --- a/internal/utils/crypto.go +++ b/internal/utils/crypto.go @@ -13,6 +13,7 @@ import ( "encoding/pem" "errors" "fmt" + "io" "math/big" "net" "os" @@ -188,28 +189,95 @@ func ParseX509FromPEM(data []byte) (key any, err error) { return nil, errors.New("failed to parse PEM block containing the key") } + return ParsePEMBlock(block) +} + +// ParseX509FromPEMRecursive allows returning the appropriate key type given some PEM encoded input. +// For Keys this is a single value of one of *rsa.PrivateKey, *rsa.PublicKey, *ecdsa.PrivateKey, *ecdsa.PublicKey, +// ed25519.PrivateKey, or ed25519.PublicKey. For certificates this is +// either a *X509.Certificate, or a []*X509.Certificate. +func ParseX509FromPEMRecursive(data []byte) (decoded any, err error) { + var ( + block *pem.Block + multi bool + certificates []*x509.Certificate + ) + + for i := 0; true; i++ { + block, data = pem.Decode(data) + + n := len(data) + + switch { + case block == nil: + return nil, fmt.Errorf("failed to parse PEM blocks: data does not appear to be PEM encoded") + case multi || n != 0: + switch block.Type { + case BlockTypeCertificate: + var certificate *x509.Certificate + + if certificate, err = x509.ParseCertificate(block.Bytes); err != nil { + return nil, fmt.Errorf("failed to parse PEM blocks: data contains multiple blocks but #%d had an error during parsing: %w", i, err) + } + + certificates = append(certificates, certificate) + default: + return nil, fmt.Errorf("failed to parse PEM blocks: data contains multiple blocks but #%d has a '%s' block type and should have a '%s' block type", i, block.Type, BlockTypeCertificate) + } + + multi = true + default: + if decoded, err = ParsePEMBlock(block); err != nil { + return nil, err + } + } + + if n == 0 { + break + } + } + + switch { + case multi: + return certificates, nil + default: + return decoded, nil + } +} + +// ParsePEMBlock parses a single PEM block into the relevant X509 data struct. +func ParsePEMBlock(block *pem.Block) (key any, err error) { + if block == nil { + return nil, errors.New("failed to parse PEM block as it was empty") + } + switch block.Type { case BlockTypeRSAPrivateKey: - key, err = x509.ParsePKCS1PrivateKey(block.Bytes) + return x509.ParsePKCS1PrivateKey(block.Bytes) case BlockTypeECDSAPrivateKey: - key, err = x509.ParseECPrivateKey(block.Bytes) + return x509.ParseECPrivateKey(block.Bytes) case BlockTypePKCS8PrivateKey: - key, err = x509.ParsePKCS8PrivateKey(block.Bytes) + return x509.ParsePKCS8PrivateKey(block.Bytes) case BlockTypeRSAPublicKey: - key, err = x509.ParsePKCS1PublicKey(block.Bytes) + return x509.ParsePKCS1PublicKey(block.Bytes) case BlockTypePKIXPublicKey: - key, err = x509.ParsePKIXPublicKey(block.Bytes) + return x509.ParsePKIXPublicKey(block.Bytes) case BlockTypeCertificate: - key, err = x509.ParseCertificate(block.Bytes) + return x509.ParseCertificate(block.Bytes) + case BlockTypeCertificateRequest: + return x509.ParseCertificateRequest(block.Bytes) + case BlockTypeX509CRL: + return x509.ParseRevocationList(block.Bytes) default: - return nil, fmt.Errorf("unknown block type: %s", block.Type) + switch { + case strings.Contains(block.Type, "PRIVATE KEY"): + return x509.ParsePKCS8PrivateKey(block.Bytes) + case strings.Contains(block.Type, "PUBLIC KEY"): + return x509.ParsePKIXPublicKey(block.Bytes) + default: + return nil, fmt.Errorf("unknown block type: %s", block.Type) + } } - - if err != nil { - return nil, err - } - - return key, nil } // CastX509AsCertificate converts an interface to an *x509.Certificate. @@ -306,8 +374,25 @@ func NewX509CertPool(directory string) (certPool *x509.CertPool, warnings []erro return certPool, warnings, errors } -// WriteCertificateBytesToPEM writes a certificate/csr to a file in the PEM format. -func WriteCertificateBytesToPEM(path string, csr bool, certs ...[]byte) (err error) { +// WriteCertificateBytesAsPEMToPath writes a certificate/csr to a file in the PEM format. +func WriteCertificateBytesAsPEMToPath(path string, csr bool, certs ...[]byte) (err error) { + var out *os.File + + if out, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600); err != nil { + return err + } + + if err = WriteCertificateBytesAsPEMToWriter(out, csr, certs...); err != nil { + _ = out.Close() + + return err + } + + return nil +} + +// WriteCertificateBytesAsPEMToWriter writes a certificate/csr to a io.Writer in the PEM format. +func WriteCertificateBytesAsPEMToWriter(wr io.Writer, csr bool, certs ...[]byte) (err error) { blockType := BlockTypeCertificate if csr { blockType = BlockTypeCertificateRequest @@ -319,26 +404,34 @@ func WriteCertificateBytesToPEM(path string, csr bool, certs ...[]byte) (err err blocks[i] = &pem.Block{Type: blockType, Bytes: cert} } - return WritePEM(path, blocks...) + return WritePEMBlocksToWriter(wr, blocks...) } -// WritePEM writes a set of *pem.Blocks to a file. -func WritePEM(path string, blocks ...*pem.Block) (err error) { +// WritePEMBlocksToPath writes a set of *pem.Blocks to a file. +func WritePEMBlocksToPath(path string, blocks ...*pem.Block) (err error) { var out *os.File if out, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600); err != nil { return err } - for _, block := range blocks { - if err = pem.Encode(out, block); err != nil { - _ = out.Close() + if err = WritePEMBlocksToWriter(out, blocks...); err != nil { + _ = out.Close() + return err + } + + return out.Close() +} + +func WritePEMBlocksToWriter(wr io.Writer, blocks ...*pem.Block) (err error) { + for _, block := range blocks { + if err = pem.Encode(wr, block); err != nil { return err } } - return out.Close() + return nil } // WriteKeyToPEM writes a key that can be encoded as a PEM to a file in the PEM format. @@ -348,7 +441,7 @@ func WriteKeyToPEM(key any, path string, pkcs8 bool) (err error) { return err } - return WritePEM(path, block) + return WritePEMBlocksToPath(path, block) } // PEMBlockFromX509Key turns a PublicKey or PrivateKey into a pem.Block.