From c673b76ef5b74ddaf51b4e80fc632ddb62b7e29a Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Tue, 2 Apr 2024 19:23:24 -0400 Subject: [PATCH 1/8] Update SECURITY.md Reflect latest 0.12.x version support --- SECURITY.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index ae1236f3b..96eff597f 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -11,8 +11,8 @@ We ask that all users or security researchers upgrade to the latest stable relea | Version | Supported | | -------- | ------------------ | -| 0.11.x | :white_check_mark: | -| < 0.11.0 | :x: | +| 0.12.x | :white_check_mark: | +| < 0.12.0 | :x: | ## Reporting Security Issues From 23d9a33ff64f11342e7b603f300bebbcf9d5da8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikko=20Nyl=C3=A9n?= Date: Mon, 22 Apr 2024 22:49:06 +0300 Subject: [PATCH 2/8] Allow using GenericSecret for HmacSHA* (#935) * Extended the pre-existing check for SUN PKCS11 generic secret to allow `SecretKey`s with `getAlgorithm()` names starting with `Generic` (e.g. `GenericSecret` and `Generic Secret`) as valid keys for `HmacSHA*` algorithm name checks in `DefaultMacAlgorithm`. This matches at least with the Sun PKCS11 and AWS CloudHSM JCE providers, but likely others as well. --- .../impl/security/DefaultMacAlgorithm.java | 8 +-- .../impl/security/KeysBridge.java | 16 +++--- .../security/DefaultMacAlgorithmTest.groovy | 26 ++++++++++ .../impl/security/KeysBridgeTest.groovy | 50 +++++++++++++++++++ 4 files changed, 90 insertions(+), 10 deletions(-) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMacAlgorithm.java b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMacAlgorithm.java index 28ecdfb86..e92277aed 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMacAlgorithm.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/DefaultMacAlgorithm.java @@ -135,12 +135,12 @@ private void assertAlgorithmName(SecretKey key, boolean signing) { throw new InvalidKeyException(msg); } - // We can ignore PKCS11 key name assertions because HSM module key algorithm names don't always align with - // JCA standard algorithm names: - boolean pkcs11Key = KeysBridge.isSunPkcs11GenericSecret(key); + // We can ignore key name assertions for generic secrets, because HSM module key algorithm names + // don't always align with JCA standard algorithm names + boolean generic = KeysBridge.isGenericSecret(key); //assert key's jca name is valid if it's a JWA standard algorithm: - if (!pkcs11Key && isJwaStandard() && !isJwaStandardJcaName(name)) { + if (!generic && isJwaStandard() && !isJwaStandardJcaName(name)) { throw new InvalidKeyException("The " + keyType(signing) + " key's algorithm '" + name + "' does not equal a valid HmacSHA* algorithm name or PKCS12 OID and cannot be used with " + getId() + "."); diff --git a/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java b/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java index a3602619a..869fab940 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/security/KeysBridge.java @@ -34,8 +34,9 @@ @SuppressWarnings({"unused"}) // reflection bridge class for the io.jsonwebtoken.security.Keys implementation public final class KeysBridge { - private static final String SUNPKCS11_GENERIC_SECRET_CLASSNAME = "sun.security.pkcs11.P11Key$P11SecretKey"; - private static final String SUNPKCS11_GENERIC_SECRET_ALGNAME = "Generic Secret"; // https://github.com/openjdk/jdk/blob/4f90abaf17716493bad740dcef76d49f16d69379/src/jdk.crypto.cryptoki/share/classes/sun/security/pkcs11/P11KeyStore.java#L1292 + // Some HSMs use generic secrets. This prefix matches the generic secret algorithm name + // used by SUN PKCS#11 provider, AWS CloudHSM JCE provider and possibly other HSMs + private static final String GENERIC_SECRET_ALG_PREFIX = "Generic"; // prevent instantiation private KeysBridge() { @@ -95,10 +96,13 @@ public static byte[] findEncoded(Key key) { return encoded; } - public static boolean isSunPkcs11GenericSecret(Key key) { - return key instanceof SecretKey && - key.getClass().getName().equals(SUNPKCS11_GENERIC_SECRET_CLASSNAME) && - SUNPKCS11_GENERIC_SECRET_ALGNAME.equals(key.getAlgorithm()); + public static boolean isGenericSecret(Key key) { + if (!(key instanceof SecretKey)) { + return false; + } + + String algName = Assert.hasText(key.getAlgorithm(), "Key algorithm cannot be null or empty."); + return algName.startsWith(GENERIC_SECRET_ALG_PREFIX); } /** diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultMacAlgorithmTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultMacAlgorithmTest.groovy index 184f502eb..6df8d95b7 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultMacAlgorithmTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/DefaultMacAlgorithmTest.groovy @@ -19,6 +19,7 @@ import io.jsonwebtoken.impl.io.Streams import io.jsonwebtoken.security.* import org.junit.Test +import javax.crypto.SecretKey import javax.crypto.spec.SecretKeySpec import java.nio.charset.StandardCharsets import java.security.Key @@ -232,4 +233,29 @@ class DefaultMacAlgorithmTest { assertSame mac, DefaultMacAlgorithm.findByKey(oidKey) } } + + /** + * Asserts that generic secrets are accepted + */ + @Test + void testValidateKeyAcceptsGenericSecret() { + def genericSecret = new SecretKey() { + @Override + String getAlgorithm() { + return 'GenericSecret' + } + + @Override + String getFormat() { + return "RAW" + } + + @Override + byte[] getEncoded() { + return Randoms.secureRandom().nextBytes(new byte[32]) + } + } + + newAlg().validateKey(genericSecret, true) + } } diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeysBridgeTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeysBridgeTest.groovy index 89d74beaa..9e62ff2c1 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeysBridgeTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/security/KeysBridgeTest.groovy @@ -17,9 +17,13 @@ package io.jsonwebtoken.impl.security import org.junit.Test +import javax.crypto.SecretKey import java.security.Key +import java.security.PrivateKey import static org.junit.Assert.assertEquals +import static org.junit.Assert.assertFalse +import static org.junit.Assert.assertTrue class KeysBridgeTest { @@ -56,4 +60,50 @@ class KeysBridgeTest { void testToStringPassword() { testFormattedOutput(new PasswordSpec("foo".toCharArray())) } + + @Test + void testIsGenericSecret() { + def secretKeyWithAlg = { alg -> + new SecretKey() { + @Override + String getAlgorithm() { + return alg + } + + @Override + String getFormat() { + return 'RAW' + } + + @Override + byte[] getEncoded() { + return new byte[0] + } + } + } + + PrivateKey genericPrivateKey = new PrivateKey() { + @Override + String getAlgorithm() { + return "Generic" + } + + @Override + String getFormat() { + return "RAW" + } + + @Override + byte[] getEncoded() { + return new byte[0] + } + } + + assertTrue KeysBridge.isGenericSecret(secretKeyWithAlg("GenericSecret")) + assertTrue KeysBridge.isGenericSecret(secretKeyWithAlg("Generic Secret")) + assertFalse KeysBridge.isGenericSecret(secretKeyWithAlg(" Generic")) + assertFalse KeysBridge.isGenericSecret(TestKeys.HS256) + assertFalse KeysBridge.isGenericSecret(TestKeys.A256GCM) + assertFalse KeysBridge.isGenericSecret(genericPrivateKey) + } } From 3489fdb19b6f9ec5d707e5286289db746aa2587d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mikko=20Nyl=C3=A9n?= Date: Thu, 25 Apr 2024 05:52:25 +0300 Subject: [PATCH 3/8] JWE arbitrary content compression (#937) Closes #938. * Fix compression being omitted for JWEs with arbitrary content The JWE content was compressed only when claims was used as payload, but when using arbitrary content, the compression was omitted, while keeping the "zip" header field, leading to decompression failing. * Refactor duplicate payload -> input stream logic from sign()/encrypt() * Preserve the content name * Fix name for JWE payload --- .../jsonwebtoken/impl/DefaultJwtBuilder.java | 30 +++--- .../groovy/io/jsonwebtoken/JwtsTest.groovy | 92 +++++++++++++++++++ 2 files changed, 106 insertions(+), 16 deletions(-) diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java index ef41c7aec..afa6a4804 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtBuilder.java @@ -595,14 +595,8 @@ private String sign(final Payload payload, final Key key, final Provider provide // Next, b64 extension requires the raw (non-encoded) payload to be included directly in the signing input, // so we ensure we have an input stream for that: - if (payload.isClaims() || payload.isCompressed()) { - ByteArrayOutputStream claimsOut = new ByteArrayOutputStream(8192); - writeAndClose("JWS Unencoded Payload", payload, claimsOut); - payloadStream = Streams.of(claimsOut.toByteArray()); - } else { - // No claims and not compressed, so just get the direct InputStream: - payloadStream = Assert.stateNotNull(payload.toInputStream(), "Payload InputStream cannot be null."); - } + payloadStream = toInputStream("JWS Unencoded Payload", payload); + if (!payload.isClaims()) { payloadStream = new CountingInputStream(payloadStream); // we'll need to assert if it's empty later } @@ -693,14 +687,7 @@ private String encrypt(final Payload content, final Key key, final Provider keyP Assert.stateNotNull(keyAlgFunction, "KeyAlgorithm function cannot be null."); assertPayloadEncoding("JWE"); - InputStream plaintext; - if (content.isClaims()) { - ByteArrayOutputStream out = new ByteArrayOutputStream(4096); - writeAndClose("JWE Claims", content, out); - plaintext = Streams.of(out.toByteArray()); - } else { - plaintext = content.toInputStream(); - } + InputStream plaintext = toInputStream("JWE Payload", content); //only expose (mutable) JweHeader functionality to KeyAlgorithm instances, not the full headerBuilder // (which exposes this JwtBuilder and shouldn't be referenced by KeyAlgorithms): @@ -820,4 +807,15 @@ private void encodeAndWrite(String name, byte[] data, OutputStream out) { Streams.writeAndClose(out, data, "Unable to write bytes"); } + private InputStream toInputStream(final String name, Payload payload) { + if (payload.isClaims() || payload.isCompressed()) { + ByteArrayOutputStream claimsOut = new ByteArrayOutputStream(8192); + writeAndClose(name, payload, claimsOut); + return Streams.of(claimsOut.toByteArray()); + } else { + // No claims and not compressed, so just get the direct InputStream: + return Assert.stateNotNull(payload.toInputStream(), "Payload InputStream cannot be null."); + } + } + } diff --git a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy index 2f1c432c8..5f6b82229 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/JwtsTest.groovy @@ -22,6 +22,7 @@ import io.jsonwebtoken.impl.io.Streams import io.jsonwebtoken.impl.lang.Bytes import io.jsonwebtoken.impl.lang.Services import io.jsonwebtoken.impl.security.* +import io.jsonwebtoken.io.CompressionAlgorithm import io.jsonwebtoken.io.Decoders import io.jsonwebtoken.io.Deserializer import io.jsonwebtoken.io.Encoders @@ -1398,6 +1399,97 @@ class JwtsTest { } } + @Test + void testJweCompressionWithArbitraryContentString() { + def codecs = [Jwts.ZIP.DEF, Jwts.ZIP.GZIP] + + for (CompressionAlgorithm zip : codecs) { + + for (AeadAlgorithm enc : Jwts.ENC.get().values()) { + + SecretKey key = enc.key().build() + + String payload = 'hello, world!' + + // encrypt and compress: + String jwe = Jwts.builder() + .content(payload) + .compressWith(zip) + .encryptWith(key, enc) + .compact() + + //decompress and decrypt: + def jwt = Jwts.parser() + .decryptWith(key) + .build() + .parseEncryptedContent(jwe) + assertEquals payload, new String(jwt.getPayload(), StandardCharsets.UTF_8) + } + } + } + + @Test + void testJweCompressionWithArbitraryContentByteArray() { + def codecs = [Jwts.ZIP.DEF, Jwts.ZIP.GZIP] + + for (CompressionAlgorithm zip : codecs) { + + for (AeadAlgorithm enc : Jwts.ENC.get().values()) { + + SecretKey key = enc.key().build() + + byte[] payload = new byte[14]; + Randoms.secureRandom().nextBytes(payload) + + // encrypt and compress: + String jwe = Jwts.builder() + .content(payload) + .compressWith(zip) + .encryptWith(key, enc) + .compact() + + //decompress and decrypt: + def jwt = Jwts.parser() + .decryptWith(key) + .build() + .parseEncryptedContent(jwe) + assertArrayEquals payload, jwt.getPayload() + } + } + } + + @Test + void testJweCompressionWithArbitraryContentInputStream() { + def codecs = [Jwts.ZIP.DEF, Jwts.ZIP.GZIP] + + for (CompressionAlgorithm zip : codecs) { + + for (AeadAlgorithm enc : Jwts.ENC.get().values()) { + + SecretKey key = enc.key().build() + + byte[] payloadBytes = new byte[14]; + Randoms.secureRandom().nextBytes(payloadBytes) + + ByteArrayInputStream payload = new ByteArrayInputStream(payloadBytes) + + // encrypt and compress: + String jwe = Jwts.builder() + .content(payload) + .compressWith(zip) + .encryptWith(key, enc) + .compact() + + //decompress and decrypt: + def jwt = Jwts.parser() + .decryptWith(key) + .build() + .parseEncryptedContent(jwe) + assertArrayEquals payloadBytes, jwt.getPayload() + } + } + } + @Test void testPasswordJwes() { From 754324879d55a374856e9e5d48e028b906fa293a Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 15 May 2024 11:03:42 -0700 Subject: [PATCH 4/8] Bump org.bouncycastle:bcpkix-jdk18on from 1.76 to 1.78 (#943) Bumps [org.bouncycastle:bcpkix-jdk18on](https://github.com/bcgit/bc-java) from 1.76 to 1.78. - [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html) - [Commits](https://github.com/bcgit/bc-java/commits) --- updated-dependencies: - dependency-name: org.bouncycastle:bcpkix-jdk18on dependency-type: direct:production ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 286e663c7..18c41732f 100644 --- a/pom.xml +++ b/pom.xml @@ -116,7 +116,7 @@ - 1.76 + 1.78 bcprov-jdk18on bcpkix-jdk18on From a7de55435bdf2c9c2d615b998b0179176d9ce333 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Sun, 16 Jun 2024 16:05:45 -0700 Subject: [PATCH 5/8] Fixes #947 (#948) --- CHANGELOG.md | 8 ++++ .../java/io/jsonwebtoken/impl/DefaultJws.java | 7 ++-- .../jsonwebtoken/impl/DefaultJwtParser.java | 38 ++++++++++--------- .../jsonwebtoken/impl/DefaultJwsTest.groovy | 11 +++++- 4 files changed, 41 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 70c53ede3..deca6a46d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,13 @@ ## Release Notes +### 0.12.6 + +This patch release: + +* Ensures that after successful JWS signature verification, an application-configured Base64Url `Decoder` output is + used to construct a `Jws` instance (instead of JJWT's default decoder). See + [Issue 947](https://github.com/jwtk/jjwt/issues/947). + ### 0.12.5 This patch release: diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJws.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJws.java index 0bf0fdf8b..dadaa51d3 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJws.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJws.java @@ -18,7 +18,6 @@ import io.jsonwebtoken.Jws; import io.jsonwebtoken.JwsHeader; import io.jsonwebtoken.JwtVisitor; -import io.jsonwebtoken.io.Decoders; public class DefaultJws

extends DefaultProtectedJwt implements Jws

{ @@ -26,9 +25,9 @@ public class DefaultJws

extends DefaultProtectedJwt implements private final String signature; - public DefaultJws(JwsHeader header, P payload, String signature) { - super(header, payload, Decoders.BASE64URL.decode(signature), DIGEST_NAME); - this.signature = signature; + public DefaultJws(JwsHeader header, P payload, byte[] signature, String b64UrlSig) { + super(header, payload, signature, DIGEST_NAME); + this.signature = b64UrlSig; } @Override diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index beb36f4bb..ad2faad66 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -265,8 +265,8 @@ private static boolean hasContentType(Header header) { return header != null && Strings.hasText(header.getContentType()); } - private void verifySignature(final TokenizedJwt tokenized, final JwsHeader jwsHeader, final String alg, - @SuppressWarnings("deprecation") SigningKeyResolver resolver, Claims claims, Payload payload) { + private byte[] verifySignature(final TokenizedJwt tokenized, final JwsHeader jwsHeader, final String alg, + @SuppressWarnings("deprecation") SigningKeyResolver resolver, Claims claims, Payload payload) { Assert.notNull(resolver, "SigningKeyResolver instance cannot be null."); @@ -354,6 +354,8 @@ private void verifySignature(final TokenizedJwt tokenized, final JwsHeader jwsHe } finally { Streams.reset(payloadStream); } + + return signature; } @Override @@ -485,7 +487,7 @@ private void verifySignature(final TokenizedJwt tokenized, final JwsHeader jwsHe } byte[] iv = null; - byte[] tag = null; + byte[] digest = null; // either JWE AEAD tag or JWS signature after Base64Url-decoding if (tokenized instanceof TokenizedJwe) { TokenizedJwe tokenizedJwe = (TokenizedJwe) tokenized; @@ -521,8 +523,8 @@ private void verifySignature(final TokenizedJwt tokenized, final JwsHeader jwsHe base64Url = base64UrlDigest; //guaranteed to be non-empty via the `alg` + digest check above: Assert.hasText(base64Url, "JWE AAD Authentication Tag cannot be null or empty."); - tag = decode(base64Url, "JWE AAD Authentication Tag"); - if (Bytes.isEmpty(tag)) { + digest = decode(base64Url, "JWE AAD Authentication Tag"); + if (Bytes.isEmpty(digest)) { String msg = "Compact JWE strings must always contain an AAD Authentication Tag."; throw new MalformedJwtException(msg); } @@ -564,7 +566,7 @@ private void verifySignature(final TokenizedJwt tokenized, final JwsHeader jwsHe // TODO: add encProvider(Provider) builder method that applies to this request only? InputStream ciphertext = payload.toInputStream(); ByteArrayOutputStream plaintext = new ByteArrayOutputStream(8192); - DecryptAeadRequest dreq = new DefaultDecryptAeadRequest(ciphertext, cek, aad, iv, tag); + DecryptAeadRequest dreq = new DefaultDecryptAeadRequest(ciphertext, cek, aad, iv, digest); encAlg.decrypt(dreq, plaintext); payload = new Payload(plaintext.toByteArray(), header.getContentType()); @@ -574,7 +576,7 @@ private void verifySignature(final TokenizedJwt tokenized, final JwsHeader jwsHe // not using a signing key resolver, so we can verify the signature before reading the payload, which is // always safer: JwsHeader jwsHeader = Assert.stateIsInstance(JwsHeader.class, header, "Not a JwsHeader. "); - verifySignature(tokenized, jwsHeader, alg, new LocatingKeyResolver(this.keyLocator), null, payload); + digest = verifySignature(tokenized, jwsHeader, alg, new LocatingKeyResolver(this.keyLocator), null, payload); integrityVerified = true; // no exception means signature verified } @@ -635,26 +637,28 @@ private void verifySignature(final TokenizedJwt tokenized, final JwsHeader jwsHe } } + // =============== Post-SKR Signature Check ================= + if (hasDigest && signingKeyResolver != null) { // TODO: remove for 1.0 + // A SigningKeyResolver has been configured, and due to it's API, we have to verify the signature after + // parsing the body. This can be a security risk, so it needs to be removed before 1.0 + JwsHeader jwsHeader = Assert.stateIsInstance(JwsHeader.class, header, "Not a JwsHeader. "); + digest = verifySignature(tokenized, jwsHeader, alg, this.signingKeyResolver, claims, payload); + //noinspection UnusedAssignment + integrityVerified = true; // no exception means verified successfully + } + Jwt jwt; Object body = claims != null ? claims : payloadBytes; if (header instanceof JweHeader) { - jwt = new DefaultJwe<>((JweHeader) header, body, iv, tag); + jwt = new DefaultJwe<>((JweHeader) header, body, iv, digest); } else if (hasDigest) { JwsHeader jwsHeader = Assert.isInstanceOf(JwsHeader.class, header, "JwsHeader required."); - jwt = new DefaultJws<>(jwsHeader, body, base64UrlDigest.toString()); + jwt = new DefaultJws<>(jwsHeader, body, digest, base64UrlDigest.toString()); } else { //noinspection rawtypes jwt = new DefaultJwt(header, body); } - // =============== Signature ================= - if (hasDigest && signingKeyResolver != null) { // TODO: remove for 1.0 - // A SigningKeyResolver has been configured, and due to it's API, we have to verify the signature after - // parsing the body. This can be a security risk, so it needs to be removed before 1.0 - JwsHeader jwsHeader = Assert.stateIsInstance(JwsHeader.class, header, "Not a JwsHeader. "); - verifySignature(tokenized, jwsHeader, alg, this.signingKeyResolver, claims, payload); - } - final boolean allowSkew = this.allowedClockSkewMillis > 0; //since 0.3: diff --git a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy index a55e550e0..1511eb103 100644 --- a/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy +++ b/impl/src/test/groovy/io/jsonwebtoken/impl/DefaultJwsTest.groovy @@ -17,8 +17,12 @@ package io.jsonwebtoken.impl import io.jsonwebtoken.JwsHeader import io.jsonwebtoken.Jwts +import io.jsonwebtoken.impl.lang.Bytes +import io.jsonwebtoken.io.Encoders import org.junit.Test +import java.security.MessageDigest + import static org.junit.Assert.* class DefaultJwsTest { @@ -26,10 +30,13 @@ class DefaultJwsTest { @Test void testConstructor() { JwsHeader header = new DefaultJwsHeader([:]) - def jws = new DefaultJws(header, 'foo', 'sig') + byte[] sig = Bytes.random(32) + String b64u = Encoders.BASE64URL.encode(sig) + def jws = new DefaultJws(header, 'foo', sig, b64u) assertSame jws.getHeader(), header assertEquals jws.getPayload(), 'foo' - assertEquals jws.getSignature(), 'sig' + assertTrue MessageDigest.isEqual(sig, jws.getDigest()) + assertEquals b64u, jws.getSignature() } @Test From 0c2d96c2d03d2c15f6bf4789a4fca8a6e419f4d7 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Mon, 17 Jun 2024 12:25:09 -0700 Subject: [PATCH 6/8] Fixes #949 (#950) * Introduced try/finally block to guarantee consumable content InputStream is closed --- CHANGELOG.md | 1 + .../jsonwebtoken/impl/DefaultJwtParser.java | 71 ++++++++++--------- 2 files changed, 39 insertions(+), 33 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index deca6a46d..8a579ae19 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ This patch release: * Ensures that after successful JWS signature verification, an application-configured Base64Url `Decoder` output is used to construct a `Jws` instance (instead of JJWT's default decoder). See [Issue 947](https://github.com/jwtk/jjwt/issues/947). +* Fixes a decompression memory leak in concurrent/multi-threaded environments introduced in 0.12.0 when decompressing JWTs with a `zip` header of `GZIP`. See [Issue 949](https://github.com/jwtk/jjwt/issues/949). ### 0.12.5 diff --git a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java index ad2faad66..75faa419a 100644 --- a/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java +++ b/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java @@ -597,43 +597,48 @@ private byte[] verifySignature(final TokenizedJwt tokenized, final JwsHeader jws Claims claims = null; byte[] payloadBytes = payload.getBytes(); if (payload.isConsumable()) { - - InputStream in = payload.toInputStream(); - - if (!hasContentType(header)) { // If there is a content type set, then the application using JJWT is expected - // to convert the byte payload themselves based on this content type - // https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10 : - // - // "This parameter is ignored by JWS implementations; any processing of this - // parameter is performed by the JWS application." - // - Map claimsMap = null; - try { - // if deserialization fails, we'll need to rewind to convert to a byte array. So if - // mark/reset isn't possible, we'll need to buffer: - if (!in.markSupported()) { - in = new BufferedInputStream(in); - in.mark(0); - } - claimsMap = deserialize(new UncloseableInputStream(in) /* Don't close in case we need to rewind */, "claims"); - } catch (DeserializationException | MalformedJwtException ignored) { // not JSON, treat it as a byte[] + InputStream in = null; + try { + in = payload.toInputStream(); + + if (!hasContentType(header)) { // If there is a content type set, then the application using JJWT is expected + // to convert the byte payload themselves based on this content type + // https://www.rfc-editor.org/rfc/rfc7515.html#section-4.1.10 : + // + // "This parameter is ignored by JWS implementations; any processing of this + // parameter is performed by the JWS application." + // + Map claimsMap = null; + try { + // if deserialization fails, we'll need to rewind to convert to a byte array. So if + // mark/reset isn't possible, we'll need to buffer: + if (!in.markSupported()) { + in = new BufferedInputStream(in); + in.mark(0); + } + claimsMap = deserialize(new UncloseableInputStream(in) /* Don't close in case we need to rewind */, "claims"); + } catch (DeserializationException | + MalformedJwtException ignored) { // not JSON, treat it as a byte[] // String msg = "Invalid claims: " + e.getMessage(); // throw new MalformedJwtException(msg, e); - } finally { - Streams.reset(in); - } - if (claimsMap != null) { - try { - claims = new DefaultClaims(claimsMap); - } catch (Throwable t) { - String msg = "Invalid claims: " + t.getMessage(); - throw new MalformedJwtException(msg); + } finally { + Streams.reset(in); + } + if (claimsMap != null) { + try { + claims = new DefaultClaims(claimsMap); + } catch (Throwable t) { + String msg = "Invalid claims: " + t.getMessage(); + throw new MalformedJwtException(msg); + } } } - } - if (claims == null) { - // consumable, but not claims, so convert to byte array: - payloadBytes = Streams.bytes(in, "Unable to convert payload to byte array."); + if (claims == null) { + // consumable, but not claims, so convert to byte array: + payloadBytes = Streams.bytes(in, "Unable to convert payload to byte array."); + } + } finally { // always ensure closed per https://github.com/jwtk/jjwt/issues/949 + Objects.nullSafeClose(in); } } From d14f27b624e6510fe41ce3e834cb3d026e3d5c4d Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 17 Jun 2024 12:51:20 -0700 Subject: [PATCH 7/8] Bump org.bouncycastle:bcprov-jdk18on from 1.76 to 1.78 (#941) * Bumps [org.bouncycastle:bcprov-jdk18on](https://github.com/bcgit/bc-java) from 1.76 to 1.78. - [Changelog](https://github.com/bcgit/bc-java/blob/main/docs/releasenotes.html) - [Commits](https://github.com/bcgit/bc-java/commits) updated-dependencies: - dependency-name: org.bouncycastle:bcprov-jdk18on dependency-type: direct:production --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a579ae19..d91c55774 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ This patch release: used to construct a `Jws` instance (instead of JJWT's default decoder). See [Issue 947](https://github.com/jwtk/jjwt/issues/947). * Fixes a decompression memory leak in concurrent/multi-threaded environments introduced in 0.12.0 when decompressing JWTs with a `zip` header of `GZIP`. See [Issue 949](https://github.com/jwtk/jjwt/issues/949). +* Upgrades BouncyCastle to 1.78. ### 0.12.5 From 5812f63a76084914c5b653025bd3b84048389223 Mon Sep 17 00:00:00 2001 From: Les Hazlewood <121180+lhazlewood@users.noreply.github.com> Date: Fri, 21 Jun 2024 13:12:01 -0700 Subject: [PATCH 8/8] 0.12.6 release (#951) Releasing 0.12.6 Additional changes from existing main branch: - Updated README.adoc `:project-version:` to be `0.12.6`. - Updated CHANGELOG.md change notes to link to the BC upgrade PR. - [maven-release-plugin] prepare release 0.12.6 - [maven-release-plugin] prepare for next development iteration --- CHANGELOG.md | 2 +- README.adoc | 2 +- api/pom.xml | 2 +- extensions/gson/pom.xml | 2 +- extensions/jackson/pom.xml | 2 +- extensions/orgjson/pom.xml | 2 +- extensions/pom.xml | 2 +- impl/pom.xml | 2 +- pom.xml | 2 +- tdjar/pom.xml | 2 +- 10 files changed, 10 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d91c55774..7a8fb137a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ This patch release: used to construct a `Jws` instance (instead of JJWT's default decoder). See [Issue 947](https://github.com/jwtk/jjwt/issues/947). * Fixes a decompression memory leak in concurrent/multi-threaded environments introduced in 0.12.0 when decompressing JWTs with a `zip` header of `GZIP`. See [Issue 949](https://github.com/jwtk/jjwt/issues/949). -* Upgrades BouncyCastle to 1.78. +* Upgrades BouncyCastle to 1.78 via [PR 941](https://github.com/jwtk/jjwt/pull/941). ### 0.12.5 diff --git a/README.adoc b/README.adoc index a3f31e2ae..a6ddf003c 100644 --- a/README.adoc +++ b/README.adoc @@ -1,6 +1,6 @@ :doctype: book = Java JWT: JSON Web Token for Java and Android -:project-version: 0.12.5 +:project-version: 0.12.6 :toc: :toc-title: :toc-placement!: diff --git a/api/pom.xml b/api/pom.xml index 56a1f7753..fac58aa6e 100644 --- a/api/pom.xml +++ b/api/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.6-SNAPSHOT + 0.12.7-SNAPSHOT ../pom.xml diff --git a/extensions/gson/pom.xml b/extensions/gson/pom.xml index 6a7dcac58..35e458e27 100644 --- a/extensions/gson/pom.xml +++ b/extensions/gson/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.6-SNAPSHOT + 0.12.7-SNAPSHOT ../../pom.xml diff --git a/extensions/jackson/pom.xml b/extensions/jackson/pom.xml index 5d2d66fd3..469789f61 100644 --- a/extensions/jackson/pom.xml +++ b/extensions/jackson/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.6-SNAPSHOT + 0.12.7-SNAPSHOT ../../pom.xml diff --git a/extensions/orgjson/pom.xml b/extensions/orgjson/pom.xml index f531f128c..5d9da307f 100644 --- a/extensions/orgjson/pom.xml +++ b/extensions/orgjson/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.6-SNAPSHOT + 0.12.7-SNAPSHOT ../../pom.xml diff --git a/extensions/pom.xml b/extensions/pom.xml index bbc28b53f..a302e93b2 100644 --- a/extensions/pom.xml +++ b/extensions/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.6-SNAPSHOT + 0.12.7-SNAPSHOT ../pom.xml diff --git a/impl/pom.xml b/impl/pom.xml index e9fe705d4..4d10f5581 100644 --- a/impl/pom.xml +++ b/impl/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.6-SNAPSHOT + 0.12.7-SNAPSHOT ../pom.xml diff --git a/pom.xml b/pom.xml index 18c41732f..9827e61e6 100644 --- a/pom.xml +++ b/pom.xml @@ -19,7 +19,7 @@ io.jsonwebtoken jjwt-root - 0.12.6-SNAPSHOT + 0.12.7-SNAPSHOT JJWT JSON Web Token support for the JVM and Android pom diff --git a/tdjar/pom.xml b/tdjar/pom.xml index 43e8bd207..be4f4e3b6 100644 --- a/tdjar/pom.xml +++ b/tdjar/pom.xml @@ -21,7 +21,7 @@ io.jsonwebtoken jjwt-root - 0.12.6-SNAPSHOT + 0.12.7-SNAPSHOT ../pom.xml