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