From d3864c14c3124c53dc9966981cbeae62d475c6bb Mon Sep 17 00:00:00 2001 From: Sergey Beryozkin Date: Thu, 20 Jan 2022 15:03:21 +0000 Subject: [PATCH] Allow to relax the JWT build keys verification --- doc/modules/ROOT/pages/generate-jwt.adoc | 2 + .../smallrye/jwt/build/impl/ImplMessages.java | 4 - .../jwt/build/impl/JwtBuildUtils.java | 2 + .../jwt/build/impl/JwtEncryptionImpl.java | 14 ++-- .../jwt/build/impl/JwtSignatureImpl.java | 9 ++- .../jwt/build/JwtBuildConfigSource.java | 22 ++++++ .../io/smallrye/jwt/build/JwtEncryptTest.java | 32 +++++++- .../io/smallrye/jwt/build/JwtSignTest.java | 75 +++++++++++++++++-- 8 files changed, 141 insertions(+), 19 deletions(-) diff --git a/doc/modules/ROOT/pages/generate-jwt.adoc b/doc/modules/ROOT/pages/generate-jwt.adoc index ae05ac22..f214c090 100644 --- a/doc/modules/ROOT/pages/generate-jwt.adoc +++ b/doc/modules/ROOT/pages/generate-jwt.adoc @@ -142,9 +142,11 @@ Smallrye JWT supports the following properties which can be used to customize th |smallrye.jwt.encrypt.key.location|none|Location of a key which will be used to encrypt the claims or inner JWT when a no-argument encrypt() method is called. |smallrye.jwt.encrypt.key|none|Key value which will be used to encrypt the claims or inner JWT when a no-argument encrypt() method is called. |smallrye.jwt.encrypt.key.id|none|Encryption key identifier which is checked only when JWK keys are used. +|smallrye.jwt.encrypt.relax-key-validation|false|Relax the validation of the encryption keys |smallrye.jwt.sign.key.location|none|Location of a key which will be used to sign the claims when either a no-argument sign() or innerSign() method is called. |smallrye.jwt.sign.key|none|Key value which will be used to sign the claims when either a no-argument sign() or innerSign() method is called. |smallrye.jwt.sign.key.id|none|Signing key identifier which is checked only when JWK keys are used. +|smallrye.jwt.sign.relax-key-validation|false|Relax the validation of the signing keys |smallrye.jwt.new-token.signature-algorithm|RS256|Signature algorithm. This property will be checked if the JWT signature builder has not already set the signature algorithm. |smallrye.jwt.new-token.key-encryption-algorithm|RSA-OAEP|Key encryption algorithm. This property will be checked if the JWT encryption builder has not already set the key encryption algorithm. |smallrye.jwt.new-token.content-encryption-algorithm|A256GCM|Content encryption algorithm. This property will be checked if the JWT encryption builder has not already set the content encryption algorithm. diff --git a/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/ImplMessages.java b/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/ImplMessages.java index 5b8b8e64..e80984f3 100644 --- a/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/ImplMessages.java +++ b/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/ImplMessages.java @@ -19,10 +19,6 @@ JwtSignatureException unsupportedSignatureAlgorithm(String algorithmName, JwtSignatureException unsupportedSignatureAlgorithm(String algorithmName); - @Message(id = 5001, value = "A key of size 2048 bits or larger MUST be used with " + - "the '%s' algorithm") - JwtEncryptionException encryptionKeySizeMustBeHigher(String algorithmName); - @Message(id = 5003, value = "%s") JwtEncryptionException joseSerializationError(String errorMessage, @Cause Throwable t); diff --git a/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/JwtBuildUtils.java b/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/JwtBuildUtils.java index 7a786b18..988933ba 100644 --- a/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/JwtBuildUtils.java +++ b/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/JwtBuildUtils.java @@ -18,9 +18,11 @@ public class JwtBuildUtils { public static final String SIGN_KEY_LOCATION_PROPERTY = "smallrye.jwt.sign.key.location"; public static final String SIGN_KEY_PROPERTY = "smallrye.jwt.sign.key"; public static final String SIGN_KEY_ID_PROPERTY = "smallrye.jwt.sign.key.id"; + public static final String SIGN_KEY_RELAX_VALIDATION_PROPERTY = "smallrye.jwt.sign.relax-key-validation"; public static final String ENC_KEY_LOCATION_PROPERTY = "smallrye.jwt.encrypt.key.location"; public static final String ENC_KEY_PROPERTY = "smallrye.jwt.encrypt.key"; public static final String ENC_KEY_ID_PROPERTY = "smallrye.jwt.encrypt.key.id"; + public static final String ENC_KEY_RELAX_VALIDATION_PROPERTY = "smallrye.jwt.encrypt.relax-key-validation"; public static final String NEW_TOKEN_ISSUER_PROPERTY = "smallrye.jwt.new-token.issuer"; public static final String NEW_TOKEN_AUDIENCE_PROPERTY = "smallrye.jwt.new-token.audience"; diff --git a/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/JwtEncryptionImpl.java b/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/JwtEncryptionImpl.java index 3a792f14..a1aff31b 100644 --- a/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/JwtEncryptionImpl.java +++ b/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/JwtEncryptionImpl.java @@ -142,12 +142,10 @@ private String encryptInternal(Key key) { String keyAlgorithm = getKeyEncryptionAlgorithm(key); jwe.setAlgorithmHeaderValue(keyAlgorithm); jwe.setEncryptionMethodHeaderParameter(getContentEncryptionAlgorithm()); - - if (key instanceof RSAPublicKey && keyAlgorithm.startsWith(KeyEncryptionAlgorithm.RSA_OAEP.getAlgorithm()) - && ((RSAPublicKey) key).getModulus().bitLength() < 2048) { - throw ImplMessages.msg.encryptionKeySizeMustBeHigher(keyAlgorithm); - } jwe.setKey(key); + if (isRelaxKeyValidation()) { + jwe.setDoKeyValidation(false); + } try { return jwe.getCompactSerialization(); } catch (org.jose4j.lang.JoseException ex) { @@ -155,6 +153,10 @@ private String encryptInternal(Key key) { } } + private boolean isRelaxKeyValidation() { + return JwtBuildUtils.getConfigProperty(JwtBuildUtils.ENC_KEY_RELAX_VALIDATION_PROPERTY, Boolean.class, false); + } + private String getKeyEncryptionAlgorithm(Key keyEncryptionKey) { String alg = (String) headers.get("alg"); if (alg == null) { @@ -220,7 +222,7 @@ private static String getKeyContentFromConfig() { String keyLocation = JwtBuildUtils.getConfigProperty(JwtBuildUtils.ENC_KEY_LOCATION_PROPERTY, String.class); if (keyLocation != null) { - return getKeyContentFromLocation(keyLocation); + return getKeyContentFromLocation(keyLocation.trim()); } String keyContent = JwtBuildUtils.getConfigProperty(JwtBuildUtils.ENC_KEY_PROPERTY, String.class); if (keyContent != null) { diff --git a/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/JwtSignatureImpl.java b/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/JwtSignatureImpl.java index 82339059..a7fc59ad 100644 --- a/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/JwtSignatureImpl.java +++ b/implementation/jwt-build/src/main/java/io/smallrye/jwt/build/impl/JwtSignatureImpl.java @@ -144,6 +144,9 @@ private String signInternal(Key signingKey) { jws.setPayload(claims.toJson()); jws.setKey(signingKey); + if (isRelaxKeyValidation()) { + jws.setDoKeyValidation(false); + } try { return jws.getCompactSerialization(); } catch (Exception ex) { @@ -151,6 +154,10 @@ private String signInternal(Key signingKey) { } } + private boolean isRelaxKeyValidation() { + return JwtBuildUtils.getConfigProperty(JwtBuildUtils.SIGN_KEY_RELAX_VALIDATION_PROPERTY, Boolean.class, false); + } + private String getSignatureAlgorithm(Key signingKey) { String alg = (String) headers.get("alg"); if (alg == null) { @@ -200,7 +207,7 @@ static String getKeyContentFromConfig() { String keyLocation = JwtBuildUtils.getConfigProperty(JwtBuildUtils.SIGN_KEY_LOCATION_PROPERTY, String.class); if (keyLocation != null) { - return getKeyContentFromLocation(keyLocation); + return getKeyContentFromLocation(keyLocation.trim()); } String keyContent = JwtBuildUtils.getConfigProperty(JwtBuildUtils.SIGN_KEY_PROPERTY, String.class); if (keyContent != null) { diff --git a/implementation/jwt-build/src/test/java/io/smallrye/jwt/build/JwtBuildConfigSource.java b/implementation/jwt-build/src/test/java/io/smallrye/jwt/build/JwtBuildConfigSource.java index 740ca141..34972485 100644 --- a/implementation/jwt-build/src/test/java/io/smallrye/jwt/build/JwtBuildConfigSource.java +++ b/implementation/jwt-build/src/test/java/io/smallrye/jwt/build/JwtBuildConfigSource.java @@ -15,6 +15,8 @@ public class JwtBuildConfigSource implements ConfigSource { private static final Set PROPERTY_NAMES = new HashSet<>(Arrays.asList( JwtBuildUtils.SIGN_KEY_ID_PROPERTY, + JwtBuildUtils.SIGN_KEY_RELAX_VALIDATION_PROPERTY, + JwtBuildUtils.ENC_KEY_RELAX_VALIDATION_PROPERTY, JwtBuildUtils.ENC_KEY_ID_PROPERTY, JwtBuildUtils.NEW_TOKEN_ISSUER_PROPERTY, JwtBuildUtils.NEW_TOKEN_AUDIENCE_PROPERTY, @@ -40,6 +42,9 @@ public class JwtBuildConfigSource implements ConfigSource { private boolean useSignKeyProperty; private boolean useEncryptionKeyProperty; + private boolean relaxSignatureKeyValidation; + private boolean relaxEncryptionKeyValidation; + @Override public Map getProperties() { Map map = new HashMap<>(); @@ -88,6 +93,14 @@ public Map getProperties() { map.put(JwtBuildUtils.NEW_TOKEN_AUDIENCE_PROPERTY, "https://custom-audience"); } + if (relaxSignatureKeyValidation) { + map.put(JwtBuildUtils.SIGN_KEY_RELAX_VALIDATION_PROPERTY, String.valueOf(relaxSignatureKeyValidation)); + } + + if (relaxEncryptionKeyValidation) { + map.put(JwtBuildUtils.ENC_KEY_RELAX_VALIDATION_PROPERTY, String.valueOf(relaxEncryptionKeyValidation)); + } + map.put(JwtBuildUtils.NEW_TOKEN_OVERRIDE_CLAIMS_PROPERTY, String.valueOf(overrideMatchingClaims)); return map; } @@ -182,4 +195,13 @@ public void setUseEncryptionKeyProperty(boolean useEncryptionKeyProperty) { this.useEncryptionKeyProperty = useEncryptionKeyProperty; } + public void setRelaxSignatureKeyValidation(boolean relax) { + this.relaxSignatureKeyValidation = relax; + + } + + public void setRelaxEncryptionKeyValidation(boolean relax) { + this.relaxEncryptionKeyValidation = relax; + } + } diff --git a/implementation/jwt-build/src/test/java/io/smallrye/jwt/build/JwtEncryptTest.java b/implementation/jwt-build/src/test/java/io/smallrye/jwt/build/JwtEncryptTest.java index 9e07fb62..c4fc173f 100644 --- a/implementation/jwt-build/src/test/java/io/smallrye/jwt/build/JwtEncryptTest.java +++ b/implementation/jwt-build/src/test/java/io/smallrye/jwt/build/JwtEncryptTest.java @@ -21,6 +21,7 @@ import java.nio.charset.StandardCharsets; import java.security.Key; +import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.PrivateKey; import java.security.PublicKey; @@ -192,7 +193,7 @@ public void testEncryptWithRsaOaep256Configured() throws Exception { } @Test - public void testEncryptWithInvalidRSAKey() throws Exception { + public void testEncryptWithShortRSAKey() throws Exception { KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); keyPairGenerator.initialize(1024); PublicKey key = keyPairGenerator.generateKeyPair().getPublic(); @@ -200,11 +201,30 @@ public void testEncryptWithInvalidRSAKey() throws Exception { Jwt.claims().jwe().encrypt(key); Assert.fail("JwtEncryptionException is expected due to the invalid key size"); } catch (JwtEncryptionException ex) { - Assert.assertEquals("SRJWT05001: A key of size 2048 bits or larger MUST be used with the 'RSA-OAEP' algorithm", + Assert.assertEquals( + "SRJWT05003: An RSA key of size 2048 bits or larger MUST be used with the all JOSE RSA algorithms (given key was only 1024 bits).", ex.getMessage()); } } + @Test + public void testEncryptWithShortRSAKeyAndRelaxedValidation() throws Exception { + KeyPair keyPair = KeyUtils.generateKeyPair(1024); + + JwtBuildConfigSource configSource = JwtSignTest.getConfigSource(); + configSource.setRelaxEncryptionKeyValidation(true); + try { + String jwt = Jwt.claims(Collections.singletonMap("customClaim", "custom-value")) + .jwe().encrypt(keyPair.getPublic()); + + JsonWebEncryption jwe = getJsonWebEncryption(jwt, keyPair.getPrivate(), true); + JwtClaims claims = JwtClaims.parse(jwe.getPlaintextString()); + checkJwtClaims(claims); + } finally { + configSource.setRelaxEncryptionKeyValidation(false); + } + } + @Test public void testEncryptWithEcKey() throws Exception { EllipticCurveJsonWebKey jwk = createECJwk(); @@ -402,9 +422,17 @@ private static JsonWebEncryption getJsonWebEncryption(String compactJwe) throws } private static JsonWebEncryption getJsonWebEncryption(String compactJwe, Key decryptionKey) throws Exception { + return getJsonWebEncryption(compactJwe, decryptionKey, false); + } + + private static JsonWebEncryption getJsonWebEncryption(String compactJwe, Key decryptionKey, boolean relaxKeyValidation) + throws Exception { JsonWebEncryption jwe = new JsonWebEncryption(); jwe.setCompactSerialization(compactJwe); jwe.setKey(decryptionKey); + if (relaxKeyValidation) { + jwe.setDoKeyValidation(false); + } return jwe; } diff --git a/implementation/jwt-build/src/test/java/io/smallrye/jwt/build/JwtSignTest.java b/implementation/jwt-build/src/test/java/io/smallrye/jwt/build/JwtSignTest.java index 4a4ffc33..7dbe3ff7 100644 --- a/implementation/jwt-build/src/test/java/io/smallrye/jwt/build/JwtSignTest.java +++ b/implementation/jwt-build/src/test/java/io/smallrye/jwt/build/JwtSignTest.java @@ -22,7 +22,7 @@ import java.net.URL; import java.nio.charset.StandardCharsets; import java.security.Key; -import java.security.KeyPairGenerator; +import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; import java.time.Duration; @@ -279,12 +279,10 @@ private void verifySignedJsonObject(String jwt) throws Exception { } @Test - public void testSignWithInvalidRSAKey() throws Exception { - KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); - keyPairGenerator.initialize(1024); - PrivateKey key = keyPairGenerator.generateKeyPair().getPrivate(); + public void testSignWithShortRSAKey() throws Exception { + KeyPair keyPair = KeyUtils.generateKeyPair(1024); try { - Jwt.claims().sign(key); + Jwt.claims().sign(keyPair.getPrivate()); Assert.fail("JwtSignatureException is expected due to the invalid key size"); } catch (JwtSignatureException ex) { Assert.assertEquals( @@ -293,6 +291,28 @@ public void testSignWithInvalidRSAKey() throws Exception { } } + @Test + public void testSignWithShortRSAKeyAndRelaxedValidation() throws Exception { + KeyPair keyPair = KeyUtils.generateKeyPair(1024); + + JwtBuildConfigSource configSource = getConfigSource(); + configSource.setRelaxSignatureKeyValidation(true); + try { + String jwt = Jwt.claims(Collections.singletonMap("customClaim", "custom-value")) + .sign(keyPair.getPrivate()); + + JsonWebSignature jws = getVerifiedJws(jwt, keyPair.getPublic(), true); + JwtClaims claims = JwtClaims.parse(jws.getPayload()); + + Assert.assertEquals(4, claims.getClaimsMap().size()); + checkDefaultClaimsAndHeaders(getJwsHeaders(jwt, 2), claims); + + Assert.assertEquals("custom-value", claims.getClaimValue("customClaim")); + } finally { + configSource.setRelaxSignatureKeyValidation(false); + } + } + @Test public void testSignClaimsConfiguredKeyLocation() throws Exception { JwtBuildConfigSource configSource = getConfigSource(); @@ -389,9 +409,16 @@ private static JsonWebSignature getVerifiedJws(String jwt) throws Exception { } static JsonWebSignature getVerifiedJws(String jwt, Key key) throws Exception { + return getVerifiedJws(jwt, key, false); + } + + static JsonWebSignature getVerifiedJws(String jwt, Key key, boolean relaxKeyValidation) throws Exception { JsonWebSignature jws = new JsonWebSignature(); jws.setKey(key); jws.setCompactSerialization(jwt); + if (relaxKeyValidation) { + jws.setDoKeyValidation(false); + } Assert.assertTrue(jws.verifySignature()); return jws; } @@ -564,6 +591,42 @@ public void testSignClaimsWithSecret() throws Exception { Assert.assertEquals("custom-value", claims.getClaimValue("customClaim")); } + @Test + public void testSignClaimsWithShortSecret() throws Exception { + String secret = "AyM1SysPpbyDfgZld3umj1qzKObw"; + + JwtSignatureException thrown = assertThrows("JwtSignatureException is expected", + JwtSignatureException.class, () -> Jwt.claims().claim("customClaim", "custom-value").signWithSecret(secret)); + Assert.assertEquals( + "A key of the same size as the hash output (i.e. 256 bits for HS256) or larger MUST be used with the HMAC SHA" + + " algorithms but this key is only 224 bits", + thrown.getCause().getMessage()); + } + + @Test + public void testSignClaimsWithShortSecretAndRelaxedValidation() throws Exception { + String secret = "AyM1SysPpbyDfgZld3umj1qzKObw"; + + JwtBuildConfigSource configSource = getConfigSource(); + configSource.setRelaxSignatureKeyValidation(true); + try { + String jwt = Jwt.claims() + .claim("customClaim", "custom-value") + .signWithSecret(secret); + + SecretKey secretKey = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "AES"); + JsonWebSignature jws = getVerifiedJws(jwt, secretKey, true); + JwtClaims claims = JwtClaims.parse(jws.getPayload()); + + Assert.assertEquals(4, claims.getClaimsMap().size()); + checkDefaultClaimsAndHeaders(getJwsHeaders(jwt, 2), claims, "HS256", 300); + + Assert.assertEquals("custom-value", claims.getClaimValue("customClaim")); + } finally { + configSource.setRelaxSignatureKeyValidation(false); + } + } + @Test public void testSignClaimsJwkSymmetricKey() throws Exception { JwtBuildConfigSource configSource = getConfigSource();