diff --git a/src/main/java/io/jsonwebtoken/Jwts.java b/src/main/java/io/jsonwebtoken/Jwts.java
index 9d45e2cf..ebac88e7 100644
--- a/src/main/java/io/jsonwebtoken/Jwts.java
+++ b/src/main/java/io/jsonwebtoken/Jwts.java
@@ -29,7 +29,9 @@ import java.util.Map;
*
* @since 0.1
*/
-public class Jwts {
+public final class Jwts {
+
+ private Jwts(){}
/**
* Creates a new {@link Header} instance suitable for plaintext (not digitally signed) JWTs. As this
diff --git a/src/main/java/io/jsonwebtoken/SignatureAlgorithm.java b/src/main/java/io/jsonwebtoken/SignatureAlgorithm.java
index d28abe55..7f5388f7 100644
--- a/src/main/java/io/jsonwebtoken/SignatureAlgorithm.java
+++ b/src/main/java/io/jsonwebtoken/SignatureAlgorithm.java
@@ -26,67 +26,67 @@ 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", null, false),
+ NONE("none", "No digital signature or MAC performed", "None", null, false),
/** JWA algorithm name for {@code HMAC using SHA-256} */
- HS256("HS256", "HMAC using SHA-256", "HmacSHA256", true),
+ HS256("HS256", "HMAC using SHA-256", "HMAC", "HmacSHA256", true),
/** JWA algorithm name for {@code HMAC using SHA-384} */
- HS384("HS384", "HMAC using SHA-384", "HmacSHA384", true),
+ HS384("HS384", "HMAC using SHA-384", "HMAC", "HmacSHA384", true),
/** JWA algorithm name for {@code HMAC using SHA-512} */
- HS512("HS512", "HMAC using SHA-512", "HmacSHA512", true),
+ HS512("HS512", "HMAC using SHA-512", "HMAC", "HmacSHA512", true),
/** JWA algorithm name for {@code RSASSA-PKCS-v1_5 using SHA-256} */
- RS256("RS256", "RSASSA-PKCS-v1_5 using SHA-256", "SHA256withRSA", true),
+ RS256("RS256", "RSASSA-PKCS-v1_5 using SHA-256", "RSA", "SHA256withRSA", true),
/** JWA algorithm name for {@code RSASSA-PKCS-v1_5 using SHA-384} */
- RS384("RS384", "RSASSA-PKCS-v1_5 using SHA-384", "SHA384withRSA", true),
+ RS384("RS384", "RSASSA-PKCS-v1_5 using SHA-384", "RSA", "SHA384withRSA", true),
/** JWA algorithm name for {@code RSASSA-PKCS-v1_5 using SHA-512} */
- RS512("RS512", "RSASSA-PKCS-v1_5 using SHA-512", "SHA512withRSA", true),
+ RS512("RS512", "RSASSA-PKCS-v1_5 using SHA-512", "RSA", "SHA512withRSA", true),
/**
* 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", "secp256r1", false),
+ ES256("ES256", "ECDSA using P-256 and SHA-256", "Elliptic Curve", "SHA256withECDSA", false),
/**
* 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", "secp384r1", false),
+ ES384("ES384", "ECDSA using P-384 and SHA-384", "Elliptic Curve", "SHA384withECDSA", false),
/**
* JWA algorithm name for {@code ECDSA using P-512 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", "secp521r1", false),
+ ES512("ES512", "ECDSA using P-512 and SHA-512", "Elliptic Curve", "SHA512withECDSA", false),
/**
* 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", "SHA256withRSAandMGF1", false),
+ PS256("PS256", "RSASSA-PSS using SHA-256 and MGF1 with SHA-256", "RSA", "SHA256withRSAandMGF1", false),
/**
* 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", "SHA384withRSAandMGF1", false),
+ PS384("PS384", "RSASSA-PSS using SHA-384 and MGF1 with SHA-384", "RSA", "SHA384withRSAandMGF1", false),
/**
* 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", "SHA512withRSAandMGF1", false);
+ PS512("PS512", "RSASSA-PSS using SHA-512 and MGF1 with SHA-512", "RSA", "SHA512withRSAandMGF1", false);
static {
RuntimeEnvironment.enableBouncyCastleIfPossible();
@@ -94,12 +94,14 @@ public enum SignatureAlgorithm {
private final String value;
private final String description;
+ private final String familyName;
private final String jcaName;
private final boolean jdkStandard;
- private SignatureAlgorithm(String value, String description, String jcaName, boolean jdkStandard) {
+ SignatureAlgorithm(String value, String description, String familyName, String jcaName, boolean jdkStandard) {
this.value = value;
this.description = description;
+ this.familyName = familyName;
this.jcaName = jcaName;
this.jdkStandard = jdkStandard;
}
@@ -122,6 +124,78 @@ public enum SignatureAlgorithm {
return description;
}
+
+ /**
+ * Returns the cryptographic family name of the signature algorithm. The value returned is according to the
+ * following table:
+ *
+ *
+ *
+ *
+ * SignatureAlgorithm |
+ * Family Name |
+ *
+ *
+ *
+ *
+ * HS256 |
+ * HMAC |
+ *
+ *
+ * HS384 |
+ * HMAC |
+ *
+ *
+ * HS512 |
+ * HMAC |
+ *
+ *
+ * RS256 |
+ * RSA |
+ *
+ *
+ * RS384 |
+ * RSA |
+ *
+ *
+ * RS512 |
+ * RSA |
+ *
+ *
+ * PS256 |
+ * RSA |
+ *
+ *
+ * PS384 |
+ * RSA |
+ *
+ *
+ * PS512 |
+ * RSA |
+ *
+ *
+ * ES256 |
+ * Elliptic Curve |
+ *
+ *
+ * ES384 |
+ * Elliptic Curve |
+ *
+ *
+ * ES512 |
+ * Elliptic Curve |
+ *
+ *
+ *
+ *
+ * @return Returns the cryptographic family name of the signature algorithm.
+ *
+ * @since 0.5
+ */
+ public String getFamilyName() {
+ return familyName;
+ }
+
/**
* Returns the name of the JCA algorithm used to compute the signature.
*
diff --git a/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java
index e39ca072..8ea3787a 100644
--- a/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java
+++ b/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java
@@ -310,14 +310,18 @@ public class DefaultJwtBuilder implements JwtBuilder {
return new DefaultJwtSigner(alg, key);
}
- public static String base64UrlEncode(Object o, String errMsg) {
+ protected String base64UrlEncode(Object o, String errMsg) {
String s;
try {
- s = OBJECT_MAPPER.writeValueAsString(o);
+ s = toJson(o);
} catch (JsonProcessingException e) {
throw new IllegalStateException(errMsg, e);
}
return TextCodec.BASE64URL.encode(s);
}
+
+ protected String toJson(Object o) throws JsonProcessingException {
+ return OBJECT_MAPPER.writeValueAsString(o);
+ }
}
diff --git a/src/main/java/io/jsonwebtoken/impl/JwtMap.java b/src/main/java/io/jsonwebtoken/impl/JwtMap.java
index ef6556f2..7c2de6d4 100644
--- a/src/main/java/io/jsonwebtoken/impl/JwtMap.java
+++ b/src/main/java/io/jsonwebtoken/impl/JwtMap.java
@@ -15,6 +15,8 @@
*/
package io.jsonwebtoken.impl;
+import io.jsonwebtoken.lang.Assert;
+
import java.util.Collection;
import java.util.Date;
import java.util.LinkedHashMap;
@@ -30,9 +32,7 @@ public class JwtMap implements Map {
}
public JwtMap(Map map) {
- if (map == null) {
- throw new IllegalArgumentException("Map argument cannot be null.");
- }
+ Assert.notNull(map, "Map argument cannot be null.");
this.map = map;
}
diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java
index 391a5c86..82916847 100644
--- a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java
+++ b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactory.java
@@ -30,8 +30,6 @@ public class DefaultSignatureValidatorFactory implements SignatureValidatorFacto
Assert.notNull(key, "Signing Key cannot be null.");
switch (alg) {
- case NONE:
- throw new IllegalArgumentException("The 'NONE' algorithm cannot be used for signing.");
case HS256:
case HS384:
case HS512:
@@ -48,9 +46,7 @@ public class DefaultSignatureValidatorFactory implements SignatureValidatorFacto
case ES512:
return new EllipticCurveSignatureValidator(alg, key);
default:
- String msg = "Unrecognized algorithm '" + alg.name() + "'. This is a bug. Please submit a ticket " +
- "via the project issue tracker.";
- throw new IllegalStateException(msg);
+ throw new IllegalArgumentException("The '" + alg.name() + "' algorithm cannot be used for signing.");
}
}
}
diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignerFactory.java b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignerFactory.java
index 5a22ce53..5eee74ce 100644
--- a/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignerFactory.java
+++ b/src/main/java/io/jsonwebtoken/impl/crypto/DefaultSignerFactory.java
@@ -30,8 +30,6 @@ public class DefaultSignerFactory implements SignerFactory {
Assert.notNull(key, "Signing Key cannot be null.");
switch (alg) {
- case NONE:
- throw new IllegalArgumentException("The 'NONE' algorithm cannot be used for signing.");
case HS256:
case HS384:
case HS512:
@@ -48,9 +46,7 @@ public class DefaultSignerFactory implements SignerFactory {
case ES512:
return new EllipticCurveSigner(alg, key);
default:
- String msg = "Unrecognized algorithm '" + alg.name() + "'. This is a bug. Please submit a ticket " +
- "via the project issue tracker.";
- throw new IllegalStateException(msg);
+ throw new IllegalArgumentException("The '" + alg.name() + "' algorithm cannot be used for signing.");
}
}
}
diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java
index 14e985ec..31d7cc02 100644
--- a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java
+++ b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveProvider.java
@@ -16,25 +16,24 @@
package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.SignatureAlgorithm;
-import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.lang.Assert;
import java.security.Key;
-import java.security.NoSuchAlgorithmException;
-import java.security.Signature;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.SecureRandom;
import java.util.HashMap;
import java.util.Map;
-abstract class EllipticCurveProvider extends SignatureProvider {
+public abstract class EllipticCurveProvider extends SignatureProvider {
- private static final Map EC_SIG_ALG_NAMES = createEcSigAlgNames();
+ private static final Map EC_CURVE_NAMES = createEcCurveNames();
- private static Map createEcSigAlgNames() {
- Map m =
- new HashMap(); //EC alg name to EC alg signature name
- m.put(SignatureAlgorithm.ES256, "SHA256withECDSA");
- m.put(SignatureAlgorithm.ES384, "SHA384withECDSA");
- m.put(SignatureAlgorithm.ES512, "SHA512withECDSA");
+ private static Map createEcCurveNames() {
+ Map m = new HashMap(); //alg to ASN1 OID name
+ m.put(SignatureAlgorithm.ES256, "secp256r1");
+ m.put(SignatureAlgorithm.ES384, "secp384r1");
+ m.put(SignatureAlgorithm.ES512, "secp521r1");
return m;
}
@@ -43,24 +42,27 @@ abstract class EllipticCurveProvider extends SignatureProvider {
Assert.isTrue(alg.isEllipticCurve(), "SignatureAlgorithm must be an Elliptic Curve algorithm.");
}
- protected Signature createSignatureInstance() {
- return newSignatureInstance();
+ public static KeyPair generateKeyPair() {
+ return generateKeyPair(SignatureAlgorithm.ES512);
}
- protected Signature newSignatureInstance() {
+ public static KeyPair generateKeyPair(SignatureAlgorithm alg) {
+ return generateKeyPair(alg, SignatureProvider.DEFAULT_SECURE_RANDOM);
+ }
+
+ public static KeyPair generateKeyPair(SignatureAlgorithm alg, SecureRandom random) {
+ return generateKeyPair("ECDSA", "BC", alg, random);
+ }
+
+ public static KeyPair generateKeyPair(String jcaAlgorithmName, String jcaProviderName, SignatureAlgorithm alg, SecureRandom random) {
+ Assert.isTrue(alg != null && alg.isEllipticCurve(), "SignatureAlgorithm argument must represent an Elliptic Curve algorithm.");
try {
- String sigAlgName = EC_SIG_ALG_NAMES.get(alg);
- if (sigAlgName == null) {
- throw new NoSuchAlgorithmException("No EllipticCurve signature algorithm for algorithm " + alg +
- ". This is a bug. Please report this to the project issue tracker.");
- }
- return Signature.getInstance(sigAlgName);
- } catch (NoSuchAlgorithmException e) {
- String msg = "Unavailable Elliptic Curve Signature algorithm.";
- if (!alg.isJdkStandard()) {
- msg += " This is not a standard JDK algorithm. Try including BouncyCastle in the runtime classpath.";
- }
- throw new SignatureException(msg, e);
+ KeyPairGenerator g = KeyPairGenerator.getInstance(jcaAlgorithmName, jcaProviderName);
+ String paramSpecCurveName = EC_CURVE_NAMES.get(alg);
+ g.initialize(org.bouncycastle.jce.ECNamedCurveTable.getParameterSpec(paramSpecCurveName), random);
+ return g.generateKeyPair();
+ } catch (Exception e) {
+ throw new IllegalStateException("Unable to generate Elliptic Curve KeyPair: " + e.getMessage(), e);
}
}
}
diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java
index ed62f9c1..741fe386 100644
--- a/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java
+++ b/src/main/java/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidator.java
@@ -28,12 +28,16 @@ import java.security.interfaces.ECPublicKey;
public class EllipticCurveSignatureValidator extends EllipticCurveProvider implements SignatureValidator {
+ private static final String NO_EC_PRIVATE_KEY_MSG =
+ "Elliptic Curve signature validation requires an ECPublicKey. ECPrivateKeys may not be used.";
+
+ private static final String EC_PUBLIC_KEY_REQD_MSG =
+ "Elliptic Curve Signature validation requires either an ECPublicKey instance.";
+
public EllipticCurveSignatureValidator(SignatureAlgorithm alg, Key key) {
super(alg, key);
- Assert.isTrue(!(key instanceof ECPrivateKey),
- "Elliptic Curve signature validation requires an ECPublicKey. ECPrivateKeys may not be used.");
- Assert.isTrue(key instanceof ECPublicKey,
- "Elliptic Curve Signature validation requires either an ECPublicKey instance.");
+ Assert.isTrue(!(key instanceof ECPrivateKey), NO_EC_PRIVATE_KEY_MSG);
+ Assert.isTrue(key instanceof ECPublicKey, EC_PUBLIC_KEY_REQD_MSG);
}
@Override
diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java b/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java
index 32bf7edf..c4ac1bcd 100644
--- a/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java
+++ b/src/main/java/io/jsonwebtoken/impl/crypto/MacProvider.java
@@ -18,12 +18,46 @@ package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.lang.Assert;
+import javax.crypto.SecretKey;
+import javax.crypto.spec.SecretKeySpec;
import java.security.Key;
+import java.security.SecureRandom;
+import java.security.Signature;
-abstract class MacProvider extends SignatureProvider {
+public abstract class MacProvider extends SignatureProvider {
protected MacProvider(SignatureAlgorithm alg, Key key) {
super(alg, key);
Assert.isTrue(alg.isHmac(), "SignatureAlgorithm must be a HMAC SHA algorithm.");
}
+
+ public static SecretKey generateKey() {
+ return generateKey(SignatureAlgorithm.HS512);
+ }
+
+ public static SecretKey generateKey(SignatureAlgorithm alg) {
+ return generateKey(alg, SignatureProvider.DEFAULT_SECURE_RANDOM);
+ }
+
+ public static SecretKey generateKey(SignatureAlgorithm alg, SecureRandom random) {
+
+ Assert.isTrue(alg.isHmac(), "SignatureAlgorithm argument must represent an HMAC algorithm.");
+
+ byte[] bytes;
+
+ switch(alg) {
+ case HS256:
+ bytes = new byte[32];
+ break;
+ case HS384:
+ bytes = new byte[48];
+ break;
+ default:
+ bytes = new byte[64];
+ }
+
+ random.nextBytes(bytes);
+
+ return new SecretKeySpec(bytes, alg.getJcaName());
+ }
}
diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java b/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java
index 51db7bf4..b8d83bf0 100644
--- a/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java
+++ b/src/main/java/io/jsonwebtoken/impl/crypto/RsaProvider.java
@@ -21,12 +21,38 @@ import io.jsonwebtoken.lang.Assert;
import java.security.InvalidAlgorithmParameterException;
import java.security.Key;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
import java.security.Signature;
import java.security.spec.MGF1ParameterSpec;
import java.security.spec.PSSParameterSpec;
+import java.util.HashMap;
+import java.util.Map;
-abstract class RsaProvider extends SignatureProvider {
+public abstract class RsaProvider extends SignatureProvider {
+
+ private static final Map PSS_PARAMETER_SPECS = createPssParameterSpecs();
+
+ private static Map createPssParameterSpecs() {
+
+ Map m = new HashMap();
+
+ MGF1ParameterSpec ps = MGF1ParameterSpec.SHA256;
+ PSSParameterSpec spec = new PSSParameterSpec(ps.getDigestAlgorithm(), "MGF1", ps, 32, 1);
+ m.put(SignatureAlgorithm.PS256, spec);
+
+ ps = MGF1ParameterSpec.SHA384;
+ spec = new PSSParameterSpec(ps.getDigestAlgorithm(), "MGF1", ps, 48, 1);
+ m.put(SignatureAlgorithm.PS384, spec);
+
+ ps = MGF1ParameterSpec.SHA512;
+ spec = new PSSParameterSpec(ps.getDigestAlgorithm(), "MGF1", ps, 64, 1);
+ m.put(SignatureAlgorithm.PS512, spec);
+
+ return m;
+ }
protected RsaProvider(SignatureAlgorithm alg, Key key) {
super(alg, key);
@@ -35,58 +61,51 @@ abstract class RsaProvider extends SignatureProvider {
protected Signature createSignatureInstance() {
- Signature sig = newSignatureInstance();
+ Signature sig = super.createSignatureInstance();
- if (alg.name().startsWith("PS")) {
-
- MGF1ParameterSpec paramSpec;
- int saltLength;
-
- switch (alg) {
- case PS256:
- paramSpec = MGF1ParameterSpec.SHA256;
- saltLength = 32;
- break;
- case PS384:
- paramSpec = MGF1ParameterSpec.SHA384;
- saltLength = 48;
- break;
- case PS512:
- paramSpec = MGF1ParameterSpec.SHA512;
- saltLength = 64;
- break;
- default:
- throw new IllegalArgumentException("Unsupported RSASSA-PSS algorithm: " + alg);
- }
-
- PSSParameterSpec pssParamSpec =
- new PSSParameterSpec(paramSpec.getDigestAlgorithm(), "MGF1", paramSpec, saltLength, 1);
-
- setParameter(sig, pssParamSpec);
+ PSSParameterSpec spec = PSS_PARAMETER_SPECS.get(alg);
+ if (spec != null) {
+ setParameter(sig, spec);
}
-
return sig;
}
- protected Signature newSignatureInstance() {
- try {
- return Signature.getInstance(alg.getJcaName());
- } catch (NoSuchAlgorithmException e) {
- String msg = "Unavailable RSA Signature algorithm.";
- if (!alg.isJdkStandard()) {
- msg += " This is not a standard JDK algorithm. Try including BouncyCastle in the runtime classpath.";
- }
- throw new SignatureException(msg, e);
- }
- }
-
protected void setParameter(Signature sig, PSSParameterSpec spec) {
try {
- sig.setParameter(spec);
+ doSetParameter(sig, spec);
} catch (InvalidAlgorithmParameterException e) {
String msg = "Unsupported RSASSA-PSS parameter '" + spec + "': " + e.getMessage();
throw new SignatureException(msg, e);
}
-
}
+
+ protected void doSetParameter(Signature sig, PSSParameterSpec spec) throws InvalidAlgorithmParameterException {
+ sig.setParameter(spec);
+ }
+
+ public static KeyPair generateKeyPair() {
+ return generateKeyPair(4096);
+ }
+
+ public static KeyPair generateKeyPair(int keySizeInBits) {
+ return generateKeyPair(keySizeInBits, SignatureProvider.DEFAULT_SECURE_RANDOM);
+ }
+
+ public static KeyPair generateKeyPair(int keySizeInBits, SecureRandom random) {
+ return generateKeyPair("RSA", keySizeInBits, random);
+ }
+
+ protected static KeyPair generateKeyPair(String jcaAlgName, int keySizeInBits, SecureRandom random) {
+ KeyPairGenerator keyGenerator;
+ try {
+ keyGenerator = KeyPairGenerator.getInstance(jcaAlgName);
+ } catch (NoSuchAlgorithmException e) {
+ throw new IllegalStateException("Unable to obtain an RSA KeyPairGenerator: " + e.getMessage(), e);
+ }
+
+ keyGenerator.initialize(keySizeInBits, random);
+
+ return keyGenerator.genKeyPair();
+ }
+
}
diff --git a/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java b/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java
index 3abb3e06..d19bf8d5 100644
--- a/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java
+++ b/src/main/java/io/jsonwebtoken/impl/crypto/SignatureProvider.java
@@ -16,14 +16,27 @@
package io.jsonwebtoken.impl.crypto;
import io.jsonwebtoken.SignatureAlgorithm;
+import io.jsonwebtoken.SignatureException;
import io.jsonwebtoken.lang.Assert;
+import io.jsonwebtoken.lang.RuntimeEnvironment;
import java.security.Key;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.security.Signature;
+import java.util.Random;
abstract class SignatureProvider {
+ public static final SecureRandom DEFAULT_SECURE_RANDOM;
+
+ static {
+ DEFAULT_SECURE_RANDOM = new SecureRandom();
+ DEFAULT_SECURE_RANDOM.nextBytes(new byte[64]);
+ }
+
protected final SignatureAlgorithm alg;
- protected final Key key;
+ protected final Key key;
protected SignatureProvider(SignatureAlgorithm alg, Key key) {
Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
@@ -31,4 +44,24 @@ abstract class SignatureProvider {
this.alg = alg;
this.key = key;
}
+
+ protected Signature createSignatureInstance() {
+ try {
+ return getSignatureInstance();
+ } catch (NoSuchAlgorithmException e) {
+ String msg = "Unavailable " + alg.getFamilyName() + " Signature algorithm '" + alg.getJcaName() + "'.";
+ if (!alg.isJdkStandard() && !isBouncyCastleAvailable()) {
+ msg += " This is not a standard JDK algorithm. Try including BouncyCastle in the runtime classpath.";
+ }
+ throw new SignatureException(msg, e);
+ }
+ }
+
+ protected Signature getSignatureInstance() throws NoSuchAlgorithmException {
+ return Signature.getInstance(alg.getJcaName());
+ }
+
+ protected boolean isBouncyCastleAvailable() {
+ return RuntimeEnvironment.BOUNCY_CASTLE_AVAILABLE;
+ }
}
diff --git a/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java b/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java
index 0e41176a..78f27873 100644
--- a/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java
+++ b/src/main/java/io/jsonwebtoken/lang/RuntimeEnvironment.java
@@ -25,6 +25,8 @@ public class RuntimeEnvironment {
private static final AtomicBoolean bcLoaded = new AtomicBoolean(false);
+ public static final boolean BOUNCY_CASTLE_AVAILABLE = Classes.isAvailable(BC_PROVIDER_CLASS_NAME);
+
public static void enableBouncyCastleIfPossible() {
if (bcLoaded.get()) {
diff --git a/src/test/groovy/io/jsonwebtoken/ExpiredJwtExceptionTest.groovy b/src/test/groovy/io/jsonwebtoken/ExpiredJwtExceptionTest.groovy
new file mode 100644
index 00000000..72255b2c
--- /dev/null
+++ b/src/test/groovy/io/jsonwebtoken/ExpiredJwtExceptionTest.groovy
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2015 jsonwebtoken.io
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.jsonwebtoken
+
+import org.testng.annotations.Test
+
+import static org.testng.Assert.*
+
+class ExpiredJwtExceptionTest {
+
+ @Test
+ void testOverloadedConstructor() {
+ def header = Jwts.header()
+ def claims = Jwts.claims()
+ def msg = 'foo'
+ def cause = new NullPointerException()
+
+ def ex = new ExpiredJwtException(header, claims, msg, cause)
+
+ assertSame ex.header, header
+ assertSame ex.claims, claims
+ assertEquals ex.message, msg
+ assertSame ex.cause, cause
+ }
+}
diff --git a/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy
index d7ff0cf1..afb731c7 100644
--- a/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy
+++ b/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy
@@ -19,23 +19,26 @@ import com.fasterxml.jackson.databind.ObjectMapper
import io.jsonwebtoken.impl.DefaultHeader
import io.jsonwebtoken.impl.DefaultJwsHeader
import io.jsonwebtoken.impl.TextCodec
-import org.bouncycastle.jce.ECNamedCurveTable
+import io.jsonwebtoken.impl.crypto.EllipticCurveProvider
+import io.jsonwebtoken.impl.crypto.MacProvider
+import io.jsonwebtoken.impl.crypto.RsaProvider
import org.testng.annotations.Test
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import java.nio.charset.Charset
import java.security.KeyPair
-import java.security.KeyPairGenerator
import java.security.PrivateKey
import java.security.PublicKey
-import java.security.SecureRandom
import static org.testng.Assert.*
class JwtsTest {
- private static final SecureRandom RANDOM = new SecureRandom();
+ @Test
+ void testSubclass() {
+ new Jwts()
+ }
@Test
void testHeaderWithNoArgs() {
@@ -398,7 +401,7 @@ class JwtsTest {
testEC(SignatureAlgorithm.ES256, true)
fail("EC private keys cannot be used to validate EC signatures.")
} catch (UnsupportedJwtException e) {
- assertEquals e.cause.message, "Elliptic Curve signature validation requires an ECPublicKey. ECPrivateKeys may not be used."
+ assertEquals e.cause.message, "Elliptic Curve signature validation requires an ECPublicKey. ECPrivateKeys may not be used."
}
}
@@ -407,8 +410,7 @@ class JwtsTest {
void testParseClaimsJwsWithUnsignedJwt() {
//create random signing key for testing:
- byte[] key = new byte[64];
- RANDOM.nextBytes(key);
+ byte[] key = MacProvider.generateKey().getEncoded()
String notSigned = Jwts.builder().setSubject("Foo").compact();
@@ -425,8 +427,7 @@ class JwtsTest {
void testForgedTokenWithSwappedHeaderUsingNoneAlgorithm() {
//create random signing key for testing:
- byte[] key = new byte[64];
- RANDOM.nextBytes(key);
+ byte[] key = MacProvider.generateKey().getEncoded()
//this is a 'real', valid JWT:
String compact = Jwts.builder().setSubject("Joe").signWith(SignatureAlgorithm.HS256, key).compact();
@@ -458,9 +459,7 @@ class JwtsTest {
void testParseForgedRsaPublicKeyAsHmacTokenVerifiedWithTheRsaPrivateKey() {
//Create a legitimate RSA public and private key pair:
- KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("RSA");
- keyGenerator.initialize(1024);
- KeyPair kp = keyGenerator.genKeyPair();
+ KeyPair kp = RsaProvider.generateKeyPair(1024)
PublicKey publicKey = kp.getPublic();
PrivateKey privateKey = kp.getPrivate();
@@ -493,11 +492,9 @@ class JwtsTest {
void testParseForgedRsaPublicKeyAsHmacTokenVerifiedWithTheRsaPublicKey() {
//Create a legitimate RSA public and private key pair:
- KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("RSA");
- keyGenerator.initialize(1024);
- KeyPair kp = keyGenerator.genKeyPair();
+ KeyPair kp = RsaProvider.generateKeyPair(1024)
PublicKey publicKey = kp.getPublic();
- PrivateKey privateKey = kp.getPrivate();
+ //PrivateKey privateKey = kp.getPrivate();
ObjectMapper om = new ObjectMapper()
String header = TextCodec.BASE64URL.encode(om.writeValueAsString(['alg': 'HS256']))
@@ -525,10 +522,7 @@ class JwtsTest {
static void testRsa(SignatureAlgorithm alg, int keySize=1024, boolean verifyWithPrivateKey=false) {
- KeyPairGenerator keyGenerator = KeyPairGenerator.getInstance("RSA");
- keyGenerator.initialize(keySize);
-
- KeyPair kp = keyGenerator.genKeyPair();
+ KeyPair kp = RsaProvider.generateKeyPair(keySize)
PublicKey publicKey = kp.getPublic();
PrivateKey privateKey = kp.getPrivate();
@@ -551,8 +545,7 @@ class JwtsTest {
static void testHmac(SignatureAlgorithm alg) {
//create random signing key for testing:
- byte[] key = new byte[64];
- RANDOM.nextBytes(key);
+ byte[] key = MacProvider.generateKey().encoded
def claims = [iss: 'joe', exp: later(), 'http://example.com/is_root':true]
@@ -566,9 +559,8 @@ class JwtsTest {
}
static void testEC(SignatureAlgorithm alg, boolean verifyWithPrivateKey=false) {
- KeyPairGenerator g = KeyPairGenerator.getInstance("ECDSA", "BC");
- g.initialize(ECNamedCurveTable.getParameterSpec(alg.getJcaName()), RANDOM);
- KeyPair pair = g.generateKeyPair();
+
+ KeyPair pair = EllipticCurveProvider.generateKeyPair(alg)
PublicKey publicKey = pair.getPublic()
PrivateKey privateKey = pair.getPrivate()
diff --git a/src/test/groovy/io/jsonwebtoken/PrematureJwtExceptionTest.groovy b/src/test/groovy/io/jsonwebtoken/PrematureJwtExceptionTest.groovy
new file mode 100644
index 00000000..8a0b1736
--- /dev/null
+++ b/src/test/groovy/io/jsonwebtoken/PrematureJwtExceptionTest.groovy
@@ -0,0 +1,38 @@
+/*
+ * Copyright (C) 2015 jsonwebtoken.io
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.jsonwebtoken
+
+import org.testng.annotations.Test
+
+import static org.testng.Assert.*
+
+class PrematureJwtExceptionTest {
+
+ @Test
+ void testOverloadedConstructor() {
+ def header = Jwts.header()
+ def claims = Jwts.claims()
+ def msg = 'foo'
+ def cause = new NullPointerException()
+
+ def ex = new PrematureJwtException(header, claims, msg, cause)
+
+ assertSame ex.header, header
+ assertSame ex.claims, claims
+ assertEquals ex.message, msg
+ assertSame ex.cause, cause
+ }
+}
diff --git a/src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy b/src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy
index f562c357..79f364a6 100644
--- a/src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy
+++ b/src/test/groovy/io/jsonwebtoken/SignatureAlgorithmTest.groovy
@@ -54,6 +54,15 @@ class SignatureAlgorithmTest {
}
}
+ @Test
+ void testHmacFamilyName() {
+ for(SignatureAlgorithm alg : SignatureAlgorithm.values()) {
+ if (alg.name().startsWith("HS")) {
+ assertEquals alg.getFamilyName(), "HMAC"
+ }
+ }
+ }
+
@Test
void testIsRsa() {
for(SignatureAlgorithm alg : SignatureAlgorithm.values()) {
@@ -65,6 +74,15 @@ class SignatureAlgorithmTest {
}
}
+ @Test
+ void testRsaFamilyName() {
+ for(SignatureAlgorithm alg : SignatureAlgorithm.values()) {
+ if (alg.name().startsWith("RS") || alg.name().startsWith("PS")) {
+ assertEquals alg.getFamilyName(), "RSA"
+ }
+ }
+ }
+
@Test
void testIsEllipticCurve() {
for(SignatureAlgorithm alg : SignatureAlgorithm.values()) {
@@ -76,6 +94,15 @@ class SignatureAlgorithmTest {
}
}
+ @Test
+ void testEllipticCurveFamilyName() {
+ for(SignatureAlgorithm alg : SignatureAlgorithm.values()) {
+ if (alg.name().startsWith("ES")) {
+ assertEquals alg.getFamilyName(), "Elliptic Curve"
+ }
+ }
+ }
+
@Test
void testIsJdkStandard() {
for(SignatureAlgorithm alg : SignatureAlgorithm.values()) {
diff --git a/src/test/groovy/io/jsonwebtoken/impl/DefaultHeaderTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/DefaultHeaderTest.groovy
new file mode 100644
index 00000000..f99f07f7
--- /dev/null
+++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultHeaderTest.groovy
@@ -0,0 +1,40 @@
+/*
+ * Copyright (C) 2015 jsonwebtoken.io
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.jsonwebtoken.impl
+
+import org.testng.annotations.Test
+import static org.testng.Assert.*
+
+class DefaultHeaderTest {
+
+ @Test
+ void testType() {
+
+ def h = new DefaultHeader()
+
+ h.setType('foo')
+ assertEquals h.getType(), 'foo'
+ }
+
+ @Test
+ void testContentType() {
+
+ def h = new DefaultHeader()
+
+ h.setContentType('bar')
+ assertEquals h.getContentType(), 'bar'
+ }
+}
diff --git a/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsHeaderTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsHeaderTest.groovy
new file mode 100644
index 00000000..e8c83124
--- /dev/null
+++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsHeaderTest.groovy
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2015 jsonwebtoken.io
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.jsonwebtoken.impl
+
+import org.testng.annotations.Test
+import static org.testng.Assert.*
+
+class DefaultJwsHeaderTest {
+
+ @Test
+ void testKeyId() {
+
+ def h = new DefaultJwsHeader()
+
+ h.setKeyId('foo')
+ assertEquals h.getKeyId(), 'foo'
+ }
+}
diff --git a/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy
new file mode 100644
index 00000000..309f372a
--- /dev/null
+++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2015 jsonwebtoken.io
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.jsonwebtoken.impl
+
+import io.jsonwebtoken.JwsHeader
+import io.jsonwebtoken.Jwts
+import org.testng.annotations.Test
+import static org.testng.Assert.*
+
+class DefaultJwsTest {
+
+ @Test
+ void testConstructor() {
+
+ JwsHeader header = Jwts.jwsHeader()
+ def jws = new DefaultJws(header, 'foo', 'sig')
+
+ assertSame jws.getHeader(), header
+ assertEquals jws.getBody(), 'foo'
+ assertEquals jws.getSignature(), 'sig'
+ }
+}
diff --git a/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy
new file mode 100644
index 00000000..4401871c
--- /dev/null
+++ b/src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2015 jsonwebtoken.io
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.jsonwebtoken.impl
+
+import com.fasterxml.jackson.core.JsonProcessingException
+import com.fasterxml.jackson.databind.JsonMappingException
+import io.jsonwebtoken.Jwts
+import io.jsonwebtoken.SignatureAlgorithm
+import io.jsonwebtoken.impl.crypto.MacProvider
+import org.testng.annotations.Test
+import static org.testng.Assert.*
+
+class DefaultJwtBuilderTest {
+
+ @Test
+ void testSetHeader() {
+ def h = Jwts.header()
+ def b = new DefaultJwtBuilder()
+ b.setHeader(h)
+ assertSame b.header, h
+ }
+
+ @Test
+ void testSetHeaderFromMap() {
+ def m = [foo: 'bar']
+ def b = new DefaultJwtBuilder()
+ b.setHeader(m)
+ assertNotNull b.header
+ assertEquals b.header.size(), 1
+ assertEquals b.header.foo, 'bar'
+ }
+
+ @Test
+ void testSetHeaderParams() {
+ def m = [a: 'b', c: 'd']
+ def b = new DefaultJwtBuilder()
+ b.setHeaderParams(m)
+ assertNotNull b.header
+ assertEquals b.header.size(), 2
+ assertEquals b.header.a, 'b'
+ assertEquals b.header.c, 'd'
+ }
+
+ @Test
+ void testSetHeaderParam() {
+ def b = new DefaultJwtBuilder()
+ b.setHeaderParam('foo', 'bar')
+ assertNotNull b.header
+ assertEquals b.header.size(), 1
+ assertEquals b.header.foo, 'bar'
+ }
+
+ @Test
+ void testSetClaims() {
+ def b = new DefaultJwtBuilder()
+ def c = Jwts.claims()
+ b.setClaims(c)
+ assertNotNull b.claims
+ assertSame b.claims, c
+ }
+
+ @Test
+ void testClaim() {
+ def b = new DefaultJwtBuilder()
+ b.claim('foo', 'bar')
+ assertNotNull b.claims
+ assertEquals b.claims.size(), 1
+ assertEquals b.claims.foo, 'bar'
+ }
+
+ @Test
+ void testExistingClaimsAndSetClaim() {
+ def b = new DefaultJwtBuilder()
+ def c = Jwts.claims()
+ b.setClaims(c)
+ b.claim('foo', 'bar')
+ assertSame b.claims, c
+ assertEquals b.claims.size(), 1
+ assertEquals c.size(), 1
+ assertEquals b.claims.foo, 'bar'
+ assertEquals c.foo, 'bar'
+ }
+
+ @Test
+ void testRemoveClaimBySettingNullValue() {
+ def b = new DefaultJwtBuilder()
+ b.claim('foo', 'bar')
+ assertNotNull b.claims
+ assertEquals b.claims.size(), 1
+ assertEquals b.claims.foo, 'bar'
+
+ b.claim('foo', null)
+ assertNotNull b.claims
+ assertNull b.claims.foo
+ }
+
+ @Test
+ void testCompactWithoutBody() {
+ def b = new DefaultJwtBuilder()
+ try {
+ b.compact()
+ fail()
+ } catch (IllegalStateException ise) {
+ assertEquals ise.message, "Either 'payload' or 'claims' must be specified."
+ }
+ }
+
+ @Test
+ void testCompactWithBothPayloadAndClaims() {
+ def b = new DefaultJwtBuilder()
+ b.setPayload('foo')
+ b.claim('a', 'b')
+ try {
+ b.compact()
+ fail()
+ } catch (IllegalStateException ise) {
+ assertEquals ise.message, "Both 'payload' and 'claims' cannot both be specified. Choose either one."
+ }
+ }
+
+ @Test
+ void testCompactWithBothKeyAndKeyBytes() {
+ def b = new DefaultJwtBuilder()
+ b.setPayload('foo')
+ def key = MacProvider.generateKey()
+ b.signWith(SignatureAlgorithm.HS256, key)
+ b.signWith(SignatureAlgorithm.HS256, key.encoded)
+ try {
+ b.compact()
+ fail()
+ } catch (IllegalStateException ise) {
+ assertEquals ise.message, "A key object and key bytes cannot both be specified. Choose either one."
+ }
+ }
+
+ @Test
+ void testCompactWithJwsHeader() {
+ def b = new DefaultJwtBuilder()
+ b.setHeader(Jwts.jwsHeader().setKeyId('a'))
+ b.setPayload('foo')
+ def key = MacProvider.generateKey()
+ b.signWith(SignatureAlgorithm.HS256, key)
+ b.compact()
+ }
+
+ @Test
+ void testBase64UrlEncodeError() {
+
+ def b = new DefaultJwtBuilder() {
+ @Override
+ protected String toJson(Object o) throws JsonProcessingException {
+ throw new JsonMappingException('foo')
+ }
+ }
+
+ try {
+ b.setPayload('foo').compact()
+ fail()
+ } catch (IllegalStateException ise) {
+ assertEquals ise.cause.message, 'foo'
+ }
+
+ }
+}
diff --git a/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy
new file mode 100644
index 00000000..8f8050b2
--- /dev/null
+++ b/src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy
@@ -0,0 +1,124 @@
+/*
+ * Copyright (C) 2015 jsonwebtoken.io
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.jsonwebtoken.impl
+
+import org.testng.annotations.Test
+import static org.testng.Assert.*
+
+class JwtMapTest {
+
+ @Test
+ void testToDateFromDate() {
+
+ def d = new Date()
+
+ Date date = JwtMap.toDate(d, 'foo')
+
+ assertSame date, d
+
+ }
+
+ @Test
+ void testToDateFromString() {
+
+ Date d = new Date(2015, 1, 1, 12, 0, 0)
+
+ String s = (d.getTime() / 1000) + '' //JWT timestamps are in seconds - need to strip millis
+
+ Date date = JwtMap.toDate(s, 'foo')
+
+ assertEquals date, d
+
+ }
+
+ @Test
+ void testToDateFromNonDateObject() {
+ try {
+ JwtMap.toDate(new Object() { @Override public String toString() {return 'hi'} }, 'foo')
+ fail()
+ } catch (IllegalStateException iae) {
+ assertEquals iae.message, "Cannot convert 'foo' value [hi] to Date instance."
+ }
+ }
+
+ @Test
+ void testContainsKey() {
+ def m = new JwtMap()
+ m.put('foo', 'bar')
+ assertTrue m.containsKey('foo')
+ }
+
+ @Test
+ void testContainsValue() {
+ def m = new JwtMap()
+ m.put('foo', 'bar')
+ assertTrue m.containsValue('bar')
+ }
+
+ @Test
+ void testRemoveByPuttingNull() {
+ def m = new JwtMap()
+ m.put('foo', 'bar')
+ assertTrue m.containsKey('foo')
+ assertTrue m.containsValue('bar')
+ m.put('foo', null)
+ assertFalse m.containsKey('foo')
+ assertFalse m.containsValue('bar')
+ }
+
+ @Test
+ void testPutAll() {
+ def m = new JwtMap();
+ m.putAll([a: 'b', c: 'd'])
+ assertEquals m.size(), 2
+ assertEquals m.a, 'b'
+ assertEquals m.c, 'd'
+ }
+
+ @Test
+ void testPutAllWithNullArgument() {
+ def m = new JwtMap();
+ m.putAll((Map)null)
+ assertEquals m.size(), 0
+ }
+
+ @Test
+ void testClear() {
+ def m = new JwtMap();
+ m.put('foo', 'bar')
+ assertEquals m.size(), 1
+ m.clear()
+ assertEquals m.size(), 0
+ }
+
+ @Test
+ void testKeySet() {
+ def m = new JwtMap()
+ m.putAll([a: 'b', c: 'd'])
+ assertEquals( m.keySet(), ['a', 'c'] as Set)
+ }
+
+ void testEntrySet() {
+
+ }
+
+ @Test
+ void testValues() {
+ def m = new JwtMap()
+ m.putAll([a: 'b', c: 'd'])
+ assertEquals( m.values(), ['b', 'd'] as Set)
+ }
+}
diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactoryTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactoryTest.groovy
new file mode 100644
index 00000000..ab5f906f
--- /dev/null
+++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactoryTest.groovy
@@ -0,0 +1,33 @@
+/*
+ * Copyright (C) 2015 jsonwebtoken.io
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.jsonwebtoken.impl.crypto
+
+import io.jsonwebtoken.SignatureAlgorithm
+import org.testng.annotations.Test
+import static org.testng.Assert.*
+
+class DefaultSignatureValidatorFactoryTest {
+
+ @Test
+ void testNoneAlgorithm() {
+ try {
+ new DefaultSignatureValidatorFactory().createSignatureValidator(SignatureAlgorithm.NONE, MacProvider.generateKey())
+ fail()
+ } catch (IllegalArgumentException iae) {
+ assertEquals iae.message, "The 'NONE' algorithm cannot be used for signing."
+ }
+ }
+}
diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignerFactoryTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignerFactoryTest.groovy
index 1c8e3d8f..dfb92df7 100644
--- a/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignerFactoryTest.groovy
+++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignerFactoryTest.groovy
@@ -24,19 +24,13 @@ import static org.testng.Assert.*
class DefaultSignerFactoryTest {
- private static final Random rng = new Random(); //doesn't need to be secure - we're just testing
-
@Test
void testCreateSignerWithNoneAlgorithm() {
- byte[] keyBytes = new byte[32];
- rng.nextBytes(keyBytes);
- SecretKeySpec key = new SecretKeySpec(keyBytes, "foo");
-
def factory = new DefaultSignerFactory();
try {
- factory.createSigner(SignatureAlgorithm.NONE, key);
+ factory.createSigner(SignatureAlgorithm.NONE, MacProvider.generateKey());
fail();
} catch (IllegalArgumentException iae) {
assertEquals iae.message, "The 'NONE' algorithm cannot be used for signing."
diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveProviderTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveProviderTest.groovy
new file mode 100644
index 00000000..bf14422c
--- /dev/null
+++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveProviderTest.groovy
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2015 jsonwebtoken.io
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.jsonwebtoken.impl.crypto
+
+import io.jsonwebtoken.SignatureAlgorithm
+import org.testng.annotations.Test
+
+import java.security.KeyPair
+import java.security.NoSuchProviderException
+import java.security.interfaces.ECPrivateKey
+import java.security.interfaces.ECPublicKey
+
+import static org.testng.Assert.*
+
+
+class EllipticCurveProviderTest {
+
+ @Test
+ void testGenerateKeyPair() {
+ KeyPair pair = EllipticCurveProvider.generateKeyPair()
+ assertNotNull pair
+ assertTrue pair.public instanceof ECPublicKey
+ assertTrue pair.private instanceof ECPrivateKey
+ }
+
+ @Test
+ void testGenerateKeyPairWithInvalidProviderName() {
+ try {
+ EllipticCurveProvider.generateKeyPair("ECDSA", "Foo", SignatureAlgorithm.ES256, null)
+ fail()
+ } catch (IllegalStateException ise) {
+ assertEquals ise.message, "Unable to generate Elliptic Curve KeyPair: no such provider: Foo"
+ assertTrue ise.cause instanceof NoSuchProviderException
+ }
+ }
+}
diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy
new file mode 100644
index 00000000..faf64ac9
--- /dev/null
+++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2015 jsonwebtoken.io
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.jsonwebtoken.impl.crypto
+
+import io.jsonwebtoken.SignatureAlgorithm
+import io.jsonwebtoken.SignatureException
+import org.testng.annotations.Test
+
+import java.security.InvalidKeyException
+import java.security.PublicKey
+import java.security.Signature
+
+import static org.testng.Assert.*
+
+class EllipticCurveSignatureValidatorTest {
+
+ @Test
+ void testDoVerifyWithInvalidKeyException() {
+
+ String msg = 'foo'
+ final InvalidKeyException ex = new InvalidKeyException(msg)
+
+ def v = new EllipticCurveSignatureValidator(SignatureAlgorithm.ES512, EllipticCurveProvider.generateKeyPair().public) {
+ @Override
+ protected boolean doVerify(Signature sig, PublicKey pk, byte[] data, byte[] signature) throws InvalidKeyException, java.security.SignatureException {
+ throw ex;
+ }
+ }
+
+ byte[] bytes = new byte[16]
+ byte[] signature = new byte[16]
+ SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(bytes)
+ SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(signature)
+
+ try {
+ v.isValid(bytes, signature)
+ fail();
+ } catch (SignatureException se) {
+ assertEquals se.message, 'Unable to verify Elliptic Curve signature using configured ECPublicKey. ' + msg
+ assertSame se.cause, ex
+ }
+ }
+}
diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy
new file mode 100644
index 00000000..889a3e1e
--- /dev/null
+++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy
@@ -0,0 +1,110 @@
+/*
+ * Copyright (C) 2015 jsonwebtoken.io
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.jsonwebtoken.impl.crypto
+
+import io.jsonwebtoken.SignatureAlgorithm
+import io.jsonwebtoken.SignatureException
+import org.testng.annotations.Test
+
+import java.security.InvalidKeyException
+import java.security.KeyPair
+import java.security.PrivateKey
+import java.security.PublicKey
+
+import static org.testng.Assert.*
+
+class EllipticCurveSignerTest {
+
+ @Test
+ void testConstructorWithoutECAlg() {
+ try {
+ new EllipticCurveSigner(SignatureAlgorithm.HS256, MacProvider.generateKey());
+ fail('EllipticCurveSigner should reject non ECPrivateKeys');
+ } catch (IllegalArgumentException expected) {
+ assertEquals expected.message, 'SignatureAlgorithm must be an Elliptic Curve algorithm.';
+ }
+ }
+
+ @Test
+ void testConstructorWithoutECPrivateKey() {
+ def key = MacProvider.generateKey()
+ try {
+ new EllipticCurveSigner(SignatureAlgorithm.ES256, key);
+ fail('EllipticCurveSigner should reject non ECPrivateKey instances.')
+ } catch (IllegalArgumentException expected) {
+ assertEquals expected.message, "Elliptic Curve signatures must be computed using an ECPrivateKey. The specified key of " +
+ "type " + key.getClass().getName() + " is not an ECPrivateKey.";
+ }
+ }
+
+ @Test
+ void testDoSignWithInvalidKeyException() {
+
+ KeyPair kp = EllipticCurveProvider.generateKeyPair()
+ PublicKey publicKey = kp.getPublic();
+ PrivateKey privateKey = kp.getPrivate();
+
+ String msg = 'foo'
+ final InvalidKeyException ex = new InvalidKeyException(msg)
+
+ def signer = new EllipticCurveSigner(SignatureAlgorithm.ES256, privateKey) {
+ @Override
+ protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException {
+ throw ex
+ }
+ }
+
+ byte[] bytes = new byte[16]
+ SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(bytes)
+
+ try {
+ signer.sign(bytes)
+ fail();
+ } catch (SignatureException se) {
+ assertEquals se.message, 'Invalid Elliptic Curve PrivateKey. ' + msg
+ assertSame se.cause, ex
+ }
+ }
+
+ @Test
+ void testDoSignWithJdkSignatureException() {
+
+ KeyPair kp = EllipticCurveProvider.generateKeyPair()
+ PublicKey publicKey = kp.getPublic();
+ PrivateKey privateKey = kp.getPrivate();
+
+ String msg = 'foo'
+ final java.security.SignatureException ex = new java.security.SignatureException(msg)
+
+ def signer = new EllipticCurveSigner(SignatureAlgorithm.ES256, privateKey) {
+ @Override
+ protected byte[] doSign(byte[] data) throws InvalidKeyException, java.security.SignatureException {
+ throw ex
+ }
+ }
+
+ byte[] bytes = new byte[16]
+ SignatureProvider.DEFAULT_SECURE_RANDOM.nextBytes(bytes)
+
+ try {
+ signer.sign(bytes)
+ fail();
+ } catch (SignatureException se) {
+ assertEquals se.message, 'Unable to calculate signature using Elliptic Curve PrivateKey. ' + msg
+ assertSame se.cause, ex
+ }
+ }
+}
diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/MacProviderTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/MacProviderTest.groovy
new file mode 100644
index 00000000..7c2c59a5
--- /dev/null
+++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/MacProviderTest.groovy
@@ -0,0 +1,49 @@
+/*
+ * Copyright (C) 2015 jsonwebtoken.io
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.jsonwebtoken.impl.crypto
+
+import io.jsonwebtoken.SignatureAlgorithm
+import org.testng.annotations.Test
+
+import static org.testng.Assert.*
+
+
+class MacProviderTest {
+
+ @Test
+ void testDefault() {
+ byte[] bytes = MacProvider.generateKey().encoded
+ assertEquals 64, bytes.length
+ }
+
+ @Test
+ void testHS256() {
+ byte[] bytes = MacProvider.generateKey(SignatureAlgorithm.HS256).encoded
+ assertEquals 32, bytes.length
+ }
+
+ @Test
+ void testHS384() {
+ byte[] bytes = MacProvider.generateKey(SignatureAlgorithm.HS384).encoded
+ assertEquals 48, bytes.length
+ }
+
+ @Test
+ void testHS512() {
+ byte[] bytes = MacProvider.generateKey(SignatureAlgorithm.HS512).encoded
+ assertEquals 64, bytes.length
+ }
+}
diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaProviderTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaProviderTest.groovy
new file mode 100644
index 00000000..a7010a58
--- /dev/null
+++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaProviderTest.groovy
@@ -0,0 +1,69 @@
+/*
+ * Copyright (C) 2015 jsonwebtoken.io
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.jsonwebtoken.impl.crypto
+
+import io.jsonwebtoken.SignatureAlgorithm
+import io.jsonwebtoken.SignatureException
+import org.testng.annotations.Test
+
+import java.security.InvalidAlgorithmParameterException
+import java.security.KeyPair
+import java.security.Signature
+import java.security.interfaces.RSAPrivateKey
+import java.security.interfaces.RSAPublicKey
+import java.security.spec.PSSParameterSpec
+
+import static org.testng.Assert.*
+
+class RsaProviderTest {
+
+ @Test
+ void testGenerateKeyPair() {
+ KeyPair pair = RsaProvider.generateKeyPair()
+ assertNotNull pair
+ assertTrue pair.public instanceof RSAPublicKey
+ assertTrue pair.private instanceof RSAPrivateKey
+ }
+
+ @Test
+ void testGenerateKeyPairWithInvalidProviderName() {
+ try {
+ RsaProvider.generateKeyPair('foo', 1024, SignatureProvider.DEFAULT_SECURE_RANDOM)
+ fail()
+ } catch (IllegalStateException ise) {
+ assertTrue ise.message.startsWith("Unable to obtain an RSA KeyPairGenerator: ")
+ }
+ }
+
+ @Test
+ void testCreateSignatureInstanceWithInvalidPSSParameterSpecAlgorithm() {
+
+ def p = new RsaProvider(SignatureAlgorithm.PS256, RsaProvider.generateKeyPair(512).public) {
+ @Override
+ protected void doSetParameter(Signature sig, PSSParameterSpec spec) throws InvalidAlgorithmParameterException {
+ throw new InvalidAlgorithmParameterException('foo')
+ }
+ }
+
+ try {
+ p.createSignatureInstance()
+ fail()
+ } catch (SignatureException se) {
+ assertTrue se.message.startsWith('Unsupported RSASSA-PSS parameter')
+ assertEquals se.cause.message, 'foo'
+ }
+ }
+}
diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy
index 89401324..bd673a0b 100644
--- a/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy
+++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/RsaSignerTest.groovy
@@ -28,7 +28,6 @@ import java.security.PublicKey
import static org.testng.Assert.*
-
class RsaSignerTest {
private static final Random rng = new Random(); //doesn't need to be secure - we're just testing
diff --git a/src/test/groovy/io/jsonwebtoken/impl/crypto/SignatureProviderTest.groovy b/src/test/groovy/io/jsonwebtoken/impl/crypto/SignatureProviderTest.groovy
new file mode 100644
index 00000000..4e7c4022
--- /dev/null
+++ b/src/test/groovy/io/jsonwebtoken/impl/crypto/SignatureProviderTest.groovy
@@ -0,0 +1,87 @@
+/*
+ * Copyright (C) 2015 jsonwebtoken.io
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.jsonwebtoken.impl.crypto
+
+import io.jsonwebtoken.SignatureAlgorithm
+import io.jsonwebtoken.SignatureException
+import org.testng.annotations.Test
+
+import java.security.NoSuchAlgorithmException
+import java.security.Signature
+
+import static org.testng.Assert.*
+
+class SignatureProviderTest {
+
+ @Test
+ void testCreateSignatureInstanceNoSuchAlgorithm() {
+
+ def p = new SignatureProvider(SignatureAlgorithm.HS256, MacProvider.generateKey()) {
+ @Override
+ protected Signature getSignatureInstance() throws NoSuchAlgorithmException {
+ throw new NoSuchAlgorithmException('foo')
+ }
+ }
+
+ try {
+ p.createSignatureInstance()
+ fail()
+ } catch (SignatureException se) {
+ assertEquals se.cause.message, 'foo'
+ }
+ }
+
+ @Test
+ void testCreateSignatureInstanceNoSuchAlgorithmNonStandardAlgorithm() {
+
+ def p = new SignatureProvider(SignatureAlgorithm.ES512, EllipticCurveProvider.generateKeyPair().getPublic()) {
+ @Override
+ protected Signature getSignatureInstance() throws NoSuchAlgorithmException {
+ throw new NoSuchAlgorithmException('foo')
+ }
+ }
+
+ try {
+ p.createSignatureInstance()
+ fail()
+ } catch (SignatureException se) {
+ assertEquals se.cause.message, 'foo'
+ }
+ }
+
+ @Test
+ void testCreateSignatureInstanceNoSuchAlgorithmNonStandardAlgorithmWithoutBouncyCastle() {
+
+ def p = new SignatureProvider(SignatureAlgorithm.ES512, EllipticCurveProvider.generateKeyPair().getPublic()) {
+ @Override
+ protected Signature getSignatureInstance() throws NoSuchAlgorithmException {
+ throw new NoSuchAlgorithmException('foo')
+ }
+
+ @Override
+ protected boolean isBouncyCastleAvailable() {
+ return false
+ }
+ }
+
+ try {
+ p.createSignatureInstance()
+ fail()
+ } catch (SignatureException se) {
+ assertTrue se.message.contains('Try including BouncyCastle in the runtime classpath')
+ }
+ }
+}