Skip to content

Duplicate header params across protected & per-recipient headers (violates RFC 7516 §7.2.1) #1470

@ilya-korotya

Description

@ilya-korotya

Summary
When creating a JWE with JSON Serialization, the same header parameters (e.g., kid, alg) appear in both the protected and per-recipient unprotected (header) sections.
RFC 7516 requires that the header parameter names across the three locations—protected, unprotected, and per-recipient header—“MUST be disjoint.”

RFC reference: https://www.rfc-editor.org/rfc/rfc7516#section-7.2.1


package jwxmergeissue

import (
	"crypto/rand"
	"crypto/rsa"
	"encoding/base64"
	"encoding/json"
	"testing"

	"github.com/lestrrat-go/jwx/v3/jwa"
	"github.com/lestrrat-go/jwx/v3/jwe"
	"github.com/stretchr/testify/require"
)

func TestDuplicateHeaders(t *testing.T) {
	recipientPrivateKey, err := rsa.GenerateKey(rand.Reader, 2048)
	require.NoError(t, err)

	// Per-recipient unprotected header includes kid
	recipientHeaders := jwe.NewHeaders()
	require.NoError(t, recipientHeaders.Set("kid", "recipient1"))

	recipient := jwe.WithKey(
		jwa.RSA_OAEP_256(),
		recipientPrivateKey.PublicKey,
		jwe.WithPerRecipientHeaders(recipientHeaders),
	)

	// Produce JSON Serialization (flattened for single recipient)
	ret, err := jwe.Encrypt([]byte("Hello!"), jwe.WithJSON(), recipient)
	require.NoError(t, err)

	var jweJSON map[string]any
	require.NoError(t, json.Unmarshal(ret, &jweJSON))

	// Decode protected header (Base64URL)
	protectedB64, ok := jweJSON["protected"].(string)
	require.True(t, ok)

	protectedJSON, err := base64.RawURLEncoding.DecodeString(protectedB64)
	require.NoError(t, err)

	var protected map[string]any
	require.NoError(t, json.Unmarshal(protectedJSON, &protected))

	// Per-recipient header
	recipHeader, ok := jweJSON["header"].(map[string]any)
	require.True(t, ok)

	// Fail if the same names appear in both places (violates RFC 7516 §7.2.1)
	if _, has := protected["kid"]; has {
		if _, also := recipHeader["kid"]; also {
			t.Fatalf("duplicate 'kid' present in both protected and per-recipient headers")
		}
	}
	if _, has := protected["alg"]; has {
		if _, also := recipHeader["alg"]; also {
			t.Fatalf("duplicate 'alg' present in both protected and per-recipient headers")
		}
	}
}

Expected behavior
Library should prevent duplicate header names across protected, unprotected, and per-recipient headers

go.mod file:

module github.com/ilya-korotya/jwx_merge_issue

go 1.24.4

toolchain go1.24.8

require (
	github.com/lestrrat-go/jwx/v3 v3.0.11
	github.com/stretchr/testify v1.11.1
)

require (
	github.com/davecgh/go-spew v1.1.1 // indirect
	github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
	github.com/goccy/go-json v0.10.3 // indirect
	github.com/lestrrat-go/blackmagic v1.0.4 // indirect
	github.com/lestrrat-go/httpcc v1.0.1 // indirect
	github.com/lestrrat-go/httprc/v3 v3.0.1 // indirect
	github.com/lestrrat-go/option v1.0.1 // indirect
	github.com/lestrrat-go/option/v2 v2.0.0 // indirect
	github.com/pmezard/go-difflib v1.0.0 // indirect
	github.com/segmentio/asm v1.2.0 // indirect
	golang.org/x/crypto v0.42.0 // indirect
	golang.org/x/sys v0.36.0 // indirect
	gopkg.in/yaml.v3 v3.0.1 // indirect
)

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions