mirror of https://github.com/jwtk/jjwt.git
Secret JWK `k` values larger than HMAC-SHA minimums (#909)
- Ensured Secret JWK 'k' byte arrays for HMAC-SHA algorithms can be larger than the identified HS* algorithm. This is allowed per https://datatracker.ietf.org/doc/html/rfc7518#section-3.2: "A key of the same size as the hash output ... _or larger_ MUST be used with this algorithm" - Ensured that, when using the JwkBuilder, Secret JWK 'alg' values would automatically be set to 'HS256', 'HS384', or 'HS512' if the specified Java SecretKey algorithm name equals a JCA standard name (HmacSHA256, HmacSHA384, etc) or JCA standard HMAC-SHA OID. - Updated CHANGELOG.md accordingly. Fixes #905
This commit is contained in:
parent
b12dabf100
commit
628bd6f4e8
|
@ -65,6 +65,8 @@ This release also:
|
||||||
[6.2.1.3](https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.3), and
|
[6.2.1.3](https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.1.3), and
|
||||||
[6.2.2.1](https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.2.1), respectively.
|
[6.2.2.1](https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.2.1), respectively.
|
||||||
[Issue 901](https://github.com/jwtk/jjwt/issues/901).
|
[Issue 901](https://github.com/jwtk/jjwt/issues/901).
|
||||||
|
* Ensures that Secret JWKs for HMAC-SHA algorithms with `k` sizes larger than the algorithm minimum can
|
||||||
|
be parsed/used as expected. See [Issue #905](https://github.com/jwtk/jjwt/issues/905)
|
||||||
* Fixes various typos in documentation and JavaDoc. Thanks to those contributing pull requests for these!
|
* Fixes various typos in documentation and JavaDoc. Thanks to those contributing pull requests for these!
|
||||||
|
|
||||||
### 0.12.3
|
### 0.12.3
|
||||||
|
|
|
@ -30,6 +30,7 @@ import javax.crypto.Cipher;
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
import javax.crypto.spec.GCMParameterSpec;
|
import javax.crypto.spec.GCMParameterSpec;
|
||||||
import javax.crypto.spec.IvParameterSpec;
|
import javax.crypto.spec.IvParameterSpec;
|
||||||
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.security.SecureRandom;
|
import java.security.SecureRandom;
|
||||||
|
@ -54,9 +55,22 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements KeyBuilderSupplie
|
||||||
protected final int tagBitLength;
|
protected final int tagBitLength;
|
||||||
protected final boolean gcm;
|
protected final boolean gcm;
|
||||||
|
|
||||||
|
static void assertKeyBitLength(int keyBitLength) {
|
||||||
|
if (keyBitLength == 128 || keyBitLength == 192 || keyBitLength == 256) return; // valid
|
||||||
|
String msg = "Invalid AES key length: " + Bytes.bitsMsg(keyBitLength) + ". AES only supports " +
|
||||||
|
"128, 192, or 256 bit keys.";
|
||||||
|
throw new IllegalArgumentException(msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
static SecretKey keyFor(byte[] bytes) {
|
||||||
|
int bitlen = (int) Bytes.bitLength(bytes);
|
||||||
|
assertKeyBitLength(bitlen);
|
||||||
|
return new SecretKeySpec(bytes, KEY_ALG_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
AesAlgorithm(String id, final String jcaTransformation, int keyBitLength) {
|
AesAlgorithm(String id, final String jcaTransformation, int keyBitLength) {
|
||||||
super(id, jcaTransformation);
|
super(id, jcaTransformation);
|
||||||
Assert.isTrue(keyBitLength == 128 || keyBitLength == 192 || keyBitLength == 256, "Invalid AES key length: it must equal 128, 192, or 256.");
|
assertKeyBitLength(keyBitLength);
|
||||||
this.keyBitLength = keyBitLength;
|
this.keyBitLength = keyBitLength;
|
||||||
this.gcm = jcaTransformation.startsWith("AES/GCM");
|
this.gcm = jcaTransformation.startsWith("AES/GCM");
|
||||||
this.ivBitLength = jcaTransformation.equals("AESWrap") ? 0 : (this.gcm ? GCM_IV_SIZE : BLOCK_SIZE);
|
this.ivBitLength = jcaTransformation.equals("AESWrap") ? 0 : (this.gcm ? GCM_IV_SIZE : BLOCK_SIZE);
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
*/
|
*/
|
||||||
package io.jsonwebtoken.impl.security;
|
package io.jsonwebtoken.impl.security;
|
||||||
|
|
||||||
|
import io.jsonwebtoken.Identifiable;
|
||||||
import io.jsonwebtoken.Jwts;
|
import io.jsonwebtoken.Jwts;
|
||||||
import io.jsonwebtoken.impl.lang.Bytes;
|
import io.jsonwebtoken.impl.lang.Bytes;
|
||||||
import io.jsonwebtoken.impl.lang.ParameterReadable;
|
import io.jsonwebtoken.impl.lang.ParameterReadable;
|
||||||
|
@ -22,11 +23,14 @@ import io.jsonwebtoken.impl.lang.RequiredParameterReader;
|
||||||
import io.jsonwebtoken.io.Encoders;
|
import io.jsonwebtoken.io.Encoders;
|
||||||
import io.jsonwebtoken.lang.Assert;
|
import io.jsonwebtoken.lang.Assert;
|
||||||
import io.jsonwebtoken.lang.Strings;
|
import io.jsonwebtoken.lang.Strings;
|
||||||
|
import io.jsonwebtoken.security.AeadAlgorithm;
|
||||||
import io.jsonwebtoken.security.InvalidKeyException;
|
import io.jsonwebtoken.security.InvalidKeyException;
|
||||||
|
import io.jsonwebtoken.security.Keys;
|
||||||
import io.jsonwebtoken.security.MacAlgorithm;
|
import io.jsonwebtoken.security.MacAlgorithm;
|
||||||
import io.jsonwebtoken.security.MalformedKeyException;
|
import io.jsonwebtoken.security.MalformedKeyException;
|
||||||
import io.jsonwebtoken.security.SecretJwk;
|
import io.jsonwebtoken.security.SecretJwk;
|
||||||
import io.jsonwebtoken.security.SecureDigestAlgorithm;
|
import io.jsonwebtoken.security.SecretKeyAlgorithm;
|
||||||
|
import io.jsonwebtoken.security.WeakKeyException;
|
||||||
|
|
||||||
import javax.crypto.SecretKey;
|
import javax.crypto.SecretKey;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
|
@ -44,61 +48,97 @@ class SecretJwkFactory extends AbstractFamilyJwkFactory<SecretKey, SecretJwk> {
|
||||||
protected SecretJwk createJwkFromKey(JwkContext<SecretKey> ctx) {
|
protected SecretJwk createJwkFromKey(JwkContext<SecretKey> ctx) {
|
||||||
SecretKey key = Assert.notNull(ctx.getKey(), "JwkContext key cannot be null.");
|
SecretKey key = Assert.notNull(ctx.getKey(), "JwkContext key cannot be null.");
|
||||||
String k;
|
String k;
|
||||||
|
byte[] encoded = null;
|
||||||
try {
|
try {
|
||||||
byte[] encoded = KeysBridge.getEncoded(key);
|
encoded = KeysBridge.getEncoded(key);
|
||||||
k = Encoders.BASE64URL.encode(encoded);
|
k = Encoders.BASE64URL.encode(encoded);
|
||||||
Assert.hasText(k, "k value cannot be null or empty.");
|
Assert.hasText(k, "k value cannot be null or empty.");
|
||||||
} catch (Throwable t) {
|
} catch (Throwable t) {
|
||||||
String msg = "Unable to encode SecretKey to JWK: " + t.getMessage();
|
String msg = "Unable to encode SecretKey to JWK: " + t.getMessage();
|
||||||
throw new InvalidKeyException(msg, t);
|
throw new InvalidKeyException(msg, t);
|
||||||
|
} finally {
|
||||||
|
Bytes.clear(encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
MacAlgorithm mac = DefaultMacAlgorithm.findByKey(key);
|
||||||
|
if (mac != null) {
|
||||||
|
ctx.put(AbstractJwk.ALG.getId(), mac.getId());
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.put(DefaultSecretJwk.K.getId(), k);
|
ctx.put(DefaultSecretJwk.K.getId(), k);
|
||||||
|
|
||||||
return new DefaultSecretJwk(ctx);
|
return createJwkFromValues(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void assertKeyBitLength(byte[] bytes, MacAlgorithm alg) {
|
private static void assertKeyBitLength(byte[] bytes, MacAlgorithm alg) {
|
||||||
long bitLen = Bytes.bitLength(bytes);
|
long bitLen = Bytes.bitLength(bytes);
|
||||||
long requiredBitLen = alg.getKeyBitLength();
|
long requiredBitLen = alg.getKeyBitLength();
|
||||||
if (bitLen != requiredBitLen) {
|
if (bitLen < requiredBitLen) {
|
||||||
// Implementors note: Don't print out any information about the `bytes` value itself - size,
|
// Implementors note: Don't print out any information about the `bytes` value itself - size,
|
||||||
// content, etc., as it is considered secret material:
|
// content, etc., as it is considered secret material:
|
||||||
String msg = "Secret JWK " + AbstractJwk.ALG + " value is '" + alg.getId() +
|
String msg = "Secret JWK " + AbstractJwk.ALG + " value is '" + alg.getId() +
|
||||||
"', but the " + DefaultSecretJwk.K + " length does not equal the '" + alg.getId() +
|
"', but the " + DefaultSecretJwk.K + " length is smaller than the " + alg.getId() +
|
||||||
"' length requirement of " + Bytes.bitsMsg(requiredBitLen) +
|
" minimum length of " + Bytes.bitsMsg(requiredBitLen) +
|
||||||
". This discrepancy could be the result of an algorithm " +
|
" required by " +
|
||||||
"substitution attack or simply an erroneously constructed JWK. In either case, it is likely " +
|
"[JWA RFC 7518, Section 3.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.2), " +
|
||||||
"to result in unexpected or undesired security consequences.";
|
"2nd paragraph: 'A key of the same size as the hash output or larger MUST be used with this " +
|
||||||
throw new MalformedKeyException(msg);
|
"algorithm.'";
|
||||||
|
throw new WeakKeyException(msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void assertSymmetric(Identifiable alg) {
|
||||||
|
if (alg instanceof MacAlgorithm || alg instanceof SecretKeyAlgorithm || alg instanceof AeadAlgorithm)
|
||||||
|
return; // valid
|
||||||
|
String msg = "Invalid Secret JWK " + AbstractJwk.ALG + " value '" + alg.getId() + "'. Secret JWKs " +
|
||||||
|
"may only be used with symmetric (secret) key algorithms.";
|
||||||
|
throw new MalformedKeyException(msg);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected SecretJwk createJwkFromValues(JwkContext<SecretKey> ctx) {
|
protected SecretJwk createJwkFromValues(JwkContext<SecretKey> ctx) {
|
||||||
ParameterReadable reader = new RequiredParameterReader(ctx);
|
ParameterReadable reader = new RequiredParameterReader(ctx);
|
||||||
byte[] bytes = reader.get(DefaultSecretJwk.K);
|
final byte[] bytes = reader.get(DefaultSecretJwk.K);
|
||||||
String jcaName = null;
|
SecretKey key;
|
||||||
|
|
||||||
String id = ctx.getAlgorithm();
|
String algId = ctx.getAlgorithm();
|
||||||
if (Strings.hasText(id)) {
|
if (!Strings.hasText(algId)) { // optional per https://www.rfc-editor.org/rfc/rfc7517.html#section-4.4
|
||||||
SecureDigestAlgorithm<?, ?> alg = Jwts.SIG.get().get(id);
|
|
||||||
if (alg instanceof MacAlgorithm) {
|
// Here we try to infer the best type of key to create based on siguse and/or key length.
|
||||||
jcaName = ((CryptoAlgorithm) alg).getJcaName(); // valid for all JJWT alg implementations
|
//
|
||||||
Assert.hasText(jcaName, "Algorithm jcaName cannot be null or empty.");
|
// AES requires 128, 192, or 256 bits, so anything larger than 256 cannot be AES, so we'll need to assume
|
||||||
assertKeyBitLength(bytes, (MacAlgorithm) alg);
|
// HMAC.
|
||||||
}
|
//
|
||||||
}
|
// Also, 256 bits works for either HMAC or AES, so we just have to choose one as there is no other
|
||||||
if (!Strings.hasText(jcaName)) {
|
// RFC-based criteria for determining. Historically, we've chosen AES due to the larger number of
|
||||||
if (ctx.isSigUse()) {
|
// KeyAlgorithm and AeadAlgorithm use cases, so that's our default.
|
||||||
|
int kBitLen = (int) Bytes.bitLength(bytes);
|
||||||
|
|
||||||
|
if (ctx.isSigUse() || kBitLen > Jwts.SIG.HS256.getKeyBitLength()) {
|
||||||
// The only JWA SecretKey signature algorithms are HS256, HS384, HS512, so choose based on bit length:
|
// The only JWA SecretKey signature algorithms are HS256, HS384, HS512, so choose based on bit length:
|
||||||
jcaName = "HmacSHA" + Bytes.bitLength(bytes);
|
key = Keys.hmacShaKeyFor(bytes);
|
||||||
} else { // not an HS* algorithm, and all standard AeadAlgorithms use AES keys:
|
} else {
|
||||||
jcaName = AesAlgorithm.KEY_ALG_NAME;
|
key = AesAlgorithm.keyFor(bytes);
|
||||||
}
|
}
|
||||||
|
ctx.setKey(key);
|
||||||
|
return new DefaultSecretJwk(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
|
//otherwise 'alg' was specified, ensure it's valid for secret key use:
|
||||||
|
Identifiable alg = Jwts.SIG.get().get(algId);
|
||||||
|
if (alg == null) alg = Jwts.KEY.get().get(algId);
|
||||||
|
if (alg == null) alg = Jwts.ENC.get().get(algId);
|
||||||
|
if (alg != null) assertSymmetric(alg); // if we found a standard alg, it must be a symmetric key algorithm
|
||||||
|
|
||||||
|
if (alg instanceof MacAlgorithm) {
|
||||||
|
assertKeyBitLength(bytes, ((MacAlgorithm) alg));
|
||||||
|
String jcaName = ((CryptoAlgorithm) alg).getJcaName();
|
||||||
|
Assert.hasText(jcaName, "Algorithm jcaName cannot be null or empty.");
|
||||||
|
key = new SecretKeySpec(bytes, jcaName);
|
||||||
|
} else {
|
||||||
|
// all other remaining JWA-standard symmetric algs use AES:
|
||||||
|
key = AesAlgorithm.keyFor(bytes);
|
||||||
}
|
}
|
||||||
Assert.stateNotNull(jcaName, "jcaName cannot be null (invariant)");
|
|
||||||
SecretKey key = new SecretKeySpec(bytes, jcaName);
|
|
||||||
ctx.setKey(key);
|
ctx.setKey(key);
|
||||||
return new DefaultSecretJwk(ctx);
|
return new DefaultSecretJwk(ctx);
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,10 +29,8 @@ import static org.junit.Assert.*
|
||||||
|
|
||||||
class AbstractJwkBuilderTest {
|
class AbstractJwkBuilderTest {
|
||||||
|
|
||||||
private static final SecretKey SKEY = TestKeys.A256GCM
|
|
||||||
|
|
||||||
private static AbstractJwkBuilder<SecretKey, SecretJwk, AbstractJwkBuilder> builder() {
|
private static AbstractJwkBuilder<SecretKey, SecretJwk, AbstractJwkBuilder> builder() {
|
||||||
return (AbstractJwkBuilder) Jwks.builder().key(SKEY)
|
return (AbstractJwkBuilder) Jwks.builder().key(TestKeys.NA256)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|
|
@ -97,7 +97,7 @@ class JwkSerializationTest {
|
||||||
|
|
||||||
static void testSecretJwk(Serializer ser, Deserializer des) {
|
static void testSecretJwk(Serializer ser, Deserializer des) {
|
||||||
|
|
||||||
def key = TestKeys.A128GCM
|
def key = TestKeys.NA256
|
||||||
def jwk = Jwks.builder().key(key).id('id').build()
|
def jwk = Jwks.builder().key(key).id('id').build()
|
||||||
assertWrapped(jwk, ['k'])
|
assertWrapped(jwk, ['k'])
|
||||||
|
|
||||||
|
|
|
@ -40,7 +40,7 @@ import static org.junit.Assert.*
|
||||||
|
|
||||||
class JwksTest {
|
class JwksTest {
|
||||||
|
|
||||||
private static final SecretKey SKEY = Jwts.SIG.HS256.key().build()
|
private static final SecretKey SKEY = TestKeys.NA256
|
||||||
private static final java.security.KeyPair EC_PAIR = Jwts.SIG.ES256.keyPair().build()
|
private static final java.security.KeyPair EC_PAIR = Jwts.SIG.ES256.keyPair().build()
|
||||||
|
|
||||||
private static String srandom() {
|
private static String srandom() {
|
||||||
|
@ -172,7 +172,7 @@ class JwksTest {
|
||||||
@Test
|
@Test
|
||||||
void testOperations() {
|
void testOperations() {
|
||||||
def val = [Jwks.OP.SIGN, Jwks.OP.VERIFY] as Set<KeyOperation>
|
def val = [Jwks.OP.SIGN, Jwks.OP.VERIFY] as Set<KeyOperation>
|
||||||
def jwk = Jwks.builder().key(TestKeys.A128GCM).operations().add(val).and().build()
|
def jwk = Jwks.builder().key(TestKeys.NA256).operations().add(val).and().build()
|
||||||
assertEquals val, jwk.getOperations()
|
assertEquals val, jwk.getOperations()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -15,9 +15,10 @@
|
||||||
*/
|
*/
|
||||||
package io.jsonwebtoken.impl.security
|
package io.jsonwebtoken.impl.security
|
||||||
|
|
||||||
import io.jsonwebtoken.security.Jwks
|
import io.jsonwebtoken.Jwts
|
||||||
import io.jsonwebtoken.security.MalformedKeyException
|
import io.jsonwebtoken.impl.lang.Bytes
|
||||||
import io.jsonwebtoken.security.SecretJwk
|
import io.jsonwebtoken.io.Encoders
|
||||||
|
import io.jsonwebtoken.security.*
|
||||||
import org.junit.Test
|
import org.junit.Test
|
||||||
|
|
||||||
import static org.junit.Assert.*
|
import static org.junit.Assert.*
|
||||||
|
@ -30,10 +31,14 @@ import static org.junit.Assert.*
|
||||||
*/
|
*/
|
||||||
class SecretJwkFactoryTest {
|
class SecretJwkFactoryTest {
|
||||||
|
|
||||||
|
private static Set<MacAlgorithm> macAlgs() {
|
||||||
|
return Jwts.SIG.get().values().findAll({ it -> it instanceof MacAlgorithm }) as Collection<MacAlgorithm>
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
// if a jwk does not have an 'alg' or 'use' param, we default to an AES key
|
// if a jwk does not have an 'alg' or 'use' param, we default to an AES key
|
||||||
void testNoAlgNoSigJcaName() {
|
void testNoAlgNoSigJcaName() {
|
||||||
SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).delete('alg').build()
|
SecretJwk jwk = Jwks.builder().key(TestKeys.NA256).build()
|
||||||
SecretJwk result = Jwks.builder().add(jwk).build() as SecretJwk
|
SecretJwk result = Jwks.builder().add(jwk).build() as SecretJwk
|
||||||
assertEquals 'AES', result.toKey().getAlgorithm()
|
assertEquals 'AES', result.toKey().getAlgorithm()
|
||||||
}
|
}
|
||||||
|
@ -47,7 +52,7 @@ class SecretJwkFactoryTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSignOpSetsKeyHmacSHA256() {
|
void testSignOpSetsKeyHmacSHA256() {
|
||||||
SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).delete('alg').build()
|
SecretJwk jwk = Jwks.builder().key(TestKeys.NA256).build()
|
||||||
SecretJwk result = Jwks.builder().add(jwk).operations().add(Jwks.OP.SIGN).and().build() as SecretJwk
|
SecretJwk result = Jwks.builder().add(jwk).operations().add(Jwks.OP.SIGN).and().build() as SecretJwk
|
||||||
assertNull result.getAlgorithm()
|
assertNull result.getAlgorithm()
|
||||||
assertNull result.get('use')
|
assertNull result.get('use')
|
||||||
|
@ -63,7 +68,7 @@ class SecretJwkFactoryTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSignOpSetsKeyHmacSHA384() {
|
void testSignOpSetsKeyHmacSHA384() {
|
||||||
SecretJwk jwk = Jwks.builder().key(TestKeys.HS384).delete('alg').build()
|
SecretJwk jwk = Jwks.builder().key(TestKeys.NA384).build()
|
||||||
SecretJwk result = Jwks.builder().add(jwk).operations().add(Jwks.OP.SIGN).and().build() as SecretJwk
|
SecretJwk result = Jwks.builder().add(jwk).operations().add(Jwks.OP.SIGN).and().build() as SecretJwk
|
||||||
assertNull result.getAlgorithm()
|
assertNull result.getAlgorithm()
|
||||||
assertNull result.get('use')
|
assertNull result.get('use')
|
||||||
|
@ -79,7 +84,7 @@ class SecretJwkFactoryTest {
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
void testSignOpSetsKeyHmacSHA512() {
|
void testSignOpSetsKeyHmacSHA512() {
|
||||||
SecretJwk jwk = Jwks.builder().key(TestKeys.HS512).delete('alg').build()
|
SecretJwk jwk = Jwks.builder().key(TestKeys.NA512).build()
|
||||||
SecretJwk result = Jwks.builder().add(jwk).operations().add(Jwks.OP.SIGN).and().build() as SecretJwk
|
SecretJwk result = Jwks.builder().add(jwk).operations().add(Jwks.OP.SIGN).and().build() as SecretJwk
|
||||||
assertNull result.getAlgorithm()
|
assertNull result.getAlgorithm()
|
||||||
assertNull result.get('use')
|
assertNull result.get('use')
|
||||||
|
@ -89,7 +94,7 @@ class SecretJwkFactoryTest {
|
||||||
@Test
|
@Test
|
||||||
// no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA256
|
// no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA256
|
||||||
void testNoAlgAndSigUseForHS256() {
|
void testNoAlgAndSigUseForHS256() {
|
||||||
SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).delete('alg').build()
|
SecretJwk jwk = Jwks.builder().key(TestKeys.NA256).build()
|
||||||
assertFalse jwk.containsKey('alg')
|
assertFalse jwk.containsKey('alg')
|
||||||
assertFalse jwk.containsKey('use')
|
assertFalse jwk.containsKey('use')
|
||||||
SecretJwk result = Jwks.builder().add(jwk).add('use', 'sig').build() as SecretJwk
|
SecretJwk result = Jwks.builder().add(jwk).add('use', 'sig').build() as SecretJwk
|
||||||
|
@ -99,7 +104,7 @@ class SecretJwkFactoryTest {
|
||||||
@Test
|
@Test
|
||||||
// no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA384
|
// no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA384
|
||||||
void testNoAlgAndSigUseForHS384() {
|
void testNoAlgAndSigUseForHS384() {
|
||||||
SecretJwk jwk = Jwks.builder().key(TestKeys.HS384).delete('alg').build()
|
SecretJwk jwk = Jwks.builder().key(TestKeys.NA384).build()
|
||||||
assertFalse jwk.containsKey('alg')
|
assertFalse jwk.containsKey('alg')
|
||||||
assertFalse jwk.containsKey('use')
|
assertFalse jwk.containsKey('use')
|
||||||
SecretJwk result = Jwks.builder().add(jwk).add('use', 'sig').build() as SecretJwk
|
SecretJwk result = Jwks.builder().add(jwk).add('use', 'sig').build() as SecretJwk
|
||||||
|
@ -109,7 +114,7 @@ class SecretJwkFactoryTest {
|
||||||
@Test
|
@Test
|
||||||
// no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA512
|
// no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA512
|
||||||
void testNoAlgAndSigUseForHS512() {
|
void testNoAlgAndSigUseForHS512() {
|
||||||
SecretJwk jwk = Jwks.builder().key(TestKeys.HS512).delete('alg').build()
|
SecretJwk jwk = Jwks.builder().key(TestKeys.NA512).build()
|
||||||
assertFalse jwk.containsKey('alg')
|
assertFalse jwk.containsKey('alg')
|
||||||
assertFalse jwk.containsKey('use')
|
assertFalse jwk.containsKey('use')
|
||||||
SecretJwk result = Jwks.builder().add(jwk).add('use', 'sig').build() as SecretJwk
|
SecretJwk result = Jwks.builder().add(jwk).add('use', 'sig').build() as SecretJwk
|
||||||
|
@ -119,20 +124,32 @@ class SecretJwkFactoryTest {
|
||||||
@Test
|
@Test
|
||||||
// no 'alg' jwk property, but 'use' is something other than 'sig', so jcaName should default to AES
|
// no 'alg' jwk property, but 'use' is something other than 'sig', so jcaName should default to AES
|
||||||
void testNoAlgAndNonSigUse() {
|
void testNoAlgAndNonSigUse() {
|
||||||
SecretJwk jwk = Jwks.builder().key(TestKeys.HS256).delete('alg').build()
|
SecretJwk jwk = Jwks.builder().key(TestKeys.NA256).build()
|
||||||
assertFalse jwk.containsKey('alg')
|
assertFalse jwk.containsKey('alg')
|
||||||
assertFalse jwk.containsKey('use')
|
assertFalse jwk.containsKey('use')
|
||||||
SecretJwk result = Jwks.builder().add(jwk).add('use', 'foo').build() as SecretJwk
|
SecretJwk result = Jwks.builder().add(jwk).add('use', 'foo').build() as SecretJwk
|
||||||
assertEquals 'AES', result.toKey().getAlgorithm()
|
assertEquals 'AES', result.toKey().getAlgorithm()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
// 'oct' type, but 'alg' value is not a secret key algorithm (and therefore malformed)
|
||||||
|
void testMismatchedAlgorithm() {
|
||||||
|
try {
|
||||||
|
Jwks.builder().key(TestKeys.NA256).add('alg', Jwts.SIG.RS256.getId()).build()
|
||||||
|
fail()
|
||||||
|
} catch (MalformedKeyException expected) {
|
||||||
|
String msg = "Invalid Secret JWK ${AbstractJwk.ALG} value 'RS256'. Secret JWKs may only be used with " +
|
||||||
|
"symmetric (secret) key algorithms."
|
||||||
|
assertEquals msg, expected.message
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Test the case where a jwk `alg` value is present, but the key material doesn't match that algs key length
|
* Test the case where a jwk `alg` value is present, but the key material doesn't match that algs key length
|
||||||
* requirements. This would be a malformed key.
|
* requirements. This would be a malformed key.
|
||||||
*/
|
*/
|
||||||
@Test
|
@Test
|
||||||
void testSizeMismatchedSecretJwk() {
|
void testSizeMismatchedSecretJwk() {
|
||||||
|
|
||||||
//first get a valid HS256 JWK:
|
//first get a valid HS256 JWK:
|
||||||
SecretJwk validJwk = Jwks.builder().key(TestKeys.HS256).build()
|
SecretJwk validJwk = Jwks.builder().key(TestKeys.HS256).build()
|
||||||
|
|
||||||
|
@ -142,12 +159,72 @@ class SecretJwkFactoryTest {
|
||||||
.add('alg', 'HS384')
|
.add('alg', 'HS384')
|
||||||
.build()
|
.build()
|
||||||
fail()
|
fail()
|
||||||
} catch (MalformedKeyException expected) {
|
} catch (WeakKeyException expected) {
|
||||||
String msg = "Secret JWK 'alg' (Algorithm) value is 'HS384', but the 'k' (Key Value) length does " +
|
String msg = "Secret JWK 'alg' (Algorithm) value is 'HS384', but the 'k' (Key Value) length is smaller " +
|
||||||
"not equal the 'HS384' length requirement of 384 bits (48 bytes). This discrepancy could " +
|
"than the HS384 minimum length of 384 bits (48 bytes) required by " +
|
||||||
"be the result of an algorithm substitution attack or simply an erroneously constructed " +
|
"[JWA RFC 7518, Section 3.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.2), 2nd " +
|
||||||
"JWK. In either case, it is likely to result in unexpected or undesired security consequences."
|
"paragraph: 'A key of the same size as the hash output or larger MUST be used with this " +
|
||||||
|
"algorithm.'"
|
||||||
assertEquals msg, expected.getMessage()
|
assertEquals msg, expected.getMessage()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Test when a {@code k} size is smaller, equal to, and larger than the minimum required number of bits/bytes for
|
||||||
|
* a given HmacSHA* algorithm. The RFCs indicate smaller-than is not allowed, while equal-to and greater-than are
|
||||||
|
* allowed.
|
||||||
|
*
|
||||||
|
* This test asserts this allowed behavior per https://github.com/jwtk/jjwt/issues/905
|
||||||
|
* @see <a href="https://github.com/jwtk/jjwt/issues/905">JJWT Issue 905</a>
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void testAllowedKeyLengths() {
|
||||||
|
|
||||||
|
def parser = Jwks.parser().build()
|
||||||
|
|
||||||
|
for (MacAlgorithm alg : macAlgs()) {
|
||||||
|
|
||||||
|
// 3 key length sizes for each alg to test:
|
||||||
|
// index 0: smaller than minimum required
|
||||||
|
// index 1: minimum required
|
||||||
|
// index 2: more than minimum required:
|
||||||
|
def sizes = [alg.keyBitLength - Byte.SIZE, alg.keyBitLength, alg.keyBitLength + Byte.SIZE]
|
||||||
|
|
||||||
|
for (int i = 0; i < sizes.size(); i++) {
|
||||||
|
|
||||||
|
def kBitLength = sizes.get(i)
|
||||||
|
def k = Bytes.random(Bytes.length(kBitLength))
|
||||||
|
|
||||||
|
def jwkJson = """
|
||||||
|
{
|
||||||
|
"kid": "${UUID.randomUUID().toString()}",
|
||||||
|
"kty": "oct",
|
||||||
|
"alg": "${alg.getId()}",
|
||||||
|
"k": "${Encoders.BASE64URL.encode(k)}"
|
||||||
|
}""".toString()
|
||||||
|
|
||||||
|
def jwk
|
||||||
|
try {
|
||||||
|
jwk = parser.parse(jwkJson)
|
||||||
|
} catch (WeakKeyException expected) {
|
||||||
|
assertEquals("Should only occur on index 0 with less-than-minimum key length", 0, i)
|
||||||
|
String msg = "Secret JWK 'alg' (Algorithm) value is '${alg.getId()}', but the 'k' (Key Value) " +
|
||||||
|
"length is smaller than the ${alg.getId()} minimum length of " +
|
||||||
|
"${Bytes.bitsMsg(alg.keyBitLength)} required by " +
|
||||||
|
"[JWA RFC 7518, Section 3.2](https://www.rfc-editor.org/rfc/rfc7518.html#section-3.2), " +
|
||||||
|
"2nd paragraph: 'A key of the same size as the hash output or larger MUST be used with " +
|
||||||
|
"this algorithm.'"
|
||||||
|
assertEquals msg, expected.getMessage()
|
||||||
|
continue // expected for index 0 (purposefully weak key), so let loop continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// otherwise not weak, sizes should reflect equal-to or greater-than alg bitlength sizes
|
||||||
|
assert jwk instanceof SecretJwk
|
||||||
|
assertEquals alg.getId(), jwk.getAlgorithm()
|
||||||
|
def bytes = jwk.toKey().getEncoded()
|
||||||
|
assertTrue Bytes.bitLength(bytes) >= alg.keyBitLength
|
||||||
|
assertEquals Bytes.length(kBitLength), jwk.toKey().getEncoded().length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ import io.jsonwebtoken.lang.Collections
|
||||||
import io.jsonwebtoken.security.Jwks
|
import io.jsonwebtoken.security.Jwks
|
||||||
|
|
||||||
import javax.crypto.SecretKey
|
import javax.crypto.SecretKey
|
||||||
|
import javax.crypto.spec.SecretKeySpec
|
||||||
import java.security.KeyPair
|
import java.security.KeyPair
|
||||||
import java.security.PrivateKey
|
import java.security.PrivateKey
|
||||||
import java.security.Provider
|
import java.security.Provider
|
||||||
|
@ -42,6 +43,11 @@ class TestKeys {
|
||||||
static SecretKey HS512 = Jwts.SIG.HS512.key().build()
|
static SecretKey HS512 = Jwts.SIG.HS512.key().build()
|
||||||
static Collection<SecretKey> HS = Collections.setOf(HS256, HS384, HS512)
|
static Collection<SecretKey> HS = Collections.setOf(HS256, HS384, HS512)
|
||||||
|
|
||||||
|
static SecretKey NA256 = new SecretKeySpec(HS256.encoded, "NONE")
|
||||||
|
static SecretKey NA384 = new SecretKeySpec(HS384.encoded, "NONE")
|
||||||
|
static SecretKey NA512 = new SecretKeySpec(HS512.encoded, "NONE")
|
||||||
|
static Collection<SecretKey> NA = [NA256, NA384, NA512]
|
||||||
|
|
||||||
static SecretKey A128GCM, A192GCM, A256GCM, A128KW, A192KW, A256KW, A128GCMKW, A192GCMKW, A256GCMKW
|
static SecretKey A128GCM, A192GCM, A256GCM, A128KW, A192KW, A256KW, A128GCMKW, A192GCMKW, A256GCMKW
|
||||||
static Collection<SecretKey> AGCM
|
static Collection<SecretKey> AGCM
|
||||||
static {
|
static {
|
||||||
|
@ -59,6 +65,7 @@ class TestKeys {
|
||||||
static Collection<SecretKey> SECRET = new LinkedHashSet<>()
|
static Collection<SecretKey> SECRET = new LinkedHashSet<>()
|
||||||
static {
|
static {
|
||||||
SECRET.addAll(HS)
|
SECRET.addAll(HS)
|
||||||
|
SECRET.addAll(NA)
|
||||||
SECRET.addAll(AGCM)
|
SECRET.addAll(AGCM)
|
||||||
SECRET.addAll(ACBC)
|
SECRET.addAll(ACBC)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue