Initial QSfera import
This commit is contained in:
+87
@@ -0,0 +1,87 @@
|
||||
load("@rules_go//go:def.bzl", "go_library", "go_test")
|
||||
|
||||
go_library(
|
||||
name = "jwk",
|
||||
srcs = [
|
||||
"cache.go",
|
||||
"convert.go",
|
||||
"ecdsa.go",
|
||||
"ecdsa_gen.go",
|
||||
"errors.go",
|
||||
"fetch.go",
|
||||
"filter.go",
|
||||
"interface.go",
|
||||
"interface_gen.go",
|
||||
"io.go",
|
||||
"jwk.go",
|
||||
"key_ops.go",
|
||||
"okp.go",
|
||||
"okp_gen.go",
|
||||
"options.go",
|
||||
"options_gen.go",
|
||||
"parser.go",
|
||||
"rsa.go",
|
||||
"rsa_gen.go",
|
||||
"set.go",
|
||||
"symmetric.go",
|
||||
"symmetric_gen.go",
|
||||
"usage.go",
|
||||
"whitelist.go",
|
||||
"x509.go",
|
||||
],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3/jwk",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"//cert",
|
||||
"//internal/base64",
|
||||
"//internal/ecutil",
|
||||
"//transform",
|
||||
"//internal/json",
|
||||
"//internal/pool",
|
||||
"//internal/tokens",
|
||||
"//jwa",
|
||||
"//jwk/ecdsa",
|
||||
"//jwk/jwkbb",
|
||||
"@com_github_lestrrat_go_blackmagic//:blackmagic",
|
||||
"@com_github_lestrrat_go_httprc_v3//:httprc",
|
||||
"@com_github_lestrrat_go_option_v2//:option",
|
||||
],
|
||||
)
|
||||
|
||||
go_test(
|
||||
name = "jwk_test",
|
||||
srcs = [
|
||||
"filter_test.go",
|
||||
"headers_test.go",
|
||||
"jwk_internal_test.go",
|
||||
"jwk_test.go",
|
||||
"options_gen_test.go",
|
||||
"refresh_test.go",
|
||||
"set_test.go",
|
||||
"x5c_test.go",
|
||||
],
|
||||
data = glob(["testdata/**"]),
|
||||
embed = [":jwk"],
|
||||
deps = [
|
||||
"//cert",
|
||||
"//internal/base64",
|
||||
"//internal/jose",
|
||||
"//internal/json",
|
||||
"//internal/jwxtest",
|
||||
"//internal/tokens",
|
||||
"//jwa",
|
||||
"//jwk/ecdsa",
|
||||
"//jws",
|
||||
"@com_github_lestrrat_go_blackmagic//:blackmagic",
|
||||
"@com_github_lestrrat_go_httprc_v3//:httprc",
|
||||
"@com_github_lestrrat_go_httprc_v3//tracesink",
|
||||
"@com_github_stretchr_testify//assert",
|
||||
"@com_github_stretchr_testify//require",
|
||||
],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "go_default_library",
|
||||
actual = ":jwk",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
+215
@@ -0,0 +1,215 @@
|
||||
# JWK [](https://pkg.go.dev/github.com/lestrrat-go/jwx/v3/jwk)
|
||||
|
||||
Package jwk implements JWK as described in [RFC7517](https://tools.ietf.org/html/rfc7517).
|
||||
If you are looking to use JWT wit JWKs, look no further than [github.com/lestrrat-go/jwx](../jwt).
|
||||
|
||||
* Parse and work with RSA/EC/Symmetric/OKP JWK types
|
||||
* Convert to and from JSON
|
||||
* Convert to and from raw key types (e.g. *rsa.PrivateKey)
|
||||
* Ability to keep a JWKS fresh using *jwk.AutoRefresh
|
||||
|
||||
## Supported key types:
|
||||
|
||||
| kty | Curve | Go Key Type |
|
||||
|:----|:------------------------|:----------------------------------------------|
|
||||
| RSA | N/A | rsa.PrivateKey / rsa.PublicKey (2) |
|
||||
| EC | P-256<br>P-384<br>P-521<br>secp256k1 (1) | ecdsa.PrivateKey / ecdsa.PublicKey (2) |
|
||||
| oct | N/A | []byte |
|
||||
| OKP | Ed25519 (1) | ed25519.PrivateKey / ed25519.PublicKey (2) |
|
||||
| | X25519 (1) | (jwx/)x25519.PrivateKey / x25519.PublicKey (2)|
|
||||
|
||||
* Note 1: Experimental
|
||||
* Note 2: Either value or pointers accepted (e.g. rsa.PrivateKey or *rsa.PrivateKey)
|
||||
|
||||
# Documentation
|
||||
|
||||
Please read the [API reference](https://pkg.go.dev/github.com/lestrrat-go/jwx/v3/jwk), or
|
||||
the how-to style documentation on how to use JWK can be found in the [docs directory](../docs/04-jwk.md).
|
||||
|
||||
# Auto-Refresh a key during a long-running process
|
||||
|
||||
<!-- INCLUDE(examples/jwk_cache_example_test.go) -->
|
||||
```go
|
||||
package examples_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/lestrrat-go/httprc/v3"
|
||||
"github.com/lestrrat-go/jwx/v3/jwk"
|
||||
)
|
||||
|
||||
func Example_jwk_cache() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
const googleCerts = `https://www.googleapis.com/oauth2/v3/certs`
|
||||
|
||||
// First, set up the `jwk.Cache` object. You need to pass it a
|
||||
// `context.Context` object to control the lifecycle of the background fetching goroutine.
|
||||
c, err := jwk.NewCache(ctx, httprc.NewClient())
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create cache: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Tell *jwk.Cache that we only want to refresh this JWKS periodically.
|
||||
if err := c.Register(ctx, googleCerts); err != nil {
|
||||
fmt.Printf("failed to register google JWKS: %s\n", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Pretend that this is your program's main loop
|
||||
MAIN:
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
break MAIN
|
||||
default:
|
||||
}
|
||||
keyset, err := c.Lookup(ctx, googleCerts)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to fetch google JWKS: %s\n", err)
|
||||
return
|
||||
}
|
||||
_ = keyset
|
||||
// The returned `keyset` will always be "reasonably" new.
|
||||
//
|
||||
// By "reasonably" we mean that we cannot guarantee that the keys will be refreshed
|
||||
// immediately after it has been rotated in the remote source. But it should be close\
|
||||
// enough, and should you need to forcefully refresh the token using the `(jwk.Cache).Refresh()` method.
|
||||
//
|
||||
// If refetching the keyset fails, a cached version will be returned from the previous
|
||||
// successful sync
|
||||
|
||||
// Do interesting stuff with the keyset... but here, we just
|
||||
// sleep for a bit
|
||||
time.Sleep(time.Second)
|
||||
|
||||
// Because we're a dummy program, we just cancel the loop now.
|
||||
// If this were a real program, you presumably loop forever
|
||||
cancel()
|
||||
}
|
||||
// OUTPUT:
|
||||
}
|
||||
```
|
||||
source: [examples/jwk_cache_example_test.go](https://github.com/lestrrat-go/jwx/blob/v3/examples/jwk_cache_example_test.go)
|
||||
<!-- END INCLUDE -->
|
||||
|
||||
Parse and use a JWK key:
|
||||
|
||||
<!-- INCLUDE(examples/jwk_example_test.go) -->
|
||||
```go
|
||||
package examples_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/json"
|
||||
"github.com/lestrrat-go/jwx/v3/jwk"
|
||||
)
|
||||
|
||||
func Example_jwk_usage() {
|
||||
// Use jwk.Cache if you intend to keep reuse the JWKS over and over
|
||||
set, err := jwk.Fetch(context.Background(), "https://www.googleapis.com/oauth2/v3/certs")
|
||||
if err != nil {
|
||||
log.Printf("failed to parse JWK: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Key sets can be serialized back to JSON
|
||||
{
|
||||
jsonbuf, err := json.Marshal(set)
|
||||
if err != nil {
|
||||
log.Printf("failed to marshal key set into JSON: %s", err)
|
||||
return
|
||||
}
|
||||
log.Printf("%s", jsonbuf)
|
||||
}
|
||||
|
||||
for i := 0; i < set.Len(); i++ {
|
||||
var rawkey any // This is where we would like to store the raw key, like *rsa.PrivateKey or *ecdsa.PrivateKey
|
||||
key, ok := set.Key(i) // This retrieves the corresponding jwk.Key
|
||||
if !ok {
|
||||
log.Printf("failed to get key at index %d", i)
|
||||
return
|
||||
}
|
||||
|
||||
// jws and jwe operations can be performed using jwk.Key, but you could also
|
||||
// covert it to their "raw" forms, such as *rsa.PrivateKey or *ecdsa.PrivateKey
|
||||
if err := jwk.Export(key, &rawkey); err != nil {
|
||||
log.Printf("failed to create public key: %s", err)
|
||||
return
|
||||
}
|
||||
_ = rawkey
|
||||
|
||||
// You can create jwk.Key from a raw key, too
|
||||
fromRawKey, err := jwk.Import(rawkey)
|
||||
if err != nil {
|
||||
log.Printf("failed to acquire raw key from jwk.Key: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
// Keys can be serialized back to JSON
|
||||
jsonbuf, err := json.Marshal(key)
|
||||
if err != nil {
|
||||
log.Printf("failed to marshal key into JSON: %s", err)
|
||||
return
|
||||
}
|
||||
|
||||
fromJSONKey, err := jwk.Parse(jsonbuf)
|
||||
if err != nil {
|
||||
log.Printf("failed to parse json: %s", err)
|
||||
return
|
||||
}
|
||||
_ = fromJSONKey
|
||||
_ = fromRawKey
|
||||
}
|
||||
// OUTPUT:
|
||||
}
|
||||
|
||||
//nolint:govet
|
||||
func Example_jwk_marshal_json() {
|
||||
// JWKs that inherently involve randomness such as RSA and EC keys are
|
||||
// not used in this example, because they may produce different results
|
||||
// depending on the environment.
|
||||
//
|
||||
// (In fact, even if you use a static source of randomness, tests may fail
|
||||
// because of internal changes in the Go runtime).
|
||||
|
||||
raw := []byte("01234567890123456789012345678901234567890123456789ABCDEF")
|
||||
|
||||
// This would create a symmetric key
|
||||
key, err := jwk.Import(raw)
|
||||
if err != nil {
|
||||
fmt.Printf("failed to create symmetric key: %s\n", err)
|
||||
return
|
||||
}
|
||||
if _, ok := key.(jwk.SymmetricKey); !ok {
|
||||
fmt.Printf("expected jwk.SymmetricKey, got %T\n", key)
|
||||
return
|
||||
}
|
||||
|
||||
key.Set(jwk.KeyIDKey, "mykey")
|
||||
|
||||
buf, err := json.MarshalIndent(key, "", " ")
|
||||
if err != nil {
|
||||
fmt.Printf("failed to marshal key into JSON: %s\n", err)
|
||||
return
|
||||
}
|
||||
fmt.Printf("%s\n", buf)
|
||||
|
||||
// OUTPUT:
|
||||
// {
|
||||
// "k": "MDEyMzQ1Njc4OTAxMjM0NTY3ODkwMTIzNDU2Nzg5MDEyMzQ1Njc4OTAxMjM0NTY3ODlBQkNERUY",
|
||||
// "kid": "mykey",
|
||||
// "kty": "oct"
|
||||
// }
|
||||
}
|
||||
```
|
||||
source: [examples/jwk_example_test.go](https://github.com/lestrrat-go/jwx/blob/v3/examples/jwk_example_test.go)
|
||||
<!-- END INCLUDE -->
|
||||
+362
@@ -0,0 +1,362 @@
|
||||
package jwk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
|
||||
"github.com/lestrrat-go/httprc/v3"
|
||||
)
|
||||
|
||||
type HTTPClient = httprc.HTTPClient
|
||||
type ErrorSink = httprc.ErrorSink
|
||||
type TraceSink = httprc.TraceSink
|
||||
|
||||
// Cache is a container built on top of github.com/lestrrat-go/httprc/v3
|
||||
// that keeps track of Set object by their source URLs.
|
||||
// The Set objects are stored in memory, and are refreshed automatically
|
||||
// behind the scenes.
|
||||
//
|
||||
// Before retrieving the Set objects, the user must pre-register the
|
||||
// URLs they intend to use by calling `Register()`
|
||||
//
|
||||
// c := jwk.NewCache(ctx, httprc.NewClient())
|
||||
// c.Register(ctx, url, options...)
|
||||
//
|
||||
// Once registered, you can call `Get()` to retrieve the Set object.
|
||||
//
|
||||
// All JWKS objects that are retrieved via this mechanism should be
|
||||
// treated read-only, as they are shared among all consumers, as well
|
||||
// as the `jwk.Cache` object.
|
||||
//
|
||||
// There are cases where `jwk.Cache` and `jwk.CachedSet` should and
|
||||
// should not be used.
|
||||
//
|
||||
// First and foremost, do NOT use a cache for those JWKS objects that
|
||||
// need constant checking. For example, unreliable or user-provided JWKS (i.e. those
|
||||
// JWKS that are not from a well-known provider) should not be fetched
|
||||
// through a `jwk.Cache` or `jwk.CachedSet`.
|
||||
//
|
||||
// For example, if you have a flaky JWKS server for development
|
||||
// that can go down often, you should consider alternatives such as
|
||||
// providing `http.Client` with a caching `http.RoundTripper` configured
|
||||
// (see `jwk.WithHTTPClient`), setting up a reverse proxy, etc.
|
||||
// These techniques allow you to set up a more robust way to both cache
|
||||
// and report precise causes of the problems than using `jwk.Cache` or
|
||||
// `jwk.CachedSet`. If you handle the caching at the HTTP level like this,
|
||||
// you will be able to use a simple `jwk.Fetch` call and not worry about the cache.
|
||||
//
|
||||
// User-provided JWKS objects may also be problematic, as it may go down
|
||||
// unexpectedly (and frequently!), and it will be hard to detect when
|
||||
// the URLs or its contents are swapped.
|
||||
//
|
||||
// A good use-case for `jwk.Cache` and `jwk.CachedSet` are for "stable"
|
||||
// JWKS objects.
|
||||
//
|
||||
// When we say "stable", we are thinking of JWKS that should mostly be
|
||||
// ALWAYS available. A good example are those JWKS objects provided by
|
||||
// major cloud providers such as Google Cloud, AWS, or Azure.
|
||||
// Stable JWKS may still experience intermittent network connectivity problems,
|
||||
// but you can expect that they will eventually recover in relatively
|
||||
// short period of time. They rarely change URLs, and the contents are
|
||||
// expected to be valid or otherwise it would cause havoc to those providers
|
||||
//
|
||||
// We also know that these stable JWKS objects are rotated periodically,
|
||||
// which is a perfect use for `jwk.Cache` and `jwk.CachedSet`. The caches
|
||||
// can be configured to periodically refresh the JWKS thereby keeping them
|
||||
// fresh without extra intervention from the developer.
|
||||
//
|
||||
// Notice that for these recommended use-cases the requirement to check
|
||||
// the validity or the availability of the JWKS objects are non-existent,
|
||||
// as it is expected that they will be available and will be valid. The
|
||||
// caching mechanism can hide intermittent connectivity problems as well
|
||||
// as keep the objects mostly fresh.
|
||||
type Cache struct {
|
||||
ctrl httprc.Controller
|
||||
}
|
||||
|
||||
// Transformer is a specialized version of `httprc.Transformer` that implements
|
||||
// conversion from a `http.Response` object to a `jwk.Set` object. Use this in
|
||||
// conjection with `httprc.NewResource` to create a `httprc.Resource` object
|
||||
// to auto-update `jwk.Set` objects.
|
||||
type Transformer struct {
|
||||
parseOptions []ParseOption
|
||||
}
|
||||
|
||||
func (t Transformer) Transform(_ context.Context, res *http.Response) (Set, error) {
|
||||
buf, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to read response body status: %w`, err)
|
||||
}
|
||||
|
||||
set, err := Parse(buf, t.parseOptions...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to parse JWK set at %q: %w`, res.Request.URL.String(), err)
|
||||
}
|
||||
|
||||
return set, nil
|
||||
}
|
||||
|
||||
// NewCache creates a new `jwk.Cache` object.
|
||||
//
|
||||
// Under the hood, `jwk.Cache` uses `httprc.Client` manage the
|
||||
// fetching and caching of JWKS objects, and thus spawns multiple goroutines
|
||||
// per `jwk.Cache` object.
|
||||
//
|
||||
// The provided `httprc.Client` object must NOT be started prior to
|
||||
// passing it to `jwk.NewCache`. The `jwk.Cache` object will start
|
||||
// the `httprc.Client` object on its own.
|
||||
func NewCache(ctx context.Context, client *httprc.Client) (*Cache, error) {
|
||||
ctrl, err := client.Start(ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to start httprc.Client: %w`, err)
|
||||
}
|
||||
|
||||
return &Cache{
|
||||
ctrl: ctrl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Register registers a URL to be managed by the cache. URLs must
|
||||
// be registered before issuing `Get`
|
||||
//
|
||||
// The `Register` method is a thin wrapper around `(httprc.Controller).Add`
|
||||
func (c *Cache) Register(ctx context.Context, u string, options ...RegisterOption) error {
|
||||
var parseOptions []ParseOption
|
||||
var resourceOptions []httprc.NewResourceOption
|
||||
waitReady := true
|
||||
for _, option := range options {
|
||||
switch option := option.(type) {
|
||||
case ParseOption:
|
||||
parseOptions = append(parseOptions, option)
|
||||
case ResourceOption:
|
||||
var v httprc.NewResourceOption
|
||||
if err := option.Value(&v); err != nil {
|
||||
return fmt.Errorf(`failed to retrieve NewResourceOption option value: %w`, err)
|
||||
}
|
||||
resourceOptions = append(resourceOptions, v)
|
||||
default:
|
||||
switch option.Ident() {
|
||||
case identHTTPClient{}:
|
||||
var cli HTTPClient
|
||||
if err := option.Value(&cli); err != nil {
|
||||
return fmt.Errorf(`failed to retrieve HTTPClient option value: %w`, err)
|
||||
}
|
||||
resourceOptions = append(resourceOptions, httprc.WithHTTPClient(cli))
|
||||
case identWaitReady{}:
|
||||
if err := option.Value(&waitReady); err != nil {
|
||||
return fmt.Errorf(`failed to retrieve WaitReady option value: %w`, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
r, err := httprc.NewResource[Set](u, &Transformer{
|
||||
parseOptions: parseOptions,
|
||||
}, resourceOptions...)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to create httprc.Resource: %w`, err)
|
||||
}
|
||||
if err := c.ctrl.Add(ctx, r, httprc.WithWaitReady(waitReady)); err != nil {
|
||||
return fmt.Errorf(`failed to add resource to httprc.Client: %w`, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// LookupResource returns the `httprc.Resource` object associated with the
|
||||
// given URL `u`. If the URL has not been registered, an error is returned.
|
||||
func (c *Cache) LookupResource(ctx context.Context, u string) (*httprc.ResourceBase[Set], error) {
|
||||
r, err := c.ctrl.Lookup(ctx, u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to lookup resource %q: %w`, u, err)
|
||||
}
|
||||
//nolint:forcetypeassert
|
||||
return r.(*httprc.ResourceBase[Set]), nil
|
||||
}
|
||||
|
||||
func (c *Cache) Lookup(ctx context.Context, u string) (Set, error) {
|
||||
r, err := c.LookupResource(ctx, u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to lookup resource %q: %w`, u, err)
|
||||
}
|
||||
set := r.Resource()
|
||||
if set == nil {
|
||||
return nil, fmt.Errorf(`resource %q is not ready`, u)
|
||||
}
|
||||
return set, nil
|
||||
}
|
||||
|
||||
func (c *Cache) Ready(ctx context.Context, u string) bool {
|
||||
r, err := c.LookupResource(ctx, u)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if err := r.Ready(ctx); err != nil {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// Refresh is identical to Get(), except it always fetches the
|
||||
// specified resource anew, and updates the cached content
|
||||
//
|
||||
// Please refer to the documentation for `(httprc.Cache).Refresh` for
|
||||
// more details
|
||||
func (c *Cache) Refresh(ctx context.Context, u string) (Set, error) {
|
||||
if err := c.ctrl.Refresh(ctx, u); err != nil {
|
||||
return nil, fmt.Errorf(`failed to refresh resource %q: %w`, u, err)
|
||||
}
|
||||
return c.Lookup(ctx, u)
|
||||
}
|
||||
|
||||
// IsRegistered returns true if the given URL `u` has already been registered
|
||||
// in the cache.
|
||||
func (c *Cache) IsRegistered(ctx context.Context, u string) bool {
|
||||
_, err := c.LookupResource(ctx, u)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// Unregister removes the given URL `u` from the cache.
|
||||
func (c *Cache) Unregister(ctx context.Context, u string) error {
|
||||
return c.ctrl.Remove(ctx, u)
|
||||
}
|
||||
|
||||
func (c *Cache) Shutdown(ctx context.Context) error {
|
||||
return c.ctrl.ShutdownContext(ctx)
|
||||
}
|
||||
|
||||
// CachedSet is a thin shim over jwk.Cache that allows the user to cloak
|
||||
// jwk.Cache as if it's a `jwk.Set`. Behind the scenes, the `jwk.Set` is
|
||||
// retrieved from the `jwk.Cache` for every operation.
|
||||
//
|
||||
// Since `jwk.CachedSet` always deals with a cached version of the `jwk.Set`,
|
||||
// all operations that mutate the object (such as AddKey(), RemoveKey(), et. al)
|
||||
// are no-ops and return an error.
|
||||
//
|
||||
// Note that since this is a utility shim over `jwk.Cache`, you _will_ lose
|
||||
// the ability to control the finer details (such as controlling how long to
|
||||
// wait for in case of a fetch failure using `context.Context`)
|
||||
//
|
||||
// Make sure that you read the documentation for `jwk.Cache` as well.
|
||||
type CachedSet interface {
|
||||
Set
|
||||
cached() (Set, error) // used as a marker
|
||||
}
|
||||
|
||||
type cachedSet struct {
|
||||
r *httprc.ResourceBase[Set]
|
||||
}
|
||||
|
||||
func (c *Cache) CachedSet(u string) (CachedSet, error) {
|
||||
r, err := c.LookupResource(context.Background(), u)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to lookup resource %q: %w`, u, err)
|
||||
}
|
||||
return NewCachedSet(r), nil
|
||||
}
|
||||
|
||||
func NewCachedSet(r *httprc.ResourceBase[Set]) CachedSet {
|
||||
return &cachedSet{
|
||||
r: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (cs *cachedSet) cached() (Set, error) {
|
||||
if err := cs.r.Ready(context.Background()); err != nil {
|
||||
return nil, fmt.Errorf(`failed to fetch resource: %w`, err)
|
||||
}
|
||||
return cs.r.Resource(), nil
|
||||
}
|
||||
|
||||
// AddKey is a no-op for `jwk.CachedSet`, as the `jwk.Set` should be treated read-only
|
||||
func (*cachedSet) AddKey(_ Key) error {
|
||||
return fmt.Errorf(`(jwk.Cachedset).AddKey: jwk.CachedSet is immutable`)
|
||||
}
|
||||
|
||||
// Clear is a no-op for `jwk.CachedSet`, as the `jwk.Set` should be treated read-only
|
||||
func (*cachedSet) Clear() error {
|
||||
return fmt.Errorf(`(jwk.cachedSet).Clear: jwk.CachedSet is immutable`)
|
||||
}
|
||||
|
||||
// Set is a no-op for `jwk.CachedSet`, as the `jwk.Set` should be treated read-only
|
||||
func (*cachedSet) Set(_ string, _ any) error {
|
||||
return fmt.Errorf(`(jwk.cachedSet).Set: jwk.CachedSet is immutable`)
|
||||
}
|
||||
|
||||
// Remove is a no-op for `jwk.CachedSet`, as the `jwk.Set` should be treated read-only
|
||||
func (*cachedSet) Remove(_ string) error {
|
||||
// TODO: Remove() should be renamed to Remove(string) error
|
||||
return fmt.Errorf(`(jwk.cachedSet).Remove: jwk.CachedSet is immutable`)
|
||||
}
|
||||
|
||||
// RemoveKey is a no-op for `jwk.CachedSet`, as the `jwk.Set` should be treated read-only
|
||||
func (*cachedSet) RemoveKey(_ Key) error {
|
||||
return fmt.Errorf(`(jwk.cachedSet).RemoveKey: jwk.CachedSet is immutable`)
|
||||
}
|
||||
|
||||
func (cs *cachedSet) Clone() (Set, error) {
|
||||
set, err := cs.cached()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to get cached jwk.Set: %w`, err)
|
||||
}
|
||||
|
||||
return set.Clone()
|
||||
}
|
||||
|
||||
// Get returns the value of non-Key field stored in the jwk.Set
|
||||
func (cs *cachedSet) Get(name string, dst any) error {
|
||||
set, err := cs.cached()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return set.Get(name, dst)
|
||||
}
|
||||
|
||||
// Key returns the Key at the specified index
|
||||
func (cs *cachedSet) Key(idx int) (Key, bool) {
|
||||
set, err := cs.cached()
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return set.Key(idx)
|
||||
}
|
||||
|
||||
func (cs *cachedSet) Index(key Key) int {
|
||||
set, err := cs.cached()
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
return set.Index(key)
|
||||
}
|
||||
|
||||
func (cs *cachedSet) Keys() []string {
|
||||
set, err := cs.cached()
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
return set.Keys()
|
||||
}
|
||||
|
||||
func (cs *cachedSet) Len() int {
|
||||
set, err := cs.cached()
|
||||
if err != nil {
|
||||
return -1
|
||||
}
|
||||
|
||||
return set.Len()
|
||||
}
|
||||
|
||||
func (cs *cachedSet) LookupKeyID(kid string) (Key, bool) {
|
||||
set, err := cs.cached()
|
||||
if err != nil {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return set.LookupKeyID(kid)
|
||||
}
|
||||
+399
@@ -0,0 +1,399 @@
|
||||
package jwk
|
||||
|
||||
import (
|
||||
"crypto/ecdh"
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/elliptic"
|
||||
"crypto/rsa"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/lestrrat-go/blackmagic"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/ecutil"
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
)
|
||||
|
||||
// # Converting between Raw Keys and `jwk.Key`s
|
||||
//
|
||||
// A converter that converts from a raw key to a `jwk.Key` is called a KeyImporter.
|
||||
// A converter that converts from a `jwk.Key` to a raw key is called a KeyExporter.
|
||||
|
||||
var keyImporters = make(map[reflect.Type]KeyImporter)
|
||||
var keyExporters = make(map[jwa.KeyType][]KeyExporter)
|
||||
|
||||
var muKeyImporters sync.RWMutex
|
||||
var muKeyExporters sync.RWMutex
|
||||
|
||||
// RegisterKeyImporter registers a KeyImporter for the given raw key. When `jwk.Import()` is called,
|
||||
// the library will look up the appropriate KeyImporter for the given raw key type (via `reflect`)
|
||||
// and execute the KeyImporters in succession until either one of them succeeds, or all of them fail.
|
||||
func RegisterKeyImporter(from any, conv KeyImporter) {
|
||||
muKeyImporters.Lock()
|
||||
defer muKeyImporters.Unlock()
|
||||
keyImporters[reflect.TypeOf(from)] = conv
|
||||
}
|
||||
|
||||
// RegisterKeyExporter registers a KeyExporter for the given key type. When `key.Raw()` is called,
|
||||
// the library will look up the appropriate KeyExporter for the given key type and execute the
|
||||
// KeyExporters in succession until either one of them succeeds, or all of them fail.
|
||||
func RegisterKeyExporter(kty jwa.KeyType, conv KeyExporter) {
|
||||
muKeyExporters.Lock()
|
||||
defer muKeyExporters.Unlock()
|
||||
convs, ok := keyExporters[kty]
|
||||
if !ok {
|
||||
convs = []KeyExporter{conv}
|
||||
} else {
|
||||
convs = append([]KeyExporter{conv}, convs...)
|
||||
}
|
||||
keyExporters[kty] = convs
|
||||
}
|
||||
|
||||
// KeyImporter is used to convert from a raw key to a `jwk.Key`. mneumonic: from the PoV of the `jwk.Key`,
|
||||
// we're _importing_ a raw key.
|
||||
type KeyImporter interface {
|
||||
// Import takes the raw key to be converted, and returns a `jwk.Key` or an error if the conversion fails.
|
||||
Import(any) (Key, error)
|
||||
}
|
||||
|
||||
// KeyImportFunc is a convenience type to implement KeyImporter as a function.
|
||||
type KeyImportFunc func(any) (Key, error)
|
||||
|
||||
func (f KeyImportFunc) Import(raw any) (Key, error) {
|
||||
return f(raw)
|
||||
}
|
||||
|
||||
// KeyExporter is used to convert from a `jwk.Key` to a raw key. mneumonic: from the PoV of the `jwk.Key`,
|
||||
// we're _exporting_ it to a raw key.
|
||||
type KeyExporter interface {
|
||||
// Export takes the `jwk.Key` to be converted, and a hint (the raw key to be converted to).
|
||||
// The hint is the object that the user requested the result to be assigned to.
|
||||
// The method should return the converted raw key, or an error if the conversion fails.
|
||||
//
|
||||
// Third party modules MUST NOT modifiy the hint object.
|
||||
//
|
||||
// When the user calls `key.Export(dst)`, the `dst` object is a _pointer_ to the
|
||||
// object that the user wants the result to be assigned to, but the converter
|
||||
// receives the _value_ that this pointer points to, to make it easier to
|
||||
// detect the type of the result.
|
||||
//
|
||||
// Note that the second argument may be an `any` (which means that the
|
||||
// user has delegated the type detection to the converter).
|
||||
//
|
||||
// Export must NOT modify the hint object, and should return jwk.ContinueError
|
||||
// if the hint object is not compatible with the converter.
|
||||
Export(Key, any) (any, error)
|
||||
}
|
||||
|
||||
// KeyExportFunc is a convenience type to implement KeyExporter as a function.
|
||||
type KeyExportFunc func(Key, any) (any, error)
|
||||
|
||||
func (f KeyExportFunc) Export(key Key, hint any) (any, error) {
|
||||
return f(key, hint)
|
||||
}
|
||||
|
||||
func init() {
|
||||
{
|
||||
f := KeyImportFunc(rsaPrivateKeyToJWK)
|
||||
k := rsa.PrivateKey{}
|
||||
RegisterKeyImporter(k, f)
|
||||
RegisterKeyImporter(&k, f)
|
||||
}
|
||||
{
|
||||
f := KeyImportFunc(rsaPublicKeyToJWK)
|
||||
k := rsa.PublicKey{}
|
||||
RegisterKeyImporter(k, f)
|
||||
RegisterKeyImporter(&k, f)
|
||||
}
|
||||
{
|
||||
f := KeyImportFunc(ecdsaPrivateKeyToJWK)
|
||||
k := ecdsa.PrivateKey{}
|
||||
RegisterKeyImporter(k, f)
|
||||
RegisterKeyImporter(&k, f)
|
||||
}
|
||||
{
|
||||
f := KeyImportFunc(ecdsaPublicKeyToJWK)
|
||||
k := ecdsa.PublicKey{}
|
||||
RegisterKeyImporter(k, f)
|
||||
RegisterKeyImporter(&k, f)
|
||||
}
|
||||
{
|
||||
f := KeyImportFunc(okpPrivateKeyToJWK)
|
||||
for _, k := range []any{ed25519.PrivateKey(nil)} {
|
||||
RegisterKeyImporter(k, f)
|
||||
}
|
||||
}
|
||||
{
|
||||
f := KeyImportFunc(ecdhPrivateKeyToJWK)
|
||||
for _, k := range []any{ecdh.PrivateKey{}, &ecdh.PrivateKey{}} {
|
||||
RegisterKeyImporter(k, f)
|
||||
}
|
||||
}
|
||||
{
|
||||
f := KeyImportFunc(okpPublicKeyToJWK)
|
||||
for _, k := range []any{ed25519.PublicKey(nil)} {
|
||||
RegisterKeyImporter(k, f)
|
||||
}
|
||||
}
|
||||
{
|
||||
f := KeyImportFunc(ecdhPublicKeyToJWK)
|
||||
for _, k := range []any{ecdh.PublicKey{}, &ecdh.PublicKey{}} {
|
||||
RegisterKeyImporter(k, f)
|
||||
}
|
||||
}
|
||||
RegisterKeyImporter([]byte(nil), KeyImportFunc(bytesToKey))
|
||||
}
|
||||
|
||||
func ecdhPrivateKeyToJWK(src any) (Key, error) {
|
||||
var raw *ecdh.PrivateKey
|
||||
switch src := src.(type) {
|
||||
case *ecdh.PrivateKey:
|
||||
raw = src
|
||||
case ecdh.PrivateKey:
|
||||
raw = &src
|
||||
default:
|
||||
return nil, fmt.Errorf(`cannot convert key type '%T' to ECDH jwk.Key`, src)
|
||||
}
|
||||
|
||||
switch raw.Curve() {
|
||||
case ecdh.X25519():
|
||||
return okpPrivateKeyToJWK(raw)
|
||||
case ecdh.P256():
|
||||
return ecdhPrivateKeyToECJWK(raw, elliptic.P256())
|
||||
case ecdh.P384():
|
||||
return ecdhPrivateKeyToECJWK(raw, elliptic.P384())
|
||||
case ecdh.P521():
|
||||
return ecdhPrivateKeyToECJWK(raw, elliptic.P521())
|
||||
default:
|
||||
return nil, fmt.Errorf(`unsupported curve %s`, raw.Curve())
|
||||
}
|
||||
}
|
||||
|
||||
func ecdhPrivateKeyToECJWK(raw *ecdh.PrivateKey, crv elliptic.Curve) (Key, error) {
|
||||
pub := raw.PublicKey()
|
||||
rawpub := pub.Bytes()
|
||||
|
||||
size := ecutil.CalculateKeySize(crv)
|
||||
var x, y, d big.Int
|
||||
x.SetBytes(rawpub[1 : 1+size])
|
||||
y.SetBytes(rawpub[1+size:])
|
||||
d.SetBytes(raw.Bytes())
|
||||
|
||||
var ecdsaPriv ecdsa.PrivateKey
|
||||
ecdsaPriv.Curve = crv
|
||||
ecdsaPriv.D = &d
|
||||
ecdsaPriv.X = &x
|
||||
ecdsaPriv.Y = &y
|
||||
return ecdsaPrivateKeyToJWK(&ecdsaPriv)
|
||||
}
|
||||
|
||||
func ecdhPublicKeyToJWK(src any) (Key, error) {
|
||||
var raw *ecdh.PublicKey
|
||||
switch src := src.(type) {
|
||||
case *ecdh.PublicKey:
|
||||
raw = src
|
||||
case ecdh.PublicKey:
|
||||
raw = &src
|
||||
default:
|
||||
return nil, fmt.Errorf(`cannot convert key type '%T' to ECDH jwk.Key`, src)
|
||||
}
|
||||
|
||||
switch raw.Curve() {
|
||||
case ecdh.X25519():
|
||||
return okpPublicKeyToJWK(raw)
|
||||
case ecdh.P256():
|
||||
return ecdhPublicKeyToECJWK(raw, elliptic.P256())
|
||||
case ecdh.P384():
|
||||
return ecdhPublicKeyToECJWK(raw, elliptic.P384())
|
||||
case ecdh.P521():
|
||||
return ecdhPublicKeyToECJWK(raw, elliptic.P521())
|
||||
default:
|
||||
return nil, fmt.Errorf(`unsupported curve %s`, raw.Curve())
|
||||
}
|
||||
}
|
||||
|
||||
func ecdhPublicKeyToECJWK(raw *ecdh.PublicKey, crv elliptic.Curve) (Key, error) {
|
||||
rawbytes := raw.Bytes()
|
||||
size := ecutil.CalculateKeySize(crv)
|
||||
var x, y big.Int
|
||||
|
||||
x.SetBytes(rawbytes[1 : 1+size])
|
||||
y.SetBytes(rawbytes[1+size:])
|
||||
var ecdsaPriv ecdsa.PublicKey
|
||||
ecdsaPriv.Curve = crv
|
||||
ecdsaPriv.X = &x
|
||||
ecdsaPriv.Y = &y
|
||||
return ecdsaPublicKeyToJWK(&ecdsaPriv)
|
||||
}
|
||||
|
||||
// These may seem a bit repetitive and redandunt, but the problem is that
|
||||
// each key type has its own Import method -- for example, Import(*ecdsa.PrivateKey)
|
||||
// vs Import(*rsa.PrivateKey), and therefore they can't just be bundled into
|
||||
// a single function.
|
||||
func rsaPrivateKeyToJWK(src any) (Key, error) {
|
||||
var raw *rsa.PrivateKey
|
||||
switch src := src.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
raw = src
|
||||
case rsa.PrivateKey:
|
||||
raw = &src
|
||||
default:
|
||||
return nil, fmt.Errorf(`cannot convert key type '%T' to RSA jwk.Key`, src)
|
||||
}
|
||||
k := newRSAPrivateKey()
|
||||
if err := k.Import(raw); err != nil {
|
||||
return nil, fmt.Errorf(`failed to initialize %T from %T: %w`, k, raw, err)
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
|
||||
func rsaPublicKeyToJWK(src any) (Key, error) {
|
||||
var raw *rsa.PublicKey
|
||||
switch src := src.(type) {
|
||||
case *rsa.PublicKey:
|
||||
raw = src
|
||||
case rsa.PublicKey:
|
||||
raw = &src
|
||||
default:
|
||||
return nil, fmt.Errorf(`cannot convert key type '%T' to RSA jwk.Key`, src)
|
||||
}
|
||||
k := newRSAPublicKey()
|
||||
if err := k.Import(raw); err != nil {
|
||||
return nil, fmt.Errorf(`failed to initialize %T from %T: %w`, k, raw, err)
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
|
||||
func ecdsaPrivateKeyToJWK(src any) (Key, error) {
|
||||
var raw *ecdsa.PrivateKey
|
||||
switch src := src.(type) {
|
||||
case *ecdsa.PrivateKey:
|
||||
raw = src
|
||||
case ecdsa.PrivateKey:
|
||||
raw = &src
|
||||
default:
|
||||
return nil, fmt.Errorf(`cannot convert key type '%T' to ECDSA jwk.Key`, src)
|
||||
}
|
||||
k := newECDSAPrivateKey()
|
||||
if err := k.Import(raw); err != nil {
|
||||
return nil, fmt.Errorf(`failed to initialize %T from %T: %w`, k, raw, err)
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
|
||||
func ecdsaPublicKeyToJWK(src any) (Key, error) {
|
||||
var raw *ecdsa.PublicKey
|
||||
switch src := src.(type) {
|
||||
case *ecdsa.PublicKey:
|
||||
raw = src
|
||||
case ecdsa.PublicKey:
|
||||
raw = &src
|
||||
default:
|
||||
return nil, fmt.Errorf(`cannot convert key type '%T' to ECDSA jwk.Key`, src)
|
||||
}
|
||||
k := newECDSAPublicKey()
|
||||
if err := k.Import(raw); err != nil {
|
||||
return nil, fmt.Errorf(`failed to initialize %T from %T: %w`, k, raw, err)
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
|
||||
func okpPrivateKeyToJWK(src any) (Key, error) {
|
||||
var raw any
|
||||
switch src.(type) {
|
||||
case ed25519.PrivateKey, *ecdh.PrivateKey:
|
||||
raw = src
|
||||
case ecdh.PrivateKey:
|
||||
raw = &src
|
||||
default:
|
||||
return nil, fmt.Errorf(`cannot convert key type '%T' to OKP jwk.Key`, src)
|
||||
}
|
||||
k := newOKPPrivateKey()
|
||||
if err := k.Import(raw); err != nil {
|
||||
return nil, fmt.Errorf(`failed to initialize %T from %T: %w`, k, raw, err)
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
|
||||
func okpPublicKeyToJWK(src any) (Key, error) {
|
||||
var raw any
|
||||
switch src.(type) {
|
||||
case ed25519.PublicKey, *ecdh.PublicKey:
|
||||
raw = src
|
||||
case ecdh.PublicKey:
|
||||
raw = &src
|
||||
default:
|
||||
return nil, fmt.Errorf(`jwk: convert raw to OKP jwk.Key: cannot convert key type '%T' to OKP jwk.Key`, src)
|
||||
}
|
||||
k := newOKPPublicKey()
|
||||
if err := k.Import(raw); err != nil {
|
||||
return nil, fmt.Errorf(`failed to initialize %T from %T: %w`, k, raw, err)
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
|
||||
func bytesToKey(src any) (Key, error) {
|
||||
var raw []byte
|
||||
switch src := src.(type) {
|
||||
case []byte:
|
||||
raw = src
|
||||
default:
|
||||
return nil, fmt.Errorf(`cannot convert key type '%T' to symmetric jwk.Key`, src)
|
||||
}
|
||||
|
||||
k := newSymmetricKey()
|
||||
if err := k.Import(raw); err != nil {
|
||||
return nil, fmt.Errorf(`failed to initialize %T from %T: %w`, k, raw, err)
|
||||
}
|
||||
return k, nil
|
||||
}
|
||||
|
||||
// Export converts a `jwk.Key` to a Export key. The dst argument must be a pointer to the
|
||||
// object that the user wants the result to be assigned to.
|
||||
//
|
||||
// Normally you would pass a pointer to the zero value of the raw key type
|
||||
// such as &(*rsa.PrivateKey) or &(*ecdsa.PublicKey), which gets assigned
|
||||
// the converted key.
|
||||
//
|
||||
// If you do not know the exact type of a jwk.Key before attempting
|
||||
// to obtain the raw key, you can simply pass a pointer to an
|
||||
// empty interface as the second argument
|
||||
//
|
||||
// If you already know the exact type, it is recommended that you
|
||||
// pass a pointer to the zero value of the actual key type for efficiency.
|
||||
//
|
||||
// Be careful when/if you are using a third party key type that implements
|
||||
// the `jwk.Key` interface, as the first argument. This function tries hard
|
||||
// to Do The Right Thing, but it is not guaranteed to work in all cases,
|
||||
// especially when the object implements the `jwk.Key` interface via
|
||||
// embedding.
|
||||
func Export(key Key, dst any) error {
|
||||
// dst better be a pointer
|
||||
rv := reflect.ValueOf(dst)
|
||||
if rv.Kind() != reflect.Ptr {
|
||||
return fmt.Errorf(`jwk.Export: destination object must be a pointer`)
|
||||
}
|
||||
muKeyExporters.RLock()
|
||||
exporters, ok := keyExporters[key.KeyType()]
|
||||
muKeyExporters.RUnlock()
|
||||
if !ok {
|
||||
return fmt.Errorf(`jwk.Export: no exporters registered for key type '%T'`, key)
|
||||
}
|
||||
for _, conv := range exporters {
|
||||
v, err := conv.Export(key, dst)
|
||||
if err != nil {
|
||||
if errors.Is(err, ContinueError()) {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf(`jwk.Export: failed to export jwk.Key to raw format: %w`, err)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, v); err != nil {
|
||||
return fmt.Errorf(`jwk.Export: failed to assign key: %w`, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`jwk.Export: no suitable exporter found for key type '%T'`, key)
|
||||
}
|
||||
+294
@@ -0,0 +1,294 @@
|
||||
// Package jwk implements JWK as described in https://tools.ietf.org/html/rfc7517
|
||||
//
|
||||
// This package implements jwk.Key to represent a single JWK, and jwk.Set to represent
|
||||
// a set of JWKs.
|
||||
//
|
||||
// The `jwk.Key` type is an interface, which hides the underlying implementation for
|
||||
// each key type. Each key type can further be converted to interfaces for known
|
||||
// types, such as `jwk.ECDSAPrivateKey`, `jwk.RSAPublicKey`, etc. This may not necessarily
|
||||
// work for third party key types (see section on "Registering a key type" below).
|
||||
//
|
||||
// Users can create a JWK in two ways. One is to unmarshal a JSON representation of a
|
||||
// key. The second one is to use `jwk.Import()` to import a raw key and convert it to
|
||||
// a jwk.Key.
|
||||
//
|
||||
// # Simple Usage
|
||||
//
|
||||
// You can parse a JWK from a JSON payload:
|
||||
//
|
||||
// jwk.ParseKey([]byte(`{"kty":"EC",...}`))
|
||||
//
|
||||
// You can go back and forth between raw key types and JWKs:
|
||||
//
|
||||
// jwkKey, _ := jwk.Import(rsaPrivateKey)
|
||||
// var rawKey *rsa.PRrivateKey
|
||||
// jwkKey.Raw(&rawKey)
|
||||
//
|
||||
// You can use them to sign/verify/encrypt/decrypt:
|
||||
//
|
||||
// jws.Sign([]byte(`...`), jws.WithKey(jwa.RS256, jwkKey))
|
||||
// jwe.Encrypt([]byte(`...`), jwe.WithKey(jwa.RSA_OAEP, jwkKey))
|
||||
//
|
||||
// See examples/jwk_parse_example_test.go and other files in the exmaples/ directory for more.
|
||||
//
|
||||
// # Advanced Usage: Registering a custom key type and conversion routines
|
||||
//
|
||||
// Caveat Emptor: Functionality around registering keys
|
||||
// (KeyProbe/KeyParser/KeyImporter/KeyExporter) should be considered experimental.
|
||||
// While we expect that the functionality itself will remain, the API may
|
||||
// change in backward incompatible ways, even during minor version
|
||||
// releases.
|
||||
//
|
||||
// ## tl;dr
|
||||
//
|
||||
// * KeyProbe: Used for parsing JWKs in JSON format. Probes hint fields to be used for later parsing by KeyParser
|
||||
// * KeyParser: Used for parsing JWKs in JSON format. Parses the JSON payload into a jwk.Key using the KeyProbe as hint
|
||||
// * KeyImporter: Used for converting raw key into jwk.Key.
|
||||
// * KeyExporter: Used for converting jwk.Key into raw key.
|
||||
//
|
||||
// ## Overview
|
||||
//
|
||||
// You can add the ability to use a JWK type that this library does not
|
||||
// implement out of the box. You can do this by registering your own
|
||||
// KeyParser, KeyImporter, and KeyExporter instances.
|
||||
//
|
||||
// func init() {
|
||||
// jwk.RegiserProbeField(reflect.StructField{Name: "SomeHint", Type: reflect.TypeOf(""), Tag: `json:"some_hint"`})
|
||||
// jwk.RegisterKeyParser(&MyKeyParser{})
|
||||
// jwk.RegisterKeyImporter(&MyKeyImporter{})
|
||||
// jwk.RegisterKeyExporter(&MyKeyExporter{})
|
||||
// }
|
||||
//
|
||||
// The KeyParser is used to parse JSON payloads and conver them into a jwk.Key.
|
||||
// The KeyImporter is used to convert a raw key (e.g. *rsa.PrivateKey, *ecdsa.PrivateKey, etc) into a jwk.Key.
|
||||
// The KeyExporter is used to convert a jwk.Key into a raw key.
|
||||
//
|
||||
// Although we believe the mechanism has been streamline quite a lot, it is also true
|
||||
// that the entire process of parsing and converting keys are much more convoluted than you might
|
||||
// think. Please know before hand that if you intend to add support for a new key type,
|
||||
// it _WILL_ require you to learn this module pretty much in-and-out.
|
||||
//
|
||||
// Read on for more explanation.
|
||||
//
|
||||
// ## Registering a KeyParser
|
||||
//
|
||||
// In order to understand how parsing works, we need to explain how the `jwk.ParseKey()` works.
|
||||
//
|
||||
// The first thing that occurs when parsing a key is a partial
|
||||
// unmarshaling of the payload into a hint / probe object.
|
||||
//
|
||||
// Because the `json.Unmarshal` works by calling the `UnmarshalJSON`
|
||||
// method on a concrete object, we need to create a concrete object first.
|
||||
// In order/ to create the appropriate Go object, we need to know which concrete
|
||||
// object to create from the JSON payload, meaning we need to peek into the
|
||||
// payload and figure out what type of key it is.
|
||||
//
|
||||
// In order to do this, we effectively need to parse the JSON payload twice.
|
||||
// First, we "probe" the payload to figure out what kind of key it is, then
|
||||
// we parse it again to create the actual key object.
|
||||
//
|
||||
// For probing, we create a new "probe" object (KeyProbe, which is not
|
||||
// directly available to end users) to populate the object with hints from the payload.
|
||||
// For example, a JWK representing an RSA key would look like:
|
||||
//
|
||||
// { "kty": "RSA", "n": ..., "e": ..., ... }
|
||||
//
|
||||
// The default KeyProbe is constructed to unmarshal "kty" and "d" fields,
|
||||
// because that is enough information to determine what kind of key to
|
||||
// construct.
|
||||
//
|
||||
// For example, if the payload contains "kty" field with the value "RSA",
|
||||
// we know that it's an RSA key. If it contains "EC", we know that it's
|
||||
// an EC key. Furthermore, if the payload contains some value in the "d" field, we can
|
||||
// also tell that this is a private key, as only private keys need
|
||||
// this field.
|
||||
//
|
||||
// For most cases, the default KeyProbe implementation should be sufficient.
|
||||
// However, there may be cases in the future where there are new key types
|
||||
// that require further information. Perhaps you are embedding another hint
|
||||
// in your JWK to further specify what kind of key it is. In that case, you
|
||||
// would need to probe more.
|
||||
//
|
||||
// Normally you can only change how an object is unmarshaled by specifying
|
||||
// JSON tags when defining a struct, but we use `reflect` package capabilities
|
||||
// to create an object dynamically, which is shared among all parsing operations.
|
||||
//
|
||||
// To add a new field to be probed, you need to register a new `reflect.StructField`
|
||||
// object that has all of the information. For example, the code below would
|
||||
// register a field named "MyHint" that is of type string, and has a JSON tag
|
||||
// of "my_hint".
|
||||
//
|
||||
// jwk.RegisterProbeField(reflect.StructField{Name: "MyHint", Type: reflect.TypeOf(""), Tag: `json:"my_hint"`})
|
||||
//
|
||||
// The value of this field can be retrieved by calling `Get()` method on the
|
||||
// KeyProbe object (from the `KeyParser`'s `ParseKey()` method discussed later)
|
||||
//
|
||||
// var myhint string
|
||||
// _ = probe.Get("MyHint", &myhint)
|
||||
//
|
||||
// var kty string
|
||||
// _ = probe.Get("Kty", &kty)
|
||||
//
|
||||
// This mechanism allows you to be flexible when trying to determine the key type
|
||||
// to instantiate.
|
||||
//
|
||||
// ## Parse via the KeyParser
|
||||
//
|
||||
// When `jwk.Parse` / `jwk.ParseKey` is called, the library will first probe
|
||||
// the payload as discussed above.
|
||||
//
|
||||
// Once the probe is done, the library will iterate over the registered parsers
|
||||
// and attempt to parse the key by calling their `ParseKey()` methods.
|
||||
//
|
||||
// The parsers will be called in reverse order that they were registered.
|
||||
// This means that it will try all parsers that were registered by third
|
||||
// parties, and once those are exhausted, the default parser will be used.
|
||||
//
|
||||
// Each parser's `ParseKey()“ method will receive three arguments: the probe object, a
|
||||
// KeyUnmarshaler, and the raw payload. The probe object can be used
|
||||
// as a hint to determine what kind of key to instantiate. An example
|
||||
// pseudocode may look like this:
|
||||
//
|
||||
// var kty string
|
||||
// _ = probe.Get("Kty", &kty)
|
||||
// switch kty {
|
||||
// case "RSA":
|
||||
// // create an RSA key
|
||||
// case "EC":
|
||||
// // create an EC key
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
// The `KeyUnmarshaler` is a thin wrapper around `json.Unmarshal`. It works almost
|
||||
// identical to `json.Unmarshal`, but it allows us to add extra magic that is
|
||||
// specific to this library (which users do not need to be aware of) before calling
|
||||
// the actual `json.Unmarshal`. Please use the `KeyUnmarshaler` to unmarshal JWKs instead of `json.Unmarshal`.
|
||||
//
|
||||
// Putting it all together, the boiler plate for registering a new parser may look like this:
|
||||
//
|
||||
// func init() {
|
||||
// jwk.RegisterFieldProbe(reflect.StructField{Name: "MyHint", Type: reflect.TypeOf(""), Tag: `json:"my_hint"`})
|
||||
// jwk.RegisterParser(&MyKeyParser{})
|
||||
// }
|
||||
//
|
||||
// type MyKeyParser struct { ... }
|
||||
// func(*MyKeyParser) ParseKey(rawProbe *KeyProbe, unmarshaler KeyUnmarshaler, data []byte) (jwk.Key, error) {
|
||||
// // Create concrete type
|
||||
// var hint string
|
||||
// if err := probe.Get("MyHint", &hint); err != nil {
|
||||
// // if it doesn't have the `my_hint` field, it probably means
|
||||
// // it's not for us, so we return ContinueParseError so that
|
||||
// // the next parser can pick it up
|
||||
// return nil, jwk.ContinueParseError()
|
||||
// }
|
||||
//
|
||||
// // Use hint to determine concrete key type
|
||||
// var key jwk.Key
|
||||
// switch hint {
|
||||
// case ...:
|
||||
// key = = myNewAwesomeJWK()
|
||||
// ...
|
||||
// }
|
||||
//
|
||||
// return unmarshaler.Unmarshal(data, key)
|
||||
// }
|
||||
//
|
||||
// ## Registering KeyImporter/KeyExporter
|
||||
//
|
||||
// If you are going to do anything with the key that was parsed by your KeyParser,
|
||||
// you will need to tell the library how to convert back and forth between
|
||||
// raw keys and JWKs. Conversion from raw keys to jwk.Keys are done by KeyImporters,
|
||||
// and conversion from jwk.Keys to raw keys are done by KeyExporters.
|
||||
//
|
||||
// ## Using jwk.Import() using KeyImporter
|
||||
//
|
||||
// Each KeyImporter is hooked to run against a specific raw key type.
|
||||
//
|
||||
// When `jwk.Import()` is called, the library will iterate over all registered
|
||||
// KeyImporters for the specified raw key type, and attempt to convert the raw
|
||||
// key to a JWK by calling the `Import()` method on each KeyImporter.
|
||||
//
|
||||
// The KeyImporter's `Import()` method will receive the raw key to be converted,
|
||||
// and should return a JWK or an error if the conversion fails, or the return
|
||||
// `jwk.ContinueError()` if the specified raw key cannot be handled by ths/ KeyImporter.
|
||||
//
|
||||
// Once a KeyImporter is available, you will be able to pass the raw key to `jwk.Import()`.
|
||||
// The following example shows how you might register a KeyImporter for a hypotheical
|
||||
// mypkg.SuperSecretKey:
|
||||
//
|
||||
// jwk.RegisterKeyImporter(&mypkg.SuperSecretKey{}, jwk.KeyImportFunc(imnportSuperSecretKey))
|
||||
//
|
||||
// func importSuperSecretKey(key any) (jwk.Key, error) {
|
||||
// mykey, ok := key.(*mypkg.SuperSecretKey)
|
||||
// if !ok {
|
||||
// // You must return jwk.ContinueError here, or otherwise
|
||||
// // processing will stop with an error
|
||||
// return nil, fmt.Errorf("invalid key type %T for importer: %w", key, jwk.ContinueError())
|
||||
// }
|
||||
//
|
||||
// return mypkg.SuperSecretJWK{ .... }, nil // You could reuse existing JWK types if you can
|
||||
// }
|
||||
//
|
||||
// ## Registering a KeyExporter
|
||||
//
|
||||
// KeyExporters are the opposite of KeyImporters: they convert a JWK to a raw key when `key.Raw(...)` is
|
||||
// called. If you intend to use `key.Raw(...)` for a JWK created using one of your KeyImporters,
|
||||
// you will also
|
||||
//
|
||||
// KeyExporters are registered by key type. For example, if you want to register a KeyExporter for
|
||||
// RSA keys, you would do:
|
||||
//
|
||||
// jwk.RegisterKeyExporter(jwa.RSA, jwk.KeyExportFunc(exportRSAKey))
|
||||
//
|
||||
// For a given JWK, it will be passed a "destination" object to store the exported raw key. For example,
|
||||
// an RSA-based private JWK can be exported to a `*rsa.PrivateKey` or to a `*any`, but not
|
||||
// to a `*ecdsa.PrivateKey`:
|
||||
//
|
||||
// var dst *rsa.PrivateKey
|
||||
// key.Raw(&dst) // OK
|
||||
//
|
||||
// var dst any
|
||||
// key.Raw(&dst) // OK
|
||||
//
|
||||
// var dst *ecdsa.PrivateKey
|
||||
// key.Raw(&dst) // Error, if key is an RSA key
|
||||
//
|
||||
// You will need to handle this distinction yourself in your KeyImporter. For example, certain
|
||||
// elliptic curve keys can be expressed in JWK in the same format, minus the "kty". In that case
|
||||
// you will need to check for the type of the destination object and return an error if it is
|
||||
// not compatible with your key.
|
||||
//
|
||||
// var raw mypkg.PrivateKey // assume a hypothetical private key type using a different curve than standard ones lie P-256
|
||||
// key, _ := jwk.Import(raw)
|
||||
// // key could be jwk.ECDSAPrivateKey, with different curve than P-256
|
||||
//
|
||||
// var dst *ecdsa.PrivateKey
|
||||
// key.Raw(&dst) // your KeyImporter will be called with *ecdsa.PrivateKey, which is not compatible with your key
|
||||
//
|
||||
// To implement this your code should look like the following:
|
||||
//
|
||||
// jwk.RegisterKeyExporter(jwk.EC, jwk.KeyExportFunc(exportMyKey))
|
||||
//
|
||||
// func exportMyKey(key jwk.Key, hint any) (any, error) {
|
||||
// // check if the type of object in hint is compatible with your key
|
||||
// switch hint.(type) {
|
||||
// case *mypkg.PrivateKey, *any:
|
||||
// // OK, we can proceed
|
||||
// default:
|
||||
// // Not compatible, return jwk.ContinueError
|
||||
// return nil, jwk.ContinueError()
|
||||
// }
|
||||
//
|
||||
// // key is a jwk.ECDSAPrivateKey or jwk.ECDSAPublicKey
|
||||
// switch key := key.(type) {
|
||||
// case jwk.ECDSAPrivateKey:
|
||||
// // convert key to mypkg.PrivateKey
|
||||
// case jwk.ECDSAPublicKey:
|
||||
// // convert key to mypkg.PublicKey
|
||||
// default:
|
||||
// // Not compatible, return jwk.ContinueError
|
||||
// return nil, jwk.ContinueError()
|
||||
// }
|
||||
// return ..., nil
|
||||
// }
|
||||
package jwk
|
||||
+402
@@ -0,0 +1,402 @@
|
||||
package jwk
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdh"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"reflect"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/base64"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/ecutil"
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
ourecdsa "github.com/lestrrat-go/jwx/v3/jwk/ecdsa"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ourecdsa.RegisterCurve(jwa.P256(), elliptic.P256())
|
||||
ourecdsa.RegisterCurve(jwa.P384(), elliptic.P384())
|
||||
ourecdsa.RegisterCurve(jwa.P521(), elliptic.P521())
|
||||
|
||||
RegisterKeyExporter(jwa.EC(), KeyExportFunc(ecdsaJWKToRaw))
|
||||
}
|
||||
|
||||
func (k *ecdsaPublicKey) Import(rawKey *ecdsa.PublicKey) error {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
|
||||
if rawKey.X == nil {
|
||||
return fmt.Errorf(`invalid ecdsa.PublicKey`)
|
||||
}
|
||||
|
||||
if rawKey.Y == nil {
|
||||
return fmt.Errorf(`invalid ecdsa.PublicKey`)
|
||||
}
|
||||
|
||||
xbuf := ecutil.AllocECPointBuffer(rawKey.X, rawKey.Curve)
|
||||
ybuf := ecutil.AllocECPointBuffer(rawKey.Y, rawKey.Curve)
|
||||
defer ecutil.ReleaseECPointBuffer(xbuf)
|
||||
defer ecutil.ReleaseECPointBuffer(ybuf)
|
||||
|
||||
k.x = make([]byte, len(xbuf))
|
||||
copy(k.x, xbuf)
|
||||
k.y = make([]byte, len(ybuf))
|
||||
copy(k.y, ybuf)
|
||||
|
||||
alg, err := ourecdsa.AlgorithmFromCurve(rawKey.Curve)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`jwk: failed to get algorithm for converting ECDSA public key to JWK: %w`, err)
|
||||
}
|
||||
k.crv = &alg
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *ecdsaPrivateKey) Import(rawKey *ecdsa.PrivateKey) error {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
|
||||
if rawKey.PublicKey.X == nil {
|
||||
return fmt.Errorf(`invalid ecdsa.PrivateKey`)
|
||||
}
|
||||
if rawKey.PublicKey.Y == nil {
|
||||
return fmt.Errorf(`invalid ecdsa.PrivateKey`)
|
||||
}
|
||||
if rawKey.D == nil {
|
||||
return fmt.Errorf(`invalid ecdsa.PrivateKey`)
|
||||
}
|
||||
|
||||
xbuf := ecutil.AllocECPointBuffer(rawKey.PublicKey.X, rawKey.Curve)
|
||||
ybuf := ecutil.AllocECPointBuffer(rawKey.PublicKey.Y, rawKey.Curve)
|
||||
dbuf := ecutil.AllocECPointBuffer(rawKey.D, rawKey.Curve)
|
||||
defer ecutil.ReleaseECPointBuffer(xbuf)
|
||||
defer ecutil.ReleaseECPointBuffer(ybuf)
|
||||
defer ecutil.ReleaseECPointBuffer(dbuf)
|
||||
|
||||
k.x = make([]byte, len(xbuf))
|
||||
copy(k.x, xbuf)
|
||||
k.y = make([]byte, len(ybuf))
|
||||
copy(k.y, ybuf)
|
||||
k.d = make([]byte, len(dbuf))
|
||||
copy(k.d, dbuf)
|
||||
|
||||
alg, err := ourecdsa.AlgorithmFromCurve(rawKey.Curve)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`jwk: failed to get algorithm for converting ECDSA private key to JWK: %w`, err)
|
||||
}
|
||||
k.crv = &alg
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildECDSAPublicKey(alg jwa.EllipticCurveAlgorithm, xbuf, ybuf []byte) (*ecdsa.PublicKey, error) {
|
||||
crv, err := ourecdsa.CurveFromAlgorithm(alg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`jwk: failed to get algorithm for ECDSA public key: %w`, err)
|
||||
}
|
||||
|
||||
var x, y big.Int
|
||||
x.SetBytes(xbuf)
|
||||
y.SetBytes(ybuf)
|
||||
|
||||
return &ecdsa.PublicKey{Curve: crv, X: &x, Y: &y}, nil
|
||||
}
|
||||
|
||||
func buildECDHPublicKey(alg jwa.EllipticCurveAlgorithm, xbuf, ybuf []byte) (*ecdh.PublicKey, error) {
|
||||
var ecdhcrv ecdh.Curve
|
||||
switch alg {
|
||||
case jwa.X25519():
|
||||
ecdhcrv = ecdh.X25519()
|
||||
case jwa.P256():
|
||||
ecdhcrv = ecdh.P256()
|
||||
case jwa.P384():
|
||||
ecdhcrv = ecdh.P384()
|
||||
case jwa.P521():
|
||||
ecdhcrv = ecdh.P521()
|
||||
default:
|
||||
return nil, fmt.Errorf(`jwk: unsupported ECDH curve %s`, alg)
|
||||
}
|
||||
|
||||
return ecdhcrv.NewPublicKey(append([]byte{0x04}, append(xbuf, ybuf...)...))
|
||||
}
|
||||
|
||||
func buildECDHPrivateKey(alg jwa.EllipticCurveAlgorithm, dbuf []byte) (*ecdh.PrivateKey, error) {
|
||||
var ecdhcrv ecdh.Curve
|
||||
switch alg {
|
||||
case jwa.X25519():
|
||||
ecdhcrv = ecdh.X25519()
|
||||
case jwa.P256():
|
||||
ecdhcrv = ecdh.P256()
|
||||
case jwa.P384():
|
||||
ecdhcrv = ecdh.P384()
|
||||
case jwa.P521():
|
||||
ecdhcrv = ecdh.P521()
|
||||
default:
|
||||
return nil, fmt.Errorf(`jwk: unsupported ECDH curve %s`, alg)
|
||||
}
|
||||
|
||||
return ecdhcrv.NewPrivateKey(dbuf)
|
||||
}
|
||||
|
||||
var ecdsaConvertibleTypes = []reflect.Type{
|
||||
reflect.TypeFor[ECDSAPrivateKey](),
|
||||
reflect.TypeFor[ECDSAPublicKey](),
|
||||
}
|
||||
|
||||
func ecdsaJWKToRaw(keyif Key, hint any) (any, error) {
|
||||
var isECDH bool
|
||||
|
||||
extracted, err := extractEmbeddedKey(keyif, ecdsaConvertibleTypes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`jwk: failed to extract embedded key: %w`, err)
|
||||
}
|
||||
|
||||
switch k := extracted.(type) {
|
||||
case ECDSAPrivateKey:
|
||||
switch hint.(type) {
|
||||
case ecdsa.PrivateKey, *ecdsa.PrivateKey:
|
||||
case ecdh.PrivateKey, *ecdh.PrivateKey:
|
||||
isECDH = true
|
||||
default:
|
||||
rv := reflect.ValueOf(hint)
|
||||
//nolint:revive
|
||||
if rv.Kind() == reflect.Ptr && rv.Elem().Kind() == reflect.Interface {
|
||||
// pointer to an interface value, presumably they want us to dynamically
|
||||
// create an object of the right type
|
||||
} else {
|
||||
return nil, fmt.Errorf(`invalid destination object type %T: %w`, hint, ContinueError())
|
||||
}
|
||||
}
|
||||
|
||||
locker, ok := k.(rlocker)
|
||||
if ok {
|
||||
locker.rlock()
|
||||
defer locker.runlock()
|
||||
}
|
||||
|
||||
crv, ok := k.Crv()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "crv" field`)
|
||||
}
|
||||
|
||||
if isECDH {
|
||||
d, ok := k.D()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "d" field`)
|
||||
}
|
||||
return buildECDHPrivateKey(crv, d)
|
||||
}
|
||||
|
||||
x, ok := k.X()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "x" field`)
|
||||
}
|
||||
y, ok := k.Y()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "y" field`)
|
||||
}
|
||||
pubk, err := buildECDSAPublicKey(crv, x, y)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to build public key: %w`, err)
|
||||
}
|
||||
|
||||
var key ecdsa.PrivateKey
|
||||
var d big.Int
|
||||
|
||||
origD, ok := k.D()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "d" field`)
|
||||
}
|
||||
|
||||
d.SetBytes(origD)
|
||||
key.D = &d
|
||||
key.PublicKey = *pubk
|
||||
|
||||
return &key, nil
|
||||
case ECDSAPublicKey:
|
||||
switch hint.(type) {
|
||||
case ecdsa.PublicKey, *ecdsa.PublicKey:
|
||||
case ecdh.PublicKey, *ecdh.PublicKey:
|
||||
isECDH = true
|
||||
default:
|
||||
rv := reflect.ValueOf(hint)
|
||||
//nolint:revive
|
||||
if rv.Kind() == reflect.Ptr && rv.Elem().Kind() == reflect.Interface {
|
||||
// pointer to an interface value, presumably they want us to dynamically
|
||||
// create an object of the right type
|
||||
} else {
|
||||
return nil, fmt.Errorf(`invalid destination object type %T: %w`, hint, ContinueError())
|
||||
}
|
||||
}
|
||||
|
||||
locker, ok := k.(rlocker)
|
||||
if ok {
|
||||
locker.rlock()
|
||||
defer locker.runlock()
|
||||
}
|
||||
|
||||
crv, ok := k.Crv()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "crv" field`)
|
||||
}
|
||||
|
||||
x, ok := k.X()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "x" field`)
|
||||
}
|
||||
|
||||
y, ok := k.Y()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "y" field`)
|
||||
}
|
||||
if isECDH {
|
||||
return buildECDHPublicKey(crv, x, y)
|
||||
}
|
||||
return buildECDSAPublicKey(crv, x, y)
|
||||
default:
|
||||
return nil, ContinueError()
|
||||
}
|
||||
}
|
||||
|
||||
func makeECDSAPublicKey(src Key) (Key, error) {
|
||||
newKey := newECDSAPublicKey()
|
||||
|
||||
// Iterate and copy everything except for the bits that should not be in the public key
|
||||
for _, k := range src.Keys() {
|
||||
switch k {
|
||||
case ECDSADKey:
|
||||
continue
|
||||
default:
|
||||
var v any
|
||||
if err := src.Get(k, &v); err != nil {
|
||||
return nil, fmt.Errorf(`ecdsa: makeECDSAPublicKey: failed to get field %q: %w`, k, err)
|
||||
}
|
||||
|
||||
if err := newKey.Set(k, v); err != nil {
|
||||
return nil, fmt.Errorf(`ecdsa: makeECDSAPublicKey: failed to set field %q: %w`, k, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newKey, nil
|
||||
}
|
||||
|
||||
func (k *ecdsaPrivateKey) PublicKey() (Key, error) {
|
||||
return makeECDSAPublicKey(k)
|
||||
}
|
||||
|
||||
func (k *ecdsaPublicKey) PublicKey() (Key, error) {
|
||||
return makeECDSAPublicKey(k)
|
||||
}
|
||||
|
||||
func ecdsaThumbprint(hash crypto.Hash, crv, x, y string) []byte {
|
||||
h := hash.New()
|
||||
fmt.Fprint(h, `{"crv":"`)
|
||||
fmt.Fprint(h, crv)
|
||||
fmt.Fprint(h, `","kty":"EC","x":"`)
|
||||
fmt.Fprint(h, x)
|
||||
fmt.Fprint(h, `","y":"`)
|
||||
fmt.Fprint(h, y)
|
||||
fmt.Fprint(h, `"}`)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// Thumbprint returns the JWK thumbprint using the indicated
|
||||
// hashing algorithm, according to RFC 7638
|
||||
func (k ecdsaPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
|
||||
var key ecdsa.PublicKey
|
||||
if err := Export(&k, &key); err != nil {
|
||||
return nil, fmt.Errorf(`failed to export ecdsa.PublicKey for thumbprint generation: %w`, err)
|
||||
}
|
||||
|
||||
xbuf := ecutil.AllocECPointBuffer(key.X, key.Curve)
|
||||
ybuf := ecutil.AllocECPointBuffer(key.Y, key.Curve)
|
||||
defer ecutil.ReleaseECPointBuffer(xbuf)
|
||||
defer ecutil.ReleaseECPointBuffer(ybuf)
|
||||
|
||||
return ecdsaThumbprint(
|
||||
hash,
|
||||
key.Curve.Params().Name,
|
||||
base64.EncodeToString(xbuf),
|
||||
base64.EncodeToString(ybuf),
|
||||
), nil
|
||||
}
|
||||
|
||||
// Thumbprint returns the JWK thumbprint using the indicated
|
||||
// hashing algorithm, according to RFC 7638
|
||||
func (k ecdsaPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
|
||||
var key ecdsa.PrivateKey
|
||||
if err := Export(&k, &key); err != nil {
|
||||
return nil, fmt.Errorf(`failed to export ecdsa.PrivateKey for thumbprint generation: %w`, err)
|
||||
}
|
||||
|
||||
xbuf := ecutil.AllocECPointBuffer(key.X, key.Curve)
|
||||
ybuf := ecutil.AllocECPointBuffer(key.Y, key.Curve)
|
||||
defer ecutil.ReleaseECPointBuffer(xbuf)
|
||||
defer ecutil.ReleaseECPointBuffer(ybuf)
|
||||
|
||||
return ecdsaThumbprint(
|
||||
hash,
|
||||
key.Curve.Params().Name,
|
||||
base64.EncodeToString(xbuf),
|
||||
base64.EncodeToString(ybuf),
|
||||
), nil
|
||||
}
|
||||
|
||||
func ecdsaValidateKey(k interface {
|
||||
Crv() (jwa.EllipticCurveAlgorithm, bool)
|
||||
X() ([]byte, bool)
|
||||
Y() ([]byte, bool)
|
||||
}, checkPrivate bool) error {
|
||||
crvtyp, ok := k.Crv()
|
||||
if !ok {
|
||||
return fmt.Errorf(`missing "crv" field`)
|
||||
}
|
||||
|
||||
crv, err := ourecdsa.CurveFromAlgorithm(crvtyp)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`invalid curve algorithm %q: %w`, crvtyp, err)
|
||||
}
|
||||
|
||||
keySize := ecutil.CalculateKeySize(crv)
|
||||
if x, ok := k.X(); !ok || len(x) != keySize {
|
||||
return fmt.Errorf(`invalid "x" length (%d) for curve %q`, len(x), crv.Params().Name)
|
||||
}
|
||||
|
||||
if y, ok := k.Y(); !ok || len(y) != keySize {
|
||||
return fmt.Errorf(`invalid "y" length (%d) for curve %q`, len(y), crv.Params().Name)
|
||||
}
|
||||
|
||||
if checkPrivate {
|
||||
if priv, ok := k.(keyWithD); ok {
|
||||
if d, ok := priv.D(); !ok || len(d) != keySize {
|
||||
return fmt.Errorf(`invalid "d" length (%d) for curve %q`, len(d), crv.Params().Name)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf(`missing "d" value`)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *ecdsaPrivateKey) Validate() error {
|
||||
if err := ecdsaValidateKey(k, true); err != nil {
|
||||
return NewKeyValidationError(fmt.Errorf(`jwk.ECDSAPrivateKey: %w`, err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *ecdsaPublicKey) Validate() error {
|
||||
if err := ecdsaValidateKey(k, false); err != nil {
|
||||
return NewKeyValidationError(fmt.Errorf(`jwk.ECDSAPublicKey: %w`, err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
load("@rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "ecdsa",
|
||||
srcs = ["ecdsa.go"],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3/jwk/ecdsa",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = ["//jwa"],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "go_default_library",
|
||||
actual = ":ecdsa",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
package ecdsa
|
||||
|
||||
import (
|
||||
"crypto/elliptic"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
)
|
||||
|
||||
var muCurves sync.RWMutex
|
||||
var algToCurveMap map[jwa.EllipticCurveAlgorithm]elliptic.Curve
|
||||
var curveToAlgMap map[elliptic.Curve]jwa.EllipticCurveAlgorithm
|
||||
var algList []jwa.EllipticCurveAlgorithm
|
||||
|
||||
func init() {
|
||||
muCurves.Lock()
|
||||
algToCurveMap = make(map[jwa.EllipticCurveAlgorithm]elliptic.Curve)
|
||||
curveToAlgMap = make(map[elliptic.Curve]jwa.EllipticCurveAlgorithm)
|
||||
muCurves.Unlock()
|
||||
}
|
||||
|
||||
// RegisterCurve registers a jwa.EllipticCurveAlgorithm constant and its
|
||||
// corresponding elliptic.Curve object. Users do not need to call this unless
|
||||
// they are registering a new ECDSA key type
|
||||
func RegisterCurve(alg jwa.EllipticCurveAlgorithm, crv elliptic.Curve) {
|
||||
muCurves.Lock()
|
||||
defer muCurves.Unlock()
|
||||
|
||||
algToCurveMap[alg] = crv
|
||||
curveToAlgMap[crv] = alg
|
||||
rebuildCurves()
|
||||
}
|
||||
|
||||
func rebuildCurves() {
|
||||
l := len(algToCurveMap)
|
||||
if cap(algList) < l {
|
||||
algList = make([]jwa.EllipticCurveAlgorithm, 0, l)
|
||||
} else {
|
||||
algList = algList[:0]
|
||||
}
|
||||
|
||||
for alg := range algToCurveMap {
|
||||
algList = append(algList, alg)
|
||||
}
|
||||
}
|
||||
|
||||
// Algorithms returns the list of registered jwa.EllipticCurveAlgorithms
|
||||
// that ca be used for ECDSA keys.
|
||||
func Algorithms() []jwa.EllipticCurveAlgorithm {
|
||||
muCurves.RLock()
|
||||
defer muCurves.RUnlock()
|
||||
|
||||
return algList
|
||||
}
|
||||
|
||||
func AlgorithmFromCurve(crv elliptic.Curve) (jwa.EllipticCurveAlgorithm, error) {
|
||||
alg, ok := curveToAlgMap[crv]
|
||||
if !ok {
|
||||
return jwa.InvalidEllipticCurve(), fmt.Errorf(`unknown elliptic curve: %q`, crv)
|
||||
}
|
||||
return alg, nil
|
||||
}
|
||||
|
||||
func CurveFromAlgorithm(alg jwa.EllipticCurveAlgorithm) (elliptic.Curve, error) {
|
||||
crv, ok := algToCurveMap[alg]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`unknown elliptic curve algorithm: %q`, alg)
|
||||
}
|
||||
return crv, nil
|
||||
}
|
||||
|
||||
func IsCurveAvailable(alg jwa.EllipticCurveAlgorithm) bool {
|
||||
_, ok := algToCurveMap[alg]
|
||||
return ok
|
||||
}
|
||||
+1432
File diff suppressed because it is too large
Load Diff
+79
@@ -0,0 +1,79 @@
|
||||
package jwk
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
var cpe = &continueError{}
|
||||
|
||||
// ContinueError returns an opaque error that can be returned
|
||||
// when a `KeyParser`, `KeyImporter`, or `KeyExporter` cannot handle the given payload,
|
||||
// but would like the process to continue with the next handler.
|
||||
func ContinueError() error {
|
||||
return cpe
|
||||
}
|
||||
|
||||
type continueError struct{}
|
||||
|
||||
func (e *continueError) Error() string {
|
||||
return "continue parsing"
|
||||
}
|
||||
|
||||
type importError struct {
|
||||
error
|
||||
}
|
||||
|
||||
func (e importError) Unwrap() error {
|
||||
return e.error
|
||||
}
|
||||
|
||||
func (importError) Is(err error) bool {
|
||||
_, ok := err.(importError)
|
||||
return ok
|
||||
}
|
||||
|
||||
func importerr(f string, args ...any) error {
|
||||
return importError{fmt.Errorf(`jwk.Import: `+f, args...)}
|
||||
}
|
||||
|
||||
var errDefaultImportError = importError{errors.New(`import error`)}
|
||||
|
||||
func ImportError() error {
|
||||
return errDefaultImportError
|
||||
}
|
||||
|
||||
type parseError struct {
|
||||
error
|
||||
}
|
||||
|
||||
func (e parseError) Unwrap() error {
|
||||
return e.error
|
||||
}
|
||||
|
||||
func (parseError) Is(err error) bool {
|
||||
_, ok := err.(parseError)
|
||||
return ok
|
||||
}
|
||||
|
||||
func bparseerr(prefix string, f string, args ...any) error {
|
||||
return parseError{fmt.Errorf(prefix+`: `+f, args...)}
|
||||
}
|
||||
|
||||
func parseerr(f string, args ...any) error {
|
||||
return bparseerr(`jwk.Parse`, f, args...)
|
||||
}
|
||||
|
||||
func rparseerr(f string, args ...any) error {
|
||||
return bparseerr(`jwk.ParseReader`, f, args...)
|
||||
}
|
||||
|
||||
func sparseerr(f string, args ...any) error {
|
||||
return bparseerr(`jwk.ParseString`, f, args...)
|
||||
}
|
||||
|
||||
var errDefaultParseError = parseError{errors.New(`parse error`)}
|
||||
|
||||
func ParseError() error {
|
||||
return errDefaultParseError
|
||||
}
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
//go:build jwx_es256k
|
||||
|
||||
package jwk
|
||||
|
||||
import (
|
||||
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
ourecdsa "github.com/lestrrat-go/jwx/v3/jwk/ecdsa"
|
||||
)
|
||||
|
||||
func init() {
|
||||
ourecdsa.RegisterCurve(jwa.Secp256k1(), secp256k1.S256())
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package jwk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// Fetcher is an interface that represents an object that fetches a JWKS.
|
||||
// Currently this is only used in the `jws.WithVerifyAuto` option.
|
||||
//
|
||||
// Particularly, do not confuse this as the backend to `jwk.Fetch()` function.
|
||||
// If you need to control how `jwk.Fetch()` implements HTTP requests look into
|
||||
// providing a custom `http.Client` object via `jwk.WithHTTPClient` option
|
||||
type Fetcher interface {
|
||||
Fetch(context.Context, string, ...FetchOption) (Set, error)
|
||||
}
|
||||
|
||||
// FetchFunc describes a type of Fetcher that is represented as a function.
|
||||
//
|
||||
// You can use this to wrap functions (e.g. `jwk.Fetch“) as a Fetcher object.
|
||||
type FetchFunc func(context.Context, string, ...FetchOption) (Set, error)
|
||||
|
||||
func (ff FetchFunc) Fetch(ctx context.Context, u string, options ...FetchOption) (Set, error) {
|
||||
return ff(ctx, u, options...)
|
||||
}
|
||||
|
||||
// CachedFetcher wraps `jwk.Cache` so that it can be used as a `jwk.Fetcher`.
|
||||
//
|
||||
// One notable diffence from a general use fetcher is that `jwk.CachedFetcher`
|
||||
// can only be used with JWKS URLs that have been registered with the cache.
|
||||
// Please read the documentation fo `(jwk.CachedFetcher).Fetch` for more details.
|
||||
//
|
||||
// This object is intended to be used with `jws.WithVerifyAuto` option, specifically
|
||||
// for a scenario where there is a very small number of JWKS URLs that are trusted
|
||||
// and used to verify JWS messages. It is NOT meant to be used as a general purpose
|
||||
// caching fetcher object.
|
||||
type CachedFetcher struct {
|
||||
cache *Cache
|
||||
}
|
||||
|
||||
// NewCachedFetcher creates a new `jwk.CachedFetcher` object.
|
||||
func NewCachedFetcher(cache *Cache) *CachedFetcher {
|
||||
return &CachedFetcher{cache}
|
||||
}
|
||||
|
||||
// Fetch fetches a JWKS from the cache. If the JWKS URL has not been registered with
|
||||
// the cache, an error is returned.
|
||||
func (f *CachedFetcher) Fetch(ctx context.Context, u string, _ ...FetchOption) (Set, error) {
|
||||
if !f.cache.IsRegistered(ctx, u) {
|
||||
return nil, fmt.Errorf(`jwk.CachedFetcher: url %q has not been registered`, u)
|
||||
}
|
||||
return f.cache.Lookup(ctx, u)
|
||||
}
|
||||
|
||||
// Fetch fetches a JWK resource specified by a URL. The url must be
|
||||
// pointing to a resource that is supported by `net/http`.
|
||||
//
|
||||
// This function is just a wrapper around `net/http` and `jwk.Parse`.
|
||||
// There is nothing special here, so you are safe to use your own
|
||||
// mechanism to fetch the JWKS.
|
||||
//
|
||||
// If you are using the same `jwk.Set` for long periods of time during
|
||||
// the lifecycle of your program, and would like to periodically refresh the
|
||||
// contents of the object with the data at the remote resource,
|
||||
// consider using `jwk.Cache`, which automatically refreshes
|
||||
// jwk.Set objects asynchronously.
|
||||
func Fetch(ctx context.Context, u string, options ...FetchOption) (Set, error) {
|
||||
var parseOptions []ParseOption
|
||||
//nolint:revive // I want to keep the type of `wl` as `Whitelist` instead of `InsecureWhitelist`
|
||||
var wl Whitelist = InsecureWhitelist{}
|
||||
var client HTTPClient = http.DefaultClient
|
||||
for _, option := range options {
|
||||
if parseOpt, ok := option.(ParseOption); ok {
|
||||
parseOptions = append(parseOptions, parseOpt)
|
||||
continue
|
||||
}
|
||||
|
||||
switch option.Ident() {
|
||||
case identHTTPClient{}:
|
||||
if err := option.Value(&client); err != nil {
|
||||
return nil, fmt.Errorf(`failed to retrieve HTTPClient option value: %w`, err)
|
||||
}
|
||||
case identFetchWhitelist{}:
|
||||
if err := option.Value(&wl); err != nil {
|
||||
return nil, fmt.Errorf(`failed to retrieve fetch whitelist option value: %w`, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !wl.IsAllowed(u) {
|
||||
return nil, fmt.Errorf(`jwk.Fetch: url %q has been rejected by whitelist`, u)
|
||||
}
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`jwk.Fetch: failed to create new request: %w`, err)
|
||||
}
|
||||
|
||||
res, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`jwk.Fetch: request failed: %w`, err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf(`jwk.Fetch: request returned status %d, expected 200`, res.StatusCode)
|
||||
}
|
||||
|
||||
buf, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`jwk.Fetch: failed to read response body for %q: %w`, u, err)
|
||||
}
|
||||
|
||||
return Parse(buf, parseOptions...)
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package jwk
|
||||
|
||||
import (
|
||||
"github.com/lestrrat-go/jwx/v3/transform"
|
||||
)
|
||||
|
||||
// KeyFilter is an interface that allows users to filter JWK key fields.
|
||||
// It provides two methods: Filter and Reject; Filter returns a new key with only
|
||||
// the fields that match the filter criteria, while Reject returns a new key with
|
||||
// only the fields that DO NOT match the filter.
|
||||
//
|
||||
// EXPERIMENTAL: This API is experimental and its interface and behavior is
|
||||
// subject to change in future releases. This API is not subject to semver
|
||||
// compatibility guarantees.
|
||||
type KeyFilter interface {
|
||||
Filter(key Key) (Key, error)
|
||||
Reject(key Key) (Key, error)
|
||||
}
|
||||
|
||||
// NewFieldNameFilter creates a new FieldNameFilter with the specified field names.
|
||||
//
|
||||
// Note that because some JWK fields are associated with the type instead of
|
||||
// stored as data, this filter will not be able to remove them. An example would
|
||||
// be the `kty` field: it's associated with the underlying JWK key type, and will
|
||||
// always be present even if you attempt to remove it.
|
||||
func NewFieldNameFilter(names ...string) KeyFilter {
|
||||
return transform.NewNameBasedFilter[Key](names...)
|
||||
}
|
||||
+148
@@ -0,0 +1,148 @@
|
||||
package jwk
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/json"
|
||||
)
|
||||
|
||||
// AsymmetricKey describes a Key that represents a key in an asymmetric key pair,
|
||||
// which in turn can be either a private or a public key. This interface
|
||||
// allows those keys to be queried if they are one or the other.
|
||||
type AsymmetricKey interface {
|
||||
IsPrivate() bool
|
||||
}
|
||||
|
||||
// KeyUsageType is used to denote what this key should be used for
|
||||
type KeyUsageType string
|
||||
|
||||
const (
|
||||
// ForSignature is the value used in the headers to indicate that
|
||||
// this key should be used for signatures
|
||||
ForSignature KeyUsageType = "sig"
|
||||
// ForEncryption is the value used in the headers to indicate that
|
||||
// this key should be used for encrypting
|
||||
ForEncryption KeyUsageType = "enc"
|
||||
)
|
||||
|
||||
type KeyOperation string
|
||||
type KeyOperationList []KeyOperation
|
||||
|
||||
const (
|
||||
KeyOpSign KeyOperation = "sign" // (compute digital signature or MAC)
|
||||
KeyOpVerify KeyOperation = "verify" // (verify digital signature or MAC)
|
||||
KeyOpEncrypt KeyOperation = "encrypt" // (encrypt content)
|
||||
KeyOpDecrypt KeyOperation = "decrypt" // (decrypt content and validate decryption, if applicable)
|
||||
KeyOpWrapKey KeyOperation = "wrapKey" // (encrypt key)
|
||||
KeyOpUnwrapKey KeyOperation = "unwrapKey" // (decrypt key and validate decryption, if applicable)
|
||||
KeyOpDeriveKey KeyOperation = "deriveKey" // (derive key)
|
||||
KeyOpDeriveBits KeyOperation = "deriveBits" // (derive bits not to be used as a key)
|
||||
)
|
||||
|
||||
// Set represents JWKS object, a collection of jwk.Key objects.
|
||||
//
|
||||
// Sets can be safely converted to and from JSON using the standard
|
||||
// `"encoding/json".Marshal` and `"encoding/json".Unmarshal`. However,
|
||||
// if you do not know if the payload contains a single JWK or a JWK set,
|
||||
// consider using `jwk.Parse()` to always get a `jwk.Set` out of it.
|
||||
//
|
||||
// Since v1.2.12, JWK sets with private parameters can be parsed as well.
|
||||
// Such private parameters can be accessed via the `Field()` method.
|
||||
// If a resource contains a single JWK instead of a JWK set, private parameters
|
||||
// are stored in _both_ the resulting `jwk.Set` object and the `jwk.Key` object .
|
||||
//
|
||||
//nolint:interfacebloat
|
||||
type Set interface {
|
||||
// AddKey adds the specified key. If the key already exists in the set,
|
||||
// an error is returned.
|
||||
AddKey(Key) error
|
||||
|
||||
// Clear resets the list of keys associated with this set, emptying the
|
||||
// internal list of `jwk.Key`s, as well as clearing any other non-key
|
||||
// fields
|
||||
Clear() error
|
||||
|
||||
// Get returns the key at index `idx`. If the index is out of range,
|
||||
// then the second return value is false.
|
||||
Key(int) (Key, bool)
|
||||
|
||||
// Get returns the value of a private field in the key set.
|
||||
//
|
||||
// For the purposes of a key set, any field other than the "keys" field is
|
||||
// considered to be a private field. In other words, you cannot use this
|
||||
// method to directly access the list of keys in the set
|
||||
Get(string, any) error
|
||||
|
||||
// Set sets the value of a single field.
|
||||
//
|
||||
// This method, which takes an `any`, exists because
|
||||
// these objects can contain extra _arbitrary_ fields that users can
|
||||
// specify, and there is no way of knowing what type they could be.
|
||||
Set(string, any) error
|
||||
|
||||
// Remove removes the specified non-key field from the set.
|
||||
// Keys may not be removed using this method. See RemoveKey for
|
||||
// removing keys.
|
||||
Remove(string) error
|
||||
|
||||
// Index returns the index where the given key exists, -1 otherwise
|
||||
Index(Key) int
|
||||
|
||||
// Len returns the number of keys in the set
|
||||
Len() int
|
||||
|
||||
// LookupKeyID returns the first key matching the given key id.
|
||||
//
|
||||
// The second return value is false if there are no keys matching the key id.
|
||||
// The set *may* contain multiple keys with the same key id. If you
|
||||
// need all of them, Len() and Key(int)
|
||||
//
|
||||
// This method is meant to be used to lookup a key with a unique ID.
|
||||
// Bacauseof this, you cannot use this method to lookup keys with an empty key ID
|
||||
// (i.e. `kid` is not specified, or is an empty string).
|
||||
LookupKeyID(string) (Key, bool)
|
||||
|
||||
// RemoveKey removes the key from the set.
|
||||
// RemoveKey returns an error when the specified key does not exist
|
||||
// in set.
|
||||
RemoveKey(Key) error
|
||||
|
||||
// Keys returns the list of keys present in the Set, except for `keys`.
|
||||
// e.g. if you had `{"keys": ["a", "b"], "c": .., "d": ...}`, this method would
|
||||
// return `["c", "d"]`. Note that the order of the keys is not guaranteed.
|
||||
//
|
||||
// TODO: name is confusing between this and Key()
|
||||
Keys() []string
|
||||
|
||||
// Clone create a new set with identical keys. Keys themselves are not cloned.
|
||||
Clone() (Set, error)
|
||||
}
|
||||
|
||||
type set struct {
|
||||
keys []Key
|
||||
mu sync.RWMutex
|
||||
dc DecodeCtx
|
||||
privateParams map[string]any
|
||||
}
|
||||
|
||||
type PublicKeyer interface {
|
||||
// PublicKey creates the corresponding PublicKey type for this object.
|
||||
// All fields are copied onto the new public key, except for those that are not allowed.
|
||||
// Returned value must not be the receiver itself.
|
||||
PublicKey() (Key, error)
|
||||
}
|
||||
|
||||
type DecodeCtx interface {
|
||||
json.DecodeCtx
|
||||
IgnoreParseError() bool
|
||||
}
|
||||
type KeyWithDecodeCtx interface {
|
||||
SetDecodeCtx(DecodeCtx)
|
||||
DecodeCtx() DecodeCtx
|
||||
}
|
||||
|
||||
// Used internally: It's used to lock a key
|
||||
type rlocker interface {
|
||||
rlock()
|
||||
runlock()
|
||||
}
|
||||
+109
@@ -0,0 +1,109 @@
|
||||
// Code generated by tools/cmd/genjwk/main.go. DO NOT EDIT.
|
||||
|
||||
package jwk
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/cert"
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
)
|
||||
|
||||
const (
|
||||
KeyTypeKey = "kty"
|
||||
KeyUsageKey = "use"
|
||||
KeyOpsKey = "key_ops"
|
||||
AlgorithmKey = "alg"
|
||||
KeyIDKey = "kid"
|
||||
X509URLKey = "x5u"
|
||||
X509CertChainKey = "x5c"
|
||||
X509CertThumbprintKey = "x5t"
|
||||
X509CertThumbprintS256Key = "x5t#S256"
|
||||
)
|
||||
|
||||
// Key defines the minimal interface for each of the
|
||||
// key types. Their use and implementation differ significantly
|
||||
// between each key type, so you should use type assertions
|
||||
// to perform more specific tasks with each key
|
||||
type Key interface {
|
||||
|
||||
// Has returns true if the specified field has a value, even if
|
||||
// the value is empty-ish (e.g. 0, false, "") as long as it has been
|
||||
// explicitly set.
|
||||
Has(string) bool
|
||||
|
||||
// Get is used to extract the value of any field, including non-standard fields, out of the key.
|
||||
//
|
||||
// The first argument is the name of the field. The second argument is a pointer
|
||||
// to a variable that will receive the value of the field. The method returns
|
||||
// an error if the field does not exist, or if the value cannot be assigned to
|
||||
// the destination variable. Note that a field is considered to "exist" even if
|
||||
// the value is empty-ish (e.g. 0, false, ""), as long as it is explicitly set.
|
||||
Get(string, any) error
|
||||
|
||||
// Set sets the value of a single field. Note that certain fields,
|
||||
// notably "kty", cannot be altered, but will not return an error
|
||||
//
|
||||
// This method, which takes an `any`, exists because
|
||||
// these objects can contain extra _arbitrary_ fields that users can
|
||||
// specify, and there is no way of knowing what type they could be
|
||||
Set(string, any) error
|
||||
|
||||
// Remove removes the field associated with the specified key.
|
||||
// There is no way to remove the `kty` (key type). You will ALWAYS be left with one field in a jwk.Key.
|
||||
Remove(string) error
|
||||
// Validate performs _minimal_ checks if the data stored in the key are valid.
|
||||
// By minimal, we mean that it does not check if the key is valid for use in
|
||||
// cryptographic operations. For example, it does not check if an RSA key's
|
||||
// `e` field is a valid exponent, or if the `n` field is a valid modulus.
|
||||
// Instead, it checks for things such as the _presence_ of some required fields,
|
||||
// or if certain keys' values are of particular length.
|
||||
//
|
||||
// Note that depending on th underlying key type, use of this method requires
|
||||
// that multiple fields in the key are properly populated. For example, an EC
|
||||
// key's "x", "y" fields cannot be validated unless the "crv" field is populated first.
|
||||
//
|
||||
// Validate is never called by `UnmarshalJSON()` or `Set`. It must explicitly be
|
||||
// called by the user
|
||||
Validate() error
|
||||
|
||||
// Thumbprint returns the JWK thumbprint using the indicated
|
||||
// hashing algorithm, according to RFC 7638
|
||||
Thumbprint(crypto.Hash) ([]byte, error)
|
||||
|
||||
// Keys returns a list of the keys contained in this jwk.Key.
|
||||
Keys() []string
|
||||
|
||||
// Clone creates a new instance of the same type
|
||||
Clone() (Key, error)
|
||||
|
||||
// PublicKey creates the corresponding PublicKey type for this object.
|
||||
// All fields are copied onto the new public key, except for those that are not allowed.
|
||||
//
|
||||
// If the key is already a public key, it returns a new copy minus the disallowed fields as above.
|
||||
PublicKey() (Key, error)
|
||||
|
||||
// KeyType returns the `kty` of a JWK
|
||||
KeyType() jwa.KeyType
|
||||
// KeyUsage returns `use` of a JWK
|
||||
KeyUsage() (string, bool)
|
||||
// KeyOps returns `key_ops` of a JWK
|
||||
KeyOps() (KeyOperationList, bool)
|
||||
// Algorithm returns `alg` of a JWK
|
||||
|
||||
// Algorithm returns the value of the `alg` field.
|
||||
//
|
||||
// This field may contain either `jwk.SignatureAlgorithm`, `jwk.KeyEncryptionAlgorithm`, or `jwk.ContentEncryptionAlgorithm`.
|
||||
// This is why there exists a `jwa.KeyAlgorithm` type that encompasses both types.
|
||||
Algorithm() (jwa.KeyAlgorithm, bool)
|
||||
// KeyID returns `kid` of a JWK
|
||||
KeyID() (string, bool)
|
||||
// X509URL returns `x5u` of a JWK
|
||||
X509URL() (string, bool)
|
||||
// X509CertChain returns `x5c` of a JWK
|
||||
X509CertChain() (*cert.Chain, bool)
|
||||
// X509CertThumbprint returns `x5t` of a JWK
|
||||
X509CertThumbprint() (string, bool)
|
||||
// X509CertThumbprintS256 returns `x5t#S256` of a JWK
|
||||
X509CertThumbprintS256() (string, bool)
|
||||
}
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
// Code generated by tools/cmd/genreadfile/main.go. DO NOT EDIT.
|
||||
|
||||
package jwk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
)
|
||||
|
||||
type sysFS struct{}
|
||||
|
||||
func (sysFS) Open(path string) (fs.File, error) {
|
||||
return os.Open(path)
|
||||
}
|
||||
|
||||
func ReadFile(path string, options ...ReadFileOption) (Set, error) {
|
||||
var parseOptions []ParseOption
|
||||
for _, option := range options {
|
||||
if po, ok := option.(ParseOption); ok {
|
||||
parseOptions = append(parseOptions, po)
|
||||
}
|
||||
}
|
||||
|
||||
var srcFS fs.FS = sysFS{}
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identFS{}:
|
||||
if err := option.Value(&srcFS); err != nil {
|
||||
return nil, fmt.Errorf("failed to set fs.FS: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
f, err := srcFS.Open(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer f.Close()
|
||||
return ParseReader(f, parseOptions...)
|
||||
}
|
||||
+710
@@ -0,0 +1,710 @@
|
||||
//go:generate ../tools/cmd/genjwk.sh
|
||||
|
||||
package jwk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"reflect"
|
||||
"slices"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/base64"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/json"
|
||||
)
|
||||
|
||||
var registry = json.NewRegistry()
|
||||
|
||||
func bigIntToBytes(n *big.Int) ([]byte, error) {
|
||||
if n == nil {
|
||||
return nil, fmt.Errorf(`invalid *big.Int value`)
|
||||
}
|
||||
return n.Bytes(), nil
|
||||
}
|
||||
|
||||
func init() {
|
||||
if err := RegisterProbeField(reflect.StructField{
|
||||
Name: "Kty",
|
||||
Type: reflect.TypeFor[string](),
|
||||
Tag: `json:"kty"`,
|
||||
}); err != nil {
|
||||
panic(fmt.Errorf("failed to register mandatory probe for 'kty' field: %w", err))
|
||||
}
|
||||
if err := RegisterProbeField(reflect.StructField{
|
||||
Name: "D",
|
||||
Type: reflect.TypeFor[json.RawMessage](),
|
||||
Tag: `json:"d,omitempty"`,
|
||||
}); err != nil {
|
||||
panic(fmt.Errorf("failed to register mandatory probe for 'kty' field: %w", err))
|
||||
}
|
||||
}
|
||||
|
||||
// Import creates a jwk.Key from the given key (RSA/ECDSA/symmetric keys).
|
||||
//
|
||||
// The constructor auto-detects the type of key to be instantiated
|
||||
// based on the input type:
|
||||
//
|
||||
// - "crypto/rsa".PrivateKey and "crypto/rsa".PublicKey creates an RSA based key
|
||||
// - "crypto/ecdsa".PrivateKey and "crypto/ecdsa".PublicKey creates an EC based key
|
||||
// - "crypto/ed25519".PrivateKey and "crypto/ed25519".PublicKey creates an OKP based key
|
||||
// - "crypto/ecdh".PrivateKey and "crypto/ecdh".PublicKey creates an OKP based key
|
||||
// - []byte creates a symmetric key
|
||||
func Import(raw any) (Key, error) {
|
||||
if raw == nil {
|
||||
return nil, importerr(`a non-nil key is required`)
|
||||
}
|
||||
|
||||
muKeyImporters.RLock()
|
||||
conv, ok := keyImporters[reflect.TypeOf(raw)]
|
||||
muKeyImporters.RUnlock()
|
||||
if !ok {
|
||||
return nil, importerr(`failed to convert %T to jwk.Key: no converters were able to convert`, raw)
|
||||
}
|
||||
|
||||
return conv.Import(raw)
|
||||
}
|
||||
|
||||
// PublicSetOf returns a new jwk.Set consisting of
|
||||
// public keys of the keys contained in the set.
|
||||
//
|
||||
// This is useful when you are generating a set of private keys, and
|
||||
// you want to generate the corresponding public versions for the
|
||||
// users to verify with.
|
||||
//
|
||||
// Be aware that all fields will be copied onto the new public key. It is the caller's
|
||||
// responsibility to remove any fields, if necessary.
|
||||
func PublicSetOf(v Set) (Set, error) {
|
||||
newSet := NewSet()
|
||||
|
||||
n := v.Len()
|
||||
for i := range n {
|
||||
k, ok := v.Key(i)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`key not found`)
|
||||
}
|
||||
pubKey, err := PublicKeyOf(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to get public key of %T: %w`, k, err)
|
||||
}
|
||||
if err := newSet.AddKey(pubKey); err != nil {
|
||||
return nil, fmt.Errorf(`failed to add key to public key set: %w`, err)
|
||||
}
|
||||
}
|
||||
|
||||
return newSet, nil
|
||||
}
|
||||
|
||||
// PublicKeyOf returns the corresponding public version of the jwk.Key.
|
||||
// If `v` is a SymmetricKey, then the same value is returned.
|
||||
// If `v` is already a public key, the key itself is returned.
|
||||
//
|
||||
// If `v` is a private key type that has a `PublicKey()` method, be aware
|
||||
// that all fields will be copied onto the new public key. It is the caller's
|
||||
// responsibility to remove any fields, if necessary
|
||||
//
|
||||
// If `v` is a raw key, the key is first converted to a `jwk.Key`
|
||||
func PublicKeyOf(v any) (Key, error) {
|
||||
// This should catch all jwk.Key instances
|
||||
if pk, ok := v.(PublicKeyer); ok {
|
||||
return pk.PublicKey()
|
||||
}
|
||||
|
||||
jk, err := Import(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`jwk.PublicKeyOf: failed to convert key into JWK: %w`, err)
|
||||
}
|
||||
|
||||
return jk.PublicKey()
|
||||
}
|
||||
|
||||
// PublicRawKeyOf returns the corresponding public key of the given
|
||||
// value `v` (e.g. given *rsa.PrivateKey, *rsa.PublicKey is returned)
|
||||
// If `v` is already a public key, the key itself is returned.
|
||||
//
|
||||
// The returned value will always be a pointer to the public key,
|
||||
// except when a []byte (e.g. symmetric key, ed25519 key) is passed to `v`.
|
||||
// In this case, the same []byte value is returned.
|
||||
//
|
||||
// This function must go through converting the object once to a jwk.Key,
|
||||
// then back to a raw key, so it's not exactly efficient.
|
||||
func PublicRawKeyOf(v any) (any, error) {
|
||||
pk, ok := v.(PublicKeyer)
|
||||
if !ok {
|
||||
k, err := Import(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`jwk.PublicRawKeyOf: failed to convert key to jwk.Key: %w`, err)
|
||||
}
|
||||
|
||||
pk, ok = k.(PublicKeyer)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`jwk.PublicRawKeyOf: failed to convert key to jwk.PublicKeyer: %w`, err)
|
||||
}
|
||||
}
|
||||
|
||||
pubk, err := pk.PublicKey()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`jwk.PublicRawKeyOf: failed to obtain public key from %T: %w`, v, err)
|
||||
}
|
||||
|
||||
var raw any
|
||||
if err := Export(pubk, &raw); err != nil {
|
||||
return nil, fmt.Errorf(`jwk.PublicRawKeyOf: failed to obtain raw key from %T: %w`, pubk, err)
|
||||
}
|
||||
return raw, nil
|
||||
}
|
||||
|
||||
// ParseRawKey is a combination of ParseKey and Raw. It parses a single JWK key,
|
||||
// and assigns the "raw" key to the given parameter. The key must either be
|
||||
// a pointer to an empty interface, or a pointer to the actual raw key type
|
||||
// such as *rsa.PrivateKey, *ecdsa.PublicKey, *[]byte, etc.
|
||||
func ParseRawKey(data []byte, rawkey any) error {
|
||||
key, err := ParseKey(data)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to parse key: %w`, err)
|
||||
}
|
||||
|
||||
if err := Export(key, rawkey); err != nil {
|
||||
return fmt.Errorf(`failed to assign to raw key variable: %w`, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
type setDecodeCtx struct {
|
||||
json.DecodeCtx
|
||||
|
||||
ignoreParseError bool
|
||||
}
|
||||
|
||||
func (ctx *setDecodeCtx) IgnoreParseError() bool {
|
||||
return ctx.ignoreParseError
|
||||
}
|
||||
|
||||
// ParseKey parses a single key JWK. Unlike `jwk.Parse` this method will
|
||||
// report failure if you attempt to pass a JWK set. Only use this function
|
||||
// when you know that the data is a single JWK.
|
||||
//
|
||||
// Given a WithPEM(true) option, this function assumes that the given input
|
||||
// is PEM encoded ASN.1 DER format key.
|
||||
//
|
||||
// Note that a successful parsing of any type of key does NOT necessarily
|
||||
// guarantee a valid key. For example, no checks against expiration dates
|
||||
// are performed for certificate expiration, no checks against missing
|
||||
// parameters are performed, etc.
|
||||
func ParseKey(data []byte, options ...ParseOption) (Key, error) {
|
||||
var parsePEM bool
|
||||
var localReg *json.Registry
|
||||
var pemDecoder PEMDecoder
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identPEM{}:
|
||||
if err := option.Value(&parsePEM); err != nil {
|
||||
return nil, fmt.Errorf(`failed to retrieve PEM option value: %w`, err)
|
||||
}
|
||||
case identPEMDecoder{}:
|
||||
if err := option.Value(&pemDecoder); err != nil {
|
||||
return nil, fmt.Errorf(`failed to retrieve PEMDecoder option value: %w`, err)
|
||||
}
|
||||
case identLocalRegistry{}:
|
||||
if err := option.Value(&localReg); err != nil {
|
||||
return nil, fmt.Errorf(`failed to retrieve local registry option value: %w`, err)
|
||||
}
|
||||
case identTypedField{}:
|
||||
var pair typedFieldPair // temporary var needed for typed field
|
||||
if err := option.Value(&pair); err != nil {
|
||||
return nil, fmt.Errorf(`failed to retrieve typed field option value: %w`, err)
|
||||
}
|
||||
if localReg == nil {
|
||||
localReg = json.NewRegistry()
|
||||
}
|
||||
localReg.Register(pair.Name, pair.Value)
|
||||
case identIgnoreParseError{}:
|
||||
return nil, fmt.Errorf(`jwk.WithIgnoreParseError() cannot be used for ParseKey()`)
|
||||
}
|
||||
}
|
||||
|
||||
if parsePEM {
|
||||
var raw any
|
||||
|
||||
// PEMDecoder should probably be deprecated, because of being a misnomer.
|
||||
if pemDecoder != nil {
|
||||
if err := decodeX509WithPEMDEcoder(&raw, data, pemDecoder); err != nil {
|
||||
return nil, fmt.Errorf(`failed to decode PEM encoded key: %w`, err)
|
||||
}
|
||||
} else {
|
||||
// This version takes into account the various X509 decoders that are
|
||||
// pre-registered.
|
||||
if err := decodeX509(&raw, data); err != nil {
|
||||
return nil, fmt.Errorf(`failed to decode X.509 encoded key: %w`, err)
|
||||
}
|
||||
}
|
||||
return Import(raw)
|
||||
}
|
||||
|
||||
probe, err := keyProbe.Probe(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`jwk.Parse: failed to probe data: %w`, err)
|
||||
}
|
||||
|
||||
unmarshaler := keyUnmarshaler{localReg: localReg}
|
||||
|
||||
muKeyParser.RLock()
|
||||
parsers := make([]KeyParser, len(keyParsers))
|
||||
copy(parsers, keyParsers)
|
||||
muKeyParser.RUnlock()
|
||||
|
||||
for i := len(parsers) - 1; i >= 0; i-- {
|
||||
parser := parsers[i]
|
||||
key, err := parser.ParseKey(probe, &unmarshaler, data)
|
||||
if err == nil {
|
||||
return key, nil
|
||||
}
|
||||
|
||||
if errors.Is(err, ContinueError()) {
|
||||
continue
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
return nil, fmt.Errorf(`jwk.Parse: no parser was able to parse the key`)
|
||||
}
|
||||
|
||||
// Parse parses JWK from the incoming []byte.
|
||||
//
|
||||
// For JWK sets, this is a convenience function. You could just as well
|
||||
// call `json.Unmarshal` against an empty set created by `jwk.NewSet()`
|
||||
// to parse a JSON buffer into a `jwk.Set`.
|
||||
//
|
||||
// This function exists because many times the user does not know before hand
|
||||
// if a JWK(s) resource at a remote location contains a single JWK key or
|
||||
// a JWK set, and `jwk.Parse()` can handle either case, returning a JWK Set
|
||||
// even if the data only contains a single JWK key
|
||||
//
|
||||
// If you are looking for more information on how JWKs are parsed, or if
|
||||
// you know for sure that you have a single key, please see the documentation
|
||||
// for `jwk.ParseKey()`.
|
||||
func Parse(src []byte, options ...ParseOption) (Set, error) {
|
||||
var parsePEM bool
|
||||
var parseX509 bool
|
||||
var localReg *json.Registry
|
||||
var ignoreParseError bool
|
||||
var pemDecoder PEMDecoder
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identPEM{}:
|
||||
if err := option.Value(&parsePEM); err != nil {
|
||||
return nil, parseerr(`failed to retrieve PEM option value: %w`, err)
|
||||
}
|
||||
case identX509{}:
|
||||
if err := option.Value(&parseX509); err != nil {
|
||||
return nil, parseerr(`failed to retrieve X509 option value: %w`, err)
|
||||
}
|
||||
case identPEMDecoder{}:
|
||||
if err := option.Value(&pemDecoder); err != nil {
|
||||
return nil, parseerr(`failed to retrieve PEMDecoder option value: %w`, err)
|
||||
}
|
||||
case identIgnoreParseError{}:
|
||||
if err := option.Value(&ignoreParseError); err != nil {
|
||||
return nil, parseerr(`failed to retrieve IgnoreParseError option value: %w`, err)
|
||||
}
|
||||
case identTypedField{}:
|
||||
var pair typedFieldPair // temporary var needed for typed field
|
||||
if err := option.Value(&pair); err != nil {
|
||||
return nil, parseerr(`failed to retrieve typed field option value: %w`, err)
|
||||
}
|
||||
if localReg == nil {
|
||||
localReg = json.NewRegistry()
|
||||
}
|
||||
localReg.Register(pair.Name, pair.Value)
|
||||
}
|
||||
}
|
||||
|
||||
s := NewSet()
|
||||
|
||||
if parsePEM || parseX509 {
|
||||
if pemDecoder == nil {
|
||||
pemDecoder = NewPEMDecoder()
|
||||
}
|
||||
src = bytes.TrimSpace(src)
|
||||
for len(src) > 0 {
|
||||
raw, rest, err := pemDecoder.Decode(src)
|
||||
if err != nil {
|
||||
return nil, parseerr(`failed to parse PEM encoded key: %w`, err)
|
||||
}
|
||||
key, err := Import(raw)
|
||||
if err != nil {
|
||||
return nil, parseerr(`failed to create jwk.Key from %T: %w`, raw, err)
|
||||
}
|
||||
if err := s.AddKey(key); err != nil {
|
||||
return nil, parseerr(`failed to add jwk.Key to set: %w`, err)
|
||||
}
|
||||
src = bytes.TrimSpace(rest)
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
if localReg != nil || ignoreParseError {
|
||||
dcKs, ok := s.(KeyWithDecodeCtx)
|
||||
if !ok {
|
||||
return nil, parseerr(`typed field was requested, but the key set (%T) does not support DecodeCtx`, s)
|
||||
}
|
||||
dc := &setDecodeCtx{
|
||||
DecodeCtx: json.NewDecodeCtx(localReg),
|
||||
ignoreParseError: ignoreParseError,
|
||||
}
|
||||
dcKs.SetDecodeCtx(dc)
|
||||
defer func() { dcKs.SetDecodeCtx(nil) }()
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(src, s); err != nil {
|
||||
return nil, parseerr(`failed to unmarshal JWK set: %w`, err)
|
||||
}
|
||||
|
||||
return s, nil
|
||||
}
|
||||
|
||||
// ParseReader parses a JWK set from the incoming byte buffer.
|
||||
func ParseReader(src io.Reader, options ...ParseOption) (Set, error) {
|
||||
// meh, there's no way to tell if a stream has "ended" a single
|
||||
// JWKs except when we encounter an EOF, so just... ReadAll
|
||||
buf, err := io.ReadAll(src)
|
||||
if err != nil {
|
||||
return nil, rparseerr(`failed to read from io.Reader: %w`, err)
|
||||
}
|
||||
|
||||
set, err := Parse(buf, options...)
|
||||
if err != nil {
|
||||
return nil, rparseerr(`failed to parse reader: %w`, err)
|
||||
}
|
||||
return set, nil
|
||||
}
|
||||
|
||||
// ParseString parses a JWK set from the incoming string.
|
||||
func ParseString(s string, options ...ParseOption) (Set, error) {
|
||||
set, err := Parse([]byte(s), options...)
|
||||
if err != nil {
|
||||
return nil, sparseerr(`failed to parse string: %w`, err)
|
||||
}
|
||||
return set, nil
|
||||
}
|
||||
|
||||
// AssignKeyID is a convenience function to automatically assign the "kid"
|
||||
// section of the key, if it already doesn't have one. It uses Key.Thumbprint
|
||||
// method with crypto.SHA256 as the default hashing algorithm
|
||||
func AssignKeyID(key Key, options ...AssignKeyIDOption) error {
|
||||
if key.Has(KeyIDKey) {
|
||||
return nil
|
||||
}
|
||||
|
||||
hash := crypto.SHA256
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identThumbprintHash{}:
|
||||
if err := option.Value(&hash); err != nil {
|
||||
return fmt.Errorf(`failed to retrieve thumbprint hash option value: %w`, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
h, err := key.Thumbprint(hash)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to generate thumbprint: %w`, err)
|
||||
}
|
||||
|
||||
if err := key.Set(KeyIDKey, base64.EncodeToString(h)); err != nil {
|
||||
return fmt.Errorf(`failed to set "kid": %w`, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// NOTE: may need to remove this to allow pluggale key types
|
||||
func cloneKey(src Key) (Key, error) {
|
||||
var dst Key
|
||||
switch src.(type) {
|
||||
case RSAPrivateKey:
|
||||
dst = newRSAPrivateKey()
|
||||
case RSAPublicKey:
|
||||
dst = newRSAPublicKey()
|
||||
case ECDSAPrivateKey:
|
||||
dst = newECDSAPrivateKey()
|
||||
case ECDSAPublicKey:
|
||||
dst = newECDSAPublicKey()
|
||||
case OKPPrivateKey:
|
||||
dst = newOKPPrivateKey()
|
||||
case OKPPublicKey:
|
||||
dst = newOKPPublicKey()
|
||||
case SymmetricKey:
|
||||
dst = newSymmetricKey()
|
||||
default:
|
||||
return nil, fmt.Errorf(`jwk.cloneKey: unknown key type %T`, src)
|
||||
}
|
||||
|
||||
for _, k := range src.Keys() {
|
||||
// It's absolutely
|
||||
var v any
|
||||
if err := src.Get(k, &v); err != nil {
|
||||
return nil, fmt.Errorf(`jwk.cloneKey: failed to get %q: %w`, k, err)
|
||||
}
|
||||
if err := dst.Set(k, v); err != nil {
|
||||
return nil, fmt.Errorf(`jwk.cloneKey: failed to set %q: %w`, k, err)
|
||||
}
|
||||
}
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
// Pem serializes the given jwk.Key in PEM encoded ASN.1 DER format,
|
||||
// using either PKCS8 for private keys and PKIX for public keys.
|
||||
// If you need to encode using PKCS1 or SEC1, you must do it yourself.
|
||||
//
|
||||
// # Argument must be of type jwk.Key or jwk.Set
|
||||
//
|
||||
// Currently only EC (including Ed25519) and RSA keys (and jwk.Set
|
||||
// comprised of these key types) are supported.
|
||||
func Pem(v any) ([]byte, error) {
|
||||
var set Set
|
||||
switch v := v.(type) {
|
||||
case Key:
|
||||
set = NewSet()
|
||||
if err := set.AddKey(v); err != nil {
|
||||
return nil, fmt.Errorf(`failed to add key to set: %w`, err)
|
||||
}
|
||||
case Set:
|
||||
set = v
|
||||
default:
|
||||
return nil, fmt.Errorf(`argument to Pem must be either jwk.Key or jwk.Set: %T`, v)
|
||||
}
|
||||
|
||||
var ret []byte
|
||||
for i := range set.Len() {
|
||||
key, _ := set.Key(i)
|
||||
typ, buf, err := asnEncode(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to encode content for key #%d: %w`, i, err)
|
||||
}
|
||||
|
||||
var block pem.Block
|
||||
block.Type = typ
|
||||
block.Bytes = buf
|
||||
ret = append(ret, pem.EncodeToMemory(&block)...)
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func asnEncode(key Key) (string, []byte, error) {
|
||||
switch key := key.(type) {
|
||||
case ECDSAPrivateKey:
|
||||
var rawkey ecdsa.PrivateKey
|
||||
if err := Export(key, &rawkey); err != nil {
|
||||
return "", nil, fmt.Errorf(`failed to get raw key from jwk.Key: %w`, err)
|
||||
}
|
||||
buf, err := x509.MarshalECPrivateKey(&rawkey)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf(`failed to marshal PKCS8: %w`, err)
|
||||
}
|
||||
return pmECPrivateKey, buf, nil
|
||||
case RSAPrivateKey, OKPPrivateKey:
|
||||
var rawkey any
|
||||
if err := Export(key, &rawkey); err != nil {
|
||||
return "", nil, fmt.Errorf(`failed to get raw key from jwk.Key: %w`, err)
|
||||
}
|
||||
buf, err := x509.MarshalPKCS8PrivateKey(rawkey)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf(`failed to marshal PKCS8: %w`, err)
|
||||
}
|
||||
return pmPrivateKey, buf, nil
|
||||
case RSAPublicKey, ECDSAPublicKey, OKPPublicKey:
|
||||
var rawkey any
|
||||
if err := Export(key, &rawkey); err != nil {
|
||||
return "", nil, fmt.Errorf(`failed to get raw key from jwk.Key: %w`, err)
|
||||
}
|
||||
buf, err := x509.MarshalPKIXPublicKey(rawkey)
|
||||
if err != nil {
|
||||
return "", nil, fmt.Errorf(`failed to marshal PKIX: %w`, err)
|
||||
}
|
||||
return pmPublicKey, buf, nil
|
||||
default:
|
||||
return "", nil, fmt.Errorf(`unsupported key type %T`, key)
|
||||
}
|
||||
}
|
||||
|
||||
type CustomDecoder = json.CustomDecoder
|
||||
type CustomDecodeFunc = json.CustomDecodeFunc
|
||||
|
||||
// RegisterCustomField allows users to specify that a private field
|
||||
// be decoded as an instance of the specified type. This option has
|
||||
// a global effect.
|
||||
//
|
||||
// For example, suppose you have a custom field `x-birthday`, which
|
||||
// you want to represent as a string formatted in RFC3339 in JSON,
|
||||
// but want it back as `time.Time`.
|
||||
//
|
||||
// In such case you would register a custom field as follows
|
||||
//
|
||||
// jwk.RegisterCustomField(`x-birthday`, time.Time{})
|
||||
//
|
||||
// Then you can use a `time.Time` variable to extract the value
|
||||
// of `x-birthday` field, instead of having to use `any`
|
||||
// and later convert it to `time.Time`
|
||||
//
|
||||
// var bday time.Time
|
||||
// _ = key.Get(`x-birthday`, &bday)
|
||||
//
|
||||
// If you need a more fine-tuned control over the decoding process,
|
||||
// you can register a `CustomDecoder`. For example, below shows
|
||||
// how to register a decoder that can parse RFC1123 format string:
|
||||
//
|
||||
// jwk.RegisterCustomField(`x-birthday`, jwk.CustomDecodeFunc(func(data []byte) (any, error) {
|
||||
// return time.Parse(time.RFC1123, string(data))
|
||||
// }))
|
||||
//
|
||||
// Please note that use of custom fields can be problematic if you
|
||||
// are using a library that does not implement MarshalJSON/UnmarshalJSON
|
||||
// and you try to roundtrip from an object to JSON, and then back to an object.
|
||||
// For example, in the above example, you can _parse_ time values formatted
|
||||
// in the format specified in RFC822, but when you convert an object into
|
||||
// JSON, it will be formatted in RFC3339, because that's what `time.Time`
|
||||
// likes to do. To avoid this, it's always better to use a custom type
|
||||
// that wraps your desired type (in this case `time.Time`) and implement
|
||||
// MarshalJSON and UnmashalJSON.
|
||||
func RegisterCustomField(name string, object any) {
|
||||
registry.Register(name, object)
|
||||
}
|
||||
|
||||
// Equal compares two keys and returns true if they are equal. The comparison
|
||||
// is solely done against the thumbprints of k1 and k2. It is possible for keys
|
||||
// that have, for example, different key IDs, key usage, etc, to be considered equal.
|
||||
func Equal(k1, k2 Key) bool {
|
||||
h := crypto.SHA256
|
||||
tp1, err := k1.Thumbprint(h)
|
||||
if err != nil {
|
||||
return false // can't report error
|
||||
}
|
||||
tp2, err := k2.Thumbprint(h)
|
||||
if err != nil {
|
||||
return false // can't report error
|
||||
}
|
||||
|
||||
return bytes.Equal(tp1, tp2)
|
||||
}
|
||||
|
||||
// IsPrivateKey returns true if the supplied key is a private key of an
|
||||
// asymmetric key pair. The argument `k` must implement the `AsymmetricKey`
|
||||
// interface.
|
||||
//
|
||||
// An error is returned if the supplied key is not an `AsymmetricKey`.
|
||||
func IsPrivateKey(k Key) (bool, error) {
|
||||
asymmetric, ok := k.(AsymmetricKey)
|
||||
if ok {
|
||||
return asymmetric.IsPrivate(), nil
|
||||
}
|
||||
return false, fmt.Errorf("jwk.IsPrivateKey: %T is not an asymmetric key", k)
|
||||
}
|
||||
|
||||
type keyValidationError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (e *keyValidationError) Error() string {
|
||||
return fmt.Sprintf(`key validation failed: %s`, e.err)
|
||||
}
|
||||
|
||||
func (e *keyValidationError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func (e *keyValidationError) Is(target error) bool {
|
||||
_, ok := target.(*keyValidationError)
|
||||
return ok
|
||||
}
|
||||
|
||||
// NewKeyValidationError wraps the given error with an error that denotes
|
||||
// `key.Validate()` has failed. This error type should ONLY be used as
|
||||
// return value from the `Validate()` method.
|
||||
func NewKeyValidationError(err error) error {
|
||||
return &keyValidationError{err: err}
|
||||
}
|
||||
|
||||
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
|
||||
for _, option := range options {
|
||||
switch option.Ident() {
|
||||
case identStrictKeyUsage{}:
|
||||
var v bool
|
||||
if err := option.Value(&v); err != nil {
|
||||
continue
|
||||
}
|
||||
strictKeyUsagePtr = &v
|
||||
}
|
||||
}
|
||||
|
||||
if strictKeyUsagePtr != nil {
|
||||
strictKeyUsage.Store(*strictKeyUsagePtr)
|
||||
}
|
||||
}
|
||||
|
||||
// These are used when validating keys.
|
||||
type keyWithD interface {
|
||||
D() ([]byte, bool)
|
||||
}
|
||||
|
||||
var _ keyWithD = &okpPrivateKey{}
|
||||
|
||||
func extractEmbeddedKey(keyif Key, concretTypes []reflect.Type) (Key, error) {
|
||||
rv := reflect.ValueOf(keyif)
|
||||
|
||||
// If the value can be converted to one of the concrete types, then we're done
|
||||
if slices.ContainsFunc(concretTypes, func(t reflect.Type) bool {
|
||||
return rv.Type().ConvertibleTo(t)
|
||||
}) {
|
||||
return keyif, nil
|
||||
}
|
||||
|
||||
// When a struct implements the Key interface via embedding, you unfortunately
|
||||
// cannot use a type switch to determine the concrete type, because
|
||||
if rv.Kind() == reflect.Ptr {
|
||||
if rv.IsNil() {
|
||||
return nil, fmt.Errorf(`invalid key value (0): %w`, ContinueError())
|
||||
}
|
||||
rv = rv.Elem()
|
||||
}
|
||||
|
||||
if rv.Kind() != reflect.Struct {
|
||||
return nil, fmt.Errorf(`invalid key value type %T (1): %w`, keyif, ContinueError())
|
||||
}
|
||||
if rv.NumField() == 0 {
|
||||
return nil, fmt.Errorf(`invalid key value type %T (2): %w`, keyif, ContinueError())
|
||||
}
|
||||
// Iterate through the fields of the struct to find the first field that
|
||||
// implements the Key interface
|
||||
rt := rv.Type()
|
||||
for i := range rv.NumField() {
|
||||
field := rv.Field(i)
|
||||
ft := rt.Field(i)
|
||||
if !ft.Anonymous {
|
||||
// We can only salvage this object if the object implements jwk.Key
|
||||
// via embedding, so we skip fields that are not anonymous
|
||||
continue
|
||||
}
|
||||
|
||||
if field.CanInterface() {
|
||||
if k, ok := field.Interface().(Key); ok {
|
||||
return extractEmbeddedKey(k, concretTypes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf(`invalid key value type %T (3): %w`, keyif, ContinueError())
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
load("@rules_go//go:def.bzl", "go_library")
|
||||
|
||||
go_library(
|
||||
name = "jwkbb",
|
||||
srcs = ["x509.go"],
|
||||
importpath = "github.com/lestrrat-go/jwx/v3/jwk/jwkbb",
|
||||
visibility = ["//visibility:public"],
|
||||
deps = [
|
||||
"@com_github_lestrrat_go_blackmagic//:blackmagic",
|
||||
],
|
||||
)
|
||||
|
||||
alias(
|
||||
name = "go_default_library",
|
||||
actual = ":jwkbb",
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package jwkbb
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
|
||||
"github.com/lestrrat-go/blackmagic"
|
||||
)
|
||||
|
||||
const (
|
||||
PrivateKeyBlockType = `PRIVATE KEY`
|
||||
PublicKeyBlockType = `PUBLIC KEY`
|
||||
ECPrivateKeyBlockType = `EC PRIVATE KEY`
|
||||
RSAPublicKeyBlockType = `RSA PUBLIC KEY`
|
||||
RSAPrivateKeyBlockType = `RSA PRIVATE KEY`
|
||||
CertificateBlockType = `CERTIFICATE`
|
||||
)
|
||||
|
||||
// EncodeX509 encodes the given value into ASN.1 DER format, and returns
|
||||
// the encoded bytes. The value must be one of the following types:
|
||||
// *rsa.PrivateKey, *ecdsa.PrivateKey, ed25519.PrivateKey,
|
||||
// *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey.
|
||||
//
|
||||
// Users can pass a pre-allocated byte slice (but make sure its length is
|
||||
// changed so that the encoded buffer is appended to the correct location)
|
||||
// as `dst` to avoid allocations.
|
||||
func EncodeX509(dst []byte, v any) ([]byte, error) {
|
||||
var block pem.Block
|
||||
// Try to convert it into a certificate
|
||||
switch v := v.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
block.Type = RSAPrivateKeyBlockType
|
||||
block.Bytes = x509.MarshalPKCS1PrivateKey(v)
|
||||
case *ecdsa.PrivateKey:
|
||||
marshaled, err := x509.MarshalECPrivateKey(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
block.Type = ECPrivateKeyBlockType
|
||||
block.Bytes = marshaled
|
||||
case ed25519.PrivateKey:
|
||||
marshaled, err := x509.MarshalPKCS8PrivateKey(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
block.Type = PrivateKeyBlockType
|
||||
block.Bytes = marshaled
|
||||
case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey:
|
||||
marshaled, err := x509.MarshalPKIXPublicKey(v)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
block.Type = PublicKeyBlockType
|
||||
block.Bytes = marshaled
|
||||
default:
|
||||
return nil, fmt.Errorf(`unsupported type %T for ASN.1 DER encoding`, v)
|
||||
}
|
||||
|
||||
encoded := pem.EncodeToMemory(&block)
|
||||
dst = append(dst, encoded...)
|
||||
return dst, nil
|
||||
}
|
||||
|
||||
func DecodeX509(dst any, block *pem.Block) error {
|
||||
switch block.Type {
|
||||
// Handle the semi-obvious cases
|
||||
case RSAPrivateKeyBlockType:
|
||||
key, err := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to parse PKCS1 private key: %w`, err)
|
||||
}
|
||||
return blackmagic.AssignIfCompatible(dst, key)
|
||||
case RSAPublicKeyBlockType:
|
||||
key, err := x509.ParsePKCS1PublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to parse PKCS1 public key: %w`, err)
|
||||
}
|
||||
return blackmagic.AssignIfCompatible(dst, key)
|
||||
case ECPrivateKeyBlockType:
|
||||
key, err := x509.ParseECPrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to parse EC private key: %w`, err)
|
||||
}
|
||||
return blackmagic.AssignIfCompatible(dst, key)
|
||||
case PublicKeyBlockType:
|
||||
// XXX *could* return dsa.PublicKey
|
||||
key, err := x509.ParsePKIXPublicKey(block.Bytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to parse PKIX public key: %w`, err)
|
||||
}
|
||||
return blackmagic.AssignIfCompatible(dst, key)
|
||||
case PrivateKeyBlockType:
|
||||
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to parse PKCS8 private key: %w`, err)
|
||||
}
|
||||
return blackmagic.AssignIfCompatible(dst, key)
|
||||
case CertificateBlockType:
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to parse certificate: %w`, err)
|
||||
}
|
||||
return blackmagic.AssignIfCompatible(dst, cert.PublicKey)
|
||||
default:
|
||||
return fmt.Errorf(`invalid PEM block type %s`, block.Type)
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package jwk
|
||||
|
||||
import "fmt"
|
||||
|
||||
func (ops *KeyOperationList) Get() KeyOperationList {
|
||||
if ops == nil {
|
||||
return nil
|
||||
}
|
||||
return *ops
|
||||
}
|
||||
|
||||
func (ops *KeyOperationList) Accept(v any) error {
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
return ops.Accept([]string{x})
|
||||
case []any:
|
||||
l := make([]string, len(x))
|
||||
for i, e := range x {
|
||||
if es, ok := e.(string); ok {
|
||||
l[i] = es
|
||||
} else {
|
||||
return fmt.Errorf(`invalid list element type: expected string, got %T`, v)
|
||||
}
|
||||
}
|
||||
return ops.Accept(l)
|
||||
case []string:
|
||||
list := make(KeyOperationList, len(x))
|
||||
for i, e := range x {
|
||||
switch e := KeyOperation(e); e {
|
||||
case KeyOpSign, KeyOpVerify, KeyOpEncrypt, KeyOpDecrypt, KeyOpWrapKey, KeyOpUnwrapKey, KeyOpDeriveKey, KeyOpDeriveBits:
|
||||
list[i] = e
|
||||
default:
|
||||
return fmt.Errorf(`invalid keyoperation %v`, e)
|
||||
}
|
||||
}
|
||||
|
||||
*ops = list
|
||||
return nil
|
||||
case []KeyOperation:
|
||||
list := make(KeyOperationList, len(x))
|
||||
for i, e := range x {
|
||||
switch e {
|
||||
case KeyOpSign, KeyOpVerify, KeyOpEncrypt, KeyOpDecrypt, KeyOpWrapKey, KeyOpUnwrapKey, KeyOpDeriveKey, KeyOpDeriveBits:
|
||||
list[i] = e
|
||||
default:
|
||||
return fmt.Errorf(`invalid keyoperation %v`, e)
|
||||
}
|
||||
}
|
||||
|
||||
*ops = list
|
||||
return nil
|
||||
case KeyOperationList:
|
||||
*ops = x
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf(`invalid value %T`, v)
|
||||
}
|
||||
}
|
||||
+321
@@ -0,0 +1,321 @@
|
||||
package jwk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto"
|
||||
"crypto/ecdh"
|
||||
"crypto/ed25519"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/lestrrat-go/blackmagic"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/base64"
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterKeyExporter(jwa.OKP(), KeyExportFunc(okpJWKToRaw))
|
||||
}
|
||||
|
||||
// Mental note:
|
||||
//
|
||||
// Curve25519 refers to a particular curve, and is represented in its Montgomery form.
|
||||
//
|
||||
// Ed25519 refers to the biratinally equivalent curve of Curve25519, except it's in Edwards form.
|
||||
// Ed25519 is the name of the curve and the also the signature scheme using that curve.
|
||||
// The full name of the scheme is Edwards Curve Digital Signature Algorithm, and thus it is
|
||||
// also referred to as EdDSA.
|
||||
//
|
||||
// X25519 refers to the Diffie-Hellman key exchange protocol that uses Cruve25519.
|
||||
// Because this is an elliptic curve based Diffie Hellman protocol, it is also referred to
|
||||
// as ECDH.
|
||||
//
|
||||
// OKP keys are used to represent private/public pairs of thse elliptic curve
|
||||
// keys. But note that the name just means Octet Key Pair.
|
||||
|
||||
func (k *okpPublicKey) Import(rawKeyIf any) error {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
|
||||
var crv jwa.EllipticCurveAlgorithm
|
||||
switch rawKey := rawKeyIf.(type) {
|
||||
case ed25519.PublicKey:
|
||||
k.x = rawKey
|
||||
crv = jwa.Ed25519()
|
||||
k.crv = &crv
|
||||
case *ecdh.PublicKey:
|
||||
k.x = rawKey.Bytes()
|
||||
crv = jwa.X25519()
|
||||
k.crv = &crv
|
||||
default:
|
||||
return fmt.Errorf(`unknown key type %T`, rawKeyIf)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *okpPrivateKey) Import(rawKeyIf any) error {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
|
||||
var crv jwa.EllipticCurveAlgorithm
|
||||
switch rawKey := rawKeyIf.(type) {
|
||||
case ed25519.PrivateKey:
|
||||
k.d = rawKey.Seed()
|
||||
k.x = rawKey.Public().(ed25519.PublicKey) //nolint:forcetypeassert
|
||||
crv = jwa.Ed25519()
|
||||
k.crv = &crv
|
||||
case *ecdh.PrivateKey:
|
||||
// k.d = rawKey.Seed()
|
||||
k.d = rawKey.Bytes()
|
||||
k.x = rawKey.PublicKey().Bytes()
|
||||
crv = jwa.X25519()
|
||||
k.crv = &crv
|
||||
default:
|
||||
return fmt.Errorf(`unknown key type %T`, rawKeyIf)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildOKPPublicKey(alg jwa.EllipticCurveAlgorithm, xbuf []byte) (any, error) {
|
||||
switch alg {
|
||||
case jwa.Ed25519():
|
||||
return ed25519.PublicKey(xbuf), nil
|
||||
case jwa.X25519():
|
||||
ret, err := ecdh.X25519().NewPublicKey(xbuf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to parse x25519 public key %x (size %d): %w`, xbuf, len(xbuf), err)
|
||||
}
|
||||
return ret, nil
|
||||
default:
|
||||
return nil, fmt.Errorf(`invalid curve algorithm %s`, alg)
|
||||
}
|
||||
}
|
||||
|
||||
// Raw returns the EC-DSA public key represented by this JWK
|
||||
func (k *okpPublicKey) Raw(v any) error {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
|
||||
crv, ok := k.Crv()
|
||||
if !ok {
|
||||
return fmt.Errorf(`missing "crv" field`)
|
||||
}
|
||||
|
||||
pubk, err := buildOKPPublicKey(crv, k.x)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`jwk.OKPPublicKey: failed to build public key: %w`, err)
|
||||
}
|
||||
|
||||
if err := blackmagic.AssignIfCompatible(v, pubk); err != nil {
|
||||
return fmt.Errorf(`jwk.OKPPublicKey: failed to assign to destination variable: %w`, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildOKPPrivateKey(alg jwa.EllipticCurveAlgorithm, xbuf []byte, dbuf []byte) (any, error) {
|
||||
if len(dbuf) == 0 {
|
||||
return nil, fmt.Errorf(`cannot use empty seed`)
|
||||
}
|
||||
switch alg {
|
||||
case jwa.Ed25519():
|
||||
if len(dbuf) != ed25519.SeedSize {
|
||||
return nil, fmt.Errorf(`ed25519: wrong private key size`)
|
||||
}
|
||||
ret := ed25519.NewKeyFromSeed(dbuf)
|
||||
//nolint:forcetypeassert
|
||||
if !bytes.Equal(xbuf, ret.Public().(ed25519.PublicKey)) {
|
||||
return nil, fmt.Errorf(`ed25519: invalid x value given d value`)
|
||||
}
|
||||
return ret, nil
|
||||
case jwa.X25519():
|
||||
ret, err := ecdh.X25519().NewPrivateKey(dbuf)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`x25519: unable to construct x25519 private key from seed: %w`, err)
|
||||
}
|
||||
return ret, nil
|
||||
default:
|
||||
return nil, fmt.Errorf(`invalid curve algorithm %s`, alg)
|
||||
}
|
||||
}
|
||||
|
||||
var okpConvertibleKeys = []reflect.Type{
|
||||
reflect.TypeFor[OKPPrivateKey](),
|
||||
reflect.TypeFor[OKPPublicKey](),
|
||||
}
|
||||
|
||||
// This is half baked. I think it will blow up if we used ecdh.* keys and/or x25519 keys
|
||||
func okpJWKToRaw(key Key, _ any /* this is unused because this is half baked */) (any, error) {
|
||||
extracted, err := extractEmbeddedKey(key, okpConvertibleKeys)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`jwk.OKP: failed to extract embedded key: %w`, err)
|
||||
}
|
||||
|
||||
switch key := extracted.(type) {
|
||||
case OKPPrivateKey:
|
||||
locker, ok := key.(rlocker)
|
||||
if ok {
|
||||
locker.rlock()
|
||||
defer locker.runlock()
|
||||
}
|
||||
|
||||
crv, ok := key.Crv()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "crv" field`)
|
||||
}
|
||||
|
||||
x, ok := key.X()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "x" field`)
|
||||
}
|
||||
|
||||
d, ok := key.D()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "d" field`)
|
||||
}
|
||||
|
||||
privk, err := buildOKPPrivateKey(crv, x, d)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`jwk.OKPPrivateKey: failed to build private key: %w`, err)
|
||||
}
|
||||
return privk, nil
|
||||
case OKPPublicKey:
|
||||
locker, ok := key.(rlocker)
|
||||
if ok {
|
||||
locker.rlock()
|
||||
defer locker.runlock()
|
||||
}
|
||||
|
||||
crv, ok := key.Crv()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "crv" field`)
|
||||
}
|
||||
|
||||
x, ok := key.X()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "x" field`)
|
||||
}
|
||||
pubk, err := buildOKPPublicKey(crv, x)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`jwk.OKPPublicKey: failed to build public key: %w`, err)
|
||||
}
|
||||
return pubk, nil
|
||||
default:
|
||||
return nil, ContinueError()
|
||||
}
|
||||
}
|
||||
|
||||
func makeOKPPublicKey(src Key) (Key, error) {
|
||||
newKey := newOKPPublicKey()
|
||||
|
||||
// Iterate and copy everything except for the bits that should not be in the public key
|
||||
for _, k := range src.Keys() {
|
||||
switch k {
|
||||
case OKPDKey:
|
||||
continue
|
||||
default:
|
||||
var v any
|
||||
if err := src.Get(k, &v); err != nil {
|
||||
return nil, fmt.Errorf(`failed to get field %q: %w`, k, err)
|
||||
}
|
||||
|
||||
if err := newKey.Set(k, v); err != nil {
|
||||
return nil, fmt.Errorf(`failed to set field %q: %w`, k, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newKey, nil
|
||||
}
|
||||
|
||||
func (k *okpPrivateKey) PublicKey() (Key, error) {
|
||||
return makeOKPPublicKey(k)
|
||||
}
|
||||
|
||||
func (k *okpPublicKey) PublicKey() (Key, error) {
|
||||
return makeOKPPublicKey(k)
|
||||
}
|
||||
|
||||
func okpThumbprint(hash crypto.Hash, crv, x string) []byte {
|
||||
h := hash.New()
|
||||
fmt.Fprint(h, `{"crv":"`)
|
||||
fmt.Fprint(h, crv)
|
||||
fmt.Fprint(h, `","kty":"OKP","x":"`)
|
||||
fmt.Fprint(h, x)
|
||||
fmt.Fprint(h, `"}`)
|
||||
return h.Sum(nil)
|
||||
}
|
||||
|
||||
// Thumbprint returns the JWK thumbprint using the indicated
|
||||
// hashing algorithm, according to RFC 7638 / 8037
|
||||
func (k okpPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
|
||||
crv, ok := k.Crv()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "crv" field`)
|
||||
}
|
||||
return okpThumbprint(
|
||||
hash,
|
||||
crv.String(),
|
||||
base64.EncodeToString(k.x),
|
||||
), nil
|
||||
}
|
||||
|
||||
// Thumbprint returns the JWK thumbprint using the indicated
|
||||
// hashing algorithm, according to RFC 7638 / 8037
|
||||
func (k okpPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
|
||||
crv, ok := k.Crv()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "crv" field`)
|
||||
}
|
||||
|
||||
return okpThumbprint(
|
||||
hash,
|
||||
crv.String(),
|
||||
base64.EncodeToString(k.x),
|
||||
), nil
|
||||
}
|
||||
|
||||
func validateOKPKey(key interface {
|
||||
Crv() (jwa.EllipticCurveAlgorithm, bool)
|
||||
X() ([]byte, bool)
|
||||
}) error {
|
||||
if v, ok := key.Crv(); !ok || v == jwa.InvalidEllipticCurve() {
|
||||
return fmt.Errorf(`invalid curve algorithm`)
|
||||
}
|
||||
|
||||
if v, ok := key.X(); !ok || len(v) == 0 {
|
||||
return fmt.Errorf(`missing "x" field`)
|
||||
}
|
||||
|
||||
if priv, ok := key.(keyWithD); ok {
|
||||
if d, ok := priv.D(); !ok || len(d) == 0 {
|
||||
return fmt.Errorf(`missing "d" field`)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *okpPublicKey) Validate() error {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
if err := validateOKPKey(k); err != nil {
|
||||
return NewKeyValidationError(fmt.Errorf(`jwk.OKPPublicKey: %w`, err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *okpPrivateKey) Validate() error {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
if err := validateOKPKey(k); err != nil {
|
||||
return NewKeyValidationError(fmt.Errorf(`jwk.OKPPrivateKey: %w`, err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+1347
File diff suppressed because it is too large
Load Diff
+76
@@ -0,0 +1,76 @@
|
||||
package jwk
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/lestrrat-go/httprc/v3"
|
||||
"github.com/lestrrat-go/option/v2"
|
||||
)
|
||||
|
||||
type identTypedField struct{}
|
||||
|
||||
type typedFieldPair struct {
|
||||
Name string
|
||||
Value any
|
||||
}
|
||||
|
||||
// WithTypedField allows a private field to be parsed into the object type of
|
||||
// your choice. It works much like the RegisterCustomField, but the effect
|
||||
// is only applicable to the jwt.Parse function call which receives this option.
|
||||
//
|
||||
// While this can be extremely useful, this option should be used with caution:
|
||||
// There are many caveats that your entire team/user-base needs to be aware of,
|
||||
// and therefore in general its use is discouraged. Only use it when you know
|
||||
// what you are doing, and you document its use clearly for others.
|
||||
//
|
||||
// First and foremost, this is a "per-object" option. Meaning that given the same
|
||||
// serialized format, it is possible to generate two objects whose internal
|
||||
// representations may differ. That is, if you parse one _WITH_ the option,
|
||||
// and the other _WITHOUT_, their internal representation may completely differ.
|
||||
// This could potentially lead to problems.
|
||||
//
|
||||
// Second, specifying this option will slightly slow down the decoding process
|
||||
// as it needs to consult multiple definitions sources (global and local), so
|
||||
// be careful if you are decoding a large number of tokens, as the effects will stack up.
|
||||
func WithTypedField(name string, object any) ParseOption {
|
||||
return &parseOption{
|
||||
option.New(identTypedField{},
|
||||
typedFieldPair{Name: name, Value: object},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
type registerResourceOption struct {
|
||||
option.Interface
|
||||
}
|
||||
|
||||
func (registerResourceOption) registerOption() {}
|
||||
func (registerResourceOption) resourceOption() {}
|
||||
|
||||
type identNewResourceOption struct{}
|
||||
|
||||
// WithHttprcResourceOption can be used to pass arbitrary `httprc.NewResourceOption`
|
||||
// to `(httprc.Client).Add` by way of `(jwk.Cache).Register`.
|
||||
func WithHttprcResourceOption(o httprc.NewResourceOption) RegisterOption {
|
||||
return ®isterResourceOption{
|
||||
option.New(identNewResourceOption{}, o),
|
||||
}
|
||||
}
|
||||
|
||||
// WithConstantInterval can be used to pass `httprc.WithConstantInterval` option to
|
||||
// `(httprc.Client).Add` by way of `(jwk.Cache).Register`.
|
||||
func WithConstantInterval(d time.Duration) RegisterOption {
|
||||
return WithHttprcResourceOption(httprc.WithConstantInterval(d))
|
||||
}
|
||||
|
||||
// WithMinInterval can be used to pass `httprc.WithMinInterval` option to
|
||||
// `(httprc.Client).Add` by way of `(jwk.Cache).Register`.
|
||||
func WithMinInterval(d time.Duration) RegisterOption {
|
||||
return WithHttprcResourceOption(httprc.WithMinInterval(d))
|
||||
}
|
||||
|
||||
// WithMaxInterval can be used to pass `httprc.WithMaxInterval` option to
|
||||
// `(httprc.Client).Add` by way of `(jwk.Cache).Register`.
|
||||
func WithMaxInterval(d time.Duration) RegisterOption {
|
||||
return WithHttprcResourceOption(httprc.WithMaxInterval(d))
|
||||
}
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
package_name: jwk
|
||||
output: jwk/options_gen.go
|
||||
interfaces:
|
||||
- name: CacheOption
|
||||
comment: |
|
||||
CacheOption is a type of Option that can be passed to the
|
||||
the `jwk.NewCache()` function.
|
||||
- name: ResourceOption
|
||||
comment: |
|
||||
ResourceOption is a type of Option that can be passed to the `httprc.NewResource` function
|
||||
by way of RegisterOption.
|
||||
- name: AssignKeyIDOption
|
||||
- name: FetchOption
|
||||
methods:
|
||||
- fetchOption
|
||||
- parseOption
|
||||
- registerOption
|
||||
comment: |
|
||||
FetchOption is a type of Option that can be passed to `jwk.Fetch()`
|
||||
FetchOption also implements the `RegisterOption`, and thus can
|
||||
safely be passed to `(*jwk.Cache).Register()`
|
||||
- name: ParseOption
|
||||
methods:
|
||||
- fetchOption
|
||||
- registerOption
|
||||
- readFileOption
|
||||
comment: |
|
||||
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()`
|
||||
- name: ReadFileOption
|
||||
comment: |
|
||||
ReadFileOption is a type of `Option` that can be passed to `jwk.ReadFile`
|
||||
- name: RegisterOption
|
||||
comment: |
|
||||
RegisterOption describes options that can be passed to `(jwk.Cache).Register()`
|
||||
- name: RegisterFetchOption
|
||||
methods:
|
||||
- fetchOption
|
||||
- registerOption
|
||||
- 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
|
||||
argument_type: HTTPClient
|
||||
comment: |
|
||||
WithHTTPClient allows users to specify the "net/http".Client object that
|
||||
is used when fetching jwk.Set objects.
|
||||
- ident: ThumbprintHash
|
||||
interface: AssignKeyIDOption
|
||||
argument_type: crypto.Hash
|
||||
- ident: LocalRegistry
|
||||
option_name: withLocalRegistry
|
||||
interface: ParseOption
|
||||
argument_type: '*json.Registry'
|
||||
comment: This option is only available for internal code. Users don't get to play with it
|
||||
- ident: PEM
|
||||
interface: ParseOption
|
||||
argument_type: bool
|
||||
comment: |
|
||||
WithPEM specifies that the input to `Parse()` is a PEM encoded key.
|
||||
|
||||
This option is planned to be deprecated in the future. The plan is to
|
||||
replace it with `jwk.WithX509(true)`
|
||||
- ident: X509
|
||||
interface: ParseOption
|
||||
argument_type: bool
|
||||
comment: |
|
||||
WithX509 specifies that the input to `Parse()` is an X.509 encoded key
|
||||
- ident: PEMDecoder
|
||||
interface: ParseOption
|
||||
argument_type: PEMDecoder
|
||||
comment: |
|
||||
WithPEMDecoder specifies the PEMDecoder object to use when decoding
|
||||
PEM encoded keys. This option can be passed to `jwk.Parse()`
|
||||
|
||||
This option is planned to be deprecated in the future. The plan is to
|
||||
use `jwk.RegisterX509Decoder()` to register a custom X.509 decoder globally.
|
||||
- ident: FetchWhitelist
|
||||
interface: FetchOption
|
||||
argument_type: Whitelist
|
||||
comment: |
|
||||
WithFetchWhitelist specifies the Whitelist object to use when
|
||||
fetching JWKs from a remote source. This option can be passed
|
||||
to both `jwk.Fetch()`
|
||||
- ident: IgnoreParseError
|
||||
interface: ParseOption
|
||||
argument_type: bool
|
||||
comment: |
|
||||
WithIgnoreParseError is only applicable when used with `jwk.Parse()`
|
||||
(i.e. to parse JWK sets). If passed to `jwk.ParseKey()`, the function
|
||||
will return an error no matter what the input is.
|
||||
|
||||
DO NOT USE WITHOUT EXHAUSTING ALL OTHER ROUTES FIRST.
|
||||
|
||||
The option specifies that errors found during parsing of individual
|
||||
keys are ignored. For example, if you had keys A, B, C where B is
|
||||
invalid (e.g. it does not contain the required fields), then the
|
||||
resulting JWKS will contain keys A and C only.
|
||||
|
||||
This options exists as an escape hatch for those times when a
|
||||
key in a JWKS that is irrelevant for your use case is causing
|
||||
your JWKS parsing to fail, and you want to get to the rest of the
|
||||
keys in the JWKS.
|
||||
|
||||
Again, DO NOT USE unless you have exhausted all other routes.
|
||||
When you use this option, you will not be able to tell if you are
|
||||
using a faulty JWKS, except for when there are JSON syntax errors.
|
||||
- ident: FS
|
||||
interface: ReadFileOption
|
||||
argument_type: fs.FS
|
||||
comment: |
|
||||
WithFS specifies the source `fs.FS` object to read the file from.
|
||||
- ident: WaitReady
|
||||
interface: RegisterOption
|
||||
argument_type: bool
|
||||
comment: |
|
||||
WithWaitReady specifies that the `jwk.Cache` should wait until the
|
||||
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.
|
||||
|
||||
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`
|
||||
+297
@@ -0,0 +1,297 @@
|
||||
// Code generated by tools/cmd/genoptions/main.go. DO NOT EDIT.
|
||||
|
||||
package jwk
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"io/fs"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/json"
|
||||
"github.com/lestrrat-go/option/v2"
|
||||
)
|
||||
|
||||
type Option = option.Interface
|
||||
|
||||
type AssignKeyIDOption interface {
|
||||
Option
|
||||
assignKeyIDOption()
|
||||
}
|
||||
|
||||
type assignKeyIDOption struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (*assignKeyIDOption) assignKeyIDOption() {}
|
||||
|
||||
// CacheOption is a type of Option that can be passed to the
|
||||
// the `jwk.NewCache()` function.
|
||||
type CacheOption interface {
|
||||
Option
|
||||
cacheOption()
|
||||
}
|
||||
|
||||
type cacheOption struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (*cacheOption) cacheOption() {}
|
||||
|
||||
// FetchOption is a type of Option that can be passed to `jwk.Fetch()`
|
||||
// FetchOption also implements the `RegisterOption`, and thus can
|
||||
// safely be passed to `(*jwk.Cache).Register()`
|
||||
type FetchOption interface {
|
||||
Option
|
||||
fetchOption()
|
||||
parseOption()
|
||||
registerOption()
|
||||
}
|
||||
|
||||
type fetchOption struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (*fetchOption) fetchOption() {}
|
||||
|
||||
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()`
|
||||
type ParseOption interface {
|
||||
Option
|
||||
fetchOption()
|
||||
registerOption()
|
||||
readFileOption()
|
||||
}
|
||||
|
||||
type parseOption struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (*parseOption) fetchOption() {}
|
||||
|
||||
func (*parseOption) registerOption() {}
|
||||
|
||||
func (*parseOption) readFileOption() {}
|
||||
|
||||
// ReadFileOption is a type of `Option` that can be passed to `jwk.ReadFile`
|
||||
type ReadFileOption interface {
|
||||
Option
|
||||
readFileOption()
|
||||
}
|
||||
|
||||
type readFileOption struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (*readFileOption) readFileOption() {}
|
||||
|
||||
// RegisterFetchOption describes options that can be passed to `(jwk.Cache).Register()` and `jwk.Fetch()`
|
||||
type RegisterFetchOption interface {
|
||||
Option
|
||||
fetchOption()
|
||||
registerOption()
|
||||
parseOption()
|
||||
}
|
||||
|
||||
type registerFetchOption struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (*registerFetchOption) fetchOption() {}
|
||||
|
||||
func (*registerFetchOption) registerOption() {}
|
||||
|
||||
func (*registerFetchOption) parseOption() {}
|
||||
|
||||
// RegisterOption describes options that can be passed to `(jwk.Cache).Register()`
|
||||
type RegisterOption interface {
|
||||
Option
|
||||
registerOption()
|
||||
}
|
||||
|
||||
type registerOption struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (*registerOption) registerOption() {}
|
||||
|
||||
// ResourceOption is a type of Option that can be passed to the `httprc.NewResource` function
|
||||
// by way of RegisterOption.
|
||||
type ResourceOption interface {
|
||||
Option
|
||||
resourceOption()
|
||||
}
|
||||
|
||||
type resourceOption struct {
|
||||
Option
|
||||
}
|
||||
|
||||
func (*resourceOption) resourceOption() {}
|
||||
|
||||
type identFS struct{}
|
||||
type identFetchWhitelist struct{}
|
||||
type identHTTPClient struct{}
|
||||
type identIgnoreParseError struct{}
|
||||
type identLocalRegistry struct{}
|
||||
type identPEM struct{}
|
||||
type identPEMDecoder struct{}
|
||||
type identStrictKeyUsage struct{}
|
||||
type identThumbprintHash struct{}
|
||||
type identWaitReady struct{}
|
||||
type identX509 struct{}
|
||||
|
||||
func (identFS) String() string {
|
||||
return "WithFS"
|
||||
}
|
||||
|
||||
func (identFetchWhitelist) String() string {
|
||||
return "WithFetchWhitelist"
|
||||
}
|
||||
|
||||
func (identHTTPClient) String() string {
|
||||
return "WithHTTPClient"
|
||||
}
|
||||
|
||||
func (identIgnoreParseError) String() string {
|
||||
return "WithIgnoreParseError"
|
||||
}
|
||||
|
||||
func (identLocalRegistry) String() string {
|
||||
return "withLocalRegistry"
|
||||
}
|
||||
|
||||
func (identPEM) String() string {
|
||||
return "WithPEM"
|
||||
}
|
||||
|
||||
func (identPEMDecoder) String() string {
|
||||
return "WithPEMDecoder"
|
||||
}
|
||||
|
||||
func (identStrictKeyUsage) String() string {
|
||||
return "WithStrictKeyUsage"
|
||||
}
|
||||
|
||||
func (identThumbprintHash) String() string {
|
||||
return "WithThumbprintHash"
|
||||
}
|
||||
|
||||
func (identWaitReady) String() string {
|
||||
return "WithWaitReady"
|
||||
}
|
||||
|
||||
func (identX509) String() string {
|
||||
return "WithX509"
|
||||
}
|
||||
|
||||
// WithFS specifies the source `fs.FS` object to read the file from.
|
||||
func WithFS(v fs.FS) ReadFileOption {
|
||||
return &readFileOption{option.New(identFS{}, v)}
|
||||
}
|
||||
|
||||
// WithFetchWhitelist specifies the Whitelist object to use when
|
||||
// fetching JWKs from a remote source. This option can be passed
|
||||
// to both `jwk.Fetch()`
|
||||
func WithFetchWhitelist(v Whitelist) FetchOption {
|
||||
return &fetchOption{option.New(identFetchWhitelist{}, v)}
|
||||
}
|
||||
|
||||
// WithHTTPClient allows users to specify the "net/http".Client object that
|
||||
// is used when fetching jwk.Set objects.
|
||||
func WithHTTPClient(v HTTPClient) RegisterFetchOption {
|
||||
return ®isterFetchOption{option.New(identHTTPClient{}, v)}
|
||||
}
|
||||
|
||||
// WithIgnoreParseError is only applicable when used with `jwk.Parse()`
|
||||
// (i.e. to parse JWK sets). If passed to `jwk.ParseKey()`, the function
|
||||
// will return an error no matter what the input is.
|
||||
//
|
||||
// DO NOT USE WITHOUT EXHAUSTING ALL OTHER ROUTES FIRST.
|
||||
//
|
||||
// The option specifies that errors found during parsing of individual
|
||||
// keys are ignored. For example, if you had keys A, B, C where B is
|
||||
// invalid (e.g. it does not contain the required fields), then the
|
||||
// resulting JWKS will contain keys A and C only.
|
||||
//
|
||||
// This options exists as an escape hatch for those times when a
|
||||
// key in a JWKS that is irrelevant for your use case is causing
|
||||
// your JWKS parsing to fail, and you want to get to the rest of the
|
||||
// keys in the JWKS.
|
||||
//
|
||||
// Again, DO NOT USE unless you have exhausted all other routes.
|
||||
// When you use this option, you will not be able to tell if you are
|
||||
// using a faulty JWKS, except for when there are JSON syntax errors.
|
||||
func WithIgnoreParseError(v bool) ParseOption {
|
||||
return &parseOption{option.New(identIgnoreParseError{}, v)}
|
||||
}
|
||||
|
||||
// This option is only available for internal code. Users don't get to play with it
|
||||
func withLocalRegistry(v *json.Registry) ParseOption {
|
||||
return &parseOption{option.New(identLocalRegistry{}, v)}
|
||||
}
|
||||
|
||||
// WithPEM specifies that the input to `Parse()` is a PEM encoded key.
|
||||
//
|
||||
// This option is planned to be deprecated in the future. The plan is to
|
||||
// replace it with `jwk.WithX509(true)`
|
||||
func WithPEM(v bool) ParseOption {
|
||||
return &parseOption{option.New(identPEM{}, v)}
|
||||
}
|
||||
|
||||
// WithPEMDecoder specifies the PEMDecoder object to use when decoding
|
||||
// PEM encoded keys. This option can be passed to `jwk.Parse()`
|
||||
//
|
||||
// This option is planned to be deprecated in the future. The plan is to
|
||||
// use `jwk.RegisterX509Decoder()` to register a custom X.509 decoder globally.
|
||||
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)}
|
||||
}
|
||||
|
||||
// WithWaitReady specifies that the `jwk.Cache` should wait until the
|
||||
// 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.
|
||||
//
|
||||
// This options is exactly the same as `httprc.WithWaitReady()`
|
||||
func WithWaitReady(v bool) RegisterOption {
|
||||
return ®isterOption{option.New(identWaitReady{}, v)}
|
||||
}
|
||||
|
||||
// WithX509 specifies that the input to `Parse()` is an X.509 encoded key
|
||||
func WithX509(v bool) ParseOption {
|
||||
return &parseOption{option.New(identX509{}, v)}
|
||||
}
|
||||
+244
@@ -0,0 +1,244 @@
|
||||
package jwk
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
|
||||
"github.com/lestrrat-go/blackmagic"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/json"
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
)
|
||||
|
||||
// KeyParser represents a type that can parse a JSON representation of a JWK into
|
||||
// a jwk.Key.
|
||||
// See KeyConvertor for a type that can convert a raw key into a jwk.Key
|
||||
type KeyParser interface {
|
||||
// ParseKey parses a JSON payload to a `jwk.Key` object. The first
|
||||
// argument is an object that contains some hints as to what kind of
|
||||
// key the JSON payload contains.
|
||||
//
|
||||
// If your KeyParser decides that the payload is not something
|
||||
// you can parse, and you would like to continue parsing with
|
||||
// the remaining KeyParser instances that are registered,
|
||||
// return a `jwk.ContinueParseError`. Any other errors will immediately
|
||||
// halt the parsing process.
|
||||
//
|
||||
// When unmarshaling JSON, use the unmarshaler object supplied as
|
||||
// the second argument. This will ensure that the JSON is unmarshaled
|
||||
// in a way that is compatible with the rest of the library.
|
||||
ParseKey(probe *KeyProbe, unmarshaler KeyUnmarshaler, payload []byte) (Key, error)
|
||||
}
|
||||
|
||||
// KeyParseFunc is a type of KeyParser that is based on a function/closure
|
||||
type KeyParseFunc func(probe *KeyProbe, unmarshaler KeyUnmarshaler, payload []byte) (Key, error)
|
||||
|
||||
func (f KeyParseFunc) ParseKey(probe *KeyProbe, unmarshaler KeyUnmarshaler, payload []byte) (Key, error) {
|
||||
return f(probe, unmarshaler, payload)
|
||||
}
|
||||
|
||||
// protects keyParsers
|
||||
var muKeyParser sync.RWMutex
|
||||
|
||||
// list of parsers
|
||||
var keyParsers = []KeyParser{KeyParseFunc(defaultParseKey)}
|
||||
|
||||
// RegisterKeyParser adds a new KeyParser. Parsers are called in FILO order.
|
||||
// That is, the last parser to be registered is called first. There is no
|
||||
// check for duplicate entries.
|
||||
func RegisterKeyParser(kp KeyParser) {
|
||||
muKeyParser.Lock()
|
||||
defer muKeyParser.Unlock()
|
||||
keyParsers = append(keyParsers, kp)
|
||||
}
|
||||
|
||||
func defaultParseKey(probe *KeyProbe, unmarshaler KeyUnmarshaler, data []byte) (Key, error) {
|
||||
var key Key
|
||||
var kty string
|
||||
var d json.RawMessage
|
||||
if err := probe.Get("Kty", &kty); err != nil {
|
||||
return nil, fmt.Errorf(`jwk.Parse: failed to get "kty" hint: %w`, err)
|
||||
}
|
||||
// We ignore errors from this field, as it's optional
|
||||
_ = probe.Get("D", &d)
|
||||
switch v, _ := jwa.LookupKeyType(kty); v {
|
||||
case jwa.RSA():
|
||||
if d != nil {
|
||||
key = newRSAPrivateKey()
|
||||
} else {
|
||||
key = newRSAPublicKey()
|
||||
}
|
||||
case jwa.EC():
|
||||
if d != nil {
|
||||
key = newECDSAPrivateKey()
|
||||
} else {
|
||||
key = newECDSAPublicKey()
|
||||
}
|
||||
case jwa.OctetSeq():
|
||||
key = newSymmetricKey()
|
||||
case jwa.OKP():
|
||||
if d != nil {
|
||||
key = newOKPPrivateKey()
|
||||
} else {
|
||||
key = newOKPPublicKey()
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf(`invalid key type from JSON (%s)`, kty)
|
||||
}
|
||||
|
||||
if err := unmarshaler.UnmarshalKey(data, key); err != nil {
|
||||
return nil, fmt.Errorf(`failed to unmarshal JSON into key (%T): %w`, key, err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
type keyUnmarshaler struct {
|
||||
localReg *json.Registry
|
||||
}
|
||||
|
||||
func (ku *keyUnmarshaler) UnmarshalKey(data []byte, key any) error {
|
||||
if ku.localReg != nil {
|
||||
dcKey, ok := key.(json.DecodeCtxContainer)
|
||||
if !ok {
|
||||
return fmt.Errorf(`typed field was requested, but the key (%T) does not support DecodeCtx`, key)
|
||||
}
|
||||
dc := json.NewDecodeCtx(ku.localReg)
|
||||
dcKey.SetDecodeCtx(dc)
|
||||
defer func() { dcKey.SetDecodeCtx(nil) }()
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, key); err != nil {
|
||||
return fmt.Errorf(`failed to unmarshal JSON into key (%T): %w`, key, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// keyProber is the object that starts the probing. When Probe() is called,
|
||||
// it creates (possibly from a cached value) an object that is used to
|
||||
// hold hint values.
|
||||
type keyProber struct {
|
||||
mu sync.RWMutex
|
||||
pool *sync.Pool
|
||||
fields map[string]reflect.StructField
|
||||
typ reflect.Type
|
||||
}
|
||||
|
||||
func (kp *keyProber) AddField(field reflect.StructField) error {
|
||||
kp.mu.Lock()
|
||||
defer kp.mu.Unlock()
|
||||
|
||||
if _, ok := kp.fields[field.Name]; ok {
|
||||
return fmt.Errorf(`field name %s is already registered`, field.Name)
|
||||
}
|
||||
kp.fields[field.Name] = field
|
||||
kp.makeStructType()
|
||||
|
||||
// Update pool (note: the logic is the same, but we need to recreate it
|
||||
// so that we don't accidentally use old stored values)
|
||||
kp.pool = &sync.Pool{
|
||||
New: kp.makeStruct,
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (kp *keyProber) makeStructType() {
|
||||
// DOES NOT LOCK
|
||||
fields := make([]reflect.StructField, 0, len(kp.fields))
|
||||
for _, f := range kp.fields {
|
||||
fields = append(fields, f)
|
||||
}
|
||||
kp.typ = reflect.StructOf(fields)
|
||||
}
|
||||
|
||||
func (kp *keyProber) makeStruct() any {
|
||||
return reflect.New(kp.typ)
|
||||
}
|
||||
|
||||
func (kp *keyProber) Probe(data []byte) (*KeyProbe, error) {
|
||||
kp.mu.RLock()
|
||||
defer kp.mu.RUnlock()
|
||||
|
||||
// if the field list unchanged, so is the pool object, so effectively
|
||||
// we should be using the cached version
|
||||
v := kp.pool.Get()
|
||||
if v == nil {
|
||||
return nil, fmt.Errorf(`probe: failed to get object from pool`)
|
||||
}
|
||||
rv, ok := v.(reflect.Value)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`probe: value returned from pool as of type %T, expected reflect.Value`, v)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(data, rv.Interface()); err != nil {
|
||||
return nil, fmt.Errorf(`probe: failed to unmarshal data: %w`, err)
|
||||
}
|
||||
|
||||
return &KeyProbe{data: rv}, nil
|
||||
}
|
||||
|
||||
// KeyProbe is the object that carries the hints when parsing a key.
|
||||
// The exact list of fields can vary depending on the types of key
|
||||
// that are registered.
|
||||
//
|
||||
// Use `Get()` to access the value of a field.
|
||||
//
|
||||
// The underlying data stored in a KeyProbe is recycled each
|
||||
// time a value is parsed, therefore you are not allowed to hold
|
||||
// onto this object after ParseKey() is done.
|
||||
type KeyProbe struct {
|
||||
data reflect.Value
|
||||
}
|
||||
|
||||
// Get returns the value of the field with the given `name“.
|
||||
// `dst` must be a pointer to a value that can hold the type of
|
||||
// the value of the field, which is determined by the
|
||||
// field type registered through `jwk.RegisterProbeField()`
|
||||
func (kp *KeyProbe) Get(name string, dst any) error {
|
||||
f := kp.data.Elem().FieldByName(name)
|
||||
if !f.IsValid() {
|
||||
return fmt.Errorf(`field %s not found`, name)
|
||||
}
|
||||
|
||||
if err := blackmagic.AssignIfCompatible(dst, f.Addr().Interface()); err != nil {
|
||||
return fmt.Errorf(`failed to assign value of field %q to %T: %w`, name, dst, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// We don't really need the object, we need to know its type
|
||||
var keyProbe = &keyProber{
|
||||
fields: make(map[string]reflect.StructField),
|
||||
}
|
||||
|
||||
// RegisterProbeField adds a new field to be probed during the initial
|
||||
// phase of parsing. This is done by partially parsing the JSON payload,
|
||||
// and we do this by calling `json.Unmarshal` using a dynamic type that
|
||||
// can possibly be modified during runtime. This function is used to
|
||||
// add a new field to this dynamic type.
|
||||
//
|
||||
// Note that the `Name` field for the given `reflect.StructField` must start
|
||||
// with an upper case alphabet, such that it is treated as an exported field.
|
||||
// So for example, if you want to probe the "my_hint" field, you should specify
|
||||
// the field name as "MyHint" or similar.
|
||||
//
|
||||
// Also the field name must be unique. If you believe that your field name may
|
||||
// collide with other packages that may want to add their own probes,
|
||||
// it is the responsibility of the caller
|
||||
// to ensure that the field name is unique (possibly by prefixing the field
|
||||
// name with a unique string). It is important to note that the field name
|
||||
// need not be the same as the JSON field name. For example, your field name
|
||||
// could be "MyPkg_MyHint", while the actual JSON field name could be "my_hint".
|
||||
//
|
||||
// If the field name is not unique, an error is returned.
|
||||
func RegisterProbeField(p reflect.StructField) error {
|
||||
// locking is done inside keyProbe
|
||||
return keyProbe.AddField(p)
|
||||
}
|
||||
|
||||
// KeyUnmarshaler is a thin wrapper around json.Unmarshal. It behaves almost
|
||||
// exactly like json.Unmarshal, but it allows us to add extra magic that
|
||||
// is specific to this library before calling the actual json.Unmarshal.
|
||||
type KeyUnmarshaler interface {
|
||||
UnmarshalKey(data []byte, key any) error
|
||||
}
|
||||
+360
@@ -0,0 +1,360 @@
|
||||
package jwk
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rsa"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"reflect"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/base64"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/pool"
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterKeyExporter(jwa.RSA(), KeyExportFunc(rsaJWKToRaw))
|
||||
}
|
||||
|
||||
func (k *rsaPrivateKey) Import(rawKey *rsa.PrivateKey) error {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
|
||||
d, err := bigIntToBytes(rawKey.D)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`invalid rsa.PrivateKey: %w`, err)
|
||||
}
|
||||
k.d = d
|
||||
|
||||
l := len(rawKey.Primes)
|
||||
|
||||
if l < 0 /* I know, I'm being paranoid */ || l > 2 {
|
||||
return fmt.Errorf(`invalid number of primes in rsa.PrivateKey: need 0 to 2, but got %d`, len(rawKey.Primes))
|
||||
}
|
||||
|
||||
if l > 0 {
|
||||
p, err := bigIntToBytes(rawKey.Primes[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf(`invalid rsa.PrivateKey: %w`, err)
|
||||
}
|
||||
k.p = p
|
||||
}
|
||||
|
||||
if l > 1 {
|
||||
q, err := bigIntToBytes(rawKey.Primes[1])
|
||||
if err != nil {
|
||||
return fmt.Errorf(`invalid rsa.PrivateKey: %w`, err)
|
||||
}
|
||||
k.q = q
|
||||
}
|
||||
|
||||
// dp, dq, qi are optional values
|
||||
if v, err := bigIntToBytes(rawKey.Precomputed.Dp); err == nil {
|
||||
k.dp = v
|
||||
}
|
||||
if v, err := bigIntToBytes(rawKey.Precomputed.Dq); err == nil {
|
||||
k.dq = v
|
||||
}
|
||||
if v, err := bigIntToBytes(rawKey.Precomputed.Qinv); err == nil {
|
||||
k.qi = v
|
||||
}
|
||||
|
||||
// public key part
|
||||
n, e, err := importRsaPublicKeyByteValues(&rawKey.PublicKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`invalid rsa.PrivateKey: %w`, err)
|
||||
}
|
||||
k.n = n
|
||||
k.e = e
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func importRsaPublicKeyByteValues(rawKey *rsa.PublicKey) ([]byte, []byte, error) {
|
||||
n, err := bigIntToBytes(rawKey.N)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf(`invalid rsa.PublicKey: %w`, err)
|
||||
}
|
||||
|
||||
data := make([]byte, 8)
|
||||
binary.BigEndian.PutUint64(data, uint64(rawKey.E))
|
||||
i := 0
|
||||
for ; i < len(data); i++ {
|
||||
if data[i] != 0x0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
return n, data[i:], nil
|
||||
}
|
||||
|
||||
func (k *rsaPublicKey) Import(rawKey *rsa.PublicKey) error {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
|
||||
n, e, err := importRsaPublicKeyByteValues(rawKey)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`invalid rsa.PrivateKey: %w`, err)
|
||||
}
|
||||
k.n = n
|
||||
k.e = e
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildRSAPublicKey(key *rsa.PublicKey, n, e []byte) {
|
||||
bin := pool.BigInt().Get()
|
||||
bie := pool.BigInt().Get()
|
||||
defer pool.BigInt().Put(bie)
|
||||
|
||||
bin.SetBytes(n)
|
||||
bie.SetBytes(e)
|
||||
|
||||
key.N = bin
|
||||
key.E = int(bie.Int64())
|
||||
}
|
||||
|
||||
var rsaConvertibleKeys = []reflect.Type{
|
||||
reflect.TypeFor[RSAPrivateKey](),
|
||||
reflect.TypeFor[RSAPublicKey](),
|
||||
}
|
||||
|
||||
func rsaJWKToRaw(key Key, hint any) (any, error) {
|
||||
extracted, err := extractEmbeddedKey(key, rsaConvertibleKeys)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to extract embedded key: %w`, err)
|
||||
}
|
||||
switch key := extracted.(type) {
|
||||
case RSAPrivateKey:
|
||||
switch hint.(type) {
|
||||
case *rsa.PrivateKey, *any:
|
||||
default:
|
||||
return nil, fmt.Errorf(`invalid destination object type %T for private RSA JWK: %w`, hint, ContinueError())
|
||||
}
|
||||
|
||||
locker, ok := key.(rlocker)
|
||||
if !ok {
|
||||
locker.rlock()
|
||||
defer locker.runlock()
|
||||
}
|
||||
|
||||
od, ok := key.D()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "d" value`)
|
||||
}
|
||||
|
||||
oq, ok := key.Q()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "q" value`)
|
||||
}
|
||||
|
||||
op, ok := key.P()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "p" value`)
|
||||
}
|
||||
|
||||
var d, q, p big.Int // note: do not use from sync.Pool
|
||||
|
||||
d.SetBytes(od)
|
||||
q.SetBytes(oq)
|
||||
p.SetBytes(op)
|
||||
|
||||
// optional fields
|
||||
var dp, dq, qi *big.Int
|
||||
|
||||
if odp, ok := key.DP(); ok {
|
||||
dp = &big.Int{} // note: do not use from sync.Pool
|
||||
dp.SetBytes(odp)
|
||||
}
|
||||
|
||||
if odq, ok := key.DQ(); ok {
|
||||
dq = &big.Int{} // note: do not use from sync.Pool
|
||||
dq.SetBytes(odq)
|
||||
}
|
||||
|
||||
if oqi, ok := key.QI(); ok {
|
||||
qi = &big.Int{} // note: do not use from sync.Pool
|
||||
qi.SetBytes(oqi)
|
||||
}
|
||||
|
||||
n, ok := key.N()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "n" value`)
|
||||
}
|
||||
|
||||
e, ok := key.E()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "e" value`)
|
||||
}
|
||||
|
||||
var privkey rsa.PrivateKey
|
||||
buildRSAPublicKey(&privkey.PublicKey, n, e)
|
||||
privkey.D = &d
|
||||
privkey.Primes = []*big.Int{&p, &q}
|
||||
|
||||
if dp != nil {
|
||||
privkey.Precomputed.Dp = dp
|
||||
}
|
||||
if dq != nil {
|
||||
privkey.Precomputed.Dq = dq
|
||||
}
|
||||
if qi != nil {
|
||||
privkey.Precomputed.Qinv = qi
|
||||
}
|
||||
// This may look like a no-op, but it's required if we want to
|
||||
// compare it against a key generated by rsa.GenerateKey
|
||||
privkey.Precomputed.CRTValues = []rsa.CRTValue{}
|
||||
return &privkey, nil
|
||||
case RSAPublicKey:
|
||||
switch hint.(type) {
|
||||
case *rsa.PublicKey, *any:
|
||||
default:
|
||||
return nil, fmt.Errorf(`invalid destination object type %T for public RSA JWK: %w`, hint, ContinueError())
|
||||
}
|
||||
|
||||
locker, ok := key.(rlocker)
|
||||
if !ok {
|
||||
locker.rlock()
|
||||
defer locker.runlock()
|
||||
}
|
||||
|
||||
n, ok := key.N()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "n" value`)
|
||||
}
|
||||
|
||||
e, ok := key.E()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`missing "e" value`)
|
||||
}
|
||||
|
||||
var pubkey rsa.PublicKey
|
||||
buildRSAPublicKey(&pubkey, n, e)
|
||||
|
||||
return &pubkey, nil
|
||||
|
||||
default:
|
||||
return nil, ContinueError()
|
||||
}
|
||||
}
|
||||
|
||||
func makeRSAPublicKey(src Key) (Key, error) {
|
||||
newKey := newRSAPublicKey()
|
||||
|
||||
// Iterate and copy everything except for the bits that should not be in the public key
|
||||
for _, k := range src.Keys() {
|
||||
switch k {
|
||||
case RSADKey, RSADPKey, RSADQKey, RSAPKey, RSAQKey, RSAQIKey:
|
||||
continue
|
||||
default:
|
||||
var v any
|
||||
if err := src.Get(k, &v); err != nil {
|
||||
return nil, fmt.Errorf(`rsa: makeRSAPublicKey: failed to get field %q: %w`, k, err)
|
||||
}
|
||||
if err := newKey.Set(k, v); err != nil {
|
||||
return nil, fmt.Errorf(`rsa: makeRSAPublicKey: failed to set field %q: %w`, k, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return newKey, nil
|
||||
}
|
||||
|
||||
func (k *rsaPrivateKey) PublicKey() (Key, error) {
|
||||
return makeRSAPublicKey(k)
|
||||
}
|
||||
|
||||
func (k *rsaPublicKey) PublicKey() (Key, error) {
|
||||
return makeRSAPublicKey(k)
|
||||
}
|
||||
|
||||
// Thumbprint returns the JWK thumbprint using the indicated
|
||||
// hashing algorithm, according to RFC 7638
|
||||
func (k rsaPrivateKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
|
||||
var key rsa.PrivateKey
|
||||
if err := Export(&k, &key); err != nil {
|
||||
return nil, fmt.Errorf(`failed to export RSA private key: %w`, err)
|
||||
}
|
||||
return rsaThumbprint(hash, &key.PublicKey)
|
||||
}
|
||||
|
||||
func (k rsaPublicKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
|
||||
var key rsa.PublicKey
|
||||
if err := Export(&k, &key); err != nil {
|
||||
return nil, fmt.Errorf(`failed to export RSA public key: %w`, err)
|
||||
}
|
||||
return rsaThumbprint(hash, &key)
|
||||
}
|
||||
|
||||
func rsaThumbprint(hash crypto.Hash, key *rsa.PublicKey) ([]byte, error) {
|
||||
buf := pool.BytesBuffer().Get()
|
||||
defer pool.BytesBuffer().Put(buf)
|
||||
|
||||
buf.WriteString(`{"e":"`)
|
||||
buf.WriteString(base64.EncodeUint64ToString(uint64(key.E)))
|
||||
buf.WriteString(`","kty":"RSA","n":"`)
|
||||
buf.WriteString(base64.EncodeToString(key.N.Bytes()))
|
||||
buf.WriteString(`"}`)
|
||||
|
||||
h := hash.New()
|
||||
if _, err := buf.WriteTo(h); err != nil {
|
||||
return nil, fmt.Errorf(`failed to write rsaThumbprint: %w`, err)
|
||||
}
|
||||
return h.Sum(nil), nil
|
||||
}
|
||||
|
||||
func validateRSAKey(key interface {
|
||||
N() ([]byte, bool)
|
||||
E() ([]byte, bool)
|
||||
}, checkPrivate bool) error {
|
||||
n, ok := key.N()
|
||||
if !ok {
|
||||
return fmt.Errorf(`missing "n" value`)
|
||||
}
|
||||
|
||||
e, ok := key.E()
|
||||
if !ok {
|
||||
return fmt.Errorf(`missing "e" value`)
|
||||
}
|
||||
|
||||
if len(n) == 0 {
|
||||
// Ideally we would like to check for the actual length, but unlike
|
||||
// EC keys, we have nothing in the key itself that will tell us
|
||||
// how many bits this key should have.
|
||||
return fmt.Errorf(`missing "n" value`)
|
||||
}
|
||||
if len(e) == 0 {
|
||||
return fmt.Errorf(`missing "e" value`)
|
||||
}
|
||||
if checkPrivate {
|
||||
if priv, ok := key.(keyWithD); ok {
|
||||
if d, ok := priv.D(); !ok || len(d) == 0 {
|
||||
return fmt.Errorf(`missing "d" value`)
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf(`missing "d" value`)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *rsaPrivateKey) Validate() error {
|
||||
if err := validateRSAKey(k, true); err != nil {
|
||||
return NewKeyValidationError(fmt.Errorf(`jwk.RSAPrivateKey: %w`, err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *rsaPublicKey) Validate() error {
|
||||
if err := validateRSAKey(k, false); err != nil {
|
||||
return NewKeyValidationError(fmt.Errorf(`jwk.RSAPublicKey: %w`, err))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+1543
File diff suppressed because it is too large
Load Diff
+319
@@ -0,0 +1,319 @@
|
||||
package jwk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"maps"
|
||||
"reflect"
|
||||
"sort"
|
||||
|
||||
"github.com/lestrrat-go/blackmagic"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/json"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/pool"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/tokens"
|
||||
)
|
||||
|
||||
const keysKey = `keys` // appease linter
|
||||
|
||||
func newSet() *set {
|
||||
return &set{
|
||||
privateParams: make(map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
// NewSet creates and empty `jwk.Set` object
|
||||
func NewSet() Set {
|
||||
return newSet()
|
||||
}
|
||||
|
||||
func (s *set) Set(n string, v any) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if n == keysKey {
|
||||
vl, ok := v.([]Key)
|
||||
if !ok {
|
||||
return fmt.Errorf(`value for field "keys" must be []jwk.Key`)
|
||||
}
|
||||
s.keys = vl
|
||||
return nil
|
||||
}
|
||||
|
||||
s.privateParams[n] = v
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *set) Get(name string, dst any) error {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
v, ok := s.privateParams[name]
|
||||
if !ok {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, v); err != nil {
|
||||
return fmt.Errorf(`failed to assign value to dst: %w`, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *set) Key(idx int) (Key, bool) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
if idx >= 0 && idx < len(s.keys) {
|
||||
return s.keys[idx], true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (s *set) Len() int {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
return len(s.keys)
|
||||
}
|
||||
|
||||
// indexNL is Index(), but without the locking
|
||||
func (s *set) indexNL(key Key) int {
|
||||
for i, k := range s.keys {
|
||||
if k == key {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
func (s *set) Index(key Key) int {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
return s.indexNL(key)
|
||||
}
|
||||
|
||||
func (s *set) AddKey(key Key) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if reflect.ValueOf(key).IsNil() {
|
||||
panic("nil key")
|
||||
}
|
||||
|
||||
if i := s.indexNL(key); i > -1 {
|
||||
return fmt.Errorf(`(jwk.Set).AddKey: key already exists`)
|
||||
}
|
||||
s.keys = append(s.keys, key)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *set) Remove(name string) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
delete(s.privateParams, name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *set) RemoveKey(key Key) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
for i, k := range s.keys {
|
||||
if k == key {
|
||||
switch i {
|
||||
case 0:
|
||||
s.keys = s.keys[1:]
|
||||
case len(s.keys) - 1:
|
||||
s.keys = s.keys[:i]
|
||||
default:
|
||||
s.keys = append(s.keys[:i], s.keys[i+1:]...)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return fmt.Errorf(`(jwk.Set).RemoveKey: specified key does not exist in set`)
|
||||
}
|
||||
|
||||
func (s *set) Clear() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.keys = nil
|
||||
s.privateParams = make(map[string]any)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *set) Keys() []string {
|
||||
ret := make([]string, len(s.privateParams))
|
||||
var i int
|
||||
for k := range s.privateParams {
|
||||
ret[i] = k
|
||||
i++
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func (s *set) MarshalJSON() ([]byte, error) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
buf := pool.BytesBuffer().Get()
|
||||
defer pool.BytesBuffer().Put(buf)
|
||||
enc := json.NewEncoder(buf)
|
||||
|
||||
fields := []string{keysKey}
|
||||
for k := range s.privateParams {
|
||||
fields = append(fields, k)
|
||||
}
|
||||
sort.Strings(fields)
|
||||
|
||||
buf.WriteByte(tokens.OpenCurlyBracket)
|
||||
for i, field := range fields {
|
||||
if i > 0 {
|
||||
buf.WriteByte(tokens.Comma)
|
||||
}
|
||||
fmt.Fprintf(buf, `%q:`, field)
|
||||
if field != keysKey {
|
||||
if err := enc.Encode(s.privateParams[field]); err != nil {
|
||||
return nil, fmt.Errorf(`failed to marshal field %q: %w`, field, err)
|
||||
}
|
||||
} else {
|
||||
buf.WriteByte(tokens.OpenSquareBracket)
|
||||
for j, k := range s.keys {
|
||||
if j > 0 {
|
||||
buf.WriteByte(tokens.Comma)
|
||||
}
|
||||
if err := enc.Encode(k); err != nil {
|
||||
return nil, fmt.Errorf(`failed to marshal key #%d: %w`, i, err)
|
||||
}
|
||||
}
|
||||
buf.WriteByte(tokens.CloseSquareBracket)
|
||||
}
|
||||
}
|
||||
buf.WriteByte(tokens.CloseCurlyBracket)
|
||||
|
||||
ret := make([]byte, buf.Len())
|
||||
copy(ret, buf.Bytes())
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (s *set) UnmarshalJSON(data []byte) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
s.privateParams = make(map[string]any)
|
||||
s.keys = nil
|
||||
|
||||
var options []ParseOption
|
||||
var ignoreParseError bool
|
||||
if dc := s.dc; dc != nil {
|
||||
if localReg := dc.Registry(); localReg != nil {
|
||||
options = append(options, withLocalRegistry(localReg))
|
||||
}
|
||||
ignoreParseError = dc.IgnoreParseError()
|
||||
}
|
||||
|
||||
var sawKeysField bool
|
||||
dec := json.NewDecoder(bytes.NewReader(data))
|
||||
LOOP:
|
||||
for {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return fmt.Errorf(`error reading token: %w`, err)
|
||||
}
|
||||
|
||||
switch tok := tok.(type) {
|
||||
case json.Delim:
|
||||
// Assuming we're doing everything correctly, we should ONLY
|
||||
// get either tokens.OpenCurlyBracket or tokens.CloseCurlyBracket here.
|
||||
if tok == tokens.CloseCurlyBracket { // End of object
|
||||
break LOOP
|
||||
} else if tok != tokens.OpenCurlyBracket {
|
||||
return fmt.Errorf(`expected '%c' but got '%c'`, tokens.OpenCurlyBracket, tok)
|
||||
}
|
||||
case string:
|
||||
switch tok {
|
||||
case "keys":
|
||||
sawKeysField = true
|
||||
var list []json.RawMessage
|
||||
if err := dec.Decode(&list); err != nil {
|
||||
return fmt.Errorf(`failed to decode "keys": %w`, err)
|
||||
}
|
||||
|
||||
for i, keysrc := range list {
|
||||
key, err := ParseKey(keysrc, options...)
|
||||
if err != nil {
|
||||
if !ignoreParseError {
|
||||
return fmt.Errorf(`failed to decode key #%d in "keys": %w`, i, err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
s.keys = append(s.keys, key)
|
||||
}
|
||||
default:
|
||||
var v any
|
||||
if err := dec.Decode(&v); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %q: %w`, tok, err)
|
||||
}
|
||||
s.privateParams[tok] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This is really silly, but we can only detect the
|
||||
// lack of the "keys" field after going through the
|
||||
// entire object once
|
||||
// Not checking for len(s.keys) == 0, because it could be
|
||||
// an empty key set
|
||||
if !sawKeysField {
|
||||
key, err := ParseKey(data, options...)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to parse sole key in key set`)
|
||||
}
|
||||
s.keys = append(s.keys, key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *set) LookupKeyID(kid string) (Key, bool) {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
for i := range s.Len() {
|
||||
key, ok := s.Key(i)
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
gotkid, ok := key.KeyID()
|
||||
if ok && gotkid == kid {
|
||||
return key, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (s *set) DecodeCtx() DecodeCtx {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
return s.dc
|
||||
}
|
||||
|
||||
func (s *set) SetDecodeCtx(dc DecodeCtx) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.dc = dc
|
||||
}
|
||||
|
||||
func (s *set) Clone() (Set, error) {
|
||||
s2 := newSet()
|
||||
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
s2.keys = make([]Key, len(s.keys))
|
||||
copy(s2.keys, s.keys)
|
||||
|
||||
maps.Copy(s2.privateParams, s.privateParams)
|
||||
|
||||
return s2, nil
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
package jwk
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v3/internal/base64"
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
)
|
||||
|
||||
func init() {
|
||||
RegisterKeyExporter(jwa.OctetSeq(), KeyExportFunc(octetSeqToRaw))
|
||||
}
|
||||
|
||||
func (k *symmetricKey) Import(rawKey []byte) error {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
|
||||
if len(rawKey) == 0 {
|
||||
return fmt.Errorf(`non-empty []byte key required`)
|
||||
}
|
||||
|
||||
k.octets = rawKey
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var symmetricConvertibleKeys = []reflect.Type{
|
||||
reflect.TypeFor[SymmetricKey](),
|
||||
}
|
||||
|
||||
func octetSeqToRaw(key Key, hint any) (any, error) {
|
||||
extracted, err := extractEmbeddedKey(key, symmetricConvertibleKeys)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to extract embedded key: %w`, err)
|
||||
}
|
||||
|
||||
switch key := extracted.(type) {
|
||||
case SymmetricKey:
|
||||
switch hint.(type) {
|
||||
case *[]byte, *any:
|
||||
default:
|
||||
return nil, fmt.Errorf(`invalid destination object type %T for symmetric key: %w`, hint, ContinueError())
|
||||
}
|
||||
|
||||
locker, ok := key.(rlocker)
|
||||
if ok {
|
||||
locker.rlock()
|
||||
defer locker.runlock()
|
||||
}
|
||||
|
||||
ooctets, ok := key.Octets()
|
||||
if !ok {
|
||||
return nil, fmt.Errorf(`jwk.SymmetricKey: missing "k" field`)
|
||||
}
|
||||
|
||||
octets := make([]byte, len(ooctets))
|
||||
copy(octets, ooctets)
|
||||
return octets, nil
|
||||
default:
|
||||
return nil, ContinueError()
|
||||
}
|
||||
}
|
||||
|
||||
// Thumbprint returns the JWK thumbprint using the indicated
|
||||
// hashing algorithm, according to RFC 7638
|
||||
func (k *symmetricKey) Thumbprint(hash crypto.Hash) ([]byte, error) {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
var octets []byte
|
||||
if err := Export(k, &octets); err != nil {
|
||||
return nil, fmt.Errorf(`failed to export symmetric key: %w`, err)
|
||||
}
|
||||
|
||||
h := hash.New()
|
||||
fmt.Fprint(h, `{"k":"`)
|
||||
fmt.Fprint(h, base64.EncodeToString(octets))
|
||||
fmt.Fprint(h, `","kty":"oct"}`)
|
||||
return h.Sum(nil), nil
|
||||
}
|
||||
|
||||
func (k *symmetricKey) PublicKey() (Key, error) {
|
||||
newKey := newSymmetricKey()
|
||||
|
||||
for _, key := range k.Keys() {
|
||||
var v any
|
||||
if err := k.Get(key, &v); err != nil {
|
||||
return nil, fmt.Errorf(`failed to get field %q: %w`, key, err)
|
||||
}
|
||||
|
||||
if err := newKey.Set(key, v); err != nil {
|
||||
return nil, fmt.Errorf(`failed to set field %q: %w`, key, err)
|
||||
}
|
||||
}
|
||||
return newKey, nil
|
||||
}
|
||||
|
||||
func (k *symmetricKey) Validate() error {
|
||||
octets, ok := k.Octets()
|
||||
if !ok || len(octets) == 0 {
|
||||
return NewKeyValidationError(fmt.Errorf(`jwk.SymmetricKey: missing "k" field`))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
+620
@@ -0,0 +1,620 @@
|
||||
// Code generated by tools/cmd/genjwk/main.go. DO NOT EDIT.
|
||||
|
||||
package jwk
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
|
||||
"github.com/lestrrat-go/blackmagic"
|
||||
"github.com/lestrrat-go/jwx/v3/cert"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/base64"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/json"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/pool"
|
||||
"github.com/lestrrat-go/jwx/v3/internal/tokens"
|
||||
"github.com/lestrrat-go/jwx/v3/jwa"
|
||||
)
|
||||
|
||||
const (
|
||||
SymmetricOctetsKey = "k"
|
||||
)
|
||||
|
||||
type SymmetricKey interface {
|
||||
Key
|
||||
Octets() ([]byte, bool)
|
||||
}
|
||||
|
||||
type symmetricKey struct {
|
||||
algorithm *jwa.KeyAlgorithm // https://tools.ietf.org/html/rfc7517#section-4.4
|
||||
keyID *string // https://tools.ietf.org/html/rfc7515#section-4.1.4
|
||||
keyOps *KeyOperationList // https://tools.ietf.org/html/rfc7517#section-4.3
|
||||
keyUsage *string // https://tools.ietf.org/html/rfc7517#section-4.2
|
||||
octets []byte
|
||||
x509CertChain *cert.Chain // https://tools.ietf.org/html/rfc7515#section-4.1.6
|
||||
x509CertThumbprint *string // https://tools.ietf.org/html/rfc7515#section-4.1.7
|
||||
x509CertThumbprintS256 *string // https://tools.ietf.org/html/rfc7515#section-4.1.8
|
||||
x509URL *string // https://tools.ietf.org/html/rfc7515#section-4.1.5
|
||||
privateParams map[string]any
|
||||
mu *sync.RWMutex
|
||||
dc json.DecodeCtx
|
||||
}
|
||||
|
||||
var _ SymmetricKey = &symmetricKey{}
|
||||
var _ Key = &symmetricKey{}
|
||||
|
||||
func newSymmetricKey() *symmetricKey {
|
||||
return &symmetricKey{
|
||||
mu: &sync.RWMutex{},
|
||||
privateParams: make(map[string]any),
|
||||
}
|
||||
}
|
||||
|
||||
func (h symmetricKey) KeyType() jwa.KeyType {
|
||||
return jwa.OctetSeq()
|
||||
}
|
||||
|
||||
func (h symmetricKey) rlock() {
|
||||
h.mu.RLock()
|
||||
}
|
||||
|
||||
func (h symmetricKey) runlock() {
|
||||
h.mu.RUnlock()
|
||||
}
|
||||
|
||||
func (h *symmetricKey) Algorithm() (jwa.KeyAlgorithm, bool) {
|
||||
if h.algorithm != nil {
|
||||
return *(h.algorithm), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (h *symmetricKey) KeyID() (string, bool) {
|
||||
if h.keyID != nil {
|
||||
return *(h.keyID), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (h *symmetricKey) KeyOps() (KeyOperationList, bool) {
|
||||
if h.keyOps != nil {
|
||||
return *(h.keyOps), true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (h *symmetricKey) KeyUsage() (string, bool) {
|
||||
if h.keyUsage != nil {
|
||||
return *(h.keyUsage), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (h *symmetricKey) Octets() ([]byte, bool) {
|
||||
if h.octets != nil {
|
||||
return h.octets, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (h *symmetricKey) X509CertChain() (*cert.Chain, bool) {
|
||||
return h.x509CertChain, true
|
||||
}
|
||||
|
||||
func (h *symmetricKey) X509CertThumbprint() (string, bool) {
|
||||
if h.x509CertThumbprint != nil {
|
||||
return *(h.x509CertThumbprint), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (h *symmetricKey) X509CertThumbprintS256() (string, bool) {
|
||||
if h.x509CertThumbprintS256 != nil {
|
||||
return *(h.x509CertThumbprintS256), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (h *symmetricKey) X509URL() (string, bool) {
|
||||
if h.x509URL != nil {
|
||||
return *(h.x509URL), true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func (h *symmetricKey) Has(name string) bool {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
switch name {
|
||||
case KeyTypeKey:
|
||||
return true
|
||||
case AlgorithmKey:
|
||||
return h.algorithm != nil
|
||||
case KeyIDKey:
|
||||
return h.keyID != nil
|
||||
case KeyOpsKey:
|
||||
return h.keyOps != nil
|
||||
case KeyUsageKey:
|
||||
return h.keyUsage != nil
|
||||
case SymmetricOctetsKey:
|
||||
return h.octets != nil
|
||||
case X509CertChainKey:
|
||||
return h.x509CertChain != nil
|
||||
case X509CertThumbprintKey:
|
||||
return h.x509CertThumbprint != nil
|
||||
case X509CertThumbprintS256Key:
|
||||
return h.x509CertThumbprintS256 != nil
|
||||
case X509URLKey:
|
||||
return h.x509URL != nil
|
||||
default:
|
||||
_, ok := h.privateParams[name]
|
||||
return ok
|
||||
}
|
||||
}
|
||||
|
||||
func (h *symmetricKey) Get(name string, dst any) error {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
switch name {
|
||||
case KeyTypeKey:
|
||||
if err := blackmagic.AssignIfCompatible(dst, h.KeyType()); err != nil {
|
||||
return fmt.Errorf(`symmetricKey.Get: failed to assign value for field %q to destination object: %w`, name, err)
|
||||
}
|
||||
case AlgorithmKey:
|
||||
if h.algorithm == nil {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, *(h.algorithm)); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
return nil
|
||||
case KeyIDKey:
|
||||
if h.keyID == nil {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, *(h.keyID)); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
return nil
|
||||
case KeyOpsKey:
|
||||
if h.keyOps == nil {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, *(h.keyOps)); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
return nil
|
||||
case KeyUsageKey:
|
||||
if h.keyUsage == nil {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, *(h.keyUsage)); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
return nil
|
||||
case SymmetricOctetsKey:
|
||||
if h.octets == nil {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, h.octets); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
return nil
|
||||
case X509CertChainKey:
|
||||
if h.x509CertChain == nil {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, h.x509CertChain); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
return nil
|
||||
case X509CertThumbprintKey:
|
||||
if h.x509CertThumbprint == nil {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, *(h.x509CertThumbprint)); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
return nil
|
||||
case X509CertThumbprintS256Key:
|
||||
if h.x509CertThumbprintS256 == nil {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, *(h.x509CertThumbprintS256)); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
return nil
|
||||
case X509URLKey:
|
||||
if h.x509URL == nil {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, *(h.x509URL)); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
v, ok := h.privateParams[name]
|
||||
if !ok {
|
||||
return fmt.Errorf(`field %q not found`, name)
|
||||
}
|
||||
if err := blackmagic.AssignIfCompatible(dst, v); err != nil {
|
||||
return fmt.Errorf(`failed to assign value for field %q: %w`, name, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *symmetricKey) Set(name string, value any) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
return h.setNoLock(name, value)
|
||||
}
|
||||
|
||||
func (h *symmetricKey) setNoLock(name string, value any) error {
|
||||
switch name {
|
||||
case "kty":
|
||||
return nil
|
||||
case AlgorithmKey:
|
||||
switch v := value.(type) {
|
||||
case string, jwa.SignatureAlgorithm, jwa.KeyEncryptionAlgorithm, jwa.ContentEncryptionAlgorithm:
|
||||
tmp, err := jwa.KeyAlgorithmFrom(v)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`invalid algorithm for %q key: %w`, AlgorithmKey, err)
|
||||
}
|
||||
h.algorithm = &tmp
|
||||
default:
|
||||
return fmt.Errorf(`invalid type for %q key: %T`, AlgorithmKey, value)
|
||||
}
|
||||
return nil
|
||||
case KeyIDKey:
|
||||
if v, ok := value.(string); ok {
|
||||
h.keyID = &v
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`invalid value for %s key: %T`, KeyIDKey, value)
|
||||
case KeyOpsKey:
|
||||
var acceptor KeyOperationList
|
||||
if err := acceptor.Accept(value); err != nil {
|
||||
return fmt.Errorf(`invalid value for %s key: %w`, KeyOpsKey, err)
|
||||
}
|
||||
h.keyOps = &acceptor
|
||||
return nil
|
||||
case KeyUsageKey:
|
||||
switch v := value.(type) {
|
||||
case KeyUsageType:
|
||||
switch v {
|
||||
case ForSignature, ForEncryption:
|
||||
tmp := v.String()
|
||||
h.keyUsage = &tmp
|
||||
default:
|
||||
return fmt.Errorf(`invalid key usage type %s`, v)
|
||||
}
|
||||
case string:
|
||||
h.keyUsage = &v
|
||||
default:
|
||||
return fmt.Errorf(`invalid key usage type %s`, v)
|
||||
}
|
||||
case SymmetricOctetsKey:
|
||||
if v, ok := value.([]byte); ok {
|
||||
h.octets = v
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`invalid value for %s key: %T`, SymmetricOctetsKey, value)
|
||||
case X509CertChainKey:
|
||||
if v, ok := value.(*cert.Chain); ok {
|
||||
h.x509CertChain = v
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`invalid value for %s key: %T`, X509CertChainKey, value)
|
||||
case X509CertThumbprintKey:
|
||||
if v, ok := value.(string); ok {
|
||||
h.x509CertThumbprint = &v
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`invalid value for %s key: %T`, X509CertThumbprintKey, value)
|
||||
case X509CertThumbprintS256Key:
|
||||
if v, ok := value.(string); ok {
|
||||
h.x509CertThumbprintS256 = &v
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`invalid value for %s key: %T`, X509CertThumbprintS256Key, value)
|
||||
case X509URLKey:
|
||||
if v, ok := value.(string); ok {
|
||||
h.x509URL = &v
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf(`invalid value for %s key: %T`, X509URLKey, value)
|
||||
default:
|
||||
if h.privateParams == nil {
|
||||
h.privateParams = map[string]any{}
|
||||
}
|
||||
h.privateParams[name] = value
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *symmetricKey) Remove(key string) error {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
switch key {
|
||||
case AlgorithmKey:
|
||||
k.algorithm = nil
|
||||
case KeyIDKey:
|
||||
k.keyID = nil
|
||||
case KeyOpsKey:
|
||||
k.keyOps = nil
|
||||
case KeyUsageKey:
|
||||
k.keyUsage = nil
|
||||
case SymmetricOctetsKey:
|
||||
k.octets = nil
|
||||
case X509CertChainKey:
|
||||
k.x509CertChain = nil
|
||||
case X509CertThumbprintKey:
|
||||
k.x509CertThumbprint = nil
|
||||
case X509CertThumbprintS256Key:
|
||||
k.x509CertThumbprintS256 = nil
|
||||
case X509URLKey:
|
||||
k.x509URL = nil
|
||||
default:
|
||||
delete(k.privateParams, key)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (k *symmetricKey) Clone() (Key, error) {
|
||||
key, err := cloneKey(k)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`symmetricKey.Clone: %w`, err)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
func (k *symmetricKey) DecodeCtx() json.DecodeCtx {
|
||||
k.mu.RLock()
|
||||
defer k.mu.RUnlock()
|
||||
return k.dc
|
||||
}
|
||||
|
||||
func (k *symmetricKey) SetDecodeCtx(dc json.DecodeCtx) {
|
||||
k.mu.Lock()
|
||||
defer k.mu.Unlock()
|
||||
k.dc = dc
|
||||
}
|
||||
|
||||
func (h *symmetricKey) UnmarshalJSON(buf []byte) error {
|
||||
h.mu.Lock()
|
||||
defer h.mu.Unlock()
|
||||
h.algorithm = nil
|
||||
h.keyID = nil
|
||||
h.keyOps = nil
|
||||
h.keyUsage = nil
|
||||
h.octets = nil
|
||||
h.x509CertChain = nil
|
||||
h.x509CertThumbprint = nil
|
||||
h.x509CertThumbprintS256 = nil
|
||||
h.x509URL = nil
|
||||
dec := json.NewDecoder(bytes.NewReader(buf))
|
||||
LOOP:
|
||||
for {
|
||||
tok, err := dec.Token()
|
||||
if err != nil {
|
||||
return fmt.Errorf(`error reading token: %w`, err)
|
||||
}
|
||||
switch tok := tok.(type) {
|
||||
case json.Delim:
|
||||
// Assuming we're doing everything correctly, we should ONLY
|
||||
// get either tokens.OpenCurlyBracket or tokens.CloseCurlyBracket here.
|
||||
if tok == tokens.CloseCurlyBracket { // End of object
|
||||
break LOOP
|
||||
} else if tok != tokens.OpenCurlyBracket {
|
||||
return fmt.Errorf(`expected '%c' but got '%c'`, tokens.OpenCurlyBracket, tok)
|
||||
}
|
||||
case string: // Objects can only have string keys
|
||||
switch tok {
|
||||
case KeyTypeKey:
|
||||
val, err := json.ReadNextStringToken(dec)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`error reading token: %w`, err)
|
||||
}
|
||||
if val != jwa.OctetSeq().String() {
|
||||
return fmt.Errorf(`invalid kty value for RSAPublicKey (%s)`, val)
|
||||
}
|
||||
case AlgorithmKey:
|
||||
var s string
|
||||
if err := dec.Decode(&s); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, AlgorithmKey, err)
|
||||
}
|
||||
alg, err := jwa.KeyAlgorithmFrom(s)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, AlgorithmKey, err)
|
||||
}
|
||||
h.algorithm = &alg
|
||||
case KeyIDKey:
|
||||
if err := json.AssignNextStringToken(&h.keyID, dec); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, KeyIDKey, err)
|
||||
}
|
||||
case KeyOpsKey:
|
||||
var decoded KeyOperationList
|
||||
if err := dec.Decode(&decoded); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, KeyOpsKey, err)
|
||||
}
|
||||
h.keyOps = &decoded
|
||||
case KeyUsageKey:
|
||||
if err := json.AssignNextStringToken(&h.keyUsage, dec); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, KeyUsageKey, err)
|
||||
}
|
||||
case SymmetricOctetsKey:
|
||||
if err := json.AssignNextBytesToken(&h.octets, dec); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, SymmetricOctetsKey, err)
|
||||
}
|
||||
case X509CertChainKey:
|
||||
var decoded cert.Chain
|
||||
if err := dec.Decode(&decoded); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertChainKey, err)
|
||||
}
|
||||
h.x509CertChain = &decoded
|
||||
case X509CertThumbprintKey:
|
||||
if err := json.AssignNextStringToken(&h.x509CertThumbprint, dec); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintKey, err)
|
||||
}
|
||||
case X509CertThumbprintS256Key:
|
||||
if err := json.AssignNextStringToken(&h.x509CertThumbprintS256, dec); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, X509CertThumbprintS256Key, err)
|
||||
}
|
||||
case X509URLKey:
|
||||
if err := json.AssignNextStringToken(&h.x509URL, dec); err != nil {
|
||||
return fmt.Errorf(`failed to decode value for key %s: %w`, X509URLKey, err)
|
||||
}
|
||||
default:
|
||||
if dc := h.dc; dc != nil {
|
||||
if localReg := dc.Registry(); localReg != nil {
|
||||
decoded, err := localReg.Decode(dec, tok)
|
||||
if err == nil {
|
||||
h.setNoLock(tok, decoded)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
decoded, err := registry.Decode(dec, tok)
|
||||
if err == nil {
|
||||
h.setNoLock(tok, decoded)
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf(`could not decode field %s: %w`, tok, err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf(`invalid token %T`, tok)
|
||||
}
|
||||
}
|
||||
if h.octets == nil {
|
||||
return fmt.Errorf(`required field k is missing`)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h symmetricKey) MarshalJSON() ([]byte, error) {
|
||||
data := make(map[string]any)
|
||||
fields := make([]string, 0, 9)
|
||||
data[KeyTypeKey] = jwa.OctetSeq()
|
||||
fields = append(fields, KeyTypeKey)
|
||||
if h.algorithm != nil {
|
||||
data[AlgorithmKey] = *(h.algorithm)
|
||||
fields = append(fields, AlgorithmKey)
|
||||
}
|
||||
if h.keyID != nil {
|
||||
data[KeyIDKey] = *(h.keyID)
|
||||
fields = append(fields, KeyIDKey)
|
||||
}
|
||||
if h.keyOps != nil {
|
||||
data[KeyOpsKey] = *(h.keyOps)
|
||||
fields = append(fields, KeyOpsKey)
|
||||
}
|
||||
if h.keyUsage != nil {
|
||||
data[KeyUsageKey] = *(h.keyUsage)
|
||||
fields = append(fields, KeyUsageKey)
|
||||
}
|
||||
if h.octets != nil {
|
||||
data[SymmetricOctetsKey] = h.octets
|
||||
fields = append(fields, SymmetricOctetsKey)
|
||||
}
|
||||
if h.x509CertChain != nil {
|
||||
data[X509CertChainKey] = h.x509CertChain
|
||||
fields = append(fields, X509CertChainKey)
|
||||
}
|
||||
if h.x509CertThumbprint != nil {
|
||||
data[X509CertThumbprintKey] = *(h.x509CertThumbprint)
|
||||
fields = append(fields, X509CertThumbprintKey)
|
||||
}
|
||||
if h.x509CertThumbprintS256 != nil {
|
||||
data[X509CertThumbprintS256Key] = *(h.x509CertThumbprintS256)
|
||||
fields = append(fields, X509CertThumbprintS256Key)
|
||||
}
|
||||
if h.x509URL != nil {
|
||||
data[X509URLKey] = *(h.x509URL)
|
||||
fields = append(fields, X509URLKey)
|
||||
}
|
||||
for k, v := range h.privateParams {
|
||||
data[k] = v
|
||||
fields = append(fields, k)
|
||||
}
|
||||
|
||||
sort.Strings(fields)
|
||||
buf := pool.BytesBuffer().Get()
|
||||
defer pool.BytesBuffer().Put(buf)
|
||||
buf.WriteByte(tokens.OpenCurlyBracket)
|
||||
enc := json.NewEncoder(buf)
|
||||
for i, f := range fields {
|
||||
if i > 0 {
|
||||
buf.WriteRune(tokens.Comma)
|
||||
}
|
||||
buf.WriteRune(tokens.DoubleQuote)
|
||||
buf.WriteString(f)
|
||||
buf.WriteString(`":`)
|
||||
v := data[f]
|
||||
switch v := v.(type) {
|
||||
case []byte:
|
||||
buf.WriteRune(tokens.DoubleQuote)
|
||||
buf.WriteString(base64.EncodeToString(v))
|
||||
buf.WriteRune(tokens.DoubleQuote)
|
||||
default:
|
||||
if err := enc.Encode(v); err != nil {
|
||||
return nil, fmt.Errorf(`failed to encode value for field %s: %w`, f, err)
|
||||
}
|
||||
buf.Truncate(buf.Len() - 1)
|
||||
}
|
||||
}
|
||||
buf.WriteByte(tokens.CloseCurlyBracket)
|
||||
ret := make([]byte, buf.Len())
|
||||
copy(ret, buf.Bytes())
|
||||
return ret, nil
|
||||
}
|
||||
|
||||
func (h *symmetricKey) Keys() []string {
|
||||
h.mu.RLock()
|
||||
defer h.mu.RUnlock()
|
||||
keys := make([]string, 0, 9+len(h.privateParams))
|
||||
keys = append(keys, KeyTypeKey)
|
||||
if h.algorithm != nil {
|
||||
keys = append(keys, AlgorithmKey)
|
||||
}
|
||||
if h.keyID != nil {
|
||||
keys = append(keys, KeyIDKey)
|
||||
}
|
||||
if h.keyOps != nil {
|
||||
keys = append(keys, KeyOpsKey)
|
||||
}
|
||||
if h.keyUsage != nil {
|
||||
keys = append(keys, KeyUsageKey)
|
||||
}
|
||||
if h.octets != nil {
|
||||
keys = append(keys, SymmetricOctetsKey)
|
||||
}
|
||||
if h.x509CertChain != nil {
|
||||
keys = append(keys, X509CertChainKey)
|
||||
}
|
||||
if h.x509CertThumbprint != nil {
|
||||
keys = append(keys, X509CertThumbprintKey)
|
||||
}
|
||||
if h.x509CertThumbprintS256 != nil {
|
||||
keys = append(keys, X509CertThumbprintS256Key)
|
||||
}
|
||||
if h.x509URL != nil {
|
||||
keys = append(keys, X509URLKey)
|
||||
}
|
||||
for k := range h.privateParams {
|
||||
keys = append(keys, k)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
var symmetricStandardFields KeyFilter
|
||||
|
||||
func init() {
|
||||
symmetricStandardFields = NewFieldNameFilter(KeyTypeKey, KeyUsageKey, KeyOpsKey, AlgorithmKey, KeyIDKey, X509URLKey, X509CertChainKey, X509CertThumbprintKey, X509CertThumbprintS256Key, SymmetricOctetsKey)
|
||||
}
|
||||
|
||||
// SymmetricStandardFieldsFilter returns a KeyFilter that filters out standard Symmetric fields.
|
||||
func SymmetricStandardFieldsFilter() KeyFilter {
|
||||
return symmetricStandardFields
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package jwk
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (k *KeyUsageType) Accept(v any) error {
|
||||
switch v := v.(type) {
|
||||
case KeyUsageType:
|
||||
if !isValidUsage(v.String()) {
|
||||
return fmt.Errorf("invalid key usage type: %q", v)
|
||||
}
|
||||
*k = v
|
||||
return nil
|
||||
case string:
|
||||
if !isValidUsage(v) {
|
||||
return fmt.Errorf("invalid key usage type: %q", v)
|
||||
}
|
||||
*k = KeyUsageType(v)
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("invalid Go type for key usage type: %T", v)
|
||||
}
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package jwk
|
||||
|
||||
import "github.com/lestrrat-go/httprc/v3"
|
||||
|
||||
type Whitelist = httprc.Whitelist
|
||||
type WhitelistFunc = httprc.WhitelistFunc
|
||||
|
||||
// InsecureWhitelist is an alias to httprc.InsecureWhitelist. Use
|
||||
// functions in the `httprc` package to interact with this type.
|
||||
type InsecureWhitelist = httprc.InsecureWhitelist
|
||||
|
||||
func NewInsecureWhitelist() InsecureWhitelist {
|
||||
return httprc.NewInsecureWhitelist()
|
||||
}
|
||||
|
||||
// BlockAllWhitelist is an alias to httprc.BlockAllWhitelist. Use
|
||||
// functions in the `httprc` package to interact with this type.
|
||||
type BlockAllWhitelist = httprc.BlockAllWhitelist
|
||||
|
||||
func NewBlockAllWhitelist() BlockAllWhitelist {
|
||||
return httprc.NewBlockAllWhitelist()
|
||||
}
|
||||
|
||||
// RegexpWhitelist is an alias to httprc.RegexpWhitelist. Use
|
||||
// functions in the `httprc` package to interact with this type.
|
||||
type RegexpWhitelist = httprc.RegexpWhitelist
|
||||
|
||||
func NewRegexpWhitelist() *RegexpWhitelist {
|
||||
return httprc.NewRegexpWhitelist()
|
||||
}
|
||||
|
||||
// MapWhitelist is an alias to httprc.MapWhitelist. Use
|
||||
// functions in the `httprc` package to interact with this type.
|
||||
type MapWhitelist = httprc.MapWhitelist
|
||||
|
||||
func NewMapWhitelist() MapWhitelist {
|
||||
return httprc.NewMapWhitelist()
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
package jwk
|
||||
|
||||
import (
|
||||
"crypto/ecdsa"
|
||||
"crypto/ed25519"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/lestrrat-go/blackmagic"
|
||||
"github.com/lestrrat-go/jwx/v3/jwk/jwkbb"
|
||||
)
|
||||
|
||||
// PEMDecoder is an interface to describe an object that can decode
|
||||
// a key from PEM encoded ASN.1 DER format.
|
||||
//
|
||||
// A PEMDecoder can be specified as an option to `jwk.Parse()` or `jwk.ParseKey()`
|
||||
// along with the `jwk.WithPEM()` option.
|
||||
type PEMDecoder interface {
|
||||
Decode([]byte) (any, []byte, error)
|
||||
}
|
||||
|
||||
// PEMEncoder is an interface to describe an object that can encode
|
||||
// a key into PEM encoded ASN.1 DER format.
|
||||
//
|
||||
// `jwk.Key` instances do not implement a way to encode themselves into
|
||||
// PEM format. Normally you can just use `jwk.EncodePEM()` to do this, but
|
||||
// this interface allows you to generalize the encoding process by
|
||||
// abstracting the `jwk.EncodePEM()` function using `jwk.PEMEncodeFunc`
|
||||
// along with alternate implementations, should you need them.
|
||||
type PEMEncoder interface {
|
||||
Encode(any) (string, []byte, error)
|
||||
}
|
||||
|
||||
type PEMEncodeFunc func(any) (string, []byte, error)
|
||||
|
||||
func (f PEMEncodeFunc) Encode(v any) (string, []byte, error) {
|
||||
return f(v)
|
||||
}
|
||||
|
||||
func encodeX509(v any) (string, []byte, error) {
|
||||
// we can't import jwk, so just use the interface
|
||||
if key, ok := v.(Key); ok {
|
||||
var raw any
|
||||
if err := Export(key, &raw); err != nil {
|
||||
return "", nil, fmt.Errorf(`failed to get raw key out of %T: %w`, key, err)
|
||||
}
|
||||
|
||||
v = raw
|
||||
}
|
||||
|
||||
// Try to convert it into a certificate
|
||||
switch v := v.(type) {
|
||||
case *rsa.PrivateKey:
|
||||
return pmRSAPrivateKey, x509.MarshalPKCS1PrivateKey(v), nil
|
||||
case *ecdsa.PrivateKey:
|
||||
marshaled, err := x509.MarshalECPrivateKey(v)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return pmECPrivateKey, marshaled, nil
|
||||
case ed25519.PrivateKey:
|
||||
marshaled, err := x509.MarshalPKCS8PrivateKey(v)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return pmPrivateKey, marshaled, nil
|
||||
case *rsa.PublicKey, *ecdsa.PublicKey, ed25519.PublicKey:
|
||||
marshaled, err := x509.MarshalPKIXPublicKey(v)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
return pmPublicKey, marshaled, nil
|
||||
default:
|
||||
return "", nil, fmt.Errorf(`unsupported type %T for ASN.1 DER encoding`, v)
|
||||
}
|
||||
}
|
||||
|
||||
// EncodePEM encodes the key into a PEM encoded ASN.1 DER format.
|
||||
// The key can be a jwk.Key or a raw key instance, but it must be one of
|
||||
// the types supported by `x509` package.
|
||||
//
|
||||
// Internally, it uses the same routine as `jwk.EncodeX509()`, and therefore
|
||||
// the same caveats apply
|
||||
func EncodePEM(v any) ([]byte, error) {
|
||||
typ, marshaled, err := encodeX509(v)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(`failed to encode key in x509: %w`, err)
|
||||
}
|
||||
|
||||
block := &pem.Block{
|
||||
Type: typ,
|
||||
Bytes: marshaled,
|
||||
}
|
||||
return pem.EncodeToMemory(block), nil
|
||||
}
|
||||
|
||||
const (
|
||||
pmPrivateKey = `PRIVATE KEY`
|
||||
pmPublicKey = `PUBLIC KEY`
|
||||
pmECPrivateKey = `EC PRIVATE KEY`
|
||||
pmRSAPublicKey = `RSA PUBLIC KEY`
|
||||
pmRSAPrivateKey = `RSA PRIVATE KEY`
|
||||
)
|
||||
|
||||
// NewPEMDecoder returns a PEMDecoder that decodes keys in PEM encoded ASN.1 DER format.
|
||||
// You can use it as argument to `jwk.WithPEMDecoder()` option.
|
||||
//
|
||||
// The use of this function is planned to be deprecated. The plan is to replace the
|
||||
// `jwk.WithPEMDecoder()` option with globally available custom X509 decoders which
|
||||
// can be registered via `jwk.RegisterX509Decoder()` function.
|
||||
func NewPEMDecoder() PEMDecoder {
|
||||
return pemDecoder{}
|
||||
}
|
||||
|
||||
type pemDecoder struct{}
|
||||
|
||||
// Decode decodes a key in PEM encoded ASN.1 DER format.
|
||||
// and returns a raw key.
|
||||
func (pemDecoder) Decode(src []byte) (any, []byte, error) {
|
||||
block, rest := pem.Decode(src)
|
||||
if block == nil {
|
||||
return nil, rest, fmt.Errorf(`failed to decode PEM data`)
|
||||
}
|
||||
var ret any
|
||||
if err := jwkbb.DecodeX509(&ret, block); err != nil {
|
||||
return nil, rest, err
|
||||
}
|
||||
return ret, rest, nil
|
||||
}
|
||||
|
||||
// X509Decoder is an interface that describes an object that can decode
|
||||
// a PEM encoded ASN.1 DER format into a specific type of key.
|
||||
//
|
||||
// This interface is experimental, and may change in the future.
|
||||
type X509Decoder interface {
|
||||
// DecodeX509 decodes the given PEM block into the destination object.
|
||||
// The destination object must be a pointer to a type that can hold the
|
||||
// decoded key, such as *rsa.PrivateKey, *ecdsa.PrivateKey, etc.
|
||||
DecodeX509(dst any, block *pem.Block) error
|
||||
}
|
||||
|
||||
// X509DecodeFunc is a function type that implements the X509Decoder interface.
|
||||
// It allows you to create a custom X509Decoder by providing a function
|
||||
// that takes a destination and a PEM block, and returns an error if the decoding fails.
|
||||
//
|
||||
// This interface is experimental, and may change in the future.
|
||||
type X509DecodeFunc func(dst any, block *pem.Block) error
|
||||
|
||||
func (f X509DecodeFunc) DecodeX509(dst any, block *pem.Block) error {
|
||||
return f(dst, block)
|
||||
}
|
||||
|
||||
var muX509Decoders sync.Mutex
|
||||
var x509Decoders = map[any]int{}
|
||||
var x509DecoderList = []X509Decoder{}
|
||||
|
||||
type identDefaultX509Decoder struct{}
|
||||
|
||||
func init() {
|
||||
RegisterX509Decoder(identDefaultX509Decoder{}, X509DecodeFunc(jwkbb.DecodeX509))
|
||||
}
|
||||
|
||||
// RegisterX509Decoder registers a new X509Decoder that can decode PEM encoded ASN.1 DER format.
|
||||
// Because the decoder could be non-comparable, you must provide an identifier that can be used
|
||||
// as a map key to identify the decoder.
|
||||
//
|
||||
// This function is experimental, and may change in the future.
|
||||
func RegisterX509Decoder(ident any, decoder X509Decoder) {
|
||||
if decoder == nil {
|
||||
panic(`jwk.RegisterX509Decoder: decoder cannot be nil`)
|
||||
}
|
||||
|
||||
muX509Decoders.Lock()
|
||||
defer muX509Decoders.Unlock()
|
||||
if _, ok := x509Decoders[ident]; ok {
|
||||
return // already registered
|
||||
}
|
||||
|
||||
x509Decoders[ident] = len(x509DecoderList)
|
||||
x509DecoderList = append(x509DecoderList, decoder)
|
||||
}
|
||||
|
||||
// UnregisterX509Decoder unregisters the X509Decoder identified by the given identifier.
|
||||
// If the identifier is not registered, it does nothing.
|
||||
//
|
||||
// This function is experimental, and may change in the future.
|
||||
func UnregisterX509Decoder(ident any) {
|
||||
muX509Decoders.Lock()
|
||||
defer muX509Decoders.Unlock()
|
||||
idx, ok := x509Decoders[ident]
|
||||
if !ok {
|
||||
return // not registered
|
||||
}
|
||||
|
||||
delete(x509Decoders, ident)
|
||||
|
||||
l := len(x509DecoderList)
|
||||
switch idx {
|
||||
case l - 1:
|
||||
// if the last element, just truncate the slice
|
||||
x509DecoderList = x509DecoderList[:l-1]
|
||||
case 0:
|
||||
// if the first element, just shift the slice
|
||||
x509DecoderList = x509DecoderList[1:]
|
||||
default:
|
||||
// if the element is in the middle, remove it by slicing
|
||||
// and appending the two slices together
|
||||
x509DecoderList = append(x509DecoderList[:idx], x509DecoderList[idx+1:]...)
|
||||
}
|
||||
}
|
||||
|
||||
// decodeX509 decodes a PEM encoded ASN.1 DER format into the given destination.
|
||||
// It tries all registered X509 decoders until one of them succeeds.
|
||||
// If no decoder can handle the PEM block, it returns an error.
|
||||
func decodeX509(dst any, src []byte) error {
|
||||
block, _ := pem.Decode(src)
|
||||
if block == nil {
|
||||
return fmt.Errorf(`failed to decode PEM data`)
|
||||
}
|
||||
|
||||
var errs []error
|
||||
for _, d := range x509DecoderList {
|
||||
if err := d.DecodeX509(dst, block); err != nil {
|
||||
errs = append(errs, err)
|
||||
continue
|
||||
}
|
||||
// successfully decoded
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf(`failed to decode X509 data using any of the decoders: %w`, errors.Join(errs...))
|
||||
}
|
||||
|
||||
func decodeX509WithPEMDEcoder(dst any, src []byte, decoder PEMDecoder) error {
|
||||
ret, _, err := decoder.Decode(src)
|
||||
if err != nil {
|
||||
return fmt.Errorf(`failed to decode PEM data: %w`, err)
|
||||
}
|
||||
|
||||
if err := blackmagic.AssignIfCompatible(dst, ret); err != nil {
|
||||
return fmt.Errorf(`failed to assign decoded key to destination: %w`, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user