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

2

8

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.

Stephen answered 7/12, 2019 at 18:18 Comment(0)
S
7

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.

Shine answered 7/12, 2019 at 21:23 Comment(1)
I also had to select the curve, secp256r1, as secp256k1 was incorrect.Stephen
M
1

It may be JVM specific, but I found an easier solution: Instead of

dataVerifyingInstance = Signature.getInstance("SHA256withECDSA");

use

dataVerifyingInstance = Signature.getInstance("SHA256withECDSAinP1363Format");

In this case implementation will simply use your original signature, otherwise it will have to decode it back from DER format:

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
        }
...
}
Merovingian answered 9/1, 2022 at 6:54 Comment(1)
ECDSAinP1363Format is implemented in the 'standard' (usually builtin) SunEC provider since Java 9 (in 2017); in any Java version the add-on/third-party BouncyCastle provider has equivalent but differently named {hash}withPLAIN-ECDSA since 1.51 (in 2014).Latricelatricia

© 2022 - 2024 — McMap. All rights reserved.