ory/hydra · error

priv/pub JWK key mismatch

Error message

priv/pub JWK key mismatch

What it means

LoadJSONWebKey (oryx/josex/utils.go:39) parses a JSON Web Key and, after validating it, checks that the key's public/private character matches the `pub` flag the caller passed. `jwk.IsPublic()` reports whether the JWK contains only public members (no "d" private exponent etc.). The error means the caller asked for, say, a public key but the JSON contained private key material — or vice versa.

Source

Thrown at oryx/josex/utils.go:39

	"encoding/pem"
	"errors"
	"fmt"

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

// LoadJSONWebKey returns a *jose.JSONWebKey for a given JSON string.
func LoadJSONWebKey(json []byte, pub bool) (*jose.JSONWebKey, error) {
	var jwk jose.JSONWebKey
	err := jwk.UnmarshalJSON(json)
	if err != nil {
		return nil, err
	}
	if !jwk.Valid() {
		return nil, errors.New("invalid JWK key")
	}
	if jwk.IsPublic() != pub {
		return nil, errors.New("priv/pub JWK key mismatch")
	}
	return &jwk, nil
}

// LoadPublicKey loads a public key from PEM/DER/JWK-encoded data.
func LoadPublicKey(data []byte) (interface{}, error) {
	input := data

	block, _ := pem.Decode(data)
	if block != nil {
		input = block.Bytes
	}

	// Try to load SubjectPublicKeyInfo
	pub, err0 := x509.ParsePKIXPublicKey(input)
	if err0 == nil {
		return pub, nil
	}

View on GitHub (pinned to 4174065ffb)

Solutions

  1. Check which half of the keypair your config expects: for LoadPublicKey/verification use the public JWK; for LoadPrivateKey/signing use the private JWK (one containing "d").
  2. Inspect the raw JWK JSON: presence of the "d" member means it is private; absence means public.
  3. Re-export the correct key half from your key management system or with `go-jose` / openssl and update the configuration.
  4. If you only need the public part and only have the private JWK, derive the public key from the private one (e.g. x509/ECDSA/RSA public of the parsed private key) instead of passing the private JWK to LoadPublicKey.

Example fix

// before: private JWK fed to LoadPublicKey -> "priv/pub JWK key mismatch"
pub, err := josex.LoadPublicKey([]byte(privateJWKJSON))

// after: export the public half first
var priv jose.JSONWebKey
_ = priv.UnmarshalJSON([]byte(privateJWKJSON))
pubJWK := priv.Public() // strips private members
pub, err := josex.LoadPublicKey(mustMarshalJSON(pubJWK))
Defensive patterns

Strategy: validation

Validate before calling

func isPublicJWK(jwkJSON []byte) bool {
    var raw struct{ D string `json:"d"` }
    return json.Unmarshal(jwkJSON, &raw) == nil && raw.D == ""
}
// call LoadPublicKey only when isPublicJWK(data), LoadPrivateKey only when !isPublicJWK(data)

Type guard

func assertKeyHalfMatches(jwkJSON []byte, wantPublic bool) error {
    var jwk jose.JSONWebKey
    if err := jwk.UnmarshalJSON(jwkJSON); err != nil { return err }
    if !jwk.Valid() { return errors.New("invalid JWK") }
    if jwk.IsPublic() != wantPublic { return errors.New("priv/pub JWK key mismatch") }
    return nil
}

Prevention

When it happens

Trigger: Calling LoadJSONWebKey(json, true) with a JWK that contains private components (e.g. RSA "d", EC "d"), or calling LoadJSONWebKey(json, false) with a public-only JWK. Since LoadPublicKey calls LoadJSONWebKey(data, true), feeding a private JWK to LoadPublicKey also triggers it (surfaced wrapped in the 'parse error' message); same for LoadPrivateKey(data) with a public JWK.

Common situations: Config files or environment variables holding a JWK where someone pasted the public half where the private half is expected (e.g. signing key config) or the opposite (verification key config); key-rotation tooling exporting the wrong half of a keypair; copying the JWKS public keys into a private-key setting.

Related errors


AI-assisted analysis of ory/hydra@4174065ffb (2026-09-03). Data as JSON: /api/errors/e9a6fd4721c74d9e. Report an issue: GitHub.