ES256 JWT 验证 - SignatureException:签名编码无效:java.io.IOException:序列标签错误
ES256 JWT validation - SignatureException: invalid encoding for signature: java.io.IOException: Sequence tag error
我有一个使用椭圆曲线 ES256 签名的 JWT,我正在尝试验证它:
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMzQifQ.eyJpc3MiOiJodHRwczovL2p3dC1pZHAuZXhhbXBsZS5jb20iLCJzdWIiOiJtYWlsdG86bWlrZUBleGFtcGxlLmNvbSIsIm5iZiI6MTU3NTY1NjY3NCwiZXhwIjoxOTI0OTkxOTk5LCJpYXQiOjE1NzU2NTY2NzQsImp0aSI6ImlkMTIzNDU2IiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9.kcj0QQrERKIfny1TfHY-Z9iDFazr84xCssTDuXtV1n1dvY7CYXuP5ZBvpi9ArOQjsS8YCd0bKsWaQ-17VnF_1A
使用这个 public 键:
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoBUyo8CQAFPeYPvv78ylh5MwFZjT
CLQeb042TjiMJxG+9DLFmRSMlBQ9T/RsLLc+PmpB1+7yPAR+oR5gZn3kJQ==
-----END PUBLIC KEY-----
这只是
{
"kty": "EC",
"use": "sig",
"crv": "P-256",
"kid": "1234",
"x": "oBUyo8CQAFPeYPvv78ylh5MwFZjTCLQeb042TjiMJxE=",
"y": "vvQyxZkUjJQUPU/0bCy3Pj5qQdfu8jwEfqEeYGZ95CU=",
"alg": "ES256"
}
它在 https://jwt.io/ 上正确验证,但是当我尝试使用本机 Java 验证它时,它会引发错误:
java.security.SignatureException: invalid encoding for signature: java.io.IOException: Sequence tag error
at com.ibm.crypto.provider.AbstractSHAwithECDSA.engineVerify(Unknown Source)
at java.security.Signature$Delegate.engineVerify(Signature.java:1219)
at java.security.Signature.verify(Signature.java:652)
at curam.platforminfrastructure.outbound.oauth2.impl.ValidateJWT.validateEllipticCurve(ValidateJWT.java:235)
at curam.platforminfrastructure.outbound.oauth2.impl.ValidateJWT.validateJWT(ValidateJWT.java:88)
at curam.platforminfrastructure.outbound.oauth2.impl.ValidateJWT.testJWT(ValidateJWT.java:53)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:76)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner.run(ParentRunner.java:193)
at org.junit.runners.ParentRunner.schedule(ParentRunner.java:52)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
at org.junit.runners.ParentRunner.access[=14=]0(ParentRunner.java:42)
at org.junit.runners.ParentRunner.evaluate(ParentRunner.java:184)
at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:89)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:41)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:541)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:763)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:463)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:209)
我的代码如下:
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.math.BigInteger;
import java.security.AlgorithmParameters;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.util.Base64;
import java.util.Base64.Decoder;
import java.util.Base64.Encoder;
public class ValidateJWT {
final Decoder urlDecoder = Base64.getUrlDecoder();
final Encoder urlEncoder = Base64.getUrlEncoder();
final Decoder decoder = Base64.getDecoder();
final Encoder encoder = Base64.getEncoder();
final ObjectMapper objectMapper = new ObjectMapper();
public boolean validateEllipticCurve(final String JWT, final String PUBLICKEY)
throws IOException {
final String[] at_arr = JWT.split("\.");
final String headerB64u = at_arr[0];
final String payloadB64u = at_arr[1];
final String signed_data = headerB64u + "." + payloadB64u;
final byte[] signature = urlDecoder.decode(at_arr[2]);
final byte[] at_headerJSON = urlDecoder.decode(headerB64u);
final JsonNode pkRoot = objectMapper.readTree(PUBLICKEY);
final String xStr = pkRoot.get("x").textValue();
final String yStr = pkRoot.get("y").textValue();
final byte xArr[] = decoder.decode(xStr);
final byte yArr[] = decoder.decode(yStr);
final BigInteger x = new BigInteger(xArr);
final BigInteger y = new BigInteger(yArr);
try {
final AlgorithmParameters parameters = AlgorithmParameters
.getInstance("EC");
parameters.init(new ECGenParameterSpec("secp256k1"));
final ECParameterSpec ecParameterSpec = parameters
.getParameterSpec(ECParameterSpec.class);
final KeyFactory keyFactory = KeyFactory.getInstance("EC");
final ECPoint ecPoint = new ECPoint(x, y);
final ECPublicKeySpec keySpec = new ECPublicKeySpec(ecPoint,
ecParameterSpec);
final ECPublicKey publicKey = (ECPublicKey) keyFactory
.generatePublic(keySpec);
final JsonNode key_root = objectMapper.readTree(at_headerJSON);
final String alg = key_root.get("alg").textValue();
Signature dataVerifyingInstance = null;
switch (alg) {
case "ES256":
dataVerifyingInstance = Signature.getInstance("SHA256withECDSA");
break;
case "ES384":
dataVerifyingInstance = Signature.getInstance("SHA384withECDSA");
break;
case "ES512":
dataVerifyingInstance = Signature.getInstance("SHA512withECDSA");
break;
}
dataVerifyingInstance.initVerify(publicKey);
dataVerifyingInstance.update(signed_data.getBytes());
final boolean verification = dataVerifyingInstance.verify(signature);
return verification;
} catch (final SignatureException | InvalidKeySpecException
| InvalidKeyException | InvalidParameterSpecException
| NoSuchAlgorithmException ex) {
ex.printStackTrace();
return false;
}
}
}
最后一行似乎引发了异常。
final boolean verification = dataVerifyingInstance.verify(signature);// <== here
我不确定是我使用了错误的曲线(secp256k1、secp256r1、secp256v1)导致了这个问题,还是可能是其他原因。任何提示将不胜感激。
问题是为 JWS 计算的签名(在椭圆曲线的情况下)是 R
和 S
值的原始串联,如 RFC7515 中指定。所以这是 32 + 32 = 64 字节(在 256 位曲线的情况下)。
但是 Java 实现期望此签名采用 DER 序列形式。这就是您遇到此异常的原因 - 只是您的签名(R
和 S
值)未经过 DER 编码。您甚至可以检查负责解码签名 (sun.security.ec.ECDSASignature::decodeSignature
) 和编码签名(用于签名操作 - sun.security.ec.ECDSASignature::encodeSignature
)的代码。据您在源代码中看到的,他们使用 sun.ec
包中的内部 类 进行 DER 编码。不鼓励您自己使用这些 类。
您最好的选择是将原始签名转换为 DER 编码签名。如果您愿意,可以使用开箱即用的 DER 编码处理的 BouncyCastle
。这是将您的 EC 原始签名转换为 DER 编码签名的方法:
private static byte[] toDerSignature(byte[] jwsSig) throws IOException {
byte[] rBytes = Arrays.copyOfRange(jwsSig, 0, 32);
byte[] sBytes = Arrays.copyOfRange(jwsSig, 32, 64);
BigInteger r = new BigInteger(1, rBytes);
BigInteger s = new BigInteger(1, sBytes);
DERSequence sequence = new DERSequence(new ASN1Encodable[] {
new ASN1Integer(r),
new ASN1Integer(s)
});
return sequence.getDEREncoded();
}
并使用此签名进行验证。
它可能是特定于 JVM 的,但我找到了一个更简单的解决方案:
而不是
dataVerifyingInstance = Signature.getInstance("SHA256withECDSA");
使用
dataVerifyingInstance = Signature.getInstance("SHA256withECDSAinP1363Format");
在这种情况下,实现将只使用您的原始签名,否则它将不得不从 DER 格式解码回来:
sun.security.ec.ECDSASignature
protected boolean engineVerify(byte[] signature) throws SignatureException {
...
byte[] sig;
if (p1363Format) {
sig = signature; //with SHA256withECDSAinP1363Format
} else {
sig = decodeSignature(signature); //with SHA256withECDSA
}
...
}
我有一个使用椭圆曲线 ES256 签名的 JWT,我正在尝试验证它:
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMzQifQ.eyJpc3MiOiJodHRwczovL2p3dC1pZHAuZXhhbXBsZS5jb20iLCJzdWIiOiJtYWlsdG86bWlrZUBleGFtcGxlLmNvbSIsIm5iZiI6MTU3NTY1NjY3NCwiZXhwIjoxOTI0OTkxOTk5LCJpYXQiOjE1NzU2NTY2NzQsImp0aSI6ImlkMTIzNDU2IiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9.kcj0QQrERKIfny1TfHY-Z9iDFazr84xCssTDuXtV1n1dvY7CYXuP5ZBvpi9ArOQjsS8YCd0bKsWaQ-17VnF_1A
使用这个 public 键:
-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoBUyo8CQAFPeYPvv78ylh5MwFZjT
CLQeb042TjiMJxG+9DLFmRSMlBQ9T/RsLLc+PmpB1+7yPAR+oR5gZn3kJQ==
-----END PUBLIC KEY-----
这只是
{
"kty": "EC",
"use": "sig",
"crv": "P-256",
"kid": "1234",
"x": "oBUyo8CQAFPeYPvv78ylh5MwFZjTCLQeb042TjiMJxE=",
"y": "vvQyxZkUjJQUPU/0bCy3Pj5qQdfu8jwEfqEeYGZ95CU=",
"alg": "ES256"
}
它在 https://jwt.io/ 上正确验证,但是当我尝试使用本机 Java 验证它时,它会引发错误:
java.security.SignatureException: invalid encoding for signature: java.io.IOException: Sequence tag error
at com.ibm.crypto.provider.AbstractSHAwithECDSA.engineVerify(Unknown Source)
at java.security.Signature$Delegate.engineVerify(Signature.java:1219)
at java.security.Signature.verify(Signature.java:652)
at curam.platforminfrastructure.outbound.oauth2.impl.ValidateJWT.validateEllipticCurve(ValidateJWT.java:235)
at curam.platforminfrastructure.outbound.oauth2.impl.ValidateJWT.validateJWT(ValidateJWT.java:88)
at curam.platforminfrastructure.outbound.oauth2.impl.ValidateJWT.testJWT(ValidateJWT.java:53)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.runners.model.FrameworkMethod.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:76)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner.run(ParentRunner.java:193)
at org.junit.runners.ParentRunner.schedule(ParentRunner.java:52)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
at org.junit.runners.ParentRunner.access[=14=]0(ParentRunner.java:42)
at org.junit.runners.ParentRunner.evaluate(ParentRunner.java:184)
at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
at org.eclipse.jdt.internal.junit4.runner.JUnit4TestReference.run(JUnit4TestReference.java:89)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:41)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:541)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:763)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:463)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:209)
我的代码如下:
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.io.IOException;
import java.math.BigInteger;
import java.security.AlgorithmParameters;
import java.security.InvalidKeyException;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.Signature;
import java.security.SignatureException;
import java.security.interfaces.ECPublicKey;
import java.security.spec.ECGenParameterSpec;
import java.security.spec.ECParameterSpec;
import java.security.spec.ECPoint;
import java.security.spec.ECPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.InvalidParameterSpecException;
import java.util.Base64;
import java.util.Base64.Decoder;
import java.util.Base64.Encoder;
public class ValidateJWT {
final Decoder urlDecoder = Base64.getUrlDecoder();
final Encoder urlEncoder = Base64.getUrlEncoder();
final Decoder decoder = Base64.getDecoder();
final Encoder encoder = Base64.getEncoder();
final ObjectMapper objectMapper = new ObjectMapper();
public boolean validateEllipticCurve(final String JWT, final String PUBLICKEY)
throws IOException {
final String[] at_arr = JWT.split("\.");
final String headerB64u = at_arr[0];
final String payloadB64u = at_arr[1];
final String signed_data = headerB64u + "." + payloadB64u;
final byte[] signature = urlDecoder.decode(at_arr[2]);
final byte[] at_headerJSON = urlDecoder.decode(headerB64u);
final JsonNode pkRoot = objectMapper.readTree(PUBLICKEY);
final String xStr = pkRoot.get("x").textValue();
final String yStr = pkRoot.get("y").textValue();
final byte xArr[] = decoder.decode(xStr);
final byte yArr[] = decoder.decode(yStr);
final BigInteger x = new BigInteger(xArr);
final BigInteger y = new BigInteger(yArr);
try {
final AlgorithmParameters parameters = AlgorithmParameters
.getInstance("EC");
parameters.init(new ECGenParameterSpec("secp256k1"));
final ECParameterSpec ecParameterSpec = parameters
.getParameterSpec(ECParameterSpec.class);
final KeyFactory keyFactory = KeyFactory.getInstance("EC");
final ECPoint ecPoint = new ECPoint(x, y);
final ECPublicKeySpec keySpec = new ECPublicKeySpec(ecPoint,
ecParameterSpec);
final ECPublicKey publicKey = (ECPublicKey) keyFactory
.generatePublic(keySpec);
final JsonNode key_root = objectMapper.readTree(at_headerJSON);
final String alg = key_root.get("alg").textValue();
Signature dataVerifyingInstance = null;
switch (alg) {
case "ES256":
dataVerifyingInstance = Signature.getInstance("SHA256withECDSA");
break;
case "ES384":
dataVerifyingInstance = Signature.getInstance("SHA384withECDSA");
break;
case "ES512":
dataVerifyingInstance = Signature.getInstance("SHA512withECDSA");
break;
}
dataVerifyingInstance.initVerify(publicKey);
dataVerifyingInstance.update(signed_data.getBytes());
final boolean verification = dataVerifyingInstance.verify(signature);
return verification;
} catch (final SignatureException | InvalidKeySpecException
| InvalidKeyException | InvalidParameterSpecException
| NoSuchAlgorithmException ex) {
ex.printStackTrace();
return false;
}
}
}
最后一行似乎引发了异常。
final boolean verification = dataVerifyingInstance.verify(signature);// <== here
我不确定是我使用了错误的曲线(secp256k1、secp256r1、secp256v1)导致了这个问题,还是可能是其他原因。任何提示将不胜感激。
问题是为 JWS 计算的签名(在椭圆曲线的情况下)是 R
和 S
值的原始串联,如 RFC7515 中指定。所以这是 32 + 32 = 64 字节(在 256 位曲线的情况下)。
但是 Java 实现期望此签名采用 DER 序列形式。这就是您遇到此异常的原因 - 只是您的签名(R
和 S
值)未经过 DER 编码。您甚至可以检查负责解码签名 (sun.security.ec.ECDSASignature::decodeSignature
) 和编码签名(用于签名操作 - sun.security.ec.ECDSASignature::encodeSignature
)的代码。据您在源代码中看到的,他们使用 sun.ec
包中的内部 类 进行 DER 编码。不鼓励您自己使用这些 类。
您最好的选择是将原始签名转换为 DER 编码签名。如果您愿意,可以使用开箱即用的 DER 编码处理的 BouncyCastle
。这是将您的 EC 原始签名转换为 DER 编码签名的方法:
private static byte[] toDerSignature(byte[] jwsSig) throws IOException {
byte[] rBytes = Arrays.copyOfRange(jwsSig, 0, 32);
byte[] sBytes = Arrays.copyOfRange(jwsSig, 32, 64);
BigInteger r = new BigInteger(1, rBytes);
BigInteger s = new BigInteger(1, sBytes);
DERSequence sequence = new DERSequence(new ASN1Encodable[] {
new ASN1Integer(r),
new ASN1Integer(s)
});
return sequence.getDEREncoded();
}
并使用此签名进行验证。
它可能是特定于 JVM 的,但我找到了一个更简单的解决方案: 而不是
dataVerifyingInstance = Signature.getInstance("SHA256withECDSA");
使用
dataVerifyingInstance = Signature.getInstance("SHA256withECDSAinP1363Format");
在这种情况下,实现将只使用您的原始签名,否则它将不得不从 DER 格式解码回来:
sun.security.ec.ECDSASignature
protected boolean engineVerify(byte[] signature) throws SignatureException {
...
byte[] sig;
if (p1363Format) {
sig = signature; //with SHA256withECDSAinP1363Format
} else {
sig = decodeSignature(signature); //with SHA256withECDSA
}
...
}