Repository: lestrrat-go/jwx
Pull request #406 (2021-06-30 09:33:39)

    Overhaul jwe.Decrypt/jwe.Message.Decrypt
* jwe.Decrypt now uses the JWE protected header byte sequence as given in the raw encrypted message.
* (jwe.Message).Decrypt is marked for deprecation
* Add jwe.WithMessage() to store the message upon decrypting with jwe.Decrypt
* Add jwe.WithPostParser, jwe.PostParser, jwe.PostParseFunc

codecov-commenter: # [Codecov](https://codecov.io/gh/lestrrat-go/jwx/pull/406?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) Report
> Merging [#406](https://codecov.io/gh/lestrrat-go/jwx/pull/406?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) (bf4f3a3) into [main](https://codecov.io/gh/lestrrat-go/jwx/commit/57cbfd36ade10be73ad6c9c4f5abbc39c0380350?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) (57cbfd3) will **increase** coverage by `0.01%`.
> The diff coverage is `56.52%`.

[![Impacted file tree graph](https://codecov.io/gh/lestrrat-go/jwx/pull/406/graphs/tree.svg?width=650&height=150&src=pr&token=kU2RSDosPt&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go)](https://codecov.io/gh/lestrrat-go/jwx/pull/406?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go)

```diff
@@            Coverage Diff             @@
##             main     #406      +/-   ##
==========================================
+ Coverage   69.56%   69.57%   +0.01%     
==========================================
  Files          76       73       -3     
  Lines        8962     9002      +40     
==========================================
+ Hits         6234     6263      +29     
- Misses       1899     1906       +7     
- Partials      829      833       +4     
```


| [Impacted Files](https://codecov.io/gh/lestrrat-go/jwx/pull/406?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go) | Coverage Δ | |
|---|---|---|
| [jwe/options.go](https://codecov.io/gh/lestrrat-go/jwx/pull/406/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andlL29wdGlvbnMuZ28=) | `50.00% <33.33%> (-10.00%)` | :arrow_down: |
| [jwe/message.go](https://codecov.io/gh/lestrrat-go/jwx/pull/406/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andlL21lc3NhZ2UuZ28=) | `49.73% <51.61%> (+0.72%)` | :arrow_up: |
| [jwe/jwe.go](https://codecov.io/gh/lestrrat-go/jwx/pull/406/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andlL2p3ZS5nbw==) | `60.45% <62.85%> (+1.51%)` | :arrow_up: |
| [internal/ecutil/ecutil.go](https://codecov.io/gh/lestrrat-go/jwx/pull/406/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-aW50ZXJuYWwvZWN1dGlsL2VjdXRpbC5nbw==) | `76.92% <0.00%> (-5.13%)` | :arrow_down: |
| [jwk/es256k.go](https://codecov.io/gh/lestrrat-go/jwx/pull/406/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andrL2VzMjU2ay5nbw==) | | |
| [jwa/secp2561k.go](https://codecov.io/gh/lestrrat-go/jwx/pull/406/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andhL3NlY3AyNTYxay5nbw==) | | |
| [internal/json/goccy.go](https://codecov.io/gh/lestrrat-go/jwx/pull/406/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-aW50ZXJuYWwvanNvbi9nb2NjeS5nbw==) | | |
| [jwk/jwk.go](https://codecov.io/gh/lestrrat-go/jwx/pull/406/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andrL2p3ay5nbw==) | `59.36% <0.00%> (+1.15%)` | :arrow_up: |
| [jwk/refresh.go](https://codecov.io/gh/lestrrat-go/jwx/pull/406/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go#diff-andrL3JlZnJlc2guZ28=) | `75.77% <0.00%> (+2.64%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/lestrrat-go/jwx/pull/406?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/lestrrat-go/jwx/pull/406?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go). Last update [57cbfd3...bf4f3a3](https://codecov.io/gh/lestrrat-go/jwx/pull/406?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=lestrrat-go).


Patch:
diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml
index ff1596c5..910c582e 100644
--- a/.github/workflows/benchmark.yml
+++ b/.github/workflows/benchmark.yml
@@ -28,7 +28,7 @@ jobs:
           go-version: ${{ matrix.go }}
       - name: Install benchstat
         run: |
-          go install golang.org/x/perf/cmd/benchstat@latest
+          go install golang.org/x/perf/cmd/benchstat
       - name: Benchmark
         run: |
           cd bench && make stdlib && make goccy && make benchstat
diff --git a/Changes b/Changes
index 2908b8cc..b46dfa1f 100644
--- a/Changes
+++ b/Changes
@@ -2,11 +2,30 @@ Changes
 =======
 
 v1.2.2
+[Deprecation notice]
+  * `(jwe.Message).Decrypt()` will be removed from the API upon the next
+    major release.
+
+[Bug Fixes]
+  * `jwe.Decrypt` and `(jwe.Message).Decrypt()` failed to decrypt even
+    with the correct message contents when used along with `jwe.RegisterCustomField`
+
+[New features]
+  JWT
   * Options `jwt.WithMinDelta()`, `jwt.WithMaxDelta()` have been added.
     These can be used to compare time-based fields in the JWT object.
   * Option `jwt.WithRequiredClaim()` has been added. This can be used
     to check that JWT contains the given claim.
 
+  JWE
+  * Option `jwe.WithMessage()` has been added. This allows the user to
+    obtain both the decrypted payload _and_ the raw `*jwe.Message` in one
+    go when `jwe.Decrypt()` is called
+  * Option `jwe.WithPostParser()`, along with `jwe.PostParser` and `jwe.PostParseFunc`
+    has been added. This allows advanced users to hook into the `jwe.Decrypt()`
+    process. The hook is called right after the JWE message has been parsed,
+    but before the actual decryption has taken place.
+
 v1.2.1 02 Jun 2021
 [New features]
   * Option `jwt.WithTypedClaim()` and `jwk.WithTypedField()` have been added.
diff --git a/jwe/interface.go b/jwe/interface.go
index ac5d2e03..c380f5a8 100644
--- a/jwe/interface.go
+++ b/jwe/interface.go
@@ -49,6 +49,14 @@ type Message struct {
 	recipients           []Recipient
 	protectedHeaders     Headers
 	unprotectedHeaders   Headers
+
+	// These two fields below are not available for the public consumers of this object.
+	// rawProtectedHeaders stores the original protected header buffer
+	rawProtectedHeaders []byte
+	// storeProtectedHeaders is a hint to be used in UnmarshalJSON().
+	// When this flag is true, UnmarshalJSON() will populate the
+	// rawProtectedHeaders field
+	storeProtectedHeaders bool
 }
 
 // contentEncrypter encrypts the content using the content using the
diff --git a/jwe/jwe.go b/jwe/jwe.go
index 7fe1f954..b09cd8c8 100644
--- a/jwe/jwe.go
+++ b/jwe/jwe.go
@@ -171,40 +171,129 @@ func Encrypt(payload []byte, keyalg jwa.KeyEncryptionAlgorithm, key interface{},
 	return Compact(msg)
 }
 
+// DecryptCtx is used internally when jwe.Decrypt is called, and is
+// passed for hooks that you may pass into it.
+//
+// Regular users should not have to touch this object, but if you need advanced handling
+// of messages, you might have to use it. Only use it when you really
+// understand how JWE processing works in this library.
+type DecryptCtx interface {
+	Algorithm() jwa.KeyEncryptionAlgorithm
+	SetAlgorithm(jwa.KeyEncryptionAlgorithm)
+	Key() interface{}
+	SetKey(interface{})
+	Message() *Message
+	SetMessage(*Message)
+}
+
+type decryptCtx struct {
+	alg jwa.KeyEncryptionAlgorithm
+	key interface{}
+	msg *Message
+}
+
+func (ctx *decryptCtx) Algorithm() jwa.KeyEncryptionAlgorithm {
+	return ctx.alg
+}
+
+func (ctx *decryptCtx) SetAlgorithm(v jwa.KeyEncryptionAlgorithm) {
+	ctx.alg = v
+}
+
+func (ctx *decryptCtx) Key() interface{} {
+	return ctx.key
+}
+
+func (ctx *decryptCtx) SetKey(v interface{}) {
+	ctx.key = v
+}
+
+func (ctx *decryptCtx) Message() *Message {
+	return ctx.msg
+}
+
+func (ctx *decryptCtx) SetMessage(m *Message) {
+	ctx.msg = m
+}
+
+// PostParser is used in conjunction with jwe.WithPostParser(), and is
+// called right after the JWE message has been parsed but
+// before the actual decryption takes place during `jwe.Decrypt()`.
+type PostParser interface {
+	Do(DecryptCtx) error
+}
+
+// PostParseFunc is a PostParser that is represented by a single function
+type PostParseFunc func(DecryptCtx) error
+
+func (fn PostParseFunc) Do(ctx DecryptCtx) error {
+	return fn(ctx)
+}
+
 // Decrypt takes the key encryption algorithm and the corresponding
 // key to decrypt the JWE message, and returns the decrypted payload.
 // The JWE message can be either compact or full JSON format.
 //
 // `key` must be a private key. It can be either in its raw format (e.g. *rsa.PrivateKey) or a jwk.Key
-func Decrypt(buf []byte, alg jwa.KeyEncryptionAlgorithm, key interface{}) ([]byte, error) {
-	if jwkKey, ok := key.(jwk.Key); ok {
-		var raw interface{}
-		if err := jwkKey.Raw(&raw); err != nil {
-			return nil, errors.Wrapf(err, `failed to retrieve raw key from %T`, key)
+func Decrypt(buf []byte, alg jwa.KeyEncryptionAlgorithm, key interface{}, options ...DecryptOption) ([]byte, error) {
+	var ctx decryptCtx
+	ctx.key = key
+	ctx.alg = alg
+
+	var dst *Message
+	var postParse PostParser
+	//nolint:forcetypeassert
+	for _, option := range options {
+		switch option.Ident() {
+		case identMessage{}:
+			dst = option.Value().(*Message)
+		case identPostParser{}:
+			postParse = option.Value().(PostParser)
 		}
-		key = raw
 	}
 
-	msg, err := Parse(buf)
+	msg, err := parseJSONOrCompact(buf, true)
 	if err != nil {
 		return nil, errors.Wrap(err, "failed to parse buffer for Decrypt")
 	}
 
-	return msg.Decrypt(alg, key)
+	ctx.msg = msg
+	if postParse != nil {
+		if err := postParse.Do(&ctx); err != nil {
+			return nil, errors.Wrap(err, `failed to execute PostParser hook`)
+		}
+	}
+
+	payload, err := doDecryptCtx(&ctx)
+	if err != nil {
+		return nil, errors.Wrap(err, `failed to decrypt message`)
+	}
+
+	if dst != nil {
+		*dst = *msg
+		dst.rawProtectedHeaders = nil
+		dst.storeProtectedHeaders = false
+	}
+
+	return payload, nil
 }
 
 // Parse parses the JWE message into a Message object. The JWE message
 // can be either compact or full JSON format.
 func Parse(buf []byte) (*Message, error) {
+	return parseJSONOrCompact(buf, false)
+}
+
+func parseJSONOrCompact(buf []byte, storeProtectedHeaders bool) (*Message, error) {
 	buf = bytes.TrimSpace(buf)
 	if len(buf) == 0 {
 		return nil, errors.New("empty buffer")
 	}
 
 	if buf[0] == '{' {
-		return parseJSON(buf)
+		return parseJSON(buf, storeProtectedHeaders)
 	}
-	return parseCompact(buf)
+	return parseCompact(buf, storeProtectedHeaders)
 }
 
 // ParseString is the same as Parse, but takes a string.
@@ -221,15 +310,16 @@ func ParseReader(src io.Reader) (*Message, error) {
 	return Parse(buf)
 }
 
-func parseJSON(buf []byte) (*Message, error) {
+func parseJSON(buf []byte, storeProtectedHeaders bool) (*Message, error) {
 	m := NewMessage()
+	m.storeProtectedHeaders = storeProtectedHeaders
 	if err := json.Unmarshal(buf, &m); err != nil {
 		return nil, errors.Wrap(err, "failed to parse JSON")
 	}
 	return m, nil
 }
 
-func parseCompact(buf []byte) (*Message, error) {
+func parseCompact(buf []byte, storeProtectedHeaders bool) (*Message, error) {
 	if pdebug.Enabled {
 		pdebug.Printf("Parse(Compact): buf = '%s'", buf)
 	}
@@ -284,6 +374,12 @@ func parseCompact(buf []byte) (*Message, error) {
 	if err := m.Set(TagKey, tagbuf); err != nil {
 		return nil, errors.Wrapf(err, `failed to set %s`, TagKey)
 	}
+
+	if storeProtectedHeaders {
+		// This is later used for decryption.
+		m.rawProtectedHeaders = parts[0]
+	}
+
 	return m, nil
 }
 
@@ -304,38 +400,6 @@ func parseCompact(buf []byte) (*Message, error) {
 //
 //   bdayif, _ := hdr.Get(`x-birthday`)
 //   bday := bdayif.(time.Time)
-//
-// For elements of headers, *the order of definition matters* when
-// verifying the encrypted content.
-//
-// Suppose we are expecting a JSON object `{"bar":1, "foo":2}`
-// in the protected header. Because we need to compute the AAD value
-// using these headers, they are usually normalized in alphabetical order.
-//
-// And since we need to compute back the AAD value after we have parsed the
-// JWE message, your custom type MUST serialize back to the same field order.
-//
-// As an example, configuring this library to parse the above JSON object
-// into the following struct will case `jwe.Decrypt` to fail:
-//
-//    type Thing struct {
-//      Foo int `json:"foo"`
-//      Bar int `json:"bar"`
-//    }
-//    jwe.RegisterCustomField(..., Thing{})
-//
-// This is because `Thing`'s JSON serialization will be rendered
-// by taking each of the fields in the order that they appear in
-// the struct definition, thereby creating a JSON that looks like
-// `{"foo":..., "bar":...}`. Notice that the order of keys are
-// different from the original JSON representation. The JSON keys
-// must be sorted.
-//
-// Therefore it is the caller's responsibility to make sure that
-// the either (1) the struct's fields are declared in alphabetical
-// order, or (2) provide a `MarshalJSON()` method in the custom type
-// such that the alphabetical order is guaranteed in resulting
-// JSON objects.
 func RegisterCustomField(name string, object interface{}) {
 	registry.Register(name, object)
 }
diff --git a/jwe/message.go b/jwe/message.go
index 64f5920e..f3e52f0c 100644
--- a/jwe/message.go
+++ b/jwe/message.go
@@ -306,14 +306,21 @@ func (m *Message) UnmarshalJSON(buf []byte) error {
 		return errors.Wrap(err, `failed to unmashal JSON into message`)
 	}
 
-	var phstr string
-	if err := json.Unmarshal(proxy.ProtectedHeaders, &phstr); err != nil {
-		return errors.Wrap(err, `failed to unmarshal protected headers into string`)
+	// Get the string value
+	var protectedHeadersStr string
+	if err := json.Unmarshal(proxy.ProtectedHeaders, &protectedHeadersStr); err != nil {
+		return errors.Wrap(err, `failed to decode protected headers (1)`)
+	}
+
+	// It's now in _quoted_ base64 string. Decode it
+	protectedHeadersRaw, err := base64.DecodeString(protectedHeadersStr)
+	if err != nil {
+		return errors.Wrap(err, "failed to base64 decoded protected headers buffer")
 	}
 
 	h := NewHeaders()
-	if err := h.Decode([]byte(phstr)); err != nil {
-		return errors.Wrap(err, `failed to decode protected headers`)
+	if err := json.Unmarshal(protectedHeadersRaw, h); err != nil {
+		return errors.Wrap(err, `failed to decode protected headers (2)`)
 	}
 
 	// if this were a flattened message, we would see a "header" and "ciphertext"
@@ -384,6 +391,11 @@ func (m *Message) UnmarshalJSON(buf []byte) error {
 	}
 
 	m.protectedHeaders = h
+	if m.storeProtectedHeaders {
+		// this is later used for decryption
+		m.rawProtectedHeaders = base64.Encode(protectedHeadersRaw)
+	}
+
 	if !proxy.UnprotectedHeaders.(isZeroer).isZero() {
 		m.unprotectedHeaders = proxy.UnprotectedHeaders
 	}
@@ -425,16 +437,42 @@ func (m *Message) makeDummyRecipient(enckeybuf string, protected Headers) error
 	return nil
 }
 
-// Decrypt decrypts the message using the specified algorithm and key
+// Decrypt decrypts the message using the specified algorithm and key.
 //
 // `key` must be a private key in its "raw" format (i.e. something like
 // *rsa.PrivateKey, instead of jwk.Key)
+//
+// This method is marked for deprecation. It will be removed from the API
+// in the next major release. You should not rely on this method
+// to work 100% of the time, especially when it was obtained via jwe.Parse
+// instead of being constructed from scratch by this library.
 func (m *Message) Decrypt(alg jwa.KeyEncryptionAlgorithm, key interface{}) ([]byte, error) {
+	var ctx decryptCtx
+	ctx.alg = alg
+	ctx.key = key
+	ctx.msg = m
+
+	return doDecryptCtx(&ctx)
+}
+
+func doDecryptCtx(dctx *decryptCtx) ([]byte, error) {
 	if pdebug.Enabled {
 		g := pdebug.FuncMarker()
 		defer g.End()
 	}
 
+	m := dctx.msg
+	alg := dctx.alg
+	key := dctx.key
+
+	if jwkKey, ok := key.(jwk.Key); ok {
+		var raw interface{}
+		if err := jwkKey.Raw(&raw); err != nil {
+			return nil, errors.Wrapf(err, `failed to retrieve raw key from %T`, key)
+		}
+		key = raw
+	}
+
 	var err error
 	ctx := context.TODO()
 	h, err := m.protectedHeaders.Clone(ctx)
@@ -455,9 +493,16 @@ func (m *Message) Decrypt(alg jwa.KeyEncryptionAlgorithm, key interface{}) ([]by
 		aad = base64.Encode(aadContainer)
 	}
 
-	computedAad, err := m.protectedHeaders.Encode()
-	if err != nil {
-		return nil, errors.Wrap(err, "failed to encode protected headers")
+	var computedAad []byte
+	if len(m.rawProtectedHeaders) > 0 {
+		computedAad = m.rawProtectedHeaders
+	} else {
+		// this is probably not required once msg.Decrypt is deprecated
+		var err error
+		computedAad, err = m.protectedHeaders.Encode()
+		if err != nil {
+			return nil, errors.Wrap(err, "failed to encode protected headers")
+		}
 	}
 
 	dec := NewDecrypter(alg, enc, key).
diff --git a/jwe/options.go b/jwe/options.go
index da36e310..c3ddb4ff 100644
--- a/jwe/options.go
+++ b/jwe/options.go
@@ -7,8 +7,22 @@ import (
 )
 
 type Option = option.Interface
+type identMessage struct{}
+type identPostParser struct{}
 type identPrettyFormat struct{}
 type identProtectedHeader struct{}
+
+type DecryptOption interface {
+	Option
+	decryptOption()
+}
+
+type decryptOption struct {
+	Option
+}
+
+func (*decryptOption) decryptOption() {}
+
 type SerializerOption interface {
 	Option
 	serializerOption()
@@ -43,3 +57,31 @@ func WithProtectedHeaders(h Headers) EncryptOption {
 	cloned, _ := h.Clone(context.Background())
 	return &encryptOption{option.New(identProtectedHeader{}, cloned)}
 }
+
+// WithMessage provides a message object to be populated by `jwe.Decrpt`
+// Using this option allows you to decrypt AND obtain the `jwe.Message`
+// in one go.
+//
+// Note that you should NOT be using the message object for anything other
+// than inspecting its contents. Particularly, do not expect the message
+// reliable when you call `Decrypt` on it. `(jwe.Message).Decrypt` is
+// slated to be deprecated in the next major version.
+func WithMessage(m *Message) DecryptOption {
+	return &decryptOption{option.New(identMessage{}, m)}
+}
+
+// WithPostParser specifies the handler to be called immediately
+// after the JWE message has been parsed, but before decryption
+// takes place.
+//
+// This option exists to allow advanced users that require the use
+// of information stored in the JWE message to determine how the
+// decryption should be handled.
+//
+// For security reasons it is highly recommended that you thoroughly
+// study how the process works before using this option. This is especially
+// true if you are trying to infer key algorithms and keys to use to
+// decrypt a message using non-standard hints.
+func WithPostParser(p PostParser) DecryptOption {
+	return &decryptOption{option.New(identPostParser{}, p)}
+}


Test patch:
diff --git a/examples/jwe_example_test.go b/examples/jwe_example_test.go
index 098ef229..0a90a70c 100644
--- a/examples/jwe_example_test.go
+++ b/examples/jwe_example_test.go
@@ -3,10 +3,13 @@ package examples_test
 import (
 	"crypto/rand"
 	"crypto/rsa"
+	"fmt"
 	"log"
 
+	"github.com/lestrrat-go/jwx/internal/jwxtest"
 	"github.com/lestrrat-go/jwx/jwa"
 	"github.com/lestrrat-go/jwx/jwe"
+	"github.com/pkg/errors"
 )
 
 func ExampleJWE_Encrypt() {
@@ -61,3 +64,79 @@ func ExampleJWE_Decrypt() {
 	}
 	// OUTPUT:
 }
+
+func ExampleJWE_ComplexDecrypt() {
+	// WARNING: THIS USAGE IS NOT FOR A CASUAL USER. ONLY use it when you must.
+	// Only use it when you understand how JWE is supposed to work. Only use it
+	// when you understand the inner workings of this code.
+
+	// In this example, the caller wants to determine the key to use by checking
+	// the value of a protected header called `jwx-hints`.
+
+	const payload = "Hello, World!"
+
+	privkey, err := jwxtest.GenerateRsaKey()
+	if err != nil {
+		fmt.Printf("failed to generate key: %s\n", err)
+		return
+	}
+
+	// First we will create a sample JWE payload
+	protected := jwe.NewHeaders()
+	protected.Set(`jwx-hints`, `foobar`) // in real life this would a more meaningful value
+	encrypted, err := jwe.Encrypt(
+		[]byte(payload),
+		jwa.RSA_OAEP,
+		privkey.PublicKey,
+		jwa.A256GCM,
+		jwa.NoCompress,
+		jwe.WithProtectedHeaders(protected),
+	)
+	if err != nil {
+		fmt.Printf("failed to encrypt message\n")
+		return
+	}
+
+	// The party responsible to determining the key is the jwe.PostParser hook.
+	//
+	// Here we are using a function turned into an interface for brevity, but in real life
+	// I would personally recommend creating a real type for your specific needs
+	// instead of passing adhoc functions. YMMV.
+	pp := func(ctx jwe.DecryptCtx) error {
+		msg := ctx.Message()
+		rawhint, _ := msg.ProtectedHeaders().Get(`jwx-hints`)
+		//nolint:forcetypeassert
+		hint, ok := rawhint.(string)
+		if ok && hint == `foobar` {
+			// This is where we are setting the key to be used.
+			//
+			// In real life you would look up the key or something.
+			// Here we just assign the key to use.
+			//
+			// You may opt to set both the algorithm and key here as well.
+			// BUT BE CAREFUL so that you don't accidentally create a
+			// vulnerability
+			ctx.SetKey(privkey)
+			return nil
+		}
+
+		// If there were errors, just return it, and the whole jwe.Decrypt will fail.
+		return errors.Errorf(`invalid value for jwx-hints: %s`, rawhint)
+	}
+
+	// Calling jwe.Decrypt with the extra argument of jwe.WithPostParser().
+	// Here we pass a nil key to jwe.Decrypt, because the PostParser will be determining the key to use
+	// when its Do() method is called
+	decrypted, err := jwe.Decrypt(encrypted, jwa.RSA_OAEP, nil, jwe.WithPostParser(jwe.PostParseFunc(pp)))
+	if err != nil {
+		fmt.Printf("failed to decrypt message: %s\n", err)
+		return
+	}
+
+	if string(decrypted) != payload {
+		fmt.Printf("wrong decrypted payload: %s\n", decrypted)
+		return
+	}
+
+	// OUTPUT:
+}
diff --git a/jwe/gh402_test.go b/jwe/gh402_test.go
new file mode 100644
index 00000000..e04896e5
--- /dev/null
+++ b/jwe/gh402_test.go
@@ -0,0 +1,87 @@
+package jwe_test
+
+import (
+	"testing"
+
+	"github.com/lestrrat-go/jwx/internal/json"
+	"github.com/lestrrat-go/jwx/jwa"
+	"github.com/lestrrat-go/jwx/jwe"
+	"github.com/stretchr/testify/assert"
+)
+
+// Pin represents the structured clevis data which can be used to decrypt the jwe message
+type Pin struct {
+	Pin     string      `json:"pin"`
+	Tang    *TangPin    `json:"tang,omitempty"`
+	Tpm2    *Tpm2Pin    `json:"tpm2,omitempty"`
+	Sss     *SssPin     `json:"sss,omitempty"`
+	Yubikey *YubikeyPin `json:"yubikey,omitempty"`
+}
+
+type TangPin struct {
+	Advertisement *json.RawMessage `json:"adv,omitempty"`
+	URL           string           `json:"url"`
+}
+
+type Tpm2Pin struct {
+	Hash    string `json:"hash,omitempty"`
+	Key     string `json:"key,omitempty"`
+	JwkPub  string `json:"jwk_pub,omitempty"`
+	JwkPriv string `json:"jwk_priv,omitempty"`
+	PcrBank string `json:"pcr_bank,omitempty"`
+	PcrIds  string `json:"pcr_ids,omitempty"`
+}
+
+type SssPin struct {
+	Jwe       []string `json:"jwe"`
+	Threshold int      `json:"t"`
+	Prime     string   `json:"p"`
+}
+
+type YubikeyPin struct {
+	Type      string     `json:"type"`
+	Challenge string     `json:"chalelenge"`
+	Slot      int        `json:"slot"`
+	Kdf       YubikeyKdf `json:"kdf"`
+}
+
+type YubikeyKdf struct {
+	Type       string `json:"type"`
+	Hash       string `json:"hash"`
+	Iterations int    `json:"iter"`
+	Salt       string `json:"salt"`
+}
+
+func TestGH402(t *testing.T) {
+	key := []byte{195, 170, 42, 171, 98, 176, 98, 162, 57, 170, 62, 69, 175, 209, 200, 151, 81, 135, 63, 43, 93, 20, 16, 111, 13, 26, 138, 188, 15, 19, 26, 242}
+	data := "eyJhbGciOiJkaXIiLCJjbGV2aXMiOnsicGluIjoic3NzIiwic3NzIjp7Imp3ZSI6WyJleUpoYkdjaU9pSkZRMFJJTFVWVElpd2lZMnhsZG1seklqcDdJbkJwYmlJNkluUmhibWNpTENKMFlXNW5JanA3SW1Ga2RpSTZleUpyWlhseklqcGJleUpoYkdjaU9pSkZRMDFTSWl3aVkzSjJJam9pVUMwMU1qRWlMQ0pyWlhsZmIzQnpJanBiSW1SbGNtbDJaVXRsZVNKZExDSnJkSGtpT2lKRlF5SXNJbmdpT2lKQlZqbEplVmszZDNCamNtMVpZMDkxZWsxTE9VSjJaMDFDZDNJd09FNXhURXBtUW5Ca1IzVXpNa1ZDWjJORmRITnpNRGswVEdweFdXSlFhVmhQTFhWTWMyeDVValZUVlRsdlNHbGhaRlI1VlhWb1UzUTJhbXQzSWl3aWVTSTZJa0ZIVkRseVpYcHlkV0pITFU1eFgxWkxPRUl3UXpGTE9WSTJNV05YU1U1UFNVaEZSMU5MYlZKclUxZEtjVEJRWHpkaFVHaDJlV3hhWDNaTFdHSk5TMGhrU1d0VFFrd3dSMWRRTmxaaVdqbExZVEJHVkZGSFpqTWlmU3g3SW1Gc1p5STZJa1ZUTlRFeUlpd2lZM0oySWpvaVVDMDFNakVpTENKclpYbGZiM0J6SWpwYkluWmxjbWxtZVNKZExDSnJkSGtpT2lKRlF5SXNJbmdpT2lKQlNIZFFhR2hPZDBac1ptc3RNR2d6VDNGS1JFVXpkekpVWDJGcExUbGlZWGhXY0VKa05TMWtNbXhIU0VseFgyTXhVSEZrTW1RMlYxSkRXbEZVYkc5cWQyUmhOblJUUkVndFUxTmhaVE5aTmxGVlZIVkVla1puSWl3aWVTSTZJa0ZrYVhkdVozaDVlamt4UVMxUFRGODFhekIwVEcwMlZtSTNSVkZxWVcxQ1NWUlhlVzVuUldSRVpuRlRSbm8xWlRkdVkycFdlR3BpWDFCSVMwbFNaRzFPT1hCa2RFMTRlRUpPYTFkV2RsOWZUbkZxVTI5SU5UUWlmVjE5TENKMWNtd2lPaUpvZEhSd09pOHZiRzlqWVd4b2IzTjBPak16TURJeEluMTlMQ0psYm1NaU9pSkJNalUyUjBOTklpd2laWEJySWpwN0ltTnlkaUk2SWxBdE5USXhJaXdpYTNSNUlqb2lSVU1pTENKNElqb2lRVWR1YjNrMFQyMTBhRWt3TjNKV1QxbFNTVGhxUTIxVGMxQjZXVVYwUkcwNWNqRkRaa0ptVmxCa2RscEdVVE0wY2taVmJFTTFRbGRrVlZweVMyOXNYMHh5UVhsdFlrY3pWVEZhY1VRMU9YSnBlVGQzV1hGWVV5SXNJbmtpT2lKQlNHaHNSVGx0T1hoR04xVkVVVXd4WjJSd2RGaE5Zbm94VUhNMloweDRObWx1ZGpodU4xVXpUamhpU0dSTlJIRldhalZKYVdGVGFGbDRZM1kyYzFSQ2VuUXpNMmxrUlRaZlYyUm1Oell3WVhkV1VVUkRXa1oxSW4wc0ltdHBaQ0k2SWpWQlNVaHJjblpDYzFOZmQzRjFMVGN5TUd0Q1dtMTBWblpxYkdsVGVWSmpUVXREVFU5dmJuWlFSVWtpZlEuLlR0SzJzWTE2bDJnSmx6c18uRFZqcGVkbGUxY29xZkFkLUZ3bnM5RmtRVkR0ak4zWU1NWjc4VjNLVmI3VWpFd2p1eTBXYjZ0eUxzTmZsVnJaVU85dDM1X2pfSlpobU0ycllfV1RyOHcuVkZMYjFvcTFqZzRsdnJpY3laaXF6USIsImV5SmhiR2NpT2lKRlEwUklMVVZUSWl3aVkyeGxkbWx6SWpwN0luQnBiaUk2SW5SaGJtY2lMQ0owWVc1bklqcDdJbUZrZGlJNmV5SnJaWGx6SWpwYmV5SmhiR2NpT2lKRlEwMVNJaXdpWTNKMklqb2lVQzAxTWpFaUxDSnJaWGxmYjNCeklqcGJJbVJsY21sMlpVdGxlU0pkTENKcmRIa2lPaUpGUXlJc0luZ2lPaUpCVUZWTVMxVllhMUp2TjJOdGFXOXFVRUV3ZFVZMGVIcFFXakJ0V21sQ1NFeFBhVWRyV0hkQ1pGTkVibVpNV1ZaTmJFeFZWR0UzTXkxNWJYSnVWME5mUlRZdE9DMWpUbnA0U1RaMWVtMTRhVFphTXpCUU1EaHhJaXdpZVNJNklrRlBPVTFWTVdGQmRWRnJNVEJrY1dGbVIwRlFYMlpKVGkwelRXTldNRkpGT0drNGFVdG5UbGhuWm14SlFtbDZNRGMxVmpCWVJIZEZNbkpSU2xka1VsRktlWGczT0hFeVJrTndkekZGTTFCMk1YRnZZMmhCZDJnaWZTeDdJbUZzWnlJNklrVlROVEV5SWl3aVkzSjJJam9pVUMwMU1qRWlMQ0pyWlhsZmIzQnpJanBiSW5abGNtbG1lU0pkTENKcmRIa2lPaUpGUXlJc0luZ2lPaUpCVUUxSFZYWTFjSFZUZWt0MU1HRjRZVFJRYldOeFRsQkxiMjR0TW1oYVVGSjJXbmRwV2xkc05sOVNNREJ2YlZsMldESXplRlJ4UldGUVdFMXphemxFYm1SdGNrUnpWbEZvTlRFMFEzTTBSbGszV0VaTlkyUkVJaXdpZVNJNklrRlZNa051VUV0RlJVeDBhSFZaYlZaeWJWZEJORFZLYjNocE1FRkhjM2w1T0daak5UTlJiMHA2UjA4NWNYVnRXVVpRTTFWbVoyWjZibkZsTnpWcE5uUXpkSFJPUW0xSWNGVlhTa1JuUkRkdGRGbEdVbXRzYzFjaWZWMTlMQ0oxY213aU9pSm9kSFJ3T2k4dmJHOWpZV3hvYjNOME9qUXhPRFl4SW4xOUxDSmxibU1pT2lKQk1qVTJSME5OSWl3aVpYQnJJanA3SW1OeWRpSTZJbEF0TlRJeElpd2lhM1I1SWpvaVJVTWlMQ0o0SWpvaVFWazJVMmxoUzFaRFMwUnFRMjUwTFdWNFJFZzJTa3RhYVdkaWRXOVpZVzV3Y0hWRVkxOXNOVXBxY1ROR05GWnpMV3hJYzFGbVYzQmFOVXRPWTNCUWFITmZObFpVUmt4ZlYwMVdja2xCZG5wWWVFeExZMmhKY0NJc0lua2lPaUpCU1RCVFZVSTFUMG80ZGtkR05YRndXRzVFZVRkd1pteFBNVjlsZW14WWQyTTNValpxY25RelJrSklPRTEwZG0xcFRHVkZNRzR0VEhZNFMyNWZMVWMzUlhJM1MwRklNSEoyWW5kMlVFZzBjbXhrU1VrM2RETmxJbjBzSW10cFpDSTZJa1JNT1ZaclRHcFVaVXBKTVhRd2ExUnNUbnBzVGxrM1RHcDFTWGxLZHpGUE1HTkdYMHN0YW5GdGFHOGlmUS4uSjJ4c2VjeTN3c2FGRjFfdi5uQzdPSzVrdXAwbkgxVGwwOFBRUE82QTllNjFYMXNGY1RFUVZjTjNkd0NWYTFMZ2p2STFfSWZqTU5TX2cweTI4WGM1ZThQZ2hFS01UQ0hkQk44UzFBdy5yV0s5Qi0yT2RtWUlIbmQzMzcxeHpRIiwiZXlKaGJHY2lPaUpGUTBSSUxVVlRJaXdpWTJ4bGRtbHpJanA3SW5CcGJpSTZJblJoYm1jaUxDSjBZVzVuSWpwN0ltRmtkaUk2ZXlKclpYbHpJanBiZXlKaGJHY2lPaUpGUTAxU0lpd2lZM0oySWpvaVVDMDFNakVpTENKclpYbGZiM0J6SWpwYkltUmxjbWwyWlV0bGVTSmRMQ0pyZEhraU9pSkZReUlzSW5naU9pSkJRbVZ1WVRaWVVUQkNTMmQwU1U5c05FMXZUakZEZFhKc2REVk1lVEZ6YUZNMFRFbHdkV05aUkVaWE9VNDVNRWgxY1RSUk4wcENPR3RNVDNabVdEUnBhSFJwTFdWQ1JXdGpVR3BCYzNWWU9HWm5kbVEyVEZSM0lpd2llU0k2SWtGUlRFWnVZMmhmZVRCaVdFNTRTRGt6UW10NWQwSmFhbXB6T1ROaFptOXFXWGhyV0d0dFFXZFRVbnA1ZGxaWGFrbHJRMEYwVWtoRVZuVmhlbkUxTWtSRFYxVnRlRU5YWm1OR05qWkdSaTB5Y1ZrellsTk9Na01pZlN4N0ltRnNaeUk2SWtWVE5URXlJaXdpWTNKMklqb2lVQzAxTWpFaUxDSnJaWGxmYjNCeklqcGJJblpsY21sbWVTSmRMQ0pyZEhraU9pSkZReUlzSW5naU9pSkJaR2hHZW05RE5FZG5WRk5CWlUxTFVYSlhjRWRCTWtndFEyeEVNVE52U1d4SVprczFTWEI2ZVZseWRuVm1Sbk5DZDJkYWJWYzRWR0ZXUkdSYVltbGlkM3BtTldSMk1rOHRkVkpmU2pSS2IyNDRNekJ0ZFVGa0lpd2llU0k2SWtGUlpFMVdka3RGWW5obWMwbHRiVFJtVHpZMlFsOHdVMnhPWDBaRmVEQkthVlpyVjFKQlFrRnBhRVExV1VKUWVVbEpjM1pGUjJoR1dqaFpOVXBWTXpoQ1JWVlJUWGhyVkZsT1pEVlpOR1UxVG1STVVVbDNObDhpZlYxOUxDSjFjbXdpT2lKb2RIUndPaTh2Ykc5allXeG9iM04wT2pNME1qVTVJbjE5TENKbGJtTWlPaUpCTWpVMlIwTk5JaXdpWlhCcklqcDdJbU55ZGlJNklsQXROVEl4SWl3aWEzUjVJam9pUlVNaUxDSjRJam9pUVZaU1praEpWVVJ2U0RCb1FYRnhOSFpJYVVGMmNVOVNXblJUVFZkS2VGVmtPWEozV2tWdVZFMUVWbWt4VG5KbGRWTnZWWHBNWmxkcFVVcDBiR3BxU21ZM1NqRnpUbGRVUmpKaVFtTnJNSEJrUnpkcFJHVnhVeUlzSW5raU9pSkJSak5aUms5dlNXNW1jVU5yVFRKa1pISnlkV1JyY21zMmN6Wk1SMUpNU0U5YVl6QlhhMmRhZFZsb1FsRk9ZbVZXY25sWFZTMW9aMTgwYkhCRE5EUjRaRmh3ZUc4NWFGOWhSVlZ0U0V0UGFWOWpVM2xhY1VjM0luMHNJbXRwWkNJNkluQnNablF4UW5WR2EyeGZaRGhyZFZOaWNVVlVSbXhxYjJwM01WZDBMVFpLYTJ0VGVXOWtWa1JEZDNjaWZRLi5CUUVtRXFhc1FHNHNKY1d2Lmo5RzBweVEwbTY0TU5OTEZicm4wT01BbmZaOTJ5bWNxV19ZM2ZIMWJEd2JFbnI1U1FHWF9jWjNfbUpGSS0zMFVJOEZuZzRUaXRxMU9JT2JySktETTh3LndRTmVJVzhzZnNua1V1aTUwTWpYd0EiLCJleUpoYkdjaU9pSkZRMFJJTFVWVElpd2lZMnhsZG1seklqcDdJbkJwYmlJNkluUmhibWNpTENKMFlXNW5JanA3SW1Ga2RpSTZleUpyWlhseklqcGJleUpoYkdjaU9pSkZRMDFTSWl3aVkzSjJJam9pVUMwMU1qRWlMQ0pyWlhsZmIzQnpJanBiSW1SbGNtbDJaVXRsZVNKZExDSnJkSGtpT2lKRlF5SXNJbmdpT2lKQlJVdzRjbkZ3UjE4dE9FRXRiM1UwYTJaMFJXbFNjbVZ5VldOb1ozWmlVRzFXYUV0bVJVNWtXVXd0ZDNOVlRucGZNMlpRZW5GUVVtWkhiRzltTkZkR2RFMTNNM0I1YnpaSVFUaFFTM0JHVlc5VWJ6ZHRjbWRSSWl3aWVTSTZJa0ZDUWxsVU9WOWpWRnBuT1RCWlJXWk9jWFV4V2tkSGFtaFphMWxwTTFkWlRqWmpialZJUmtjdGJ5MUdOR3BIWWpkVFpEazFTbWsxTkhkVWRUQmxOVmM1UjE5cGVFRlZYMWh0Wm1GSmQyZFRXVXN3WDJScVpEVWlmU3g3SW1Gc1p5STZJa1ZUTlRFeUlpd2lZM0oySWpvaVVDMDFNakVpTENKclpYbGZiM0J6SWpwYkluWmxjbWxtZVNKZExDSnJkSGtpT2lKRlF5SXNJbmdpT2lKQlZYSktNME5GUzFZd2FGWlNNamhLWTE5eWRXUkpUWEF0YmxCUmRtdFNSemxGVlZsVVRrTjJiVEZrY2tWUlJVNXVWMjB3VXpNelMycEtTMWx1VFhObVJrTlRPR3BYV0hJMFkxUTNTMFV4V2xkWmFGRnJkRzF3SWl3aWVTSTZJa0ZrYUVkWGFFVjJNREE1VjJwdFRUQjFZWFpFTm5sR01HczNMVXR0WkRCTmNHNTNRelZtWVVReFZuTkhWSGhLVG5SYVptSmlWbGhGVTIxeVJYZ3pYMVJoVERZMFpVeFhObFZwTFVveWJscHZhbEpUU0hWWmFtTWlmVjE5TENKMWNtd2lPaUpvZEhSd09pOHZiRzlqWVd4b2IzTjBPalF6TVRVMUluMTlMQ0psYm1NaU9pSkJNalUyUjBOTklpd2laWEJySWpwN0ltTnlkaUk2SWxBdE5USXhJaXdpYTNSNUlqb2lSVU1pTENKNElqb2lRV1JrT0Rnd05FdFlRbGhuWmpacU1EQkdVbEIyUzBsWmFGaE5ZMnRpWjE5c1NXbHdhbVF6TlhGUk9FVk9MVEZGUTNOUVZrNHRTa1I1ZW1sVmVuSkhjVTVOUkRGSmJGOUlTMUIyVGpaUmFVMU9kVXB0VVcxcWJpSXNJbmtpT2lKQlF5MW9Tak5DUTBvMU9VSnZhbWR1Ym5oZlZuaHBWekZNTVU5UlYxTXRZMmxzV2xkRGVIaGhZVUpyYjA0MVV6RnpiMnh4Y1ZwbVFuQkNiakZSWW5OTlJEZGZiVFEwVm1weGRWQlFjMjQwVmpFNFFqQlZkV2hDSW4wc0ltdHBaQ0k2SWtaUmNWZDBUWGQ0V2pWbVEyWjFSekp1WldWT1lreEpkQzF6Tm1Ka2RUQXpaMUZSU0Vvd00wY3RNMDBpZlEuLmduekZoT21WQk41UFNWT2kuZlBkQWJ2YlgtLUFXV1pPREZEdy1Yd2VvZHJQamc5UGM4SlFQQjIwZTlPQ2Y5RTZmSEtNYUJlR2xVSWRyOHViRjdRUjllM3Vnalk2ZEdLOEoyeklyYXcuTlV6T3M4a01HVVhMa3BXNWxja3BnZyIsImV5SmhiR2NpT2lKRlEwUklMVVZUSWl3aVkyeGxkbWx6SWpwN0luQnBiaUk2SW5SaGJtY2lMQ0owWVc1bklqcDdJbUZrZGlJNmV5SnJaWGx6SWpwYmV5SmhiR2NpT2lKRlEwMVNJaXdpWTNKMklqb2lVQzAxTWpFaUxDSnJaWGxmYjNCeklqcGJJbVJsY21sMlpVdGxlU0pkTENKcmRIa2lPaUpGUXlJc0luZ2lPaUpCWTNVeWFFOTVXSGRPVG1OYVpXRnJkVTFRVW01aGFqSnJXbGxJUldkMVFuRmZiR0ZMU0VZeFUwbzFjMVp3UmswMVVsZFJYMVp5U2pKVmVtMHpXa0k0T1Y5Nk5HZExVVzVETFU1TFJHTk5NMHcxUkVsdlh6UlNJaXdpZVNJNklrRmlOVXBTU1hOeU5HSm9kRXhvV1dabVVITlpaMUZqT0hocGNXcFplak5wUm1GeVpIbEVkVFZRZHpSVFRIZExWR1JuV2tSMGNtODVNSHByTVRKVFpERmhYemgxZVRkV1drMHRTRVkzUWs5bGNXeG9RV3R2VEdnaWZTeDdJbUZzWnlJNklrVlROVEV5SWl3aVkzSjJJam9pVUMwMU1qRWlMQ0pyWlhsZmIzQnpJanBiSW5abGNtbG1lU0pkTENKcmRIa2lPaUpGUXlJc0luZ2lPaUpCVm1Sa1ZFWklNRWxwTm1OWk1sQk9XVVp1WVZkT2IwaENlRzVuVFZaR1kzZFlObVYzU25vd1VVUlZjbkF5UmxsT1pVNHpaQzFxYWxwcVdYQmZkRkJ0Ukdjd05YcG1iSHAyU0VoSWJscFlUM2RLT1RSSU5VOHhJaXdpZVNJNklrRllaRkZ5VDBFdFJXWjFTM2xHU0drdE5sVlFSRGMwUnkxaVdVdFhNbFV4YVZselpFTkRSakp5WWs5T1JXeDBTVFowT0ZSZk56bHZjR1pLTkcxU1gySnliMVZqVVhBNU1GOWFZM05RY3poZlZtRndSbmhyVmpJaWZWMTlMQ0oxY213aU9pSm9kSFJ3T2k4dmJHOWpZV3hvYjNOME9qTTBPRGM1SW4xOUxDSmxibU1pT2lKQk1qVTJSME5OSWl3aVpYQnJJanA3SW1OeWRpSTZJbEF0TlRJeElpd2lhM1I1SWpvaVJVTWlMQ0o0SWpvaVFXUkNVVlJKUkdjNGNEWm5NVzh4T0d4R1dYRm1kMXBhTWxaaFJteG9VMlF0YVhkV09EQnRXalZaWm5wWU5qbFNWV3BxYzNKc1ZHZEtjbXMwY2pKaWRrSlZUbE5YY0cxbVEyeHBYek0xYm1Wb2NrZHpRbW94VUNJc0lua2lPaUpCVTJGVVYxY3RSRk5ZVVhObFgxUmxibDg0VkdkVmFrWTNVRWxRTm5wcVpUUmxSbk55YkY5WFRESkhNRGwzTFROalYzVlRlVlZmUzFvemF6VndTVlp0TlZkaU1tcGlXREptV2sxWmVFNVpRbDh5Vm10d1QxWnVJbjBzSW10cFpDSTZJbEk0YWtWSk1YQlRNVWxoY0hWVVZHTktObGhJWkROWFUzbGZRbmhXU1hWb1VVeDFVekJwYjJkU09UUWlmUS4uTEk2ZC1TNUlYSDRIY2lxeS5ESWl3M1hFeXlaMVZWZDFPcWYwS1pJeGZrUWtnZXo4bXl2N0dfZGFMOWxIdjVFVlFMQzREVG9PMWF6UDQ5SDNGUk5PRHZIcXBmaWp1a3VDX2JmLVZldy41NGpyZnZWdEYxR3ItYU9TVEFjSWV3Il0sInAiOiI0NERteG1EZVFiWTJ3eHpIYm9PODFkbTBCbENNcVJzeEtLQ0NDQWo4TVVjIiwidCI6M319LCJlbmMiOiJBMjU2R0NNIn0..zz3fUXsiaME2cSoy.LTQovHUvDP4MXT2_sHgf_cM2gicobD5kGXEl5eY.MK3Lf6IwaoVUvCTp1Q5VOA"
+
+	decrypt := func(customField bool) {
+		t.Helper()
+		m := jwe.NewMessage()
+		plain, err := jwe.Decrypt([]byte(data), jwa.DIRECT, key, jwe.WithMessage(m))
+		if !assert.NoError(t, err, `jwe.Decrypt should succeed`) {
+			return
+		}
+		if string(plain) != "testing Shamir Secret Sharing" {
+			t.Errorf("expected 'testing Shamir Secret Sharing', got %s", string(plain))
+			return
+		}
+
+		if customField {
+			v, ok := m.ProtectedHeaders().Get("clevis")
+			if !assert.True(t, ok, `m.Get("clevis") should be true`) {
+				return
+			}
+
+			if !assert.IsType(t, Pin{}, v, `result of m.Get("clevis") should be an instance of Pin{}`) {
+				return
+			}
+		}
+	}
+	decrypt(false)
+
+	// register field deserialized and run decryption again
+	jwe.RegisterCustomField("clevis", Pin{})
+	decrypt(true) // used to fail before, but this should pass
+}