jwtk/jjwt · error · InvalidKeyException

PrivateKeys may not be used to verify digital signatures. Pr

Error message

PrivateKeys may not be used to verify digital signatures. PrivateKeys are used to sign, and PublicKeys are used to verify.

What it means

The parser resolves the signature verification key and, after unwrapping Provider wrappers, checks that it is not a PrivateKey. Private keys are for signing only; verification must use the corresponding PublicKey, so a PrivateKey is rejected with InvalidKeyException.

Source

Thrown at impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java:299

        }
        Assert.stateNotNull(algorithm, "JWS Signature Algorithm cannot be null.");

        //digitally signed, let's assert the signature:
        Key key;
        if (claims != null) {
            key = resolver.resolveSigningKey(jwsHeader, claims);
        } else {
            key = resolver.resolveSigningKey(jwsHeader, payload.getBytes());
        }
        if (key == null) {
            String msg = "Cannot verify JWS signature: unable to locate signature verification key for JWS with header: " + jwsHeader;
            throw new UnsupportedJwtException(msg);
        }
        Provider provider = ProviderKey.getProvider(key, this.provider); // extract if necessary
        key = ProviderKey.getKey(key); // unwrap if necessary, MUST be called after ProviderKey.getProvider
        Assert.stateNotNull(key, "ProviderKey cannot be null."); //ProviderKey impl doesn't allow null
        if (key instanceof PrivateKey) {
            throw new InvalidKeyException(PRIV_KEY_VERIFY_MSG);
        }

        final byte[] signature = decode(tokenized.getDigest(), "JWS signature");

        //re-create the jwt part without the signature.  This is what is needed for signature verification:
        InputStream payloadStream = null;
        InputStream verificationInput;
        if (jwsHeader.isPayloadEncoded()) {
            int len = tokenized.getProtected().length() + 1 + tokenized.getPayload().length();
            CharBuffer cb = CharBuffer.allocate(len);
            cb.put(Strings.wrap(tokenized.getProtected()));
            cb.put(SEPARATOR_CHAR);
            cb.put(Strings.wrap(tokenized.getPayload()));
            cb.rewind();
            ByteBuffer bb = StandardCharsets.US_ASCII.encode(cb);
            bb.rewind();
            byte[] data = new byte[bb.remaining()];
            bb.get(data);

View on GitHub (pinned to fb71496164)

Solutions

  1. Pass the corresponding PublicKey (e.g. certificate.getPublicKey() or keyPair.getPublic()) to verifyWith(...)
  2. In SigningKeyResolver implementations, ensure only PublicKeys are returned for verification
  3. Fix keystore loading code to retrieve the public certificate rather than the private key entry

Example fix

// before
JwsParser p = Jwts.parser().verifyWith(privateKey).build();
// after
PublicKey pub = keyPair.getPublic();
JwsParser p = Jwts.parser().verifyWith(pub).build();
Defensive patterns

Strategy: type-guard

Type guard

boolean isVerificationKey(Key k) {
    return !(k instanceof PrivateKey) && k instanceof PublicKey || k instanceof SecretKey;
}

Try / catch

try {
    return parser.parseSignedClaims(token);
} catch (InvalidKeyException e) {
    log.error("Verification key must be public/secret, not private");
    throw new UnauthorizedException(e);
}

Prevention

When it happens

Trigger: Passing a PrivateKey to parser.verifyWith(key) (or returning one from a SigningKeyResolver) and then parsing a signed JWT.

Common situations: Copy-pasting the signing key configuration into the parser config; loading the wrong key from a keystore (getKey instead of getCertificate().getPublicKey()); symmetric code where the same Key variable is used for both sign and verify paths.

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/8552485ccf472461. Report an issue: GitHub.