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);
+ }
+}