From d644d8f9eeacf92200460aa88c40a37f9a887624 Mon Sep 17 00:00:00 2001 From: Les Hazlewood Date: Thu, 7 May 2015 23:29:40 -0700 Subject: [PATCH] #27: finished EC implementations. Also added test cases to get to 100% code coverage on all code except the lang package. --- src/main/java/io/jsonwebtoken/Jwts.java | 4 +- .../io/jsonwebtoken/SignatureAlgorithm.java | 102 ++++++++-- .../jsonwebtoken/impl/DefaultJwtBuilder.java | 8 +- .../java/io/jsonwebtoken/impl/JwtMap.java | 6 +- .../DefaultSignatureValidatorFactory.java | 6 +- .../impl/crypto/DefaultSignerFactory.java | 6 +- .../impl/crypto/EllipticCurveProvider.java | 54 +++--- .../EllipticCurveSignatureValidator.java | 12 +- .../jsonwebtoken/impl/crypto/MacProvider.java | 36 +++- .../jsonwebtoken/impl/crypto/RsaProvider.java | 105 ++++++----- .../impl/crypto/SignatureProvider.java | 35 +++- .../jsonwebtoken/lang/RuntimeEnvironment.java | 2 + .../ExpiredJwtExceptionTest.groovy | 38 ++++ .../groovy/io/jsonwebtoken/JwtsTest.groovy | 42 ++--- .../PrematureJwtExceptionTest.groovy | 38 ++++ .../SignatureAlgorithmTest.groovy | 27 +++ .../impl/DefaultHeaderTest.groovy | 40 ++++ .../impl/DefaultJwsHeaderTest.groovy | 31 +++ .../jsonwebtoken/impl/DefaultJwsTest.groovy | 35 ++++ .../impl/DefaultJwtBuilderTest.groovy | 177 ++++++++++++++++++ .../io/jsonwebtoken/impl/JwtMapTest.groovy | 124 ++++++++++++ ...efaultSignatureValidatorFactoryTest.groovy | 33 ++++ .../crypto/DefaultSignerFactoryTest.groovy | 8 +- .../crypto/EllipticCurveProviderTest.groovy | 49 +++++ ...EllipticCurveSignatureValidatorTest.groovy | 56 ++++++ .../crypto/EllipticCurveSignerTest.groovy | 110 +++++++++++ .../impl/crypto/MacProviderTest.groovy | 49 +++++ .../impl/crypto/RsaProviderTest.groovy | 69 +++++++ .../impl/crypto/RsaSignerTest.groovy | 1 - .../impl/crypto/SignatureProviderTest.groovy | 87 +++++++++ 30 files changed, 1252 insertions(+), 138 deletions(-) create mode 100644 src/test/groovy/io/jsonwebtoken/ExpiredJwtExceptionTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/PrematureJwtExceptionTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/impl/DefaultHeaderTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/impl/DefaultJwsHeaderTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/impl/DefaultJwtBuilderTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/impl/JwtMapTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/impl/crypto/DefaultSignatureValidatorFactoryTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveProviderTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignatureValidatorTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/impl/crypto/EllipticCurveSignerTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/impl/crypto/MacProviderTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/impl/crypto/RsaProviderTest.groovy create mode 100644 src/test/groovy/io/jsonwebtoken/impl/crypto/SignatureProviderTest.groovy 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: + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + * + *
SignatureAlgorithmFamily Name
HS256HMAC
HS384HMAC
HS512HMAC
RS256RSA
RS384RSA
RS512RSA
PS256RSA
PS384RSA
PS512RSA
ES256Elliptic Curve
ES384Elliptic Curve
ES512Elliptic 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') + } + } +}