diff --git a/CHANGELOG.md b/CHANGELOG.md index 480216e5..f3aabe54 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,32 @@ ## Release Notes +### 0.11.5 + +This patch release adds additional security guards against an ECDSA bug in Java SE versions 15-15.0.6, 17-17.0.2, and 18 +([CVE-2022-21449](https://nvd.nist.gov/vuln/detail/CVE-2022-21449)) in addition to the guards added in the JJWT 0.11.3 +release. This patch allows JJWT users using those JVM versions to upgrade to JJWT 0.11.5, even if they are unable to +upgrade their JVM to patched/fixed JVM version in a timely manner. Note: if your application does not use these JVM +versions, you are not exposed to the JVM vulnerability. + +Note that the CVE is not a bug within JJWT itself - it is a bug within the above listed JVM versions, and the +JJWT 0.11.5 release adds additional precautions within JJWT in case an application team is not able to upgrade +their JVM in a timely manner. + +However, even with these additional JJWT security guards, the root cause of the issue is the JVM, so it **strongly +recommended** to upgrade your JVM to version +15.0.7, 17.0.3, or 18.0.1 or later to ensure the bug does not surface elsewhere in your application code or any other +third party library in your application that may not contain similar security guards. + +Issues included in this patch are listed in the [JJWT 0.11.5 milestone](https://github.com/jwtk/jjwt/milestone/26?closed=1). + +#### Credits + +Thank you to [Neil Madden](https://neilmadden.blog), the security researcher that first discovered the JVM +vulnerability as covered in his [Psychic Signatures in Java](https://neilmadden.blog/2022/04/19/psychic-signatures-in-java/) +blog post. Neil worked directly with the JJWT team to provide these additional guards, beyond what was in the JJWT 0.11.3 +release, and we're grateful for his help and collaboration in reviewing our fixes and for the additional tests he +provided the JJWT team. + ### 0.11.4 This patch release: @@ -20,18 +47,14 @@ Issues included in this patch are listed in the [JJWT 0.11.4 milestone](https:// ### 0.11.3 This patch release adds security guards against an ECDSA bug in Java SE versions 15-15.0.6, 17-17.0.2, and 18 -([CVE-2022-21449](https://nvd.nist.gov/vuln/detail/CVE-2022-21449)). This patch allows JJWT users using those JVM -versions to upgrade to JJWT 0.11.3, even if they are unable to upgrade their JVM to patched/fixed JVM version in a -timely manner. Note: if your application does not use these JVM versions, you are not exposed to the JVM vulnerability. +([CVE-2022-21449](https://nvd.nist.gov/vuln/detail/CVE-2022-21449)). Note: if your application does not use these +JVM versions, you are not exposed to the JVM vulnerability. -Note that the CVE is not a bug within JJWT itself - it is a bug within the above listed JVM versions, and the -JJWT 0.11.3 release adds additional precautions within JJWT in case an application team is not able to upgrade -their JVM in a timely manner. - -However, even with these additional JJWT security guards, the root cause of the issue is the JVM, so it **strongly -recommended** to upgrade your JVM to version -15.0.7, 17.0.3, or 18.0.1 or later to ensure the bug does not surface elsewhere in your application code or any other -third party library in your application that may not contain similar security guards. +Note that the CVE is not a bug within JJWT itself - it is a bug within the above listed JVM versions. However, even +with these additional JJWT security guards, the root cause of the issue is the JVM, so it **strongly +recommended** to upgrade your JVM to version 15.0.7, 17.0.3, or 18.0.1 or later to ensure the bug does not surface +elsewhere in your application code or any other third party library in your application that may not contain similar +security guards. Issues included in this patch are listed in the [JJWT 0.11.3 milestone](https://github.com/jwtk/jjwt/milestone/24). diff --git a/README.md b/README.md index 06ca77f2..a56ba10b 100644 --- a/README.md +++ b/README.md @@ -580,15 +580,15 @@ for any RSA key, the requirement is the RSA key (modulus) length in bits MUST be #### Elliptic Curve -JWT Elliptic Curve signature algorithms `ES256`, `ES384`, and `ES512` all require a minimum key length -(aka an Elliptic Curve order bit length) that is _at least_ as many bits as the algorithm signature's individual +JWT Elliptic Curve signature algorithms `ES256`, `ES384`, and `ES512` all require a key length +(aka an Elliptic Curve order bit length) equal to the algorithm signature's individual `R` and `S` components per [RFC 7512 Section 3.4](https://tools.ietf.org/html/rfc7518#section-3.4). This means: -* `ES256` requires that you use a private key that is at least 256 bits (32 bytes) long. +* `ES256` requires that you use a private key that is exactly 256 bits (32 bytes) long. -* `ES384` requires that you use a private key that is at least 384 bits (48 bytes) long. +* `ES384` requires that you use a private key that is exactly 384 bits (48 bytes) long. -* `ES512` requires that you use a private key that is at least 512 bits (64 bytes) long. +* `ES512` requires that you use a private key that is exactly 521 bits (65 or 66 bytes) long (depending on format). #### Creating Safe Keys diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index 2c7ab117..c57e5e0a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -15,21 +15,11 @@ */ package io.jsonwebtoken.impl; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.CompressionCodec; -import io.jsonwebtoken.Header; -import io.jsonwebtoken.JwsHeader; -import io.jsonwebtoken.JwtBuilder; -import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.*; import io.jsonwebtoken.impl.crypto.DefaultJwtSigner; import io.jsonwebtoken.impl.crypto.JwtSigner; import io.jsonwebtoken.impl.lang.LegacyServices; -import io.jsonwebtoken.io.Decoders; -import io.jsonwebtoken.io.Encoder; -import io.jsonwebtoken.io.Encoders; -import io.jsonwebtoken.io.SerializationException; -import io.jsonwebtoken.io.Serializer; +import io.jsonwebtoken.io.*; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; @@ -120,6 +110,7 @@ public class DefaultJwtBuilder implements JwtBuilder { Assert.notNull(key, "Key argument cannot be null."); Assert.notNull(alg, "SignatureAlgorithm cannot be null."); alg.assertValidSigningKey(key); //since 0.10.0 for https://github.com/jwtk/jjwt/issues/334 + createSigner(alg, key); // since 0.11.5: fail fast if key cannot be used for alg. this.algorithm = alg; this.key = key; return this; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index 591433f3..1070acce 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -15,27 +15,7 @@ */ package io.jsonwebtoken.impl; -import io.jsonwebtoken.ClaimJwtException; -import io.jsonwebtoken.Claims; -import io.jsonwebtoken.Clock; -import io.jsonwebtoken.CompressionCodec; -import io.jsonwebtoken.CompressionCodecResolver; -import io.jsonwebtoken.ExpiredJwtException; -import io.jsonwebtoken.Header; -import io.jsonwebtoken.IncorrectClaimException; -import io.jsonwebtoken.InvalidClaimException; -import io.jsonwebtoken.Jws; -import io.jsonwebtoken.JwsHeader; -import io.jsonwebtoken.Jwt; -import io.jsonwebtoken.JwtHandler; -import io.jsonwebtoken.JwtHandlerAdapter; -import io.jsonwebtoken.JwtParser; -import io.jsonwebtoken.MalformedJwtException; -import io.jsonwebtoken.MissingClaimException; -import io.jsonwebtoken.PrematureJwtException; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SigningKeyResolver; -import io.jsonwebtoken.UnsupportedJwtException; +import io.jsonwebtoken.*; import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver; import io.jsonwebtoken.impl.crypto.DefaultJwtSignatureValidator; import io.jsonwebtoken.impl.crypto.JwtSignatureValidator; @@ -403,13 +383,13 @@ public class DefaultJwtParser implements JwtParser { throw e; } catch (InvalidKeyException | IllegalArgumentException e) { String algName = algorithm.getValue(); - String msg = "The parsed JWT indicates it was signed with the " + algName + " signature " + - "algorithm, but the specified signing key of type " + key.getClass().getName() + - " may not be used to validate " + algName + " signatures. Because the specified " + - "signing key reflects a specific and expected algorithm, and the JWT does not reflect " + + String msg = "The parsed JWT indicates it was signed with the '" + algName + "' signature " + + "algorithm, but the provided " + key.getClass().getName() + " key may " + + "not be used to verify " + algName + " signatures. Because the specified " + + "key reflects a specific and expected algorithm, and the JWT does not reflect " + "this algorithm, it is likely that the JWT was not expected and therefore should not be " + - "trusted. Another possibility is that the parser was configured with the incorrect " + - "signing key, but this cannot be assumed for security reasons."; + "trusted. Another possibility is that the parser was provided the incorrect " + + "signature verification key, but this cannot be assumed for security reasons."; throw new UnsupportedJwtException(msg, e); } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java index 45ea11d2..15b7f991 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java @@ -19,12 +19,15 @@ import io.jsonwebtoken.JwtException; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.InvalidKeyException; import io.jsonwebtoken.security.SignatureException; +import java.math.BigInteger; import java.security.Key; import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.SecureRandom; +import java.security.interfaces.ECKey; import java.security.spec.ECGenParameterSpec; import java.util.HashMap; import java.util.Map; @@ -39,16 +42,43 @@ public abstract class EllipticCurveProvider extends SignatureProvider { private static final Map EC_CURVE_NAMES = createEcCurveNames(); private static Map createEcCurveNames() { - Map m = new HashMap(); //alg to ASN1 OID name + Map m = new HashMap<>(); //alg to ASN1 OID name m.put(SignatureAlgorithm.ES256, "secp256r1"); m.put(SignatureAlgorithm.ES384, "secp384r1"); m.put(SignatureAlgorithm.ES512, "secp521r1"); return m; } + protected static String byteSizeString(int bytesLength) { + return bytesLength + " bytes (" + (bytesLength * Byte.SIZE) + " bits)"; + } + + protected final int requiredSignatureByteLength; + protected final int fieldByteLength; + protected EllipticCurveProvider(SignatureAlgorithm alg, Key key) { super(alg, key); Assert.isTrue(alg.isEllipticCurve(), "SignatureAlgorithm must be an Elliptic Curve algorithm."); + if (!(key instanceof ECKey)) { + String msg = "Elliptic Curve signatures require an ECKey. The provided key of type " + + key.getClass().getName() + " is not a " + ECKey.class.getName() + " instance."; + throw new InvalidKeyException(msg); + } + this.requiredSignatureByteLength = getSignatureByteArrayLength(alg); + this.fieldByteLength = this.requiredSignatureByteLength / 2; + + ECKey ecKey = (ECKey) key; // can cast here because of the Assert.isTrue assertion above + BigInteger order = ecKey.getParams().getOrder(); + int keyFieldByteLength = (order.bitLength() + 7) / Byte.SIZE; //for ES512 (can be 65 or 66, this ensures 66) + int concatByteLength = keyFieldByteLength * 2; + + if (concatByteLength != this.requiredSignatureByteLength) { + String msg = "EllipticCurve key has a field size of " + + byteSizeString(keyFieldByteLength) + ", but " + alg.name() + " requires a field size of " + + byteSizeString(this.fieldByteLength) + " per [RFC 7518, Section 3.4 (validation)]" + + "(https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)."; + throw new InvalidKeyException(msg); + } } /** @@ -148,16 +178,15 @@ public abstract class EllipticCurveProvider extends SignatureProvider { /** * Returns the expected signature byte array length (R + S parts) for - * the specified ECDSA algorithm. + * the specified ECDSA algorithm. Expected lengths are mandated by + * JWA RFC 7518, Section 3.4. * * @param alg The ECDSA algorithm. Must be supported and not * {@code null}. * @return The expected byte array length for the signature. * @throws JwtException If the algorithm is not supported. */ - public static int getSignatureByteArrayLength(final SignatureAlgorithm alg) - throws JwtException { - + public static int getSignatureByteArrayLength(final SignatureAlgorithm alg) throws JwtException { switch (alg) { case ES256: return 64; @@ -180,7 +209,7 @@ public abstract class EllipticCurveProvider extends SignatureProvider { * @return The ECDSA JWS encoded signature. * @throws JwtException If the ASN.1/DER signature format is invalid. */ - public static byte[] transcodeSignatureToConcat(final byte[] derSignature, int outputLength) throws JwtException { + public static byte[] transcodeDERToConcat(final byte[] derSignature, int outputLength) throws JwtException { if (derSignature.length < 8 || derSignature[0] != 48) { throw new JwtException("Invalid ECDSA signature format"); @@ -213,9 +242,9 @@ public abstract class EllipticCurveProvider extends SignatureProvider { rawLen = Math.max(rawLen, outputLength / 2); if ((derSignature[offset - 1] & 0xff) != derSignature.length - offset - || (derSignature[offset - 1] & 0xff) != 2 + rLength + 2 + sLength - || derSignature[offset] != 2 - || derSignature[offset + 2 + rLength] != 2) { + || (derSignature[offset - 1] & 0xff) != 2 + rLength + 2 + sLength + || derSignature[offset] != 2 + || derSignature[offset + 2 + rLength] != 2) { throw new JwtException("Invalid ECDSA signature format"); } @@ -238,7 +267,7 @@ public abstract class EllipticCurveProvider extends SignatureProvider { * @return The ASN.1/DER encoded signature. * @throws JwtException If the ECDSA JWS signature format is invalid. */ - public static byte[] transcodeSignatureToDER(byte[] jwsSignature) throws JwtException { + public static byte[] transcodeConcatToDER(byte[] jwsSignature) throws JwtException { try { return concatToDER(jwsSignature); } catch (Exception e) { // CVE-2022-21449 guard @@ -257,10 +286,6 @@ public abstract class EllipticCurveProvider extends SignatureProvider { i--; } - if (i == 0) { // r == 0, JVM bug CVE-2202-21449 guard: - throw new JwtException("Invalid ECDSA Signature format"); - } - int j = i; if (jwsSignature[rawLen - i] < 0) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java index c6765057..d9d3c38c 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java @@ -19,16 +19,18 @@ import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.security.SignatureException; -import java.security.InvalidKeyException; +import java.math.BigInteger; import java.security.Key; import java.security.PublicKey; import java.security.Signature; +import java.security.interfaces.ECKey; import java.security.interfaces.ECPublicKey; +import java.util.Arrays; public class EllipticCurveSignatureValidator extends EllipticCurveProvider implements SignatureValidator { private static final String EC_PUBLIC_KEY_REQD_MSG = - "Elliptic Curve signature validation requires an ECPublicKey instance."; + "Elliptic Curve signature validation requires an ECPublicKey instance."; private static final String DER_ENCODING_SYS_PROPERTY_NAME = "io.jsonwebtoken.impl.crypto.EllipticCurveSignatureValidator.derEncodingSupported"; @@ -39,24 +41,48 @@ public class EllipticCurveSignatureValidator extends EllipticCurveProvider imple } @Override - public boolean isValid(byte[] data, byte[] signature) { + public boolean isValid(byte[] data, byte[] concatSignature) { Signature sig = createSignatureInstance(); PublicKey publicKey = (PublicKey) key; try { - int expectedSize = getSignatureByteArrayLength(alg); - /* - * If the expected size is not valid for JOSE, fall back to ASN.1 DER signature IFF the application - * is configured to do so. This fallback is for backwards compatibility ONLY (to support tokens - * generated by early versions of jjwt) and backwards compatibility will be removed in a future - * version of this library. This fallback is only enabled if the system property is set to 'true' due to - * the risk of CVE-2022-21449 attacks on early JVM versions 15, 17 and 18. - */ - //TODO: remove for 1.0 (DER-encoding support is not in the JWT RFCs) + // mandated per https://datatracker.ietf.org/doc/html/rfc7518#section-3.4 : + int requiredConcatByteLength = getSignatureByteArrayLength(alg); + byte[] derSignature; - if (expectedSize != signature.length && signature[0] == 0x30 && "true".equalsIgnoreCase(System.getProperty(DER_ENCODING_SYS_PROPERTY_NAME))) { - derSignature = signature; + + if (requiredConcatByteLength != concatSignature.length) { + + /* + * If the expected size is not valid for JOSE, fall back to ASN.1 DER signature IFF the application + * is configured to do so. This fallback is for backwards compatibility ONLY (to support tokens + * generated by early versions of jjwt) and backwards compatibility will be removed in a future + * version of this library. This fallback is only enabled if the system property is set to 'true' due to + * the risk of CVE-2022-21449 attacks on early JVM versions 15, 17 and 18. + */ + // TODO: remove for 1.0 (DER-encoding support is not in the JWT RFCs) + if (concatSignature[0] == 0x30 && "true".equalsIgnoreCase(System.getProperty(DER_ENCODING_SYS_PROPERTY_NAME))) { + derSignature = concatSignature; + } else { + String msg = "Provided signature is " + byteSizeString(concatSignature.length) + " but " + + alg.name() + " signatures must be exactly " + byteSizeString(requiredConcatByteLength) + " per " + + "[RFC 7518, Section 3.4 (validation)](https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)."; + throw new SignatureException(msg); + } } else { - derSignature = EllipticCurveProvider.transcodeSignatureToDER(signature); + + //guard for JVM security bug CVE-2022-21449: + ECKey ecKey = (ECKey) publicKey; // we can cast here because of the assertions made in the constructor + BigInteger order = ecKey.getParams().getOrder(); + BigInteger r = new BigInteger(1, Arrays.copyOfRange(concatSignature, 0, this.fieldByteLength)); + BigInteger s = new BigInteger(1, Arrays.copyOfRange(concatSignature, this.fieldByteLength, concatSignature.length)); + if (r.signum() < 1 || s.signum() < 1 || r.compareTo(order) >= 0 || s.compareTo(order) >= 0) { + return false; + } + + // Convert from concat to DER encoding since + // 1) SHAXXXWithECDSAInP1363Format algorithms are only available on >= JDK 9 and + // 2) the SignatureAlgorithm enum JCA alg names are all SHAXXXwithECDSA (which expects DER formatting) + derSignature = transcodeConcatToDER(concatSignature); } return doVerify(sig, publicKey, data, derSignature); } catch (Exception e) { @@ -66,7 +92,7 @@ public class EllipticCurveSignatureValidator extends EllipticCurveProvider imple } protected boolean doVerify(Signature sig, PublicKey publicKey, byte[] data, byte[] signature) - throws InvalidKeyException, java.security.SignatureException { + throws java.security.InvalidKeyException, java.security.SignatureException { sig.initVerify(publicKey); sig.update(data); return sig.verify(signature); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java index 9aeb1e8b..16c826fb 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java @@ -23,16 +23,15 @@ import java.security.InvalidKeyException; import java.security.Key; import java.security.PrivateKey; import java.security.Signature; -import java.security.interfaces.ECKey; public class EllipticCurveSigner extends EllipticCurveProvider implements Signer { public EllipticCurveSigner(SignatureAlgorithm alg, Key key) { super(alg, key); - if (!(key instanceof PrivateKey && key instanceof ECKey)) { - String msg = "Elliptic Curve signatures must be computed using an EC PrivateKey. The specified key of " + + if (!(key instanceof PrivateKey)) { + String msg = "Elliptic Curve signatures must be computed using an EC PrivateKey. The specified key of " + "type " + key.getClass().getName() + " is not an EC PrivateKey."; - throw new IllegalArgumentException(msg); + throw new io.jsonwebtoken.security.InvalidKeyException(msg); } } @@ -54,6 +53,6 @@ public class EllipticCurveSigner extends EllipticCurveProvider implements Signer Signature sig = createSignatureInstance(); sig.initSign(privateKey); sig.update(data); - return transcodeSignatureToConcat(sig.sign(), getSignatureByteArrayLength(alg)); + return transcodeDERToConcat(sig.sign(), getSignatureByteArrayLength(alg)); } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index c9de8390..009e51e8 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -19,10 +19,12 @@ import io.jsonwebtoken.impl.DefaultHeader import io.jsonwebtoken.impl.DefaultJwsHeader import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver import io.jsonwebtoken.impl.compression.GzipCompressionCodec +import io.jsonwebtoken.impl.crypto.EllipticCurveProvider import io.jsonwebtoken.impl.lang.Services import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.io.Serializer import io.jsonwebtoken.lang.Strings +import io.jsonwebtoken.security.InvalidKeyException import io.jsonwebtoken.security.Keys import io.jsonwebtoken.security.WeakKeyException import org.junit.Test @@ -576,6 +578,71 @@ class JwtsTest { } } + /** + * @since 0.11.5 + */ + @Test + void testBuilderWithEcdsaPublicKey() { + def builder = Jwts.builder().setSubject('foo') + def pair = Keys.keyPairFor(SignatureAlgorithm.ES256) + try { + builder.signWith(pair.public, SignatureAlgorithm.ES256) //public keys can't be used to create signatures + } catch (InvalidKeyException expected) { + String msg = "ECDSA signing keys must be PrivateKey instances." + assertEquals msg, expected.getMessage() + } + } + + /** + * @since 0.11.5 as part of testing guards against JVM CVE-2022-21449 + */ + @Test + void testBuilderWithMismatchedEllipticCurveKeyAndAlgorithm() { + def builder = Jwts.builder().setSubject('foo') + def pair = Keys.keyPairFor(SignatureAlgorithm.ES384) + try { + builder.signWith(pair.private, SignatureAlgorithm.ES256) //ES384 keys can't be used to create ES256 signatures + } catch (InvalidKeyException expected) { + String msg = "EllipticCurve key has a field size of 48 bytes (384 bits), but ES256 requires a " + + "field size of 32 bytes (256 bits) per [RFC 7518, Section 3.4 (validation)]" + + "(https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)." + assertEquals msg, expected.getMessage() + } + } + + /** + * @since 0.11.5 as part of testing guards against JVM CVE-2022-21449 + */ + @Test + void testParserWithMismatchedEllipticCurveKeyAndAlgorithm() { + def pair = Keys.keyPairFor(SignatureAlgorithm.ES256) + def jws = Jwts.builder().setSubject('foo').signWith(pair.private).compact() + def parser = Jwts.parserBuilder().setSigningKey(Keys.keyPairFor(SignatureAlgorithm.ES384).public).build() + try { + parser.parseClaimsJws(jws) + } catch (UnsupportedJwtException expected) { + String msg = 'The parsed JWT indicates it was signed with the \'ES256\' signature algorithm, but ' + + 'the provided sun.security.ec.ECPublicKeyImpl key may not be used to verify ES256 signatures. ' + + 'Because the specified key reflects a specific and expected algorithm, and the JWT does not ' + + 'reflect this algorithm, it is likely that the JWT was not expected and therefore should not ' + + 'be trusted. Another possibility is that the parser was provided the incorrect signature ' + + 'verification key, but this cannot be assumed for security reasons.' + assertEquals msg, expected.getMessage() + } + } + + /** + * @since 0.11.5 as part of testing guards against JVM CVE-2022-21449 + */ + @Test(expected=io.jsonwebtoken.security.SignatureException) + void testEcdsaInvalidSignatureValue() { + def withoutSignature = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" + def invalidEncodedSignature = "_____wAAAAD__________7zm-q2nF56E87nKwvxjJVH_____AAAAAP__________vOb6racXnoTzucrC_GMlUQ" + String jws = withoutSignature + '.' + invalidEncodedSignature + def keypair = EllipticCurveProvider.generateKeyPair(SignatureAlgorithm.ES256) + Jwts.parserBuilder().setSigningKey(keypair.public).build().parseClaimsJws(jws) + } + //Asserts correct/expected behavior discussed in https://github.com/jwtk/jjwt/issues/20 @Test void testParseClaimsJwsWithUnsignedJwt() { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveProviderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveProviderTest.groovy index 7e61b8f9..ccebf30b 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveProviderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveProviderTest.groovy @@ -15,7 +15,10 @@ */ package io.jsonwebtoken.impl.crypto + import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.security.InvalidKeyException +import io.jsonwebtoken.security.Keys import org.junit.Test import java.security.KeyPair @@ -65,4 +68,29 @@ class EllipticCurveProviderTest { assertEquals ise.message, "SignatureAlgorithm argument must represent an Elliptic Curve algorithm." } } + + @Test + void testConstructorWithNonEcKey() { + def key = Keys.secretKeyFor(SignatureAlgorithm.HS256) + try { + new EllipticCurveProvider(SignatureAlgorithm.ES256, key) {} + } catch (InvalidKeyException expected) { + String msg = 'Elliptic Curve signatures require an ECKey. The provided key of type ' + + 'javax.crypto.spec.SecretKeySpec is not a java.security.interfaces.ECKey instance.' + assertEquals msg, expected.getMessage() + } + } + + @Test + void testConstructorWithInvalidKeyFieldLength() { + def keypair = Keys.keyPairFor(SignatureAlgorithm.ES256) + try { + new EllipticCurveProvider(SignatureAlgorithm.ES384, keypair.public){} + } catch (InvalidKeyException expected) { + String msg = "EllipticCurve key has a field size of 32 bytes (256 bits), but ES384 requires a " + + "field size of 48 bytes (384 bits) per [RFC 7518, Section 3.4 (validation)]" + + "(https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)." + assertEquals msg, expected.getMessage() + } + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy index 22e8b3a1..51aba2e9 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy @@ -36,21 +36,23 @@ class EllipticCurveSignatureValidatorTest { String msg = 'foo' final InvalidKeyException ex = new InvalidKeyException(msg) + def alg = SignatureAlgorithm.ES512 + def keypair = EllipticCurveProvider.generateKeyPair(alg) - def v = new EllipticCurveSignatureValidator(SignatureAlgorithm.ES512, EllipticCurveProvider.generateKeyPair().public) { + def v = new EllipticCurveSignatureValidator(alg, EllipticCurveProvider.generateKeyPair(alg).public) { @Override protected boolean doVerify(Signature sig, PublicKey pk, byte[] data, byte[] signature) throws InvalidKeyException, java.security.SignatureException { throw ex; } } - byte[] bytes = new byte[16] - byte[] signature = new byte[16] - SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(bytes) - SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(signature) + byte[] data = new byte[32] + SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(data) + + byte[] signature = new EllipticCurveSigner(alg, keypair.getPrivate()).sign(data) try { - v.isValid(bytes, signature) + v.isValid(data, signature) fail(); } catch (SignatureException se) { assertEquals se.message, 'Unable to verify Elliptic Curve signature using configured ECPublicKey. ' + msg @@ -80,12 +82,24 @@ class EllipticCurveSignatureValidatorTest { void legacySignatureCompatDefaultTest() { def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" def keypair = EllipticCurveProvider.generateKeyPair() - def signature = Signature.getInstance(SignatureAlgorithm.ES512.jcaName) + def alg = SignatureAlgorithm.ES512 + def signature = Signature.getInstance(alg.jcaName) def data = withoutSignature.getBytes("US-ASCII") signature.initSign(keypair.private) signature.update(data) def signed = signature.sign() - assertFalse new EllipticCurveSignatureValidator(SignatureAlgorithm.ES512, keypair.public).isValid(data, signed) + def validator = new EllipticCurveSignatureValidator(alg, keypair.public) + try { + validator.isValid(data, signed) + fail() + } catch (SignatureException expected) { + String signedBytesString = EllipticCurveProvider.byteSizeString(signed.length) + String msg = "Unable to verify Elliptic Curve signature using configured ECPublicKey. Provided " + + "signature is $signedBytesString but ES512 signatures must be exactly 132 bytes (1056 bits) " + + "per [RFC 7518, Section 3.4 (validation)]" + + "(https://datatracker.ietf.org/doc/html/rfc7518#section-3.4)." as String + assertEquals msg, expected.getMessage() + } } @Test @@ -107,54 +121,54 @@ class EllipticCurveSignatureValidatorTest { @Test // asserts guard for JVM security bug CVE-2022-21449: void testSignatureAllZeros() { - try { - byte[] forgedSig = new byte[64] - def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" - def keypair = EllipticCurveProvider.generateKeyPair() - def data = withoutSignature.getBytes("US-ASCII") - new EllipticCurveSignatureValidator(SignatureAlgorithm.ES256, keypair.public).isValid(data, forgedSig) - fail("SignatureException expected") - } catch(SignatureException expected) { - assertEquals 'Unable to verify Elliptic Curve signature using configured ECPublicKey. Invalid ECDSA signature format.', expected.getMessage() - } + byte[] forgedSig = new byte[64] + def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" + def alg = SignatureAlgorithm.ES256 + def keypair = EllipticCurveProvider.generateKeyPair(alg) + def data = withoutSignature.getBytes("US-ASCII") + def validator = new EllipticCurveSignatureValidator(alg, keypair.public) + assertFalse validator.isValid(data, forgedSig) } @Test // asserts guard for JVM security bug CVE-2022-21449: void testSignatureRZero() { - try { - byte[] r = new byte[32] - byte[] s = new byte[32]; Arrays.fill(s, Byte.MAX_VALUE) - byte[] sig = new byte[r.length + s.length] - System.arraycopy(r, 0, sig, 0, r.length) - System.arraycopy(s, 0, sig, r.length, s.length) + byte[] r = new byte[32] + byte[] s = new byte[32]; Arrays.fill(s, Byte.MAX_VALUE) + byte[] sig = new byte[r.length + s.length] + System.arraycopy(r, 0, sig, 0, r.length) + System.arraycopy(s, 0, sig, r.length, s.length) - def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" - def keypair = EllipticCurveProvider.generateKeyPair() - def data = withoutSignature.getBytes("US-ASCII") - new EllipticCurveSignatureValidator(SignatureAlgorithm.ES256, keypair.public).isValid(data, sig) - fail("SignatureException expected") - } catch(SignatureException expected) { - assertEquals 'Unable to verify Elliptic Curve signature using configured ECPublicKey. Invalid ECDSA signature format.', expected.getMessage() - } + def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" + def keypair = EllipticCurveProvider.generateKeyPair(SignatureAlgorithm.ES256) + def data = withoutSignature.getBytes("US-ASCII") + def validator = new EllipticCurveSignatureValidator(SignatureAlgorithm.ES256, keypair.public) + assertFalse validator.isValid(data, sig) } @Test // asserts guard for JVM security bug CVE-2022-21449: void testSignatureSZero() { - try { - byte[] r = new byte[32]; Arrays.fill(r, Byte.MAX_VALUE); - byte[] s = new byte[32] - byte[] sig = new byte[r.length + s.length] - System.arraycopy(r, 0, sig, 0, r.length) - System.arraycopy(s, 0, sig, r.length, s.length) + byte[] r = new byte[32]; Arrays.fill(r, Byte.MAX_VALUE); + byte[] s = new byte[32] + byte[] sig = new byte[r.length + s.length] + System.arraycopy(r, 0, sig, 0, r.length) + System.arraycopy(s, 0, sig, r.length, s.length) - def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" - def keypair = EllipticCurveProvider.generateKeyPair() - def data = withoutSignature.getBytes("US-ASCII") - new EllipticCurveSignatureValidator(SignatureAlgorithm.ES256, keypair.public).isValid(data, sig) - fail("SignatureException expected") - } catch(SignatureException expected) { - assertEquals 'Unable to verify Elliptic Curve signature using configured ECPublicKey. Invalid ECDSA signature format.', expected.getMessage() - } + def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" + def keypair = EllipticCurveProvider.generateKeyPair(SignatureAlgorithm.ES256) + def data = withoutSignature.getBytes("US-ASCII") + def validator = new EllipticCurveSignatureValidator(SignatureAlgorithm.ES256, keypair.public) + assertFalse validator.isValid(data, sig) + } + + @Test // asserts guard for JVM security bug CVE-2022-21449: + void ecdsaInvalidSignatureValuesTest() { + def withoutSignature = "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" + def invalidEncodedSignature = "_____wAAAAD__________7zm-q2nF56E87nKwvxjJVH_____AAAAAP__________vOb6racXnoTzucrC_GMlUQ" + def keypair = EllipticCurveProvider.generateKeyPair(SignatureAlgorithm.ES256) + def data = withoutSignature.getBytes("US-ASCII") + def invalidSignature = Decoders.BASE64URL.decode(invalidEncodedSignature) + def validator = new EllipticCurveSignatureValidator(SignatureAlgorithm.ES256, keypair.public) + assertFalse("Forged signature must not be considered valid.", validator.isValid(data, invalidSignature)) } @Test @@ -173,7 +187,7 @@ class EllipticCurveSignatureValidatorTest { try { def signature = new byte[257] SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(signature) - EllipticCurveProvider.transcodeSignatureToDER(signature) + EllipticCurveProvider.transcodeConcatToDER(signature) fail() } catch (JwtException e) { assertEquals e.message, 'Invalid ECDSA signature format.' @@ -184,7 +198,7 @@ class EllipticCurveSignatureValidatorTest { void invalidDERSignatureToJoseFormatTest() { def verify = { signature -> try { - EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) + EllipticCurveProvider.transcodeDERToConcat(signature, 132) fail() } catch (JwtException e) { assertEquals e.message, 'Invalid ECDSA signature format' @@ -209,14 +223,14 @@ class EllipticCurveSignatureValidatorTest { def signature = new byte[32] SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(signature) signature[0] = 0 as byte - EllipticCurveProvider.transcodeSignatureToDER(signature) //no exception + EllipticCurveProvider.transcodeConcatToDER(signature) //no exception } @Test void edgeCaseSignatureToConcatLengthTest() { try { def signature = Decoders.BASE64.decode("MIEAAGg3OVb/ZeX12cYrhK3c07TsMKo7Kc6SiqW++4CAZWCX72DkZPGTdCv2duqlupsnZL53hiG3rfdOLj8drndCU+KHGrn5EotCATdMSLCXJSMMJoHMM/ZPG+QOHHPlOWnAvpC1v4lJb32WxMFNz1VAIWrl9Aa6RPG1GcjCTScKjvEE") - EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) + EllipticCurveProvider.transcodeDERToConcat(signature, 132) fail() } catch (JwtException e) { @@ -227,7 +241,7 @@ class EllipticCurveSignatureValidatorTest { void edgeCaseSignatureToConcatInvalidSignatureTest() { try { def signature = Decoders.BASE64.decode("MIGBAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") - EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) + EllipticCurveProvider.transcodeDERToConcat(signature, 132) fail() } catch (JwtException e) { assertEquals e.message, 'Invalid ECDSA signature format' @@ -238,7 +252,7 @@ class EllipticCurveSignatureValidatorTest { void edgeCaseSignatureToConcatInvalidSignatureBranchTest() { try { def signature = Decoders.BASE64.decode("MIGBAD4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") - EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) + EllipticCurveProvider.transcodeDERToConcat(signature, 132) fail() } catch (JwtException e) { assertEquals e.message, 'Invalid ECDSA signature format' @@ -249,7 +263,7 @@ class EllipticCurveSignatureValidatorTest { void edgeCaseSignatureToConcatInvalidSignatureBranch2Test() { try { def signature = Decoders.BASE64.decode("MIGBAj4AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") - EllipticCurveProvider.transcodeSignatureToConcat(signature, 132) + EllipticCurveProvider.transcodeDERToConcat(signature, 132) fail() } catch (JwtException e) { assertEquals e.message, 'Invalid ECDSA signature format' @@ -260,7 +274,7 @@ class EllipticCurveSignatureValidatorTest { void verifySwarmTest() { for (SignatureAlgorithm algorithm : [SignatureAlgorithm.ES256, SignatureAlgorithm.ES384, SignatureAlgorithm.ES512]) { def withoutSignature = "eyJhbGciOiJFUzUxMiIsInR5cCI6IkpXVCJ9.eyJ0ZXN0IjoidGVzdCIsImlhdCI6MTQ2NzA2NTgyN30" - def keypair = EllipticCurveProvider.generateKeyPair() + def keypair = EllipticCurveProvider.generateKeyPair(algorithm) def data = withoutSignature.getBytes("US-ASCII") def signature = new EllipticCurveSigner(algorithm, keypair.private).sign(data) assert new EllipticCurveSignatureValidator(algorithm, keypair.public).isValid(data, signature) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy index 97baac2d..d8a27c06 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy @@ -43,13 +43,14 @@ class EllipticCurveSignerTest { @Test void testConstructorWithoutECPrivateKey() { - def key = Keys.secretKeyFor(SignatureAlgorithm.HS256) + def pair = Keys.keyPairFor(SignatureAlgorithm.ES256) try { - new EllipticCurveSigner(SignatureAlgorithm.ES256, key) - fail('EllipticCurveSigner should reject non ECPrivateKey instances.') - } catch (IllegalArgumentException expected) { - assertEquals expected.message, "Elliptic Curve signatures must be computed using an EC PrivateKey. The specified key of " + - "type " + key.getClass().getName() + " is not an EC PrivateKey." + new EllipticCurveSigner(SignatureAlgorithm.ES256, pair.public) + fail('EllipticCurveSigner should reject public ECKey instances.') + } catch (io.jsonwebtoken.security.InvalidKeyException expected) { + String msg = 'Elliptic Curve signatures must be computed using an EC PrivateKey. The specified key of ' + + 'type sun.security.ec.ECPublicKeyImpl is not an EC PrivateKey.' + assertEquals(msg, expected.getMessage()) } } @@ -59,7 +60,6 @@ class EllipticCurveSignerTest { SignatureAlgorithm alg = SignatureAlgorithm.ES256 KeyPair kp = Keys.keyPairFor(alg) - PublicKey publicKey = kp.getPublic() PrivateKey privateKey = kp.getPrivate() String msg = 'foo' @@ -87,14 +87,15 @@ class EllipticCurveSignerTest { @Test void testDoSignWithJoseSignatureFormatException() { - KeyPair kp = EllipticCurveProvider.generateKeyPair() + SignatureAlgorithm alg = SignatureAlgorithm.ES256 + KeyPair kp = EllipticCurveProvider.generateKeyPair(alg) PublicKey publicKey = kp.getPublic(); PrivateKey privateKey = kp.getPrivate(); String msg = 'foo' final JwtException ex = new JwtException(msg) - def signer = new EllipticCurveSigner(SignatureAlgorithm.ES256, privateKey) { + def signer = new EllipticCurveSigner(alg, privateKey) { @Override protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException, JwtException { throw ex @@ -116,14 +117,15 @@ class EllipticCurveSignerTest { @Test void testDoSignWithJdkSignatureException() { - KeyPair kp = EllipticCurveProvider.generateKeyPair() + SignatureAlgorithm alg = SignatureAlgorithm.ES256 + KeyPair kp = EllipticCurveProvider.generateKeyPair(alg) PublicKey publicKey = kp.getPublic(); PrivateKey privateKey = kp.getPrivate(); String msg = 'foo' final java.security.SignatureException ex = new java.security.SignatureException(msg) - def signer = new EllipticCurveSigner(SignatureAlgorithm.ES256, privateKey) { + def signer = new EllipticCurveSigner(alg, privateKey) { @Override protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException { throw ex