jwtk/jjwt · error · io.jsonwebtoken.security.InvalidKeyException

${curveId} keys may not be used with ECDH-ES key agreement a

Error message

${curveId} keys may not be used with ECDH-ES key agreement algorithms per https://www.rfc-editor.org/rfc/rfc8037#section-3.1.

What it means

Thrown by EcdhKeyAlgorithm.assertCurve when the resolved curve is an EdwardsCurve that is a signature curve (Ed25519 or Ed449), blocking its use with ECDH-ES key agreement. RFC 8037 section 3.1 mandates that the OKP keys usable for ECDH-ES are the X25519/X448 key agreement types, not the Ed25519/Ed449 signature types, even though both share curve IDs — so JJWT rejects signing keys presented for encryption. You must use X25519/X448 keys for ECDH-ES.

Source

Thrown at impl/src/main/java/io/jsonwebtoken/impl/security/EcdhKeyAlgorithm.java:169

        if (request instanceof SecureRequest) {
            return ((SecureRequest<?, ?>) request).getKey() instanceof ECKey ? super.getJcaName(request) : XDH_JCA_NAME;
        } else {
            return request.getPayload() instanceof ECKey ? super.getJcaName(request) : XDH_JCA_NAME;
        }
    }

    private static AbstractCurve assertCurve(Key key) {
        Curve curve = StandardCurves.findByKey(key);
        if (curve == null) {
            String type = key instanceof PublicKey ? "encryption " : "decryption ";
            String msg = "Unable to determine JWA-standard Elliptic Curve for " + type + "key [" +
                    KeysBridge.toString(key) + "]";
            throw new InvalidKeyException(msg);
        }
        if (curve instanceof EdwardsCurve && ((EdwardsCurve) curve).isSignatureCurve()) {
            String msg = curve.getId() + " keys may not be used with ECDH-ES key agreement algorithms per " +
                    "https://www.rfc-editor.org/rfc/rfc8037#section-3.1.";
            throw new InvalidKeyException(msg);
        }
        return Assert.isInstanceOf(AbstractCurve.class, curve, "AbstractCurve instance expected.");
    }

    @Override
    public KeyResult getEncryptionKey(KeyRequest<PublicKey> request) throws SecurityException {
        Assert.notNull(request, "Request cannot be null.");
        JweHeader header = Assert.notNull(request.getHeader(), "Request JweHeader cannot be null.");
        PublicKey publicKey = Assert.notNull(request.getPayload(), "Encryption PublicKey cannot be null.");

        Curve curve = assertCurve(publicKey);
        // note: we don't need to validate if specified key's point is on a supported curve here
        // because that will automatically be asserted when using Jwks.builder().... below
        Assert.stateNotNull(curve, "Internal implementation state: Curve cannot be null.");

        // Generate our ephemeral key pair:
        final SecureRandom random = ensureSecureRandom(request);
        DynamicJwkBuilder<?, ?> jwkBuilder = Jwks.builder().random(random);

View on GitHub (pinned to fb71496164)

Solutions

  1. Generate a separate X25519 key pair for encryption: Jwts.KEY.X25519? or Keys for XDH — use JJWT's X25519 curve keyPair utilities for ECDH-ES
  2. Keep Ed25519 keys exclusively for JWS signing (EdDSA) and X25519/X448 keys exclusively for ECDH-ES key agreement
  3. If loading OKP JWKs, ensure the crv/kty actually denotes X25519 (key-agreement OKP), not Ed25519 (signature OKP)
  4. Catch InvalidKeyException around encryption/decryption to surface a clear configuration error to callers

Example fix

// before: reusing Ed25519 signing keys for encryption
KeyPair ed = Jwts.SIG.EdDSA.keyPair().build();
Jwts.builder().encryptWith(ed.getPublic(), Jwts.KEYDIR.ECDH_ES, enc);
// after: dedicated X25519 agreement keys
KeyPair x25519 = Jwts.KEY.X25519.keyPair().build();
String jwe = Jwts.builder().encryptWith(x25519.getPublic(),
    Jwts.KEYDIR.ECDH_ES, Jwts.Enc.A256GCM).compact();
Defensive patterns

Strategy: validation

Validate before calling

// ensure curve is an agreement curve, not a signature curve, before ECDH-ES
static void requireAgreementCurve(Key k) {
    String alg = k.getAlgorithm();
    if (alg != null && (alg.contains("Ed25519") || alg.contains("Ed448") || alg.equals("EdDSA"))) {
        throw new IllegalArgumentException("Use X25519/X448 keys for ECDH-ES, not " + alg);
    }
}

Type guard

boolean isKeyAgreementOkp(Key k) {
    String a = k.getAlgorithm();
    return a != null && (a.equals("XDH") || a.contains("X25519") || a.contains("X448"));
}

Try / catch

try {
    return Jwts.builder().encryptWith(pub, Jwts.KEYDIR.ECDH_ES, enc).compact();
} catch (InvalidKeyException e) {
    throw new KeyConfigException("Ed25519/Ed449 keys cannot be used with ECDH-ES (RFC 8037 §3.1)", e);
}

Prevention

When it happens

Trigger: Calling encryptWith(ed25519PublicKey, Jwts.KEYDIR.ECDH_ES/ECDH_ES+AxxxKW, enc) or decryptWith an Ed25519/Ed449 private key — i.e. using an EdDSA signing key pair (e.g. generated via Jwts.SIG.EdDSA or an OKP JWK with crv=Ed25519) as the ECDH-ES key agreement key.

Common situations: Generating one OKP key pair and trying to use it for both JWT signing (EdDSA) and JWE encryption (ECDH-ES); confusing Ed25519 and X25519 since both are '25519' curves; migrating configs that specify a single curve id for all OKP keys.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of jwtk/jjwt@fb71496164 (2026-09-09). Data as JSON: /api/errors/62cd68490261fdcd. Report an issue: GitHub.