diff --git a/hutool-json/pom.xml b/hutool-json/pom.xml
index 8f6e7f401..786939070 100755
--- a/hutool-json/pom.xml
+++ b/hutool-json/pom.xml
@@ -59,6 +59,18 @@
2.0.25
test
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.11.5
+ test
+
+
+ io.jsonwebtoken
+ jjwt-gson
+ 0.11.5
+ test
+
diff --git a/hutool-json/src/main/java/org/dromara/hutool/json/jwt/signers/AsymmetricJWTSigner.java b/hutool-json/src/main/java/org/dromara/hutool/json/jwt/signers/AsymmetricJWTSigner.java
index 371758a8a..4dfb33834 100644
--- a/hutool-json/src/main/java/org/dromara/hutool/json/jwt/signers/AsymmetricJWTSigner.java
+++ b/hutool-json/src/main/java/org/dromara/hutool/json/jwt/signers/AsymmetricJWTSigner.java
@@ -70,16 +70,38 @@ public class AsymmetricJWTSigner implements JWTSigner {
@Override
public String sign(final String headerBase64, final String payloadBase64) {
- return Base64.encodeUrlSafe(sign.sign(StrUtil.format("{}.{}", headerBase64, payloadBase64)));
+ final String dataStr = StrUtil.format("{}.{}", headerBase64, payloadBase64);
+ return Base64.encodeUrlSafe(sign(ByteUtil.toBytes(dataStr, charset)));
+ }
+
+ /**
+ * 签名字符串数据
+ *
+ * @param data 数据
+ * @return 签名
+ */
+ protected byte[] sign(byte[] data) {
+ return sign.sign(data);
}
@Override
public boolean verify(final String headerBase64, final String payloadBase64, final String signBase64) {
- return sign.verify(
+ return verify(
ByteUtil.toBytes(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
public String getAlgorithm() {
return this.sign.getSignature().getAlgorithm();
diff --git a/hutool-json/src/main/java/org/dromara/hutool/json/jwt/signers/EllipticCurveJWTSigner.java b/hutool-json/src/main/java/org/dromara/hutool/json/jwt/signers/EllipticCurveJWTSigner.java
new file mode 100644
index 000000000..440983572
--- /dev/null
+++ b/hutool-json/src/main/java/org/dromara/hutool/json/jwt/signers/EllipticCurveJWTSigner.java
@@ -0,0 +1,197 @@
+package org.dromara.hutool.json.jwt.signers;
+
+import org.dromara.hutool.json.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(final String algorithm, final Key key) {
+ super(algorithm, key);
+ }
+
+ /**
+ * 构造
+ *
+ * @param algorithm 算法
+ * @param keyPair 密钥对
+ */
+ public EllipticCurveJWTSigner(final String algorithm, final 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);
+ }
+ }
+
+ /**
+ * DER格式转换为pair (R, S)
+ *
+ * @param derSignature DER格式签名
+ * @param outputLength 算法签名长度
+ * @return pair (R, S)
+ * @throws JWTException JWT异常
+ */
+ private static byte[] derToConcat(final byte[] derSignature, final 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;
+ }
+
+ /**
+ * pair (R, S)转换为DER格式
+ *
+ * @param jwsSignature JWT签名
+ * @return DER格式签名
+ */
+ private static byte[] concatToDER(final byte[] jwsSignature) {
+
+ final 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;
+ }
+
+ final 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-json/src/main/java/org/dromara/hutool/json/jwt/signers/JWTSignerUtil.java b/hutool-json/src/main/java/org/dromara/hutool/json/jwt/signers/JWTSignerUtil.java
index 4022a625b..5bc22fe69 100644
--- a/hutool-json/src/main/java/org/dromara/hutool/json/jwt/signers/JWTSignerUtil.java
+++ b/hutool-json/src/main/java/org/dromara/hutool/json/jwt/signers/JWTSignerUtil.java
@@ -13,6 +13,7 @@
package org.dromara.hutool.json.jwt.signers;
import org.dromara.hutool.core.lang.Assert;
+import org.dromara.hutool.core.regex.ReUtil;
import java.security.Key;
import java.security.KeyPair;
@@ -261,6 +262,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);
}
@@ -278,6 +285,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-json/src/test/java/org/dromara/hutool/json/jwt/Issue3205Test.java b/hutool-json/src/test/java/org/dromara/hutool/json/jwt/Issue3205Test.java
new file mode 100644
index 000000000..d3c3b7da1
--- /dev/null
+++ b/hutool-json/src/test/java/org/dromara/hutool/json/jwt/Issue3205Test.java
@@ -0,0 +1,37 @@
+package org.dromara.hutool.json.jwt;
+
+import io.jsonwebtoken.Jwts;
+import org.dromara.hutool.core.date.DateUtil;
+import org.dromara.hutool.crypto.KeyUtil;
+import org.dromara.hutool.json.jwt.signers.AlgorithmUtil;
+import org.dromara.hutool.json.jwt.signers.JWTSigner;
+import org.dromara.hutool.json.jwt.signers.JWTSignerUtil;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.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.of()
+ .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);
+
+ Assertions.assertTrue(signed);
+ }
+}