Skip to Content
@exortek/jwkvalidate — strict shape checks

jwk.validate / jwk.isValid

Strict RFC 7517 §4 (common) + RFC 7518 §6 (per-kty) + RFC 8037 §2 (OKP) shape enforcement. Runs on every importJWK and every thumbprint implicitly — call it directly when you’re receiving JWKs at a system boundary (JWKS fetch, admin panel input, DB read).

import { jwk } from '@exortek/jwk' jwk.validate(untrustedJwk) // throws JwkError on failure jwk.validate(untrustedJwk, { requirePublic: true }) // additional guard const ok = jwk.isValid(untrustedJwk) // non-throwing variant

What gets checked

Common members (RFC 7517 §4)

MemberCheck
ktyRequired string; one of EC / RSA / oct / OKP.
kidIf present, must be a string.
useIf present, "sig" or "enc" — nothing else.
algIf present, must be a string.
key_opsArray of unique strings from sign / verify / encrypt / decrypt / wrapKey / unwrapKey / deriveKey / deriveBits.

use × key_ops consistency (§4.3)

When both are set the JWK MUST be consistent:

useAllowed key_ops
'sig'sign, verify
'enc'encrypt, decrypt, wrapKey, unwrapKey, deriveKey, deriveBits

Any key_ops entry outside its use’s allowlist raises KEY_OPS_CONFLICT with the offending op named.

Per-kty members

Coordinate / component lengths are decoded from base64url and measured — the wrong-length x on a P-256 key is caught here, not by a cryptic OpenSSL error deep inside createPublicKey.

  • ECcrv in the supported set; x, y decode to EC_COORD_BYTES[crv] bytes; d (private) same length.
  • RSAn, e present and base64url; d (private) present; CRT parameters p / q / dp / dq / qi all present or all absent (partial sets rejected — RFC 7518 §6.3.2).
  • octk present, base64url, non-empty.
  • OKPcrv in the RFC 8037 set; x decodes to OKP_KEY_BYTES[crv] bytes; d (private) same length.

requirePublic / requirePrivate guards

Assert secret-carrying state at the JWK boundary — no more “did I get the private form by accident?” foot-guns:

// Endpoints that publish JWKS: refuse anything carrying secrets. jwk.validate(receivedJwk, { requirePublic: true }) // Endpoints that sign / decrypt: refuse public-only material. jwk.validate(receivedJwk, { requirePrivate: true })

Both are opt-in — the default (validate(jwk)) accepts either shape.

isValid — non-throwing filter

Handy for JWKS ingestion, where a single bad key shouldn’t kill the whole set:

const rotationSet = (jwks.keys || []).filter(k => jwk.isValid(k, { requirePublic: true }))

isValid swallows JwkError and returns false; any other error type (a bug in a validator) still throws.

Errors

CodeWhen
INVALID_JWKShape / length / secret-carrying mismatch, use / key_ops shape.
MISSING_REQUIRED_MEMBERkty, or a per-kty required string, is missing.
UNSUPPORTED_KTYkty not EC / RSA / oct / OKP.
UNSUPPORTED_CURVEcrv outside the per-kty matrix.
KEY_OPS_CONFLICTuse and key_ops inconsistent per §4.3.
Last updated on