From 9189253668e7410202bcd4af9ef5855bf2e84dab Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Fri, 27 Jul 2018 15:03:53 -0400 Subject: [PATCH] 334: key strength assertions and signature validation. Resolves #334 --- .../main/java/io/jsonwebtoken/JwtParser.java | 1 + .../io/jsonwebtoken/SignatureAlgorithm.java | 340 ++++++--- .../io/jsonwebtoken/SignatureException.java | 6 +- .../security/InvalidKeyException.java | 11 + .../jsonwebtoken/security/KeyException.java | 11 + .../{crypto => security}/Keys.java | 18 +- .../security/SecurityException.java | 17 + .../security/SignatureException.java | 15 + .../security/WeakKeyException.java | 11 + .../SignatureAlgorithmTest.groovy | 648 +++++++++++++++++- .../{crypto => security}/KeysTest.groovy | 12 +- .../security/SignatureExceptionTest.groovy | 22 + .../jsonwebtoken/impl/DefaultJwtBuilder.java | 15 +- .../jsonwebtoken/impl/DefaultJwtParser.java | 13 +- .../impl/crypto/EllipticCurveProvider.java | 30 +- .../EllipticCurveSignatureValidator.java | 8 +- .../impl/crypto/EllipticCurveSigner.java | 8 +- .../jsonwebtoken/impl/crypto/MacProvider.java | 31 +- .../jsonwebtoken/impl/crypto/MacSigner.java | 2 +- .../jsonwebtoken/impl/crypto/RsaProvider.java | 6 +- .../impl/crypto/RsaSignatureValidator.java | 2 +- .../jsonwebtoken/impl/crypto/RsaSigner.java | 2 +- .../impl/crypto/SignatureProvider.java | 10 +- .../io/jsonwebtoken/impl/crypto/Signer.java | 2 +- .../io/jsonwebtoken/JwtParserTest.groovy | 1 + .../groovy/io/jsonwebtoken/JwtsTest.groovy | 144 ++-- .../RsaSigningKeyResolverAdapterTest.groovy | 8 +- .../jsonwebtoken/impl/DefaultJwsTest.groovy | 11 +- .../impl/DefaultJwtBuilderTest.groovy | 13 +- .../impl/DefaultJwtParserTest.groovy | 16 +- .../DefaultJwtSignatureValidatorTest.groovy | 5 +- .../impl/crypto/DefaultJwtSignerTest.groovy | 5 +- ...efaultSignatureValidatorFactoryTest.groovy | 8 +- .../crypto/DefaultSignerFactoryTest.groovy | 7 +- ...EllipticCurveSignatureValidatorTest.groovy | 10 +- .../crypto/EllipticCurveSignerTest.groovy | 26 +- .../impl/crypto/MacProviderTest.groovy | 26 +- .../impl/crypto/MacSignerTest.groovy | 33 +- .../crypto/PowermockMacProviderTest.groovy | 50 ++ .../impl/crypto/RsaProviderTest.groovy | 4 +- .../crypto/RsaSignatureValidatorTest.groovy | 16 +- .../impl/crypto/RsaSignerTest.groovy | 11 +- .../impl/crypto/SignatureProviderTest.groovy | 4 +- .../{crypto => security}/KeysImplTest.groovy | 31 +- 44 files changed, 1348 insertions(+), 322 deletions(-) create mode 100644 api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/KeyException.java rename api/src/main/java/io/jsonwebtoken/{crypto => security}/Keys.java (89%) create mode 100644 api/src/main/java/io/jsonwebtoken/security/SecurityException.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/SignatureException.java create mode 100644 api/src/main/java/io/jsonwebtoken/security/WeakKeyException.java rename api/src/test/groovy/io/jsonwebtoken/{crypto => security}/KeysTest.groovy (85%) create mode 100644 api/src/test/groovy/io/jsonwebtoken/security/SignatureExceptionTest.groovy create mode 100644 impl/src/test/groovy/io/jsonwebtoken/impl/crypto/PowermockMacProviderTest.groovy rename impl/src/test/groovy/io/jsonwebtoken/{crypto => security}/KeysImplTest.groovy (75%) diff --git a/api/src/main/java/io/jsonwebtoken/JwtParser.java b/api/src/main/java/io/jsonwebtoken/JwtParser.java index 9b76e2ea..82d48494 100644 --- a/api/src/main/java/io/jsonwebtoken/JwtParser.java +++ b/api/src/main/java/io/jsonwebtoken/JwtParser.java @@ -17,6 +17,7 @@ package io.jsonwebtoken; import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Deserializer; +import io.jsonwebtoken.security.SignatureException; import java.security.Key; import java.util.Date; diff --git a/api/src/main/java/io/jsonwebtoken/SignatureAlgorithm.java b/api/src/main/java/io/jsonwebtoken/SignatureAlgorithm.java index a44c0b15..d216a522 100644 --- a/api/src/main/java/io/jsonwebtoken/SignatureAlgorithm.java +++ b/api/src/main/java/io/jsonwebtoken/SignatureAlgorithm.java @@ -16,6 +16,16 @@ package io.jsonwebtoken; import io.jsonwebtoken.lang.RuntimeEnvironment; +import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.Keys; +import io.jsonwebtoken.security.SignatureException; +import io.jsonwebtoken.security.WeakKeyException; + +import javax.crypto.SecretKey; +import java.security.Key; +import java.security.PrivateKey; +import java.security.interfaces.ECKey; +import java.security.interfaces.RSAKey; /** * Type-safe representation of standard JWT signature algorithm names as defined in the @@ -25,85 +35,104 @@ import io.jsonwebtoken.lang.RuntimeEnvironment; */ public enum SignatureAlgorithm { - /** JWA name for {@code No digital signature or MAC performed} */ - NONE("none", "No digital signature or MAC performed", "None", null, false), + /** + * JWA name for {@code No digital signature or MAC performed} + */ + NONE("none", "No digital signature or MAC performed", "None", null, false, 0, 0), - /** JWA algorithm name for {@code HMAC using SHA-256} */ - HS256("HS256", "HMAC using SHA-256", "HMAC", "HmacSHA256", true), + /** + * JWA algorithm name for {@code HMAC using SHA-256} + */ + HS256("HS256", "HMAC using SHA-256", "HMAC", "HmacSHA256", true, 256, 256), - /** JWA algorithm name for {@code HMAC using SHA-384} */ - HS384("HS384", "HMAC using SHA-384", "HMAC", "HmacSHA384", true), + /** + * JWA algorithm name for {@code HMAC using SHA-384} + */ + HS384("HS384", "HMAC using SHA-384", "HMAC", "HmacSHA384", true, 384, 384), - /** JWA algorithm name for {@code HMAC using SHA-512} */ - HS512("HS512", "HMAC using SHA-512", "HMAC", "HmacSHA512", true), + /** + * JWA algorithm name for {@code HMAC using SHA-512} + */ + HS512("HS512", "HMAC using SHA-512", "HMAC", "HmacSHA512", true, 512, 512), - /** JWA algorithm name for {@code RSASSA-PKCS-v1_5 using SHA-256} */ - RS256("RS256", "RSASSA-PKCS-v1_5 using SHA-256", "RSA", "SHA256withRSA", true), + /** + * JWA algorithm name for {@code RSASSA-PKCS-v1_5 using SHA-256} + */ + RS256("RS256", "RSASSA-PKCS-v1_5 using SHA-256", "RSA", "SHA256withRSA", true, 256, 2048), - /** JWA algorithm name for {@code RSASSA-PKCS-v1_5 using SHA-384} */ - RS384("RS384", "RSASSA-PKCS-v1_5 using SHA-384", "RSA", "SHA384withRSA", true), + /** + * JWA algorithm name for {@code RSASSA-PKCS-v1_5 using SHA-384} + */ + RS384("RS384", "RSASSA-PKCS-v1_5 using SHA-384", "RSA", "SHA384withRSA", true, 384, 2048), - /** JWA algorithm name for {@code RSASSA-PKCS-v1_5 using SHA-512} */ - RS512("RS512", "RSASSA-PKCS-v1_5 using SHA-512", "RSA", "SHA512withRSA", true), + /** + * JWA algorithm name for {@code RSASSA-PKCS-v1_5 using SHA-512} + */ + RS512("RS512", "RSASSA-PKCS-v1_5 using SHA-512", "RSA", "SHA512withRSA", true, 512, 2048), /** * JWA algorithm name for {@code ECDSA using P-256 and SHA-256}. This is not a JDK standard algorithm and * requires that a JCA provider like BouncyCastle be in the runtime classpath. BouncyCastle will be used * automatically if found in the runtime classpath. */ - ES256("ES256", "ECDSA using P-256 and SHA-256", "Elliptic Curve", "SHA256withECDSA", false), + ES256("ES256", "ECDSA using P-256 and SHA-256", "ECDSA", "SHA256withECDSA", false, 256, 256), /** * JWA algorithm name for {@code ECDSA using P-384 and SHA-384}. This is not a JDK standard algorithm and * requires that a JCA provider like BouncyCastle be in the runtime classpath. BouncyCastle will be used * automatically if found in the runtime classpath. */ - ES384("ES384", "ECDSA using P-384 and SHA-384", "Elliptic Curve", "SHA384withECDSA", false), + ES384("ES384", "ECDSA using P-384 and SHA-384", "ECDSA", "SHA384withECDSA", false, 384, 384), /** - * JWA algorithm name for {@code ECDSA using P-512 and SHA-512}. This is not a JDK standard algorithm and + * JWA algorithm name for {@code ECDSA using P-521 and SHA-512}. This is not a JDK standard algorithm and * requires that a JCA provider like BouncyCastle be in the runtime classpath. BouncyCastle will be used * automatically if found in the runtime classpath. */ - ES512("ES512", "ECDSA using P-512 and SHA-512", "Elliptic Curve", "SHA512withECDSA", false), + ES512("ES512", "ECDSA using P-521 and SHA-512", "ECDSA", "SHA512withECDSA", false, 512, 521), /** * JWA algorithm name for {@code RSASSA-PSS using SHA-256 and MGF1 with SHA-256}. This is not a JDK standard * algorithm and requires that a JCA provider like BouncyCastle be in the runtime classpath. BouncyCastle * will be used automatically if found in the runtime classpath. */ - PS256("PS256", "RSASSA-PSS using SHA-256 and MGF1 with SHA-256", "RSA", "SHA256withRSAandMGF1", false), + PS256("PS256", "RSASSA-PSS using SHA-256 and MGF1 with SHA-256", "RSA", "SHA256withRSAandMGF1", false, 256, 2048), /** * JWA algorithm name for {@code RSASSA-PSS using SHA-384 and MGF1 with SHA-384}. This is not a JDK standard * algorithm and requires that a JCA provider like BouncyCastle be in the runtime classpath. BouncyCastle * will be used automatically if found in the runtime classpath. */ - PS384("PS384", "RSASSA-PSS using SHA-384 and MGF1 with SHA-384", "RSA", "SHA384withRSAandMGF1", false), + PS384("PS384", "RSASSA-PSS using SHA-384 and MGF1 with SHA-384", "RSA", "SHA384withRSAandMGF1", false, 384, 2048), /** * JWA algorithm name for {@code RSASSA-PSS using SHA-512 and MGF1 with SHA-512}. This is not a JDK standard * algorithm and requires that a JCA provider like BouncyCastle be in the classpath. BouncyCastle will be used * automatically if found in the runtime classpath. */ - PS512("PS512", "RSASSA-PSS using SHA-512 and MGF1 with SHA-512", "RSA", "SHA512withRSAandMGF1", false); + PS512("PS512", "RSASSA-PSS using SHA-512 and MGF1 with SHA-512", "RSA", "SHA512withRSAandMGF1", false, 512, 2048); static { RuntimeEnvironment.enableBouncyCastleIfPossible(); } - private final String value; - private final String description; - private final String familyName; - private final String jcaName; + private final String value; + private final String description; + private final String familyName; + private final String jcaName; private final boolean jdkStandard; + private final int digestLength; + private final int minKeyLength; - SignatureAlgorithm(String value, String description, String familyName, String jcaName, boolean jdkStandard) { + SignatureAlgorithm(String value, String description, String familyName, String jcaName, boolean jdkStandard, + int digestLength, int minKeyLength) { this.value = value; this.description = description; this.familyName = familyName; this.jcaName = jcaName; this.jdkStandard = jdkStandard; + this.digestLength = digestLength; + this.minKeyLength = minKeyLength; } /** @@ -130,67 +159,66 @@ public enum SignatureAlgorithm { * following table: * * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * - * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * *
Crypto Family
SignatureAlgorithmFamily Name
HS256HMAC
HS384HMAC
HS512HMAC
RS256RSA
RS384RSA
RS512RSA
PS256RSA
PS384RSA
PS512RSA
ES256Elliptic Curve
ES384Elliptic Curve
ES512Elliptic Curve
Crypto Family
SignatureAlgorithmFamily Name
HS256HMAC
HS384HMAC
HS512HMAC
RS256RSA
RS384RSA
RS512RSA
PS256RSA
PS384RSA
PS512RSA
ES256ECDSA
ES384ECDSA
ES512ECDSA
* * @return Returns the cryptographic family name of the signature algorithm. - * * @since 0.5 */ public String getFamilyName() { @@ -225,7 +253,7 @@ public enum SignatureAlgorithm { * @return {@code true} if the enum instance represents an HMAC signature algorithm, {@code false} otherwise. */ public boolean isHmac() { - return name().startsWith("HS"); + return familyName.equals("HMAC"); } /** @@ -236,18 +264,152 @@ public enum SignatureAlgorithm { * {@code false} otherwise. */ public boolean isRsa() { - return getDescription().startsWith("RSASSA"); + return familyName.equals("RSA"); } /** - * Returns {@code true} if the enum instance represents an Elliptic Curve signature algorithm, {@code false} + * Returns {@code true} if the enum instance represents an Elliptic Curve ECDSA signature algorithm, {@code false} * otherwise. * - * @return {@code true} if the enum instance represents an Elliptic Curve signature algorithm, {@code false} + * @return {@code true} if the enum instance represents an Elliptic Curve ECDSA signature algorithm, {@code false} * otherwise. */ public boolean isEllipticCurve() { - return name().startsWith("ES"); + return familyName.equals("ECDSA"); + } + + /** + * Returns quietly if the specified key is allowed to create signatures using this algorithm + * according to the JWT JWA Specification (RFC 7518) or throws an + * {@link InvalidKeyException} if the key is not allowed or not secure enough for this algorithm. + * + * @param key the key to check for validity. + * @throws InvalidKeyException if the key is not allowed or not secure enough for this algorithm. + * @since 0.10.0 + */ + public void assertValidSigningKey(Key key) throws InvalidKeyException { + assertValid(key, true); + } + + /** + * Returns quietly if the specified key is allowed to verify signatures using this algorithm + * according to the JWT JWA Specification (RFC 7518) or throws an + * {@link InvalidKeyException} if the key is not allowed or not secure enough for this algorithm. + * + * @param key the key to check for validity. + * @throws InvalidKeyException if the key is not allowed or not secure enough for this algorithm. + * @since 0.10.0 + */ + public void assertValidVerificationKey(Key key) throws InvalidKeyException { + assertValid(key, false); + } + + /** + * @since 0.10.0 to support assertValid(Key, boolean) + */ + private static String keyType(boolean signing) { + return signing ? "signing" : "verification"; + } + + /** + * @since 0.10.0 + */ + private void assertValid(Key key, boolean signing) throws InvalidKeyException { + + if (this == NONE) { + + String msg = "The 'NONE' signature algorithm does not support cryptographic keys."; + throw new InvalidKeyException(msg); + + } else if (isHmac()) { + + if (!(key instanceof SecretKey)) { + String msg = this.familyName + " " + keyType(signing) + " keys must be SecretKey instances."; + throw new InvalidKeyException(msg); + } + SecretKey secretKey = (SecretKey) key; + + byte[] encoded = secretKey.getEncoded(); + if (encoded == null) { + throw new InvalidKeyException("The " + keyType(signing) + " key's encoded bytes cannot be null."); + } + + String alg = secretKey.getAlgorithm(); + if (alg == null) { + throw new InvalidKeyException("The " + keyType(signing) + " key's algorithm cannot be null."); + } + if (!HS256.jcaName.equals(alg) && !HS384.jcaName.equals(alg) && !HS512.jcaName.equals(alg)) { + throw new InvalidKeyException("The " + keyType(signing) + " key's algorithm '" + alg + + "' does not equal a valid HmacSHA* algorithm name and cannot be used with " + name() + "."); + } + + int size = encoded.length * 8; //size in bits + if (size < this.minKeyLength) { + String msg = "The " + keyType(signing) + " key's size is " + size + " bits which " + + "is not secure enough for the " + name() + " algorithm. The JWT " + + "JWA Specification (RFC 7518, Section 3.2) states that keys used with " + name() + " MUST have a " + + "size >= " + minKeyLength + " bits (the key size must be greater than or equal to the hash " + + "output size). Consider using the " + Keys.class.getName() + " class's " + + "'secretKeyFor(SignatureAlgorithm." + name() + ")' method to create a key guaranteed to be " + + "secure enough for " + name() + ". See " + + "https://tools.ietf.org/html/rfc7518#section-3.2 for more information."; + throw new WeakKeyException(msg); + } + + } else { //EC or RSA + + if (signing) { + if (!(key instanceof PrivateKey)) { + String msg = familyName + " signing keys must be PrivateKey instances."; + throw new InvalidKeyException(msg); + } + } + + if (isEllipticCurve()) { + + if (!(key instanceof ECKey)) { + String msg = familyName + " " + keyType(signing) + " keys must be ECKey instances."; + throw new InvalidKeyException(msg); + } + + ECKey ecKey = (ECKey) key; + int size = ecKey.getParams().getOrder().bitLength(); + if (size < this.minKeyLength) { + String msg = "The " + keyType(signing) + " key's size (ECParameterSpec order) is " + size + + " bits which is not secure enough for the " + name() + " algorithm. The JWT " + + "JWA Specification (RFC 7518, Section 3.4) states that keys used with " + + name() + " MUST have a size >= " + this.minKeyLength + + " bits. Consider using the " + Keys.class.getName() + " class's " + + "'keyPairFor(SignatureAlgorithm." + name() + ")' method to create a key pair guaranteed " + + "to be secure enough for " + name() + ". See " + + "https://tools.ietf.org/html/rfc7518#section-3.4 for more information."; + throw new WeakKeyException(msg); + } + + } else { //RSA + + if (!(key instanceof RSAKey)) { + String msg = familyName + " " + keyType(signing) + " keys must be RSAKey instances."; + throw new InvalidKeyException(msg); + } + + RSAKey rsaKey = (RSAKey)key; + int size = rsaKey.getModulus().bitLength(); + if (size < this.minKeyLength) { + + String section = name().startsWith("P") ? "3.5" : "3.3"; + + String msg = "The " + keyType(signing) + " key's size is " + size + " bits which is not secure " + + "enough for the " + name() + " algorithm. The JWT JWA Specification (RFC 7518, Section " + + section + ") states that keys used with " + name() + " MUST have a size >= " + + this.minKeyLength + " bits. Consider using the " + Keys.class.getName() + " class's " + + "'keyPairFor(SignatureAlgorithm." + name() + ")' method to create a key pair guaranteed " + + "to be secure enough for " + name() + ". See " + + "https://tools.ietf.org/html/rfc7518#section-" + section + " for more information."; + throw new WeakKeyException(msg); + } + } + } } /** diff --git a/api/src/main/java/io/jsonwebtoken/SignatureException.java b/api/src/main/java/io/jsonwebtoken/SignatureException.java index 312403f9..e98b4c72 100644 --- a/api/src/main/java/io/jsonwebtoken/SignatureException.java +++ b/api/src/main/java/io/jsonwebtoken/SignatureException.java @@ -15,12 +15,16 @@ */ package io.jsonwebtoken; +import io.jsonwebtoken.security.SecurityException; + /** * Exception indicating that either calculating a signature or verifying an existing signature of a JWT failed. * * @since 0.1 + * @deprecated in favor of {@link io.jsonwebtoken.security.SecurityException}; this class will be removed before 1.0 */ -public class SignatureException extends JwtException { +@Deprecated +public class SignatureException extends SecurityException { public SignatureException(String message) { super(message); diff --git a/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java b/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java new file mode 100644 index 00000000..3c21361b --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/InvalidKeyException.java @@ -0,0 +1,11 @@ +package io.jsonwebtoken.security; + +/** + * @since 0.10.0 + */ +public class InvalidKeyException extends KeyException { + + public InvalidKeyException(String message) { + super(message); + } +} diff --git a/api/src/main/java/io/jsonwebtoken/security/KeyException.java b/api/src/main/java/io/jsonwebtoken/security/KeyException.java new file mode 100644 index 00000000..d435a762 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/KeyException.java @@ -0,0 +1,11 @@ +package io.jsonwebtoken.security; + +/** + * @since 0.10.0 + */ +public class KeyException extends SecurityException { + + public KeyException(String message) { + super(message); + } +} diff --git a/api/src/main/java/io/jsonwebtoken/crypto/Keys.java b/api/src/main/java/io/jsonwebtoken/security/Keys.java similarity index 89% rename from api/src/main/java/io/jsonwebtoken/crypto/Keys.java rename to api/src/main/java/io/jsonwebtoken/security/Keys.java index c3c79ec3..92cf43b5 100644 --- a/api/src/main/java/io/jsonwebtoken/crypto/Keys.java +++ b/api/src/main/java/io/jsonwebtoken/security/Keys.java @@ -1,4 +1,4 @@ -package io.jsonwebtoken.crypto; +package io.jsonwebtoken.security; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.lang.Assert; @@ -24,6 +24,22 @@ public final class Keys { private Keys() { } + /* + public static final int bitLength(Key key) throws IllegalArgumentException { + Assert.notNull(key, "Key cannot be null."); + if (key instanceof SecretKey) { + byte[] encoded = key.getEncoded(); + return Arrays.length(encoded) * 8; + } else if (key instanceof RSAKey) { + return ((RSAKey)key).getModulus().bitLength(); + } else if (key instanceof ECKey) { + return ((ECKey)key).getParams().getOrder().bitLength(); + } + + throw new IllegalArgumentException("Unsupported key type: " + key.getClass().getName()); + } + */ + /** * Returns a new {@link SecretKey} with a key length suitable for use with the specified {@link SignatureAlgorithm}. * diff --git a/api/src/main/java/io/jsonwebtoken/security/SecurityException.java b/api/src/main/java/io/jsonwebtoken/security/SecurityException.java new file mode 100644 index 00000000..57e80b16 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SecurityException.java @@ -0,0 +1,17 @@ +package io.jsonwebtoken.security; + +import io.jsonwebtoken.JwtException; + +/** + * @since 0.10.0 + */ +public class SecurityException extends JwtException { + + public SecurityException(String message) { + super(message); + } + + public SecurityException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/api/src/main/java/io/jsonwebtoken/security/SignatureException.java b/api/src/main/java/io/jsonwebtoken/security/SignatureException.java new file mode 100644 index 00000000..2ab77cfd --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/SignatureException.java @@ -0,0 +1,15 @@ +package io.jsonwebtoken.security; + +/** + * @since 0.10.0 + */ +public class SignatureException extends io.jsonwebtoken.SignatureException { + + public SignatureException(String message) { + super(message); + } + + public SignatureException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/api/src/main/java/io/jsonwebtoken/security/WeakKeyException.java b/api/src/main/java/io/jsonwebtoken/security/WeakKeyException.java new file mode 100644 index 00000000..ad32cde2 --- /dev/null +++ b/api/src/main/java/io/jsonwebtoken/security/WeakKeyException.java @@ -0,0 +1,11 @@ +package io.jsonwebtoken.security; + +/** + * @since 0.10.0 + */ +public class WeakKeyException extends InvalidKeyException { + + public WeakKeyException(String message) { + super(message); + } +} diff --git a/api/src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy b/api/src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy index 2a5757b8..e900f8d1 100644 --- a/api/src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy @@ -15,17 +15,33 @@ */ package io.jsonwebtoken +import io.jsonwebtoken.security.InvalidKeyException +import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.security.SignatureException import org.junit.Test + +import javax.crypto.SecretKey +import java.security.Key +import java.security.PrivateKey +import java.security.interfaces.ECPrivateKey +import java.security.interfaces.ECPublicKey +import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey +import java.security.spec.ECParameterSpec + +import static org.easymock.EasyMock.* import static org.junit.Assert.* class SignatureAlgorithmTest { + private static final Random random = new Random() //does not need to be secure for testing + @Test void testNames() { def algNames = ['HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'PS256', 'PS384', 'PS512', 'NONE'] - for( String name : algNames ) { + for (String name : algNames) { testName(name) } } @@ -44,7 +60,7 @@ class SignatureAlgorithmTest { @Test void testIsHmac() { - for(SignatureAlgorithm alg : SignatureAlgorithm.values()) { + for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { if (alg.name().startsWith("HS")) { assertTrue alg.isHmac() } else { @@ -55,7 +71,7 @@ class SignatureAlgorithmTest { @Test void testHmacFamilyName() { - for(SignatureAlgorithm alg : SignatureAlgorithm.values()) { + for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { if (alg.name().startsWith("HS")) { assertEquals alg.getFamilyName(), "HMAC" } @@ -64,7 +80,7 @@ class SignatureAlgorithmTest { @Test void testIsRsa() { - for(SignatureAlgorithm alg : SignatureAlgorithm.values()) { + for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { if (alg.getDescription().startsWith("RSASSA")) { assertTrue alg.isRsa() } else { @@ -75,7 +91,7 @@ class SignatureAlgorithmTest { @Test void testRsaFamilyName() { - for(SignatureAlgorithm alg : SignatureAlgorithm.values()) { + for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { if (alg.name().startsWith("RS") || alg.name().startsWith("PS")) { assertEquals alg.getFamilyName(), "RSA" } @@ -84,7 +100,7 @@ class SignatureAlgorithmTest { @Test void testIsEllipticCurve() { - for(SignatureAlgorithm alg : SignatureAlgorithm.values()) { + for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { if (alg.name().startsWith("ES")) { assertTrue alg.isEllipticCurve() } else { @@ -95,16 +111,16 @@ class SignatureAlgorithmTest { @Test void testEllipticCurveFamilyName() { - for(SignatureAlgorithm alg : SignatureAlgorithm.values()) { + for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { if (alg.name().startsWith("ES")) { - assertEquals alg.getFamilyName(), "Elliptic Curve" + assertEquals alg.getFamilyName(), "ECDSA" } } } @Test void testIsJdkStandard() { - for(SignatureAlgorithm alg : SignatureAlgorithm.values()) { + for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { if (alg.name().startsWith("ES") || alg.name().startsWith("PS") || alg == SignatureAlgorithm.NONE) { assertFalse alg.isJdkStandard() } else { @@ -112,4 +128,618 @@ class SignatureAlgorithmTest { } } } + + @Test + void testAssertValidSigningKeyWithNoneAlgorithm() { + Key key = createMock(Key) + try { + SignatureAlgorithm.NONE.assertValidSigningKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals "The 'NONE' signature algorithm does not support cryptographic keys." as String, expected.message + } + } + + @Test + void testAssertValidHmacSigningKeyHappyPath() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isHmac() }) { + + SecretKey key = createMock(SecretKey) + int numBits = alg.minKeyLength + int numBytes = numBits / 8 as int + expect(key.getEncoded()).andReturn(new byte[numBytes]) + expect(key.getAlgorithm()).andReturn(alg.jcaName) + + replay key + + alg.assertValidSigningKey(key) + + verify key + } + } + + @Test + void testAssertValidHmacSigningKeyNotSecretKey() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isHmac() }) { + + Key key = createMock(Key) + + try { + alg.assertValidSigningKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals 'HMAC signing keys must be SecretKey instances.', expected.message + } + } + } + + @Test + void testAssertValidHmacSigningKeyNullBytes() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isHmac() }) { + + SecretKey key = createMock(SecretKey) + expect(key.getEncoded()).andReturn(null) + + replay key + + try { + alg.assertValidSigningKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals "The signing key's encoded bytes cannot be null.", expected.message + } + + verify key + } + } + + @Test + void testAssertValidHmacSigningKeyMissingAlgorithm() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isHmac() }) { + + SecretKey key = createMock(SecretKey) + expect(key.getEncoded()).andReturn(new byte[alg.minKeyLength / 8 as int]) + expect(key.getAlgorithm()).andReturn(null) + + replay key + + try { + alg.assertValidSigningKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals "The signing key's algorithm cannot be null.", expected.message + } + + verify key + } + } + + @Test + void testAssertValidHmacSigningKeyUnsupportedAlgorithm() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isHmac() }) { + + SecretKey key = createMock(SecretKey) + expect(key.getEncoded()).andReturn(new byte[alg.minKeyLength / 8 as int]) + expect(key.getAlgorithm()).andReturn('AES') + + replay key + + try { + alg.assertValidSigningKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals "The signing key's algorithm 'AES' does not equal a valid HmacSHA* algorithm " + + "name and cannot be used with ${alg.name()}." as String, expected.message + } + + verify key + } + } + + @Test + void testAssertValidHmacSigningKeyInsufficientKeyLength() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isHmac() }) { + + SecretKey key = createMock(SecretKey) + int numBits = alg.minKeyLength - 8 //8 bits shorter than expected + int numBytes = numBits / 8 as int + expect(key.getEncoded()).andReturn(new byte[numBytes]) + expect(key.getAlgorithm()).andReturn(alg.jcaName) + + replay key + + try { + alg.assertValidSigningKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals "The signing key's size is $numBits bits which is not secure enough for the " + + "${alg.name()} algorithm. The JWT JWA Specification " + + "(RFC 7518, Section 3.2) states that keys used with ${alg.name()} MUST have a size >= " + + "${alg.minKeyLength} bits (the key size must be greater than or equal to the hash output " + + "size). Consider using the ${Keys.class.getName()} class's 'secretKeyFor(" + + "SignatureAlgorithm.${alg.name()})' method to create a key guaranteed to be secure enough " + + "for ${alg.name()}. See https://tools.ietf.org/html/rfc7518#section-3.2 for " + + "more information." as String, expected.message + } + + verify key + } + } + + @Test + void testAssertValidECSigningKeyHappyPath() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isEllipticCurve() }) { + + ECPrivateKey key = createMock(ECPrivateKey) + ECParameterSpec spec = createMock(ECParameterSpec) + int numBits = alg.minKeyLength + int numBytes = numBits / 8 as int + byte[] orderBytes = new byte[numBytes + 1] + random.nextBytes(orderBytes) + BigInteger order = new BigInteger(orderBytes) + expect(key.getParams()).andReturn(spec) + expect(spec.getOrder()).andReturn(order) + + replay key, spec + + alg.assertValidSigningKey(key) + + verify key, spec + } + } + + @Test + void testAssertValidECSigningNotPrivateKey() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isEllipticCurve() }) { + + ECPublicKey key = createMock(ECPublicKey) + + replay key + + try { + alg.assertValidSigningKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals 'ECDSA signing keys must be PrivateKey instances.', expected.message + } + + verify key + } + } + + @Test + void testAssertValidECSigningKeyNotECKey() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isEllipticCurve() }) { + + PrivateKey key = createMock(PrivateKey) + + try { + alg.assertValidSigningKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals 'ECDSA signing keys must be ECKey instances.', expected.message + } + } + } + + @Test + void testAssertValidECSigningKeyInsufficientKeyLength() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isEllipticCurve() }) { + + ECPrivateKey key = createMock(ECPrivateKey) + ECParameterSpec spec = createMock(ECParameterSpec) + int numBits = alg.minKeyLength - 8 // 8 bits less than expected + int numBytes = numBits / 8 as int + byte[] orderBytes = new byte[numBytes] + random.nextBytes(orderBytes) + BigInteger order = new BigInteger(orderBytes) + expect(key.getParams()).andReturn(spec) + expect(spec.getOrder()).andReturn(order) + + replay key, spec + + try { + alg.assertValidSigningKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals "The signing key's size (ECParameterSpec order) is ${order.bitLength()} bits " + + "which is not secure enough for the ${alg.name()} algorithm. The JWT JWA Specification " + + "(RFC 7518, Section 3.4) states that keys used with ${alg.name()} MUST have a size >= " + + "${alg.minKeyLength} bits. Consider using the ${Keys.class.getName()} class's " + + "'keyPairFor(SignatureAlgorithm.${alg.name()})' method to create a key pair guaranteed " + + "to be secure enough for ${alg.name()}. See " + + "https://tools.ietf.org/html/rfc7518#section-3.4 for more information." as String, expected.message + } + + verify key, spec + } + } + + @Test + void testAssertValidRSASigningKeyHappyPath() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isRsa() }) { + + RSAPrivateKey key = createMock(RSAPrivateKey) + int numBits = alg.minKeyLength + int numBytes = numBits / 8 as int + byte[] modulusBytes = new byte[numBytes + 1] + random.nextBytes(modulusBytes) + BigInteger modulus = new BigInteger(modulusBytes) + expect(key.getModulus()).andReturn(modulus) + + replay key + + alg.assertValidSigningKey(key) + + verify key + } + } + + @Test + void testAssertValidRSASigningNotPrivateKey() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isRsa() }) { + + RSAPublicKey key = createMock(RSAPublicKey) + + replay key + + try { + alg.assertValidSigningKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals 'RSA signing keys must be PrivateKey instances.', expected.message + } + + verify key + } + } + + @Test + void testAssertValidRSASigningKeyNotRSAKey() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isRsa() }) { + + PrivateKey key = createMock(PrivateKey) + + try { + alg.assertValidSigningKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals 'RSA signing keys must be RSAKey instances.', expected.message + } + } + } + + @Test + void testAssertValidRSASigningKeyInsufficientKeyLength() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isRsa() }) { + + String section = alg.name().startsWith("P") ? "3.5" : "3.3" + + RSAPrivateKey key = createMock(RSAPrivateKey) + int numBits = alg.minKeyLength - 8 + int numBytes = numBits / 8 as int + byte[] modulusBytes = new byte[numBytes] + random.nextBytes(modulusBytes) + BigInteger modulus = new BigInteger(modulusBytes) + expect(key.getModulus()).andReturn(modulus) + + replay key + + try { + alg.assertValidSigningKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals "The signing key's size is ${modulus.bitLength()} bits which is not secure " + + "enough for the ${alg.name()} algorithm. The JWT JWA Specification " + + "(RFC 7518, Section ${section}) states that keys used with ${alg.name()} MUST have a size >= " + + "${alg.minKeyLength} bits. Consider using the ${Keys.class.getName()} class's " + + "'keyPairFor(SignatureAlgorithm.${alg.name()})' method to create a key pair guaranteed " + + "to be secure enough for ${alg.name()}. See " + + "https://tools.ietf.org/html/rfc7518#section-${section} for more information." as String, expected.message + } + + verify key + } + } + + @Test + void testAssertValidVerificationKeyWithNoneAlgorithm() { + Key key = createMock(Key) + try { + SignatureAlgorithm.NONE.assertValidVerificationKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals "The 'NONE' signature algorithm does not support cryptographic keys." as String, expected.message + } + } + + @Test + void testAssertValidHmacVerificationKeyHappyPath() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isHmac() }) { + + SecretKey key = createMock(SecretKey) + int numBits = alg.minKeyLength + int numBytes = numBits / 8 as int + expect(key.getEncoded()).andReturn(new byte[numBytes]) + expect(key.getAlgorithm()).andReturn(alg.jcaName) + + replay key + + alg.assertValidVerificationKey(key) + + verify key + } + } + + @Test + void testAssertValidHmacVerificationKeyNotSecretKey() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isHmac() }) { + + Key key = createMock(Key) + + try { + alg.assertValidVerificationKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals 'HMAC verification keys must be SecretKey instances.', expected.message + } + } + } + + @Test + void testAssertValidHmacVerificationKeyNullBytes() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isHmac() }) { + + SecretKey key = createMock(SecretKey) + expect(key.getEncoded()).andReturn(null) + + replay key + + try { + alg.assertValidVerificationKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals "The verification key's encoded bytes cannot be null.", expected.message + } + + verify key + } + } + + @Test + void testAssertValidHmacVerificationKeyMissingAlgorithm() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isHmac() }) { + + SecretKey key = createMock(SecretKey) + expect(key.getEncoded()).andReturn(new byte[alg.minKeyLength / 8 as int]) + expect(key.getAlgorithm()).andReturn(null) + + replay key + + try { + alg.assertValidVerificationKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals "The verification key's algorithm cannot be null.", expected.message + } + + verify key + } + } + + @Test + void testAssertValidHmacVerificationKeyUnsupportedAlgorithm() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isHmac() }) { + + SecretKey key = createMock(SecretKey) + expect(key.getEncoded()).andReturn(new byte[alg.minKeyLength / 8 as int]) + expect(key.getAlgorithm()).andReturn('AES') + + replay key + + try { + alg.assertValidVerificationKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals "The verification key's algorithm 'AES' does not equal a valid HmacSHA* algorithm " + + "name and cannot be used with ${alg.name()}." as String, expected.message + } + + verify key + } + } + + @Test + void testAssertValidHmacVerificationKeyInsufficientKeyLength() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isHmac() }) { + + SecretKey key = createMock(SecretKey) + int numBits = alg.minKeyLength - 8 // 8 bits (1 byte) less than required + int numBytes = numBits / 8 as int + expect(key.getEncoded()).andReturn(new byte[numBytes]) + expect(key.getAlgorithm()).andReturn(alg.jcaName) + + replay key + + try { + alg.assertValidVerificationKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals "The verification key's size is $numBits bits which is not secure enough for the " + + "${alg.name()} algorithm. The JWT JWA Specification " + + "(RFC 7518, Section 3.2) states that keys used with ${alg.name()} MUST have a size >= " + + "${alg.minKeyLength} bits (the key size must be greater than or equal to the hash output " + + "size). Consider using the ${Keys.class.getName()} class's 'secretKeyFor(" + + "SignatureAlgorithm.${alg.name()})' method to create a key guaranteed to be secure enough " + + "for ${alg.name()}. See https://tools.ietf.org/html/rfc7518#section-3.2 for " + + "more information." as String, expected.message + } + + verify key + } + } + + @Test + void testAssertValidECVerificationKeyHappyPath() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isEllipticCurve() }) { + + ECPrivateKey key = createMock(ECPrivateKey) + ECParameterSpec spec = createMock(ECParameterSpec) + int numBits = alg.minKeyLength + int numBytes = numBits / 8 as int + byte[] orderBytes = new byte[numBytes + 1] + random.nextBytes(orderBytes) + BigInteger order = new BigInteger(orderBytes) + expect(key.getParams()).andReturn(spec) + expect(spec.getOrder()).andReturn(order) + + replay key, spec + + alg.assertValidVerificationKey(key) + + verify key, spec + } + } + + @Test + void testAssertValidECVerificationKeyNotECKey() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isEllipticCurve() }) { + + PrivateKey key = createMock(PrivateKey) + + try { + alg.assertValidVerificationKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals 'ECDSA verification keys must be ECKey instances.', expected.message + } + } + } + + @Test + void testAssertValidECVerificationKeyInsufficientKeyLength() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isEllipticCurve() }) { + + ECPrivateKey key = createMock(ECPrivateKey) + ECParameterSpec spec = createMock(ECParameterSpec) + int numBits = alg.minKeyLength - 8 // 8 bits = 1 byte + int numBytes = numBits / 8 as int + byte[] orderBytes = new byte[numBytes] + random.nextBytes(orderBytes) + BigInteger order = new BigInteger(orderBytes) + expect(key.getParams()).andReturn(spec) + expect(spec.getOrder()).andReturn(order) + + replay key, spec + + try { + alg.assertValidVerificationKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals "The verification key's size (ECParameterSpec order) is ${order.bitLength()} bits " + + "which is not secure enough for the ${alg.name()} algorithm. The JWT JWA Specification " + + "(RFC 7518, Section 3.4) states that keys used with ${alg.name()} MUST have a size >= " + + "${alg.minKeyLength} bits. Consider using the ${Keys.class.getName()} class's " + + "'keyPairFor(SignatureAlgorithm.${alg.name()})' method to create a key pair guaranteed " + + "to be secure enough for ${alg.name()}. See " + + "https://tools.ietf.org/html/rfc7518#section-3.4 for more information." as String, expected.message + } + + verify key, spec + } + } + + @Test + void testAssertValidRSAVerificationKeyHappyPath() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isRsa() }) { + + RSAPrivateKey key = createMock(RSAPrivateKey) + int numBits = alg.minKeyLength + int numBytes = numBits / 8 as int + byte[] modulusBytes = new byte[numBytes + 1] + random.nextBytes(modulusBytes) + BigInteger modulus = new BigInteger(modulusBytes) + expect(key.getModulus()).andReturn(modulus) + + replay key + + alg.assertValidVerificationKey(key) + + verify key + } + } + + @Test + void testAssertValidRSAVerificationKeyNotRSAKey() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isRsa() }) { + + PrivateKey key = createMock(PrivateKey) + + try { + alg.assertValidVerificationKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals 'RSA verification keys must be RSAKey instances.', expected.message + } + } + } + + @Test + void testAssertValidRSAVerificationKeyInsufficientKeyLength() { + + for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isRsa() }) { + + String section = alg.name().startsWith("P") ? "3.5" : "3.3" + + RSAPrivateKey key = createMock(RSAPrivateKey) + int numBits = alg.minKeyLength - 8 // 8 bits = 1 byte + int numBytes = numBits / 8 as int + byte[] modulusBytes = new byte[numBytes] + random.nextBytes(modulusBytes) + BigInteger modulus = new BigInteger(modulusBytes) + expect(key.getModulus()).andReturn(modulus) + + replay key + + try { + alg.assertValidVerificationKey(key) + fail() + } catch (InvalidKeyException expected) { + assertEquals "The verification key's size is ${modulus.bitLength()} bits which is not secure enough " + + "for the ${alg.name()} algorithm. The JWT JWA Specification " + + "(RFC 7518, Section ${section}) states that keys used with ${alg.name()} MUST have a size >= " + + "${alg.minKeyLength} bits. Consider using the ${Keys.class.getName()} class's " + + "'keyPairFor(SignatureAlgorithm.${alg.name()})' method to create a key pair guaranteed " + + "to be secure enough for ${alg.name()}. See " + + "https://tools.ietf.org/html/rfc7518#section-${section} for more information." as String, expected.message + } + + verify key + } + } } diff --git a/api/src/test/groovy/io/jsonwebtoken/crypto/KeysTest.groovy b/api/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy similarity index 85% rename from api/src/test/groovy/io/jsonwebtoken/crypto/KeysTest.groovy rename to api/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy index ba624e97..0e46cde5 100644 --- a/api/src/test/groovy/io/jsonwebtoken/crypto/KeysTest.groovy +++ b/api/src/test/groovy/io/jsonwebtoken/security/KeysTest.groovy @@ -1,4 +1,4 @@ -package io.jsonwebtoken.crypto +package io.jsonwebtoken.security import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.lang.Classes @@ -13,14 +13,8 @@ import java.security.KeyPair import static org.easymock.EasyMock.eq import static org.easymock.EasyMock.expect import static org.easymock.EasyMock.same -import static org.junit.Assert.assertEquals -import static org.junit.Assert.assertSame -import static org.junit.Assert.fail -import static org.powermock.api.easymock.PowerMock.mockStatic -import static org.powermock.api.easymock.PowerMock.createMock -import static org.powermock.api.easymock.PowerMock.replay -import static org.powermock.api.easymock.PowerMock.reset -import static org.powermock.api.easymock.PowerMock.verify +import static org.junit.Assert.* +import static org.powermock.api.easymock.PowerMock.* /** * This test class is for cursory API-level testing only (what is available to the API module at build time). diff --git a/api/src/test/groovy/io/jsonwebtoken/security/SignatureExceptionTest.groovy b/api/src/test/groovy/io/jsonwebtoken/security/SignatureExceptionTest.groovy new file mode 100644 index 00000000..8110ef4b --- /dev/null +++ b/api/src/test/groovy/io/jsonwebtoken/security/SignatureExceptionTest.groovy @@ -0,0 +1,22 @@ +package io.jsonwebtoken.security + +import org.junit.Test + +import static org.junit.Assert.assertEquals + +class SignatureExceptionTest { + + @Test + void testStringConstructor() { + def exception = new SignatureException("my message") + assertEquals "my message", exception.getMessage() + } + + @Test + void testCauseConstructor() { + def ioException = new IOException("root error") + def exception = new SignatureException("wrapping", ioException) + assertEquals "wrapping", exception.getMessage() + assertEquals ioException, exception.getCause() + } +} \ No newline at end of file diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index c40b90e6..54e256c8 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -22,19 +22,20 @@ import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwtBuilder; import io.jsonwebtoken.JwtParser; import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.io.Encoder; import io.jsonwebtoken.impl.crypto.DefaultJwtSigner; import io.jsonwebtoken.impl.crypto.JwtSigner; +import io.jsonwebtoken.impl.io.InstanceLocator; 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.impl.io.InstanceLocator; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Classes; import io.jsonwebtoken.lang.Collections; import io.jsonwebtoken.lang.Strings; +import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.security.Key; import java.util.Date; @@ -108,13 +109,12 @@ public class DefaultJwtBuilder implements JwtBuilder { } @Override - public JwtBuilder signWith(SignatureAlgorithm alg, byte[] secretKey) { + public JwtBuilder signWith(SignatureAlgorithm alg, byte[] secretKeyBytes) { Assert.notNull(alg, "SignatureAlgorithm cannot be null."); - Assert.notEmpty(secretKey, "secret key byte array cannot be null or empty."); + Assert.notEmpty(secretKeyBytes, "secret key byte array cannot be null or empty."); Assert.isTrue(alg.isHmac(), "Key bytes may only be specified for HMAC signatures. If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead."); - this.algorithm = alg; - this.key = new SecretKeySpec(secretKey, alg.getJcaName()); - return this; + SecretKey key = new SecretKeySpec(secretKeyBytes, alg.getJcaName()); + return signWith(alg, key); } @Override @@ -129,6 +129,7 @@ public class DefaultJwtBuilder implements JwtBuilder { public JwtBuilder signWith(SignatureAlgorithm alg, Key key) { Assert.notNull(alg, "SignatureAlgorithm cannot be null."); Assert.notNull(key, "Key argument cannot be null."); + alg.assertValidSigningKey(key); //since 0.10.0 for https://github.com/jwtk/jjwt/issues/334 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 13b2c035..7812a94a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -34,22 +34,24 @@ import io.jsonwebtoken.MalformedJwtException; import io.jsonwebtoken.MissingClaimException; import io.jsonwebtoken.PrematureJwtException; import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SignatureException; import io.jsonwebtoken.SigningKeyResolver; import io.jsonwebtoken.UnsupportedJwtException; -import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.impl.compression.DefaultCompressionCodecResolver; import io.jsonwebtoken.impl.crypto.DefaultJwtSignatureValidator; import io.jsonwebtoken.impl.crypto.JwtSignatureValidator; +import io.jsonwebtoken.impl.io.InstanceLocator; +import io.jsonwebtoken.io.Decoder; import io.jsonwebtoken.io.Decoders; import io.jsonwebtoken.io.DeserializationException; import io.jsonwebtoken.io.Deserializer; -import io.jsonwebtoken.impl.io.InstanceLocator; import io.jsonwebtoken.lang.Assert; import io.jsonwebtoken.lang.Classes; import io.jsonwebtoken.lang.DateFormats; import io.jsonwebtoken.lang.Objects; import io.jsonwebtoken.lang.Strings; +import io.jsonwebtoken.security.InvalidKeyException; +import io.jsonwebtoken.security.SignatureException; +import io.jsonwebtoken.security.WeakKeyException; import javax.crypto.spec.SecretKeySpec; import java.security.Key; @@ -359,8 +361,11 @@ public class DefaultJwtParser implements JwtParser { JwtSignatureValidator validator; try { + algorithm.assertValidVerificationKey(key); //since 0.10.0: https://github.com/jwtk/jjwt/issues/334 validator = createSignatureValidator(algorithm, key); - } catch (IllegalArgumentException e) { + } catch (WeakKeyException e) { + 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() + 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 23107df1..2c7b01cd 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java @@ -15,6 +15,10 @@ */ package io.jsonwebtoken.impl.crypto; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.lang.Assert; + import java.security.Key; import java.security.KeyPair; import java.security.KeyPairGenerator; @@ -22,10 +26,6 @@ import java.security.SecureRandom; import java.util.HashMap; import java.util.Map; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.lang.Assert; - /** * ElliptiCurve crypto provider. * @@ -172,8 +172,7 @@ public abstract class EllipticCurveProvider extends SignatureProvider { * * @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[] transcodeSignatureToConcat(final byte[] derSignature, int outputLength) throws JwtException { if (derSignature.length < 8 || derSignature[0] != 48) { throw new JwtException("Invalid ECDSA signature format"); @@ -191,16 +190,16 @@ public abstract class EllipticCurveProvider extends SignatureProvider { byte rLength = derSignature[offset + 1]; int i = rLength; - while ((i > 0) - && (derSignature[(offset + 2 + rLength) - i] == 0)) + while ((i > 0) && (derSignature[(offset + 2 + rLength) - i] == 0)) { i--; + } byte sLength = derSignature[offset + 2 + rLength + 1]; int j = sLength; - while ((j > 0) - && (derSignature[(offset + 2 + rLength + 2 + sLength) - j] == 0)) + while ((j > 0) && (derSignature[(offset + 2 + rLength + 2 + sLength) - j] == 0)) { j--; + } int rawLen = Math.max(i, j); rawLen = Math.max(rawLen, outputLength / 2); @@ -234,16 +233,15 @@ public abstract class EllipticCurveProvider extends SignatureProvider { * * @throws JwtException If the ECDSA JWS signature format is invalid. */ - public static byte[] transcodeSignatureToDER(byte[] jwsSignature) - throws JwtException { + public static byte[] transcodeSignatureToDER(byte[] jwsSignature) throws JwtException { int rawLen = jwsSignature.length / 2; int i = rawLen; - while((i > 0) - && (jwsSignature[rawLen - i] == 0)) + while((i > 0) && (jwsSignature[rawLen - i] == 0)) { i--; + } int j = i; @@ -253,9 +251,9 @@ public abstract class EllipticCurveProvider extends SignatureProvider { int k = rawLen; - while ((k > 0) - && (jwsSignature[2 * rawLen - k] == 0)) + while ((k > 0) && (jwsSignature[2 * rawLen - k] == 0)) { k--; + } int l = k; 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 09ab14db..3fc1fd9a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java @@ -15,16 +15,16 @@ */ package io.jsonwebtoken.impl.crypto; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.SignatureException; + import java.security.InvalidKeyException; import java.security.Key; import java.security.PublicKey; import java.security.Signature; import java.security.interfaces.ECPublicKey; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SignatureException; -import io.jsonwebtoken.lang.Assert; - public class EllipticCurveSignatureValidator extends EllipticCurveProvider implements SignatureValidator { private static final String EC_PUBLIC_KEY_REQD_MSG = 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 2e1f3c7b..9aeb1e8b 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSigner.java @@ -15,16 +15,16 @@ */ package io.jsonwebtoken.impl.crypto; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.SignatureException; + import java.security.InvalidKeyException; import java.security.Key; import java.security.PrivateKey; import java.security.Signature; import java.security.interfaces.ECKey; -import io.jsonwebtoken.JwtException; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SignatureException; - public class EllipticCurveSigner extends EllipticCurveProvider implements Signer { public EllipticCurveSigner(SignatureAlgorithm alg, Key key) { diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java index 9afa71cf..b0117411 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java @@ -18,9 +18,10 @@ package io.jsonwebtoken.impl.crypto; import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.lang.Assert; +import javax.crypto.KeyGenerator; import javax.crypto.SecretKey; -import javax.crypto.spec.SecretKeySpec; import java.security.Key; +import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; public abstract class MacProvider extends SignatureProvider { @@ -31,11 +32,11 @@ public abstract class MacProvider extends SignatureProvider { } /** - * Generates a new secure-random 512 bit secret key suitable for creating and verifying HMAC signatures. This is a - * convenience method that immediately delegates to {@link #generateKey(SignatureAlgorithm)} using {@link + * Generates a new secure-random 512 bit secret key suitable for creating and verifying HMAC-SHA signatures. This + * is a convenience method that immediately delegates to {@link #generateKey(SignatureAlgorithm)} using {@link * SignatureAlgorithm#HS512} as the method argument. * - * @return a new secure-random 512 bit secret key suitable for creating and verifying HMAC signatures. + * @return a new secure-random 512 bit secret key suitable for creating and verifying HMAC-SHA signatures. * @see #generateKey(SignatureAlgorithm) * @see #generateKey(SignatureAlgorithm, SecureRandom) * @since 0.5 @@ -78,26 +79,22 @@ public abstract class MacProvider extends SignatureProvider { * @see #generateKey() * @see #generateKey(SignatureAlgorithm) * @since 0.5 + * @deprecated since 0.10.0 - use {@link #generateKey(SignatureAlgorithm)} instead. */ + @Deprecated public static SecretKey generateKey(SignatureAlgorithm alg, SecureRandom random) { Assert.isTrue(alg.isHmac(), "SignatureAlgorithm argument must represent an HMAC algorithm."); - byte[] bytes; + KeyGenerator gen; - switch (alg) { - case HS256: - bytes = new byte[32]; - break; - case HS384: - bytes = new byte[48]; - break; - default: - bytes = new byte[64]; + try { + gen = KeyGenerator.getInstance(alg.getJcaName()); + } catch (NoSuchAlgorithmException e) { + throw new IllegalStateException("The " + alg.getJcaName() + " algorithm is not available. " + + "This should never happen on JDK 7 or later - please report this to the JJWT developers.", e); } - random.nextBytes(bytes); - - return new SecretKeySpec(bytes, alg.getJcaName()); + return gen.generateKey(); } } diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacSigner.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacSigner.java index 46ad8c43..07379f53 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacSigner.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/crypto/MacSigner.java @@ -16,8 +16,8 @@ package io.jsonwebtoken.impl.crypto; import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SignatureException; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.SignatureException; import javax.crypto.Mac; import javax.crypto.SecretKey; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java index cd5346dc..2d6991f4 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java @@ -16,8 +16,8 @@ package io.jsonwebtoken.impl.crypto; import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SignatureException; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.SignatureException; import java.security.InvalidAlgorithmParameterException; import java.security.Key; @@ -127,9 +127,9 @@ public abstract class RsaProvider extends SignatureProvider { * @see #generateKeyPair(String, int, SecureRandom) * @since 0.10.0 */ - @SuppressWarnings("unused") //used by io.jsonwebtoken.crypto.Keys + @SuppressWarnings("unused") //used by io.jsonwebtoken.security.Keys public static KeyPair generateKeyPair(SignatureAlgorithm alg) { - Assert.isTrue("RSA".equalsIgnoreCase(alg.getFamilyName()), "Only RSA algorithms are supported by this method."); + Assert.isTrue(alg.isRsa(), "Only RSA algorithms are supported by this method."); int keySizeInBits = 4096; switch (alg) { case RS256: diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java index a306d748..90dc8b47 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSignatureValidator.java @@ -16,8 +16,8 @@ package io.jsonwebtoken.impl.crypto; import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SignatureException; import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.security.SignatureException; import java.security.InvalidKeyException; import java.security.Key; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSigner.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSigner.java index 087ac33f..6bc87931 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSigner.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/crypto/RsaSigner.java @@ -16,7 +16,7 @@ package io.jsonwebtoken.impl.crypto; import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.security.SignatureException; import java.security.InvalidKeyException; import java.security.Key; diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java index e10c7a77..7419a478 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java @@ -15,16 +15,16 @@ */ package io.jsonwebtoken.impl.crypto; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.lang.Assert; +import io.jsonwebtoken.lang.RuntimeEnvironment; +import io.jsonwebtoken.security.SignatureException; + import java.security.Key; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.security.Signature; -import io.jsonwebtoken.SignatureAlgorithm; -import io.jsonwebtoken.SignatureException; -import io.jsonwebtoken.lang.Assert; -import io.jsonwebtoken.lang.RuntimeEnvironment; - abstract class SignatureProvider { /** diff --git a/impl/src/main/java/io/jsonwebtoken/impl/crypto/Signer.java b/impl/src/main/java/io/jsonwebtoken/impl/crypto/Signer.java index 096cc135..18600f24 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/crypto/Signer.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/crypto/Signer.java @@ -15,7 +15,7 @@ */ package io.jsonwebtoken.impl.crypto; -import io.jsonwebtoken.SignatureException; +import io.jsonwebtoken.security.SignatureException; public interface Signer { diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy index 7660179f..a4e5bfc0 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtParserTest.groovy @@ -19,6 +19,7 @@ import io.jsonwebtoken.impl.DefaultClock import io.jsonwebtoken.impl.FixedClock import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.lang.Strings +import io.jsonwebtoken.security.SignatureException import org.junit.Test import javax.crypto.spec.SecretKeySpec diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index a671f584..3d4db5b0 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -19,12 +19,11 @@ 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.crypto.MacProvider -import io.jsonwebtoken.impl.crypto.RsaProvider -import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.impl.io.RuntimeClasspathSerializerLocator +import io.jsonwebtoken.io.Encoders import io.jsonwebtoken.lang.Strings +import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.security.WeakKeyException import org.junit.Test import javax.crypto.Mac @@ -348,11 +347,12 @@ class JwtsTest { @Test void testUncompressedJwt() { - byte[] key = MacProvider.generateKey().getEncoded() + SignatureAlgorithm alg = SignatureAlgorithm.HS256 + byte[] key = Keys.secretKeyFor(alg).encoded String id = UUID.randomUUID().toString() - String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(SignatureAlgorithm.HS256, key) + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(alg, key) .claim("state", "hello this is an amazing jwt").compact() def jws = Jwts.parser().setSigningKey(key).parseClaimsJws(compact) @@ -369,11 +369,12 @@ class JwtsTest { @Test void testCompressedJwtWithDeflate() { - byte[] key = MacProvider.generateKey().getEncoded() + SignatureAlgorithm alg = SignatureAlgorithm.HS256 + byte[] key = Keys.secretKeyFor(alg).encoded String id = UUID.randomUUID().toString() - String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(SignatureAlgorithm.HS256, key) + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(alg, key) .claim("state", "hello this is an amazing jwt").compressWith(CompressionCodecs.DEFLATE).compact() def jws = Jwts.parser().setSigningKey(key).parseClaimsJws(compact) @@ -390,11 +391,12 @@ class JwtsTest { @Test void testCompressedJwtWithGZIP() { - byte[] key = MacProvider.generateKey().getEncoded() + SignatureAlgorithm alg = SignatureAlgorithm.HS256 + byte[] key = Keys.secretKeyFor(alg).encoded String id = UUID.randomUUID().toString() - String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(SignatureAlgorithm.HS256, key) + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(alg, key) .claim("state", "hello this is an amazing jwt").compressWith(CompressionCodecs.GZIP).compact() def jws = Jwts.parser().setSigningKey(key).parseClaimsJws(compact) @@ -410,11 +412,13 @@ class JwtsTest { @Test void testCompressedWithCustomResolver() { - byte[] key = MacProvider.generateKey().getEncoded() + + SignatureAlgorithm alg = SignatureAlgorithm.HS256 + byte[] key = Keys.secretKeyFor(alg).encoded String id = UUID.randomUUID().toString() - String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(SignatureAlgorithm.HS256, key) + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(alg, key) .claim("state", "hello this is an amazing jwt").compressWith(new GzipCompressionCodec() { @Override String getAlgorithmName() { @@ -446,11 +450,13 @@ class JwtsTest { @Test(expected = CompressionException.class) void testCompressedJwtWithUnrecognizedHeader() { - byte[] key = MacProvider.generateKey().getEncoded() + + SignatureAlgorithm alg = SignatureAlgorithm.HS256 + byte[] key = Keys.secretKeyFor(alg).encoded String id = UUID.randomUUID().toString() - String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(SignatureAlgorithm.HS256, key) + String compact = Jwts.builder().setId(id).setAudience("an audience").signWith(alg, key) .claim("state", "hello this is an amazing jwt").compressWith(new GzipCompressionCodec() { @Override String getAlgorithmName() { @@ -464,11 +470,12 @@ class JwtsTest { @Test void testCompressStringPayloadWithDeflate() { - byte[] key = MacProvider.generateKey().getEncoded() + SignatureAlgorithm alg = SignatureAlgorithm.HS256 + byte[] key = Keys.secretKeyFor(alg).encoded String payload = "this is my test for a payload" - String compact = Jwts.builder().setPayload(payload).signWith(SignatureAlgorithm.HS256, key) + String compact = Jwts.builder().setPayload(payload).signWith(alg, key) .compressWith(CompressionCodecs.DEFLATE).compact() def jws = Jwts.parser().setSigningKey(key).parsePlaintextJws(compact) @@ -482,62 +489,62 @@ class JwtsTest { @Test void testHS256() { - testHmac(SignatureAlgorithm.HS256); + testHmac(SignatureAlgorithm.HS256) } @Test void testHS384() { - testHmac(SignatureAlgorithm.HS384); + testHmac(SignatureAlgorithm.HS384) } @Test void testHS512() { - testHmac(SignatureAlgorithm.HS512); + testHmac(SignatureAlgorithm.HS512) } @Test void testRS256() { - testRsa(SignatureAlgorithm.RS256); + testRsa(SignatureAlgorithm.RS256) } @Test void testRS384() { - testRsa(SignatureAlgorithm.RS384); + testRsa(SignatureAlgorithm.RS384) } @Test void testRS512() { - testRsa(SignatureAlgorithm.RS512); + testRsa(SignatureAlgorithm.RS512) } @Test void testPS256() { - testRsa(SignatureAlgorithm.PS256); + testRsa(SignatureAlgorithm.PS256) } @Test void testPS384() { - testRsa(SignatureAlgorithm.PS384); + testRsa(SignatureAlgorithm.PS384) } @Test void testPS512() { - testRsa(SignatureAlgorithm.PS512, 2048, false); + testRsa(SignatureAlgorithm.PS512) } @Test void testRSA256WithPrivateKeyValidation() { - testRsa(SignatureAlgorithm.RS256, 1024, true); + testRsa(SignatureAlgorithm.RS256, true) } @Test void testRSA384WithPrivateKeyValidation() { - testRsa(SignatureAlgorithm.RS384, 1024, true); + testRsa(SignatureAlgorithm.RS384, true) } @Test void testRSA512WithPrivateKeyValidation() { - testRsa(SignatureAlgorithm.RS512, 1024, true); + testRsa(SignatureAlgorithm.RS512, true) } @Test @@ -565,17 +572,34 @@ class JwtsTest { } } + @Test + void testParseClaimsJwsWithWeakHmacKey() { + + SignatureAlgorithm alg = SignatureAlgorithm.HS384 + def key = Keys.secretKeyFor(alg) + def weakKey = Keys.secretKeyFor(SignatureAlgorithm.HS256) + + String jws = Jwts.builder().setSubject("Foo").signWith(alg, key).compact() + + try { + Jwts.parser().setSigningKey(weakKey).parseClaimsJws(jws) + fail('parseClaimsJws must fail for weak keys') + } catch (WeakKeyException expected) { + } + } + //Asserts correct/expected behavior discussed in https://github.com/jwtk/jjwt/issues/20 @Test void testParseClaimsJwsWithUnsignedJwt() { //create random signing key for testing: - byte[] key = MacProvider.generateKey().getEncoded() + SignatureAlgorithm alg = SignatureAlgorithm.HS256 + byte[] key = Keys.secretKeyFor(alg).encoded - String notSigned = Jwts.builder().setSubject("Foo").compact(); + String notSigned = Jwts.builder().setSubject("Foo").compact() try { - Jwts.parser().setSigningKey(key).parseClaimsJws(notSigned); + Jwts.parser().setSigningKey(key).parseClaimsJws(notSigned) fail('parseClaimsJws must fail for unsigned JWTs') } catch (UnsupportedJwtException expected) { assertEquals expected.message, 'Unsigned Claims JWTs are not supported.' @@ -587,27 +611,28 @@ class JwtsTest { void testForgedTokenWithSwappedHeaderUsingNoneAlgorithm() { //create random signing key for testing: - byte[] key = MacProvider.generateKey().getEncoded() + SignatureAlgorithm alg = SignatureAlgorithm.HS256 + byte[] key = Keys.secretKeyFor(alg).encoded //this is a 'real', valid JWT: - String compact = Jwts.builder().setSubject("Joe").signWith(SignatureAlgorithm.HS256, key).compact(); + String compact = Jwts.builder().setSubject("Joe").signWith(alg, key).compact() //Now strip off the signature so we can add it back in later on a forged token: - int i = compact.lastIndexOf('.'); - String signature = compact.substring(i + 1); + int i = compact.lastIndexOf('.') + String signature = compact.substring(i + 1) //now let's create a fake header and payload with whatever we want (without signing): - String forged = Jwts.builder().setSubject("Not Joe").compact(); + String forged = Jwts.builder().setSubject("Not Joe").compact() //assert that our forged header has a 'NONE' algorithm: assertEquals Jwts.parser().parseClaimsJwt(forged).getHeader().get('alg'), 'none' //now let's forge it by appending the signature the server expects: - forged += signature; + forged += signature //now assert that, when the server tries to parse the forged token, parsing fails: try { - Jwts.parser().setSigningKey(key).parse(forged); + Jwts.parser().setSigningKey(key).parse(forged) fail("Parsing must fail for a forged token.") } catch (MalformedJwtException expected) { assertEquals expected.message, 'JWT string has a digest/signature, but the header does not reference a valid signature algorithm.' @@ -619,9 +644,9 @@ class JwtsTest { void testParseForgedRsaPublicKeyAsHmacTokenVerifiedWithTheRsaPrivateKey() { //Create a legitimate RSA public and private key pair: - KeyPair kp = RsaProvider.generateKeyPair(1024) - PublicKey publicKey = kp.getPublic(); - PrivateKey privateKey = kp.getPrivate(); + KeyPair kp = Keys.keyPairFor(SignatureAlgorithm.RS256) + PublicKey publicKey = kp.getPublic() + PrivateKey privateKey = kp.getPrivate() String header = base64Url(toJson(['alg': 'HS256'])) String body = base64Url(toJson('foo')) @@ -651,7 +676,7 @@ class JwtsTest { void testParseForgedRsaPublicKeyAsHmacTokenVerifiedWithTheRsaPublicKey() { //Create a legitimate RSA public and private key pair: - KeyPair kp = RsaProvider.generateKeyPair(1024) + KeyPair kp = Keys.keyPairFor(SignatureAlgorithm.RS256) PublicKey publicKey = kp.getPublic(); //PrivateKey privateKey = kp.getPrivate(); @@ -683,7 +708,7 @@ class JwtsTest { void testParseForgedEllipticCurvePublicKeyAsHmacToken() { //Create a legitimate RSA public and private key pair: - KeyPair kp = EllipticCurveProvider.generateKeyPair() + KeyPair kp = Keys.keyPairFor(SignatureAlgorithm.ES256) PublicKey publicKey = kp.getPublic(); //PrivateKey privateKey = kp.getPrivate(); @@ -703,29 +728,29 @@ class JwtsTest { // Assert that the parser does not recognized the forged token: try { - Jwts.parser().setSigningKey(publicKey).parse(forged); + Jwts.parser().setSigningKey(publicKey).parse(forged) fail("Forged token must not be successfully parsed.") } catch (UnsupportedJwtException expected) { assertTrue expected.getMessage().startsWith('The parsed JWT indicates it was signed with the') } } - static void testRsa(SignatureAlgorithm alg, int keySize = 1024, boolean verifyWithPrivateKey = false) { + static void testRsa(SignatureAlgorithm alg, boolean verifyWithPrivateKey = false) { - KeyPair kp = RsaProvider.generateKeyPair(keySize) - PublicKey publicKey = kp.getPublic(); - PrivateKey privateKey = kp.getPrivate(); + KeyPair kp = Keys.keyPairFor(alg) + PublicKey publicKey = kp.getPublic() + PrivateKey privateKey = kp.getPrivate() def claims = [iss: 'joe', exp: later(), 'http://example.com/is_root': true] - String jwt = Jwts.builder().setClaims(claims).signWith(alg, privateKey).compact(); + String jwt = Jwts.builder().setClaims(claims).signWith(alg, privateKey).compact() - def key = publicKey; + def key = publicKey if (verifyWithPrivateKey) { - key = privateKey; + key = privateKey } - def token = Jwts.parser().setSigningKey(key).parse(jwt); + def token = Jwts.parser().setSigningKey(key).parse(jwt) assert [alg: alg.name()] == token.header //noinspection GrEqualsBetweenInconvertibleTypes @@ -733,12 +758,13 @@ class JwtsTest { } static void testHmac(SignatureAlgorithm alg) { + //create random signing key for testing: - byte[] key = MacProvider.generateKey().encoded + byte[] key = Keys.secretKeyFor(alg).encoded def claims = [iss: 'joe', exp: later(), 'http://example.com/is_root': true] - String jwt = Jwts.builder().setClaims(claims).signWith(alg, key).compact(); + String jwt = Jwts.builder().setClaims(claims).signWith(alg, key).compact() def token = Jwts.parser().setSigningKey(key).parse(jwt) @@ -749,20 +775,20 @@ class JwtsTest { static void testEC(SignatureAlgorithm alg, boolean verifyWithPrivateKey = false) { - KeyPair pair = EllipticCurveProvider.generateKeyPair(alg) + KeyPair pair = Keys.keyPairFor(alg) PublicKey publicKey = pair.getPublic() PrivateKey privateKey = pair.getPrivate() def claims = [iss: 'joe', exp: later(), 'http://example.com/is_root': true] - String jwt = Jwts.builder().setClaims(claims).signWith(alg, privateKey).compact(); + String jwt = Jwts.builder().setClaims(claims).signWith(alg, privateKey).compact() - def key = publicKey; + def key = publicKey if (verifyWithPrivateKey) { - key = privateKey; + key = privateKey } - def token = Jwts.parser().setSigningKey(key).parse(jwt); + def token = Jwts.parser().setSigningKey(key).parse(jwt) assert token.header == [alg: alg.name()] //noinspection GrEqualsBetweenInconvertibleTypes diff --git a/impl/src/test/groovy/io/jsonwebtoken/RsaSigningKeyResolverAdapterTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/RsaSigningKeyResolverAdapterTest.groovy index 79fcf548..03f3e9bf 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/RsaSigningKeyResolverAdapterTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/RsaSigningKeyResolverAdapterTest.groovy @@ -15,7 +15,7 @@ */ package io.jsonwebtoken -import io.jsonwebtoken.impl.crypto.RsaProvider +import io.jsonwebtoken.security.Keys import org.junit.Test import static org.junit.Assert.assertEquals @@ -26,9 +26,11 @@ class RsaSigningKeyResolverAdapterTest { @Test void testResolveClaimsSigningKeyWithRsaKey() { - def pair = RsaProvider.generateKeyPair(1024) //real apps should use 4096 or better. We're only reducing the size here so the tests are fast + def alg = SignatureAlgorithm.RS256 - def compact = Jwts.builder().claim('foo', 'bar').signWith(SignatureAlgorithm.RS256, pair.private).compact() + def pair = Keys.keyPairFor(alg) + + def compact = Jwts.builder().claim('foo', 'bar').signWith(alg, pair.private).compact() Jws jws = Jwts.parser().setSigningKey(pair.public).parseClaimsJws(compact) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy index 33f0d97a..c2ff205c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy @@ -18,9 +18,11 @@ package io.jsonwebtoken.impl import io.jsonwebtoken.JwsHeader import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.impl.crypto.MacProvider +import io.jsonwebtoken.security.Keys import org.junit.Test -import static org.junit.Assert.* + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertSame class DefaultJwsTest { @@ -38,8 +40,9 @@ class DefaultJwsTest { @Test void testToString() { //create random signing key for testing: - byte[] key = MacProvider.generateKey().encoded - String compact = Jwts.builder().claim('foo', 'bar').signWith(SignatureAlgorithm.HS256, key).compact(); + SignatureAlgorithm alg = SignatureAlgorithm.HS256 + byte[] key = Keys.secretKeyFor(alg).encoded + String compact = Jwts.builder().claim('foo', 'bar').signWith(alg, key).compact(); int i = compact.lastIndexOf('.') String signature = compact.substring(i + 1) def jws = Jwts.parser().setSigningKey(key).parseClaimsJws(compact) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy index dfeea5cd..9f8d1491 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy @@ -16,14 +16,14 @@ package io.jsonwebtoken.impl import com.fasterxml.jackson.databind.ObjectMapper +import io.jsonwebtoken.CompressionCodecs import io.jsonwebtoken.Jwts import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.io.Encoder import io.jsonwebtoken.io.EncodingException -import io.jsonwebtoken.CompressionCodecs -import io.jsonwebtoken.impl.crypto.MacProvider import io.jsonwebtoken.io.SerializationException import io.jsonwebtoken.io.Serializer +import io.jsonwebtoken.security.Keys import org.junit.Test import static org.junit.Assert.* @@ -174,8 +174,9 @@ class DefaultJwtBuilderTest { def b = new DefaultJwtBuilder() b.setHeader(Jwts.jwsHeader().setKeyId('a')) b.setPayload('foo') - def key = MacProvider.generateKey() - b.signWith(SignatureAlgorithm.HS256, key) + def alg = SignatureAlgorithm.HS256 + def key = Keys.secretKeyFor(alg) + b.signWith(alg, key) b.compact() } @@ -338,14 +339,14 @@ class DefaultJwtBuilderTest { def serializer = new Serializer() { @Override byte[] serialize(Object o) throws SerializationException { - return objectMapper.writeValueAsBytes(o); + return objectMapper.writeValueAsBytes(o) } } def b = new DefaultJwtBuilder().serializeToJsonWith(serializer) assertSame serializer, b.serializer - def key = MacProvider.generateKey(SignatureAlgorithm.HS256) + def key = Keys.secretKeyFor(SignatureAlgorithm.HS256) String jws = b.signWith(SignatureAlgorithm.HS256, key) .claim('foo', 'bar') diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy index 50e15848..e0390236 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtParserTest.groovy @@ -4,13 +4,9 @@ import com.fasterxml.jackson.databind.ObjectMapper import io.jsonwebtoken.Jwts import io.jsonwebtoken.MalformedJwtException import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.io.Decoder -import io.jsonwebtoken.io.DecodingException -import io.jsonwebtoken.impl.crypto.MacProvider -import io.jsonwebtoken.io.DeserializationException -import io.jsonwebtoken.io.Deserializer -import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.io.* import io.jsonwebtoken.lang.Strings +import io.jsonwebtoken.security.Keys import org.junit.Test import javax.crypto.Mac @@ -60,7 +56,7 @@ class DefaultJwtParserTest { def p = new DefaultJwtParser().deserializeJsonWith(deserializer) assertSame deserializer, p.deserializer - def key = MacProvider.generateKey(SignatureAlgorithm.HS256) + def key = Keys.secretKeyFor(SignatureAlgorithm.HS256) String jws = Jwts.builder().claim('foo', 'bar').signWith(SignatureAlgorithm.HS256, key).compact() @@ -74,7 +70,7 @@ class DefaultJwtParserTest { String body = Encoders.BASE64URL.encode('{"hello":"world"}'.getBytes(Strings.UTF_8)) String compact = header + '.' + body + '.' - SecretKey key = MacProvider.generateKey(SignatureAlgorithm.HS256) + SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256) Mac mac = Mac.getInstance('HmacSHA256') mac.init(key) byte[] signatureBytes = mac.doFinal(compact.getBytes(Strings.UTF_8)) @@ -92,7 +88,7 @@ class DefaultJwtParserTest { String body = Encoders.BASE64URL.encode('{"hello":"world"}'.getBytes(Strings.UTF_8)) String compact = header + '.' + body + '.' - SecretKey key = MacProvider.generateKey(SignatureAlgorithm.HS256) + SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256) Mac mac = Mac.getInstance('HmacSHA256') mac.init(key) byte[] signatureBytes = mac.doFinal(compact.getBytes(Strings.UTF_8)) @@ -110,7 +106,7 @@ class DefaultJwtParserTest { String body = Encoders.BASE64URL.encode('{"hello":"world"}'.getBytes(Strings.UTF_8)) String compact = header + '.' + body + '.' - SecretKey key = MacProvider.generateKey(SignatureAlgorithm.HS256) + SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256) Mac mac = Mac.getInstance('HmacSHA256') mac.init(key) byte[] signatureBytes = mac.doFinal(compact.getBytes(Strings.UTF_8)) diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidatorTest.groovy index 0d65f7fb..b510ae80 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidatorTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignatureValidatorTest.groovy @@ -2,6 +2,7 @@ package io.jsonwebtoken.impl.crypto import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.security.Keys import org.junit.Test import static org.junit.Assert.assertNotNull @@ -15,7 +16,7 @@ class DefaultJwtSignatureValidatorTest { void testDeprecatedTwoArgCtor() { def alg = SignatureAlgorithm.HS256 - def key = MacProvider.generateKey(alg) + def key = Keys.secretKeyFor(alg) def validator = new DefaultJwtSignatureValidator(alg, key) assertNotNull validator.signatureValidator @@ -28,7 +29,7 @@ class DefaultJwtSignatureValidatorTest { void testDeprecatedThreeArgCtor() { def alg = SignatureAlgorithm.HS256 - def key = MacProvider.generateKey(alg) + def key = Keys.secretKeyFor(alg) def validator = new DefaultJwtSignatureValidator(DefaultSignatureValidatorFactory.INSTANCE, alg, key) assertNotNull validator.signatureValidator diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignerTest.groovy index 2ff81f25..5888899a 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignerTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultJwtSignerTest.groovy @@ -2,6 +2,7 @@ package io.jsonwebtoken.impl.crypto import io.jsonwebtoken.SignatureAlgorithm import io.jsonwebtoken.io.Encoders +import io.jsonwebtoken.security.Keys import org.junit.Test import static org.junit.Assert.assertNotNull @@ -16,7 +17,7 @@ class DefaultJwtSignerTest { void testDeprecatedTwoArgCtor() { def alg = SignatureAlgorithm.HS256 - def key = MacProvider.generateKey(alg) + def key = Keys.secretKeyFor(alg) def signer = new DefaultJwtSigner(alg, key) assertNotNull signer.signer @@ -30,7 +31,7 @@ class DefaultJwtSignerTest { void testDeprecatedThreeArgCtor() { def alg = SignatureAlgorithm.HS256 - def key = MacProvider.generateKey(alg) + def key = Keys.secretKeyFor(alg) def signer = new DefaultJwtSigner(DefaultSignerFactory.INSTANCE, alg, key) assertNotNull signer.signer diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactoryTest.groovy index 04ec851d..a09c198a 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactoryTest.groovy @@ -16,15 +16,19 @@ package io.jsonwebtoken.impl.crypto import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.security.Keys import org.junit.Test -import static org.junit.Assert.* + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail class DefaultSignatureValidatorFactoryTest { @Test void testNoneAlgorithm() { try { - new DefaultSignatureValidatorFactory().createSignatureValidator(SignatureAlgorithm.NONE, MacProvider.generateKey()) + new DefaultSignatureValidatorFactory().createSignatureValidator( + SignatureAlgorithm.NONE, Keys.secretKeyFor(SignatureAlgorithm.HS256)) fail() } catch (IllegalArgumentException iae) { assertEquals iae.message, "The 'NONE' algorithm cannot be used for signing." diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignerFactoryTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignerFactoryTest.groovy index d3139640..5d33fd74 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignerFactoryTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignerFactoryTest.groovy @@ -16,8 +16,11 @@ package io.jsonwebtoken.impl.crypto import io.jsonwebtoken.SignatureAlgorithm +import io.jsonwebtoken.security.Keys import org.junit.Test -import static org.junit.Assert.* + +import static org.junit.Assert.assertEquals +import static org.junit.Assert.fail class DefaultSignerFactoryTest { @@ -27,7 +30,7 @@ class DefaultSignerFactoryTest { def factory = new DefaultSignerFactory(); try { - factory.createSigner(SignatureAlgorithm.NONE, MacProvider.generateKey()); + factory.createSigner(SignatureAlgorithm.NONE, Keys.secretKeyFor(SignatureAlgorithm.HS256)) fail(); } catch (IllegalArgumentException iae) { assertEquals iae.message, "The 'NONE' algorithm cannot be used for signing." 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 f175f21c..cdb37951 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy @@ -17,8 +17,8 @@ package io.jsonwebtoken.impl.crypto import io.jsonwebtoken.JwtException import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.SignatureException import io.jsonwebtoken.io.Decoders +import io.jsonwebtoken.security.SignatureException import org.bouncycastle.jce.provider.BouncyCastleProvider import org.junit.Test @@ -143,6 +143,14 @@ class EllipticCurveSignatureValidatorTest { EllipticCurveProvider.transcodeSignatureToDER(signature) } + @Test + void testPaddedSignatureToDER() { + def signature = new byte[32] + SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(signature) + signature[0] = 0 as byte + EllipticCurveProvider.transcodeSignatureToDER(signature) //no exception + } + @Test void edgeCaseSignatureToConcatLengthTest() { try { 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 d4e4b395..97baac2d 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy @@ -17,7 +17,8 @@ package io.jsonwebtoken.impl.crypto import io.jsonwebtoken.JwtException import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.SignatureException +import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.security.SignatureException import org.junit.Test import java.security.InvalidKeyException @@ -31,37 +32,40 @@ class EllipticCurveSignerTest { @Test void testConstructorWithoutECAlg() { + SignatureAlgorithm alg = SignatureAlgorithm.HS256 try { - new EllipticCurveSigner(SignatureAlgorithm.HS256, MacProvider.generateKey()); - fail('EllipticCurveSigner should reject non ECPrivateKeys'); + new EllipticCurveSigner(alg, Keys.secretKeyFor(alg)) + fail('EllipticCurveSigner should reject non ECPrivateKeys') } catch (IllegalArgumentException expected) { - assertEquals expected.message, 'SignatureAlgorithm must be an Elliptic Curve algorithm.'; + assertEquals expected.message, 'SignatureAlgorithm must be an Elliptic Curve algorithm.' } } @Test void testConstructorWithoutECPrivateKey() { - def key = MacProvider.generateKey() + def key = Keys.secretKeyFor(SignatureAlgorithm.HS256) try { - new EllipticCurveSigner(SignatureAlgorithm.ES256, key); + 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."; + "type " + key.getClass().getName() + " is not an EC PrivateKey." } } @Test void testDoSignWithInvalidKeyException() { - KeyPair kp = EllipticCurveProvider.generateKeyPair() - PublicKey publicKey = kp.getPublic(); - PrivateKey privateKey = kp.getPrivate(); + SignatureAlgorithm alg = SignatureAlgorithm.ES256 + + KeyPair kp = Keys.keyPairFor(alg) + PublicKey publicKey = kp.getPublic() + PrivateKey privateKey = kp.getPrivate() String msg = 'foo' final InvalidKeyException ex = new InvalidKeyException(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 diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacProviderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacProviderTest.groovy index ea3a87e5..086028cd 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacProviderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacProviderTest.groovy @@ -17,31 +17,39 @@ package io.jsonwebtoken.impl.crypto import io.jsonwebtoken.SignatureAlgorithm import org.junit.Test -import static org.junit.Assert.* + +import javax.crypto.SecretKey + +import static org.junit.Assert.assertEquals class MacProviderTest { + private void testHmac(SignatureAlgorithm alg) { + testHmac(alg, MacProvider.generateKey(alg)) + } + + private void testHmac(SignatureAlgorithm alg, SecretKey key) { + assertEquals alg.jcaName, key.algorithm + assertEquals alg.digestLength / 8 as int, key.encoded.length + } + @Test void testDefault() { - byte[] bytes = MacProvider.generateKey().encoded - assertEquals 64, bytes.length + testHmac(SignatureAlgorithm.HS512, MacProvider.generateKey()) } @Test void testHS256() { - byte[] bytes = MacProvider.generateKey(SignatureAlgorithm.HS256).encoded - assertEquals 32, bytes.length + testHmac(SignatureAlgorithm.HS256) } @Test void testHS384() { - byte[] bytes = MacProvider.generateKey(SignatureAlgorithm.HS384).encoded - assertEquals 48, bytes.length + testHmac(SignatureAlgorithm.HS384) } @Test void testHS512() { - byte[] bytes = MacProvider.generateKey(SignatureAlgorithm.HS512).encoded - assertEquals 64, bytes.length + testHmac(SignatureAlgorithm.HS512) } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacSignerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacSignerTest.groovy index f796beae..ab6ebca7 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacSignerTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/MacSignerTest.groovy @@ -16,18 +16,47 @@ package io.jsonwebtoken.impl.crypto import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.SignatureException +import io.jsonwebtoken.security.SignatureException import org.junit.Test -import static org.junit.Assert.* import javax.crypto.Mac import java.security.InvalidKeyException +import java.security.Key import java.security.NoSuchAlgorithmException +import static org.junit.Assert.* + class MacSignerTest { private static final Random rng = new Random(); //doesn't need to be secure - we're just testing + @Test + void testCtorArgNotASecretKey() { + + def key = new Key() { + @Override + String getAlgorithm() { + return null + } + + @Override + String getFormat() { + return null + } + + @Override + byte[] getEncoded() { + return new byte[0] + } + } + + try { + new MacSigner(SignatureAlgorithm.HS256, key) + fail() + } catch (IllegalArgumentException expected) { + } + } + @Test void testNoSuchAlgorithmException() { byte[] key = new byte[32]; diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/PowermockMacProviderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/PowermockMacProviderTest.groovy new file mode 100644 index 00000000..824ad0e6 --- /dev/null +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/PowermockMacProviderTest.groovy @@ -0,0 +1,50 @@ +package io.jsonwebtoken.impl.crypto + +import io.jsonwebtoken.SignatureAlgorithm +import org.junit.Test +import org.junit.runner.RunWith +import org.powermock.core.classloader.annotations.PrepareForTest +import org.powermock.modules.junit4.PowerMockRunner + +import javax.crypto.KeyGenerator +import java.security.NoSuchAlgorithmException + +import static org.easymock.EasyMock.eq +import static org.easymock.EasyMock.expect +import static org.junit.Assert.* +import static org.powermock.api.easymock.PowerMock.* + +/** + * This needs to be a separate class beyond MacProviderTest because it mocks the KeyGenerator class which messes up + * the other implementation tests in MacProviderTest. + */ +@RunWith(PowerMockRunner.class) +@PrepareForTest([KeyGenerator]) +class PowermockMacProviderTest { + + @Test + void testNoSuchAlgorithm() { + + mockStatic(KeyGenerator) + + def alg = SignatureAlgorithm.HS256 + def ex = new NoSuchAlgorithmException('foo') + + expect(KeyGenerator.getInstance(eq(alg.jcaName))).andThrow(ex) + + replay KeyGenerator + + try { + MacProvider.generateKey(alg) + fail() + } catch (IllegalStateException e) { + assertEquals 'The HmacSHA256 algorithm is not available. This should never happen on JDK 7 or later - ' + + 'please report this to the JJWT developers.', e.message + assertSame ex, e.getCause() + } + + verify KeyGenerator + + reset KeyGenerator + } +} diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaProviderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaProviderTest.groovy index 7887fc74..5d27a295 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaProviderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaProviderTest.groovy @@ -16,7 +16,8 @@ package io.jsonwebtoken.impl.crypto import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.SignatureException +import io.jsonwebtoken.security.SignatureException +import org.junit.Test import java.security.InvalidAlgorithmParameterException import java.security.KeyPair @@ -25,7 +26,6 @@ import java.security.interfaces.RSAPrivateKey import java.security.interfaces.RSAPublicKey import java.security.spec.PSSParameterSpec -import org.junit.Test import static org.junit.Assert.* class RsaProviderTest { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignatureValidatorTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignatureValidatorTest.groovy index 5cc3fef9..99bf2ea4 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignatureValidatorTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignatureValidatorTest.groovy @@ -16,7 +16,8 @@ package io.jsonwebtoken.impl.crypto import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.SignatureException +import io.jsonwebtoken.security.Keys +import io.jsonwebtoken.security.SignatureException import org.junit.Test import java.security.* @@ -30,7 +31,7 @@ class RsaSignatureValidatorTest { @Test void testConstructorWithNonRsaKey() { try { - new RsaSignatureValidator(SignatureAlgorithm.RS256, MacProvider.generateKey()); + new RsaSignatureValidator(SignatureAlgorithm.RS256, Keys.secretKeyFor(SignatureAlgorithm.HS256)); fail() } catch (IllegalArgumentException iae) { assertEquals "RSA Signature validation requires either a RSAPublicKey or RSAPrivateKey instance.", iae.message @@ -40,17 +41,16 @@ class RsaSignatureValidatorTest { @Test void testDoVerifyWithInvalidKeyException() { - KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("RSA"); - keyGenerator.initialize(1024); + SignatureAlgorithm alg = SignatureAlgorithm.RS256 - KeyPair kp = keyGenerator.genKeyPair(); - PublicKey publicKey = kp.getPublic(); - PrivateKey privateKey = kp.getPrivate(); + KeyPair kp = Keys.keyPairFor(alg) + PublicKey publicKey = kp.getPublic() + PrivateKey privateKey = kp.getPrivate() String msg = 'foo' final InvalidKeyException ex = new InvalidKeyException(msg) - RsaSignatureValidator v = new RsaSignatureValidator(SignatureAlgorithm.RS256, publicKey) { + RsaSignatureValidator v = new RsaSignatureValidator(alg, publicKey) { @Override protected boolean doVerify(Signature sig, PublicKey pk, byte[] data, byte[] signature) throws InvalidKeyException, java.security.SignatureException { throw ex; diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy index 4b804442..d0470636 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy @@ -16,17 +16,12 @@ package io.jsonwebtoken.impl.crypto import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.SignatureException +import io.jsonwebtoken.security.SignatureException +import org.junit.Test import javax.crypto.spec.SecretKeySpec -import java.security.InvalidKeyException -import java.security.KeyPair -import java.security.KeyPairGenerator -import java.security.MessageDigest -import java.security.PrivateKey -import java.security.PublicKey +import java.security.* -import org.junit.Test import static org.junit.Assert.* class RsaSignerTest { diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/SignatureProviderTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/SignatureProviderTest.groovy index e595231c..91158c0d 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/SignatureProviderTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/crypto/SignatureProviderTest.groovy @@ -16,12 +16,12 @@ package io.jsonwebtoken.impl.crypto import io.jsonwebtoken.SignatureAlgorithm -import io.jsonwebtoken.SignatureException +import io.jsonwebtoken.security.SignatureException +import org.junit.Test import java.security.NoSuchAlgorithmException import java.security.Signature -import org.junit.Test import static org.junit.Assert.* class SignatureProviderTest { diff --git a/impl/src/test/groovy/io/jsonwebtoken/crypto/KeysImplTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/security/KeysImplTest.groovy similarity index 75% rename from impl/src/test/groovy/io/jsonwebtoken/crypto/KeysImplTest.groovy rename to impl/src/test/groovy/io/jsonwebtoken/security/KeysImplTest.groovy index 151d4c05..1ff2420c 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/crypto/KeysImplTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/security/KeysImplTest.groovy @@ -1,4 +1,4 @@ -package io.jsonwebtoken.crypto +package io.jsonwebtoken.security import io.jsonwebtoken.SignatureAlgorithm import org.junit.Test @@ -10,6 +10,7 @@ import java.security.PublicKey import java.security.interfaces.ECPrivateKey import java.security.interfaces.ECPublicKey import java.security.interfaces.RSAPrivateKey +import java.security.interfaces.RSAPublicKey import static org.junit.Assert.* @@ -27,11 +28,9 @@ class KeysImplTest { String name = alg.name() - int bitLength = name.equalsIgnoreCase("NONE") ? 0 : name.substring(2).toInteger() - - if (name.startsWith('H')) { + if (alg.isHmac()) { SecretKey key = Keys.secretKeyFor(alg) - assertEquals bitLength, key.getEncoded().length * 8 //convert byte count to bit count + assertEquals alg.minKeyLength, key.getEncoded().length * 8 //convert byte count to bit count assertEquals alg.jcaName, key.algorithm } else { try { @@ -51,40 +50,40 @@ class KeysImplTest { for (SignatureAlgorithm alg : SignatureAlgorithm.values()) { String name = alg.name() - int bitLength = name.equalsIgnoreCase("NONE") ? 0 : name.substring(2).toInteger() - if (name.startsWith('R') || name.startsWith('P')) { + if (alg.isRsa()) { KeyPair pair = Keys.keyPairFor(alg) assertNotNull pair + PublicKey pub = pair.getPublic() + assert pub instanceof RSAPublicKey + assertEquals alg.familyName, pub.algorithm + assertEquals alg.digestLength * 8, pub.modulus.bitLength() + PrivateKey priv = pair.getPrivate() assert priv instanceof RSAPrivateKey - assertEquals alg.familyName, pub.algorithm assertEquals alg.familyName, priv.algorithm - assertEquals bitLength * 8, priv.modulus.bitLength() + assertEquals alg.digestLength * 8, priv.modulus.bitLength() - } else if (name.startsWith('E')) { + } else if (alg.isEllipticCurve()) { KeyPair pair = Keys.keyPairFor(alg); assertNotNull pair - if (alg == SignatureAlgorithm.ES512) { - bitLength = 521 - } - - String asn1oid = "secp${bitLength}r1" + String asn1oid = "secp${alg.minKeyLength}r1" PublicKey pub = pair.getPublic() assert pub instanceof ECPublicKey assertEquals "ECDSA", pub.algorithm assertEquals asn1oid, pub.params.name - assertEquals bitLength, pub.params.order.bitLength() + assertEquals alg.minKeyLength, pub.params.order.bitLength() PrivateKey priv = pair.getPrivate() assert priv instanceof ECPrivateKey assertEquals "ECDSA", priv.algorithm assertEquals asn1oid, priv.params.name + assertEquals alg.minKeyLength, priv.params.order.bitLength() } else { try {