javajwtcryptographyelliptic-curvejava-security

ES256 JWT validation - SignatureException: invalid encoding for signature: java.io.IOException: Sequence tag error


I have a JWT which is signed using Elliptic Curve ES256 am trying to validate it:

eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjEyMzQifQ.eyJpc3MiOiJodHRwczovL2p3dC1pZHAuZXhhbXBsZS5jb20iLCJzdWIiOiJtYWlsdG86bWlrZUBleGFtcGxlLmNvbSIsIm5iZiI6MTU3NTY1NjY3NCwiZXhwIjoxOTI0OTkxOTk5LCJpYXQiOjE1NzU2NTY2NzQsImp0aSI6ImlkMTIzNDU2IiwidHlwIjoiaHR0cHM6Ly9leGFtcGxlLmNvbS9yZWdpc3RlciJ9.kcj0QQrERKIfny1TfHY-Z9iDFazr84xCssTDuXtV1n1dvY7CYXuP5ZBvpi9ArOQjsS8YCd0bKsWaQ-17VnF_1A

Using this public key:

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEoBUyo8CQAFPeYPvv78ylh5MwFZjT
CLQeb042TjiMJxG+9DLFmRSMlBQ9T/RsLLc+PmpB1+7yPAR+oR5gZn3kJQ==
-----END PUBLIC KEY-----

Which is just

{
  "kty": "EC",
  "use": "sig",
  "crv": "P-256",
  "kid": "1234",
  "x": "oBUyo8CQAFPeYPvv78ylh5MwFZjTCLQeb042TjiMJxE=",
  "y": "vvQyxZkUjJQUPU/0bCy3Pj5qQdfu8jwEfqEeYGZ95CU=",
  "alg": "ES256"
}

It validates correctly on https://jwt.io/ however when I try to verify it using native Java, it raises an error:


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$1.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$3.run(ParentRunner.java:193)
    at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
    at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
    at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
    at org.junit.runners.ParentRunner$2.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)

My code is as follows:

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

}

The last line seems to be raising the exception.

final boolean verification = dataVerifyingInstance.verify(signature);// <== here

I am not sure if I am causing the problem by using the wrong curve (secp256k1, secp256r1, secp256v1) or possibly it is something else. Any tips would be really appreciated.


Solution

  • The problem is that the signature which is computed for JWS (in case of Elliptic Curve) is the raw concatenation of R and S values as specified in RFC7515. So this is 32 + 32 = 64 bytes (in case of 256 bit curves).

    However Java implementations expect this signature to be in a DER sequence form. This is why you are encountering this exception - simply your signature (R and S values) is not DER encoded. You can even inspect the code which is responsible for decoding the signature (sun.security.ec.ECDSASignature::decodeSignature) and encoding the signature (for signing operation - sun.security.ec.ECDSASignature::encodeSignature). As far as you can see in the source code they are using internal classes from sun.ec package for DER encoding. And using those classes on your own is not encouraged.

    Your best choice is to convert raw signature to DER encoded one. If you want you can use BouncyCastle which has DER encoding handling out of the box. Here is the method which will convert your EC raw signature to DER encoded one :

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

    and use this signature for verification.