Skip to content

fix(deps): update module github.com/go-jose/go-jose/v3 to v4#3093

Open
renovate[bot] wants to merge 1 commit into
masterfrom
renovate/github-com-go-jose-go-jose-v3-4-x
Open

fix(deps): update module github.com/go-jose/go-jose/v3 to v4#3093
renovate[bot] wants to merge 1 commit into
masterfrom
renovate/github-com-go-jose-go-jose-v3-4-x

Conversation

@renovate

@renovate renovate Bot commented May 5, 2026

Copy link
Copy Markdown
Contributor

This PR contains the following updates:

Package Change Age Confidence
github.com/go-jose/go-jose/v3 v3.0.5v4.1.4 age confidence

Release Notes

go-jose/go-jose (github.com/go-jose/go-jose/v3)

v4.1.4

Compare Source

What's Changed

Fixes Panic in JWE decryption. See GHSA-78h2-9frx-2jm8

Full Changelog: go-jose/go-jose@v4.1.3...v4.1.4

v4.1.3

Compare Source

This release drops Go 1.23 support as that Go release is no longer supported. With that, we can drop x/crypto and no longer have any external dependencies in go-jose outside of the standard library!

This release fixes a bug where a critical b64 header was ignored if in an unprotected header. It is now rejected instead of ignored.

What's Changed

Full Changelog: go-jose/go-jose@v4.1.2...v4.1.3

v4.1.2

Compare Source

What's Changed

go-jose v4.1.2 improves some documentation, errors, and removes the only 3rd-party dependency.

New Contributors

Full Changelog: go-jose/go-jose@v4.1.1...v4.1.2

v4.1.1

Compare Source

What's Changed

New Contributors

Full Changelog: go-jose/go-jose@v4.1.0...v4.1.1

v4.1.0

Compare Source

What's Changed

New Contributors

Full Changelog: go-jose/go-jose@v4.0.5...v4.1.0

v4.0.5

Compare Source

What's Changed

Fixes GHSA-c6gw-w398-hv78

Various other dependency updates, small fixes, and documentation updates in the full changelog

New Contributors

Full Changelog: go-jose/go-jose@v4.0.4...v4.0.5

v4.0.4: Version 4.0.4

Compare Source

Fixed

  • Reverted "Allow unmarshalling JSONWebKeySets with unsupported key types" as a breaking change. See #​136 / #​137.

v4.0.3: Version 4.0.3

Compare Source

Changed

  • Allow unmarshalling JSONWebKeySets with unsupported key types (#​130)
  • Document that OpaqueKeyEncrypter can't be implemented (for now) (#​129)
  • Dependency updates

v4.0.2: Version 4.0.2

Compare Source

What's Changed

New Contributors

Full Changelog: go-jose/go-jose@v4.0.1...v4.0.2

v4.0.1: Version 4.0.1

Compare Source

Fixed

  • An attacker could send a JWE containing compressed data that used large
    amounts of memory and CPU when decompressed by Decrypt or DecryptMulti.
    Those functions now return an error if the decompressed data would exceed
    250kB or 10x the compressed size (whichever is larger). Thanks to
    Enze Wang@​Alioth and Jianjun Chen@​Zhongguancun Lab (@​zer0yu and @​chenjj)
    for reporting.

v4.0.0: Version 4.0.0

Compare Source

This release makes some breaking changes in order to more thoroughly address the vulnerabilities discussed in Three New Attacks Against JSON Web Tokens, "Sign/encrypt confusion", "Billion hash attack", and "Polyglot token".

Changed

  • Limit JWT encryption types (exclude password or public key types) (#​78)
  • Enforce minimum length for HMAC keys (#​85)
  • jwt: match any audience in a list, rather than requiring all audiences (#​81)
  • jwt: accept only Compact Serialization (#​75)
  • jws: Add expected algorithms for signatures (#​74)
  • Require specifying expected algorithms for ParseEncrypted,
    ParseSigned, ParseDetached, jwt.ParseEncrypted, jwt.ParseSigned,
    jwt.ParseSignedAndEncrypted (#​69, #​74)
    • Usually there is a small, known set of appropriate algorithms for a program to use and it's a mistake to allow unexpected algorithms. For instance the "billion hash attack" relies in part on programs accepting the PBES2 encryption algorithm and doing the necessary work even if they weren't specifically configured to allow PBES2.
  • Revert "Strip padding off base64 strings" (#​82)
  • The specs require base64url encoding without padding.
  • Minimum supported Go version is now 1.21

Added

  • ParseSignedCompact, ParseSignedJSON, ParseEncryptedCompact, ParseEncryptedJSON.
    • These allow parsing a specific serialization, as opposed to ParseSigned and ParseEncrypted, which try to automatically detect which serialization was provided. It's common to require a specific serialization for a specific protocol - for instance JWT requires Compact serialization.

Configuration

📅 Schedule: (UTC)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

Rebasing: Whenever PR is behind base branch, or you tick the rebase/retry checkbox.

🔕 Ignore: Close this PR and you won't be reminded about this update again.


  • If you want to rebase/retry this PR, check this box

This PR was generated by Mend Renovate. View the repository job log.

@renovate renovate Bot requested a review from clD11 as a code owner May 5, 2026 12:56
@renovate renovate Bot force-pushed the renovate/github-com-go-jose-go-jose-v3-4-x branch 15 times, most recently from 2ba9d02 to 93218fb Compare May 6, 2026 13:41
@renovate renovate Bot force-pushed the renovate/github-com-go-jose-go-jose-v3-4-x branch 2 times, most recently from d06ebcc to 752ee56 Compare May 19, 2026 10:21
@renovate renovate Bot force-pushed the renovate/github-com-go-jose-go-jose-v3-4-x branch from 752ee56 to d19b64b Compare June 1, 2026 19:34
@renovate renovate Bot force-pushed the renovate/github-com-go-jose-go-jose-v3-4-x branch from d19b64b to de6ef9e Compare June 10, 2026 13:31
@github-actions

Copy link
Copy Markdown

[puLL-Merge] - go-jose/go-jose@v3.0.5..v4.1.4

Diff
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..c507d44
--- /dev/null
+++ .github/dependabot.yml
@@ -0,0 +1,10 @@
+version: 2
+updates:
+  - package-ecosystem: "gomod"
+    directory: "/"
+    schedule:
+      interval: "weekly"
+  - package-ecosystem: "github-actions"
+    directory: "/"
+    schedule:
+      interval: "weekly"
diff --git a/.github/workflows/cram.yml b/.github/workflows/cram.yml
new file mode 100644
index 0000000..f093bc8
--- /dev/null
+++ .github/workflows/cram.yml
@@ -0,0 +1,26 @@
+name: Cram testing jose-util
+on:
+  push:
+    branches: [ '**' ]
+  pull_request:
+    branches: [ '**' ]
+
+jobs:
+  cram:
+    runs-on: ubuntu-latest
+    steps:
+      - uses: actions/checkout@v6
+        with:
+          persist-credentials: false
+
+      - uses: actions/setup-python@v6
+
+      - run: pip install cram
+
+      - uses: actions/setup-go@v6
+
+      - run: go build .
+        working-directory: jose-util
+
+      - run: PATH=$PWD:$PATH cram -v jose-util.t
+        working-directory: jose-util
diff --git .github/workflows/go.yml .github/workflows/go.yml
index 41225ff..69b6d50 100644
--- .github/workflows/go.yml
+++ .github/workflows/go.yml
@@ -2,9 +2,9 @@ name: Go
 
 on:
   push:
-    branches: [ v3 ]
+    branches: [ '**' ]
   pull_request:
-    branches: [ v3 ]
+    branches: [ '**' ]
 
 jobs:
   build:
@@ -12,16 +12,16 @@ jobs:
       fail-fast: false
       matrix:
         GO_VERSION:
-          - "1.16.x"
-          - "1.21.4"
+          - "1.24.x"
+          - "1.25.x"
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/checkout@v3
+    - uses: actions/checkout@v6
       with:
         persist-credentials: false
 
     - name: Set up Go ${{ matrix.GO_VERSION }}
-      uses: actions/setup-go@v4
+      uses: actions/setup-go@v6
       with:
         go-version: "${{ matrix.GO_VERSION }}"
 
@@ -35,18 +35,20 @@ jobs:
     strategy:
       matrix:
         GO_VERSION:
-          # go1.16 does not meet the minimum requirements to run govulncheck so
-          # we will skip testing against it.
-          - "1.21.4"
+          # We only need to test vulns on one version
+          - "1.25.x"
     runs-on: ubuntu-latest
     steps:
-      - uses: actions/checkout@v3
+      - uses: actions/checkout@v6
         with:
           persist-credentials: false
 
       - name: Set up Go ${{ matrix.GO_VERSION }}
-        uses: actions/setup-go@v4
+        uses: actions/setup-go@v6
         with:
+          # When Go produces a security release, we want govulncheck to run
+          # against the most recently released Go version.
+          check-latest: true
           go-version: "${{ matrix.GO_VERSION }}"
 
       - name: Run govulncheck
diff --git CHANGELOG.md CHANGELOG.md
deleted file mode 100644
index 7820c2f..0000000
--- CHANGELOG.md
+++ /dev/null
@@ -1,8 +0,0 @@
-# v3.0.1
-
-Fixed:
- - Security issue: an attacker specifying a large "p2c" value can cause
-   JSONWebEncryption.Decrypt and JSONWebEncryption.DecryptMulti to consume large
-   amounts of CPU, causing a DoS. Thanks to Matt Schwager (@mschwager) for the
-   disclosure and to Tom Tervoort for originally publishing the category of attack.
-   https://i.blackhat.com/BH-US-23/Presentations/US-23-Tervoort-Three-New-Attacks-Against-JSON-Web-Tokens.pdf
diff --git CONTRIBUTING.md CONTRIBUTING.md
index b63e1f8..4b4805a 100644
--- CONTRIBUTING.md
+++ CONTRIBUTING.md
@@ -7,9 +7,3 @@ When submitting code, please make every effort to follow existing conventions
 and style in order to keep the code as readable as possible. Please also make
 sure all tests pass by running `go test`, and format your code with `go fmt`.
 We also recommend using `golint` and `errcheck`.
-
-Before your code can be accepted into the project you must also sign the
-Individual Contributor License Agreement.  We use [cla-assistant.io][1] and you
-will be prompted to sign once a pull request is opened.
-
-[1]: https://cla-assistant.io/
diff --git README.md README.md
index 57da657..55c5509 100644
--- README.md
+++ README.md
@@ -1,26 +1,13 @@
 # Go JOSE
 
-[![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v3.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v3)
-[![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v3/jwt.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v3/jwt)
+[![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4)
+[![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4/jwt.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4/jwt)
 [![license](https://img.shields.io/badge/license-apache_2.0-blue.svg?style=flat)](https://raw.githubusercontent.com/go-jose/go-jose/master/LICENSE)
-[![test](https://img.shields.io/github/checks-status/go-jose/go-jose/v3)](https://github.com/go-jose/go-jose/actions)
 
 Package jose aims to provide an implementation of the Javascript Object Signing
 and Encryption set of standards. This includes support for JSON Web Encryption,
 JSON Web Signature, and JSON Web Token standards.
 
-**Help Wanted!** If you'd like to help us develop this library please reach
-out to css (at) css.bio. While I'm still working on keeping this maintained,
-I have limited time for in-depth development and could use some additional help.
-
-**Disclaimer**: This library contains encryption software that is subject to
-the U.S. Export Administration Regulations. You may not export, re-export,
-transfer or download this code or any part of it in violation of any United
-States law, directive or regulation. In particular this software may not be
-exported or re-exported in any form or on any media to Iran, North Sudan,
-Syria, Cuba, or North Korea, or to denied persons or entities mentioned on any
-US maintained blocked list.
-
 ## Overview
 
 The implementation follows the
@@ -41,14 +28,20 @@ libraries in other languages.
 
 ### Versions
 
-[Version 3](https://github.com/go-jose/go-jose)
-([branch](https://github.com/go-jose/go-jose/tree/v3),
-[doc](https://pkg.go.dev/github.com/go-jose/go-jose/v3), [releases](https://github.com/go-jose/go-jose/releases)) is the current stable version:
+The forthcoming Version 5 will be released with several breaking API changes,
+and will require Golang's `encoding/json/v2`, which is currently requires 
+Go 1.25 built with GOEXPERIMENT=jsonv2.
+
+Version 4 is the current stable version:
+
+    import "github.com/go-jose/go-jose/v4"
+
+It supports at least the current and previous Golang release. Currently it
+requires Golang 1.24.
 
-    import "github.com/go-jose/go-jose/v3"
+Version 3 is only receiving critical security updates. Migration to Version 4 is recommended.
 
-The old [square/go-jose](https://github.com/square/go-jose) repo contains the prior v1 and v2 versions, which
-are still useable but not actively developed anymore. 
+Versions 1 and 2 are obsolete, but can be found in the old repository, [square/go-jose](https://github.com/square/go-jose).
 
 ### Supported algorithms
 
@@ -56,36 +49,36 @@ See below for a table of supported algorithms. Algorithm identifiers match
 the names in the [JSON Web Algorithms](https://dx.doi.org/10.17487/RFC7518)
 standard where possible. The Godoc reference has a list of constants.
 
- Key encryption             | Algorithm identifier(s)
- :------------------------- | :------------------------------
- RSA-PKCS#1v1.5             | RSA1_5
- RSA-OAEP                   | RSA-OAEP, RSA-OAEP-256
- AES key wrap               | A128KW, A192KW, A256KW
- AES-GCM key wrap           | A128GCMKW, A192GCMKW, A256GCMKW
- ECDH-ES + AES key wrap     | ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW
- ECDH-ES (direct)           | ECDH-ES<sup>1</sup>
- Direct encryption          | dir<sup>1</sup>
+| Key encryption         | Algorithm identifier(s)                        |
+|:-----------------------|:-----------------------------------------------|
+| RSA-PKCS#1v1.5         | RSA1_5                                         |
+| RSA-OAEP               | RSA-OAEP, RSA-OAEP-256                         |
+| AES key wrap           | A128KW, A192KW, A256KW                         |
+| AES-GCM key wrap       | A128GCMKW, A192GCMKW, A256GCMKW                |
+| ECDH-ES + AES key wrap | ECDH-ES+A128KW, ECDH-ES+A192KW, ECDH-ES+A256KW |
+| ECDH-ES (direct)       | ECDH-ES<sup>1</sup>                            |
+| Direct encryption      | dir<sup>1</sup>                                |
 
 <sup>1. Not supported in multi-recipient mode</sup>
 
- Signing / MAC              | Algorithm identifier(s)
- :------------------------- | :------------------------------
- RSASSA-PKCS#1v1.5          | RS256, RS384, RS512
- RSASSA-PSS                 | PS256, PS384, PS512
- HMAC                       | HS256, HS384, HS512
- ECDSA                      | ES256, ES384, ES512
- Ed25519                    | EdDSA<sup>2</sup>
+| Signing / MAC     | Algorithm identifier(s) |
+|:------------------|:------------------------|
+| RSASSA-PKCS#1v1.5 | RS256, RS384, RS512     |
+| RSASSA-PSS        | PS256, PS384, PS512     |
+| HMAC              | HS256, HS384, HS512     |
+| ECDSA             | ES256, ES384, ES512     |
+| Ed25519           | EdDSA<sup>2</sup>       |
 
 <sup>2. Only available in version 2 of the package</sup>
 
- Content encryption         | Algorithm identifier(s)
- :------------------------- | :------------------------------
- AES-CBC+HMAC               | A128CBC-HS256, A192CBC-HS384, A256CBC-HS512
- AES-GCM                    | A128GCM, A192GCM, A256GCM
+| Content encryption | Algorithm identifier(s)                     |
+|:-------------------|:--------------------------------------------|
+| AES-CBC+HMAC       | A128CBC-HS256, A192CBC-HS384, A256CBC-HS512 |
+| AES-GCM            | A128GCM, A192GCM, A256GCM                   |
 
- Compression                | Algorithm identifiers(s)
- :------------------------- | -------------------------------
- DEFLATE (RFC 1951)         | DEF
+| Compression        | Algorithm identifiers(s) |
+|:-------------------|--------------------------|
+| DEFLATE (RFC 1951) | DEF                      |
 
 ### Supported key types
 
@@ -94,22 +87,22 @@ library, and can be passed to corresponding functions such as `NewEncrypter` or
 `NewSigner`. Each of these keys can also be wrapped in a JWK if desired, which
 allows attaching a key id.
 
- Algorithm(s)               | Corresponding types
- :------------------------- | -------------------------------
- RSA                        | *[rsa.PublicKey](https://pkg.go.dev/crypto/rsa/#PublicKey), *[rsa.PrivateKey](https://pkg.go.dev/crypto/rsa/#PrivateKey)
- ECDH, ECDSA                | *[ecdsa.PublicKey](https://pkg.go.dev/crypto/ecdsa/#PublicKey), *[ecdsa.PrivateKey](https://pkg.go.dev/crypto/ecdsa/#PrivateKey)
- EdDSA<sup>1</sup>          | [ed25519.PublicKey](https://pkg.go.dev/crypto/ed25519#PublicKey), [ed25519.PrivateKey](https://pkg.go.dev/crypto/ed25519#PrivateKey)
- AES, HMAC                  | []byte
+| Algorithm(s)      | Corresponding types                                                                                                                  |
+|:------------------|--------------------------------------------------------------------------------------------------------------------------------------|
+| RSA               | *[rsa.PublicKey](https://pkg.go.dev/crypto/rsa/#PublicKey), *[rsa.PrivateKey](https://pkg.go.dev/crypto/rsa/#PrivateKey)             |
+| ECDH, ECDSA       | *[ecdsa.PublicKey](https://pkg.go.dev/crypto/ecdsa/#PublicKey), *[ecdsa.PrivateKey](https://pkg.go.dev/crypto/ecdsa/#PrivateKey)     |
+| EdDSA<sup>1</sup> | [ed25519.PublicKey](https://pkg.go.dev/crypto/ed25519#PublicKey), [ed25519.PrivateKey](https://pkg.go.dev/crypto/ed25519#PrivateKey) |
+| AES, HMAC         | []byte                                                                                                                               |
 
 <sup>1. Only available in version 2 or later of the package</sup>
 
 ## Examples
 
-[![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v3.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v3)
-[![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v3/jwt.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v3/jwt)
+[![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4)
+[![godoc](https://pkg.go.dev/badge/github.com/go-jose/go-jose/v4/jwt.svg)](https://pkg.go.dev/github.com/go-jose/go-jose/v4/jwt)
 
 Examples can be found in the Godoc
 reference for this package. The
-[`jose-util`](https://github.com/go-jose/go-jose/tree/v3/jose-util)
+[`jose-util`](https://github.com/go-jose/go-jose/tree/main/jose-util)
 subdirectory also contains a small command-line utility which might be useful
 as an example as well.
diff --git asymmetric.go asymmetric.go
index d4d4961..7784cd4 100644
--- asymmetric.go
+++ asymmetric.go
@@ -29,8 +29,8 @@ import (
 	"fmt"
 	"math/big"
 
-	josecipher "github.com/go-jose/go-jose/v3/cipher"
-	"github.com/go-jose/go-jose/v3/json"
+	josecipher "github.com/go-jose/go-jose/v4/cipher"
+	"github.com/go-jose/go-jose/v4/json"
 )
 
 // A generic RSA-based encrypter/verifier
@@ -414,6 +414,9 @@ func (ctx ecKeyGenerator) genKey() ([]byte, rawHeader, error) {
 
 // Decrypt the given payload and return the content encryption key.
 func (ctx ecDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) {
+	if recipient == nil {
+		return nil, errors.New("go-jose/go-jose: missing recipient")
+	}
 	epk, err := headers.getEPK()
 	if err != nil {
 		return nil, errors.New("go-jose/go-jose: invalid epk header")
@@ -461,13 +464,18 @@ func (ctx ecDecrypterSigner) decryptKey(headers rawHeader, recipient *recipientI
 		return nil, ErrUnsupportedAlgorithm
 	}
 
+	encryptedKey := recipient.encryptedKey
+	if len(encryptedKey) == 0 {
+		return nil, errors.New("go-jose/go-jose: missing JWE Encrypted Key")
+	}
+
 	key := deriveKey(string(algorithm), keySize)
 	block, err := aes.NewCipher(key)
 	if err != nil {
 		return nil, err
 	}
 
-	return josecipher.KeyUnwrap(block, recipient.encryptedKey)
+	return josecipher.KeyUnwrap(block, encryptedKey)
 }
 
 func (ctx edDecrypterSigner) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) {
diff --git asymmetric_test.go asymmetric_test.go
index 1e5b1a3..dc709d8 100644
--- asymmetric_test.go
+++ asymmetric_test.go
@@ -22,6 +22,7 @@ import (
 	"crypto/rand"
 	"crypto/rsa"
 	"errors"
+	"github.com/go-jose/go-jose/v4/json"
 	"io"
 	"testing"
 )
@@ -163,6 +164,10 @@ func TestInvalidECDecrypt(t *testing.T) {
 
 	generator := randomKeyGenerator{size: 16}
 
+	recipient := recipientInfo{
+		// decryptKey will error out before the contents here matter
+		encryptedKey: []byte("not used"),
+	}
 	// Missing epk header
 	headers := rawHeader{}
 
@@ -170,17 +175,24 @@ func TestInvalidECDecrypt(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	if _, err := dec.decryptKey(headers, nil, generator); err == nil {
+	want := "go-jose/go-jose: missing epk header"
+	_, err := dec.decryptKey(headers, &recipient, generator)
+	if err == nil {
 		t.Error("ec decrypter accepted object with missing epk header")
+	} else if err.Error() != want {
+		t.Errorf("decryptKey with missing epk header: got %q, want %q", err, want)
 	}
 
 	// Invalid epk header
-	if err := headers.set(headerEPK, &JSONWebKey{}); err == nil {
-		t.Fatal("epk header should be invalid")
-	}
+	invalid := json.RawMessage("invalid")
+	headers["epk"] = &invalid
 
-	if _, err := dec.decryptKey(headers, nil, generator); err == nil {
+	want = "go-jose/go-jose: invalid epk header"
+	_, err = dec.decryptKey(headers, &recipient, generator)
+	if err == nil {
 		t.Error("ec decrypter accepted object with invalid epk header")
+	} else if err.Error() != want {
+		t.Errorf("decryptKey with invalid epk header: got %q, want %q", err, want)
 	}
 }
 
@@ -367,6 +379,12 @@ func TestInvalidECPublicKey(t *testing.T) {
 		D: fromBase64Int("0_NxaRPUMQoAJt50Gz8YiTr8gRTwyEaCumd-MToTmIo"),
 	}
 
+	recipient := recipientInfo{
+		// encryptedKey must be non-empty to pass initial checks, but the actual
+		// bytes don't matter because we'll error out before using them.
+		encryptedKey: []byte("not used"),
+	}
+
 	headers := rawHeader{}
 
 	if err := headers.set(headerAlgorithm, ECDH_ES); err != nil {
@@ -381,9 +399,15 @@ func TestInvalidECPublicKey(t *testing.T) {
 		privateKey: ecTestKey256,
 	}
 
-	if _, err := dec.decryptKey(headers, nil, randomKeyGenerator{size: 16}); err == nil {
+	_, err := dec.decryptKey(headers, &recipient, randomKeyGenerator{size: 16})
+	if err == nil {
 		t.Fatal("decrypter accepted JWS with invalid ECDH public key")
 	}
+
+	want := "go-jose/go-jose: invalid epk header"
+	if err.Error() != want {
+		t.Errorf("decryptKey with invalid ECDH public key: got %q, want %q", err, want)
+	}
 }
 
 func TestInvalidAlgorithmEC(t *testing.T) {
diff --git cipher/key_wrap.go cipher/key_wrap.go
index b9effbc..a2f86e3 100644
--- cipher/key_wrap.go
+++ cipher/key_wrap.go
@@ -66,12 +66,20 @@ func KeyWrap(block cipher.Block, cek []byte) ([]byte, error) {
 }
 
 // KeyUnwrap implements NIST key unwrapping; it unwraps a content encryption key (cek) with the given block cipher.
+//
+// https://datatracker.ietf.org/doc/html/rfc7518#section-4.4
+// https://datatracker.ietf.org/doc/html/rfc7518#section-4.6
+// https://datatracker.ietf.org/doc/html/rfc7518#section-4.8
 func KeyUnwrap(block cipher.Block, ciphertext []byte) ([]byte, error) {
+	n := (len(ciphertext) / 8) - 1
+	if n <= 0 {
+		return nil, errors.New("go-jose/go-jose: JWE Encrypted Key too short")
+	}
+
 	if len(ciphertext)%8 != 0 {
 		return nil, errors.New("go-jose/go-jose: key wrap input must be 8 byte blocks")
 	}
 
-	n := (len(ciphertext) / 8) - 1
 	r := make([][]byte, n)
 
 	for i := range r {
diff --git cipher/key_wrap_test.go cipher/key_wrap_test.go
index 64ee58d..d65e760 100644
--- cipher/key_wrap_test.go
+++ cipher/key_wrap_test.go
@@ -23,6 +23,47 @@ import (
 	"testing"
 )
 
+func TestKeyUnwrapShort(t *testing.T) {
+	// Test vectors from: http://csrc.nist.gov/groups/ST/toolkit/documents/kms/key-wrap.pdf
+	kek0, _ := hex.DecodeString("000102030405060708090A0B0C0D0E0F")
+	block0, _ := aes.NewCipher(kek0)
+	want := "go-jose/go-jose: JWE Encrypted Key too short"
+	_, err := KeyUnwrap(block0, nil)
+	if err == nil {
+		t.Error("KeyUnwrap(_, nil): got nil, want error")
+	} else if err.Error() != want {
+		t.Errorf("KeyUnwrap(_, nil): got %q, want %q", err, want)
+	}
+
+	input := []byte{}
+	_, err = KeyUnwrap(block0, []byte{})
+	if err == nil {
+		t.Errorf("KeyUnwrap(_, %q): got nil, want error", input)
+	} else if err.Error() != want {
+		t.Errorf("KeyUnwrap(_, %q): got %q, want %q", input, err, want)
+	}
+
+	for n := 0; n < 16; n++ {
+		input := bytes.Repeat([]byte("a"), n)
+		_, err = KeyUnwrap(block0, input)
+		if err == nil {
+			t.Errorf("KeyUnwrap(_, %q): got nil, want error", input)
+		} else if err.Error() != want {
+			t.Errorf("KeyUnwrap(_, nil): got %q, want %q", err, want)
+		}
+	}
+
+	input = bytes.Repeat([]byte("a"), 17)
+	want = "go-jose/go-jose: key wrap input must be 8 byte blocks"
+	_, err = KeyUnwrap(block0, input)
+	if err == nil {
+		t.Errorf("KeyUnwrap(_, %q): got nil, want error", input)
+	} else if err.Error() != want {
+		t.Errorf("KeyUnwrap(_, %q): got %q, want %q", input, err, want)
+	}
+
+}
+
 func TestAesKeyWrap(t *testing.T) {
 	// Test vectors from: http://csrc.nist.gov/groups/ST/toolkit/documents/kms/key-wrap.pdf
 	kek0, _ := hex.DecodeString("000102030405060708090A0B0C0D0E0F")
diff --git crypter.go crypter.go
index 506d3b7..31290fc 100644
--- crypter.go
+++ crypter.go
@@ -22,7 +22,7 @@ import (
 	"errors"
 	"fmt"
 
-	"github.com/go-jose/go-jose/v3/json"
+	"github.com/go-jose/go-jose/v4/json"
 )
 
 // Encrypter represents an encrypter which produces an encrypted JWE object.
@@ -286,6 +286,10 @@ func makeJWERecipient(alg KeyAlgorithm, encryptionKey interface{}) (recipientKey
 		return newSymmetricRecipient(alg, encryptionKey)
 	case string:
 		return newSymmetricRecipient(alg, []byte(encryptionKey))
+	case JSONWebKey:
+		recipient, err := makeJWERecipient(alg, encryptionKey.Key)
+		recipient.keyID = encryptionKey.KeyID
+		return recipient, err
 	case *JSONWebKey:
 		recipient, err := makeJWERecipient(alg, encryptionKey.Key)
 		recipient.keyID = encryptionKey.KeyID
@@ -440,6 +444,9 @@ func (ctx *genericEncrypter) Options() EncrypterOptions {
 //
 // Note that ed25519 is only available for signatures, not encryption, so is
 // not an option here.
+//
+// Automatically decompresses plaintext, but returns an error if the decompressed
+// data would be >250kB or >10x the size of the compressed data, whichever is larger.
 func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error) {
 	headers := obj.mergedHeaders(nil)
 
@@ -447,16 +454,15 @@ func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error)
 		return nil, errors.New("go-jose/go-jose: too many recipients in payload; expecting only one")
 	}
 
-	critical, err := headers.getCritical()
+	err := headers.checkNoCritical()
 	if err != nil {
-		return nil, fmt.Errorf("go-jose/go-jose: invalid crit header")
+		return nil, err
 	}
 
-	if len(critical) > 0 {
-		return nil, fmt.Errorf("go-jose/go-jose: unsupported crit header")
+	key, err := tryJWKS(decryptionKey, obj.Header)
+	if err != nil {
+		return nil, err
 	}
-
-	key := tryJWKS(decryptionKey, obj.Header)
 	decrypter, err := newDecrypter(key)
 	if err != nil {
 		return nil, err
@@ -511,19 +517,21 @@ func (obj JSONWebEncryption) Decrypt(decryptionKey interface{}) ([]byte, error)
 //
 // The decryptionKey argument must have one of the types allowed for the
 // decryptionKey argument of Decrypt().
+//
+// Automatically decompresses plaintext, but returns an error if the decompressed
+// data would be >250kB or >3x the size of the compressed data, whichever is larger.
 func (obj JSONWebEncryption) DecryptMulti(decryptionKey interface{}) (int, Header, []byte, error) {
 	globalHeaders := obj.mergedHeaders(nil)
 
-	critical, err := globalHeaders.getCritical()
+	err := globalHeaders.checkNoCritical()
 	if err != nil {
-		return -1, Header{}, nil, fmt.Errorf("go-jose/go-jose: invalid crit header")
+		return -1, Header{}, nil, err
 	}
 
-	if len(critical) > 0 {
-		return -1, Header{}, nil, fmt.Errorf("go-jose/go-jose: unsupported crit header")
+	key, err := tryJWKS(decryptionKey, obj.Header)
+	if err != nil {
+		return -1, Header{}, nil, err
 	}
-
-	key := tryJWKS(decryptionKey, obj.Header)
 	decrypter, err := newDecrypter(key)
 	if err != nil {
 		return -1, Header{}, nil, err
diff --git crypter_test.go crypter_test.go
index 9f319e7..e315d13 100644
--- crypter_test.go
+++ crypter_test.go
@@ -68,7 +68,7 @@ func TestCompressionError(t *testing.T) {
 		"ciphertext":"luLq8QTsJEXbZdRvEzIiHWEitTZTORZqXIk",
 		"tag":"S1j6wvSGtTUCXhED91lUGQ"
 	}`
-	jwe, err := ParseEncrypted(jweBytes)
+	jwe, err := ParseEncrypted(jweBytes, []KeyAlgorithm{A128KW}, []ContentEncryption{A128GCM})
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -113,7 +113,7 @@ func RoundtripJWE(keyAlg KeyAlgorithm, encAlg ContentEncryption, compressionAlg
 		return fmt.Errorf("error in serializer: %s", err)
 	}
 
-	parsed, err := ParseEncrypted(msg)
+	parsed, err := ParseEncrypted(msg, []KeyAlgorithm{keyAlg}, []ContentEncryption{encAlg})
 	if err != nil {
 		return fmt.Errorf("error in parse: %s, on msg '%s'", err, msg)
 	}
@@ -191,6 +191,8 @@ func TestRoundtripsJWECorrupted(t *testing.T) {
 		func(obj *JSONWebEncryption) (string, error) { return obj.FullSerialize(), nil },
 	}
 
+	// corrupter functions return true to skip (i.e. no change was made so no error is expected),
+	// or false to do the test and expect an error.
 	bitflip := func(slice []byte) bool {
 		if len(slice) > 0 {
 			slice[0] ^= 0xFF
@@ -199,6 +201,22 @@ func TestRoundtripsJWECorrupted(t *testing.T) {
 		return true
 	}
 
+	shorten := func(slice *[]byte) bool {
+		if len(*slice) > 0 {
+			*slice = (*slice)[:len(*slice)-1]
+			return false
+		}
+		return true
+	}
+
+	empty := func(slice *[]byte) bool {
+		if len(*slice) > 0 {
+			*slice = nil
+			return false
+		}
+		return true
+	}
+
 	corrupters := []func(*JSONWebEncryption) bool{
 		func(obj *JSONWebEncryption) bool {
 			// Set invalid ciphertext
@@ -216,6 +234,14 @@ func TestRoundtripsJWECorrupted(t *testing.T) {
 			// Mess with encrypted key
 			return bitflip(obj.recipients[0].encryptedKey)
 		},
+		func(obj *JSONWebEncryption) bool {
+			// Remove encrypted key
+			return empty(&obj.recipients[0].encryptedKey)
+		},
+		func(obj *JSONWebEncryption) bool {
+			// Shorten encrypted key
+			return shorten(&obj.recipients[0].encryptedKey)
+		},
 		func(obj *JSONWebEncryption) bool {
 			// Mess with GCM-KW auth tag
 			tag, _ := obj.protected.getTag()
@@ -255,7 +281,7 @@ func TestRoundtripsJWECorrupted(t *testing.T) {
 	}
 }
 
-func TestEncrypterWithJWKAndKeyID(t *testing.T) {
+func TestEncrypterWithJWKAndKeyIDByReference(t *testing.T) {
 	enc, err := NewEncrypter(A128GCM, Recipient{Algorithm: A128KW, Key: &JSONWebKey{
 		KeyID: "test-id",
 		Key:   []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
@@ -269,8 +295,8 @@ func TestEncrypterWithJWKAndKeyID(t *testing.T) {
 	serialized1, _ := ciphertext.CompactSerialize()
 	serialized2 := ciphertext.FullSerialize()
 
-	parsed1, _ := ParseEncrypted(serialized1)
-	parsed2, _ := ParseEncrypted(serialized2)
+	parsed1, _ := ParseEncrypted(serialized1, []KeyAlgorithm{A128KW}, []ContentEncryption{A128GCM})
+	parsed2, _ := ParseEncrypted(serialized2, []KeyAlgorithm{A128KW}, []ContentEncryption{A128GCM})
 
 	if parsed1.Header.KeyID != "test-id" {
 		t.Errorf("expected message to have key id from JWK, but found '%s' instead", parsed1.Header.KeyID)
@@ -280,6 +306,31 @@ func TestEncrypterWithJWKAndKeyID(t *testing.T) {
 	}
 }
 
+func TestEncrypterWithJWKAndKeyIDByValue(t *testing.T) {
+	enc, err := NewEncrypter(A128GCM, Recipient{Algorithm: A128KW, Key: JSONWebKey{
+		KeyID: "test-id-value",
+		Key:   []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
+	}}, nil)
+	if err != nil {
+		t.Error(err)
+	}
+
+	ciphertext, _ := enc.Encrypt([]byte("Lorem ipsum dolor sit amet"))
+
+	serialized1, _ := ciphertext.CompactSerialize()
+	serialized2 := ciphertext.FullSerialize()
+
+	parsed1, _ := ParseEncrypted(serialized1, []KeyAlgorithm{A128KW}, []ContentEncryption{A128GCM})
+	parsed2, _ := ParseEncrypted(serialized2, []KeyAlgorithm{A128KW}, []ContentEncryption{A128GCM})
+
+	if parsed1.Header.KeyID != "test-id-value" {
+		t.Errorf("expected message to have key id from JWK by value, but found '%s' instead", parsed1.Header.KeyID)
+	}
+	if parsed2.Header.KeyID != "test-id-value" {
+		t.Errorf("expected message to have key id from JWK by value, but found '%s' instead", parsed2.Header.KeyID)
+	}
+}
+
 func TestEncrypterWithBrokenRand(t *testing.T) {
 	keyAlgs := []KeyAlgorithm{ECDH_ES_A128KW, A128KW, RSA1_5, RSA_OAEP, RSA_OAEP_256, A128GCMKW, PBES2_HS256_A128KW}
 	encAlgs := []ContentEncryption{A128GCM, A192GCM, A256GCM, A128CBC_HS256, A192CBC_HS384, A256CBC_HS512}
@@ -356,7 +407,7 @@ func TestMultiRecipientJWE(t *testing.T) {
 
 	msg := obj.FullSerialize()
 
-	parsed, err := ParseEncrypted(msg)
+	parsed, err := ParseEncrypted(msg, []KeyAlgorithm{RSA_OAEP, A256GCMKW}, []ContentEncryption{A128GCM})
 	if err != nil {
 		t.Fatal("error in parse: ", err)
 	}
@@ -446,7 +497,7 @@ func TestEncrypterExtraHeaderInclusion(t *testing.T) {
 		t.Fatal("error in encrypt: ", err)
 	}
 
-	parsed, err := ParseEncrypted(obj.FullSerialize())
+	parsed, err := ParseEncrypted(obj.FullSerialize(), []KeyAlgorithm{A256GCMKW}, []ContentEncryption{A256GCM})
 	if err != nil {
 		t.Fatal("error in parse: ", err)
 	}
@@ -578,12 +629,12 @@ func TestPBES2JWKEncryption(t *testing.T) {
 		t.Fatal("error on CompactSerialize")
 	}
 
-	jwe1, err := ParseEncrypted(serialized)
+	jwe1, err := ParseEncrypted(serialized, []KeyAlgorithm{PBES2_HS256_A128KW}, []ContentEncryption{A128CBC_HS256})
 	if err != nil {
 		t.Fatal("error in ParseEncrypted")
 	}
 
-	jwe2, err := ParseEncrypted(serializationReference)
+	jwe2, err := ParseEncrypted(serializationReference, []KeyAlgorithm{PBES2_HS256_A128KW}, []ContentEncryption{A128CBC_HS256})
 	if err != nil {
 		t.Fatal("error in ParseEncrypted")
 	}
@@ -634,8 +685,8 @@ func TestEncrypterWithPBES2(t *testing.T) {
 			serialized1, _ := ciphertext.CompactSerialize()
 			serialized2 := ciphertext.FullSerialize()
 
-			parsed1, _ := ParseEncrypted(serialized1)
-			parsed2, _ := ParseEncrypted(serialized2)
+			parsed1, _ := ParseEncrypted(serialized1, []KeyAlgorithm{alg}, []ContentEncryption{A128GCM})
+			parsed2, _ := ParseEncrypted(serialized2, []KeyAlgorithm{alg}, []ContentEncryption{A128GCM})
 
 			actual1, err := parsed1.Decrypt("password")
 			if err != nil {
@@ -681,8 +732,8 @@ func TestRejectTooHighP2C(t *testing.T) {
 			serialized1, _ := ciphertext.CompactSerialize()
 			serialized2 := ciphertext.FullSerialize()
 
-			parsed1, _ := ParseEncrypted(serialized1)
-			parsed2, _ := ParseEncrypted(serialized2)
+			parsed1, _ := ParseEncrypted(serialized1, []KeyAlgorithm{alg}, []ContentEncryption{A128GCM})
+			parsed2, _ := ParseEncrypted(serialized2, []KeyAlgorithm{alg}, []ContentEncryption{A128GCM})
 
 			_, err = parsed1.Decrypt("password")
 			if err == nil {
diff --git cryptosigner/cryptosigner.go cryptosigner/cryptosigner.go
index ddad5c9..4aba524 100644
--- cryptosigner/cryptosigner.go
+++ cryptosigner/cryptosigner.go
@@ -30,7 +30,7 @@ import (
 	"io"
 	"math/big"
 
-	"github.com/go-jose/go-jose/v3"
+	"github.com/go-jose/go-jose/v4"
 )
 
 // Opaque creates an OpaqueSigner from a "crypto".Signer
diff --git cryptosigner/cryptosigner_test.go cryptosigner/cryptosigner_test.go
index 28397f2..42c38d1 100644
--- cryptosigner/cryptosigner_test.go
+++ cryptosigner/cryptosigner_test.go
@@ -30,7 +30,7 @@ import (
 	"reflect"
 	"testing"
 
-	"github.com/go-jose/go-jose/v3"
+	"github.com/go-jose/go-jose/v4"
 )
 
 func TestRoundtripsJWSCryptoSigner(t *testing.T) {
@@ -81,7 +81,7 @@ func roundtripJWS(sigAlg jose.SignatureAlgorithm, serializer func(*jose.JSONWebS
 		return fmt.Errorf("error on serialize: %s", err)
 	}
 
-	obj, err = jose.ParseSigned(msg)
+	obj, err = jose.ParseSigned(msg, []jose.SignatureAlgorithm{sigAlg})
 	if err != nil {
 		return fmt.Errorf("error on parse: %s", err)
 	}
diff --git doc_test.go doc_test.go
index 315317a..b7f0102 100644
--- doc_test.go
+++ doc_test.go
@@ -57,7 +57,7 @@ func Example_jWE() {
 
 	// Parse the serialized, encrypted JWE object. An error would indicate that
 	// the given input did not represent a valid message.
-	object, err = ParseEncrypted(serialized)
+	object, err = ParseEncrypted(serialized, []KeyAlgorithm{RSA_OAEP}, []ContentEncryption{A128GCM})
 	if err != nil {
 		panic(err)
 	}
@@ -103,7 +103,7 @@ func Example_jWS() {
 
 	// Parse the serialized, protected JWS object. An error would indicate that
 	// the given input did not represent a valid message.
-	object, err = ParseSigned(serialized)
+	object, err = ParseSigned(serialized, []SignatureAlgorithm{PS512})
 	if err != nil {
 		panic(err)
 	}
diff --git encoding.go encoding.go
index 62f8b8a..4f6e0d4 100644
--- encoding.go
+++ encoding.go
@@ -21,12 +21,13 @@ import (
 	"compress/flate"
 	"encoding/base64"
 	"encoding/binary"
+	"fmt"
 	"io"
 	"math/big"
 	"strings"
 	"unicode"
 
-	"github.com/go-jose/go-jose/v3/json"
+	"github.com/go-jose/go-jose/v4/json"
 )
 
 // Helper function to serialize known-good objects.
@@ -85,7 +86,7 @@ func decompress(algorithm CompressionAlgorithm, input []byte) ([]byte, error) {
 	}
 }
 
-// Compress with DEFLATE
+// deflate compresses the input.
 func deflate(input []byte) ([]byte, error) {
 	output := new(bytes.Buffer)
 
@@ -97,15 +98,24 @@ func deflate(input []byte) ([]byte, error) {
 	return output.Bytes(), err
 }
 
-// Decompress with DEFLATE
+// inflate decompresses the input.
+//
+// Errors if the decompressed data would be >250kB or >10x the size of the
+// compressed data, whichever is larger.
 func inflate(input []byte) ([]byte, error) {
 	output := new(bytes.Buffer)
 	reader := flate.NewReader(bytes.NewBuffer(input))
 
-	_, err := io.Copy(output, reader)
-	if err != nil {
+	maxCompressedSize := max(250_000, 10*int64(len(input)))
+
+	limit := maxCompressedSize + 1
+	n, err := io.CopyN(output, reader, limit)
+	if err != nil && err != io.EOF {
 		return nil, err
 	}
+	if n == limit {
+		return nil, fmt.Errorf("uncompressed data would be too large (>%d bytes)", maxCompressedSize)
+	}
 
 	err = reader.Close()
 	return output.Bytes(), err
@@ -154,7 +164,7 @@ func (b *byteBuffer) UnmarshalJSON(data []byte) error {
 		return nil
 	}
 
-	decoded, err := base64URLDecode(encoded)
+	decoded, err := base64.RawURLEncoding.DecodeString(encoded)
 	if err != nil {
 		return err
 	}
@@ -184,12 +194,6 @@ func (b byteBuffer) toInt() int {
 	return int(b.bigInt().Int64())
 }
 
-// base64URLDecode is implemented as defined in https://www.rfc-editor.org/rfc/rfc7515.html#appendix-C
-func base64URLDecode(value string) ([]byte, error) {
-	value = strings.TrimRight(value, "=")
-	return base64.RawURLEncoding.DecodeString(value)
-}
-
 func base64EncodeLen(sl []byte) int {
 	return base64.RawURLEncoding.EncodedLen(len(sl))
 }
diff --git encoding_test.go encoding_test.go
index fc48685..4b1d451 100644
--- encoding_test.go
+++ encoding_test.go
@@ -18,6 +18,8 @@ package jose
 
 import (
 	"bytes"
+	"crypto/rand"
+	"io"
 	"strings"
 	"testing"
 )
@@ -57,6 +59,38 @@ func TestInvalidCompression(t *testing.T) {
 	}
 }
 
+// TestLargeZip tests that we can decompress a large input, so long as its
+// compression ratio is reasonable.
+func TestLargeZip(t *testing.T) {
+	input := new(bytes.Buffer)
+	_, err := io.CopyN(input, rand.Reader, 251_000)
+	if err != nil {
+		t.Fatalf("generating input: %s", err)
+	}
+	compressed, err := compress(DEFLATE, input.Bytes())
+	if err != nil {
+		t.Errorf("compressing: %s", err)
+	}
+	t.Logf("compression ratio: %g", float64(len(input.Bytes()))/float64(len(compressed)))
+	_, err = decompress(DEFLATE, compressed)
+	if err != nil {
+		t.Errorf("decompressing large input with low compression ratio: %s", err)
+	}
+}
+
+func TestZipBomb(t *testing.T) {
+	input := strings.Repeat("a", 251_000)
+	compressed, err := compress(DEFLATE, []byte(input))
+	if err != nil {
+		t.Errorf("compressing: %s", err)
+	}
+	t.Logf("compression ratio: %d %g", len(compressed), float64(len(input))/float64(len(compressed)))
+	out, err := decompress(DEFLATE, compressed)
+	if err == nil {
+		t.Errorf("expected error decompressing zip bomb, got none. output size %d", len(out))
+	}
+}
+
 func TestByteBufferTrim(t *testing.T) {
 	buf := newBufferFromInt(1)
 	if !bytes.Equal(buf.data, []byte{1}) {
diff --git go.mod go.mod
index 075426d..6beaf0c 100644
--- go.mod
+++ go.mod
@@ -1,9 +1,3 @@
-module github.com/go-jose/go-jose/v3
+module github.com/go-jose/go-jose/v4
 
-go 1.12
-
-require (
-	github.com/google/go-cmp v0.5.9
-	github.com/stretchr/testify v1.7.0
-	golang.org/x/crypto v0.6.0
-)
+go 1.24.0
diff --git go.sum go.sum
index 30296e5..e69de29 100644
--- go.sum
+++ go.sum
@@ -1,42 +0,0 @@
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
-golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git jose-util/README.md jose-util/README.md
index 76d12f4..c321ad0 100644
--- jose-util/README.md
+++ jose-util/README.md
@@ -7,8 +7,7 @@ with JOSE messages when testing or debugging.
 ## Installation
 
 ```
-$ go get -u github.com/go-jose/go-jose/jose-util
-$ go install github.com/go-jose/go-jose/jose-util
+$ go install github.com/go-jose/go-jose/v4/jose-util@latest
 ```
 
 ## Usage
@@ -112,12 +111,3 @@ Expands a compact message to the JWE/JWS JSON Serialization format.
 
     jose-util expand --format JWE   # Expands a compact JWE to JWE JSON Serialization
     jose-util expand --format JWS   # Expands a compact JWS to JWS JSON Serialization
-
-### Decode base64
-
-The JOSE format uses url-safe base64 in payloads, but the `base64` utility that ships with
-most Linux distributions (or macOS) only supports the standard base64 encoding. To make it easier
-to deal with these payloads a `b64decode` command is available in `jose-util` that can decode
-both regular and url-safe base64 data.
-
-    echo "8J-Ukgo" | jose-util b64decode
diff --git jose-util/crypto.go jose-util/crypto.go
index 5710dc3..0e39175 100644
--- jose-util/crypto.go
+++ jose-util/crypto.go
@@ -17,78 +17,216 @@
 package main
 
 import (
-	"github.com/go-jose/go-jose/jose-util/generator"
-	jose "github.com/go-jose/go-jose/v3"
+	"flag"
+	"fmt"
+
+	"github.com/go-jose/go-jose/v4"
+	"github.com/go-jose/go-jose/v4/jose-util/generator"
 )
 
-func encrypt() {
-	pub, err := generator.LoadPublicKey(keyBytes())
-	app.FatalIfError(err, "unable to read public key")
+var allKeyAlgorithms = []jose.KeyAlgorithm{
+	jose.ED25519,
+	jose.RSA1_5,
+	jose.RSA_OAEP,
+	jose.RSA_OAEP_256,
+	jose.A128KW,
+	jose.A192KW,
+	jose.A256KW,
+	jose.DIRECT,
+	jose.ECDH_ES,
+	jose.ECDH_ES_A128KW,
+	jose.ECDH_ES_A192KW,
+	jose.ECDH_ES_A256KW,
+	jose.A128GCMKW,
+	jose.A192GCMKW,
+	jose.A256GCMKW,
+	jose.PBES2_HS256_A128KW,
+	jose.PBES2_HS384_A192KW,
+	jose.PBES2_HS512_A256KW,
+}
+
+var allSignatureAlgorithms = []jose.SignatureAlgorithm{
+	jose.EdDSA,
+	jose.HS256,
+	jose.HS384,
+	jose.HS512,
+	jose.RS256,
+	jose.RS384,
+	jose.RS512,
+	jose.ES256,
+	jose.ES384,
+	jose.ES512,
+	jose.PS256,
+	jose.PS384,
+	jose.PS512,
+}
+
+var allContentEncryption = []jose.ContentEncryption{
+	jose.A128CBC_HS256,
+	jose.A192CBC_HS384,
+	jose.A256CBC_HS512,
+	jose.A128GCM,
+	jose.A192GCM,
+	jose.A256GCM,
+}
+
+func encrypt(args []string) error {
+	fs := flag.NewFlagSet("encrypt", flag.ExitOnError)
+	encryptAlgFlag := fs.String("alg", "", "Key management algorithm (e.g. RSA-OAEP)")
+	encryptEncFlag := fs.String("enc", "", "Content encryption algorithm (e.g. A128GCM)")
+	encryptFullFlag := fs.Bool("full", false, "Use JSON Serialization format (instead of compact)")
+	registerCommon(fs)
+	err := fs.Parse(args)
+
+	bytes, err := keyBytes()
+	if err != nil {
+		return err
+	}
+
+	pub, err := generator.LoadPublicKey(bytes)
+	if err != nil {
+		return fmt.Errorf("unable to read public key: %w", err)
+	}
 
 	alg := jose.KeyAlgorithm(*encryptAlgFlag)
 	enc := jose.ContentEncryption(*encryptEncFlag)
 
 	crypter, err := jose.NewEncrypter(enc, jose.Recipient{Algorithm: alg, Key: pub}, nil)
-	app.FatalIfError(err, "unable to instantiate encrypter")
+	if err != nil {
+		return fmt.Errorf("unable to instantiate encrypter: %w", err)
+	}
 
-	obj, err := crypter.Encrypt(readInput(*inFile))
-	app.FatalIfError(err, "unable to encrypt")
+	input, err := readInput(*inFile)
+	if err != nil {
+		return err
+	}
+
+	obj, err := crypter.Encrypt(input)
+	if err != nil {
+		return fmt.Errorf("unable to encrypt: %w", err)
+	}
 
 	var msg string
 	if *encryptFullFlag {
 		msg = obj.FullSerialize()
 	} else {
 		msg, err = obj.CompactSerialize()
-		app.FatalIfError(err, "unable to serialize message")
+		if err != nil {
+			return fmt.Errorf("unable to serialzie message: %w", err)
+		}
 	}
 
-	writeOutput(*outFile, []byte(msg))
+	return writeOutput(*outFile, []byte(msg))
 }
 
-func decrypt() {
-	priv, err := generator.LoadPrivateKey(keyBytes())
-	app.FatalIfError(err, "unable to read private key")
+func decrypt(args []string) error {
+	fs := flag.NewFlagSet("decrypt", flag.ExitOnError)
+	registerCommon(fs)
+	fs.Parse(args)
+
+	bytes, err := keyBytes()
+	if err != nil {
+		return err
+	}
+
+	priv, err := generator.LoadPrivateKey(bytes)
+	if err != nil {
+		return fmt.Errorf("unable to read private key %s: %w", priv, err)
+	}
+
+	input, err := readInput(*inFile)
+	if err != nil {
+		return err
+	}
 
-	obj, err := jose.ParseEncrypted(string(readInput(*inFile)))
-	app.FatalIfError(err, "unable to parse message")
+	obj, err := jose.ParseEncrypted(string(input), allKeyAlgorithms, allContentEncryption)
+	if err != nil {
+		return fmt.Errorf("unable to parse message: %w", err)
+	}
 
 	plaintext, err := obj.Decrypt(priv)
-	app.FatalIfError(err, "unable to decrypt message")
+	if err != nil {
+		return fmt.Errorf("unable to decrypt message: %w", err)
+	}
 
-	writeOutput(*outFile, plaintext)
+	return writeOutput(*outFile, plaintext)
 }
 
-func sign() {
-	signingKey, err := generator.LoadPrivateKey(keyBytes())
-	app.FatalIfError(err, "unable to read private key")
+func sign(args []string) error {
+	fs := flag.NewFlagSet("encrypt", flag.ExitOnError)
+	signAlgFlag := fs.String("alg", "", "Key management algorithm (e.g. RSA-OAEP)")
+	signFullFlag := fs.Bool("full", false, "Use JSON Serialization format (instead of compact)")
+	registerCommon(fs)
+	fs.Parse(args)
+
+	bytes, err := keyBytes()
+	if err != nil {
+		return err
+	}
+
+	signingKey, err := generator.LoadPrivateKey(bytes)
+	if err != nil {
+		return fmt.Errorf("unable to read private key: %w", err)
+	}
 
 	alg := jose.SignatureAlgorithm(*signAlgFlag)
 	signer, err := jose.NewSigner(jose.SigningKey{Algorithm: alg, Key: signingKey}, nil)
-	app.FatalIfError(err, "unable to make signer")
+	if err != nil {
+		return fmt.Errorf("unable to make signer: %w", err)
+	}
 
-	obj, err := signer.Sign(readInput(*inFile))
-	app.FatalIfError(err, "unable to sign")
+	input, err := readInput(*inFile)
+	if err != nil {
+		return err
+	}
+
+	obj, err := signer.Sign(input)
+	if err != nil {
+		return fmt.Errorf("unable to sign: %w", err)
+	}
 
 	var msg string
 	if *signFullFlag {
 		msg = obj.FullSerialize()
 	} else {
 		msg, err = obj.CompactSerialize()
-		app.FatalIfError(err, "unable to serialize message")
+		if err != nil {
+			return fmt.Errorf("unable to serialize message: %w", err)
+		}
 	}
 
-	writeOutput(*outFile, []byte(msg))
+	return writeOutput(*outFile, []byte(msg))
 }
 
-func verify() {
-	verificationKey, err := generator.LoadPublicKey(keyBytes())
-	app.FatalIfError(err, "unable to read public key")
+func verify(args []string) error {
+	fs := flag.NewFlagSet("verify", flag.ExitOnError)
+	registerCommon(fs)
+	fs.Parse(args)
 
-	obj, err := jose.ParseSigned(string(readInput(*inFile)))
-	app.FatalIfError(err, "unable to parse message")
+	bytes, err := keyBytes()
+	if err != nil {
+		return err
+	}
+
+	verificationKey, err := generator.LoadPublicKey(bytes)
+	if err != nil {
+		return fmt.Errorf("unable to read public key: %w", err)
+	}
+
+	input, err := readInput(*inFile)
+	if err != nil {
+		return err
+	}
+
+	obj, err := jose.ParseSigned(string(input), allSignatureAlgorithms)
+	if err != nil {
+		return fmt.Errorf("unable to parse message: %w", err)
+	}
 
 	plaintext, err := obj.Verify(verificationKey)
-	app.FatalIfError(err, "invalid signature")
+	if err != nil {
+		return fmt.Errorf("invalid signature: %w", err)
+	}
 
-	writeOutput(*outFile, plaintext)
+	return writeOutput(*outFile, plaintext)
 }
diff --git jose-util/format.go jose-util/format.go
index e6dda25..7c90e71 100644
--- jose-util/format.go
+++ jose-util/format.go
@@ -1,4 +1,4 @@
-/*-
+/*
  * Copyright 2019 Square Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
@@ -16,29 +16,49 @@
 
 package main
 
-import jose "github.com/go-jose/go-jose/v3"
+import (
+	"flag"
+	"fmt"
 
-func expand() {
-	input := string(readInput(*inFile))
+	"github.com/go-jose/go-jose/v4"
+)
+
+func expand(args []string) error {
+	fs := flag.NewFlagSet("expand", flag.ExitOnError)
+	expandFormatFlag := fs.String("format", "", "Type of message to expand (JWS or JWE, defaults to JWE)")
+	registerCommon(fs)
+	fs.Parse(args)
+
+	bytes, err := readInput(*inFile)
+	if err != nil {
+		return err
+	}
+
+	input := string(bytes)
 
 	var serialized string
-	var err error
 	switch *expandFormatFlag {
 	case "", "JWE":
 		var jwe *jose.JSONWebEncryption
-		jwe, err = jose.ParseEncrypted(input)
+		jwe, err = jose.ParseEncrypted(input, allKeyAlgorithms, allContentEncryption)
 		if err == nil {
 			serialized = jwe.FullSerialize()
 		}
 	case "JWS":
 		var jws *jose.JSONWebSignature
-		jws, err = jose.ParseSigned(input)
+		jws, err = jose.ParseSigned(input, allSignatureAlgorithms)
 		if err == nil {
 			serialized = jws.FullSerialize()
 		}
 	}
 
-	app.FatalIfError(err, "unable to expand message")
-	writeOutput(*outFile, []byte(serialized))
-	writeOutput(*outFile, []byte("\n"))
+	if err != nil {
+		return fmt.Errorf("unable to expand message: %w", err)
+	}
+	err = writeOutput(*outFile, []byte(serialized))
+	if err != nil {
+		return err
+	}
+
+	return writeOutput(*outFile, []byte("\n"))
 }
diff --git jose-util/generate.go jose-util/generate.go
index 1f5b6c4..6d23df8 100644
--- jose-util/generate.go
+++ jose-util/generate.go
@@ -19,14 +19,21 @@ package main
 import (
 	"crypto"
 	"encoding/base64"
-	"errors"
+	"flag"
 	"fmt"
 
-	"github.com/go-jose/go-jose/jose-util/generator"
-	"github.com/go-jose/go-jose/v3"
+	"github.com/go-jose/go-jose/v4"
+	"github.com/go-jose/go-jose/v4/jose-util/generator"
 )
 
-func generate() {
+func generate(args []string) error {
+	fs := flag.NewFlagSet("generate", flag.ExitOnError)
+	generateUseFlag := fs.String("use", "", "Desired public key usage (use header), one of [enc sig]")
+	generateAlgFlag := fs.String("alg", "", "Desired key pair algorithm (alg header)")
+	generateKeySizeFlag := fs.Int("size", 0, "Key size in bits (e.g. 2048 if generating an RSA key)")
+	generateKeyIdentFlag := fs.String("kid", "", "Optional Key ID (kid header, generate random kid if not set)")
+	fs.Parse(args)
+
 	var privKey crypto.PrivateKey
 	var pubKey crypto.PublicKey
 	var err error
@@ -40,9 +47,11 @@ func generate() {
 		// According to RFC 7517 section-8.2.  This is unlikely to change in the
 		// near future. If it were, new values could be found in the registry under
 		// "JSON Web Key Use": https://www.iana.org/assignments/jose/jose.xhtml
-		app.FatalIfError(errors.New("invalid key use.  Must be \"sig\" or \"enc\""), "unable to generate key")
+		return fmt.Errorf("invalid key use '%s'.  Must be \"sig\" or \"enc\"", *generateUseFlag)
+	}
+	if err != nil {
+		return fmt.Errorf("unable to generate key: %w", err)
 	}
-	app.FatalIfError(err, "unable to generate key")
 
 	kid := *generateKeyIdentFlag
 
@@ -51,7 +60,9 @@ func generate() {
 	// Generate a canonical kid based on RFC 7638
 	if kid == "" {
 		thumb, err := priv.Thumbprint(crypto.SHA256)
-		app.FatalIfError(err, "unable to compute thumbprint")
+		if err != nil {
+			return fmt.Errorf("unable to compute thumbprint: %w", err)
+		}
 
 		kid = base64.URLEncoding.EncodeToString(thumb)
 		priv.KeyID = kid
@@ -63,21 +74,32 @@ func generate() {
 	pub := jose.JSONWebKey{Key: pubKey, KeyID: kid, Algorithm: *generateAlgFlag, Use: *generateUseFlag}
 
 	if priv.IsPublic() || !pub.IsPublic() || !priv.Valid() || !pub.Valid() {
-		app.Fatalf("invalid keys were generated")
+		// This should never happen
+		panic("invalid keys were generated")
 	}
 
 	privJSON, err := priv.MarshalJSON()
-	app.FatalIfError(err, "failed to marshal private key to JSON")
+	if err != nil {
+		return fmt.Errorf("failed to marshal private key to JSON: %w", err)
+	}
 	pubJSON, err := pub.MarshalJSON()
-	app.FatalIfError(err, "failed to marshal public key to JSON")
+	if err != nil {
+		return fmt.Errorf("failed to marshal public key to JSON: %w", err)
+	}
 
 	name := fmt.Sprintf("jwk-%s-%s", *generateUseFlag, kid)
 	pubFile := fmt.Sprintf("%s-pub.json", name)
 	privFile := fmt.Sprintf("%s-priv.json", name)
 
 	err = writeNewFile(pubFile, pubJSON, 0444)
-	app.FatalIfError(err, "error on write to file %s", pubFile)
+	if err != nil {
+		return fmt.Errorf("error on write to file %s: %w", pubFile, err)
+	}
 
 	err = writeNewFile(privFile, privJSON, 0400)
-	app.FatalIfError(err, "error on write to file %s", privFile)
+	if err != nil {
+		return fmt.Errorf("error on write to file %s: %w", privFile, err)
+	}
+
+	return nil
 }
diff --git jose-util/generator/generate.go jose-util/generator/generate.go
index 859c750..ff3c53e 100644
--- jose-util/generator/generate.go
+++ jose-util/generator/generate.go
@@ -26,7 +26,7 @@ import (
 	"errors"
 	"fmt"
 
-	jose "github.com/go-jose/go-jose/v3"
+	"github.com/go-jose/go-jose/v4"
 )
 
 // NewSigningKey generates a keypair for corresponding SignatureAlgorithm.
diff --git jose-util/generator/utils.go jose-util/generator/utils.go
index aae676b..25f500e 100644
--- jose-util/generator/utils.go
+++ jose-util/generator/utils.go
@@ -21,7 +21,7 @@ import (
 	"encoding/pem"
 	"errors"
 
-	jose "github.com/go-jose/go-jose/v3"
+	"github.com/go-jose/go-jose/v4"
 )
 
 func LoadJSONWebKey(json []byte, pub bool) (*jose.JSONWebKey, error) {
diff --git jose-util/go.mod jose-util/go.mod
deleted file mode 100644
index fb46edc..0000000
--- jose-util/go.mod
+++ /dev/null
@@ -1,12 +0,0 @@
-module github.com/go-jose/go-jose/jose-util
-
-go 1.12
-
-require (
-	github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc // indirect
-	github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf // indirect
-	github.com/go-jose/go-jose/v3 v3.0.1
-	gopkg.in/alecthomas/kingpin.v2 v2.2.6
-)
-
-replace github.com/go-jose/go-jose/v3 => ../
diff --git jose-util/go.sum jose-util/go.sum
deleted file mode 100644
index daf821b..0000000
--- jose-util/go.sum
+++ /dev/null
@@ -1,48 +0,0 @@
-github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc h1:cAKDfWh5VpdgMhJosfJnn5/FoN2SRZ4p7fJNX58YPaU=
-github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
-github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf h1:qet1QNfXsQxTZqLG4oE62mJzwPIB8+Tee4RNCL9ulrY=
-github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0=
-github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
-github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
-github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
-github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
-github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
-github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
-github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
-github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
-github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
-golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
-golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
-golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc=
-golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
-golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
-golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
-golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
-golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
-golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
-golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
-golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
-golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
-golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
-golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
-golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
-golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
-golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
-golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
-golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
-golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
-golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
-golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6 h1:jMFz6MfLP0/4fUyZle81rXUoxOBFi19VUFKVDOQfozc=
-gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
-gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
-gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git jose-util/io.go jose-util/io.go
index 51b9598..ab26fdd 100644
--- jose-util/io.go
+++ jose-util/io.go
@@ -17,12 +17,13 @@
 package main
 
 import (
+	"fmt"
 	"io"
 	"os"
 )
 
 // Read input from file or stdin
-func readInput(path string) []byte {
+func readInput(path string) ([]byte, error) {
 	var bytes []byte
 	var err error
 
@@ -32,27 +33,14 @@ func readInput(path string) []byte {
 		bytes, err = io.ReadAll(os.Stdin)
 	}
 
-	app.FatalIfError(err, "unable to read input")
-	return bytes
-}
-
-// Get input stream from file or stdin
-func inputStream(path string) *os.File {
-	var file *os.File
-	var err error
-
-	if path != "" {
-		file, err = os.Open(path)
-	} else {
-		file = os.Stdin
+	if err != nil {
+		return nil, fmt.Errorf("unable to read input: %w", err)
 	}
-
-	app.FatalIfError(err, "unable to read input")
-	return file
+	return bytes, nil
 }
 
 // Write output to file or stdin
-func writeOutput(path string, data []byte) {
+func writeOutput(path string, data []byte) error {
 	var err error
 
 	if path != "" {
@@ -61,29 +49,22 @@ func writeOutput(path string, data []byte) {
 		_, err = os.Stdout.Write(data)
 	}
 
-	app.FatalIfError(err, "unable to write output")
-}
-
-// Get output stream for file or stdout
-func outputStream(path string) *os.File {
-	var file *os.File
-	var err error
-
-	if path != "" {
-		file, err = os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
-	} else {
-		file = os.Stdout
+	if err != nil {
+		return fmt.Errorf("unable to write output: %w", err)
 	}
-
-	app.FatalIfError(err, "unable to write output")
-	return file
+	return nil
 }
 
 // Byte contents of key file
-func keyBytes() []byte {
+func keyBytes() ([]byte, error) {
+	if *keyFile == "" {
+		return nil, fmt.Errorf("no key file provided. See -h for usage")
+	}
 	keyBytes, err := os.ReadFile(*keyFile)
-	app.FatalIfError(err, "unable to read key file")
-	return keyBytes
+	if err != nil {
+		return nil, fmt.Errorf("unable to read key file: %w", err)
+	}
+	return keyBytes, nil
 }
 
 // Write new file to current dir
diff --git jose-util/jose-util.t jose-util/jose-util.t
index 1820ae4..8786ee1 100644
--- jose-util/jose-util.t
+++ jose-util/jose-util.t
@@ -105,17 +105,3 @@ Generate signing keys in JWK format.
   $ jose-util generate-key --use sig --alg RS256 --kid test && ls jwk-sig-test-*.json
   jwk-sig-test-priv.json
   jwk-sig-test-pub.json
-
-Base64-decode data in various formats (padded, unpadded, standard, url-safe).
-
-  $ echo "8J+Ukgo=" | jose-util b64decode
-  🔒
-
-  $ echo "8J+Ukgo" | jose-util b64decode
-  🔒
-
-  $ echo "8J-Ukgo=" | jose-util b64decode
-  🔒
-
-  $ echo "8J-Ukgo" | jose-util b64decode
-  🔒
diff --git jose-util/main.go jose-util/main.go
index c99aabd..5841924 100644
--- jose-util/main.go
+++ jose-util/main.go
@@ -17,94 +17,78 @@
 package main
 
 import (
-	"bufio"
-	"encoding/base64"
+	"flag"
 	"fmt"
-	"io"
 	"os"
-
-	kingpin "gopkg.in/alecthomas/kingpin.v2"
-
-	generator "github.com/go-jose/go-jose/jose-util/generator"
-	jose "github.com/go-jose/go-jose/v3"
 )
 
 var (
-	app = kingpin.New("jose-util", "A command-line utility for dealing with JOSE objects")
-
 	// Util-wide flags
-	keyFile = app.Flag("key", "Path to key file (if applicable, PEM, DER or JWK format)").ExistingFile()
-	inFile  = app.Flag("in", "Path to input file (if applicable, stdin if missing)").ExistingFile()
-	outFile = app.Flag("out", "Path to output file (if applicable, stdout if missing)").ExistingFile()
-
-	// Encrypt
-	encryptCommand  = app.Command("encrypt", "Encrypt a plaintext, output ciphertext")
-	encryptAlgFlag  = encryptCommand.Flag("alg", "Key management algorithm (e.g. RSA-OAEP)").Required().String()
-	encryptEncFlag  = encryptCommand.Flag("enc", "Content encryption algorithm (e.g. A128GCM)").Required().String()
-	encryptFullFlag = encryptCommand.Flag("full", "Use JSON Serialization format (instead of compact)").Bool()
-
-	// Decrypt
-	decryptCommand = app.Command("decrypt", "Decrypt a ciphertext, output plaintext")
-
-	// Sign
-	signCommand  = app.Command("sign", "Sign a payload, output signed message")
-	signAlgFlag  = signCommand.Flag("alg", "Key management algorithm (e.g. RSA-OAEP)").Required().String()
-	signFullFlag = signCommand.Flag("full", "Use JSON Serialization format (instead of compact)").Bool()
-
-	// Verify
-	verifyCommand = app.Command("verify", "Verify a signed message, output payload")
+	keyFile *string
+	inFile  *string
+	outFile *string
+)
 
-	// Expand
-	expandCommand    = app.Command("expand", "Expand JOSE object to JSON Serialization format")
-	expandFormatFlag = expandCommand.Flag("format", "Type of message to expand (JWS or JWE, defaults to JWE)").String()
+func registerCommon(fs *flag.FlagSet) {
+	keyFile = fs.String("key", "", "Path to key file (if applicable, PEM, DER or JWK format)")
+	inFile = fs.String("in", "", "Path to input file (if applicable, stdin if missing)")
+	outFile = fs.String("out", "", "Path to output file (if applicable, stdout if missing)")
+}
 
-	// Base64-decode
-	base64DecodeCommand = app.Command("b64decode", "Decode a base64-encoded payload (auto-selects standard/url-safe)")
+func main() {
+	subCommands := map[string]struct {
+		desc string
+		run  func(args []string) error
+	}{
+		"encrypt": {
+			desc: "Encrypt a plaintext, output ciphertext",
+			run:  encrypt,
+		},
+		"decrypt": {
+			desc: "Decrypt a ciphertext, output plaintext",
+			run:  decrypt,
+		},
+		"sign": {
+			desc: "Sign a payload, output signed message",
+			run:  sign,
+		},
+		"verify": {
+			desc: "Verify a signed message, output payload",
+			run:  verify,
+		},
+		"expand": {
+			desc: "Expand JOSE object to JSON Serialization format",
+			run:  expand,
+		},
+		"generate-key": {
+			desc: "Generate a public/private key pair in JWK format",
+			run:  generate,
+		},
+	}
 
-	// Generate key
-	generateCommand = app.Command("generate-key", "Generate a public/private key pair in JWK format")
-	generateUseFlag = generateCommand.Flag("use", "Desired public key usage (use header), one of [enc sig]").Required().Enum("enc", "sig")
-	generateAlgFlag = generateCommand.Flag("alg", "Desired key pair algorithm (alg header)").Required().Enum(
-		// For signing
-		string(jose.EdDSA),
-		string(jose.ES256), string(jose.ES384), string(jose.ES512),
-		string(jose.RS256), string(jose.RS384), string(jose.RS512),
-		string(jose.PS256), string(jose.PS384), string(jose.PS512),
-		// For encryption
-		string(jose.RSA1_5), string(jose.RSA_OAEP), string(jose.RSA_OAEP_256),
-		string(jose.ECDH_ES), string(jose.ECDH_ES_A128KW), string(jose.ECDH_ES_A192KW), string(jose.ECDH_ES_A256KW),
-	)
-	generateKeySizeFlag  = generateCommand.Flag("size", "Key size in bits (e.g. 2048 if generating an RSA key)").Int()
-	generateKeyIdentFlag = generateCommand.Flag("kid", "Optional Key ID (kid header, generate random kid if not set)").String()
-)
+	usage := func() {
+		fmt.Printf("Usage: jose-utils [subcommand]\nSubcommands:\n")
+		for name, command := range subCommands {
+			fmt.Printf("  %s\n", name)
+			fmt.Printf("\t%s\n", command.desc)
+		}
+		fmt.Printf("Pass -h to each subcommand for more information")
+		os.Exit(1)
+	}
 
-func main() {
-	app.Version("v3")
-	app.UsageTemplate(kingpin.LongHelpTemplate)
+	if len(os.Args) < 2 {
+		usage()
+	}
 
-	command := kingpin.MustParse(app.Parse(os.Args[1:]))
+	cmd, ok := subCommands[os.Args[1]]
+	if !ok {
+		fmt.Fprintf(os.Stderr, "invalid subcommand %s\n", os.Args[1])
+		usage()
+	}
 
-	switch command {
-	case encryptCommand.FullCommand():
-		encrypt()
-	case decryptCommand.FullCommand():
-		decrypt()
-	case signCommand.FullCommand():
-		sign()
-	case verifyCommand.FullCommand():
-		verify()
-	case expandCommand.FullCommand():
-		expand()
-	case generateCommand.FullCommand():
-		generate()
-	case base64DecodeCommand.FullCommand():
-		in := inputStream(*inFile)
-		out := outputStream(*outFile)
-		io.Copy(out, base64.NewDecoder(base64.RawStdEncoding, generator.Base64Reader{In: bufio.NewReader(in)}))
-		defer in.Close()
-		defer out.Close()
-	default:
-		fmt.Fprintf(os.Stderr, "invalid command: %s\n", command)
+	err := cmd.run(os.Args[2:])
+	if err != nil {
+		fmt.Fprintf(os.Stderr, "error running command: %s\n", err)
 		os.Exit(1)
 	}
 }
diff --git jwe.go jwe.go
index 4267ac7..6102f91 100644
--- jwe.go
+++ jwe.go
@@ -18,10 +18,11 @@ package jose
 
 import (
 	"encoding/base64"
+	"errors"
 	"fmt"
 	"strings"
 
-	"github.com/go-jose/go-jose/v3/json"
+	"github.com/go-jose/go-jose/v4/json"
 )
 
 // rawJSONWebEncryption represents a raw JWE JSON object. Used for parsing/serializing.
@@ -104,29 +105,75 @@ func (obj JSONWebEncryption) computeAuthData() []byte {
 	return output
 }
 
-// ParseEncrypted parses an encrypted message in compact or JWE JSON Serialization format.
-func ParseEncrypted(input string) (*JSONWebEncryption, error) {
+func containsKeyAlgorithm(haystack []KeyAlgorithm, needle KeyAlgorithm) bool {
+	for _, algorithm := range haystack {
+		if algorithm == needle {
+			return true
+		}
+	}
+	return false
+}
+
+func containsContentEncryption(haystack []ContentEncryption, needle ContentEncryption) bool {
+	for _, algorithm := range haystack {
+		if algorithm == needle {
+			return true
+		}
+	}
+	return false
+}
+
+// ParseEncrypted parses an encrypted message in JWE Compact or JWE JSON Serialization.
+//
+// https://datatracker.ietf.org/doc/html/rfc7516#section-3.1
+// https://datatracker.ietf.org/doc/html/rfc7516#section-3.2
+//
+// The keyAlgorithms and contentEncryption parameters are used to validate the "alg" and "enc"
+// header parameters respectively. They must be nonempty, and each "alg" or "enc" header in
+// parsed data must contain a value that is present in the corresponding parameter. That
+// includes the protected and unprotected headers as well as all recipients. To accept
+// multiple algorithms, pass a slice of all the algorithms you want to accept.
+func ParseEncrypted(input string,
+	keyEncryptionAlgorithms []KeyAlgorithm,
+	contentEncryption []ContentEncryption,
+) (*JSONWebEncryption, error) {
 	input = stripWhitespace(input)
 	if strings.HasPrefix(input, "{") {
-		return parseEncryptedFull(input)
+		return ParseEncryptedJSON(input, keyEncryptionAlgorithms, contentEncryption)
 	}
 
-	return parseEncryptedCompact(input)
+	return ParseEncryptedCompact(input, keyEncryptionAlgorithms, contentEncryption)
 }
 
-// parseEncryptedFull parses a message in compact format.
-func parseEncryptedFull(input string) (*JSONWebEncryption, error) {
+// ParseEncryptedJSON parses a message in JWE JSON Serialization.
+//
+// https://datatracker.ietf.org/doc/html/rfc7516#section-3.2
+func ParseEncryptedJSON(
+	input string,
+	keyEncryptionAlgorithms []KeyAlgorithm,
+	contentEncryption []ContentEncryption,
+) (*JSONWebEncryption, error) {
 	var parsed rawJSONWebEncryption
 	err := json.Unmarshal([]byte(input), &parsed)
 	if err != nil {
 		return nil, err
 	}
 
-	return parsed.sanitized()
+	return parsed.sanitized(keyEncryptionAlgorithms, contentEncryption)
 }
 
 // sanitized produces a cleaned-up JWE object from the raw JSON.
-func (parsed *rawJSONWebEncryption) sanitized() (*JSONWebEncryption, error) {
+func (parsed *rawJSONWebEncryption) sanitized(
+	keyEncryptionAlgorithms []KeyAlgorithm,
+	contentEncryption []ContentEncryption,
+) (*JSONWebEncryption, error) {
+	if len(keyEncryptionAlgorithms) == 0 {
+		return nil, errors.New("go-jose/go-jose: no key algorithms provided")
+	}
+	if len(contentEncryption) == 0 {
+		return nil, errors.New("go-jose/go-jose: no content encryption algorithms provided")
+	}
+
 	obj := &JSONWebEncryption{
 		original:    parsed,
 		unprotected: parsed.Unprotected,
@@ -170,7 +217,7 @@ func (parsed *rawJSONWebEncryption) sanitized() (*JSONWebEncryption, error) {
 	} else {
 		obj.recipients = make([]recipientInfo, len(parsed.Recipients))
 		for r := range parsed.Recipients {
-			encryptedKey, err := base64URLDecode(parsed.Recipients[r].EncryptedKey)
+			encryptedKey, err := base64.RawURLEncoding.DecodeString(parsed.Recipients[r].EncryptedKey)
 			if err != nil {
 				return nil, err
 			}
@@ -185,10 +232,31 @@ func (parsed *rawJSONWebEncryption) sanitized() (*JSONWebEncryption, error) {
 		}
 	}
 
-	for _, recipient := range obj.recipients {
+	for i, recipient := range obj.recipients {
 		headers := obj.mergedHeaders(&recipient)
-		if headers.getAlgorithm() == "" || headers.getEncryption() == "" {
-			return nil, fmt.Errorf("go-jose/go-jose: message is missing alg/enc headers")
+		if headers.getAlgorithm() == "" {
+			return nil, fmt.Errorf(`go-jose/go-jose: recipient %d: missing header "alg"`, i)
+		}
+		if headers.getEncryption() == "" {
+			return nil, fmt.Errorf(`go-jose/go-jose: recipient %d: missing header "enc"`, i)
+		}
+		err := validateAlgEnc(headers, keyEncryptionAlgorithms, contentEncryption)
+		if err != nil {
+			return nil, fmt.Errorf("go-jose/go-jose: recipient %d: %s", i, err)
+		}
+
+	}
+
+	if obj.protected != nil {
+		err := validateAlgEnc(*obj.protected, keyEncryptionAlgorithms, contentEncryption)
+		if err != nil {
+			return nil, fmt.Errorf("go-jose/go-jose: protected header: %s", err)
+		}
+	}
+	if obj.unprotected != nil {
+		err := validateAlgEnc(*obj.unprotected, keyEncryptionAlgorithms, contentEncryption)
+		if err != nil {
+			return nil, fmt.Errorf("go-jose/go-jose: unprotected header: %s", err)
 		}
 	}
 
@@ -200,34 +268,62 @@ func (parsed *rawJSONWebEncryption) sanitized() (*JSONWebEncryption, error) {
 	return obj, nil
 }
 
-// parseEncryptedCompact parses a message in compact format.
-func parseEncryptedCompact(input string) (*JSONWebEncryption, error) {
-	parts := strings.Split(input, ".")
-	if len(parts) != 5 {
-		return nil, fmt.Errorf("go-jose/go-jose: compact JWE format must have five parts")
+func validateAlgEnc(headers rawHeader, keyAlgorithms []KeyAlgorithm, contentEncryption []ContentEncryption) error {
+	alg := headers.getAlgorithm()
+	enc := headers.getEncryption()
+	if alg != "" && !containsKeyAlgorithm(keyAlgorithms, alg) {
+		return fmt.Errorf("unexpected key algorithm %q; expected %q", alg, keyAlgorithms)
+	}
+	if enc != "" && !containsContentEncryption(contentEncryption, enc) {
+		return fmt.Errorf("unexpected content encryption algorithm %q; expected %q", enc, contentEncryption)
+	}
+	return nil
+}
+
+// ParseEncryptedCompact parses a message in JWE Compact Serialization.
+//
+// https://datatracker.ietf.org/doc/html/rfc7516#section-3.1
+func ParseEncryptedCompact(
+	input string,
+	keyAlgorithms []KeyAlgorithm,
+	contentEncryption []ContentEncryption,
+) (*JSONWebEncryption, error) {
+	var parts [5]string
+	var ok bool
+
+	for i := range 4 {
+		parts[i], input, ok = strings.Cut(input, ".")
+		if !ok {
+			return nil, errors.New("go-jose/go-jose: compact JWE format must have five parts")
+		}
+	}
+	// Validate that the last part does not contain more dots
+	if strings.ContainsRune(input, '.') {
+		return nil, errors.New("go-jose/go-jose: compact JWE format must have five parts")
 	}
+	parts[4] = input
 
-	rawProtected, err := base64URLDecode(parts[0])
+	rawProtected, err := base64.RawURLEncoding.DecodeString(parts[0])
 	if err != nil {
 		return nil, err
 	}
 
-	encryptedKey, err := base64URLDecode(parts[1])
+	encryptedKey, err := base64.RawURLEncoding.DecodeString(parts[1])
 	if err != nil {
 		return nil, err
 	}
 
-	iv, err := base64URLDecode(parts[2])
+	iv, err := base64.RawURLEncoding.DecodeString(parts[2])
 	if err != nil {
 		return nil, err
 	}
 
-	ciphertext, err := base64URLDecode(parts[3])
+	ciphertext, err := base64.RawURLEncoding.DecodeString(parts[3])
 	if err != nil {
 		return nil, err
 	}
 
-	tag, err := base64URLDecode(parts[4])
+	tag, err := base64.RawURLEncoding.DecodeString(parts[4])
 	if err != nil {
 		return nil, err
 	}
@@ -240,7 +336,7 @@ func parseEncryptedCompact(input string) (*JSONWebEncryption, error) {
 		Tag:          newBuffer(tag),
 	}
 
-	return raw.sanitized()
+	return raw.sanitized(keyAlgorithms, contentEncryption)
 }
 
 // CompactSerialize serializes an object using the compact serialization format.
diff --git jwe_test.go jwe_test.go
index 4eba24e..51147a3 100644
--- jwe_test.go
+++ jwe_test.go
@@ -30,7 +30,7 @@ import (
 func TestCompactParseJWE(t *testing.T) {
 	// Should parse
 	msg := "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA.dGVzdA"
-	_, err := ParseEncrypted(msg)
+	_, err := ParseEncrypted(msg, []KeyAlgorithm{RSA_OAEP}, []ContentEncryption{A128GCM})
 	if err != nil {
 		t.Error("Unable to parse valid message:", err)
 	}
@@ -58,7 +58,7 @@ func TestCompactParseJWE(t *testing.T) {
 	}
 
 	for _, msg := range failures {
-		_, err = ParseEncrypted(msg)
+		_, err = ParseEncrypted(msg, []KeyAlgorithm{RSA_OAEP}, []ContentEncryption{A128GCM})
 		if err == nil {
 			t.Error("Able to parse invalid message", msg)
 		}
@@ -75,7 +75,7 @@ func TestFullParseJWE(t *testing.T) {
 	}
 
 	for i := range successes {
-		_, err := ParseEncrypted(successes[i])
+		_, err := ParseEncrypted(successes[i], []KeyAlgorithm{KeyAlgorithm("XYZ")}, []ContentEncryption{ContentEncryption("XYZ")})
 		if err != nil {
 			t.Error("Unble to parse valid message", err, successes[i])
 		}
@@ -114,9 +114,9 @@ func TestFullParseJWE(t *testing.T) {
 	}
 
 	for i := range failures {
-		_, err := ParseEncrypted(failures[i])
+		_, err := ParseEncrypted(failures[i], []KeyAlgorithm{KeyAlgorithm("XYZ")}, []ContentEncryption{ContentEncryption("XYZ")})
 		if err == nil {
-			t.Error("Able to parse invalid message", err, failures[i])
+			t.Error("Able to parse invalid message", failures[i])
 		}
 	}
 }
@@ -185,7 +185,7 @@ func TestRejectUnprotectedJWENonce(t *testing.T) {
 	"ciphertext": "does-not-matter",
 	"tag": "does-not-matter"
 	}`
-	_, err := ParseEncrypted(input)
+	_, err := ParseEncrypted(input, []KeyAlgorithm{KeyAlgorithm("XYZ")}, []ContentEncryption{ContentEncryption("XYZ")})
 	if err == nil {
 		t.Error("JWE with an unprotected nonce parsed as valid.")
 	} else if err.Error() != "go-jose/go-jose: Nonce parameter included in unprotected header" {
@@ -203,7 +203,7 @@ func TestRejectUnprotectedJWENonce(t *testing.T) {
 		"ciphertext": "does-not-matter",
 		"tag": "does-not-matter"
 	}`
-	_, err = ParseEncrypted(input)
+	_, err = ParseEncrypted(input, []KeyAlgorithm{KeyAlgorithm("XYZ")}, []ContentEncryption{ContentEncryption("XYZ")})
 	if err == nil {
 		t.Error("JWE with an unprotected nonce parsed as valid.")
 	} else if err.Error() != "go-jose/go-jose: Nonce parameter included in unprotected header" {
@@ -222,7 +222,7 @@ func TestRejectUnprotectedJWENonce(t *testing.T) {
 			"encrypted_key": "does-not-matter"
 		}]
 	}`
-	_, err = ParseEncrypted(input)
+	_, err = ParseEncrypted(input, []KeyAlgorithm{KeyAlgorithm("XYZ")}, []ContentEncryption{ContentEncryption("XYZ")})
 	if err == nil {
 		t.Error("JWS with an unprotected nonce parsed as valid.")
 	} else if err.Error() != "go-jose/go-jose: Nonce parameter included in unprotected header" {
@@ -328,7 +328,7 @@ func TestVectorsJWE(t *testing.T) {
 func TestJWENilProtected(t *testing.T) {
 	key := []byte("1234567890123456")
 	serialized := `{"unprotected":{"alg":"dir","enc":"A128GCM"}}`
-	jwe, _ := ParseEncrypted(serialized)
+	jwe, _ := ParseEncrypted(serialized, []KeyAlgorithm{DIRECT}, []ContentEncryption{A128GCM})
 	if _, err := jwe.Decrypt(key); err == nil {
 		t.Error(err)
 	}
@@ -377,13 +377,13 @@ func TestVectorsJWECorrupt(t *testing.T) {
 		PhDO6ufSC7kV4bNqgHR-4ziS7KNwzN83_5kogXqxUpymUoJDNc.tk-GT
 		W_VVhiTIKFF.D_BE6ImZUl9F.52a-zFnRb3YQwiC7UrhVyQ`)
 
-	msg, _ := ParseEncrypted(corruptCiphertext)
+	msg, _ := ParseEncrypted(corruptCiphertext, []KeyAlgorithm{RSA_OAEP}, []ContentEncryption{A128GCM})
 	_, err := msg.Decrypt(priv)
 	if err != ErrCryptoFailure {
 		t.Error("should detect corrupt ciphertext")
 	}
 
-	msg, _ = ParseEncrypted(corruptAuthtag)
+	msg, _ = ParseEncrypted(corruptAuthtag, []KeyAlgorithm{RSA_OAEP}, []ContentEncryption{A128GCM})
 	_, err = msg.Decrypt(priv)
 	if err != ErrCryptoFailure {
 		t.Error("should detect corrupt auth tag")
@@ -443,7 +443,20 @@ func TestSampleNimbusJWEMessagesRSA(t *testing.T) {
 	}
 
 	for _, msg := range rsaSampleMessages {
-		obj, err := ParseEncrypted(msg)
+		obj, err := ParseEncrypted(msg,
+			[]KeyAlgorithm{
+				RSA1_5,
+				RSA_OAEP,
+				RSA_OAEP_256,
+			},
+			[]ContentEncryption{
+				A256CBC_HS512,
+				A256GCM,
+				A128CBC_HS256,
+				A128GCM,
+				A192CBC_HS384,
+				A192GCM,
+			})
 		if err != nil {
 			t.Error("unable to parse message", msg, err)
 			continue
@@ -514,7 +527,23 @@ func TestSampleNimbusJWEMessagesAESKW(t *testing.T) {
 
 	for i, msgs := range aesSampleMessages {
 		for _, msg := range msgs {
-			obj, err := ParseEncrypted(msg)
+			obj, err := ParseEncrypted(msg,
+				[]KeyAlgorithm{
+					A128GCMKW,
+					A128KW,
+					A192GCMKW,
+					A192KW,
+					A256GCMKW,
+					A256GCMKW,
+					A256KW,
+				}, []ContentEncryption{
+					A128CBC_HS256,
+					A128GCM,
+					A192CBC_HS384,
+					A192GCM,
+					A256CBC_HS512,
+					A256GCM,
+				})
 			if err != nil {
 				t.Error("unable to parse message", msg, err)
 				continue
@@ -558,7 +587,17 @@ func TestSampleJose4jJWEMessagesECDH(t *testing.T) {
 	}
 
 	for _, msg := range ecSampleMessages {
-		obj, err := ParseEncrypted(msg)
+		obj, err := ParseEncrypted(msg,
+			[]KeyAlgorithm{
+				ECDH_ES,
+				ECDH_ES_A128KW,
+				ECDH_ES_A192KW,
+				ECDH_ES_A256KW,
+			}, []ContentEncryption{
+				A128CBC_HS256,
+				A192CBC_HS384,
+				A256CBC_HS512,
+			})
 		if err != nil {
 			t.Error("unable to parse message", msg, err)
 			continue
@@ -597,7 +636,7 @@ func TestPrecomputedECDHMessagesFromJose4j(t *testing.T) {
 			t.Fatal(i, err)
 		}
 
-		parsed, err := ParseEncrypted(vector.message)
+		parsed, err := ParseEncrypted(vector.message, []KeyAlgorithm{ECDH_ES}, []ContentEncryption{A192CBC_HS384, A256CBC_HS512})
 		if err != nil {
 			t.Fatal(i, err)
 		}
@@ -632,7 +671,7 @@ func TestSampleAESCBCHMACMessagesFromNodeJose(t *testing.T) {
 	}
 
 	for _, sample := range samples {
-		obj, err := ParseEncrypted(sample.ciphertext)
+		obj, err := ParseEncrypted(sample.ciphertext, []KeyAlgorithm{DIRECT}, []ContentEncryption{A128CBC_HS256, A192CBC_HS384, A256CBC_HS512})
 		if err != nil {
 			t.Error("unable to parse message", sample.ciphertext, err)
 			continue
@@ -663,7 +702,7 @@ func TestTamperedJWE(t *testing.T) {
 	serialized = regexp.MustCompile(`"iv":"[^"]+"`).
 		ReplaceAllString(serialized, `"iv":"UotNnfiavtNOOSZAcfI03i"`)
 
-	object, _ = ParseEncrypted(serialized)
+	object, _ = ParseEncrypted(serialized, []KeyAlgorithm{DIRECT}, []ContentEncryption{A128GCM})
 
 	_, err := object.Decrypt(key)
 	if err == nil {
@@ -674,7 +713,65 @@ func TestTamperedJWE(t *testing.T) {
 func TestJWEWithNullAlg(t *testing.T) {
 	// {"alg":null,"enc":"A128GCM"}
 	serialized := `{"protected":"eyJhbGciOm51bGwsImVuYyI6IkExMjhHQ00ifQ"}`
-	if _, err := ParseEncrypted(serialized); err == nil {
+	if _, err := ParseEncrypted(serialized, []KeyAlgorithm{KeyAlgorithm("null")}, []ContentEncryption{A128GCM}); err == nil {
 		t.Error(err)
 	}
 }
+
+func TestEmptyEncryptedKey(t *testing.T) {
+	// These inputs use key wrapping with an empty wrapped key.
+	// All fields except the unprotected header are empty; in particular "JWE Encrypted Key" is empty.
+	serializedCompact := `eyJhbGciOiJQQkVTMi1IUzUxMitBMjU2S1ciLCJjdHkiOiJhcHBsaWNhdGlvbi9qd2sranNvbiIsImVuYyI6IkEyNTZHQ00iLCJwMmMiOjIxMDAwMCwicDJzIjoiY000YyJ9....`
+	serializedJSON := `{"unprotected":{"alg":"PBES2-HS512+A256KW","cty":"application/jwk+json","enc":"A256GCM","p2c":210000,"p2s":"cM4c"}}`
+	acceptedAlgs := []KeyAlgorithm{PBES2_HS512_A256KW}
+	acceptedContentAlgs := []ContentEncryption{A256GCM}
+	item, err := ParseEncrypted(serializedCompact, acceptedAlgs, acceptedContentAlgs)
+	if err != nil {
+		t.Fatalf("ParseEncrypted(%q): %s", serializedCompact, err)
+	}
+
+	secret := []byte("valid-key-used-for-decryption")
+
+	// Note: we check this "want" value to distinguish from other error cases, but future refactorings to give
+	// a more useful error message would be okay.
+	want := "go-jose/go-jose: error in cryptographic primitive"
+	_, err = item.Decrypt(secret)
+	if err == nil {
+		t.Errorf("Decrypt() after ParseEncrypted() with empty encrypted key should fail")
+	} else if err.Error() != want {
+		t.Errorf("Decrypt() after ParseEncrypted() with empty encrypted key: got %q, want %q", err, want)
+	}
+
+	item, err = ParseEncryptedCompact(serializedCompact, acceptedAlgs, acceptedContentAlgs)
+	if err != nil {
+		t.Fatalf("ParseEncryptedCompact(%q): %s", serializedCompact, err)
+	}
+
+	_, err = item.Decrypt(secret)
+	if err == nil {
+		t.Errorf("Decrypt() after ParseEncryptedCompact() with empty encrypted key should fail")
+	} else if err.Error() != want {
+		t.Errorf("Decrypt() after ParseEncryptedCompact() with empty encrypted key: got %q, want %q", err, want)
+	}
+
+	item, err = ParseEncryptedJSON(serializedJSON, acceptedAlgs, acceptedContentAlgs)
+	if err != nil {
+		t.Fatalf("ParseEncryptedJSON(%q): %s", serializedJSON, err)
+	}
+
+	_, err = item.Decrypt(secret)
+	if err == nil {
+		t.Errorf("Decrypt() after ParseEncryptedJSON() with empty encrypted key should fail")
+	} else if err.Error() != want {
+		t.Errorf("Decrypt() after ParseEncryptedJSON() with empty encrypted key: got %q, want %q", err, want)
+	}
+}
+
+func BenchmarkParseEncryptedCompat(b *testing.B) {
+	msg := "eyJhbGciOiJSU0EtT0FFUCIsImVuYyI6IkExMjhHQ00ifQ.dGVzdA.dGVzdA.dGVzdA.dGVzdA"
+	for range b.N {
+		if _, err := ParseEncryptedCompact(msg, []KeyAlgorithm{RSA_OAEP}, []ContentEncryption{A128GCM}); err != nil {
+			panic(err)
+		}
+	}
+}
diff --git jwk.go jwk.go
index e402195..164d6a1 100644
--- jwk.go
+++ jwk.go
@@ -35,7 +35,7 @@ import (
 	"reflect"
 	"strings"
 
-	"github.com/go-jose/go-jose/v3/json"
+	"github.com/go-jose/go-jose/v4/json"
 )
 
 // rawJSONWebKey represents a public or private key in JWK format, used for parsing/serializing.
@@ -175,6 +175,8 @@ func (k JSONWebKey) MarshalJSON() ([]byte, error) {
 }
 
 // UnmarshalJSON reads a key from its JSON representation.
+//
+// Returns ErrUnsupportedKeyType for unrecognized or unsupported "kty" header values.
 func (k *JSONWebKey) UnmarshalJSON(data []byte) (err error) {
 	var raw rawJSONWebKey
 	err = json.Unmarshal(data, &raw)
@@ -228,7 +230,7 @@ func (k *JSONWebKey) UnmarshalJSON(data []byte) (err error) {
 		}
 		key, err = raw.symmetricKey()
 	case "OKP":
-		if raw.Crv == "Ed25519" && raw.X != nil {
+		if raw.Crv == "Ed25519" {
 			if raw.D != nil {
 				key, err = raw.edPrivateKey()
 				if err == nil {
@@ -238,17 +240,27 @@ func (k *JSONWebKey) UnmarshalJSON(data []byte) (err error) {
 				key, err = raw.edPublicKey()
 				keyPub = key
 			}
-		} else {
-			err = fmt.Errorf("go-jose/go-jose: unknown curve %s'", raw.Crv)
 		}
-	default:
-		err = fmt.Errorf("go-jose/go-jose: unknown json web key type '%s'", raw.Kty)
+	case "":
+		// kty MUST be present
+		err = fmt.Errorf("go-jose/go-jose: missing json web key type")
 	}
 
 	if err != nil {
 		return
 	}
 
+	if key == nil {
+		// RFC 7517:
+		// 5.  JWK Set Format
+		// ...
+		//     Implementations SHOULD ignore JWKs within a JWK Set that use "kty"
+		//     (key type) values that are not understood by them, that are missing
+		//     required members, or for which values are out of the supported
+		//     ranges.
+		return ErrUnsupportedKeyType
+	}
+
 	if certPub != nil && keyPub != nil {
 		if !reflect.DeepEqual(certPub, keyPub) {
 			return errors.New("go-jose/go-jose: invalid JWK, public keys in key and x5c fields do not match")
@@ -266,7 +278,7 @@ func (k *JSONWebKey) UnmarshalJSON(data []byte) (err error) {
 
 	// x5t parameters are base64url-encoded SHA thumbprints
 	// See RFC 7517, Section 4.8, https://tools.ietf.org/html/rfc7517#section-4.8
-	x5tSHA1bytes, err := base64URLDecode(raw.X5tSHA1)
+	x5tSHA1bytes, err := base64.RawURLEncoding.DecodeString(raw.X5tSHA1)
 	if err != nil {
 		return errors.New("go-jose/go-jose: invalid JWK, x5t header has invalid encoding")
 	}
@@ -286,7 +298,7 @@ func (k *JSONWebKey) UnmarshalJSON(data []byte) (err error) {
 
 	k.CertificateThumbprintSHA1 = x5tSHA1bytes
 
-	x5tSHA256bytes, err := base64URLDecode(raw.X5tSHA256)
+	x5tSHA256bytes, err := base64.RawURLEncoding.DecodeString(raw.X5tSHA256)
 	if err != nil {
 		return errors.New("go-jose/go-jose: invalid JWK, x5t#S256 header has invalid encoding")
 	}
@@ -581,10 +593,10 @@ func fromEcPublicKey(pub *ecdsa.PublicKey) (*rawJSONWebKey, error) {
 
 func (key rawJSONWebKey) edPrivateKey() (ed25519.PrivateKey, error) {
 	var missing []string
-	switch {
-	case key.D == nil:
+	if key.D == nil {
 		missing = append(missing, "D")
-	case key.X == nil:
+	}
+	if key.X == nil {
 		missing = append(missing, "X")
 	}
 
@@ -611,19 +623,21 @@ func (key rawJSONWebKey) edPublicKey() (ed25519.PublicKey, error) {
 
 func (key rawJSONWebKey) rsaPrivateKey() (*rsa.PrivateKey, error) {
 	var missing []string
-	switch {
-	case key.N == nil:
+	if key.N == nil {
 		missing = append(missing, "N")
-	case key.E == nil:
+	}
+	if key.E == nil {
 		missing = append(missing, "E")
-	case key.D == nil:
+	}
+	if key.D == nil {
 		missing = append(missing, "D")
-	case key.P == nil:
+	}
+	if key.P == nil {
 		missing = append(missing, "P")
-	case key.Q == nil:
+	}
+	if key.Q == nil {
 		missing = append(missing, "Q")
 	}
-
 	if len(missing) > 0 {
 		return nil, fmt.Errorf("go-jose/go-jose: invalid RSA private key, missing %s value(s)", strings.Join(missing, ", "))
 	}
@@ -698,8 +712,19 @@ func (key rawJSONWebKey) ecPrivateKey() (*ecdsa.PrivateKey, error) {
 		return nil, fmt.Errorf("go-jose/go-jose: unsupported elliptic curve '%s'", key.Crv)
 	}
 
-	if key.X == nil || key.Y == nil || key.D == nil {
-		return nil, fmt.Errorf("go-jose/go-jose: invalid EC private key, missing x/y/d values")
+	var missing []string
+	if key.X == nil {
+		missing = append(missing, "X")
+	}
+	if key.Y == nil {
+		missing = append(missing, "Y")
+	}
+	if key.D == nil {
+		missing = append(missing, "D")
+	}
+
+	if len(missing) > 0 {
+		return nil, fmt.Errorf("go-jose/go-jose: invalid EC private key, missing %s value(s)", strings.Join(missing, ", "))
 	}
 
 	// The length of this octet string MUST be the full size of a coordinate for
@@ -779,7 +804,13 @@ func (key rawJSONWebKey) symmetricKey() ([]byte, error) {
 	return key.K.bytes(), nil
 }
 
-func tryJWKS(key interface{}, headers ...Header) interface{} {
+var (
+	// ErrJWKSKidNotFound is returned when a JWKS does not contain a JWK with a
+	// key ID which matches one in the provided tokens headers.
+	ErrJWKSKidNotFound = errors.New("go-jose/go-jose: JWK with matching kid not found in JWK Set")
+)
+
+func tryJWKS(key interface{}, headers ...Header) (interface{}, error) {
 	var jwks JSONWebKeySet
 
 	switch jwksType := key.(type) {
@@ -788,9 +819,11 @@ func tryJWKS(key interface{}, headers ...Header) interface{} {
 	case JSONWebKeySet:
 		jwks = jwksType
 	default:
-		return key
+		// If the specified key is not a JWKS, return as is.
+		return key, nil
 	}
 
+	// Determine the KID to search for from the headers.
 	var kid string
 	for _, header := range headers {
 		if header.KeyID != "" {
@@ -799,14 +832,17 @@ func tryJWKS(key interface{}, headers ...Header) interface{} {
 		}
 	}
 
+	// If no KID is specified in the headers, reject.
 	if kid == "" {
-		return key
+		return nil, ErrJWKSKidNotFound
 	}
 
+	// Find the JWK with the matching KID. If no JWK with the specified KID is
+	// found, reject.
 	keys := jwks.Key(kid)
 	if len(keys) == 0 {
-		return key
+		return nil, ErrJWKSKidNotFound
 	}
 
-	return keys[0].Key
+	return keys[0].Key, nil
 }
diff --git jwk_test.go jwk_test.go
index 1c79ed0..846de26 100644
--- jwk_test.go
+++ jwk_test.go
@@ -27,16 +27,16 @@ import (
 	"crypto/sha256"
 	"crypto/x509"
 	"encoding/hex"
+	"errors"
 	"math/big"
 	"reflect"
 	"strings"
 	"testing"
 
-	"github.com/go-jose/go-jose/v3/json"
+	"github.com/go-jose/go-jose/v4/json"
 
-	"github.com/google/go-cmp/cmp"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
+	"github.com/go-jose/go-jose/v4/testutils/assert"
+	"github.com/go-jose/go-jose/v4/testutils/require"
 )
 
 // Test chain of two X.509 certificates
@@ -316,8 +316,9 @@ func TestRoundtripX509(t *testing.T) {
 			jsonbar2, err := jwk2.MarshalJSON()
 			require.NoError(t, err)
 
-			require.Empty(t, cmp.Diff(jsonbar, jsonbar2))
 			if !bytes.Equal(jsonbar, jsonbar2) {
+				t.Logf("Original JSON: %s", string(jsonbar))
+				t.Logf("New JSON: %s", string(jsonbar2))
 				t.Error("roundtrip should not lose information")
 			}
 		})
@@ -364,7 +365,9 @@ func TestRoundtripX509Hex(t *testing.T) {
 	var j1, j2 map[string]interface{}
 	require.NoError(t, json.Unmarshal(js, &j1))
 	require.NoError(t, json.Unmarshal([]byte(output), &j2))
-	require.Empty(t, cmp.Diff(j1, j2))
+	if !reflect.DeepEqual(j1, j2) {
+		t.Errorf("Not equal after round trip: '%v' '%v'", j1, j2)
+	}
 }
 
 func TestCertificatesURL(t *testing.T) {
@@ -384,7 +387,9 @@ func TestCertificatesURL(t *testing.T) {
 	var j1, j2 map[string]interface{}
 	require.NoError(t, json.Unmarshal(js, &j1))
 	require.NoError(t, json.Unmarshal([]byte(urlJWK), &j2))
-	require.Empty(t, cmp.Diff(j1, j2))
+	if !reflect.DeepEqual(j1, j2) {
+		t.Errorf("Not equal after round trip: '%v' '%v'", j1, j2)
+	}
 
 	var invalidURLJWK = `{
    "kty":"RSA",
@@ -393,7 +398,7 @@ func TestCertificatesURL(t *testing.T) {
    "x5u": "://example.com/keys.json"
 }`
 	err = jwk2.UnmarshalJSON([]byte(invalidURLJWK))
-	require.EqualError(t, err, "go-jose/go-jose: invalid JWK, x5u header is invalid URL: parse \"://example.com/keys.json\": missing protocol scheme")
+	require.Equal(t, err.Error(), "go-jose/go-jose: invalid JWK, x5u header is invalid URL: parse \"://example.com/keys.json\": missing protocol scheme")
 }
 
 func TestInvalidThumbprintsX509(t *testing.T) {
@@ -475,46 +480,6 @@ func TestInvalidThumbprintsX509(t *testing.T) {
 	}
 }
 
-func TestPaddedThumbprintIsStripped(t *testing.T) {
-	var hexJWK = `{
-	"e": "AQAB",
-	"kid": "dpuEmX8znJJqBq4gurHOy8TfMRc",
-	"kty": "RSA",
-	"n": "9TjhCGd6luJZF71eiz5xGDh2ax6R44r7t1CfNV0E-_oGd5OggY2-rJlVu9DeNzP-oSsoMZ0S0rKVpDt4paCrOmCAmfXfNTIzQC-oqkKH7p8l7KJ2GuDotaLb9qJKGBhty6c-hInWaMAoI1TnKkYUBQsCv07dKdm7hse2GtJZFkIlAFYeltnu9KKAgs23YMXnKsfQvyS4FCzWZdxQuPfLveOP2dd79khzzGO9g3Wp2Y4DzQOoF7ZDtUhnfVcqDq5q17Gj3cCabN0dvuUI7HIRFPlQsvRJy4i3FbVG4_lx7HthXLsAj30GexPK_v2oY1WjvxF55hrMjkUcv8Y9LKm2fQ",
-	"use": "sig",
-	"x5c": [
-		"MIIFLTCCBBWgAwIBAgIEWcXexjANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJHQjEUMBIGA1UEChMLT3BlbkJhbmtpbmcxLjAsBgNVBAMTJU9wZW5CYW5raW5nIFByZS1Qcm9kdWN0aW9uIElzc3VpbmcgQ0EwHhcNMjAwNjEyMTExMzU1WhcNMjEwNzEyMTE0MzU1WjBhMQswCQYDVQQGEwJHQjEUMBIGA1UEChMLT3BlbkJhbmtpbmcxGzAZBgNVBAsTEjAwMTU4MDAwMDEwM1VBdkFBTTEfMB0GA1UEAxMWNG1Zc1Rnd1hBRVhjbldROXpoRHBhVzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPU44QhnepbiWRe9Xos+cRg4dmsekeOK+7dQnzVdBPv6BneToIGNvqyZVbvQ3jcz/qErKDGdEtKylaQ7eKWgqzpggJn13zUyM0AvqKpCh+6fJeyidhrg6LWi2/aiShgYbcunPoSJ1mjAKCNU5ypGFAULAr9O3SnZu4bHthrSWRZCJQBWHpbZ7vSigILNt2DF5yrH0L8kuBQs1mXcULj3y73jj9nXe/ZIc8xjvYN1qdmOA80DqBe2Q7VIZ31XKg6uatexo93AmmzdHb7lCOxyERT5ULL0ScuItxW1RuP5cex7YVy7AI99BnsTyv79qGNVo78ReeYazI5FHL/GPSyptn0CAwEAAaOCAfkwggH1MA4GA1UdDwEB/wQEAwIGwDAVBgNVHSUEDjAMBgorBgEEAYI3CgMMMIHgBgNVHSAEgdgwgdUwgdIGCysGAQQBqHWBBgFkMIHCMCoGCCsGAQUFBwIBFh5odHRwOi8vb2IudHJ1c3Rpcy5jb20vcG9saWNpZXMwgZMGCCsGAQUFBwICMIGGDIGDVXNlIG9mIHRoaXMgQ2VydGlmaWNhdGUgY29uc3RpdHV0ZXMgYWNjZXB0YW5jZSBvZiB0aGUgT3BlbkJhbmtpbmcgUm9vdCBDQSBDZXJ0aWZpY2F0aW9uIFBvbGljaWVzIGFuZCBDZXJ0aWZpY2F0ZSBQcmFjdGljZSBTdGF0ZW1lbnQwbQYIKwYBBQUHAQEEYTBfMCYGCCsGAQUFBzABhhpodHRwOi8vb2IudHJ1c3Rpcy5jb20vb2NzcDA1BggrBgEFBQcwAoYpaHR0cDovL29iLnRydXN0aXMuY29tL29iX3BwX2lzc3VpbmdjYS5jcnQwOgYDVR0fBDMwMTAvoC2gK4YpaHR0cDovL29iLnRydXN0aXMuY29tL29iX3BwX2lzc3VpbmdjYS5jcmwwHwYDVR0jBBgwFoAUUHORxiFy03f0/gASBoFceXluP1AwHQYDVR0OBBYEFEDCcD9DKG/GDmRqKInVaycbBXqBMA0GCSqGSIb3DQEBCwUAA4IBAQBiQCbu2aSj28pAIO+Cf36ELT9ATWwR6kTCxgUoYHxh3G2uCn4ocOE1Nzl/sSnSVTcp8O2CdeYcRWXfj5jP4jpIL/zkpC1CD1VWKWNJJF2C3RMPlY/sheHhUFB3dCPTZDDChA09gEWSHVFxdIA64/wWTWutOwNZbF5iD+QXYkarMBE4Ake/2Yoeno5HWtJTc/Sgm9EKj7SDvYuLouNWIrw1/W+F52eFeyRSKLPCmPUV8iz3vRRb8jfRTEFeBzDMy4GGIEKmV9HYkDDEiB1y0RO2GU2PquFMaNlN5I1a9YgkCQBNeWJMCXYuuBSZ545dwXgfeNwZ3a89IWaKYtwM6g2N"
-	],
-	"x5t": "bnKJSxqsULiTOPSwNIqMX0xzgcU=",
-	"x5t#S256": "SCfnsU0X0cxg_11iOLMqWuy2d5wMnIYIM9bDOsfCRRU="
-}`
-
-	var output = `{
-	"e": "AQAB",
-	"kid": "dpuEmX8znJJqBq4gurHOy8TfMRc",
-	"kty": "RSA",
-	"n": "9TjhCGd6luJZF71eiz5xGDh2ax6R44r7t1CfNV0E-_oGd5OggY2-rJlVu9DeNzP-oSsoMZ0S0rKVpDt4paCrOmCAmfXfNTIzQC-oqkKH7p8l7KJ2GuDotaLb9qJKGBhty6c-hInWaMAoI1TnKkYUBQsCv07dKdm7hse2GtJZFkIlAFYeltnu9KKAgs23YMXnKsfQvyS4FCzWZdxQuPfLveOP2dd79khzzGO9g3Wp2Y4DzQOoF7ZDtUhnfVcqDq5q17Gj3cCabN0dvuUI7HIRFPlQsvRJy4i3FbVG4_lx7HthXLsAj30GexPK_v2oY1WjvxF55hrMjkUcv8Y9LKm2fQ",
-	"use": "sig",
-	"x5c": [
-		"MIIFLTCCBBWgAwIBAgIEWcXexjANBgkqhkiG9w0BAQsFADBTMQswCQYDVQQGEwJHQjEUMBIGA1UEChMLT3BlbkJhbmtpbmcxLjAsBgNVBAMTJU9wZW5CYW5raW5nIFByZS1Qcm9kdWN0aW9uIElzc3VpbmcgQ0EwHhcNMjAwNjEyMTExMzU1WhcNMjEwNzEyMTE0MzU1WjBhMQswCQYDVQQGEwJHQjEUMBIGA1UEChMLT3BlbkJhbmtpbmcxGzAZBgNVBAsTEjAwMTU4MDAwMDEwM1VBdkFBTTEfMB0GA1UEAxMWNG1Zc1Rnd1hBRVhjbldROXpoRHBhVzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPU44QhnepbiWRe9Xos+cRg4dmsekeOK+7dQnzVdBPv6BneToIGNvqyZVbvQ3jcz/qErKDGdEtKylaQ7eKWgqzpggJn13zUyM0AvqKpCh+6fJeyidhrg6LWi2/aiShgYbcunPoSJ1mjAKCNU5ypGFAULAr9O3SnZu4bHthrSWRZCJQBWHpbZ7vSigILNt2DF5yrH0L8kuBQs1mXcULj3y73jj9nXe/ZIc8xjvYN1qdmOA80DqBe2Q7VIZ31XKg6uatexo93AmmzdHb7lCOxyERT5ULL0ScuItxW1RuP5cex7YVy7AI99BnsTyv79qGNVo78ReeYazI5FHL/GPSyptn0CAwEAAaOCAfkwggH1MA4GA1UdDwEB/wQEAwIGwDAVBgNVHSUEDjAMBgorBgEEAYI3CgMMMIHgBgNVHSAEgdgwgdUwgdIGCysGAQQBqHWBBgFkMIHCMCoGCCsGAQUFBwIBFh5odHRwOi8vb2IudHJ1c3Rpcy5jb20vcG9saWNpZXMwgZMGCCsGAQUFBwICMIGGDIGDVXNlIG9mIHRoaXMgQ2VydGlmaWNhdGUgY29uc3RpdHV0ZXMgYWNjZXB0YW5jZSBvZiB0aGUgT3BlbkJhbmtpbmcgUm9vdCBDQSBDZXJ0aWZpY2F0aW9uIFBvbGljaWVzIGFuZCBDZXJ0aWZpY2F0ZSBQcmFjdGljZSBTdGF0ZW1lbnQwbQYIKwYBBQUHAQEEYTBfMCYGCCsGAQUFBzABhhpodHRwOi8vb2IudHJ1c3Rpcy5jb20vb2NzcDA1BggrBgEFBQcwAoYpaHR0cDovL29iLnRydXN0aXMuY29tL29iX3BwX2lzc3VpbmdjYS5jcnQwOgYDVR0fBDMwMTAvoC2gK4YpaHR0cDovL29iLnRydXN0aXMuY29tL29iX3BwX2lzc3VpbmdjYS5jcmwwHwYDVR0jBBgwFoAUUHORxiFy03f0/gASBoFceXluP1AwHQYDVR0OBBYEFEDCcD9DKG/GDmRqKInVaycbBXqBMA0GCSqGSIb3DQEBCwUAA4IBAQBiQCbu2aSj28pAIO+Cf36ELT9ATWwR6kTCxgUoYHxh3G2uCn4ocOE1Nzl/sSnSVTcp8O2CdeYcRWXfj5jP4jpIL/zkpC1CD1VWKWNJJF2C3RMPlY/sheHhUFB3dCPTZDDChA09gEWSHVFxdIA64/wWTWutOwNZbF5iD+QXYkarMBE4Ake/2Yoeno5HWtJTc/Sgm9EKj7SDvYuLouNWIrw1/W+F52eFeyRSKLPCmPUV8iz3vRRb8jfRTEFeBzDMy4GGIEKmV9HYkDDEiB1y0RO2GU2PquFMaNlN5I1a9YgkCQBNeWJMCXYuuBSZ545dwXgfeNwZ3a89IWaKYtwM6g2N"
-	],
-	"x5t": "bnKJSxqsULiTOPSwNIqMX0xzgcU",
-	"x5t#S256": "SCfnsU0X0cxg_11iOLMqWuy2d5wMnIYIM9bDOsfCRRU"
-}`
-
-	var jwk2 JSONWebKey
-	err := jwk2.UnmarshalJSON([]byte(hexJWK))
-	require.NoError(t, err)
-
-	js, err := jwk2.MarshalJSON()
-	require.NoError(t, err)
-
-	var j1, j2 map[string]interface{}
-	require.NoError(t, json.Unmarshal(js, &j1))
-	require.NoError(t, json.Unmarshal([]byte(output), &j2))
-	require.Empty(t, cmp.Diff(j1, j2))
-}
-
 func TestKeyMismatchX509(t *testing.T) {
 	x5tSHA1 := sha1.Sum(testCertificates[0].Raw)
 	x5tSHA256 := sha256.Sum256(testCertificates[0].Raw)
@@ -714,6 +679,15 @@ func TestWebKeyVectorsInvalid(t *testing.T) {
 	}
 }
 
+// TestJWKUnsupported checks for an error when parsing a JWK with an unsupported key type.
+func TestJWKUnsupported(t *testing.T) {
+	var jwk JSONWebKey
+	err := jwk.UnmarshalJSON([]byte(`{"kty": "XXX"}`))
+	if !errors.Is(err, ErrUnsupportedKeyType) {
+		t.Error("expected ErrUnsupportedKeyType, got:", err)
+	}
+}
+
 // Test vectors from RFC 7520
 var cookbookJWKs = []string{
 	// EC Public
@@ -862,9 +836,9 @@ func TestEd25519Serialization(t *testing.T) {
 		t.Fatal(err)
 	}
 
-	assert.True(t, bytes.Equal(
-		[]byte(jwk.Key.(ed25519.PrivateKey).Public().(ed25519.PublicKey)),
-		[]byte(jwk2.Key.(ed25519.PrivateKey).Public().(ed25519.PublicKey))))
+	assert.EqualSlice(t,
+		jwk.Key.(ed25519.PrivateKey).Public().(ed25519.PublicKey),
+		jwk2.Key.(ed25519.PrivateKey).Public().(ed25519.PublicKey))
 }
 
 type fakeOpaqueSigner struct {
diff --git jws.go jws.go
index e37007d..c40bd3e 100644
--- jws.go
+++ jws.go
@@ -23,7 +23,7 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/go-jose/go-jose/v3/json"
+	"github.com/go-jose/go-jose/v4/json"
 )
 
 // rawJSONWebSignature represents a raw JWS JSON object. Used for parsing/serializing.
@@ -75,22 +75,64 @@ type Signature struct {
 	original  *rawSignatureInfo
 }
 
-// ParseSigned parses a signed message in compact or JWS JSON Serialization format.
-func ParseSigned(signature string) (*JSONWebSignature, error) {
+// ParseSigned parses a signed message in JWS Compact or JWS JSON Serialization. Validation fails if
+// the JWS is signed with an algorithm that isn't in the provided list of signature algorithms.
+// Applications should decide for themselves which signature algorithms are acceptable. If you're
+// not sure which signature algorithms your application might receive, consult the documentation of
+// the program which provides them or the protocol that you are implementing. You can also try
+// getting an example JWS and decoding it with a tool like https://jwt.io to see what its "alg"
+// header parameter indicates. The signature on the JWS does not get validated during parsing. Call
+// Verify() after parsing to validate the signature and obtain the payload.
+//
+// https://datatracker.ietf.org/doc/html/rfc7515#section-7
+func ParseSigned(
+	signature string,
+	signatureAlgorithms []SignatureAlgorithm,
+) (*JSONWebSignature, error) {
 	signature = stripWhitespace(signature)
 	if strings.HasPrefix(signature, "{") {
-		return parseSignedFull(signature)
+		return ParseSignedJSON(signature, signatureAlgorithms)
 	}
 
-	return parseSignedCompact(signature, nil)
+	return parseSignedCompact(signature, nil, signatureAlgorithms)
+}
+
+// ParseSignedCompact parses a message in JWS Compact Serialization. Validation fails if the JWS is
+// signed with an algorithm that isn't in the provided list of signature algorithms. Applications
+// should decide for themselves which signature algorithms are acceptable.If you're not sure which
+// signature algorithms your application might receive, consult the documentation of the program
+// which provides them or the protocol that you are implementing. You can also try getting an
+// example JWS and decoding it with a tool like https://jwt.io to see what its "alg" header
+// parameter indicates. The signature on the JWS does not get validated during parsing. Call
+// Verify() after parsing to validate the signature and obtain the payload.
+//
+// https://datatracker.ietf.org/doc/html/rfc7515#section-7.1
+func ParseSignedCompact(
+	signature string,
+	signatureAlgorithms []SignatureAlgorithm,
+) (*JSONWebSignature, error) {
+	return parseSignedCompact(signature, nil, signatureAlgorithms)
 }
 
 // ParseDetached parses a signed message in compact serialization format with detached payload.
-func ParseDetached(signature string, payload []byte) (*JSONWebSignature, error) {
+// Validation fails if the JWS is signed with an algorithm that isn't in the provided list of
+// signature algorithms. Applications should decide for themselves which signature algorithms are
+// acceptable. If you're not sure which signature algorithms your application might receive, consult
+// the documentation of the program which provides them or the protocol that you are implementing.
+// You can also try getting an example JWS and decoding it with a tool like https://jwt.io to see
+// what its "alg" header parameter indicates. The signature on the JWS does not get validated during
+// parsing. Call Verify() after parsing to validate the signature and obtain the payload.
+//
+// https://datatracker.ietf.org/doc/html/rfc7515#appendix-F
+func ParseDetached(
+	signature string,
+	payload []byte,
+	signatureAlgorithms []SignatureAlgorithm,
+) (*JSONWebSignature, error) {
 	if payload == nil {
 		return nil, errors.New("go-jose/go-jose: nil payload")
 	}
-	return parseSignedCompact(stripWhitespace(signature), payload)
+	return parseSignedCompact(stripWhitespace(signature), payload, signatureAlgorithms)
 }
 
 // Get a header value
@@ -137,19 +179,55 @@ func (obj JSONWebSignature) computeAuthData(payload []byte, signature *Signature
 	return authData.Bytes(), nil
 }
 
-// parseSignedFull parses a message in full format.
-func parseSignedFull(input string) (*JSONWebSignature, error) {
+// ParseSignedJSON parses a message in JWS JSON Serialization.
+//
+// https://datatracker.ietf.org/doc/html/rfc7515#section-7.2
+func ParseSignedJSON(
+	input string,
+	signatureAlgorithms []SignatureAlgorithm,
+) (*JSONWebSignature, error) {
 	var parsed rawJSONWebSignature
 	err := json.Unmarshal([]byte(input), &parsed)
 	if err != nil {
 		return nil, err
 	}
 
-	return parsed.sanitized()
+	return parsed.sanitized(signatureAlgorithms)
+}
+
+func containsSignatureAlgorithm(haystack []SignatureAlgorithm, needle SignatureAlgorithm) bool {
+	for _, algorithm := range haystack {
+		if algorithm == needle {
+			return true
+		}
+	}
+	return false
+}
+
+// ErrUnexpectedSignatureAlgorithm is returned when the signature algorithm in
+// the JWS header does not match one of the expected algorithms.
+type ErrUnexpectedSignatureAlgorithm struct {
+	// Got is the signature algorithm found in the JWS header.
+	Got      SignatureAlgorithm
+	expected []SignatureAlgorithm
+}
+
+func (e *ErrUnexpectedSignatureAlgorithm) Error() string {
+	return fmt.Sprintf("unexpected signature algorithm %q; expected %q", e.Got, e.expected)
+}
+
+func newErrUnexpectedSignatureAlgorithm(got SignatureAlgorithm, expected []SignatureAlgorithm) error {
+	return &ErrUnexpectedSignatureAlgorithm{
+		Got:      got,
+		expected: expected,
+	}
 }
 
 // sanitized produces a cleaned-up JWS object from the raw JSON.
-func (parsed *rawJSONWebSignature) sanitized() (*JSONWebSignature, error) {
+func (parsed *rawJSONWebSignature) sanitized(signatureAlgorithms []SignatureAlgorithm) (*JSONWebSignature, error) {
+	if len(signatureAlgorithms) == 0 {
+		return nil, errors.New("go-jose/go-jose: no signature algorithms specified")
+	}
 	if parsed.Payload == nil {
 		return nil, fmt.Errorf("go-jose/go-jose: missing payload in JWS message")
 	}
@@ -198,6 +276,11 @@ func (parsed *rawJSONWebSignature) sanitized() (*JSONWebSignature, error) {
 			return nil, err
 		}
 
+		alg := SignatureAlgorithm(signature.Header.Algorithm)
+		if !containsSignatureAlgorithm(signatureAlgorithms, alg) {
+			return nil, newErrUnexpectedSignatureAlgorithm(alg, signatureAlgorithms)
+		}
+
 		if signature.header != nil {
 			signature.Unprotected, err = signature.header.sanitized()
 			if err != nil {
@@ -241,6 +324,11 @@ func (parsed *rawJSONWebSignature) sanitized() (*JSONWebSignature, error) {
 			return nil, err
 		}
 
+		alg := SignatureAlgorithm(obj.Signatures[i].Header.Algorithm)
+		if !containsSignatureAlgorithm(signatureAlgorithms, alg) {
+			return nil, newErrUnexpectedSignatureAlgorithm(alg, signatureAlgorithms)
+		}
+
 		if obj.Signatures[i].header != nil {
 			obj.Signatures[i].Unprotected, err = obj.Signatures[i].header.sanitized()
 			if err != nil {
@@ -273,30 +361,43 @@ func (parsed *rawJSONWebSignature) sanitized() (*JSONWebSignature, error) {
 	return obj, nil
 }
 
+const tokenDelim = "."
+
 // parseSignedCompact parses a message in compact format.
-func parseSignedCompact(input string, payload []byte) (*JSONWebSignature, error) {
-	parts := strings.Split(input, ".")
-	if len(parts) != 3 {
+func parseSignedCompact(
+	input string,
+	payload []byte,
+	signatureAlgorithms []SignatureAlgorithm,
+) (*JSONWebSignature, error) {
+	protected, s, ok := strings.Cut(input, tokenDelim)
+	if !ok { // no period found
+		return nil, fmt.Errorf("go-jose/go-jose: compact JWS format must have three parts")
+	}
+	claims, sig, ok := strings.Cut(s, tokenDelim)
+	if !ok { // only one period found
+		return nil, fmt.Errorf("go-jose/go-jose: compact JWS format must have three parts")
+	}
+	if strings.ContainsRune(sig, '.') { // too many periods found
 		return nil, fmt.Errorf("go-jose/go-jose: compact JWS format must have three parts")
 	}
 
-	if parts[1] != "" && payload != nil {
+	if claims != "" && payload != nil {
 		return nil, fmt.Errorf("go-jose/go-jose: payload is not detached")
 	}
 
-	rawProtected, err := base64URLDecode(parts[0])
+	rawProtected, err := base64.RawURLEncoding.DecodeString(protected)
 	if err != nil {
 		return nil, err
 	}
 
 	if payload == nil {
-		payload, err = base64URLDecode(parts[1])
+		payload, err = base64.RawURLEncoding.DecodeString(claims)
 		if err != nil {
 			return nil, err
 		}
 	}
 
-	signature, err := base64URLDecode(parts[2])
+	signature, err := base64.RawURLEncoding.DecodeString(sig)
 	if err != nil {
 		return nil, err
 	}
@@ -306,7 +407,7 @@ func parseSignedCompact(input string, payload []byte) (*JSONWebSignature, error)
 		Protected: newBuffer(rawProtected),
 		Signature: newBuffer(signature),
 	}
-	return raw.sanitized()
+	return raw.sanitized(signatureAlgorithms)
 }
 
 func (obj JSONWebSignature) compactSerialize(detached bool) (string, error) {
diff --git jws_test.go jws_test.go
index 9b8700c..84c4a12 100644
--- jws_test.go
+++ jws_test.go
@@ -19,10 +19,11 @@ package jose
 import (
 	"crypto/x509"
 	"encoding/base64"
+	"errors"
 	"strings"
 	"testing"
 
-	"github.com/stretchr/testify/assert"
+	"github.com/go-jose/go-jose/v4/testutils/assert"
 )
 
 const trustedCA = `
@@ -86,7 +87,7 @@ func TestEmbeddedHMAC(t *testing.T) {
 	// protected: {"alg":"HS256", "jwk":{"kty":"oct", "k":"MTEx"}}, aka HMAC key.
 	msg := `{"payload":"TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ","protected":"eyJhbGciOiJIUzI1NiIsICJqd2siOnsia3R5Ijoib2N0IiwgImsiOiJNVEV4In19","signature":"lvo41ZZsuHwQvSh0uJtEXRR3vmuBJ7in6qMoD7p9jyo"}`
 
-	_, err := ParseSigned(msg)
+	_, err := ParseSigned(msg, []SignatureAlgorithm{HS256})
 	if err == nil {
 		t.Error("should not allow parsing JWS with embedded JWK with HMAC key")
 	}
@@ -95,14 +96,14 @@ func TestEmbeddedHMAC(t *testing.T) {
 func TestCompactParseJWS(t *testing.T) {
 	// Should parse
 	msg := "eyJhbGciOiJYWVoifQ.cGF5bG9hZA.c2lnbmF0dXJl"
-	_, err := ParseSigned(msg)
+	_, err := ParseSigned(msg, []SignatureAlgorithm{SignatureAlgorithm("XYZ")})
 	if err != nil {
 		t.Error("Unable to parse valid message:", err)
 	}
 
 	// Should parse (detached signature missing payload)
 	msg = "eyJhbGciOiJYWVoifQ..c2lnbmF0dXJl"
-	_, err = ParseSigned(msg)
+	_, err = ParseSigned(msg, []SignatureAlgorithm{SignatureAlgorithm("XYZ")})
 	if err != nil {
 		t.Error("Unable to parse valid message:", err)
 	}
@@ -119,10 +120,13 @@ func TestCompactParseJWS(t *testing.T) {
 		"////.eyJhbGciOiJYWVoifQ.c2lnbmF0dXJl",
 		// Invalid header
 		"cGF5bG9hZA.cGF5bG9hZA.c2lnbmF0dXJl",
+		// Too many parts
+		"eyJhbGciOiJYWVoifQ.cGF5bG9hZA.c2lnbmF0dXJl.....................................................",
+		"eyJhbGciOiJYWVoifQ.cGF5bG9hZA.c2lnbmF0dXJl.cGF5bG9hZA.cGF5bG9hZA.cGF5bG9hZA....................",
 	}
 
 	for i := range failures {
-		_, err = ParseSigned(failures[i])
+		_, err = ParseSigned(failures[i], []SignatureAlgorithm{SignatureAlgorithm("XYZ")})
 		if err == nil {
 			t.Error("Able to parse invalid message")
 		}
@@ -132,13 +136,19 @@ func TestCompactParseJWS(t *testing.T) {
 func TestFullParseJWS(t *testing.T) {
 	// Messages that should succeed to parse
 	successes := []string{
-		"{\"payload\":\"CUJD\",\"signatures\":[{\"protected\":\"e30\",\"header\":{\"kid\":\"XYZ\"},\"signature\":\"CUJD\"},{\"protected\":\"e30\",\"signature\":\"CUJD\"}]}",
+		`{
+		  "header":{"alg":"XYZ"},
+		  "payload":"CUJD",
+		  "signatures":[
+			{"protected":"eyJhbGciOiJBQkMifQo","header":{"kid":"XYZ"},"signature":"CUJD"},
+			{"protected":"eyJhbGciOiJBQkMifQo","signature":"CUJD"}
+		  ]}`,
 	}
 
 	for i := range successes {
-		_, err := ParseSigned(successes[i])
+		_, err := ParseSigned(successes[i], []SignatureAlgorithm{SignatureAlgorithm("ABC")})
 		if err != nil {
-			t.Error("Unble to parse valid message", err, successes[i])
+			t.Error("Unable to parse valid message", err, successes[i])
 		}
 	}
 
@@ -161,7 +171,7 @@ func TestFullParseJWS(t *testing.T) {
 	}
 
 	for i := range failures {
-		_, err := ParseSigned(failures[i])
+		_, err := ParseSigned(failures[i], []SignatureAlgorithm{SignatureAlgorithm("XYZ")})
 		if err == nil {
 			t.Error("Able to parse invalid message", err, failures[i])
 		}
@@ -177,7 +187,7 @@ func TestRejectUnprotectedJWSNonce(t *testing.T) {
 		"payload": "does-not-matter",
 		"signature": "does-not-matter"
 	}`
-	_, err := ParseSigned(input)
+	_, err := ParseSigned(input, []SignatureAlgorithm{SignatureAlgorithm("XYZ")})
 	if err == nil {
 		t.Error("JWS with an unprotected nonce parsed as valid.")
 	} else if err != ErrUnprotectedNonce {
@@ -192,7 +202,7 @@ func TestRejectUnprotectedJWSNonce(t *testing.T) {
 			"signature": "does-not-matter"
 		}]
 	}`
-	_, err = ParseSigned(input)
+	_, err = ParseSigned(input, []SignatureAlgorithm{SignatureAlgorithm("XYZ")})
 	if err == nil {
 		t.Error("JWS with an unprotected nonce parsed as valid.")
 	} else if err != ErrUnprotectedNonce {
@@ -214,7 +224,7 @@ func TestVerifyFlattenedWithIncludedUnprotectedKey(t *testing.T) {
 			"signature": "hRt2eYqBd_MyMRNIh8PEIACoFtmBi7BHTLBaAhpSU6zyDAFdEBaX7us4VB9Vo1afOL03Q8iuoRA0AT4akdV_mQTAQ_jhTcVOAeXPr0tB8b8Q11UPQ0tXJYmU4spAW2SapJIvO50ntUaqU05kZd0qw8-noH1Lja-aNnU-tQII4iYVvlTiRJ5g8_CADsvJqOk6FcHuo2mG643TRnhkAxUtazvHyIHeXMxydMMSrpwUwzMtln4ZJYBNx4QGEq6OhpAD_VSp-w8Lq5HOwGQoNs0bPxH1SGrArt67LFQBfjlVr94E1sn26p4vigXm83nJdNhWAMHHE9iV67xN-r29LT-FjA"
 	}`
 
-	jws, err := ParseSigned(input)
+	jws, err := ParseSigned(input, []SignatureAlgorithm{RS256})
 	if err != nil {
 		t.Fatal("Unable to parse valid message", err)
 	}
@@ -253,7 +263,7 @@ func TestDetachedVerifyJWS(t *testing.T) {
 	}
 
 	for _, msg := range sampleMessages {
-		obj, err := ParseSigned(msg)
+		obj, err := ParseSigned(msg, []SignatureAlgorithm{RS256, RS384})
 		if err != nil {
 			t.Error("unable to parse message", msg, err)
 			continue
@@ -279,7 +289,7 @@ func TestVerifyFlattenedWithPrivateProtected(t *testing.T) {
 	// Base64-decoded, it's '{"nonce":"8HIepUNFZUa-exKTrXVf4g"}'
 	input := `{"header":{"alg":"RS256","jwk":{"kty":"RSA","n":"7ixeydcbxxppzxrBphrW1atUiEZqTpiHDpI-79olav5XxAgWolHmVsJyxzoZXRxmtED8PF9-EICZWBGdSAL9ZTD0hLUCIsPcpdgT_LqNW3Sh2b2caPL2hbMF7vsXvnCGg9varpnHWuYTyRrCLUF9vM7ES-V3VCYTa7LcCSRm56Gg9r19qar43Z9kIKBBxpgt723v2cC4bmLmoAX2s217ou3uCpCXGLOeV_BesG4--Nl3pso1VhCfO85wEWjmW6lbv7Kg4d7Jdkv5DjDZfJ086fkEAYZVYGRpIgAvJBH3d3yKDCrSByUEud1bWuFjQBmMaeYOrVDXO_mbYg5PwUDMhw","e":"AQAB"}},"protected":"eyJub25jZSI6IjhISWVwVU5GWlVhLWV4S1RyWFZmNGcifQ","payload":"eyJjb250YWN0IjpbIm1haWx0bzpmb29AYmFyLmNvbSJdfQ","signature":"AyvVGMgXsQ1zTdXrZxE_gyO63pQgotL1KbI7gv6Wi8I7NRy0iAOkDAkWcTQT9pcCYApJ04lXfEDZfP5i0XgcFUm_6spxi5mFBZU-NemKcvK9dUiAbXvb4hB3GnaZtZiuVnMQUb_ku4DOaFFKbteA6gOYCnED_x7v0kAPHIYrQnvIa-KZ6pTajbV9348zgh9TL7NgGIIsTcMHd-Jatr4z1LQ0ubGa8tS300hoDhVzfoDQaEetYjCo1drR1RmdEN1SIzXdHOHfubjA3ZZRbrF_AJnNKpRRoIwzu1VayOhRmdy1qVSQZq_tENF4VrQFycEL7DhG7JLoXC4T2p1urwMlsw"}`
 
-	jws, err := ParseSigned(input)
+	jws, err := ParseSigned(input, []SignatureAlgorithm{RS256})
 	if err != nil {
 		t.Error("Unable to parse valid message.")
 	}
@@ -323,7 +333,7 @@ func TestSampleNimbusJWSMessagesRSA(t *testing.T) {
 	}
 
 	for _, msg := range rsaSampleMessages {
-		obj, err := ParseSigned(msg)
+		obj, err := ParseSigned(msg, []SignatureAlgorithm{RS256, RS384, RS512, PS256})
 		if err != nil {
 			t.Error("unable to parse message", msg, err)
 			continue
@@ -363,7 +373,7 @@ func TestSampleNimbusJWSMessagesEC(t *testing.T) {
 	}
 
 	for i, msg := range ecSampleMessages {
-		obj, err := ParseSigned(msg)
+		obj, err := ParseSigned(msg, []SignatureAlgorithm{ES256, ES384, ES512})
 		if err != nil {
 			t.Error("unable to parse message", msg, err)
 			continue
@@ -379,37 +389,10 @@ func TestSampleNimbusJWSMessagesEC(t *testing.T) {
 	}
 }
 
-// Test vectors generated with nimbus-jose-jwt
-func TestSampleNimbusJWSMessagesHMAC(t *testing.T) {
-	hmacTestKey := fromHexBytes("DF1FA4F36FFA7FC42C81D4B3C033928D")
-
-	hmacSampleMessages := []string{
-		"eyJhbGciOiJIUzI1NiJ9.TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ.W5tc_EUhxexcvLYEEOckyyvdb__M5DQIVpg6Nmk1XGM",
-		"eyJhbGciOiJIUzM4NCJ9.TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ.sBu44lXOJa4Nd10oqOdYH2uz3lxlZ6o32QSGHaoGdPtYTDG5zvSja6N48CXKqdAh",
-		"eyJhbGciOiJIUzUxMiJ9.TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ.M0yR4tmipsORIix-BitIbxEPGaxPchDfj8UNOpKuhDEfnb7URjGvCKn4nOlyQ1z9mG1FKbwnqR1hOVAWSzAU_w",
-	}
-
-	for _, msg := range hmacSampleMessages {
-		obj, err := ParseSigned(msg)
-		if err != nil {
-			t.Error("unable to parse message", msg, err)
-			continue
-		}
-		payload, err := obj.Verify(hmacTestKey)
-		if err != nil {
-			t.Error("unable to verify message", msg, err)
-			continue
-		}
-		if string(payload) != "Lorem ipsum dolor sit amet" {
-			t.Error("payload is not what we expected for msg", msg)
-		}
-	}
-}
-
 func TestHeaderFieldsCompact(t *testing.T) {
 	msg := "eyJhbGciOiJFUzUxMiJ9.TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ.AeYNFC1rwIgQv-5fwd8iRyYzvTaSCYTEICepgu9gRId-IW99kbSVY7yH0MvrQnqI-a0L8zwKWDR35fW5dukPAYRkADp3Y1lzqdShFcEFziUVGo46vqbiSajmKFrjBktJcCsfjKSaLHwxErF-T10YYPCQFHWb2nXJOOI3CZfACYqgO84g"
 
-	obj, err := ParseSigned(msg)
+	obj, err := ParseSigned(msg, []SignatureAlgorithm{ES512})
 	if err != nil {
 		t.Fatal("unable to parse message", msg, err)
 	}
@@ -427,7 +410,7 @@ func TestHeaderFieldsCompact(t *testing.T) {
 func TestHeaderFieldsFull(t *testing.T) {
 	msg := `{"payload":"TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ","protected":"eyJhbGciOiJFUzUxMiJ9","header":{"custom":"test"},"signature":"AeYNFC1rwIgQv-5fwd8iRyYzvTaSCYTEICepgu9gRId-IW99kbSVY7yH0MvrQnqI-a0L8zwKWDR35fW5dukPAYRkADp3Y1lzqdShFcEFziUVGo46vqbiSajmKFrjBktJcCsfjKSaLHwxErF-T10YYPCQFHWb2nXJOOI3CZfACYqgO84g"}`
 
-	obj, err := ParseSigned(msg)
+	obj, err := ParseSigned(msg, []SignatureAlgorithm{ES512})
 	if err != nil {
 		t.Fatal("unable to parse message", msg, err)
 	}
@@ -445,9 +428,8 @@ func TestHeaderFieldsFull(t *testing.T) {
 	}
 }
 
-// Test vectors generated with nimbus-jose-jwt
 func TestErrorMissingPayloadJWS(t *testing.T) {
-	_, err := (&rawJSONWebSignature{}).sanitized()
+	_, err := (&rawJSONWebSignature{}).sanitized([]SignatureAlgorithm{RS256})
 	if err == nil {
 		t.Error("was able to parse message with missing payload")
 	}
@@ -456,6 +438,60 @@ func TestErrorMissingPayloadJWS(t *testing.T) {
 	}
 }
 
+func TestErrorUnexpectedSignatureAlgorithmInProtected(t *testing.T) {
+	// protected: {"alg":"HS256", "jwk":{"kty":"oct", "k":"MTEx"}}
+	msg := `{"payload":"TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ","protected":"eyJhbGciOiJIUzI1NiIsICJqd2siOnsia3R5Ijoib2N0IiwgImsiOiJNVEV4In19","signature":"lvo41ZZsuHwQvSh0uJtEXRR3vmuBJ7in6qMoD7p9jyo"}`
+
+	_, err := ParseSigned(msg, []SignatureAlgorithm{ES256})
+	if err == nil {
+		t.Fatal("was able to parse message with unexpected signature algorithm")
+	}
+	var errUnexpectedSigAlg *ErrUnexpectedSignatureAlgorithm
+	if !errors.As(err, &errUnexpectedSigAlg) {
+		t.Fatal("unexpected error type, should be UnsupportedAlgorithmError")
+	}
+	if errUnexpectedSigAlg.Got != HS256 {
+		t.Fatalf("unexpected algo should be HS256, got: %s", errUnexpectedSigAlg.Got)
+	}
+	if len(errUnexpectedSigAlg.expected) != 1 {
+		t.Fatalf("expected algo should be a single algo, got: %d", len(errUnexpectedSigAlg.expected))
+	}
+	if errUnexpectedSigAlg.expected[0] != ES256 {
+		t.Fatalf("expected algo should be ES256, got: %s", errUnexpectedSigAlg.expected)
+	}
+}
+
+func TestErrorUnexpectedSignatureAlgorithmInSignatures(t *testing.T) {
+	// protected: {"alg":"HS256", "jwk":{"kty":"oct", "k":"MTEx"}}
+	msg := `{
+		"payload":"TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQ",
+		"signatures":[
+			{
+				"protected":"eyJhbGciOiJIUzI1NiIsICJqd2siOnsia3R5Ijoib2N0IiwgImsiOiJNVEV4In19",
+				"signature":"lvo41ZZsuHwQvSh0uJtEXRR3vmuBJ7in6qMoD7p9jyo"
+			}
+		]
+	}`
+
+	_, err := ParseSigned(msg, []SignatureAlgorithm{ES256})
+	if err == nil {
+		t.Fatal("was able to parse message with unexpected signature algorithm")
+	}
+	var errUnexpectedSigAlg *ErrUnexpectedSignatureAlgorithm
+	if !errors.As(err, &errUnexpectedSigAlg) {
+		t.Fatal("unexpected error type, should be UnsupportedAlgorithmError")
+	}
+	if errUnexpectedSigAlg.Got != HS256 {
+		t.Fatalf("unexpected algo should be HS256, got: %s", errUnexpectedSigAlg.Got)
+	}
+	if len(errUnexpectedSigAlg.expected) != 1 {
+		t.Fatalf("expected algo should be a single algo, got: %d", len(errUnexpectedSigAlg.expected))
+	}
+	if errUnexpectedSigAlg.expected[0] != ES256 {
+		t.Fatalf("expected algo should be ES256, got: %s", errUnexpectedSigAlg.expected)
+	}
+}
+
 // Test that a null value in the header doesn't panic
 func TestNullHeaderValue(t *testing.T) {
 	msg := `{
@@ -475,7 +511,7 @@ func TestNullHeaderValue(t *testing.T) {
 			t.Errorf("ParseSigned panic'd when parsing a message with a null protected header value")
 		}
 	}()
-	if _, err := ParseSigned(msg); err != nil {
+	if _, err := ParseSigned(msg, []SignatureAlgorithm{ES256}); err != nil {
 		t.Fatal(err)
 	}
 }
@@ -518,12 +554,12 @@ func TestEmbedJWKBug(t *testing.T) {
 
 	// Expected output with embed set to true is a JWS with the public JWK embedded, with kid header empty.
 	// Expected output with embed set to false is that we set the kid header for key identification instead.
-	parsed, err := ParseSigned(output)
+	parsed, err := ParseSigned(output, []SignatureAlgorithm{RS256})
 	if err != nil {
 		t.Fatal(err)
 	}
 
-	parsedNoEmbed, err := ParseSigned(outputNoEmbed)
+	parsedNoEmbed, err := ParseSigned(outputNoEmbed, []SignatureAlgorithm{RS256})
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -584,7 +620,7 @@ func TestJWSWithCertificateChain(t *testing.T) {
 			t.Fatal(err)
 		}
 
-		parsed, err := ParseSigned(signed.FullSerialize())
+		parsed, err := ParseSigned(signed.FullSerialize(), []SignatureAlgorithm{RS256})
 		if err != nil {
 			t.Fatal(err)
 		}
@@ -623,7 +659,7 @@ func TestDetachedCompactSerialization(t *testing.T) {
 	msg := "eyJhbGciOiJSUzI1NiJ9.JC4wMg.W5tc_EUhxexcvLYEEOckyyvdb__M5DQIVpg6Nmk1XGM"
 	exp := "eyJhbGciOiJSUzI1NiJ9..W5tc_EUhxexcvLYEEOckyyvdb__M5DQIVpg6Nmk1XGM"
 
-	obj, err := ParseSigned(msg)
+	obj, err := ParseSigned(msg, []SignatureAlgorithm{RS256})
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -637,7 +673,7 @@ func TestDetachedCompactSerialization(t *testing.T) {
 		t.Fatalf("got '%s', expected '%s'", ser, exp)
 	}
 
-	obj, err = ParseDetached(ser, []byte("$.02"))
+	obj, err = ParseDetached(ser, []byte("$.02"), []SignatureAlgorithm{RS256})
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -661,7 +697,9 @@ func TestJWSComputeAuthDataBase64(t *testing.T) {
 		},
 	})
 	// Invalid header, should return error
-	assert.NotNil(t, err)
+	if err == nil {
+		t.Errorf("expected error when computing auth data for invalid signature")
+	}
 
 	payload := []byte{0x01}
 	encodedPayload := base64.RawURLEncoding.EncodeToString(payload)
@@ -674,7 +712,8 @@ func TestJWSComputeAuthDataBase64(t *testing.T) {
 			Protected: b64TrueHeader,
 		},
 	})
-	assert.Nil(t, err)
+	assert.NoError(t, err, "computing auth data for \"b64\": true")
+
 	// Payload should be b64 encoded
 	assert.Len(t, data, len(b64TrueHeader.base64())+len(encodedPayload)+1)
 
@@ -683,7 +722,43 @@ func TestJWSComputeAuthDataBase64(t *testing.T) {
 			Protected: b64FalseHeader,
 		},
 	})
-	assert.Nil(t, err)
+	assert.NoError(t, err, "computing auth data for \"b64\": false")
 	// Payload should *not* be b64 encoded
 	assert.Len(t, data, len(b64FalseHeader.base64())+len(payload)+1)
 }
+
+func TestInvalidHMACKeySize(t *testing.T) {
+	s, err := NewSigner(SigningKey{
+		Key:       make([]byte, 31),
+		Algorithm: HS256,
+	}, nil)
+	assert.NoError(t, err)
+	_, err = s.Sign([]byte("Lorem ipsum dolor sit amet"))
+	assert.ErrorIs(t, err, ErrInvalidKeySize)
+
+	s, err = NewSigner(SigningKey{
+		Key:       make([]byte, 47),
+		Algorithm: HS384,
+	}, nil)
+	assert.NoError(t, err)
+	_, err = s.Sign([]byte("Lorem ipsum dolor sit amet"))
+	assert.ErrorIs(t, err, ErrInvalidKeySize)
+
+	s, err = NewSigner(SigningKey{
+		Key:       make([]byte, 63),
+		Algorithm: HS512,
+	}, nil)
+	assert.NoError(t, err)
+	_, err = s.Sign([]byte("Lorem ipsum dolor sit amet"))
+	assert.ErrorIs(t, err, ErrInvalidKeySize)
+}
+
+func BenchmarkParseSignedCompat(b *testing.B) {
+	raw := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0In0.OFD0iVfPczqWBA_TRi1jGB5PF699eekcHt4D6qNoimc`
+
+	for range b.N {
+		if _, err := ParseSignedCompact(raw, []SignatureAlgorithm{HS256}); err != nil {
+			panic(err)
+		}
+	}
+}
diff --git jwt/builder.go jwt/builder.go
index 7df270c..d68bb37 100644
--- jwt/builder.go
+++ jwt/builder.go
@@ -21,13 +21,13 @@ import (
 	"bytes"
 	"reflect"
 
-	"github.com/go-jose/go-jose/v3/json"
+	"github.com/go-jose/go-jose/v4/json"
 
-	"github.com/go-jose/go-jose/v3"
+	"github.com/go-jose/go-jose/v4"
 )
 
 // Builder is a utility for making JSON Web Tokens. Calls can be chained, and
-// errors are accumulated until the final call to CompactSerialize/FullSerialize.
+// errors are accumulated until the final call to Serialize.
 type Builder interface {
 	// Claims encodes claims into JWE/JWS form. Multiple calls will merge claims
 	// into single JSON object. If you are passing private claims, make sure to set
@@ -36,15 +36,13 @@ type Builder interface {
 	Claims(i interface{}) Builder
 	// Token builds a JSONWebToken from provided data.
 	Token() (*JSONWebToken, error)
-	// FullSerialize serializes a token using the JWS/JWE JSON Serialization format.
-	FullSerialize() (string, error)
-	// CompactSerialize serializes a token using the compact serialization format.
-	CompactSerialize() (string, error)
+	// Serialize serializes a token.
+	Serialize() (string, error)
 }
 
 // NestedBuilder is a utility for making Signed-Then-Encrypted JSON Web Tokens.
 // Calls can be chained, and errors are accumulated until final call to
-// CompactSerialize/FullSerialize.
+// Serialize.
 type NestedBuilder interface {
 	// Claims encodes claims into JWE/JWS form. Multiple calls will merge claims
 	// into single JSON object. If you are passing private claims, make sure to set
@@ -53,10 +51,8 @@ type NestedBuilder interface {
 	Claims(i interface{}) NestedBuilder
 	// Token builds a NestedJSONWebToken from provided data.
 	Token() (*NestedJSONWebToken, error)
-	// FullSerialize serializes a token using the JSON Serialization format.
-	FullSerialize() (string, error)
-	// CompactSerialize serializes a token using the compact serialization format.
-	CompactSerialize() (string, error)
+	// Serialize serializes a token.
+	Serialize() (string, error)
 }
 
 type builder struct {
@@ -194,7 +190,7 @@ func (b *signedBuilder) Token() (*JSONWebToken, error) {
 	return b.builder.token(sig.Verify, h)
 }
 
-func (b *signedBuilder) CompactSerialize() (string, error) {
+func (b *signedBuilder) Serialize() (string, error) {
 	sig, err := b.sign()
 	if err != nil {
 		return "", err
@@ -203,15 +199,6 @@ func (b *signedBuilder) CompactSerialize() (string, error) {
 	return sig.CompactSerialize()
 }
 
-func (b *signedBuilder) FullSerialize() (string, error) {
-	sig, err := b.sign()
-	if err != nil {
-		return "", err
-	}
-
-	return sig.FullSerialize(), nil
-}
-
 func (b *signedBuilder) sign() (*jose.JSONWebSignature, error) {
 	if b.err != nil {
 		return nil, b.err
@@ -232,7 +219,7 @@ func (b *encryptedBuilder) Claims(i interface{}) Builder {
 	}
 }
 
-func (b *encryptedBuilder) CompactSerialize() (string, error) {
+func (b *encryptedBuilder) Serialize() (string, error) {
 	enc, err := b.encrypt()
 	if err != nil {
 		return "", err
@@ -241,15 +228,6 @@ func (b *encryptedBuilder) CompactSerialize() (string, error) {
 	return enc.CompactSerialize()
 }
 
-func (b *encryptedBuilder) FullSerialize() (string, error) {
-	enc, err := b.encrypt()
-	if err != nil {
-		return "", err
-	}
-
-	return enc.FullSerialize(), nil
-}
-
 func (b *encryptedBuilder) Token() (*JSONWebToken, error) {
 	enc, err := b.encrypt()
 	if err != nil {
@@ -280,6 +258,8 @@ func (b *nestedBuilder) Claims(i interface{}) NestedBuilder {
 	}
 }
 
+// Token produced a token suitable for serialization. It cannot be decrypted
+// without serializing and then deserializing.
 func (b *nestedBuilder) Token() (*NestedJSONWebToken, error) {
 	enc, err := b.signAndEncrypt()
 	if err != nil {
@@ -287,12 +267,13 @@ func (b *nestedBuilder) Token() (*NestedJSONWebToken, error) {
 	}
 
 	return &NestedJSONWebToken{
-		enc:     enc,
-		Headers: []jose.Header{enc.Header},
+		allowedSignatureAlgorithms: nil,
+		enc:                        enc,
+		Headers:                    []jose.Header{enc.Header},
 	}, nil
 }
 
-func (b *nestedBuilder) CompactSerialize() (string, error) {
+func (b *nestedBuilder) Serialize() (string, error) {
 	enc, err := b.signAndEncrypt()
 	if err != nil {
 		return "", err
diff --git jwt/builder_test.go jwt/builder_test.go
index 091315b..65ce4c4 100644
--- jwt/builder_test.go
+++ jwt/builder_test.go
@@ -30,17 +30,21 @@ import (
 	"testing"
 	"time"
 
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
+	"github.com/go-jose/go-jose/v4/testutils/assert"
+	"github.com/go-jose/go-jose/v4/testutils/require"
 
-	"github.com/go-jose/go-jose/v3"
-	"github.com/go-jose/go-jose/v3/json"
+	"github.com/go-jose/go-jose/v4"
+	"github.com/go-jose/go-jose/v4/json"
 )
 
 type testClaims struct {
 	Subject string `json:"sub"`
 }
 
+func (tc1 testClaims) Equal(tc2 testClaims) bool {
+	return tc1.Subject == tc2.Subject
+}
+
 type invalidMarshalClaims struct {
 }
 
@@ -89,10 +93,10 @@ func TestIntegerAndFloatsNormalize(t *testing.T) {
 }
 
 func TestBuilderCustomClaimsNonPointer(t *testing.T) {
-	jwt, err := Signed(rsaSigner).Claims(testClaims{"foo"}).CompactSerialize()
+	jwt, err := Signed(rsaSigner).Claims(testClaims{"foo"}).Serialize()
 	require.NoError(t, err, "Error creating JWT.")
 
-	parsed, err := ParseSigned(jwt)
+	parsed, err := ParseSigned(jwt, []jose.SignatureAlgorithm{jose.RS256})
 	require.NoError(t, err, "Error parsing JWT.")
 
 	out := &testClaims{}
@@ -102,10 +106,10 @@ func TestBuilderCustomClaimsNonPointer(t *testing.T) {
 }
 
 func TestBuilderCustomClaimsPointer(t *testing.T) {
-	jwt, err := Signed(rsaSigner).Claims(&testClaims{"foo"}).CompactSerialize()
+	jwt, err := Signed(rsaSigner).Claims(&testClaims{"foo"}).Serialize()
 	require.NoError(t, err, "Error creating JWT.")
 
-	parsed, err := ParseSigned(jwt)
+	parsed, err := ParseSigned(jwt, []jose.SignatureAlgorithm{jose.RS256})
 	require.NoError(t, err, "Error parsing JWT.")
 
 	out := &testClaims{}
@@ -122,160 +126,114 @@ func TestBuilderMergeClaims(t *testing.T) {
 		Claims(map[string]interface{}{
 			"Scopes": []string{"read:users"},
 		}).
-		CompactSerialize()
+		Serialize()
 	require.NoError(t, err, "Error creating JWT.")
 
-	parsed, err := ParseSigned(jwt)
+	parsed, err := ParseSigned(jwt, []jose.SignatureAlgorithm{jose.RS256})
 	require.NoError(t, err, "Error parsing JWT.")
 
 	out := make(map[string]interface{})
 	if assert.NoError(t, parsed.Claims(&testPrivRSAKey1.PublicKey, &out), "Error unmarshaling claims.") {
-		assert.Equal(t, map[string]interface{}{
+		assert.EqualJSON(t, map[string]interface{}{
 			"sub":    "42",
 			"Scopes": []interface{}{"read:users"},
 		}, out)
 	}
 
-	_, err = Signed(rsaSigner).Claims("invalid-claims").Claims(&testClaims{"foo"}).CompactSerialize()
+	_, err = Signed(rsaSigner).Claims("invalid-claims").Claims(&testClaims{"foo"}).Serialize()
 	assert.Equal(t, err, ErrInvalidClaims)
 
-	_, err = Signed(rsaSigner).Claims(&invalidMarshalClaims{}).CompactSerialize()
-	assert.EqualError(t, err, "json: error calling MarshalJSON for type *jwt.invalidMarshalClaims: failed marshaling invalid claims")
-}
-
-func TestSignedFullSerializeAndToken(t *testing.T) {
-	b := Signed(rsaSigner).Claims(&testClaims{"foo"})
-
-	jwt, err := b.FullSerialize()
-	require.NoError(t, err, "Error creating JWT.")
-	parsed, err := ParseSigned(jwt)
-	require.NoError(t, err, "Error parsing JWT.")
-	out := &testClaims{}
-	if assert.NoError(t, parsed.Claims(&testPrivRSAKey1.PublicKey, &out), "Error unmarshaling claims.") {
-		assert.Equal(t, &testClaims{
-			Subject: "foo",
-		}, out)
-	}
-
-	jwt2, err := b.Token()
-	require.NoError(t, err, "Error creating JWT.")
-	out2 := &testClaims{}
-	if assert.NoError(t, jwt2.Claims(&testPrivRSAKey1.PublicKey, &out2), "Error unmarshaling claims.") {
-		assert.Equal(t, &testClaims{
-			Subject: "foo",
-		}, out2)
-	}
-
-	b2 := Signed(rsaSigner).Claims(&invalidMarshalClaims{})
-	_, err = b2.FullSerialize()
-	require.EqualError(t, err, "json: error calling MarshalJSON for type *jwt.invalidMarshalClaims: failed marshaling invalid claims")
-	_, err = b2.Token()
-	require.EqualError(t, err, "json: error calling MarshalJSON for type *jwt.invalidMarshalClaims: failed marshaling invalid claims")
-}
-
-func TestEncryptedFullSerializeAndToken(t *testing.T) {
-	recipient := jose.Recipient{
-		Algorithm: jose.RSA1_5,
-		Key:       testPrivRSAKey1.Public(),
-	}
-	encrypter, err := jose.NewEncrypter(jose.A128CBC_HS256, recipient, nil)
-	require.NoError(t, err, "Error creating encrypter.")
-
-	b := Encrypted(encrypter).Claims(&testClaims{"foo"})
-
-	jwt, err := b.FullSerialize()
-	require.NoError(t, err, "Error creating JWT.")
-	parsed, err := ParseEncrypted(jwt)
-	require.NoError(t, err, "Error parsing JWT.")
-	out := &testClaims{}
-	if assert.NoError(t, parsed.Claims(testPrivRSAKey1, &out)) {
-		assert.Equal(t, &testClaims{
-			Subject: "foo",
-		}, out)
-	}
-
-	jwt2, err := b.Token()
-	require.NoError(t, err, "Error creating JWT.")
-	out2 := &testClaims{}
-	if assert.NoError(t, jwt2.Claims(testPrivRSAKey1, &out2)) {
-		assert.Equal(t, &testClaims{
-			Subject: "foo",
-		}, out2)
-	}
-
-	b2 := Encrypted(encrypter).Claims(&invalidMarshalClaims{})
-
-	_, err = b2.FullSerialize()
-	require.EqualError(t, err, "json: error calling MarshalJSON for type *jwt.invalidMarshalClaims: failed marshaling invalid claims")
-	_, err = b2.Token()
-	require.EqualError(t, err, "json: error calling MarshalJSON for type *jwt.invalidMarshalClaims: failed marshaling invalid claims")
+	_, err = Signed(rsaSigner).Claims(&invalidMarshalClaims{}).Serialize()
+	assert.Equal(t, err.Error(), "json: error calling MarshalJSON for type *jwt.invalidMarshalClaims: failed marshaling invalid claims")
 }
 
 func TestBuilderSignedAndEncrypted(t *testing.T) {
+	encryptionKey := []byte("itsa16bytesecret" + "itsa16bytesecret")
 	recipient := jose.Recipient{
-		Algorithm: jose.RSA1_5,
-		Key:       testPrivRSAKey1.Public(),
+		Algorithm: jose.DIRECT,
+		Key:       encryptionKey,
 	}
 	encrypter, err := jose.NewEncrypter(jose.A128CBC_HS256, recipient, (&jose.EncrypterOptions{}).WithContentType("JWT").WithType("JWT"))
 	require.NoError(t, err, "Error creating encrypter.")
 
 	jwt1, err := SignedAndEncrypted(rsaSigner, encrypter).Claims(&testClaims{"foo"}).Token()
 	require.NoError(t, err, "Error marshaling signed-then-encrypted token.")
-	if nested, err := jwt1.Decrypt(testPrivRSAKey1); assert.NoError(t, err, "Error decrypting signed-then-encrypted token.") {
+	jwt1Serialized, err := jwt1.enc.CompactSerialize()
+	require.NoError(t, err, "Error serializing signed-then-encrypted token.")
+	jwt1Deserialized, err := ParseSignedAndEncrypted(jwt1Serialized,
+		[]jose.KeyAlgorithm{jose.DIRECT},
+		[]jose.ContentEncryption{jose.A128CBC_HS256},
+		[]jose.SignatureAlgorithm{jose.RS256})
+	require.NoError(t, err, "Error parsing signed-then-encrypted token.")
+	if nested, err := jwt1Deserialized.Decrypt(encryptionKey); assert.NoError(t, err, "Error decrypting signed-then-encrypted token.") {
 		out := &testClaims{}
 		assert.NoError(t, nested.Claims(&testPrivRSAKey1.PublicKey, out))
-		assert.Equal(t, &testClaims{"foo"}, out)
+		assert.Equal(t, testClaims{"foo"}, *out)
 	}
 
 	b := SignedAndEncrypted(rsaSigner, encrypter).Claims(&testClaims{"foo"})
-	tok1, err := b.CompactSerialize()
+	tok1, err := b.Serialize()
 	if assert.NoError(t, err) {
-		jwt, err := ParseSignedAndEncrypted(tok1)
+		jwt, err := ParseSignedAndEncrypted(
+			tok1,
+			[]jose.KeyAlgorithm{jose.DIRECT},
+			[]jose.ContentEncryption{jose.A128CBC_HS256},
+			[]jose.SignatureAlgorithm{jose.RS256})
 		if assert.NoError(t, err, "Error parsing signed-then-encrypted compact token.") {
-			if nested, err := jwt.Decrypt(testPrivRSAKey1); assert.NoError(t, err) {
+			if nested, err := jwt.Decrypt(encryptionKey); assert.NoError(t, err) {
 				out := &testClaims{}
 				assert.NoError(t, nested.Claims(&testPrivRSAKey1.PublicKey, out))
-				assert.Equal(t, &testClaims{"foo"}, out)
+				assert.Equal(t, testClaims{"foo"}, *out)
 			}
 		}
 	}
 
-	tok2, err := b.FullSerialize()
+	tok2, err := b.Serialize()
 	if assert.NoError(t, err) {
-		jwe, err := ParseSignedAndEncrypted(tok2)
+		jwe, err := ParseSignedAndEncrypted(
+			tok2,
+			[]jose.KeyAlgorithm{jose.DIRECT},
+			[]jose.ContentEncryption{jose.A128CBC_HS256},
+			[]jose.SignatureAlgorithm{jose.RS256})
 		if assert.NoError(t, err, "Error parsing signed-then-encrypted full token.") {
-			assert.Equal(t, []jose.Header{{
-				Algorithm: string(jose.RSA1_5),
+			expected := []jose.Header{{
+				Algorithm: string(jose.DIRECT),
 				ExtraHeaders: map[jose.HeaderKey]interface{}{
 					jose.HeaderType:        "JWT",
 					jose.HeaderContentType: "JWT",
 					"enc":                  "A128CBC-HS256",
 				},
-			}}, jwe.Headers)
-			if jws, err := jwe.Decrypt(testPrivRSAKey1); assert.NoError(t, err) {
-				assert.Equal(t, []jose.Header{{
+			}}
+			if !reflect.DeepEqual(jwe.Headers, expected) {
+				t.Errorf("headers from ParseSignedAndEncrypted() = %v, want %v", jwe.Headers, expected)
+			}
+			if jws, err := jwe.Decrypt(encryptionKey); assert.NoError(t, err) {
+				expected := []jose.Header{{
 					Algorithm: string(jose.RS256),
 					ExtraHeaders: map[jose.HeaderKey]interface{}{
 						jose.HeaderType: "JWT",
 					},
-				}}, jws.Headers)
+				}}
+				if !reflect.DeepEqual(jws.Headers, expected) {
+					t.Errorf("headers from Decrypt() = %v, want %v", jws.Headers, expected)
+				}
 				out := &testClaims{}
 				assert.NoError(t, jws.Claims(&testPrivRSAKey1.PublicKey, out))
-				assert.Equal(t, &testClaims{"foo"}, out)
+				assert.Equal(t, testClaims{"foo"}, *out)
 			}
 		}
 	}
 
 	b2 := SignedAndEncrypted(rsaSigner, encrypter).Claims(&invalidMarshalClaims{})
-	_, err = b2.CompactSerialize()
-	assert.EqualError(t, err, "json: error calling MarshalJSON for type *jwt.invalidMarshalClaims: failed marshaling invalid claims")
-	_, err = b2.FullSerialize()
-	assert.EqualError(t, err, "json: error calling MarshalJSON for type *jwt.invalidMarshalClaims: failed marshaling invalid claims")
+	_, err = b2.Serialize()
+	assert.Equal(t, err.Error(), "json: error calling MarshalJSON for type *jwt.invalidMarshalClaims: failed marshaling invalid claims")
+	_, err = b2.Serialize()
+	assert.Equal(t, err.Error(), "json: error calling MarshalJSON for type *jwt.invalidMarshalClaims: failed marshaling invalid claims")
 
 	encrypter2, err := jose.NewEncrypter(jose.A128CBC_HS256, recipient, nil)
 	require.NoError(t, err, "Error creating encrypter.")
-	_, err = SignedAndEncrypted(rsaSigner, encrypter2).CompactSerialize()
-	assert.EqualError(t, err, "go-jose/go-jose/jwt: expected content type to be JWT (cty header)")
+	_, err = SignedAndEncrypted(rsaSigner, encrypter2).Serialize()
+	assert.Equal(t, err.Error(), "go-jose/go-jose/jwt: expected content type to be JWT (cty header)")
 }
 
 func TestBuilderHeadersSigner(t *testing.T) {
@@ -288,7 +246,7 @@ func TestBuilderHeadersSigner(t *testing.T) {
 			Claims: &Claims{Issuer: "foo"},
 		},
 		{
-			Keys:   []*rsa.PrivateKey{testPrivRSAKey1, testPrivRSAKey2},
+			Keys:   []*rsa.PrivateKey{testPrivRSAKey2},
 			Claims: &Claims{Issuer: "foo"},
 		},
 	}
@@ -322,16 +280,12 @@ func TestBuilderHeadersSigner(t *testing.T) {
 		}
 
 		var token string
-		if len(tc.Keys) == 1 {
-			token, err = Signed(signer).Claims(tc.Claims).CompactSerialize()
-		} else {
-			token, err = Signed(signer).Claims(tc.Claims).FullSerialize()
-		}
+		token, err = Signed(signer).Claims(tc.Claims).Serialize()
 		if err != nil {
 			t.Errorf("case %d: failed to create token: %v", i, err)
 			continue
 		}
-		jws, err := jose.ParseSigned(token)
+		jws, err := jose.ParseSignedCompact(token, []jose.SignatureAlgorithm{jose.RS256})
 		if err != nil {
 			t.Errorf("case %d: parse signed: %v", i, err)
 			continue
@@ -369,19 +323,22 @@ func TestBuilderHeadersEncrypter(t *testing.T) {
 	encrypter, err := jose.NewEncrypter(jose.A128CBC_HS256, recipient, (&jose.EncrypterOptions{}).WithType(wantType))
 	require.NoError(t, err, "failed to create encrypter")
 
-	token, err := Encrypted(encrypter).Claims(claims).CompactSerialize()
+	token, err := Encrypted(encrypter).Claims(claims).Serialize()
 	require.NoError(t, err, "failed to create token")
 
-	jwe, err := jose.ParseEncrypted(token)
+	jwe, err := jose.ParseEncrypted(token, []jose.KeyAlgorithm{jose.RSA1_5}, []jose.ContentEncryption{jose.A128CBC_HS256})
 	if assert.NoError(t, err, "error parsing encrypted token") {
-		assert.Equal(t, jose.Header{
+		expected := jose.Header{
 			ExtraHeaders: map[jose.HeaderKey]interface{}{
 				jose.HeaderType: string(wantType),
 				"enc":           "A128CBC-HS256",
 			},
 			Algorithm: string(jose.RSA1_5),
 			KeyID:     wantKeyID,
-		}, jwe.Header)
+		}
+		if !reflect.DeepEqual(jwe.Header, expected) {
+			t.Errorf("header from ParseEncrypted() = %v, want %v", jwe.Header, expected)
+		}
 	}
 }
 
@@ -404,23 +361,23 @@ func BenchmarkStructClaims(b *testing.B) {
 	}
 }
 
-func BenchmarkSignedCompactSerializeRSA(b *testing.B) {
+func BenchmarkSignedSerializeRSA(b *testing.B) {
 	tb := Signed(rsaSigner).Claims(sampleClaims)
 
 	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
-		if _, err := tb.CompactSerialize(); err != nil {
+		if _, err := tb.Serialize(); err != nil {
 			b.Fatal(err)
 		}
 	}
 }
 
-func BenchmarkSignedCompactSerializeSHA(b *testing.B) {
+func BenchmarkSignedSerializeSHA(b *testing.B) {
 	tb := Signed(hmacSigner).Claims(sampleClaims)
 
 	b.ResetTimer()
 	for i := 0; i < b.N; i++ {
-		if _, err := tb.CompactSerialize(); err != nil {
+		if _, err := tb.Serialize(); err != nil {
 			b.Fatal(err)
 		}
 	}
@@ -451,7 +408,7 @@ func mustMakeSigner(alg jose.SignatureAlgorithm, k interface{}) jose.Signer {
 }
 
 var (
-	sharedKey           = []byte("secret")
+	sharedKey           = []byte("0102030405060708090A0B0C0D0E0F10")
 	sharedEncryptionKey = []byte("itsa16bytesecret")
 
 	testPrivRSAKey1 = mustUnmarshalRSA(`-----BEGIN PRIVATE KEY-----
diff --git jwt/claims.go jwt/claims.go
index b2a8dc8..e73412a 100644
--- jwt/claims.go
+++ jwt/claims.go
@@ -21,7 +21,7 @@ import (
 	"strconv"
 	"time"
 
-	"github.com/go-jose/go-jose/v3/json"
+	"github.com/go-jose/go-jose/v4/json"
 )
 
 // Claims represents public claim values (as specified in RFC 7519).
diff --git jwt/claims_test.go jwt/claims_test.go
index db96e0f..a45f736 100644
--- jwt/claims_test.go
+++ jwt/claims_test.go
@@ -21,9 +21,9 @@ import (
 	"testing"
 	"time"
 
-	"github.com/go-jose/go-jose/v3/json"
+	"github.com/go-jose/go-jose/v4/json"
 
-	"github.com/stretchr/testify/assert"
+	"github.com/go-jose/go-jose/v4/testutils/assert"
 )
 
 func TestEncodeClaims(t *testing.T) {
@@ -72,15 +72,19 @@ func TestDecodeClaims(t *testing.T) {
 	if err := json.Unmarshal(s, &c); assert.NoError(t, err) {
 		assert.Equal(t, "issuer", c.Issuer)
 		assert.Equal(t, "subject", c.Subject)
-		assert.Equal(t, Audience{"a1", "a2"}, c.Audience)
-		assert.True(t, now.Equal(c.IssuedAt.Time()))
-		assert.True(t, now.Add(1*time.Hour).Equal(c.Expiry.Time()))
+		assert.EqualSlice(t, Audience{"a1", "a2"}, c.Audience)
+		if !now.Equal(c.IssuedAt.Time()) {
+			t.Errorf("IssuedAt = %s, want %s", c.IssuedAt.Time(), now)
+		}
+		if !now.Add(1 * time.Hour).Equal(c.Expiry.Time()) {
+			t.Errorf("Expiry = %s, want %s", c.Expiry.Time(), now.Add(1*time.Hour))
+		}
 	}
 
 	s2 := []byte(`{"aud": "a1"}`)
 	c2 := Claims{}
 	if err := json.Unmarshal(s2, &c2); assert.NoError(t, err) {
-		assert.Equal(t, Audience{"a1"}, c2.Audience)
+		assert.EqualSlice(t, Audience{"a1"}, c2.Audience)
 	}
 
 	invalid := []struct {
@@ -100,14 +104,20 @@ func TestDecodeClaims(t *testing.T) {
 
 func TestNumericDate(t *testing.T) {
 	zeroDate := NewNumericDate(time.Time{})
-	assert.True(t, time.Time{}.Equal(zeroDate.Time()), "Expected derived time to be time.Time{}")
+	if !zeroDate.Time().Equal(time.Time{}) {
+		t.Errorf("zeroDate.Time() = %s, want %s", zeroDate.Time(), time.Time{})
+	}
 
 	zeroDate2 := (*NumericDate)(nil)
-	assert.True(t, time.Time{}.Equal(zeroDate2.Time()), "Expected derived time to be time.Time{}")
+	if !zeroDate2.Time().Equal(time.Time{}) {
+		t.Errorf("zeroDate2.Time() = %s, want %s", zeroDate2.Time(), time.Time{})
+	}
 
 	nonZeroDate := NewNumericDate(time.Unix(0, 0))
 	expected := time.Date(1970, 1, 1, 0, 0, 0, 0, time.UTC)
-	assert.Truef(t, expected.Equal(nonZeroDate.Time()), "Expected derived time to be %s", expected)
+	if !nonZeroDate.Time().Equal(expected) {
+		t.Errorf("nonZeroDate.Time() = %s, want %s", nonZeroDate.Time(), expected)
+	}
 }
 
 func TestEncodeClaimsTimeValues(t *testing.T) {
@@ -127,8 +137,14 @@ func TestEncodeClaimsTimeValues(t *testing.T) {
 
 	c2 := Claims{}
 	if err := json.Unmarshal(b, &c2); assert.NoError(t, err) {
-		assert.True(t, c.NotBefore.Time().Equal(c2.NotBefore.Time()))
-		assert.True(t, c.IssuedAt.Time().Equal(c2.IssuedAt.Time()))
-		assert.True(t, c.Expiry.Time().Equal(c2.Expiry.Time()))
+		if !c.NotBefore.Time().Equal(c2.NotBefore.Time()) {
+			t.Errorf("c2.NotBefore = %s, want %s", c2.NotBefore.Time(), c.NotBefore.Time())
+		}
+		if !c.IssuedAt.Time().Equal(c2.IssuedAt.Time()) {
+			t.Errorf("c2.IssuedAt = %s, want %s", c2.IssuedAt.Time(), c.IssuedAt.Time())
+		}
+		if !c.Expiry.Time().Equal(c2.Expiry.Time()) {
+			t.Errorf("c2.Expiry = %s, want %s", c2.Expiry.Time(), c.Expiry.Time())
+		}
 	}
 }
diff --git jwt/example_test.go jwt/example_test.go
index 4d9ceea..f814a47 100644
--- jwt/example_test.go
+++ jwt/example_test.go
@@ -26,24 +26,27 @@ import (
 	"crypto/x509"
 	"encoding/pem"
 
-	"github.com/go-jose/go-jose/v3"
-	"github.com/go-jose/go-jose/v3/jwt"
+	"github.com/go-jose/go-jose/v4"
+	"github.com/go-jose/go-jose/v4/jwt"
 )
 
-var sharedKey = []byte("secret")
+var sharedKey = []byte("0102030405060708090A0B0C0D0E0F10")
 var sharedEncryptionKey = []byte("itsa16bytesecret")
 var signer, _ = jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: sharedKey}, &jose.SignerOptions{})
 
 func ExampleParseSigned() {
-	raw := `eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0In0.gpHyA1B1H6X4a4Edm9wo7D3X2v3aLSDBDG2_5BzXYe0`
-	tok, err := jwt.ParseSigned(raw)
+	raw := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0In0.OFD0iVfPczqWBA_TRi1jGB5PF699eekcHt4D6qNoimc`
+
+	tok, err := jwt.ParseSigned(raw, []jose.SignatureAlgorithm{jose.HS256})
 	if err != nil {
-		panic(err)
+		fmt.Printf("parsing JWT: %s\n", err)
+		return
 	}
 
 	out := jwt.Claims{}
 	if err := tok.Claims(sharedKey, &out); err != nil {
-		panic(err)
+		fmt.Printf("validating claims: %s\n", err)
+		return
 	}
 	fmt.Printf("iss: %s, sub: %s\n", out.Issuer, out.Subject)
 	// Output: iss: issuer, sub: subject
@@ -52,14 +55,16 @@ func ExampleParseSigned() {
 func ExampleParseEncrypted() {
 	key := []byte("itsa16bytesecret")
 	raw := `eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIn0..jg45D9nmr6-8awml.z-zglLlEw9MVkYHi-Znd9bSwc-oRGbqKzf9WjXqZxno.kqji2DiZHZmh-1bLF6ARPw`
-	tok, err := jwt.ParseEncrypted(raw)
+	tok, err := jwt.ParseEncrypted(raw, []jose.KeyAlgorithm{jose.DIRECT}, []jose.ContentEncryption{jose.A128GCM})
 	if err != nil {
-		panic(err)
+		fmt.Printf("parsing JWT: %s\n", err)
+		return
 	}
 
 	out := jwt.Claims{}
 	if err := tok.Claims(key, &out); err != nil {
-		panic(err)
+		fmt.Printf("validating claims: %s\n", err)
+		return
 	}
 	fmt.Printf("iss: %s, sub: %s\n", out.Issuer, out.Subject)
 	// Output: iss: issuer, sub: subject
@@ -67,19 +72,25 @@ func ExampleParseEncrypted() {
 
 func ExampleParseSignedAndEncrypted() {
 	raw := `eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIiwiY3R5IjoiSldUIn0..-keV-9YpsxotBEHw.yC9SHWgnkjykgJqXZGlzYC5Wg_EdWKO5TgfqeqsWWJYw7fX9zXQE3NtXmA3nAiUrYOr3H2s0AgTeAhTNbELLEHQu0blfRaPa_uKOAgFgmhJwbGe2iFLn9J0U72wk56318nI-pTLCV8FijoGpXvAxQlaKrPLKkl9yDQimPhb7UiDwLWYkJeoayciAXhR5f40E8ORGjCz8oawXRvjDaSjgRElUwy4kMGzvJy_difemEh4lfMSIwUNVEqJkEYaalRttSymMYuV6NvBVU0N0Jb6omdM4tW961OySB4KPWCWH9UJUX0XSEcqbW9WLxpg3ftx5R7xNiCnaVaCx_gJZfXJ9yFLqztIrKh2N05zHM0tddSOwCOnq7_1rJtaVz0nTXjSjf1RrVaxJya59p3K-e41QutiGFiJGzXG-L2OyLETIaVSU3ptvaCz4IxCF3GzeCvOgaICvXkpBY1-bv-fk1ilyjmcTDnLp2KivWIxcnoQmpN9xj06ZjagdG09AHUhS5WixADAg8mIdGcanNblALecnCWG-otjM9Kw.RZoaHtSgnzOin2od3D9tnA`
-	tok, err := jwt.ParseSignedAndEncrypted(raw)
+	tok, err := jwt.ParseSignedAndEncrypted(raw,
+		[]jose.KeyAlgorithm{jose.DIRECT},
+		[]jose.ContentEncryption{jose.A128GCM},
+		[]jose.SignatureAlgorithm{jose.RS256})
 	if err != nil {
-		panic(err)
+		fmt.Printf("validating claims: %s\n", err)
+		return
 	}
 
 	nested, err := tok.Decrypt(sharedEncryptionKey)
 	if err != nil {
-		panic(err)
+		fmt.Printf("decrypting JWT: %s\n", err)
+		return
 	}
 
 	out := jwt.Claims{}
 	if err := nested.Claims(&rsaPrivKey.PublicKey, &out); err != nil {
-		panic(err)
+		fmt.Printf("validating claims: %s\n", err)
+		return
 	}
 
 	fmt.Printf("iss: %s, sub: %s\n", out.Issuer, out.Subject)
@@ -100,7 +111,8 @@ func ExampleClaims_Validate() {
 		Time:   time.Date(2016, 1, 1, 0, 10, 0, 0, time.UTC),
 	})
 	if err != nil {
-		panic(err)
+		fmt.Printf("validating claims: %s\n", err)
+		return
 	}
 
 	fmt.Printf("valid!")
@@ -108,15 +120,17 @@ func ExampleClaims_Validate() {
 }
 
 func ExampleClaims_Validate_withParse() {
-	raw := `eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0In0.gpHyA1B1H6X4a4Edm9wo7D3X2v3aLSDBDG2_5BzXYe0`
-	tok, err := jwt.ParseSigned(raw)
+	raw := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0In0.OFD0iVfPczqWBA_TRi1jGB5PF699eekcHt4D6qNoimc`
+	tok, err := jwt.ParseSigned(raw, []jose.SignatureAlgorithm{jose.HS256})
 	if err != nil {
-		panic(err)
+		fmt.Printf("parsing JWT: %s\n", err)
+		return
 	}
 
 	cl := jwt.Claims{}
 	if err := tok.Claims(sharedKey, &cl); err != nil {
-		panic(err)
+		fmt.Printf("validating claims: %s\n", err)
+		return
 	}
 
 	err = cl.Validate(jwt.Expected{
@@ -124,7 +138,8 @@ func ExampleClaims_Validate_withParse() {
 		Subject: "subject",
 	})
 	if err != nil {
-		panic(err)
+		fmt.Printf("validating claims: %s\n", err)
+		return
 	}
 
 	fmt.Printf("valid!")
@@ -132,10 +147,11 @@ func ExampleClaims_Validate_withParse() {
 }
 
 func ExampleSigned() {
-	key := []byte("secret")
+	key := []byte("0102030405060708090A0B0C0D0E0F10")
 	sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: key}, (&jose.SignerOptions{}).WithType("JWT"))
 	if err != nil {
-		panic(err)
+		fmt.Printf("making signer: %s\n", err)
+		return
 	}
 
 	cl := jwt.Claims{
@@ -144,20 +160,22 @@ func ExampleSigned() {
 		NotBefore: jwt.NewNumericDate(time.Date(2016, 1, 1, 0, 0, 0, 0, time.UTC)),
 		Audience:  jwt.Audience{"leela", "fry"},
 	}
-	raw, err := jwt.Signed(sig).Claims(cl).CompactSerialize()
+	raw, err := jwt.Signed(sig).Claims(cl).Serialize()
 	if err != nil {
-		panic(err)
+		fmt.Printf("signing JWT: %s\n", err)
+		return
 	}
 
 	fmt.Println(raw)
-	// Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsibGVlbGEiLCJmcnkiXSwiaXNzIjoiaXNzdWVyIiwibmJmIjoxNDUxNjA2NDAwLCJzdWIiOiJzdWJqZWN0In0.4PgCj0VO-uG_cb1mNA38NjJyp0N-NdGIDLoYelEkciw
+	// Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsibGVlbGEiLCJmcnkiXSwiaXNzIjoiaXNzdWVyIiwibmJmIjoxNDUxNjA2NDAwLCJzdWIiOiJzdWJqZWN0In0.qEmW0Ehle1yO9XE7xZooC3AUVDF2NnJFDSgn4_6QzUo
 }
 
 func ExampleSigned_privateClaims() {
-	key := []byte("secret")
+	key := []byte("0102030405060708090A0B0C0D0E0F10")
 	sig, err := jose.NewSigner(jose.SigningKey{Algorithm: jose.HS256, Key: key}, (&jose.SignerOptions{}).WithType("JWT"))
 	if err != nil {
-		panic(err)
+		fmt.Printf("making signer: %s\n", err)
+		return
 	}
 
 	cl := jwt.Claims{
@@ -176,13 +194,14 @@ func ExampleSigned_privateClaims() {
 		"custom claim value",
 	}
 
-	raw, err := jwt.Signed(sig).Claims(cl).Claims(privateCl).CompactSerialize()
+	raw, err := jwt.Signed(sig).Claims(cl).Claims(privateCl).Serialize()
 	if err != nil {
-		panic(err)
+		fmt.Printf("signing JWT: %s\n", err)
+		return
 	}
 
 	fmt.Println(raw)
-	// Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsibGVlbGEiLCJmcnkiXSwiY3VzdG9tIjoiY3VzdG9tIGNsYWltIHZhbHVlIiwiaXNzIjoiaXNzdWVyIiwibmJmIjoxNDUxNjA2NDAwLCJzdWIiOiJzdWJqZWN0In0.knXH3ReNJToS5XI7BMCkk80ugpCup3tOy53xq-ga47o
+	// Output: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsibGVlbGEiLCJmcnkiXSwiY3VzdG9tIjoiY3VzdG9tIGNsYWltIHZhbHVlIiwiaXNzIjoiaXNzdWVyIiwibmJmIjoxNDUxNjA2NDAwLCJzdWIiOiJzdWJqZWN0In0.m6GDh-23MdwYKmzGHuWLMUcx874cGbyMG7nv-5J1ifk
 }
 
 func ExampleEncrypted() {
@@ -192,16 +211,18 @@ func ExampleEncrypted() {
 		(&jose.EncrypterOptions{}).WithType("JWT"),
 	)
 	if err != nil {
-		panic(err)
+		fmt.Printf("making encrypter: %s\n", err)
+		return
 	}
 
 	cl := jwt.Claims{
 		Subject: "subject",
 		Issuer:  "issuer",
 	}
-	raw, err := jwt.Encrypted(enc).Claims(cl).CompactSerialize()
+	raw, err := jwt.Encrypted(enc).Claims(cl).Serialize()
 	if err != nil {
-		panic(err)
+		fmt.Printf("encrypting JWT: %s\n", err)
+		return
 	}
 
 	fmt.Println(raw)
@@ -216,16 +237,18 @@ func ExampleSignedAndEncrypted() {
 		},
 		(&jose.EncrypterOptions{}).WithType("JWT").WithContentType("JWT"))
 	if err != nil {
-		panic(err)
+		fmt.Printf("making encrypter: %s\n", err)
+		return
 	}
 
 	cl := jwt.Claims{
 		Subject: "subject",
 		Issuer:  "issuer",
 	}
-	raw, err := jwt.SignedAndEncrypted(rsaSigner, enc).Claims(cl).CompactSerialize()
+	raw, err := jwt.SignedAndEncrypted(rsaSigner, enc).Claims(cl).Serialize()
 	if err != nil {
-		panic(err)
+		fmt.Printf("encrypting and signing JWT: %s\n", err)
+		return
 	}
 
 	fmt.Println(raw)
@@ -241,25 +264,28 @@ func ExampleSigned_multipleClaims() {
 	}{
 		[]string{"foo", "bar"},
 	}
-	raw, err := jwt.Signed(signer).Claims(c).Claims(c2).CompactSerialize()
+	raw, err := jwt.Signed(signer).Claims(c).Claims(c2).Serialize()
 	if err != nil {
-		panic(err)
+		fmt.Printf("validating claims: %s\n", err)
+		return
 	}
 
 	fmt.Println(raw)
-	// Output: eyJhbGciOiJIUzI1NiJ9.eyJTY29wZXMiOlsiZm9vIiwiYmFyIl0sImlzcyI6Imlzc3VlciIsInN1YiI6InN1YmplY3QifQ.esKOIsmwkudr_gnfnB4SngxIr-7pspd5XzG3PImfQ6Y
+	// Output: eyJhbGciOiJIUzI1NiJ9.eyJTY29wZXMiOlsiZm9vIiwiYmFyIl0sImlzcyI6Imlzc3VlciIsInN1YiI6InN1YmplY3QifQ.9VjIUvZ8VPFg1mMPq0kTbN7CpVOfn-WChY9RAVu-I6o
 }
 
 func ExampleJSONWebToken_Claims_map() {
-	raw := `eyJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0In0.gpHyA1B1H6X4a4Edm9wo7D3X2v3aLSDBDG2_5BzXYe0`
-	tok, err := jwt.ParseSigned(raw)
+	raw := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIiLCJzdWIiOiJzdWJqZWN0In0.OFD0iVfPczqWBA_TRi1jGB5PF699eekcHt4D6qNoimc`
+	tok, err := jwt.ParseSigned(raw, []jose.SignatureAlgorithm{jose.HS256})
 	if err != nil {
-		panic(err)
+		fmt.Printf("parsing JWT: %s\n", err)
+		return
 	}
 
 	out := make(map[string]interface{})
 	if err := tok.Claims(sharedKey, &out); err != nil {
-		panic(err)
+		fmt.Printf("validating claims: %s\n", err)
+		return
 	}
 
 	fmt.Printf("iss: %s, sub: %s\n", out["iss"], out["sub"])
@@ -267,21 +293,23 @@ func ExampleJSONWebToken_Claims_map() {
 }
 
 func ExampleJSONWebToken_Claims_multiple() {
-	raw := `eyJhbGciOiJIUzI1NiJ9.eyJTY29wZXMiOlsiZm9vIiwiYmFyIl0sImlzcyI6Imlzc3VlciIsInN1YiI6InN1YmplY3QifQ.esKOIsmwkudr_gnfnB4SngxIr-7pspd5XzG3PImfQ6Y`
-	tok, err := jwt.ParseSigned(raw)
+	raw := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJpc3N1ZXIiLCJzY29wZXMiOlsiczEiLCJzMiJdLCJzdWIiOiJzdWJqZWN0In0.O9XxAYZsxXxWpTftO75vLpyYZ1g7FHxBvyvctGg3Ih0`
+	tok, err := jwt.ParseSigned(raw, []jose.SignatureAlgorithm{jose.HS256})
 	if err != nil {
-		panic(err)
+		fmt.Printf("parsing JWT: %s\n", err)
+		return
 	}
 
 	out := jwt.Claims{}
 	out2 := struct {
-		Scopes []string
+		Scopes []string `json:"scopes"`
 	}{}
 	if err := tok.Claims(sharedKey, &out, &out2); err != nil {
-		panic(err)
+		fmt.Printf("validating claims: %s\n", err)
+		return
 	}
 	fmt.Printf("iss: %s, sub: %s, scopes: %s\n", out.Issuer, out.Subject, strings.Join(out2.Scopes, ","))
-	// Output: iss: issuer, sub: subject, scopes: foo,bar
+	// Output: iss: issuer, sub: subject, scopes: s1,s2
 }
 
 func mustUnmarshalRSA(data string) *rsa.PrivateKey {
diff --git jwt/jwt.go jwt/jwt.go
index 8553fc5..c4998d7 100644
--- jwt/jwt.go
+++ jwt/jwt.go
@@ -21,8 +21,8 @@ import (
 	"fmt"
 	"strings"
 
-	jose "github.com/go-jose/go-jose/v3"
-	"github.com/go-jose/go-jose/v3/json"
+	jose "github.com/go-jose/go-jose/v4"
+	"github.com/go-jose/go-jose/v4/json"
 )
 
 // JSONWebToken represents a JSON Web Token (as specified in RFC7519).
@@ -35,6 +35,8 @@ type JSONWebToken struct {
 type NestedJSONWebToken struct {
 	enc     *jose.JSONWebEncryption
 	Headers []jose.Header
+	// Used when parsing and decrypting an input
+	allowedSignatureAlgorithms []jose.SignatureAlgorithm
 }
 
 // Claims deserializes a JSONWebToken into dest using the provided key.
@@ -75,7 +77,7 @@ func (t *NestedJSONWebToken) Decrypt(decryptionKey interface{}) (*JSONWebToken,
 		return nil, err
 	}
 
-	sig, err := ParseSigned(string(b))
+	sig, err := ParseSigned(string(b), t.allowedSignatureAlgorithms)
 	if err != nil {
 		return nil, err
 	}
@@ -84,8 +86,8 @@ func (t *NestedJSONWebToken) Decrypt(decryptionKey interface{}) (*JSONWebToken,
 }
 
 // ParseSigned parses token from JWS form.
-func ParseSigned(s string) (*JSONWebToken, error) {
-	sig, err := jose.ParseSigned(s)
+func ParseSigned(s string, signatureAlgorithms []jose.SignatureAlgorithm) (*JSONWebToken, error) {
+	sig, err := jose.ParseSignedCompact(s, signatureAlgorithms)
 	if err != nil {
 		return nil, err
 	}
@@ -101,9 +103,57 @@ func ParseSigned(s string) (*JSONWebToken, error) {
 	}, nil
 }
 
+func validateKeyEncryptionAlgorithm(algs []jose.KeyAlgorithm) error {
+	for _, alg := range algs {
+		switch alg {
+		case jose.ED25519,
+			jose.RSA1_5,
+			jose.RSA_OAEP,
+			jose.RSA_OAEP_256,
+			jose.ECDH_ES,
+			jose.ECDH_ES_A128KW,
+			jose.ECDH_ES_A192KW,
+			jose.ECDH_ES_A256KW:
+			return fmt.Errorf("asymmetric encryption algorithms not supported for JWT: "+
+				"invalid key encryption algorithm: %s", alg)
+		case jose.PBES2_HS256_A128KW,
+			jose.PBES2_HS384_A192KW,
+			jose.PBES2_HS512_A256KW:
+			return fmt.Errorf("password-based encryption not supported for JWT: "+
+				"invalid key encryption algorithm: %s", alg)
+		}
+	}
+	return nil
+}
+
+func parseEncryptedCompact(
+	s string,
+	keyAlgorithms []jose.KeyAlgorithm,
+	contentEncryption []jose.ContentEncryption,
+) (*jose.JSONWebEncryption, error) {
+	err := validateKeyEncryptionAlgorithm(keyAlgorithms)
+	if err != nil {
+		return nil, err
+	}
+	enc, err := jose.ParseEncryptedCompact(s, keyAlgorithms, contentEncryption)
+	if err != nil {
+		return nil, err
+	}
+	return enc, nil
+}
+
 // ParseEncrypted parses token from JWE form.
-func ParseEncrypted(s string) (*JSONWebToken, error) {
-	enc, err := jose.ParseEncrypted(s)
+//
+// The keyAlgorithms and contentEncryption parameters are used to validate the "alg" and "enc"
+// header parameters respectively. They must be nonempty, and each "alg" or "enc" header in
+// parsed data must contain a value that is present in the corresponding parameter. That
+// includes the protected and unprotected headers as well as all recipients. To accept
+// multiple algorithms, pass a slice of all the algorithms you want to accept.
+func ParseEncrypted(s string,
+	keyAlgorithms []jose.KeyAlgorithm,
+	contentEncryption []jose.ContentEncryption,
+) (*JSONWebToken, error) {
+	enc, err := parseEncryptedCompact(s, keyAlgorithms, contentEncryption)
 	if err != nil {
 		return nil, err
 	}
@@ -115,8 +165,22 @@ func ParseEncrypted(s string) (*JSONWebToken, error) {
 }
 
 // ParseSignedAndEncrypted parses signed-then-encrypted token from JWE form.
-func ParseSignedAndEncrypted(s string) (*NestedJSONWebToken, error) {
-	enc, err := jose.ParseEncrypted(s)
+//
+// The encryptionKeyAlgorithms and contentEncryption parameters are used to validate the "alg" and "enc"
+// header parameters, respectively, of the outer JWE. They must be nonempty, and each "alg" or "enc"
+// header in parsed data must contain a value that is present in the corresponding parameter. That
+// includes the protected and unprotected headers as well as all recipients. To accept
+// multiple algorithms, pass a slice of all the algorithms you want to accept.
+//
+// The signatureAlgorithms parameter is used to validate the "alg" header parameter of the
+// inner JWS. It must be nonempty, and the "alg" header in the inner JWS must contain a value
+// that is present in the parameter.
+func ParseSignedAndEncrypted(s string,
+	encryptionKeyAlgorithms []jose.KeyAlgorithm,
+	contentEncryption []jose.ContentEncryption,
+	signatureAlgorithms []jose.SignatureAlgorithm,
+) (*NestedJSONWebToken, error) {
+	enc, err := parseEncryptedCompact(s, encryptionKeyAlgorithms, contentEncryption)
 	if err != nil {
 		return nil, err
 	}
@@ -127,7 +191,8 @@ func ParseSignedAndEncrypted(s string) (*NestedJSONWebToken, error) {
 	}
 
 	return &NestedJSONWebToken{
-		enc:     enc,
-		Headers: []jose.Header{enc.Header},
+		allowedSignatureAlgorithms: signatureAlgorithms,
+		enc:                        enc,
+		Headers:                    []jose.Header{enc.Header},
 	}, nil
 }
diff --git jwt/jwt_test.go jwt/jwt_test.go
index 75d05de..2006531 100644
--- jwt/jwt_test.go
+++ jwt/jwt_test.go
@@ -21,12 +21,13 @@ import (
 	"strings"
 	"testing"
 
-	jose "github.com/go-jose/go-jose/v3"
-	"github.com/stretchr/testify/assert"
+	"github.com/go-jose/go-jose/v4/testutils/assert"
+
+	jose "github.com/go-jose/go-jose/v4"
 )
 
 var (
-	hmacSignedToken                = `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJzdWJqZWN0IiwiaXNzIjoiaXNzdWVyIiwic2NvcGVzIjpbInMxIiwiczIiXX0.Y6_PfQHrzRJ_Vlxij5VI07-pgDIuJNN3Z_g5sSaGQ0c`
+	hmacSignedToken                string
 	rsaSignedToken                 = `eyJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJpc3N1ZXIiLCJzY29wZXMiOlsiczEiLCJzMiJdLCJzdWIiOiJzdWJqZWN0In0.UDDtyK9gC9kyHltcP7E_XODsnqcJWZIiXeGmSAH7SE9YKy3N0KSfFIN85dCNjTfs6zvy4rkrCHzLB7uKAtzMearh3q7jL4nxbhUMhlUcs_9QDVoN4q_j58XmRqBqRnBk-RmDu9TgcV8RbErP4awpIhwWb5UU-hR__4_iNbHdKqwSUPDKYGlf5eicuiYrPxH8mxivk4LRD-vyRdBZZKBt0XIDnEU4TdcNCzAXojkftqcFWYsczwS8R4JHd1qYsMyiaWl4trdHZkO4QkeLe34z4ZAaPMt3wE-gcU-VoqYTGxz-K3Le2VaZ0r3j_z6bOInsv0yngC_cD1dCXMyQJWnWjQ`
 	rsaSignedTokenWithKid          = `eyJhbGciOiJSUzI1NiIsImtpZCI6ImZvb2JhciJ9.eyJpc3MiOiJpc3N1ZXIiLCJzY29wZXMiOlsiczEiLCJzMiJdLCJzdWIiOiJzdWJqZWN0In0.RxZhTRfPDb6UJ58FwvC89GgJGC8lAO04tz5iLlBpIJsyPZB0X_UgXSj0SGVFm2jbP_i-ZVH4HFC2fMB1n-so9CnCOpunWwhYNdgF6ewQJ0ADTWwfDGsK12UOmyT2naaZN8ZUBF8cgPtOgdWqQjk2Ng9QFRJxlUuKYczBp7vjWvgX8WMwQcaA-eK7HtguR4e9c4FMbeFK8Soc4jCsVTjIKdSn9SErc42gFu65NI1hZ3OPe_T7AZqdDjCkJpoiJ65GdD_qvGkVndJSEcMp3riXQpAy0JbctVkYecdFaGidbxHRrdcQYHtKn-XGMCh2uoBKleUr1fTMiyCGPQQesy3xHw`
 	invalidPayloadSignedToken      = `eyJhbGciOiJIUzI1NiJ9.aW52YWxpZC1wYXlsb2Fk.ScBKKm18jcaMLGYDNRUqB5gVMRZl4DM6dh3ShcxeNgY`
@@ -36,15 +37,28 @@ var (
 	invalidPayloadEncryptedToken   = `eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIn0..T4jCS4Yyw1GCH0aW.y4gFaMITdBs_QZM8RKrL.6MPyk1cMVaOJFoNGlEuaRQ`
 	invalidPartsEncryptedToken     = `eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4R0NNIn0..NZrU98U4QNO0y-u6.HSq5CvlmkUT1BPqLGZ4`
 	signedAndEncryptedToken        = `eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwiY3R5IjoiSldUIn0.icnR7M1HSgMDaUnJhfzT5nLmT0eRPeNsKPkioNcyq9TZsm-LgbE7wZkNFGfQqYwvbmrZ3UpOhNkrq4n2KN3N1dtjH9TVxzfMxz2OMh0dRWUNMi58EMadhmIpH3PLyyaeDyd0dyHpOIRPFTAoOdn2GoO_flV5CvPMhgdVKYB3h3vQW-ZZDu4cOZwXAjTuThdoUZCNWFhJhXyj-PrKLyVpX6rE1o4X05IS8008SLZyx-PZlsUPyLs6CJi7Z4PzZRzOJTV00a-7UOi-fBKBZV5V8eRpWuzJ673pMALlRCBzrRin-JeEA_QnAejtMAHG7RSGP60easQN4I-0jLTQNNNynw.oFrO-5ZgRrnWmbkPsbyMiQ.BVaWUzlrdfhe0otPJpb3DGoDCT6-BOmN_Pgq5NOqVFYIAwG5pM4pf7TaiPUJeQLf0phbLgpT4RfJ20Zhwfc2MH5unCqc8TZEP2dOrYRhb8o-X57x6IQppIDbjK2i_CAWf3yF5JUB7qRqOizpKZTh3HFTVEglY3WF8tAJ8KpnatTUmwcnqlyjdBFvYu4usiyvc_u9wNbXx5-lFt0slQYleHQMUirBprKyswIBjMoFJEe7kDvU_MCKI4NI9_fSfWJpaUdNxQEvRYR1PV4ZQdwBY0X9u2n2QH5iVQMrmgmQ5hPbWxwRv1-7jXBMPBpGeFQZHeEtSwif1_Umwyt8cDyRChb3OM7XQ3eY0UJRrbmvhcLWIcMp8FpblDaBinbjD6qIVXZVmaAdIbi2a_HblfoeL3-UABb82AAxOqQcAFjDEDTR2TFalDXSwgPZrAaQ_Mql3eFe9r2y0UVkgG7XYF4ik8sSK48CkZPUvkZFr-K9QMq-RZLzT3Zw0edxNaKgje27S26H9qClh6CCr9nk38AZZ76_Xz7f-Fil5xI0Dq95UzvwW__U3JJWE6OVUVx_RVJgdOJn8_B7hluckwBLUblscA.83pPXNnH0sKgHvFboiJVDA`
-	invalidSignedAndEncryptedToken = `eyJhbGciOiJSU0ExXzUiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2In0.QKYu3DkFEXBUa2U0Sgtm-e44BMuaFVbMu2T-GB3qEGONrmOuaB5BtNCvBUnuj6HR0v6u-tvawToRSzExQQLFTvPcLiQR8iclWirqAFUqLrg8kRU3qIRLkmErYeGIfCML1jq9ofKg0DI5-YrU5RSyUg9cwfXKEx8KNwFcjeVeDZwWEACdU8xBnQp57rNfr0Tj-dPnGKID7LU5ZV0vhK90FpEG7UqOeSHFmvONQyz6Ca-ZkE8X2swqGad-q5xl8f9pApdFqHzADox5OlgtxPkr-Khkm6WGfvf1K_e-iW5LYtvWIAjNByft2TexsNcYpdAO2oNAgh2nkhoohl-zCWU-og.UAU65JWKqvHZ_Z0V-xLyjQ.M6sQ4lAzKFelSmL6C6uoK00rB8IFCAK-eJ0iByGhtg8eYtmSBFsP_oUySfKPtxcPRkQ7YxnEX5D-DOo20wCV7il2Be9No__0R6_5heISOMXcKmKP3D6pFusaPisNGOgLw8SKXBuVpe20PvOJ9RgOXRKucSR2UMINXtqIn9RdxbKOlBBmMJhnX4TeQ00fRILng2sMbUHsWExSthQODHGx6VcwLFp-Aqmsnv2q2KkLpA8sEm48AHHFQXSGtlVGVgWKi3dOQYUnDJW4P64Xxr1Uq3yT7w_dRwK4BA7l3Biecj5dwkKrFMJ_RaCt-ED_R15zpxg6PmnXeeJnif58Fai40ZWOsGvLZNYwL1jbi-TrsargpdUQedfzuTk8Na2NkCzFNg2BYXVDHJ_WAX1daVyhvunaURwAlBatAcmnOGxWebwV1xQoQ7iHg6ZGohCannn_pqGwJlMHMgnCcnCIhwfj9uL9Ejz_TVceZNMlT1KvLRafVfxGhkp48bdnd8OcXmjT9pQzZUB3OqrstWKhbItZ1xMpy6dZ54ldWvtTTyQ4tQJaVWgXERUM1erDT6Ypyl15-fumOB9MRcgMG3NDblKowA.P9WTBITvVUgrLjX6bS0opQ`
+	invalidSignedAndEncryptedToken = `eyJhbGciOiJkaXIiLCJlbmMiOiJBMTI4Q0JDLUhTMjU2IiwidHlwIjoiSldUIn0K..PmqSKNuL84466r0zAFCy6A.sSN_7NJs4l7FAj6HcBzdYjV3FGu1MsqZCk_zNjpp6qsYynR3pJWU3jLUVYrRkeQKOaJAmDwOHHdrq9tBPh4-GD3zgtM1Fm5mJ2oVrbQUr2nllYWIsdb2LhVR9pxdhjDm6wlpJxcIuY1PkeMIVUupXEBEemk9atiJWAjoGqXVPO_pI6-egGseqYo1PjUXY7Cnz6SBq0W3yNk3Edf_xA-lLZuiALuatsweNJUbTwqTzhOHQ6aPAiPafc7jQGvp9YXYi6X5oQ85cXPTCnGQWS7LMHPME5gRzb4_Cz7M1QhZGdbOS8bLnxKMRZwbhvdc9-JhTqi8JbA0mp0OnWr77iPBCeFAUVEbqxTMeWOwLCQ6RjoTtx3vXnVfgqKSrkdMKGhC33tEfxy_Wg5WEm_3jeITSCvQJtwItNVhhlOrcqPD71JMLhneAnRtSqys5TbporUOpwi43DStCYBdrueE-M0dlo3C6tO0KDgAgg48JaiW_76AcO32vJTKKl9rZ0ybku58lqHJtMNR4bJ7PjTv3hPhfA.p4VEoJ3y7THiJwRpXBlsHQ`
 )
 
 type customClaims struct {
 	Scopes []string `json:"scopes,omitempty"`
 }
 
+func init() {
+	var err error
+	hmacSignedToken, err = Signed(hmacSigner).Claims(Claims{
+		Subject: "subject",
+		Issuer:  "issuer",
+	}).Claims(customClaims{
+		Scopes: []string{"s1", "s2"},
+	}).Serialize()
+	if err != nil {
+		panic(err)
+	}
+}
+
 func TestGetClaimsWithoutVerification(t *testing.T) {
-	tok, err := ParseSigned(hmacSignedToken)
+	tok, err := ParseSigned(hmacSignedToken, []jose.SignatureAlgorithm{jose.HS256})
 	if assert.NoError(t, err, "Error parsing signed token.") {
 		c := &Claims{}
 		c2 := &customClaims{}
@@ -55,10 +69,10 @@ func TestGetClaimsWithoutVerification(t *testing.T) {
 		}
 		assert.Equal(t, "subject", c.Subject)
 		assert.Equal(t, "issuer", c.Issuer)
-		assert.Equal(t, []string{"s1", "s2"}, c2.Scopes)
+		assert.EqualSlice(t, []string{"s1", "s2"}, c2.Scopes)
 
 	}
-	tok, err = ParseEncrypted(hmacEncryptedToken)
+	tok, err = ParseEncrypted(hmacEncryptedToken, []jose.KeyAlgorithm{jose.DIRECT}, []jose.ContentEncryption{jose.A128GCM})
 	if assert.NoError(t, err, "Error parsing encrypted token.") {
 		c := Claims{}
 		err := tok.UnsafeClaimsWithoutVerification(c)
@@ -78,7 +92,7 @@ func TestDecodeTokenWithJWKS(t *testing.T) {
 		},
 	}
 
-	tok, err := ParseSigned(rsaSignedTokenWithKid)
+	tok, err := ParseSigned(rsaSignedTokenWithKid, []jose.SignatureAlgorithm{jose.RS256})
 	if assert.NoError(t, err, "Error parsing signed token.") {
 		cl := make(map[string]interface{})
 		expected := map[string]interface{}{
@@ -88,98 +102,111 @@ func TestDecodeTokenWithJWKS(t *testing.T) {
 		}
 
 		if assert.NoError(t, tok.Claims(jwks, &cl)) {
-			assert.Equal(t, expected, cl)
+			assert.EqualJSON(t, expected, cl)
 		}
 
 		cl = make(map[string]interface{})
 		if assert.NoError(t, tok.Claims(*jwks, &cl)) {
-			assert.Equal(t, expected, cl)
+			assert.EqualJSON(t, expected, cl)
 		}
 	}
 }
 
+// TestDecodeTokenWithMismatchedJWKSKID tests a case where the JWT has a KID
+// header which does not match any of the keys in the JWKS.
+func TestDecodeTokenWithMismatchedJWKSKID(t *testing.T) {
+	jwks := &jose.JSONWebKeySet{
+		Keys: []jose.JSONWebKey{
+			{
+				KeyID: "does-not-match",
+				Key:   &testPrivRSAKey1.PublicKey,
+			},
+		},
+	}
+
+	tok, err := ParseSigned(rsaSignedTokenWithKid, []jose.SignatureAlgorithm{jose.RS256})
+	if assert.NoError(t, err, "Error parsing signed token.") {
+		cl := make(map[string]interface{})
+		err := tok.Claims(jwks, &cl)
+		assert.Error(t, err, "Expected error when JWT KID does not match any key in JWKS.")
+		assert.ErrorIs(t, err, jose.ErrJWKSKidNotFound)
+	}
+
+	tok, err = ParseSigned(rsaSignedToken, []jose.SignatureAlgorithm{jose.RS256})
+	if assert.NoError(t, err, "Error parsing signed token.") {
+		cl := make(map[string]interface{})
+		err := tok.Claims(jwks, &cl)
+		assert.Error(t, err, "Expected error when JWT KID does not match any key in JWKS.")
+		assert.ErrorIs(t, err, jose.ErrJWKSKidNotFound)
+	}
+}
+
 func TestDecodeToken(t *testing.T) {
-	tok, err := ParseSigned(hmacSignedToken)
+	tok, err := ParseSigned(hmacSignedToken, []jose.SignatureAlgorithm{jose.HS256})
 	if assert.NoError(t, err, "Error parsing signed token.") {
 		c := &Claims{}
 		c2 := &customClaims{}
 		if assert.NoError(t, tok.Claims(sharedKey, c, c2)) {
 			assert.Equal(t, "subject", c.Subject)
 			assert.Equal(t, "issuer", c.Issuer)
-			assert.Equal(t, []string{"s1", "s2"}, c2.Scopes)
+			assert.EqualSlice(t, []string{"s1", "s2"}, c2.Scopes)
 		}
 	}
-	assert.EqualError(t, tok.Claims([]byte("invalid-secret")), "go-jose/go-jose: error in cryptographic primitive")
+	assert.Equal(t, tok.Claims([]byte("invalid-secret")).Error(), "go-jose/go-jose: error in cryptographic primitive")
 
-	tok2, err := ParseSigned(rsaSignedToken)
+	tok2, err := ParseSigned(rsaSignedToken, []jose.SignatureAlgorithm{jose.RS256})
 	if assert.NoError(t, err, "Error parsing encrypted token.") {
 		c := make(map[string]interface{})
 		if assert.NoError(t, tok2.Claims(&testPrivRSAKey1.PublicKey, &c)) {
-			assert.Equal(t, map[string]interface{}{
+			assert.EqualJSON(t, map[string]interface{}{
 				"sub":    "subject",
 				"iss":    "issuer",
 				"scopes": []interface{}{"s1", "s2"},
 			}, c)
 		}
 	}
-	assert.EqualError(t, tok.Claims(&testPrivRSAKey2.PublicKey), "go-jose/go-jose: error in cryptographic primitive")
+	assert.Equal(t, tok.Claims(&testPrivRSAKey2.PublicKey).Error(), "go-jose/go-jose: error in cryptographic primitive")
 
-	tok3, err := ParseSigned(invalidPayloadSignedToken)
+	tok3, err := ParseSigned(invalidPayloadSignedToken, []jose.SignatureAlgorithm{jose.HS256})
 	if assert.NoError(t, err, "Error parsing signed token.") {
 		assert.Error(t, tok3.Claims(sharedKey, &Claims{}), "Expected unmarshaling claims to fail.")
 	}
 
-	_, err = ParseSigned(invalidPartsSignedToken)
-	assert.EqualError(t, err, "go-jose/go-jose: compact JWS format must have three parts")
+	_, err = ParseSigned(invalidPartsSignedToken, []jose.SignatureAlgorithm{jose.HS256})
+	assert.Equal(t, err.Error(), "go-jose/go-jose: compact JWS format must have three parts")
 
-	tok4, err := ParseEncrypted(hmacEncryptedToken)
+	tok4, err := ParseEncrypted(hmacEncryptedToken, []jose.KeyAlgorithm{jose.DIRECT}, []jose.ContentEncryption{jose.A128GCM})
 	if assert.NoError(t, err, "Error parsing encrypted token.") {
 		c := Claims{}
 		if assert.NoError(t, tok4.Claims(sharedEncryptionKey, &c)) {
 			assert.Equal(t, "foo", c.Subject)
 		}
 	}
-	assert.EqualError(t, tok4.Claims([]byte("invalid-secret-key")), "go-jose/go-jose: error in cryptographic primitive")
+	assert.Equal(t, tok4.Claims([]byte("invalid-secret-key")).Error(), "go-jose/go-jose: error in cryptographic primitive")
 
-	tok5, err := ParseEncrypted(rsaEncryptedToken)
-	if assert.NoError(t, err, "Error parsing encrypted token.") {
-		c := make(map[string]interface{})
-		if assert.NoError(t, tok5.Claims(testPrivRSAKey1, &c)) {
-			assert.Equal(t, map[string]interface{}{
-				"sub":    "subject",
-				"iss":    "issuer",
-				"scopes": []interface{}{"s1", "s2"},
-			}, c)
-		}
-	}
-	assert.EqualError(t, tok5.Claims(testPrivRSAKey2), "go-jose/go-jose: error in cryptographic primitive")
+	_, err = ParseEncrypted(rsaEncryptedToken, []jose.KeyAlgorithm{jose.RSA1_5}, []jose.ContentEncryption{jose.A128CBC_HS256})
+	assert.Error(t, err, "Expected error trying to parse token with symmetric encryption algorithm")
 
-	tok6, err := ParseEncrypted(invalidPayloadEncryptedToken)
+	tok6, err := ParseEncrypted(invalidPayloadEncryptedToken, []jose.KeyAlgorithm{jose.DIRECT}, []jose.ContentEncryption{jose.A128GCM})
 	if assert.NoError(t, err, "Error parsing encrypted token.") {
 		assert.Error(t, tok6.Claims(sharedEncryptionKey, &Claims{}))
 	}
 
-	_, err = ParseEncrypted(invalidPartsEncryptedToken)
-	assert.EqualError(t, err, "go-jose/go-jose: compact JWE format must have five parts")
+	_, err = ParseEncrypted(invalidPartsEncryptedToken, []jose.KeyAlgorithm{jose.DIRECT}, []jose.ContentEncryption{jose.A128GCM})
+	assert.Equal(t, err.Error(), "go-jose/go-jose: compact JWE format must have five parts")
 
-	tok7, err := ParseSignedAndEncrypted(signedAndEncryptedToken)
-	if assert.NoError(t, err, "Error parsing signed-then-encrypted token.") {
-		c := make(map[string]interface{})
-		if nested, err := tok7.Decrypt(testPrivRSAKey1); assert.NoError(t, err) {
-			assert.NoError(t, nested.Claims(testPrivRSAKey1.Public(), &c))
-			assert.Equal(t, map[string]interface{}{
-				"sub":    "subject",
-				"iss":    "issuer",
-				"scopes": []interface{}{"s1", "s2"},
-			}, c)
-			assert.EqualError(t, nested.Claims(testPrivRSAKey2.Public()), "go-jose/go-jose: error in cryptographic primitive")
-		}
-	}
-	_, err = tok7.Decrypt(testPrivRSAKey2)
-	assert.EqualError(t, err, "go-jose/go-jose: error in cryptographic primitive")
+	_, err = ParseSignedAndEncrypted(signedAndEncryptedToken,
+		[]jose.KeyAlgorithm{jose.RSA1_5},
+		[]jose.ContentEncryption{jose.A128CBC_HS256},
+		[]jose.SignatureAlgorithm{jose.RS256},
+	)
+	assert.Equal(t, err.Error(), "asymmetric encryption algorithms not supported for JWT: invalid key encryption algorithm: RSA1_5")
 
-	_, err = ParseSignedAndEncrypted(invalidSignedAndEncryptedToken)
-	assert.EqualError(t, err, "go-jose/go-jose/jwt: expected content type to be JWT (cty header)")
+	_, err = ParseSignedAndEncrypted(invalidSignedAndEncryptedToken,
+		[]jose.KeyAlgorithm{jose.DIRECT},
+		[]jose.ContentEncryption{jose.A128CBC_HS256},
+		[]jose.SignatureAlgorithm{jose.RS256})
+	assert.Equal(t, err.Error(), "go-jose/go-jose/jwt: expected content type to be JWT (cty header)")
 }
 
 func TestTamperedJWT(t *testing.T) {
@@ -195,14 +222,14 @@ func TestTamperedJWT(t *testing.T) {
 		Issuer:  "bar",
 	}
 
-	raw, _ := Encrypted(sig).Claims(cl).CompactSerialize()
+	raw, _ := Encrypted(sig).Claims(cl).Serialize()
 
 	// Modify with valid base64 junk
 	r := strings.Split(raw, ".")
 	r[2] = "b3RoZXJ0aGluZw"
 	raw = strings.Join(r, ".")
 
-	tok, _ := ParseEncrypted(raw)
+	tok, _ := ParseEncrypted(raw, []jose.KeyAlgorithm{jose.DIRECT}, []jose.ContentEncryption{jose.A128GCM})
 
 	cl = Claims{}
 	err := tok.Claims(key, &cl)
@@ -213,7 +240,7 @@ func TestTamperedJWT(t *testing.T) {
 
 func BenchmarkDecodeSignedToken(b *testing.B) {
 	for i := 0; i < b.N; i++ {
-		if _, err := ParseSigned(hmacSignedToken); err != nil {
+		if _, err := ParseSigned(hmacSignedToken, []jose.SignatureAlgorithm{jose.HS256}); err != nil {
 			b.Fatal(err)
 		}
 	}
@@ -221,8 +248,46 @@ func BenchmarkDecodeSignedToken(b *testing.B) {
 
 func BenchmarkDecodeEncryptedHMACToken(b *testing.B) {
 	for i := 0; i < b.N; i++ {
-		if _, err := ParseEncrypted(hmacEncryptedToken); err != nil {
+		if _, err := ParseEncrypted(hmacEncryptedToken, []jose.KeyAlgorithm{jose.DIRECT}, []jose.ContentEncryption{jose.A128GCM}); err != nil {
 			b.Fatal(err)
 		}
 	}
 }
+
+func TestValidateKeyEncryptionAlgorithm(t *testing.T) {
+	for _, alg := range []jose.KeyAlgorithm{
+		jose.RSA1_5, jose.RSA_OAEP, jose.RSA_OAEP_256,
+		jose.ECDH_ES, jose.ECDH_ES_A128KW, jose.ECDH_ES_A192KW, jose.ECDH_ES_A256KW,
+	} {
+		err := validateKeyEncryptionAlgorithm([]jose.KeyAlgorithm{alg})
+		if err == nil {
+			t.Errorf("expected error for %s, got none", alg)
+		}
+		if !strings.Contains(err.Error(), "asymmetric encryption algorithms not supported") {
+			t.Errorf("got wrong error for %s: %s", alg, err)
+		}
+	}
+	for _, alg := range []jose.KeyAlgorithm{
+		jose.PBES2_HS256_A128KW, jose.PBES2_HS384_A192KW, jose.PBES2_HS512_A256KW,
+	} {
+		err := validateKeyEncryptionAlgorithm([]jose.KeyAlgorithm{alg})
+		if err == nil {
+			t.Errorf("expected error for %s, got none", alg)
+		}
+		if !strings.Contains(err.Error(), "password-based encryption not supported") {
+			t.Errorf("got wrong error for %s: %s", alg, err)
+		}
+	}
+
+	for _, alg := range []jose.KeyAlgorithm{
+		jose.A128KW, jose.A192KW, jose.A256KW,
+		jose.A128GCMKW, jose.A192GCMKW, jose.A256GCMKW,
+		jose.DIRECT,
+		jose.KeyAlgorithm("XYZ"),
+	} {
+		err := validateKeyEncryptionAlgorithm([]jose.KeyAlgorithm{alg})
+		if err != nil {
+			t.Errorf("expected success for %s, got %s", alg, err)
+		}
+	}
+}
diff --git jwt/validation.go jwt/validation.go
index 09d8541..841a93e 100644
--- jwt/validation.go
+++ jwt/validation.go
@@ -33,8 +33,9 @@ type Expected struct {
 	Issuer string
 	// Subject matches the "sub" claim exactly.
 	Subject string
-	// Audience matches the values in "aud" claim, regardless of their order.
-	Audience Audience
+	// AnyAudience matches if there is a non-empty intersection between
+	// its values and the values in the "aud" claim.
+	AnyAudience Audience
 	// ID matches the "jti" claim exactly.
 	ID string
 	// Time matches the "exp", "nbf" and "iat" claims with leeway.
@@ -88,12 +89,18 @@ func (c Claims) ValidateWithLeeway(e Expected, leeway time.Duration) error {
 		return ErrInvalidID
 	}
 
-	if len(e.Audience) != 0 {
-		for _, v := range e.Audience {
-			if !c.Audience.Contains(v) {
-				return ErrInvalidAudience
+	if len(e.AnyAudience) != 0 {
+		var intersection bool
+		for _, v := range e.AnyAudience {
+			if c.Audience.Contains(v) {
+				intersection = true
+				break
 			}
 		}
+
+		if !intersection {
+			return ErrInvalidAudience
+		}
 	}
 
 	// validate using the e.Time, or time.Now if not provided
diff --git jwt/validation_test.go jwt/validation_test.go
index 7d6e843..71a8de2 100644
--- jwt/validation_test.go
+++ jwt/validation_test.go
@@ -21,7 +21,7 @@ import (
 	"testing"
 	"time"
 
-	"github.com/stretchr/testify/assert"
+	"github.com/go-jose/go-jose/v4/testutils/assert"
 )
 
 func TestFieldsMatch(t *testing.T) {
@@ -35,13 +35,15 @@ func TestFieldsMatch(t *testing.T) {
 	valid := []Expected{
 		{Issuer: "issuer"},
 		{Subject: "subject"},
-		{Audience: Audience{"a1", "a2"}},
-		{Audience: Audience{"a2", "a1"}},
+		{AnyAudience: Audience{"a1", "a2"}},
+		{AnyAudience: Audience{"a2", "a1"}},
+		{AnyAudience: Audience{"a1"}},
+		{AnyAudience: Audience{"a2"}},
 		{ID: "42"},
 	}
 
 	for _, v := range valid {
-		assert.NoError(t, c.Validate(v))
+		assert.NoError(t, c.Validate(v), "expected %#v to match %#v", c, v)
 	}
 
 	invalid := []struct {
@@ -50,7 +52,8 @@ func TestFieldsMatch(t *testing.T) {
 	}{
 		{Expected{Issuer: "invalid-issuer"}, ErrInvalidIssuer},
 		{Expected{Subject: "invalid-subject"}, ErrInvalidSubject},
-		{Expected{Audience: Audience{"invalid-audience"}}, ErrInvalidAudience},
+		{Expected{AnyAudience: Audience{"invalid-audience"}}, ErrInvalidAudience},
+		{Expected{AnyAudience: Audience{"invalid-audience", "invalid2"}}, ErrInvalidAudience},
 		{Expected{ID: "invalid-id"}, ErrInvalidID},
 	}
 
diff --git opaque.go opaque.go
index 68db085..4294272 100644
--- opaque.go
+++ opaque.go
@@ -83,6 +83,9 @@ func (o *opaqueVerifier) verifyPayload(payload []byte, signature []byte, alg Sig
 }
 
 // OpaqueKeyEncrypter is an interface that supports encrypting keys with an opaque key.
+//
+// Note: this cannot currently be implemented outside this package because of its
+// unexported method.
 type OpaqueKeyEncrypter interface {
 	// KeyID returns the kid
 	KeyID() string
diff --git opaque_test.go opaque_test.go
index e3c31aa..163ac18 100644
--- opaque_test.go
+++ opaque_test.go
@@ -249,7 +249,7 @@ func TestOpaqueSignerKeyRotation(t *testing.T) {
 			if err != nil {
 				t.Fatal(err, alg, i)
 			}
-			jws1 = rtSerialize(t, serializer, jws1, vw)
+			jws1 = rtSerialize(t, serializer, jws1, vw, alg)
 			if kid := jws1.Signatures[0].Protected.KeyID; kid != "first" {
 				t.Errorf("expected kid %q but got %q", "first", kid)
 			}
@@ -263,7 +263,7 @@ func TestOpaqueSignerKeyRotation(t *testing.T) {
 			if err != nil {
 				t.Error(err, alg, i)
 			}
-			jws2 = rtSerialize(t, serializer, jws2, vw)
+			jws2 = rtSerialize(t, serializer, jws2, vw, alg)
 			if kid := jws2.Signatures[0].Protected.KeyID; kid != "next" {
 				t.Errorf("expected kid %q but got %q", "next", kid)
 			}
@@ -271,12 +271,12 @@ func TestOpaqueSignerKeyRotation(t *testing.T) {
 	}
 }
 
-func rtSerialize(t *testing.T, serializer func(*JSONWebSignature) (string, error), sig *JSONWebSignature, vk interface{}) *JSONWebSignature {
+func rtSerialize(t *testing.T, serializer func(*JSONWebSignature) (string, error), sig *JSONWebSignature, vk interface{}, alg SignatureAlgorithm) *JSONWebSignature {
 	b, err := serializer(sig)
 	if err != nil {
 		t.Fatal(err)
 	}
-	sig, err = ParseSigned(b)
+	sig, err = ParseSigned(b, []SignatureAlgorithm{alg})
 	if err != nil {
 		t.Fatal(err)
 	}
@@ -286,67 +286,18 @@ func rtSerialize(t *testing.T, serializer func(*JSONWebSignature) (string, error
 	return sig
 }
 
-func TestOpaqueKeyRoundtripJWE(t *testing.T) {
-	keyAlgs := []KeyAlgorithm{
-		ECDH_ES_A128KW, ECDH_ES_A192KW, ECDH_ES_A256KW, A128KW, A192KW, A256KW,
-		RSA1_5, RSA_OAEP, RSA_OAEP_256, A128GCMKW, A192GCMKW, A256GCMKW,
-		PBES2_HS256_A128KW, PBES2_HS384_A192KW, PBES2_HS512_A256KW,
-	}
-	encAlgs := []ContentEncryption{A128GCM, A192GCM, A256GCM, A128CBC_HS256, A192CBC_HS384, A256CBC_HS512}
-	kid := "test-kid"
-
-	serializers := []func(*JSONWebEncryption) (string, error){
-		func(obj *JSONWebEncryption) (string, error) { return obj.CompactSerialize() },
-		func(obj *JSONWebEncryption) (string, error) { return obj.FullSerialize(), nil },
-	}
-
-	for _, alg := range keyAlgs {
-		for _, enc := range encAlgs {
-			for _, testKey := range generateTestKeys(alg, enc) {
-				for _, serializer := range serializers {
-					kew := makeOpaqueKeyEncrypter(t, testKey.enc, alg, kid)
-					encrypter, err := NewEncrypter(
-						enc,
-						Recipient{
-							Algorithm: alg,
-							Key:       kew,
-						},
-						&EncrypterOptions{},
-					)
-					if err != nil {
-						t.Fatal(err, alg)
-					}
-
-					jwe, err := encrypter.Encrypt([]byte("foo bar"))
-					if err != nil {
-						t.Fatal(err, alg)
-					}
-
-					dw := makeOpaqueKeyDecrypter(t, testKey.dec, alg)
-					jwe = jweSerialize(t, serializer, jwe, dw)
-					if jwe.Header.KeyID != kid {
-						t.Errorf("expected jwe kid to equal %s but got %s", kid, jwe.Header.KeyID)
-					}
-
-					out, err := jwe.Decrypt(dw)
-					if err != nil {
-						t.Fatal(err, out)
-					}
-					if string(out) != "foo bar" {
-						t.Errorf("expected decrypted jwe to equal %s but got %s", "foo bar", string(out))
-					}
-				}
-			}
-		}
-	}
-}
-
-func jweSerialize(t *testing.T, serializer func(*JSONWebEncryption) (string, error), jwe *JSONWebEncryption, d OpaqueKeyDecrypter) *JSONWebEncryption {
+func jweSerialize(t *testing.T,
+	serializer func(*JSONWebEncryption) (string, error),
+	jwe *JSONWebEncryption,
+	d OpaqueKeyDecrypter,
+	alg KeyAlgorithm,
+	enc ContentEncryption,
+) *JSONWebEncryption {
 	b, err := serializer(jwe)
 	if err != nil {
 		t.Fatal(err)
 	}
-	jwe, err = ParseEncrypted(b)
+	jwe, err = ParseEncrypted(b, []KeyAlgorithm{alg}, []ContentEncryption{enc})
 	if err != nil {
 		t.Fatal(err)
 	}
diff --git shared.go shared.go
index 489a04e..35130b3 100644
--- shared.go
+++ shared.go
@@ -23,7 +23,7 @@ import (
 	"errors"
 	"fmt"
 
-	"github.com/go-jose/go-jose/v3/json"
+	"github.com/go-jose/go-jose/v4/json"
 )
 
 // KeyAlgorithm represents a key management algorithm.
@@ -71,6 +71,15 @@ var (
 	// ErrUnprotectedNonce indicates that while parsing a JWS or JWE object, a
 	// nonce header parameter was included in an unprotected header object.
 	ErrUnprotectedNonce = errors.New("go-jose/go-jose: Nonce parameter included in unprotected header")
+
+	// ErrMissingX5cHeader indicates that the JWT header is missing x5c headers.
+	ErrMissingX5cHeader = errors.New("go-jose/go-jose: no x5c header present in message")
+
+	// ErrUnsupportedEllipticCurve indicates unsupported or unknown elliptic curve has been found.
+	ErrUnsupportedEllipticCurve = errors.New("go-jose/go-jose: unsupported/unknown elliptic curve")
+
+	// ErrUnsupportedCriticalHeader is returned when a header is marked critical but not supported by go-jose.
+	ErrUnsupportedCriticalHeader = errors.New("go-jose/go-jose: unsupported critical header")
 )
 
 // Key management algorithms
@@ -161,8 +170,8 @@ const (
 )
 
 // supportedCritical is the set of supported extensions that are understood and processed.
-var supportedCritical = map[string]bool{
-	headerB64: true,
+var supportedCritical = map[string]struct{}{
+	headerB64: {},
 }
 
 // rawHeader represents the JOSE header for JWE/JWS objects (used for parsing).
@@ -199,7 +208,7 @@ type Header struct {
 // not be validated with the given verify options.
 func (h Header) Certificates(opts x509.VerifyOptions) ([][]*x509.Certificate, error) {
 	if len(h.certificates) == 0 {
-		return nil, errors.New("go-jose/go-jose: no x5c header present in message")
+		return nil, ErrMissingX5cHeader
 	}
 
 	leaf := h.certificates[0]
@@ -340,6 +349,32 @@ func (parsed rawHeader) getCritical() ([]string, error) {
 	return q, nil
 }
 
+// checkNoCritical verifies there are no critical headers present.
+func (parsed rawHeader) checkNoCritical() error {
+	if _, ok := parsed[headerCritical]; ok {
+		return ErrUnsupportedCriticalHeader
+	}
+
+	return nil
+}
+
+// checkSupportedCritical verifies there are no unsupported critical headers.
+// Supported headers are passed in as a set: map of names to empty structs
+func (parsed rawHeader) checkSupportedCritical(supported map[string]struct{}) error {
+	crit, err := parsed.getCritical()
+	if err != nil {
+		return err
+	}
+
+	for _, name := range crit {
+		if _, ok := supported[name]; !ok {
+			return ErrUnsupportedCriticalHeader
+		}
+	}
+
+	return nil
+}
+
 // getS2C extracts parsed "p2c" from the raw JSON.
 func (parsed rawHeader) getP2C() (int, error) {
 	v := parsed[headerP2C]
@@ -501,7 +536,7 @@ func curveName(crv elliptic.Curve) (string, error) {
 	case elliptic.P521():
 		return "P-521", nil
 	default:
-		return "", fmt.Errorf("go-jose/go-jose: unsupported/unknown elliptic curve")
+		return "", ErrUnsupportedEllipticCurve
 	}
 }
 
diff --git a/shared_test.go b/shared_test.go
new file mode 100644
index 0000000..8e046ec
--- /dev/null
+++ shared_test.go
@@ -0,0 +1,38 @@
+package jose
+
+import (
+	"reflect"
+	"testing"
+)
+
+func TestHeaderEqual(t *testing.T) {
+	header1 := Header{
+		KeyID:        "1-2-3-4",
+		Algorithm:    "test",
+		ExtraHeaders: map[HeaderKey]interface{}{"kid": "1-2-3-4"},
+	}
+	header2 := Header{
+		KeyID:        "1-2-3-4",
+		Algorithm:    "test",
+		ExtraHeaders: map[HeaderKey]interface{}{"kid": "1-2-3-4"},
+	}
+	if !reflect.DeepEqual(header1, header2) {
+		t.Fatalf("header1 and header2 are not equal, expected equal")
+	}
+}
+
+func TestHeaderNotEqual(t *testing.T) {
+	header1 := Header{
+		KeyID:        "1-2-3-4",
+		Algorithm:    "test",
+		ExtraHeaders: map[HeaderKey]interface{}{"kid": "1-2-3-4"},
+	}
+	header2 := Header{
+		KeyID:        "1-2-3-4",
+		Algorithm:    "test",
+		ExtraHeaders: map[HeaderKey]interface{}{"kid": "9-9-9-9"},
+	}
+	if reflect.DeepEqual(header1, header2) {
+		t.Fatalf("header1 and header2 are equal, expected not equal")
+	}
+}
diff --git signing.go signing.go
index 52f3d85..5dbd04c 100644
--- signing.go
+++ signing.go
@@ -25,7 +25,7 @@ import (
 	"errors"
 	"fmt"
 
-	"github.com/go-jose/go-jose/v3/json"
+	"github.com/go-jose/go-jose/v4/json"
 )
 
 // NonceSource represents a source of random nonces to go into JWS objects
@@ -49,6 +49,11 @@ type Signer interface {
 //   - JSONWebKey
 //   - []byte (an HMAC key)
 //   - Any type that satisfies the OpaqueSigner interface
+//
+// If the key is an HMAC key, it must have at least as many bytes as the relevant hash output:
+//   - HS256: 32 bytes
+//   - HS384: 48 bytes
+//   - HS512: 64 bytes
 type SigningKey struct {
 	Algorithm SignatureAlgorithm
 	Key       interface{}
@@ -353,8 +358,15 @@ func (ctx *genericSigner) Options() SignerOptions {
 //   - *rsa.PublicKey
 //   - *JSONWebKey
 //   - JSONWebKey
+//   - *JSONWebKeySet
+//   - JSONWebKeySet
 //   - []byte (an HMAC key)
 //   - Any type that implements the OpaqueVerifier interface.
+//
+// If the key is an HMAC key, it must have at least as many bytes as the relevant hash output:
+//   - HS256: 32 bytes
+//   - HS384: 48 bytes
+//   - HS512: 64 bytes
 func (obj JSONWebSignature) Verify(verificationKey interface{}) ([]byte, error) {
 	err := obj.DetachedVerify(obj.payload, verificationKey)
 	if err != nil {
@@ -378,7 +390,10 @@ func (obj JSONWebSignature) UnsafePayloadWithoutVerification() []byte {
 // The verificationKey argument must have one of the types allowed for the
 // verificationKey argument of JSONWebSignature.Verify().
 func (obj JSONWebSignature) DetachedVerify(payload []byte, verificationKey interface{}) error {
-	key := tryJWKS(verificationKey, obj.headers()...)
+	key, err := tryJWKS(verificationKey, obj.headers()...)
+	if err != nil {
+		return err
+	}
 	verifier, err := newVerifier(key)
 	if err != nil {
 		return err
@@ -389,15 +404,23 @@ func (obj JSONWebSignature) DetachedVerify(payload []byte, verificationKey inter
 	}
 
 	signature := obj.Signatures[0]
-	headers := signature.mergedHeaders()
-	critical, err := headers.getCritical()
-	if err != nil {
-		return err
+
+	if signature.header != nil {
+		// Per https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.11,
+		// 4.1.11. "crit" (Critical) Header Parameter
+		// "When used, this Header Parameter MUST be integrity
+		// protected; therefore, it MUST occur only within the JWS
+		// Protected Header."
+		err = signature.header.checkNoCritical()
+		if err != nil {
+			return err
+		}
 	}
 
-	for _, name := range critical {
-		if !supportedCritical[name] {
-			return ErrCryptoFailure
+	if signature.protected != nil {
+		err = signature.protected.checkSupportedCritical(supportedCritical)
+		if err != nil {
+			return err
 		}
 	}
 
@@ -406,6 +429,7 @@ func (obj JSONWebSignature) DetachedVerify(payload []byte, verificationKey inter
 		return ErrCryptoFailure
 	}
 
+	headers := signature.mergedHeaders()
 	alg := headers.getSignatureAlgorithm()
 	err = verifier.verifyPayload(input, signature.Signature, alg)
 	if err == nil {
@@ -443,7 +467,10 @@ func (obj JSONWebSignature) VerifyMulti(verificationKey interface{}) (int, Signa
 // The verificationKey argument must have one of the types allowed for the
 // verificationKey argument of JSONWebSignature.Verify().
 func (obj JSONWebSignature) DetachedVerifyMulti(payload []byte, verificationKey interface{}) (int, Signature, error) {
-	key := tryJWKS(verificationKey, obj.headers()...)
+	key, err := tryJWKS(verificationKey, obj.headers()...)
+	if err != nil {
+		return -1, Signature{}, err
+	}
 	verifier, err := newVerifier(key)
 	if err != nil {
 		return -1, Signature{}, err
@@ -451,14 +478,22 @@ func (obj JSONWebSignature) DetachedVerifyMulti(payload []byte, verificationKey
 
 outer:
 	for i, signature := range obj.Signatures {
-		headers := signature.mergedHeaders()
-		critical, err := headers.getCritical()
-		if err != nil {
-			continue
+		if signature.header != nil {
+			// Per https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.11,
+			// 4.1.11. "crit" (Critical) Header Parameter
+			// "When used, this Header Parameter MUST be integrity
+			// protected; therefore, it MUST occur only within the JWS
+			// Protected Header."
+			err = signature.header.checkNoCritical()
+			if err != nil {
+				continue outer
+			}
 		}
 
-		for _, name := range critical {
-			if !supportedCritical[name] {
+		if signature.protected != nil {
+			// Check for only supported critical headers
+			err = signature.protected.checkSupportedCritical(supportedCritical)
+			if err != nil {
 				continue outer
 			}
 		}
@@ -468,6 +503,7 @@ outer:
 			continue
 		}
 
+		headers := signature.mergedHeaders()
 		alg := headers.getSignatureAlgorithm()
 		err = verifier.verifyPayload(input, signature.Signature, alg)
 		if err == nil {
diff --git signing_test.go signing_test.go
index 63404dc..d86ad0e 100644
--- signing_test.go
+++ signing_test.go
@@ -28,7 +28,7 @@ import (
 	"strings"
 	"testing"
 
-	"github.com/go-jose/go-jose/v3/json"
+	"github.com/go-jose/go-jose/v4/json"
 )
 
 type staticNonceSource string
@@ -59,7 +59,7 @@ func RoundtripJWS(sigAlg SignatureAlgorithm, serializer func(*JSONWebSignature)
 		return fmt.Errorf("error on serialize: %s", err)
 	}
 
-	obj, err = ParseSigned(msg)
+	obj, err = ParseSigned(msg, []SignatureAlgorithm{sigAlg})
 	if err != nil {
 		return fmt.Errorf("error on parse: %s", err)
 	}
@@ -109,10 +109,12 @@ func TestRoundtripsJWS(t *testing.T) {
 		signingKey, verificationKey := GenerateSigningTestKey(alg)
 
 		for i, serializer := range serializers {
-			err := RoundtripJWS(alg, serializer, corrupter, signingKey, verificationKey, "test_nonce")
-			if err != nil {
-				t.Error(err, alg, i)
-			}
+			t.Run(fmt.Sprintf("RoundTripsJWS%d-%s", i, alg), func(t *testing.T) {
+				err := RoundtripJWS(alg, serializer, corrupter, signingKey, verificationKey, "test_nonce")
+				if err != nil {
+					t.Error(err)
+				}
+			})
 		}
 	}
 }
@@ -235,6 +237,8 @@ func TestMultiRecipientJWS(t *testing.T) {
 	sharedKey := []byte{
 		0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
 		0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+		0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
+		0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
 	}
 	jwkSharedKey := JSONWebKey{
 		KeyID: "123",
@@ -263,7 +267,7 @@ func TestMultiRecipientJWS(t *testing.T) {
 
 	msg := obj.FullSerialize()
 
-	obj, err = ParseSigned(msg)
+	obj, err = ParseSigned(msg, []SignatureAlgorithm{RS256, HS384, HS512})
 	if err != nil {
 		t.Fatal("error on parse: ", err)
 	}
@@ -304,7 +308,7 @@ func GenerateSigningTestKey(sigAlg SignatureAlgorithm) (sig, ver interface{}) {
 		sig = rsaTestKey
 		ver = &rsaTestKey.PublicKey
 	case HS256, HS384, HS512:
-		sig, _, _ = randomKeyGenerator{size: 16}.genKey()
+		sig, _, _ = randomKeyGenerator{size: 64}.genKey()
 		ver = sig
 	case ES256:
 		key, _ := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
@@ -349,7 +353,20 @@ func TestInvalidJWS(t *testing.T) {
 	}
 	obj.Signatures[0].header = &rawHeader{}
 
-	if err = obj.Signatures[0].header.set(headerCritical, []string{"TEST"}); err != nil {
+	// headerB64 is known, but should be in protected
+	if err = obj.Signatures[0].header.set(headerCritical, []string{headerB64}); err != nil {
+		t.Fatal(err)
+	}
+
+	_, err = obj.Verify(&rsaTestKey.PublicKey)
+	if err == nil {
+		t.Error("should not verify message with an unprotected but known crit header")
+	}
+
+	// reject unknown critical headers
+	obj.Signatures[0].protected = &rawHeader{}
+	obj.Signatures[0].header = &rawHeader{}
+	if err = obj.Signatures[0].protected.set(headerCritical, []string{"unknown-critical-header"}); err != nil {
 		t.Fatal(err)
 	}
 
@@ -412,7 +429,7 @@ func TestSignerKid(t *testing.T) {
 
 	serialized := signed.FullSerialize()
 
-	parsed, err := ParseSigned(serialized)
+	parsed, err := ParseSigned(serialized, []SignatureAlgorithm{ES256})
 	if err != nil {
 		t.Error("problem parsing signed object", err)
 	}
@@ -452,7 +469,7 @@ func TestEmbedJwk(t *testing.T) {
 		t.Error("Failed to sign payload")
 	}
 
-	object, err = ParseSigned(object.FullSerialize())
+	object, err = ParseSigned(object.FullSerialize(), []SignatureAlgorithm{ES256})
 	if err != nil {
 		t.Error("Failed to parse jws")
 	}
@@ -473,7 +490,7 @@ func TestEmbedJwk(t *testing.T) {
 		t.Error("Failed to sign payload")
 	}
 
-	object, err = ParseSigned(object.FullSerialize())
+	object, err = ParseSigned(object.FullSerialize(), []SignatureAlgorithm{ES256})
 	if err != nil {
 		t.Error("Failed to parse jws")
 	}
@@ -540,7 +557,7 @@ func TestSignerExtraHeaderInclusion(t *testing.T) {
 		t.Error("Failed to sign payload")
 	}
 
-	object, err = ParseSigned(object.FullSerialize())
+	object, err = ParseSigned(object.FullSerialize(), []SignatureAlgorithm{ES256})
 	if err != nil {
 		t.Error("Failed to parse jws")
 	}
@@ -589,7 +606,7 @@ func TestSignerB64(t *testing.T) {
 		t.Errorf("Invalid serialization, got '%s', expected '%s'", msg, exp)
 	}
 
-	parsed, err := ParseSigned(msg)
+	parsed, err := ParseSigned(msg, []SignatureAlgorithm{HS256})
 	if err != nil {
 		t.Errorf("Error on parse: %s", err)
 	}
@@ -607,7 +624,7 @@ func TestSignerB64(t *testing.T) {
 func BenchmarkParseSigned(b *testing.B) {
 	msg := `eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c`
 	for i := 0; i < b.N; i++ {
-		_, err := ParseSigned(msg)
+		_, err := ParseSigned(msg, []SignatureAlgorithm{HS256})
 		if err != nil {
 			b.Errorf("Error on parse: %s", err)
 		}
diff --git symmetric.go symmetric.go
index 10d8e19..f2ff29e 100644
--- symmetric.go
+++ symmetric.go
@@ -21,6 +21,7 @@ import (
 	"crypto/aes"
 	"crypto/cipher"
 	"crypto/hmac"
+	"crypto/pbkdf2"
 	"crypto/rand"
 	"crypto/sha256"
 	"crypto/sha512"
@@ -30,9 +31,7 @@ import (
 	"hash"
 	"io"
 
-	"golang.org/x/crypto/pbkdf2"
-
-	josecipher "github.com/go-jose/go-jose/v3/cipher"
+	josecipher "github.com/go-jose/go-jose/v4/cipher"
 )
 
 // RandReader is a cryptographically secure random number generator (stubbed out in tests).
@@ -330,7 +329,10 @@ func (ctx *symmetricKeyCipher) encryptKey(cek []byte, alg KeyAlgorithm) (recipie
 
 		// derive key
 		keyLen, h := getPbkdf2Params(alg)
-		key := pbkdf2.Key(ctx.key, salt, ctx.p2c, keyLen, h)
+		key, err := pbkdf2.Key(h, string(ctx.key), salt, ctx.p2c, keyLen)
+		if err != nil {
+			return recipientInfo{}, nil
+		}
 
 		// use AES cipher with derived key
 		block, err := aes.NewCipher(key)
@@ -364,11 +366,21 @@ func (ctx *symmetricKeyCipher) encryptKey(cek []byte, alg KeyAlgorithm) (recipie
 
 // Decrypt the content encryption key.
 func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipientInfo, generator keyGenerator) ([]byte, error) {
-	switch headers.getAlgorithm() {
-	case DIRECT:
-		cek := make([]byte, len(ctx.key))
-		copy(cek, ctx.key)
-		return cek, nil
+	if recipient == nil {
+		return nil, fmt.Errorf("go-jose/go-jose: missing recipient")
+	}
+
+	alg := headers.getAlgorithm()
+	if alg == DIRECT {
+		return bytes.Clone(ctx.key), nil
+	}
+
+	encryptedKey := recipient.encryptedKey
+	if len(encryptedKey) == 0 {
+		return nil, fmt.Errorf("go-jose/go-jose: missing JWE Encrypted Key")
+	}
+
+	switch alg {
 	case A128GCMKW, A192GCMKW, A256GCMKW:
 		aead := newAESGCM(len(ctx.key))
 
@@ -383,7 +395,7 @@ func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipien
 
 		parts := &aeadParts{
 			iv:         iv.bytes(),
-			ciphertext: recipient.encryptedKey,
+			ciphertext: encryptedKey,
 			tag:        tag.bytes(),
 		}
 
@@ -399,7 +411,7 @@ func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipien
 			return nil, err
 		}
 
-		cek, err := josecipher.KeyUnwrap(block, recipient.encryptedKey)
+		cek, err := josecipher.KeyUnwrap(block, encryptedKey)
 		if err != nil {
 			return nil, err
 		}
@@ -432,7 +444,10 @@ func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipien
 
 		// derive key
 		keyLen, h := getPbkdf2Params(alg)
-		key := pbkdf2.Key(ctx.key, salt, p2c, keyLen, h)
+		key, err := pbkdf2.Key(h, string(ctx.key), salt, p2c, keyLen)
+		if err != nil {
+			return nil, err
+		}
 
 		// use AES cipher with derived key
 		block, err := aes.NewCipher(key)
@@ -440,7 +455,7 @@ func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipien
 			return nil, err
 		}
 
-		cek, err := josecipher.KeyUnwrap(block, recipient.encryptedKey)
+		cek, err := josecipher.KeyUnwrap(block, encryptedKey)
 		if err != nil {
 			return nil, err
 		}
@@ -454,7 +469,7 @@ func (ctx *symmetricKeyCipher) decryptKey(headers rawHeader, recipient *recipien
 func (ctx symmetricMac) signPayload(payload []byte, alg SignatureAlgorithm) (Signature, error) {
 	mac, err := ctx.hmac(payload, alg)
 	if err != nil {
-		return Signature{}, errors.New("go-jose/go-jose: failed to compute hmac")
+		return Signature{}, err
 	}
 
 	return Signature{
@@ -486,12 +501,24 @@ func (ctx symmetricMac) verifyPayload(payload []byte, mac []byte, alg SignatureA
 func (ctx symmetricMac) hmac(payload []byte, alg SignatureAlgorithm) ([]byte, error) {
 	var hash func() hash.Hash
 
+	// https://datatracker.ietf.org/doc/html/rfc7518#section-3.2
+	// A key of the same size as the hash output (for instance, 256 bits for
+	// "HS256") or larger MUST be used
 	switch alg {
 	case HS256:
+		if len(ctx.key)*8 < 256 {
+			return nil, ErrInvalidKeySize
+		}
 		hash = sha256.New
 	case HS384:
+		if len(ctx.key)*8 < 384 {
+			return nil, ErrInvalidKeySize
+		}
 		hash = sha512.New384
 	case HS512:
+		if len(ctx.key)*8 < 512 {
+			return nil, ErrInvalidKeySize
+		}
 		hash = sha512.New
 	default:
 		return nil, ErrUnsupportedAlgorithm
diff --git symmetric_test.go symmetric_test.go
index 6dbc6a0..5d87dc0 100644
--- symmetric_test.go
+++ symmetric_test.go
@@ -19,12 +19,11 @@ package jose
 import (
 	"bytes"
 	"crypto/cipher"
+	"crypto/pbkdf2"
 	"crypto/rand"
 	"crypto/sha256"
 	"io"
 	"testing"
-
-	"golang.org/x/crypto/pbkdf2"
 )
 
 func TestInvalidSymmetricAlgorithms(t *testing.T) {
@@ -182,7 +181,10 @@ func TestVectorPBES2_HS256A_128KW(t *testing.T) {
 		188, 66, 125, 36, 200, 222, 124, 5, 103, 249, 52, 117, 184, 140, 81,
 		246, 158, 161, 177, 20, 33, 245, 57, 59, 4}
 
-	derivedKey := pbkdf2.Key(cipher.key, salt, cipher.p2c, 16, sha256.New)
+	derivedKey, err := pbkdf2.Key(sha256.New, string(cipher.key), salt, cipher.p2c, 16)
+	if err != nil {
+		t.Fatal("Unable to encrypt:", err)
+	}
 	if !bytes.Equal(derivedKey, expectedDerivedKey) {
 		t.Error("Derived key did not match")
 	}
diff --git a/testutils/assert/equal.go b/testutils/assert/equal.go
new file mode 100644
index 0000000..e0b22e5
--- /dev/null
+++ testutils/assert/equal.go
@@ -0,0 +1,103 @@
+package assert
+
+import (
+	"bytes"
+	"encoding/json"
+	"errors"
+	"fmt"
+	"slices"
+)
+
+type TInterface interface {
+	Errorf(format string, args ...any)
+	Helper()
+}
+
+func Equal[T comparable](t TInterface, actual, expected T) bool {
+	t.Helper()
+	if expected != actual {
+		t.Errorf("expected '%+v', but actual value is '%+v'", expected, actual)
+		return false
+	}
+	return true
+}
+
+func EqualSlice[T comparable](t TInterface, actual, expected []T) bool {
+	t.Helper()
+	if !slices.Equal(expected, actual) {
+		t.Errorf("expected slice (%+v) is not equal to actual slice (%+v)", expected, actual)
+		return false
+	}
+	return true
+}
+
+func EqualJSON[K comparable, V comparable](t TInterface, actual, expected map[K]V) bool {
+	t.Helper()
+	if len(expected) != len(actual) {
+		t.Errorf("length mismatch: expected %d, got %d", len(expected), len(actual))
+		return false
+	}
+
+	for i := range expected {
+		if _, ok := actual[i]; !ok {
+			t.Errorf("expected map's keys (%+v) don't match actual map's keys (%+v)", expected, actual)
+			return false
+		}
+		v1, err1 := json.Marshal(expected[i])
+		v2, err2 := json.Marshal(actual[i])
+		if err1 != nil || err2 != nil || !bytes.Equal(v1, v2) {
+			t.Errorf("expected JSON output (%+v) is not equal to actual JSON output (%+v)", expected, actual)
+			return false
+		}
+	}
+	return true
+}
+
+func NoError(t TInterface, err error, errMsg ...any) bool {
+	t.Helper()
+	if err != nil {
+		t.Errorf("expected no error. Got error: %s%s", err, getMsgParameter(errMsg))
+		return false
+	}
+	return true
+}
+
+func Error(t TInterface, err error, errMsg ...any) bool {
+	t.Helper()
+	if err == nil {
+		t.Errorf("expected an error, but got none%s", getMsgParameter(errMsg))
+		return false
+	}
+	return true
+}
+
+func ErrorIs(t TInterface, actual, expected error) bool {
+	t.Helper()
+	if !errors.Is(actual, expected) {
+		t.Errorf("expected error %s, got %s", expected, actual)
+		return false
+	}
+	return true
+}
+
+func Len[T comparable](t TInterface, expected []T, length int) bool {
+	t.Helper()
+	if len(expected) != length {
+		t.Errorf("expected length %d, got %d", length, len(expected))
+		return false
+	}
+	return true
+}
+
+func getMsgParameter(errMsg ...any) string {
+	if len(errMsg) > 0 {
+		msg := errMsg[0]
+		errMsgString, ok := msg.(string)
+		if ok && len(errMsg) > 1 {
+			return ". Message: " + fmt.Sprintf(errMsgString, errMsg[1:]...)
+		} else {
+			return ". Message: " + errMsgString
+		}
+	}
+	return ""
+}
diff --git a/testutils/assert/equal_test.go b/testutils/assert/equal_test.go
new file mode 100644
index 0000000..1d46284
--- /dev/null
+++ testutils/assert/equal_test.go
@@ -0,0 +1,135 @@
+package assert
+
+import (
+	"errors"
+	"fmt"
+	"testing"
+)
+
+type mockT struct {
+	errors []string
+	failed bool
+}
+
+func (m *mockT) Errorf(format string, args ...any) {
+	m.errors = append(m.errors, fmt.Sprintf(format, args...))
+	m.failed = true
+}
+func (m *mockT) Helper() {
+
+}
+func TestEqual(t *testing.T) {
+	m := &mockT{}
+	if !Equal(m, "one", "one") {
+		t.Fatalf("expected equal")
+	}
+	m.failed = false
+	m.errors = []string{}
+	if Equal(m, "one", "two") {
+		if !m.failed {
+			t.Fatalf("test didn't fail. Expected test to have failed = true")
+		}
+	}
+}
+
+func TestEqualSlice(t *testing.T) {
+	m := &mockT{}
+	if !EqualSlice(m, []string{"one", "two"}, []string{"one", "two"}) {
+		t.Fatalf("expected equal")
+	}
+	m.failed = false
+	m.errors = []string{}
+	if EqualSlice(m, []string{"one", "two"}, []string{"one", "three"}) {
+		if !m.failed {
+			t.Fatalf("test didn't fail. Expected test to have failed = true")
+		}
+	}
+}
+
+func TestJSON(t *testing.T) {
+	m := &mockT{}
+	if !EqualJSON(m, map[string]string{"one": "two"}, map[string]string{"one": "two"}) {
+		t.Fatalf("expected equal")
+	}
+	m.failed = false
+	m.errors = []string{}
+	if EqualJSON(m, map[string]string{"one": "two"}, map[string]string{"one": "three"}) {
+		if !m.failed {
+			t.Fatalf("test didn't fail. Expected test to have failed = true")
+		}
+	}
+	m.failed = false
+	m.errors = []string{}
+	if EqualJSON(m, map[string]string{"one": "two"}, map[string]string{"two": "three"}) {
+		if !m.failed {
+			t.Fatalf("test didn't fail. Expected test to have failed = true")
+		}
+	}
+}
+
+func TestNoError(t *testing.T) {
+	m := &mockT{}
+	if !NoError(m, nil) {
+		t.Fatalf("expected no error")
+	}
+	m.failed = false
+	m.errors = []string{}
+	if NoError(m, errors.New("error")) {
+		if !m.failed {
+			t.Fatalf("test didn't fail. Expected test to have failed = true")
+		}
+	}
+}
+
+func TestError(t *testing.T) {
+	m := &mockT{}
+	if !Error(m, errors.New("error")) {
+		t.Fatalf("expected error")
+	}
+	m.failed = false
+	m.errors = []string{}
+	if Error(m, nil) {
+		if !m.failed {
+			t.Fatalf("test didn't fail. Expected test to have failed = true")
+		}
+	}
+}
+
+func TestErrorIs(t *testing.T) {
+	m := &mockT{}
+
+	var ErrNotFound = errors.New("not found")
+	if !ErrorIs(m, ErrNotFound, ErrNotFound) {
+		t.Fatalf("expected error not found")
+	}
+	m.failed = false
+	m.errors = []string{}
+	if ErrorIs(m, errors.New("another error"), ErrNotFound) {
+		if !m.failed {
+			t.Fatalf("test didn't fail. Expected test to have failed = true")
+		}
+	}
+}
+
+func TestLen(t *testing.T) {
+	m := &mockT{}
+
+	if !Len(m, []int{1, 1, 1, 1}, 4) {
+		t.Fatalf("expected len 4")
+	}
+	m.failed = false
+	m.errors = []string{}
+	if Len(m, []int{1, 1, 1, 1}, 5) {
+		if !m.failed {
+			t.Fatalf("test didn't fail. Expected test to have failed = true")
+		}
+	}
+}
+
+func TestGetMsgParameter(t *testing.T) {
+	message := getMsgParameter("this is the %s", "message")
+	expected := ". Message: this is the message"
+	if message != expected {
+		t.Fatalf("got %s, expected %s", message, expected)
+	}
+}
diff --git a/testutils/require/equal.go b/testutils/require/equal.go
new file mode 100644
index 0000000..8d41b52
--- /dev/null
+++ testutils/require/equal.go
@@ -0,0 +1,25 @@
+package require
+
+import (
+	"github.com/go-jose/go-jose/v4/testutils/assert"
+)
+
+type TInterface interface {
+	Errorf(format string, args ...any)
+	Helper()
+	FailNow()
+}
+
+func NoError(t TInterface, err error, errMsg ...any) {
+	t.Helper()
+	if !assert.NoError(t, err, errMsg...) {
+		t.FailNow()
+	}
+}
+
+func Equal[T comparable](t TInterface, actual, expected T) {
+	t.Helper()
+	if !assert.Equal(t, actual, expected) {
+		t.FailNow()
+	}
+}
diff --git a/testutils/require/equal_test.go b/testutils/require/equal_test.go
new file mode 100644
index 0000000..df23363
--- /dev/null
+++ testutils/require/equal_test.go
@@ -0,0 +1,54 @@
+package require
+
+import (
+	"errors"
+	"fmt"
+	"testing"
+)
+
+type mockT struct {
+	errors  []string
+	failed  bool
+	failnow bool
+}
+
+func (m *mockT) Errorf(format string, args ...any) {
+	m.errors = append(m.errors, fmt.Sprintf(format, args...))
+	m.failed = true
+}
+func (m *mockT) Helper() {
+
+}
+
+func (m *mockT) FailNow() {
+	m.failnow = true
+}
+func TestEqual(t *testing.T) {
+	m := &mockT{}
+	Equal(m, "one", "one")
+	if m.failnow {
+		t.Fatalf("expected equal")
+	}
+	m.failed = false
+	m.failnow = false
+	m.errors = []string{}
+	Equal(m, "one", "two")
+	if !m.failnow {
+		t.Fatalf("test didn't fail. Expected test to have failnow = true")
+	}
+}
+
+func TestNoError(t *testing.T) {
+	m := &mockT{}
+	NoError(m, nil)
+	if m.failnow {
+		t.Fatalf("expected no error")
+	}
+	m.failed = false
+	m.failnow = false
+	m.errors = []string{}
+	NoError(m, errors.New("error"))
+	if !m.failnow {
+		t.Fatalf("test didn't fail. Expected test to have failnow = true")
+	}
+}

Description

Major version bump from v3 to v4 of go-jose. Key changes: mandatory algorithm validation on parse (preventing algorithm confusion attacks), decompression bomb protection, HMAC minimum key size enforcement, empty encrypted key rejection, removal of external dependencies (kingpin, testify, go-cmp, x/crypto), module path update, and JWT API hardening (restricted encryption algorithms, AudienceAnyAudience semantics, Serialize() replacing CompactSerialize()/FullSerialize()).

Possible Issues

  1. jose-util/crypto.go:decrypt accepts all algorithms (allKeyAlgorithms, allContentEncryption) when parsing, bypassing the algorithm restriction the library is designed to enforce. Callers relying on the CLI for secure decryption won't benefit from algorithm pinning.
  2. jose-util/crypto.go:sign — flag set name is "encrypt" instead of "sign".
  3. jose-util/main.gousage() prints "jose-utils" (with trailing 's') but binary is jose-util.
  4. jwt/jwt.go:validateKeyEncryptionAlgorithm rejects ED25519 as a key encryption algorithm, but ED25519 is a KeyAlgorithm constant typically used for signing, not encryption. This may confuse users who accidentally pass it, but it could also reject a legitimate (if unusual) use.
  5. jwt/builder.go — removing FullSerialize() and CompactSerialize() in favor of a single Serialize() is a breaking change; NestedBuilder.Serialize lost the old FullSerialize path entirely.
  6. encoding.go:inflate — the decompression limit uses max(250_000, 10*int64(len(input))), which for very large compressed inputs allows proportionally large output. A fixed absolute cap may be safer.
  7. jwk.goErrUnsupportedKeyType returned for unknown kty values. Callers iterating a JWKS and unmarshaling individual keys need to handle this sentinel; previously they'd get a descriptive string error. This is intentional per RFC 7517 §5 but could break existing error-matching code.

Security Hotspots

  1. Algorithm confusion preventionParseSigned, ParseEncrypted now require explicit algorithm allowlists. If callers pass overly broad lists (as jose-util does with allKeyAlgorithms), the protection is nullified.
  2. Decompression bomb (encoding.go:inflate) — limit is max(250KB, 10× compressed size). An attacker supplying a 25KB compressed payload can decompress up to 250KB; supplying 100KB compressed can decompress up to 1MB. Sufficient for most use cases but the multiplicative factor could still be abused with moderately compressible data.
  3. HMAC key size enforcement (symmetric.go) — now rejects keys shorter than hash output. Good, but enforcement happens at sign/verify time, not at NewSigner, so invalid keys aren't caught early.
  4. Empty encrypted key (symmetric.go, asymmetric.go) — now explicitly rejected, fixing a potential oracle or panic vector.
  5. tryJWKS now returns error when no KID matches, preventing silent fallback to the raw key set object (which would fail verification with a confusing error).
Changes

Changes

CI/Config:

  • .github/workflows/go.yml — bumped Go versions to 1.24.x/1.25.x, updated action versions, broadened branch triggers
  • .github/workflows/cram.yml — new workflow for jose-util cram tests
  • .github/dependabot.yml — new dependabot config

Module:

  • go.mod — module path v3v4, Go 1.24.0 minimum, all external deps removed
  • All import paths updated from v3 to v4

Core library (jwe.go, jws.go, crypter.go, signing.go):

  • ParseEncrypted/ParseSigned/ParseDetached require algorithm allowlists
  • New exported ParseEncryptedCompact, ParseEncryptedJSON, ParseSignedCompact, ParseSignedJSON
  • crit header handling refactored into checkNoCritical/checkSupportedCritical
  • tryJWKS returns (interface{}, error) instead of interface{}
  • makeJWERecipient supports JSONWebKey by value (not just pointer)
  • ErrUnexpectedSignatureAlgorithm structured error type added

Crypto (asymmetric.go, symmetric.go, cipher/key_wrap.go):

  • Nil recipient checks added to decryptKey methods
  • Empty encrypted key explicitly rejected before key unwrap
  • KeyUnwrap validates minimum ciphertext length
  • HMAC minimum key size enforced per RFC 7518 §3.2
  • pbkdf2 migrated from x/crypto to stdlib crypto/pbkdf2

Encoding (encoding.go):

  • inflate bounded by max(250KB, 10× input) to prevent decompression bombs
  • base64URLDecode helper removed; direct base64.RawURLEncoding.DecodeString used (no more padding tolerance)

JWK (jwk.go):

  • ErrUnsupportedKeyType sentinel for unknown kty
  • ErrJWKSKidNotFound sentinel when JWKS lookup fails
  • Missing field detection fixed (switch→individual if statements for RSA/EC/Ed25519)

JWT (jwt/):

  • Builder.CompactSerialize/FullSerialize → single Serialize
  • Expected.AudienceExpected.AnyAudience (intersection semantics)
  • ParseEncrypted rejects asymmetric/PBES key algorithms
  • ParseSignedAndEncrypted takes explicit algorithm lists for both layers

jose-util:

  • Migrated from kingpin to stdlib flag
  • Removed b64decode command
  • Functions return error instead of calling app.FatalIfError

Testing:

  • Replaced testify and go-cmp with internal testutils/assert and testutils/require
  • New tests for decompression limits, empty encrypted keys, algorithm validation, HMAC key sizes
sequenceDiagram
    participant Caller
    participant ParseEncrypted
    participant sanitized
    participant Decrypt
    participant decryptKey
    participant KeyUnwrap

    Caller->>ParseEncrypted: input, keyAlgorithms, contentEncryption
    ParseEncrypted->>sanitized: raw parsed data, keyAlgorithms, contentEncryption
    sanitized->>sanitized: validate alg/enc against allowlists
    sanitized-->>ParseEncrypted: *JSONWebEncryption or error
    ParseEncrypted-->>Caller: *JSONWebEncryption

    Caller->>Decrypt: decryptionKey
    Decrypt->>Decrypt: checkNoCritical()
    Decrypt->>Decrypt: tryJWKS(decryptionKey)
    Decrypt->>decryptKey: headers, recipient, generator
    decryptKey->>decryptKey: check recipient != nil
    decryptKey->>decryptKey: check encryptedKey non-empty
    decryptKey->>KeyUnwrap: block, encryptedKey
    KeyUnwrap->>KeyUnwrap: validate min length
    KeyUnwrap-->>decryptKey: CEK or error
    decryptKey-->>Decrypt: CEK
    Decrypt->>Decrypt: decrypt ciphertext with CEK
    Decrypt->>Decrypt: inflate with size limit
    Decrypt-->>Caller: plaintext or error
Loading

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants