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.2.1](https://datatracker.ietf.org/doc/html/rfc7518#section-6.2.2.1), respectively.
|
||||
[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!
|
||||
|
||||
### 0.12.3
|
||||
|
|
|
@ -30,6 +30,7 @@ import javax.crypto.Cipher;
|
|||
import javax.crypto.SecretKey;
|
||||
import javax.crypto.spec.GCMParameterSpec;
|
||||
import javax.crypto.spec.IvParameterSpec;
|
||||
import javax.crypto.spec.SecretKeySpec;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.security.SecureRandom;
|
||||
|
@ -54,9 +55,22 @@ abstract class AesAlgorithm extends CryptoAlgorithm implements KeyBuilderSupplie
|
|||
protected final int tagBitLength;
|
||||
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) {
|
||||
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.gcm = jcaTransformation.startsWith("AES/GCM");
|
||||
this.ivBitLength = jcaTransformation.equals("AESWrap") ? 0 : (this.gcm ? GCM_IV_SIZE : BLOCK_SIZE);
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
*/
|
||||
package io.jsonwebtoken.impl.security;
|
||||
|
||||
import io.jsonwebtoken.Identifiable;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.impl.lang.Bytes;
|
||||
import io.jsonwebtoken.impl.lang.ParameterReadable;
|
||||
|
@ -22,11 +23,14 @@ import io.jsonwebtoken.impl.lang.RequiredParameterReader;
|
|||
import io.jsonwebtoken.io.Encoders;
|
||||
import io.jsonwebtoken.lang.Assert;
|
||||
import io.jsonwebtoken.lang.Strings;
|
||||
import io.jsonwebtoken.security.AeadAlgorithm;
|
||||
import io.jsonwebtoken.security.InvalidKeyException;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import io.jsonwebtoken.security.MacAlgorithm;
|
||||
import io.jsonwebtoken.security.MalformedKeyException;
|
||||
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.spec.SecretKeySpec;
|
||||
|
@ -44,61 +48,97 @@ class SecretJwkFactory extends AbstractFamilyJwkFactory<SecretKey, SecretJwk> {
|
|||
protected SecretJwk createJwkFromKey(JwkContext<SecretKey> ctx) {
|
||||
SecretKey key = Assert.notNull(ctx.getKey(), "JwkContext key cannot be null.");
|
||||
String k;
|
||||
byte[] encoded = null;
|
||||
try {
|
||||
byte[] encoded = KeysBridge.getEncoded(key);
|
||||
encoded = KeysBridge.getEncoded(key);
|
||||
k = Encoders.BASE64URL.encode(encoded);
|
||||
Assert.hasText(k, "k value cannot be null or empty.");
|
||||
} catch (Throwable t) {
|
||||
String msg = "Unable to encode SecretKey to JWK: " + t.getMessage();
|
||||
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);
|
||||
|
||||
return new DefaultSecretJwk(ctx);
|
||||
return createJwkFromValues(ctx);
|
||||
}
|
||||
|
||||
private static void assertKeyBitLength(byte[] bytes, MacAlgorithm alg) {
|
||||
long bitLen = Bytes.bitLength(bytes);
|
||||
long requiredBitLen = alg.getKeyBitLength();
|
||||
if (bitLen != requiredBitLen) {
|
||||
if (bitLen < requiredBitLen) {
|
||||
// Implementors note: Don't print out any information about the `bytes` value itself - size,
|
||||
// content, etc., as it is considered secret material:
|
||||
String msg = "Secret JWK " + AbstractJwk.ALG + " value is '" + alg.getId() +
|
||||
"', but the " + DefaultSecretJwk.K + " length does not equal the '" + alg.getId() +
|
||||
"' length requirement of " + Bytes.bitsMsg(requiredBitLen) +
|
||||
". This discrepancy could be the result of an algorithm " +
|
||||
"substitution attack or simply an erroneously constructed JWK. In either case, it is likely " +
|
||||
"to result in unexpected or undesired security consequences.";
|
||||
throw new MalformedKeyException(msg);
|
||||
"', but the " + DefaultSecretJwk.K + " length is smaller than the " + alg.getId() +
|
||||
" minimum length of " + Bytes.bitsMsg(requiredBitLen) +
|
||||
" 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.'";
|
||||
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
|
||||
protected SecretJwk createJwkFromValues(JwkContext<SecretKey> ctx) {
|
||||
ParameterReadable reader = new RequiredParameterReader(ctx);
|
||||
byte[] bytes = reader.get(DefaultSecretJwk.K);
|
||||
String jcaName = null;
|
||||
final byte[] bytes = reader.get(DefaultSecretJwk.K);
|
||||
SecretKey key;
|
||||
|
||||
String id = ctx.getAlgorithm();
|
||||
if (Strings.hasText(id)) {
|
||||
SecureDigestAlgorithm<?, ?> alg = Jwts.SIG.get().get(id);
|
||||
if (alg instanceof MacAlgorithm) {
|
||||
jcaName = ((CryptoAlgorithm) alg).getJcaName(); // valid for all JJWT alg implementations
|
||||
Assert.hasText(jcaName, "Algorithm jcaName cannot be null or empty.");
|
||||
assertKeyBitLength(bytes, (MacAlgorithm) alg);
|
||||
}
|
||||
}
|
||||
if (!Strings.hasText(jcaName)) {
|
||||
if (ctx.isSigUse()) {
|
||||
String algId = ctx.getAlgorithm();
|
||||
if (!Strings.hasText(algId)) { // optional per https://www.rfc-editor.org/rfc/rfc7517.html#section-4.4
|
||||
|
||||
// Here we try to infer the best type of key to create based on siguse and/or key length.
|
||||
//
|
||||
// AES requires 128, 192, or 256 bits, so anything larger than 256 cannot be AES, so we'll need to assume
|
||||
// HMAC.
|
||||
//
|
||||
// Also, 256 bits works for either HMAC or AES, so we just have to choose one as there is no other
|
||||
// RFC-based criteria for determining. Historically, we've chosen AES due to the larger number of
|
||||
// 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:
|
||||
jcaName = "HmacSHA" + Bytes.bitLength(bytes);
|
||||
} else { // not an HS* algorithm, and all standard AeadAlgorithms use AES keys:
|
||||
jcaName = AesAlgorithm.KEY_ALG_NAME;
|
||||
key = Keys.hmacShaKeyFor(bytes);
|
||||
} else {
|
||||
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);
|
||||
return new DefaultSecretJwk(ctx);
|
||||
}
|
||||
|
|
|
@ -29,10 +29,8 @@ import static org.junit.Assert.*
|
|||
|
||||
class AbstractJwkBuilderTest {
|
||||
|
||||
private static final SecretKey SKEY = TestKeys.A256GCM
|
||||
|
||||
private static AbstractJwkBuilder<SecretKey, SecretJwk, AbstractJwkBuilder> builder() {
|
||||
return (AbstractJwkBuilder) Jwks.builder().key(SKEY)
|
||||
return (AbstractJwkBuilder) Jwks.builder().key(TestKeys.NA256)
|
||||
}
|
||||
|
||||
@Test
|
||||
|
|
|
@ -97,7 +97,7 @@ class JwkSerializationTest {
|
|||
|
||||
static void testSecretJwk(Serializer ser, Deserializer des) {
|
||||
|
||||
def key = TestKeys.A128GCM
|
||||
def key = TestKeys.NA256
|
||||
def jwk = Jwks.builder().key(key).id('id').build()
|
||||
assertWrapped(jwk, ['k'])
|
||||
|
||||
|
|
|
@ -40,7 +40,7 @@ import static org.junit.Assert.*
|
|||
|
||||
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 String srandom() {
|
||||
|
@ -172,7 +172,7 @@ class JwksTest {
|
|||
@Test
|
||||
void testOperations() {
|
||||
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()
|
||||
}
|
||||
|
||||
|
|
|
@ -15,9 +15,10 @@
|
|||
*/
|
||||
package io.jsonwebtoken.impl.security
|
||||
|
||||
import io.jsonwebtoken.security.Jwks
|
||||
import io.jsonwebtoken.security.MalformedKeyException
|
||||
import io.jsonwebtoken.security.SecretJwk
|
||||
import io.jsonwebtoken.Jwts
|
||||
import io.jsonwebtoken.impl.lang.Bytes
|
||||
import io.jsonwebtoken.io.Encoders
|
||||
import io.jsonwebtoken.security.*
|
||||
import org.junit.Test
|
||||
|
||||
import static org.junit.Assert.*
|
||||
|
@ -30,10 +31,14 @@ import static org.junit.Assert.*
|
|||
*/
|
||||
class SecretJwkFactoryTest {
|
||||
|
||||
private static Set<MacAlgorithm> macAlgs() {
|
||||
return Jwts.SIG.get().values().findAll({ it -> it instanceof MacAlgorithm }) as Collection<MacAlgorithm>
|
||||
}
|
||||
|
||||
@Test
|
||||
// if a jwk does not have an 'alg' or 'use' param, we default to an AES key
|
||||
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
|
||||
assertEquals 'AES', result.toKey().getAlgorithm()
|
||||
}
|
||||
|
@ -47,7 +52,7 @@ class SecretJwkFactoryTest {
|
|||
|
||||
@Test
|
||||
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
|
||||
assertNull result.getAlgorithm()
|
||||
assertNull result.get('use')
|
||||
|
@ -63,7 +68,7 @@ class SecretJwkFactoryTest {
|
|||
|
||||
@Test
|
||||
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
|
||||
assertNull result.getAlgorithm()
|
||||
assertNull result.get('use')
|
||||
|
@ -79,7 +84,7 @@ class SecretJwkFactoryTest {
|
|||
|
||||
@Test
|
||||
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
|
||||
assertNull result.getAlgorithm()
|
||||
assertNull result.get('use')
|
||||
|
@ -89,7 +94,7 @@ class SecretJwkFactoryTest {
|
|||
@Test
|
||||
// no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA256
|
||||
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('use')
|
||||
SecretJwk result = Jwks.builder().add(jwk).add('use', 'sig').build() as SecretJwk
|
||||
|
@ -99,7 +104,7 @@ class SecretJwkFactoryTest {
|
|||
@Test
|
||||
// no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA384
|
||||
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('use')
|
||||
SecretJwk result = Jwks.builder().add(jwk).add('use', 'sig').build() as SecretJwk
|
||||
|
@ -109,7 +114,7 @@ class SecretJwkFactoryTest {
|
|||
@Test
|
||||
// no 'alg' jwk property, but 'use' is 'sig', so forces jcaName to be HmacSHA512
|
||||
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('use')
|
||||
SecretJwk result = Jwks.builder().add(jwk).add('use', 'sig').build() as SecretJwk
|
||||
|
@ -119,20 +124,32 @@ class SecretJwkFactoryTest {
|
|||
@Test
|
||||
// no 'alg' jwk property, but 'use' is something other than 'sig', so jcaName should default to AES
|
||||
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('use')
|
||||
SecretJwk result = Jwks.builder().add(jwk).add('use', 'foo').build() as SecretJwk
|
||||
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
|
||||
* requirements. This would be a malformed key.
|
||||
*/
|
||||
@Test
|
||||
void testSizeMismatchedSecretJwk() {
|
||||
|
||||
//first get a valid HS256 JWK:
|
||||
SecretJwk validJwk = Jwks.builder().key(TestKeys.HS256).build()
|
||||
|
||||
|
@ -142,12 +159,72 @@ class SecretJwkFactoryTest {
|
|||
.add('alg', 'HS384')
|
||||
.build()
|
||||
fail()
|
||||
} catch (MalformedKeyException expected) {
|
||||
String msg = "Secret JWK 'alg' (Algorithm) value is 'HS384', but the 'k' (Key Value) length does " +
|
||||
"not equal the 'HS384' length requirement of 384 bits (48 bytes). This discrepancy could " +
|
||||
"be the result of an algorithm substitution attack or simply an erroneously constructed " +
|
||||
"JWK. In either case, it is likely to result in unexpected or undesired security consequences."
|
||||
} catch (WeakKeyException expected) {
|
||||
String msg = "Secret JWK 'alg' (Algorithm) value is 'HS384', but the 'k' (Key Value) length is smaller " +
|
||||
"than the HS384 minimum length of 384 bits (48 bytes) 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()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 javax.crypto.SecretKey
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
import java.security.KeyPair
|
||||
import java.security.PrivateKey
|
||||
import java.security.Provider
|
||||
|
@ -42,6 +43,11 @@ class TestKeys {
|
|||
static SecretKey HS512 = Jwts.SIG.HS512.key().build()
|
||||
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 Collection<SecretKey> AGCM
|
||||
static {
|
||||
|
@ -59,6 +65,7 @@ class TestKeys {
|
|||
static Collection<SecretKey> SECRET = new LinkedHashSet<>()
|
||||
static {
|
||||
SECRET.addAll(HS)
|
||||
SECRET.addAll(NA)
|
||||
SECRET.addAll(AGCM)
|
||||
SECRET.addAll(ACBC)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue