From 4b3d0839410d1a3edd010ece3035e3c29d68c5b8 Mon Sep 17 00:00:00 2001 From: Daisuke Maki Date: Thu, 3 Oct 2024 16:05:48 +0900 Subject: [PATCH] Allow flexible key usage values --- jwk/jwk.go | 17 ++++++++++ jwk/jwk_test.go | 8 ++--- jwk/options.yaml | 21 +++++++++++- jwk/options_gen.go | 33 +++++++++++++++++++ jwk/options_gen_test.go | 1 + jwk/usage.go | 72 +++++++++++++++++++++++++++++++++-------- 6 files changed, 131 insertions(+), 21 deletions(-) diff --git a/jwk/jwk.go b/jwk/jwk.go index 1c24fba53..13a4a55e0 100644 --- a/jwk/jwk.go +++ b/jwk/jwk.go @@ -619,3 +619,20 @@ func IsKeyValidationError(err error) bool { var kve keyValidationError return errors.Is(err, &kve) } + +// Configure is used to configure global behavior of the jwk package. +func Configure(options ...GlobalOption) { + var strictKeyUsagePtr *bool + //nolint:forcetypeassert + for _, option := range options { + switch option.Ident() { + case identStrictKeyUsage{}: + v := option.Value().(bool) + strictKeyUsagePtr = &v + } + } + + if strictKeyUsagePtr != nil { + strictKeyUsage.Store(*strictKeyUsagePtr) + } +} diff --git a/jwk/jwk_test.go b/jwk/jwk_test.go index 6aa1ada19..e47d3194f 100644 --- a/jwk/jwk_test.go +++ b/jwk/jwk_test.go @@ -783,13 +783,9 @@ func TestAccept(t *testing.T) { for _, test := range testcases { var usage jwk.KeyUsageType if test.Error { - if !assert.Error(t, usage.Accept(test.Args), `KeyUsage.Accept should fail`) { - return - } + require.Error(t, usage.Accept(test.Args), `KeyUsage.Accept should fail`) } else { - if !assert.NoError(t, usage.Accept(test.Args), `KeyUsage.Accept should succeed`) { - return - } + require.NoError(t, usage.Accept(test.Args), `KeyUsage.Accept should succeed`) } } }) diff --git a/jwk/options.yaml b/jwk/options.yaml index 68cd72dd0..49f4faedb 100644 --- a/jwk/options.yaml +++ b/jwk/options.yaml @@ -41,6 +41,10 @@ interfaces: - parseOption comment: | RegisterFetchOption describes options that can be passed to `(jwk.Cache).Register()` and `jwk.Fetch()` + - name: GlobalOption + comment: | + GlobalOption is a type of Option that can be passed to the `jwk.Configure()` to + change the global configuration of the jwk package. options: - ident: HTTPClient interface: RegisterFetchOption @@ -109,4 +113,19 @@ options: first fetch is done before returning from the `Register()` call. This option is by default true. Specify a false value if you would - like to return immediately from the `Register()` call. \ No newline at end of file + like to return immediately from the `Register()` call. + + This options is exactly the same as `httprc.WithWaitReady()` + - ident: StrictKeyUsage + interface: GlobalOption + argument_type: bool + comment: | + WithStrictKeyUsage specifies if during JWK parsing, the "use" field + should be confined to the values that have been registered via + `jwk.RegisterKeyType()`. By default this option is true, and the + initial allowed values are "use" and "enc" only. + + If this option is set to false, then the "use" field can be any + value. If this options is set to true, then the "use" field must + be one of the registered values, and otherwise an error will be + reported during parsing / assignment to `jwk.KeyUsageType` \ No newline at end of file diff --git a/jwk/options_gen.go b/jwk/options_gen.go index e8ee968af..b0af1ba92 100644 --- a/jwk/options_gen.go +++ b/jwk/options_gen.go @@ -56,6 +56,19 @@ func (*fetchOption) parseOption() {} func (*fetchOption) registerOption() {} +// GlobalOption is a type of Option that can be passed to the `jwk.Configure()` to +// change the global configuration of the jwk package. +type GlobalOption interface { + Option + globalOption() +} + +type globalOption struct { + Option +} + +func (*globalOption) globalOption() {} + // ParseOption is a type of Option that can be passed to `jwk.Parse()` // ParseOption also implements the `ReadFileOption` and `NewCacheOption`, // and thus safely be passed to `jwk.ReadFile` and `(*jwk.Cache).Configure()` @@ -138,6 +151,7 @@ type identIgnoreParseError struct{} type identLocalRegistry struct{} type identPEM struct{} type identPEMDecoder struct{} +type identStrictKeyUsage struct{} type identThumbprintHash struct{} type identWaitReady struct{} @@ -169,6 +183,10 @@ func (identPEMDecoder) String() string { return "WithPEMDecoder" } +func (identStrictKeyUsage) String() string { + return "WithStrictKeyUsage" +} + func (identThumbprintHash) String() string { return "WithThumbprintHash" } @@ -234,6 +252,19 @@ func WithPEMDecoder(v PEMDecoder) ParseOption { return &parseOption{option.New(identPEMDecoder{}, v)} } +// WithStrictKeyUsage specifies if during JWK parsing, the "use" field +// should be confined to the values that have been registered via +// `jwk.RegisterKeyType()`. By default this option is true, and the +// initial allowed values are "use" and "enc" only. +// +// If this option is set to false, then the "use" field can be any +// value. If this options is set to true, then the "use" field must +// be one of the registered values, and otherwise an error will be +// reported during parsing / assignment to `jwk.KeyUsageType` +func WithStrictKeyUsage(v bool) GlobalOption { + return &globalOption{option.New(identStrictKeyUsage{}, v)} +} + func WithThumbprintHash(v crypto.Hash) AssignKeyIDOption { return &assignKeyIDOption{option.New(identThumbprintHash{}, v)} } @@ -243,6 +274,8 @@ func WithThumbprintHash(v crypto.Hash) AssignKeyIDOption { // // This option is by default true. Specify a false value if you would // like to return immediately from the `Register()` call. +// +// This options is exactly the same as `httprc.WithWaitReady()` func WithWaitReady(v bool) RegisterOption { return ®isterOption{option.New(identWaitReady{}, v)} } diff --git a/jwk/options_gen_test.go b/jwk/options_gen_test.go index 8fe6fefa8..d3103ef5f 100644 --- a/jwk/options_gen_test.go +++ b/jwk/options_gen_test.go @@ -16,6 +16,7 @@ func TestOptionIdent(t *testing.T) { require.Equal(t, "withLocalRegistry", identLocalRegistry{}.String()) require.Equal(t, "WithPEM", identPEM{}.String()) require.Equal(t, "WithPEMDecoder", identPEMDecoder{}.String()) + require.Equal(t, "WithStrictKeyUsage", identStrictKeyUsage{}.String()) require.Equal(t, "WithThumbprintHash", identThumbprintHash{}.String()) require.Equal(t, "WithWaitReady", identWaitReady{}.String()) } diff --git a/jwk/usage.go b/jwk/usage.go index c21892395..a520cf82a 100644 --- a/jwk/usage.go +++ b/jwk/usage.go @@ -1,6 +1,54 @@ package jwk -import "fmt" +import ( + "fmt" + "sync" + "sync/atomic" +) + +var strictKeyUsage = atomic.Bool{} +var keyUsageNames = map[string]struct{}{} +var muKeyUsageName sync.RWMutex + +// RegisterKeyUsage registers a possible value that can be used for KeyUsageType. +// Normally, key usage (or the "use" field in a JWK) is either "sig" or "enc", +// but other values may be used. +// +// While this module only works with "sig" and "enc", it is possible that +// systems choose to use other values. This function allows users to register +// new values to be accepted as valid key usage types. Values are case sensitive. +// +// Furthermore, the check against registered values can be completely turned off +// by setting the global option `jwk.WithStrictKeyUsage(false)`. +func RegisterKeyUsage(v string) { + muKeyUsageName.Lock() + defer muKeyUsageName.Unlock() + keyUsageNames[v] = struct{}{} +} + +func UnregisterKeyUsage(v string) { + muKeyUsageName.Lock() + defer muKeyUsageName.Unlock() + delete(keyUsageNames, v) +} + +func init() { + strictKeyUsage.Store(true) + RegisterKeyUsage("sig") + RegisterKeyUsage("enc") +} + +func isValidUsage(v string) bool { + // This function can return true if strictKeyUsage is false + if !strictKeyUsage.Load() { + return true + } + + muKeyUsageName.RLock() + defer muKeyUsageName.RUnlock() + _, ok := keyUsageNames[v] + return ok +} func (k KeyUsageType) String() string { return string(k) @@ -9,22 +57,18 @@ func (k KeyUsageType) String() string { func (k *KeyUsageType) Accept(v interface{}) error { switch v := v.(type) { case KeyUsageType: - switch v { - case ForSignature, ForEncryption: - *k = v - return nil - default: - return fmt.Errorf("invalid key usage type %s", v) + if !isValidUsage(v.String()) { + return fmt.Errorf("invalid key usage type: %q", v) } + *k = v + return nil case string: - switch v { - case ForSignature.String(), ForEncryption.String(): - *k = KeyUsageType(v) - return nil - default: - return fmt.Errorf("invalid key usage type %s", v) + if !isValidUsage(v) { + return fmt.Errorf("invalid key usage type: %q", v) } + *k = KeyUsageType(v) + return nil } - return fmt.Errorf("invalid value for key usage type %s", v) + return fmt.Errorf("invalid Go type for key usage type: %T", v) }