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
- 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
- Keep Ed25519 keys exclusively for JWS signing (EdDSA) and X25519/X448 keys exclusively for ECDH-ES key agreement
- If loading OKP JWKs, ensure the crv/kty actually denotes X25519 (key-agreement OKP), not Ed25519 (signature OKP)
- 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
- Maintain separate key pairs: Ed25519 for signing, X25519/X448 for encryption
- When loading OKP JWKs, verify crv is X25519/X448 (not Ed25519/Ed449) for ECDH-ES consumers
- Document key usage per algorithm in your key management config to avoid curve mix-ups
- Add a startup check that decrypt keys resolve to agreement curves before serving traffic
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
- Unable to determine JWA-standard Elliptic Curve for ${type}k
- PublicKeys may not be used to decrypt data. PublicKeys are u
- Unexpected content JWE.
- Unexpected Claims JWE.
- PrivateKeys may not be used to encrypt data. PublicKeys are
AI-assisted analysis of jwtk/jjwt@fb71496164 (2026-09-09).
Data as JSON: /api/errors/62cd68490261fdcd.
Report an issue: GitHub.