From aab5ba5098ebdd5a7cd2824ba732000e2058ef33 Mon Sep 17 00:00:00 2001 From: Looly Date: Tue, 18 Jul 2023 02:44:58 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8DJWTSignerUtil=E4=B8=ADES256?= =?UTF-8?q?=E7=AD=BE=E5=90=8D=E4=B8=8D=E7=AC=A6=E5=90=88=E8=A7=84=E8=8C=83?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + hutool-jwt/pom.xml | 12 ++ .../jwt/signers/AsymmetricJWTSigner.java | 30 ++- .../jwt/signers/EllipticCurveJWTSigner.java | 182 ++++++++++++++++++ .../cn/hutool/jwt/signers/JWTSignerUtil.java | 12 ++ .../java/cn/hutool/jwt/Issue3205Test.java | 37 ++++ 6 files changed, 270 insertions(+), 4 deletions(-) create mode 100644 hutool-jwt/src/main/java/cn/hutool/jwt/signers/EllipticCurveJWTSigner.java create mode 100644 hutool-jwt/src/test/java/cn/hutool/jwt/Issue3205Test.java diff --git a/CHANGELOG.md b/CHANGELOG.md index cd3c11516..0b8122ab9 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,7 @@ * 【core 】 修复BeanUtil.isCommonFieldsEqual判空导致的问题 * 【extra 】 修复CompressUtil.createArchiver 将文件压缩为tgz时文件名规则无效问题(issue#I7LLL7@Gitee) * 【core 】 修复脱敏银行卡号长度bug(pr#3210@Github) +* 【jwt 】 修复JWTSignerUtil中ES256签名不符合规范问题(issue#3205@Github) ------------------------------------------------------------------------------------------------------------- # 5.8.20(2023-06-16) diff --git a/hutool-jwt/pom.xml b/hutool-jwt/pom.xml index 37ad0f8b3..d8d8f441a 100755 --- a/hutool-jwt/pom.xml +++ b/hutool-jwt/pom.xml @@ -41,5 +41,17 @@ ${bouncycastle.version} test + + io.jsonwebtoken + jjwt-impl + 0.11.5 + test + + + io.jsonwebtoken + jjwt-gson + 0.11.5 + test + diff --git a/hutool-jwt/src/main/java/cn/hutool/jwt/signers/AsymmetricJWTSigner.java b/hutool-jwt/src/main/java/cn/hutool/jwt/signers/AsymmetricJWTSigner.java index 4d9a4cb34..74f9c4cbd 100755 --- a/hutool-jwt/src/main/java/cn/hutool/jwt/signers/AsymmetricJWTSigner.java +++ b/hutool-jwt/src/main/java/cn/hutool/jwt/signers/AsymmetricJWTSigner.java @@ -57,14 +57,36 @@ public class AsymmetricJWTSigner implements JWTSigner { @Override public String sign(String headerBase64, String payloadBase64) { - return Base64.encodeUrlSafe(sign.sign(StrUtil.format("{}.{}", headerBase64, payloadBase64))); + final String dataStr = StrUtil.format("{}.{}", headerBase64, payloadBase64); + return Base64.encodeUrlSafe(sign(StrUtil.bytes(dataStr, charset))); + } + + /** + * 签名字符串数据 + * + * @param data 数据 + * @return 签名 + */ + protected byte[] sign(byte[] data) { + return sign.sign(data); } @Override public boolean verify(String headerBase64, String payloadBase64, String signBase64) { - return sign.verify( - StrUtil.bytes(StrUtil.format("{}.{}", headerBase64, payloadBase64), charset), - Base64.decode(signBase64)); + return verify( + StrUtil.bytes(StrUtil.format("{}.{}", headerBase64, payloadBase64), charset), + Base64.decode(signBase64)); + } + + /** + * 验签数据 + * + * @param data 数据 + * @param signed 签名 + * @return 是否通过 + */ + protected boolean verify(byte[] data, byte[] signed) { + return sign.verify(data, signed); } @Override diff --git a/hutool-jwt/src/main/java/cn/hutool/jwt/signers/EllipticCurveJWTSigner.java b/hutool-jwt/src/main/java/cn/hutool/jwt/signers/EllipticCurveJWTSigner.java new file mode 100644 index 000000000..dd88c292a --- /dev/null +++ b/hutool-jwt/src/main/java/cn/hutool/jwt/signers/EllipticCurveJWTSigner.java @@ -0,0 +1,182 @@ +package cn.hutool.jwt.signers; + +import cn.hutool.jwt.JWTException; + +import java.security.Key; +import java.security.KeyPair; + +/** + * 椭圆曲线(Elliptic Curve)的JWT签名器。
+ * 按照https://datatracker.ietf.org/doc/html/rfc7518#section-3.4,
+ * Elliptic Curve Digital Signature Algorithm (ECDSA)算法签名需要转换DER格式为pair (R, S) + * + * @author looly + * @since 5.8.21 + */ +public class EllipticCurveJWTSigner extends AsymmetricJWTSigner { + + /** + * 构造 + * + * @param algorithm 算法 + * @param key 密钥 + */ + public EllipticCurveJWTSigner(String algorithm, Key key) { + super(algorithm, key); + } + + /** + * 构造 + * + * @param algorithm 算法 + * @param keyPair 密钥对 + */ + public EllipticCurveJWTSigner(String algorithm, KeyPair keyPair) { + super(algorithm, keyPair); + } + + @Override + protected byte[] sign(final byte[] data) { + // https://datatracker.ietf.org/doc/html/rfc7518#section-3.4 + return derToConcat(super.sign(data), getSignatureByteArrayLength(getAlgorithm())); + } + + @Override + protected boolean verify(final byte[] data, final byte[] signed) { + // https://datatracker.ietf.org/doc/html/rfc7518#section-3.4 + return super.verify(data, concatToDER(signed)); + } + + /** + * 获取签名长度 + * @param alg 算法 + * @return 长度 + * @throws JWTException JWT异常 + */ + private static int getSignatureByteArrayLength(final String alg) throws JWTException { + switch (alg) { + case "ES256": + case "SHA256withECDSA": + return 64; + case "ES384": + case "SHA384withECDSA": + return 96; + case "ES512": + case "SHA512withECDSA": + return 132; + default: + throw new JWTException("Unsupported Algorithm: {}", alg); + } + } + + private static byte[] derToConcat(final byte[] derSignature, int outputLength) throws JWTException { + + if (derSignature.length < 8 || derSignature[0] != 48) { + throw new JWTException("Invalid ECDSA signature format"); + } + + final int offset; + if (derSignature[1] > 0) { + offset = 2; + } else if (derSignature[1] == (byte) 0x81) { + offset = 3; + } else { + throw new JWTException("Invalid ECDSA signature format"); + } + + final byte rLength = derSignature[offset + 1]; + + int i = rLength; + while ((i > 0) && (derSignature[(offset + 2 + rLength) - i] == 0)) { + i--; + } + + final byte sLength = derSignature[offset + 2 + rLength + 1]; + + int j = sLength; + while ((j > 0) && (derSignature[(offset + 2 + rLength + 2 + sLength) - j] == 0)) { + j--; + } + + int rawLen = Math.max(i, j); + rawLen = Math.max(rawLen, outputLength / 2); + + if ((derSignature[offset - 1] & 0xff) != derSignature.length - offset + || (derSignature[offset - 1] & 0xff) != 2 + rLength + 2 + sLength + || derSignature[offset] != 2 + || derSignature[offset + 2 + rLength] != 2) { + throw new JWTException("Invalid ECDSA signature format"); + } + + final byte[] concatSignature = new byte[2 * rawLen]; + + System.arraycopy(derSignature, (offset + 2 + rLength) - i, concatSignature, rawLen - i, i); + System.arraycopy(derSignature, (offset + 2 + rLength + 2 + sLength) - j, concatSignature, 2 * rawLen - j, j); + + return concatSignature; + } + + private static byte[] concatToDER(byte[] jwsSignature) throws ArrayIndexOutOfBoundsException { + + int rawLen = jwsSignature.length / 2; + + int i = rawLen; + + while ((i > 0) && (jwsSignature[rawLen - i] == 0)) { + i--; + } + + int j = i; + + if (jwsSignature[rawLen - i] < 0) { + j += 1; + } + + int k = rawLen; + + while ((k > 0) && (jwsSignature[2 * rawLen - k] == 0)) { + k--; + } + + int l = k; + + if (jwsSignature[2 * rawLen - k] < 0) { + l += 1; + } + + int len = 2 + j + 2 + l; + + if (len > 255) { + throw new JWTException("Invalid ECDSA signature format"); + } + + int offset; + + final byte[] derSignature; + + if (len < 128) { + derSignature = new byte[2 + 2 + j + 2 + l]; + offset = 1; + } else { + derSignature = new byte[3 + 2 + j + 2 + l]; + derSignature[1] = (byte) 0x81; + offset = 2; + } + + derSignature[0] = 48; + derSignature[offset++] = (byte) len; + derSignature[offset++] = 2; + derSignature[offset++] = (byte) j; + + System.arraycopy(jwsSignature, rawLen - i, derSignature, (offset + j) - i, i); + + offset += j; + + derSignature[offset++] = 2; + derSignature[offset++] = (byte) l; + + System.arraycopy(jwsSignature, 2 * rawLen - k, derSignature, (offset + l) - k, k); + + return derSignature; + } +} diff --git a/hutool-jwt/src/main/java/cn/hutool/jwt/signers/JWTSignerUtil.java b/hutool-jwt/src/main/java/cn/hutool/jwt/signers/JWTSignerUtil.java index 0d631a08d..9829b7daf 100755 --- a/hutool-jwt/src/main/java/cn/hutool/jwt/signers/JWTSignerUtil.java +++ b/hutool-jwt/src/main/java/cn/hutool/jwt/signers/JWTSignerUtil.java @@ -1,6 +1,7 @@ package cn.hutool.jwt.signers; import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ReUtil; import java.security.Key; import java.security.KeyPair; @@ -252,6 +253,12 @@ public class JWTSignerUtil { if (null == algorithmId || NoneJWTSigner.ID_NONE.equals(algorithmId)) { return none(); } + + // issue3205@Github + if(ReUtil.isMatch("es\\d{3}", algorithmId.toLowerCase())){ + return new EllipticCurveJWTSigner(AlgorithmUtil.getAlgorithm(algorithmId), keyPair); + } + return new AsymmetricJWTSigner(AlgorithmUtil.getAlgorithm(algorithmId), keyPair); } @@ -269,6 +276,11 @@ public class JWTSignerUtil { return NoneJWTSigner.NONE; } if (key instanceof PrivateKey || key instanceof PublicKey) { + // issue3205@Github + if(ReUtil.isMatch("ES\\d{3}", algorithmId)){ + return new EllipticCurveJWTSigner(algorithmId, key); + } + return new AsymmetricJWTSigner(AlgorithmUtil.getAlgorithm(algorithmId), key); } return new HMacJWTSigner(AlgorithmUtil.getAlgorithm(algorithmId), key); diff --git a/hutool-jwt/src/test/java/cn/hutool/jwt/Issue3205Test.java b/hutool-jwt/src/test/java/cn/hutool/jwt/Issue3205Test.java new file mode 100644 index 000000000..b24957d34 --- /dev/null +++ b/hutool-jwt/src/test/java/cn/hutool/jwt/Issue3205Test.java @@ -0,0 +1,37 @@ +package cn.hutool.jwt; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.crypto.KeyUtil; +import cn.hutool.jwt.signers.AlgorithmUtil; +import cn.hutool.jwt.signers.JWTSigner; +import cn.hutool.jwt.signers.JWTSignerUtil; +import io.jsonwebtoken.Jwts; +import org.junit.Assert; +import org.junit.Test; + +import java.security.KeyPair; + +/** + *https://github.com/dromara/hutool/issues/3205 + */ +public class Issue3205Test { + @Test + public void es256Test() { + final String id = "es256"; + final KeyPair keyPair = KeyUtil.generateKeyPair(AlgorithmUtil.getAlgorithm(id)); + final JWTSigner signer = JWTSignerUtil.createSigner(id, keyPair); + + final JWT jwt = JWT.create() + .setPayload("sub", "1234567890") + .setPayload("name", "looly") + .setPayload("admin", true) + .setExpiresAt(DateUtil.tomorrow()) + .setSigner(signer); + + final String token = jwt.sign(); + + final boolean signed = Jwts.parserBuilder().setSigningKey(keyPair.getPublic()).build().isSigned(token); + + Assert.assertTrue(signed); + } +}