- Added JwtBuilder#signWith(Key) with tests and refactoring. Deprecated additional signWith methods that can lead to confusion.

- Made some tests more deterministic with mocks instead of relying on Random number generator - BigInteger values were non-deterministic because of a random leading sign bit being there (or not) during test runs.

Resolves #366
This commit is contained in:
Les Hazlewood 2018-07-27 23:41:05 -04:00
parent 711d0d5556
commit a669cad29b
11 changed files with 643 additions and 106 deletions

View File

@ -15,9 +15,13 @@
*/
package io.jsonwebtoken;
import io.jsonwebtoken.io.Decoder;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.io.Encoder;
import io.jsonwebtoken.io.Serializer;
import io.jsonwebtoken.security.Keys;
import java.security.InvalidKeyException;
import java.security.Key;
import java.util.Date;
import java.util.Map;
@ -139,7 +143,8 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
* @return the builder instance for method chaining.
* @since 0.2
*/
@Override //only for better/targeted JavaDoc
@Override
//only for better/targeted JavaDoc
JwtBuilder setIssuer(String iss);
/**
@ -165,7 +170,8 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
* @return the builder instance for method chaining.
* @since 0.2
*/
@Override //only for better/targeted JavaDoc
@Override
//only for better/targeted JavaDoc
JwtBuilder setSubject(String sub);
/**
@ -191,7 +197,8 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
* @return the builder instance for method chaining.
* @since 0.2
*/
@Override //only for better/targeted JavaDoc
@Override
//only for better/targeted JavaDoc
JwtBuilder setAudience(String aud);
/**
@ -219,7 +226,8 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
* @return the builder instance for method chaining.
* @since 0.2
*/
@Override //only for better/targeted JavaDoc
@Override
//only for better/targeted JavaDoc
JwtBuilder setExpiration(Date exp);
/**
@ -247,7 +255,8 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
* @return the builder instance for method chaining.
* @since 0.2
*/
@Override //only for better/targeted JavaDoc
@Override
//only for better/targeted JavaDoc
JwtBuilder setNotBefore(Date nbf);
/**
@ -275,7 +284,8 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
* @return the builder instance for method chaining.
* @since 0.2
*/
@Override //only for better/targeted JavaDoc
@Override
//only for better/targeted JavaDoc
JwtBuilder setIssuedAt(Date iat);
/**
@ -305,7 +315,8 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
* @return the builder instance for method chaining.
* @since 0.2
*/
@Override //only for better/targeted JavaDoc
@Override
//only for better/targeted JavaDoc
JwtBuilder setId(String jti);
/**
@ -326,21 +337,53 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
* </pre>
* <p>if desired.</p>
*
* @param name the JWT Claims property name
* @param name the JWT Claims property name
* @param value the value to set for the specified Claims property name
* @return the builder instance for method chaining.
* @since 0.2
*/
JwtBuilder claim(String name, Object value);
/**
* Signs the constructed JWT with the specified key using the key's
* {@link SignatureAlgorithm#forSigningKey(Key) recommended signature algorithm}, producing a JWS. If the
* recommended signature algorithm isn't sufficient for your needs, consider using
* {@link #signWith(Key, SignatureAlgorithm)} instead.
*
* <p>If you are looking to invoke this method with a byte array that you are confident may be used for HMAC-SHA
* algorithms, consider using {@link Keys Keys}.{@link Keys#hmacShaKeyFor(byte[]) hmacShaKeyFor(bytes)} to
* convert the byte array into a valid {@code Key}.</p>
*
* @param key the key to use for signing
* @return the builder instance for method chaining.
* @throws InvalidKeyException if the Key is insufficient or explicitly disallowed by the JWT specification as
* described by {@link SignatureAlgorithm#forSigningKey(Key)}.
* @see #signWith(Key, SignatureAlgorithm)
* @since 0.10.0
*/
JwtBuilder signWith(Key key) throws InvalidKeyException;
/**
* Signs the constructed JWT using the specified algorithm with the specified key, producing a JWS.
*
* <h4>Deprecation Notice: Deprecated as of 0.10.0</h4>
*
* <p>Use {@link Keys Keys}.{@link Keys#hmacShaKeyFor(byte[]) hmacShaKeyFor(bytes)} to
* obtain the {@code Key} and then invoke {@link #signWith(Key)} or {@link #signWith(Key, SignatureAlgorithm)}.</p>
*
* <p>This method will be removed in the 1.0 release.</p>
*
* @param alg the JWS algorithm to use to digitally sign the JWT, thereby producing a JWS.
* @param secretKey the algorithm-specific signing key to use to digitally sign the JWT.
* @return the builder for method chaining.
* @throws InvalidKeyException if the Key is insufficient or explicitly disallowed by the JWT specification as
* described by {@link SignatureAlgorithm#forSigningKey(Key)}.
* @deprecated as of 0.10.0: use {@link Keys Keys}.{@link Keys#hmacShaKeyFor(byte[]) hmacShaKeyFor(bytes)} to
* obtain the {@code Key} and then invoke {@link #signWith(Key)} or {@link #signWith(Key, SignatureAlgorithm)}.
* This method will be removed in the 1.0 release.
*/
JwtBuilder signWith(SignatureAlgorithm alg, byte[] secretKey);
@Deprecated
JwtBuilder signWith(SignatureAlgorithm alg, byte[] secretKey) throws InvalidKeyException;
/**
* Signs the constructed JWT using the specified algorithm with the specified key, producing a JWS.
@ -348,7 +391,7 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
* <p>This is a convenience method: the string argument is first BASE64-decoded to a byte array and this resulting
* byte array is used to invoke {@link #signWith(SignatureAlgorithm, byte[])}.</p>
*
* <h4>Deprecation Notice: Deprecated as of 0.10.0, will be removed in 1.0.0</h4>
* <h4>Deprecation Notice: Deprecated as of 0.10.0, will be removed in the 1.0 release.</h4>
*
* <p>This method has been deprecated because the {@code key} argument for this method can be confusing: keys for
* cryptographic operations are always binary (byte arrays), and many people were confused as to how bytes were
@ -368,26 +411,63 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
* StackOverflow answer</a> explaining why raw (non-base64-encoded) strings are almost always incorrect for
* signature operations.</p>
*
* <p>Finally, please use the {@link #signWith(SignatureAlgorithm, Key)} method, as this method and the
* {@code byte[]} variant will be removed before the 1.0.0 release.</p>
* <p>To perform the correct logic with base64EncodedSecretKey strings with JJWT >= 0.10.0, you may do this:
* <pre><code>
* byte[] keyBytes = {@link Decoders Decoders}.{@link Decoders#BASE64 BASE64}.{@link Decoder#decode(Object) decode(base64EncodedSecretKey)};
* Key key = {@link Keys Keys}.{@link Keys#hmacShaKeyFor(byte[]) hmacShaKeyFor(keyBytes)};
* jwtBuilder.signWith(key); //or {@link #signWith(Key, SignatureAlgorithm)}
* </code></pre>
* </p>
*
* <p>This method will be removed in the 1.0 release.</p>
*
* @param alg the JWS algorithm to use to digitally sign the JWT, thereby producing a JWS.
* @param base64EncodedSecretKey the BASE64-encoded algorithm-specific signing key to use to digitally sign the
* JWT.
* @return the builder for method chaining.
* @deprecated as of 0.10.0 - use {@link #signWith(SignatureAlgorithm, Key)} instead.
* @throws InvalidKeyException if the Key is insufficient or explicitly disallowed by the JWT specification as
* described by {@link SignatureAlgorithm#forSigningKey(Key)}.
* @deprecated as of 0.10.0: use {@link #signWith(Key)} or {@link #signWith(Key, SignatureAlgorithm)} instead. This
* method will be removed in the 1.0 release.
*/
@Deprecated
JwtBuilder signWith(SignatureAlgorithm alg, String base64EncodedSecretKey);
JwtBuilder signWith(SignatureAlgorithm alg, String base64EncodedSecretKey) throws InvalidKeyException;
/**
* Signs the constructed JWT using the specified algorithm with the specified key, producing a JWS.
*
* <p>It is typically recommended to call the {@link #signWith(Key)} instead for simplicity.
* However, this method can be useful if the recommended algorithm heuristics do not meet your needs or if
* you want explicit control over the signature algorithm used with the specified key.</p>
*
* @param alg the JWS algorithm to use to digitally sign the JWT, thereby producing a JWS.
* @param key the algorithm-specific signing key to use to digitally sign the JWT.
* @return the builder for method chaining.
* @throws InvalidKeyException if the Key is insufficient or explicitly disallowed by the JWT specification for
* the specified algorithm.
* @see #signWith(Key)
* @deprecated since 0.10.0: use {@link #signWith(Key, SignatureAlgorithm)} instead. This method will be removed
* in the 1.0 release.
*/
JwtBuilder signWith(SignatureAlgorithm alg, Key key);
@Deprecated
JwtBuilder signWith(SignatureAlgorithm alg, Key key) throws InvalidKeyException;
/**
* Signs the constructed JWT with the specified key using the specified algorithm, producing a JWS.
*
* <p>It is typically recommended to call the {@link #signWith(Key)} instead for simplicity.
* However, this method can be useful if the recommended algorithm heuristics do not meet your needs or if
* you want explicit control over the signature algorithm used with the specified key.</p>
*
* @param key the signing key to use to digitally sign the JWT.
* @param alg the JWS algorithm to use with the key to digitally sign the JWT, thereby producing a JWS.
* @return the builder for method chaining.
* @throws InvalidKeyException if the Key is insufficient or explicitly disallowed by the JWT specification for
* the specified algorithm.
* @see #signWith(Key)
* @since 0.10.0
*/
JwtBuilder signWith(Key key, SignatureAlgorithm alg) throws InvalidKeyException;
/**
* Compresses the JWT body using the specified {@link CompressionCodec}.
@ -407,10 +487,9 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
* <p>Compression when creating JWE tokens however should be universally accepted for any
* library that supports JWE.</p>
*
* @see io.jsonwebtoken.CompressionCodecs
*
* @param codec implementation of the {@link CompressionCodec} to be used.
* @return the builder for method chaining.
* @see io.jsonwebtoken.CompressionCodecs
* @since 0.6.0
*/
JwtBuilder compressWith(CompressionCodec codec);
@ -439,7 +518,7 @@ public interface JwtBuilder extends ClaimsMutator<JwtBuilder> {
* @return the builder for method chaining.
* @since 0.10.0
*/
JwtBuilder serializeToJsonWith(Serializer<Map<String,?>> serializer);
JwtBuilder serializeToJsonWith(Serializer<Map<String, ?>> serializer);
/**
* Actually builds the JWT and serializes it to a compact, URL-safe string according to the

View File

@ -26,6 +26,9 @@ import java.security.Key;
import java.security.PrivateKey;
import java.security.interfaces.ECKey;
import java.security.interfaces.RSAKey;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Type-safe representation of standard JWT signature algorithm names as defined in the
@ -116,6 +119,13 @@ public enum SignatureAlgorithm {
RuntimeEnvironment.enableBouncyCastleIfPossible();
}
//purposefully ordered higher to lower:
private static final List<SignatureAlgorithm> PREFERRED_HMAC_ALGS = Collections.unmodifiableList(Arrays.asList(
SignatureAlgorithm.HS512, SignatureAlgorithm.HS384, SignatureAlgorithm.HS256));
//purposefully ordered higher to lower:
private static final List<SignatureAlgorithm> PREFERRED_EC_ALGS = Collections.unmodifiableList(Arrays.asList(
SignatureAlgorithm.ES512, SignatureAlgorithm.ES384, SignatureAlgorithm.ES256));
private final String value;
private final String description;
private final String familyName;
@ -278,6 +288,18 @@ public enum SignatureAlgorithm {
return familyName.equals("ECDSA");
}
/**
* Returns the minimum key length in bits (not bytes) that may be used with this algorithm according to the
* <a href="https://tools.ietf.org/html/rfc7518">JWT JWA Specification (RFC 7518)</a>.
*
* @return the minimum key length in bits (not bytes) that may be used with this algorithm according to the
* <a href="https://tools.ietf.org/html/rfc7518">JWT JWA Specification (RFC 7518)</a>.
* @since 0.10.0
*/
public int getMinKeyLength() {
return this.minKeyLength;
}
/**
* Returns quietly if the specified key is allowed to create signatures using this algorithm
* according to the <a href="https://tools.ietf.org/html/rfc7518">JWT JWA Specification (RFC 7518)</a> or throws an
@ -393,7 +415,7 @@ public enum SignatureAlgorithm {
throw new InvalidKeyException(msg);
}
RSAKey rsaKey = (RSAKey)key;
RSAKey rsaKey = (RSAKey) key;
int size = rsaKey.getModulus().bitLength();
if (size < this.minKeyLength) {
@ -412,6 +434,194 @@ public enum SignatureAlgorithm {
}
}
/**
* Returns the recommended signature algorithm to be used with the specified key according to the following
* heuristics:
*
* <table>
* <caption>Key Signature Algorithm</caption>
* <thead>
* <tr>
* <th>If the Key is a:</th>
* <th>And:</th>
* <th>With a key size of:</th>
* <th>The returned SignatureAlgorithm will be:</th>
* </tr>
* </thead>
* <tbody>
* <tr>
* <td>{@link SecretKey}</td>
* <td><code>{@link Key#getAlgorithm() getAlgorithm()}.equals("HmacSHA256")</code><sup>1</sup></td>
* <td>256 &lt;= size &lt;= 383 <sup>2</sup></td>
* <td>{@link SignatureAlgorithm#HS256 HS256}</td>
* </tr>
* <tr>
* <td>{@link SecretKey}</td>
* <td><code>{@link Key#getAlgorithm() getAlgorithm()}.equals("HmacSHA384")</code><sup>1</sup></td>
* <td>384 &lt;= size &lt;= 511</td>
* <td>{@link SignatureAlgorithm#HS384 HS384}</td>
* </tr>
* <tr>
* <td>{@link SecretKey}</td>
* <td><code>{@link Key#getAlgorithm() getAlgorithm()}.equals("HmacSHA512")</code><sup>1</sup></td>
* <td>512 &lt;= size</td>
* <td>{@link SignatureAlgorithm#HS512 HS512}</td>
* </tr>
* <tr>
* <td>{@link ECKey}</td>
* <td><code>instanceof {@link PrivateKey}</code></td>
* <td>256 &lt;= size &lt;= 383 <sup>3</sup></td>
* <td>{@link SignatureAlgorithm#ES256 ES256}</td>
* </tr>
* <tr>
* <td>{@link ECKey}</td>
* <td><code>instanceof {@link PrivateKey}</code></td>
* <td>384 &lt;= size &lt;= 511</td>
* <td>{@link SignatureAlgorithm#ES384 ES384}</td>
* </tr>
* <tr>
* <td>{@link ECKey}</td>
* <td><code>instanceof {@link PrivateKey}</code></td>
* <td>4096 &lt;= size</td>
* <td>{@link SignatureAlgorithm#ES512 ES512}</td>
* </tr>
* <tr>
* <td>{@link RSAKey}</td>
* <td><code>instanceof {@link PrivateKey}</code></td>
* <td>2048 &lt;= size &lt;= 3071 <sup>4,5</sup></td>
* <td>{@link SignatureAlgorithm#RS256 RS256}</td>
* </tr>
* <tr>
* <td>{@link RSAKey}</td>
* <td><code>instanceof {@link PrivateKey}</code></td>
* <td>3072 &lt;= size &lt;= 4095 <sup>5</sup></td>
* <td>{@link SignatureAlgorithm#RS384 RS384}</td>
* </tr>
* <tr>
* <td>{@link RSAKey}</td>
* <td><code>instanceof {@link PrivateKey}</code></td>
* <td>4096 &lt;= size <sup>5</sup></td>
* <td>{@link SignatureAlgorithm#RS512 RS512}</td>
* </tr>
* </tbody>
* </table>
* <p>Notes:</p>
* <ol>
* <li>{@code SecretKey} instances must have an {@link Key#getAlgorithm() algorithm} name equal
* to {@code HmacSHA256}, {@code HmacSHA384} or {@code HmacSHA512}. If not, the key bytes might not be
* suitable for HMAC signatures will be rejected with a {@link InvalidKeyException}. </li>
* <li>The JWT <a href="https://tools.ietf.org/html/rfc7518#section-3.2">JWA Specification (RFC 7518,
* Section 3.2)</a> mandates that HMAC-SHA-* signing keys <em>MUST</em> be 256 bits or greater.
* {@code SecretKey}s with key lengths less than 256 bits will be rejected with an
* {@link WeakKeyException}.</li>
* <li>The JWT <a href="https://tools.ietf.org/html/rfc7518#section-3.4">JWA Specification (RFC 7518,
* Section 3.4)</a> mandates that ECDSA signing key lengths <em>MUST</em> be 256 bits or greater.
* {@code ECKey}s with key lengths less than 256 bits will be rejected with a
* {@link WeakKeyException}.</li>
* <li>The JWT <a href="https://tools.ietf.org/html/rfc7518#section-3.3">JWA Specification (RFC 7518,
* Section 3.3)</a> mandates that RSA signing key lengths <em>MUST</em> be 2048 bits or greater.
* {@code RSAKey}s with key lengths less than 2048 bits will be rejected with a
* {@link WeakKeyException}.</li>
* <li>Technically any RSA key of length >= 2048 bits may be used with the {@link #RS256}, {@link #RS384}, and
* {@link #RS512} algorithms, so we assume an RSA signature algorithm based on the key length to
* parallel similar decisions in the JWT specification for HMAC and ECDSA signature algorithms.
* This is not required - just a convenience.</li>
* </ol>
* <p>This implementation does not return the {@link #PS256}, {@link #PS256}, {@link #PS256} RSA variant for any
* specified {@link RSAKey} because:
* <ul>
* <li>The JWT <a href="https://tools.ietf.org/html/rfc7518#section-3.1">JWA Specification (RFC 7518,
* Section 3.1)</a> indicates that {@link #RS256}, {@link #RS384}, and {@link #RS512} are
* recommended algorithms while the {@code PS}* variants are simply marked as optional.</li>
* <li>The {@link #RS256}, {@link #RS384}, and {@link #RS512} algorithms are available in the JDK by default
* while the {@code PS}* variants require an additional JCA Provider (like BouncyCastle).</li>
* </ul>
* </p>
*
* <p>Finally, this method will throw an {@link InvalidKeyException} for any key that does not match the
* heuristics and requirements documented above, since that inevitably means the Key is either insufficient or
* explicitly disallowed by the JWT specification.</p>
*
* @param key the key to inspect
* @return the recommended signature algorithm to be used with the specified key
* @throws InvalidKeyException for any key that does not match the heuristics and requirements documented above,
* since that inevitably means the Key is either insufficient or explicitly disallowed by the JWT specification.
* @since 0.10.0
*/
public static SignatureAlgorithm forSigningKey(Key key) throws InvalidKeyException {
if (key == null) {
throw new InvalidKeyException("Key argument cannot be null.");
}
if (!(key instanceof SecretKey ||
(key instanceof PrivateKey && (key instanceof ECKey || key instanceof RSAKey)))) {
String msg = "JWT standard signing algorithms require either 1) a SecretKey for HMAC-SHA algorithms or " +
"2) a private RSAKey for RSA algorithms or 3) a private ECKey for Elliptic Curve algorithms. " +
"The specified key is of type " + key.getClass().getName();
throw new InvalidKeyException(msg);
}
if (key instanceof SecretKey) {
SecretKey secretKey = (SecretKey)key;
String secretKeyAlg = secretKey.getAlgorithm();
for(SignatureAlgorithm alg : PREFERRED_HMAC_ALGS) {
if (alg.jcaName.equals(secretKeyAlg)) {
alg.assertValidSigningKey(key);
return alg;
}
}
String msg = "The specified SecretKey algorithm did not equal one of the three required JCA " +
"algorithm names of HmacSHA256, HmacSHA384, or HmacSHA512.";
throw new InvalidKeyException(msg);
}
if (key instanceof RSAKey) {
RSAKey rsaKey = (RSAKey) key;
int bitLength = rsaKey.getModulus().bitLength();
if (bitLength >= 4096) {
RS512.assertValidSigningKey(key);
return RS512;
} else if (bitLength >= 3072) {
RS384.assertValidSigningKey(key);
return RS384;
} else if (bitLength >= RS256.minKeyLength) {
RS256.assertValidSigningKey(key);
return RS256;
}
String msg = "The specified RSA signing key is not strong enough to be used with JWT RSA signature " +
"algorithms. The JWT specification requires RSA keys to be >= 2048 bits long. The specified RSA " +
"key is " + bitLength + " bits. See https://tools.ietf.org/html/rfc7518#section-3.3 for more " +
"information.";
throw new WeakKeyException(msg);
}
// if we've made it this far in the method, the key is an ECKey due to the instanceof assertions at the
// top of the method
ECKey ecKey = (ECKey) key;
int bitLength = ecKey.getParams().getOrder().bitLength();
for (SignatureAlgorithm alg : PREFERRED_EC_ALGS) {
if (bitLength >= alg.minKeyLength) {
alg.assertValidSigningKey(key);
return alg;
}
}
String msg = "The specified Elliptic Curve signing key is not strong enough to be used with JWT ECDSA " +
"signature algorithms. The JWT specification requires ECDSA keys to be >= 256 bits long. " +
"The specified ECDSA key is " + bitLength + " bits. See " +
"https://tools.ietf.org/html/rfc7518#section-3.4 for more information.";
throw new WeakKeyException(msg);
}
/**
* Looks up and returns the corresponding {@code SignatureAlgorithm} enum instance based on a
* case-<em>insensitive</em> name comparison.

View File

@ -5,7 +5,11 @@ import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.security.KeyPair;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* Utility class for securely generating {@link SecretKey}s and {@link KeyPair}s.
@ -20,6 +24,10 @@ public final class Keys {
private static final Class[] SIG_ARG_TYPES = new Class[]{SignatureAlgorithm.class};
//purposefully ordered higher to lower:
private static final List<SignatureAlgorithm> PREFERRED_HMAC_ALGS = Collections.unmodifiableList(Arrays.asList(
SignatureAlgorithm.HS512, SignatureAlgorithm.HS384, SignatureAlgorithm.HS256));
//prevent instantiation
private Keys() {
}
@ -40,6 +48,39 @@ public final class Keys {
}
*/
/**
* Creates a new SecretKey instance for use with HMAC-SHA algorithms based on the specified key byte array.
*
* @param bytes the key byte array
* @return a new SecretKey instance for use with HMAC-SHA algorithms based on the specified key byte array.
* @throws WeakKeyException if the key byte array length is less than 256 bits (32 bytes) as mandated by the
* <a href="https://tools.ietf.org/html/rfc7518#section-3.2">JWT JWA Specification
* (RFC 7518, Section 3.2)</a>
*/
public static SecretKey hmacShaKeyFor(byte[] bytes) throws WeakKeyException {
if (bytes == null) {
throw new InvalidKeyException("SecretKey byte array cannot be null.");
}
int bitLength = bytes.length * 8;
for (SignatureAlgorithm alg : PREFERRED_HMAC_ALGS) {
if (bitLength >= alg.getMinKeyLength()) {
return new SecretKeySpec(bytes, alg.getJcaName());
}
}
String msg = "The specified key byte array is " + bitLength + " bits which " +
"is not secure enough for any JWT HMAC-SHA algorithm. The JWT " +
"JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a " +
"size >= 256 bits (the key size must be greater than or equal to the hash " +
"output size). Consider using the " + Keys.class.getName() + "#secretKeyFor(SignatureAlgorithm) method " +
"to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm. See " +
"https://tools.ietf.org/html/rfc7518#section-3.2 for more information.";
throw new WeakKeyException(msg);
}
/**
* Returns a new {@link SecretKey} with a key length suitable for use with the specified {@link SignatureAlgorithm}.
*

View File

@ -18,9 +18,11 @@ package io.jsonwebtoken
import io.jsonwebtoken.security.InvalidKeyException
import io.jsonwebtoken.security.Keys
import io.jsonwebtoken.security.SignatureException
import io.jsonwebtoken.security.WeakKeyException
import org.junit.Test
import javax.crypto.SecretKey
import javax.crypto.spec.SecretKeySpec
import java.security.Key
import java.security.PrivateKey
import java.security.interfaces.ECPrivateKey
@ -34,8 +36,6 @@ import static org.junit.Assert.*
class SignatureAlgorithmTest {
private static final Random random = new Random() //does not need to be secure for testing
@Test
void testNames() {
def algNames = ['HS256', 'HS384', 'HS512', 'RS256', 'RS384', 'RS512',
@ -129,6 +129,171 @@ class SignatureAlgorithmTest {
}
}
@Test
void testGetMinKeyLength() {
for(SignatureAlgorithm alg : SignatureAlgorithm.values()) {
if (alg == SignatureAlgorithm.NONE) {
assertEquals 0, alg.getMinKeyLength()
} else {
if (alg.isRsa()) {
assertEquals 2048, alg.getMinKeyLength()
} else {
int num = alg.name().substring(2, 5).toInteger()
if (alg == SignatureAlgorithm.ES512) {
num = 521
}
assertEquals num, alg.getMinKeyLength()
}
}
}
}
@Test
void testForSigningKeyNullArgument() {
try {
SignatureAlgorithm.forSigningKey(null)
} catch (InvalidKeyException expected) {
assertEquals 'Key argument cannot be null.', expected.message
}
}
@Test
void testForSigningKeyInvalidType() {
def key = new Key() {
@Override
String getAlgorithm() {
return null
}
@Override
String getFormat() {
return null
}
@Override
byte[] getEncoded() {
return new byte[0]
}
}
try {
SignatureAlgorithm.forSigningKey(key)
fail()
} catch (InvalidKeyException expected) {
assertTrue expected.getMessage().startsWith("JWT standard signing algorithms require either 1) a " +
"SecretKey for HMAC-SHA algorithms or 2) a private RSAKey for RSA algorithms or 3) a private " +
"ECKey for Elliptic Curve algorithms. The specified key is of type ")
}
}
@Test
void testForSigningKeySecretKeyInvalidAlgName() {
try {
SignatureAlgorithm.forSigningKey(new SecretKeySpec(new byte[1], 'AES'))
fail()
} catch (InvalidKeyException e) {
assertEquals "The specified SecretKey algorithm did not equal one of the three required JCA " +
"algorithm names of HmacSHA256, HmacSHA384, or HmacSHA512.", e.message
}
}
@Test
void testForSigningKeySecretKeyWeakKey() {
try {
SignatureAlgorithm.forSigningKey(new SecretKeySpec(new byte[1], 'HmacSHA256'))
fail()
} catch (WeakKeyException expected) {
}
}
@Test
void testForSigningKeySecretKeyHappyPath() {
for(SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isHmac() }) {
int numBytes = alg.minKeyLength / 8 as int
assertEquals alg, SignatureAlgorithm.forSigningKey(Keys.hmacShaKeyFor(new byte[numBytes]))
}
}
@Test
void testForSigningKeyRSAWeakKey() {
RSAPrivateKey key = createMock(RSAPrivateKey)
BigInteger modulus = createMock(BigInteger)
expect(key.getModulus()).andStubReturn(modulus)
expect(modulus.bitLength()).andStubReturn(1024)
replay key, modulus
try {
SignatureAlgorithm.forSigningKey(key)
fail()
} catch (WeakKeyException expected) {
}
verify key, modulus
}
@Test
void testForSigningKeyRSAHappyPath() {
for(SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.name().startsWith("RS") }) {
int heuristicKeyLength = (alg == SignatureAlgorithm.RS512 ? 4096 : (alg == SignatureAlgorithm.RS384 ? 3072 : 2048))
RSAPrivateKey key = createMock(RSAPrivateKey)
BigInteger modulus = createMock(BigInteger)
expect(key.getModulus()).andStubReturn(modulus)
expect(modulus.bitLength()).andStubReturn(heuristicKeyLength)
replay key, modulus
assertEquals alg, SignatureAlgorithm.forSigningKey(key)
verify key, modulus
}
}
@Test
void testForSigningKeyECWeakKey() {
ECPrivateKey key = createMock(ECPrivateKey)
ECParameterSpec spec = createMock(ECParameterSpec)
BigInteger order = createMock(BigInteger)
expect(key.getParams()).andStubReturn(spec)
expect(spec.getOrder()).andStubReturn(order)
expect(order.bitLength()).andReturn(128)
replay key, spec, order
try {
SignatureAlgorithm.forSigningKey(key)
fail()
} catch (WeakKeyException expected) {
}
verify key, spec, order
}
@Test
void testForSigningKeyECHappyPath() {
for(SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isEllipticCurve() }) {
ECPrivateKey key = createMock(ECPrivateKey)
ECParameterSpec spec = createMock(ECParameterSpec)
BigInteger order = createMock(BigInteger)
expect(key.getParams()).andStubReturn(spec)
expect(spec.getOrder()).andStubReturn(order)
expect(order.bitLength()).andStubReturn(alg.minKeyLength)
replay key, spec, order
assertEquals alg, SignatureAlgorithm.forSigningKey(key)
verify key, spec, order
}
}
@Test
void testAssertValidSigningKeyWithNoneAlgorithm() {
Key key = createMock(Key)
@ -279,19 +444,16 @@ class SignatureAlgorithmTest {
ECPrivateKey key = createMock(ECPrivateKey)
ECParameterSpec spec = createMock(ECParameterSpec)
int numBits = alg.minKeyLength
int numBytes = numBits / 8 as int
byte[] orderBytes = new byte[numBytes + 1]
random.nextBytes(orderBytes)
BigInteger order = new BigInteger(orderBytes)
expect(key.getParams()).andReturn(spec)
expect(spec.getOrder()).andReturn(order)
BigInteger order = createMock(BigInteger)
expect(key.getParams()).andStubReturn(spec)
expect(spec.getOrder()).andStubReturn(order)
expect(order.bitLength()).andStubReturn(alg.minKeyLength)
replay key, spec
replay key, spec, order
alg.assertValidSigningKey(key)
verify key, spec
verify key, spec, order
}
}
@ -338,15 +500,12 @@ class SignatureAlgorithmTest {
ECPrivateKey key = createMock(ECPrivateKey)
ECParameterSpec spec = createMock(ECParameterSpec)
int numBits = alg.minKeyLength - 8 // 8 bits less than expected
int numBytes = numBits / 8 as int
byte[] orderBytes = new byte[numBytes]
random.nextBytes(orderBytes)
BigInteger order = new BigInteger(orderBytes)
expect(key.getParams()).andReturn(spec)
expect(spec.getOrder()).andReturn(order)
BigInteger order = createMock(BigInteger)
expect(key.getParams()).andStubReturn(spec)
expect(spec.getOrder()).andStubReturn(order)
expect(order.bitLength()).andStubReturn(alg.minKeyLength - 8) //1 byte less than expected
replay key, spec
replay key, spec, order
try {
alg.assertValidSigningKey(key)
@ -361,7 +520,7 @@ class SignatureAlgorithmTest {
"https://tools.ietf.org/html/rfc7518#section-3.4 for more information." as String, expected.message
}
verify key, spec
verify key, spec, order
}
}
@ -371,18 +530,15 @@ class SignatureAlgorithmTest {
for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isRsa() }) {
RSAPrivateKey key = createMock(RSAPrivateKey)
int numBits = alg.minKeyLength
int numBytes = numBits / 8 as int
byte[] modulusBytes = new byte[numBytes + 1]
random.nextBytes(modulusBytes)
BigInteger modulus = new BigInteger(modulusBytes)
expect(key.getModulus()).andReturn(modulus)
BigInteger modulus = createMock(BigInteger)
expect(key.getModulus()).andStubReturn(modulus)
expect(modulus.bitLength()).andStubReturn(alg.minKeyLength)
replay key
replay key, modulus
alg.assertValidSigningKey(key)
verify key
verify key, modulus
}
}
@ -430,14 +586,11 @@ class SignatureAlgorithmTest {
String section = alg.name().startsWith("P") ? "3.5" : "3.3"
RSAPrivateKey key = createMock(RSAPrivateKey)
int numBits = alg.minKeyLength - 8
int numBytes = numBits / 8 as int
byte[] modulusBytes = new byte[numBytes]
random.nextBytes(modulusBytes)
BigInteger modulus = new BigInteger(modulusBytes)
expect(key.getModulus()).andReturn(modulus)
BigInteger modulus = createMock(BigInteger)
expect(key.getModulus()).andStubReturn(modulus)
expect(modulus.bitLength()).andStubReturn(alg.minKeyLength - 8) // 1 less byte
replay key
replay key, modulus
try {
alg.assertValidSigningKey(key)
@ -452,7 +605,7 @@ class SignatureAlgorithmTest {
"https://tools.ietf.org/html/rfc7518#section-${section} for more information." as String, expected.message
}
verify key
verify key, modulus
}
}
@ -606,19 +759,16 @@ class SignatureAlgorithmTest {
ECPrivateKey key = createMock(ECPrivateKey)
ECParameterSpec spec = createMock(ECParameterSpec)
int numBits = alg.minKeyLength
int numBytes = numBits / 8 as int
byte[] orderBytes = new byte[numBytes + 1]
random.nextBytes(orderBytes)
BigInteger order = new BigInteger(orderBytes)
expect(key.getParams()).andReturn(spec)
expect(spec.getOrder()).andReturn(order)
BigInteger order = createMock(BigInteger)
expect(key.getParams()).andStubReturn(spec)
expect(spec.getOrder()).andStubReturn(order)
expect(order.bitLength()).andStubReturn(alg.minKeyLength)
replay key, spec
replay key, spec, order
alg.assertValidVerificationKey(key)
verify key, spec
verify key, spec, order
}
}
@ -645,15 +795,12 @@ class SignatureAlgorithmTest {
ECPrivateKey key = createMock(ECPrivateKey)
ECParameterSpec spec = createMock(ECParameterSpec)
int numBits = alg.minKeyLength - 8 // 8 bits = 1 byte
int numBytes = numBits / 8 as int
byte[] orderBytes = new byte[numBytes]
random.nextBytes(orderBytes)
BigInteger order = new BigInteger(orderBytes)
expect(key.getParams()).andReturn(spec)
expect(spec.getOrder()).andReturn(order)
BigInteger order = createMock(BigInteger)
expect(key.getParams()).andStubReturn(spec)
expect(spec.getOrder()).andStubReturn(order)
expect(order.bitLength()).andStubReturn(alg.minKeyLength - 8) // 1 less byte
replay key, spec
replay key, spec, order
try {
alg.assertValidVerificationKey(key)
@ -668,7 +815,7 @@ class SignatureAlgorithmTest {
"https://tools.ietf.org/html/rfc7518#section-3.4 for more information." as String, expected.message
}
verify key, spec
verify key, spec, order
}
}
@ -678,18 +825,15 @@ class SignatureAlgorithmTest {
for (SignatureAlgorithm alg : SignatureAlgorithm.values().findAll { it.isRsa() }) {
RSAPrivateKey key = createMock(RSAPrivateKey)
int numBits = alg.minKeyLength
int numBytes = numBits / 8 as int
byte[] modulusBytes = new byte[numBytes + 1]
random.nextBytes(modulusBytes)
BigInteger modulus = new BigInteger(modulusBytes)
expect(key.getModulus()).andReturn(modulus)
BigInteger modulus = createMock(BigInteger)
expect(key.getModulus()).andStubReturn(modulus)
expect(modulus.bitLength()).andStubReturn(alg.minKeyLength)
replay key
replay key, modulus
alg.assertValidVerificationKey(key)
verify key
verify key, modulus
}
}
@ -717,14 +861,11 @@ class SignatureAlgorithmTest {
String section = alg.name().startsWith("P") ? "3.5" : "3.3"
RSAPrivateKey key = createMock(RSAPrivateKey)
int numBits = alg.minKeyLength - 8 // 8 bits = 1 byte
int numBytes = numBits / 8 as int
byte[] modulusBytes = new byte[numBytes]
random.nextBytes(modulusBytes)
BigInteger modulus = new BigInteger(modulusBytes)
expect(key.getModulus()).andReturn(modulus)
BigInteger modulus = createMock(BigInteger)
expect(key.getModulus()).andStubReturn(modulus)
expect(modulus.bitLength()).andStubReturn(alg.minKeyLength - 8) //one less byte
replay key
replay key, modulus
try {
alg.assertValidVerificationKey(key)
@ -739,7 +880,7 @@ class SignatureAlgorithmTest {
"https://tools.ietf.org/html/rfc7518#section-${section} for more information." as String, expected.message
}
verify key
verify key, modulus
}
}
}

View File

@ -30,6 +30,32 @@ class KeysTest {
new Keys()
}
@Test
void testHmacShaKeyForWithNullArgument() {
try {
Keys.hmacShaKeyFor(null)
} catch (InvalidKeyException expected) {
assertEquals 'SecretKey byte array cannot be null.', expected.message
}
}
@Test
void testHmacShaKeyForWithWeakKey() {
int numBytes = 31
int numBits = numBytes * 8
try {
Keys.hmacShaKeyFor(new byte[numBytes])
} catch (WeakKeyException expected) {
assertEquals "The specified key byte array is " + numBits + " bits which " +
"is not secure enough for any JWT HMAC-SHA algorithm. The JWT " +
"JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a " +
"size >= 256 bits (the key size must be greater than or equal to the hash " +
"output size). Consider using the " + Keys.class.getName() + "#secretKeyFor(SignatureAlgorithm) method " +
"to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm. See " +
"https://tools.ietf.org/html/rfc7518#section-3.2 for more information." as String, expected.message
}
}
@Test
void testSecretKeyFor() {

View File

@ -34,6 +34,7 @@ import io.jsonwebtoken.lang.Assert;
import io.jsonwebtoken.lang.Classes;
import io.jsonwebtoken.lang.Collections;
import io.jsonwebtoken.lang.Strings;
import io.jsonwebtoken.security.InvalidKeyException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
@ -109,16 +110,33 @@ public class DefaultJwtBuilder implements JwtBuilder {
}
@Override
public JwtBuilder signWith(SignatureAlgorithm alg, byte[] secretKeyBytes) {
public JwtBuilder signWith(Key key) throws InvalidKeyException {
Assert.notNull(key, "Key argument cannot be null.");
SignatureAlgorithm alg = SignatureAlgorithm.forSigningKey(key);
return signWith(key, alg);
}
@Override
public JwtBuilder signWith(Key key, SignatureAlgorithm alg) throws InvalidKeyException {
Assert.notNull(key, "Key argument cannot be null.");
Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
alg.assertValidSigningKey(key); //since 0.10.0 for https://github.com/jwtk/jjwt/issues/334
this.algorithm = alg;
this.key = key;
return this;
}
@Override
public JwtBuilder signWith(SignatureAlgorithm alg, byte[] secretKeyBytes) throws InvalidKeyException {
Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
Assert.notEmpty(secretKeyBytes, "secret key byte array cannot be null or empty.");
Assert.isTrue(alg.isHmac(), "Key bytes may only be specified for HMAC signatures. If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead.");
SecretKey key = new SecretKeySpec(secretKeyBytes, alg.getJcaName());
return signWith(alg, key);
return signWith(key, alg);
}
@Override
public JwtBuilder signWith(SignatureAlgorithm alg, String base64EncodedSecretKey) {
public JwtBuilder signWith(SignatureAlgorithm alg, String base64EncodedSecretKey) throws InvalidKeyException {
Assert.hasText(base64EncodedSecretKey, "base64-encoded secret key cannot be null or empty.");
Assert.isTrue(alg.isHmac(), "Base64-encoded key bytes may only be specified for HMAC signatures. If using RSA or Elliptic Curve, use the signWith(SignatureAlgorithm, Key) method instead.");
byte[] bytes = Decoders.BASE64.decode(base64EncodedSecretKey);
@ -127,12 +145,7 @@ public class DefaultJwtBuilder implements JwtBuilder {
@Override
public JwtBuilder signWith(SignatureAlgorithm alg, Key key) {
Assert.notNull(alg, "SignatureAlgorithm cannot be null.");
Assert.notNull(key, "Key argument cannot be null.");
alg.assertValidSigningKey(key); //since 0.10.0 for https://github.com/jwtk/jjwt/issues/334
this.algorithm = alg;
this.key = key;
return this;
return signWith(key, alg);
}
@Override

View File

@ -641,7 +641,7 @@ class JwtParserTest {
SecretKeySpec key = new SecretKeySpec(randomKey(), "HmacSHA256")
String compact = Jwts.builder().setSubject(subject).signWith(SignatureAlgorithm.HS256, key).compact()
String compact = Jwts.builder().setSubject(subject).signWith(key, SignatureAlgorithm.HS256).compact()
def signingKeyResolver = new SigningKeyResolverAdapter() {
@Override

View File

@ -579,7 +579,7 @@ class JwtsTest {
def key = Keys.secretKeyFor(alg)
def weakKey = Keys.secretKeyFor(SignatureAlgorithm.HS256)
String jws = Jwts.builder().setSubject("Foo").signWith(alg, key).compact()
String jws = Jwts.builder().setSubject("Foo").signWith(key, alg).compact()
try {
Jwts.parser().setSigningKey(weakKey).parseClaimsJws(jws)
@ -743,7 +743,7 @@ class JwtsTest {
def claims = [iss: 'joe', exp: later(), 'http://example.com/is_root': true]
String jwt = Jwts.builder().setClaims(claims).signWith(alg, privateKey).compact()
String jwt = Jwts.builder().setClaims(claims).signWith(privateKey, alg).compact()
def key = publicKey
if (verifyWithPrivateKey) {
@ -781,7 +781,7 @@ class JwtsTest {
def claims = [iss: 'joe', exp: later(), 'http://example.com/is_root': true]
String jwt = Jwts.builder().setClaims(claims).signWith(alg, privateKey).compact()
String jwt = Jwts.builder().setClaims(claims).signWith(privateKey, alg).compact()
def key = publicKey
if (verifyWithPrivateKey) {

View File

@ -30,7 +30,7 @@ class RsaSigningKeyResolverAdapterTest {
def pair = Keys.keyPairFor(alg)
def compact = Jwts.builder().claim('foo', 'bar').signWith(alg, pair.private).compact()
def compact = Jwts.builder().claim('foo', 'bar').signWith(pair.private, alg).compact()
Jws<Claims> jws = Jwts.parser().setSigningKey(pair.public).parseClaimsJws(compact)

View File

@ -26,6 +26,10 @@ import io.jsonwebtoken.io.Serializer
import io.jsonwebtoken.security.Keys
import org.junit.Test
import javax.crypto.KeyGenerator
import javax.crypto.SecretKeyFactory
import java.security.KeyFactory
import static org.junit.Assert.*
class DefaultJwtBuilderTest {
@ -176,8 +180,12 @@ class DefaultJwtBuilderTest {
b.setPayload('foo')
def alg = SignatureAlgorithm.HS256
def key = Keys.secretKeyFor(alg)
b.signWith(key, alg)
String s1 = b.compact()
//ensure deprecated signWith(alg, key) produces the same result:
b.signWith(alg, key)
b.compact()
String s2 = b.compact()
assertEquals s1, s2
}
@Test
@ -220,6 +228,25 @@ class DefaultJwtBuilderTest {
}
}
@Test
void testSignWithKeyOnly() {
def b = new DefaultJwtBuilder()
b.setHeader(Jwts.jwsHeader().setKeyId('a'))
b.setPayload('foo')
def key = KeyGenerator.getInstance('HmacSHA256').generateKey()
b.signWith(key)
String s1 = b.compact()
//ensure matches same result with specified algorithm:
b.signWith(key, SignatureAlgorithm.HS256)
String s2 = b.compact()
assertEquals s1, s2
}
@Test
void testSignWithBytesWithoutHmac() {
def bytes = new byte[16];
@ -348,7 +375,7 @@ class DefaultJwtBuilderTest {
def key = Keys.secretKeyFor(SignatureAlgorithm.HS256)
String jws = b.signWith(SignatureAlgorithm.HS256, key)
String jws = b.signWith(key, SignatureAlgorithm.HS256)
.claim('foo', 'bar')
.compact()

View File

@ -58,7 +58,7 @@ class DefaultJwtParserTest {
def key = Keys.secretKeyFor(SignatureAlgorithm.HS256)
String jws = Jwts.builder().claim('foo', 'bar').signWith(SignatureAlgorithm.HS256, key).compact()
String jws = Jwts.builder().claim('foo', 'bar').signWith(key, SignatureAlgorithm.HS256).compact()
assertEquals 'bar', p.setSigningKey(key).parseClaimsJws(jws).getBody().get('foo')
}