Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Error when verifying ECDSA signature in Java with BouncyCastle

I have tested a solution to verify an ECDSA signature (How can I get a PublicKey object from EC public key bytes?) that works perfect with the given data.

This is the data:

byte[] pubKey = DatatypeConverter.parseHexBinary("049a55ad1e210cd113457ccd3465b930c9e7ade5e760ef64b63142dad43a308ed08e2d85632e8ff0322d3c7fda14409eafdc4c5b8ee0882fe885c92e3789c36a7a");
byte[] message = DatatypeConverter.parseHexBinary("54686973206973206a75737420736f6d6520706f696e746c6573732064756d6d7920737472696e672e205468616e6b7320616e7977617920666f722074616b696e67207468652074696d6520746f206465636f6465206974203b2d29");
byte[] signature = DatatypeConverter.parseHexBinary("304402205fef461a4714a18a5ca6dce6d5ab8604f09f3899313a28ab430eb9860f8be9d602203c8d36446be85383af3f2e8630f40c4172543322b5e8973e03fff2309755e654");

And this is the code (which prints true):

private static boolean isValidSignature(byte[] pubKey, byte[] message,byte[] signature) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, SignatureException, InvalidKeySpecException {
    Signature ecdsaVerify = Signature.getInstance("SHA256withECDSA", new BouncyCastleProvider());
    ecdsaVerify.initVerify(getPublicKeyFromBytes(pubKey));
    ecdsaVerify.update(message);
    return ecdsaVerify.verify(signature);
}

private static PublicKey getPublicKeyFromBytes(byte[] pubKey) throws NoSuchAlgorithmException, InvalidKeySpecException {
    ECNamedCurveParameterSpec spec = ECNamedCurveTable.getParameterSpec("prime256v1");
    KeyFactory kf = KeyFactory.getInstance("ECDSA", new BouncyCastleProvider());
    ECNamedCurveSpec params = new ECNamedCurveSpec("prime256v1", spec.getCurve(), spec.getG(), spec.getN());
    ECPoint point =  ECPointUtil.decodePoint(params.getCurve(), pubKey);
    ECPublicKeySpec pubKeySpec = new ECPublicKeySpec(point, params);
    ECPublicKey pk = (ECPublicKey) kf.generatePublic(pubKeySpec);
    return pk;
}

public static void main (String[] args) {
    System.out.println(isValidSignature(pubKey, message, signature));
}

My problem comes when I change the signature and data to an example input from an already implemented system:

final static byte[] pubKey = DatatypeConverter.parseHexBinary("0447303876C6FED5550DF3EE1136989FCD87293D54A5D8E2F2F6D7FBE9A81089B889A5917443AF33E696178CEF4C9D6A4288B2745B29AF6C8BCAD1348F78EB9F9B");
final static byte[] message = DatatypeConverter.parseHexBinary("02158001f53611a06e2d1a270000013ed9305dc2780524015110500000002d0100140092569202017aa00c5dd30000000000000000000000000000000007d1000001020001b20788b80059f48d95cdefc8c6000200200030d41e0000012016840310a50733a9870fffd0430100");
final static byte[] signature = DatatypeConverter.parseHexBinary("531F8918FF250132959B01F7F56FDFD9E6CA3EC2144E12A6DA37C281489A3D96");

New data outputs this error:

java.security.SignatureException: error decoding signature bytes.
    at org.bouncycastle.jcajce.provider.asymmetric.util.DSABase.engineVerify(Unknown Source)
    at java.security.Signature$Delegate.engineVerify(Signature.java:1178)
    at java.security.Signature.verify(Signature.java:612)
    at its.sec.exec.TestProgram.isValidSignature(TestProgram.java:168)
    at its.sec.exec.TestProgram.execution(TestProgram.java:101)
    at its.sec.exec.TestProgram.main(TestProgram.java:55)

I assume the problem is about the signature that comes with the secured message because:

  • The key pair is the same length and format that the example. And are correct since it comes from the certificate that signs the message.
  • The message itself (payload) shouldn't affect the security process.

Last thing worth mention is that my documentation says that the signature must be preceded by a field called "R" which "contains the x coordinate of the elliptic curve point resulting from multiplying the generator element by the ephemeral private key" and its length must be the same as the signature (32 byte).

Can someone point me out what I'm missing here?

EDIT: Solution

As Peter Dettman pointed in his answer, the signature was not correctly formatted (also content was incorrect too) in order to be computed by the verify() method. Here is a good explanation that mainly says that:

When encoded in DER, this (signature) becomes the following sequence of bytes:

0x30 b1 0x02 b2 (vr) 0x02 b3 (vs)

where:

  • b1 is a single byte value, equal to the length, in bytes, of the remaining list of bytes (from the first 0x02 to the end of the encoding);
  • b2 is a single byte value, equal to the length, in bytes, of (vr);
  • b3 is a single byte value, equal to the length, in bytes, of (vs);
  • (vr) is the signed big-endian encoding of the value "r", of minimal length;
  • (vs) is the signed big-endian encoding of the value "s", of minimal length.

Applying that change, signature grows to 70 bytes and the execution outputs no error.

like image 972
CarlosRos Avatar asked May 11 '15 18:05

CarlosRos


2 Answers

The expected ECDSA signature format that the BC (and other provider) implementations work with is a DER-encoded ASN.1 sequence containing two integer values r and s. This signature format has been specified in ANSI X9.62. This is the format in the first set of data you give (note that signature is a total of 70 bytes).

In the second set of data, signature is only 32 bytes, and is not an ASN.1 sequence at all. I would guess that this value is only the s value, and it is missing the r value and the ASN.1 INTEGER encoding for them both, instead encoding the values as a unsigned big integer value with the same size as the key.

like image 158
Peter Dettman Avatar answered Sep 30 '22 12:09

Peter Dettman


this is a sample code to write r and s in ASN1 DER encoded format

    // construct the ASN1Sequence with r and s
    ByteArrayOutputStream outs = new ByteArrayOutputStream();

    byte radd = (byte)(((signed[0] & 0x80) > 0) ? 1 : 0);
    byte sadd = (byte)(((signed[32] & 0x80) > 0) ? 1 : 0);

    byte length = (byte)(0x44 + radd + sadd);

    outs.write(0x30);
    outs.write(length); // length 68 bytes +
    outs.write(0x02); // ASN1Integer
    outs.write(0x20 + radd); // length 32 bytes
    if(radd > 0)
        outs.write(0x00); // positive val
    outs.write(signed, 0, 32);
    outs.write(0x02); // ASN1Integer
    outs.write(0x20 + sadd); // length 32 bytes
    if(sadd > 0)
        outs.write(0x00); // positive val
    outs.write(signed, 32, 32);

    signed = outs.toByteArray();
like image 33
Ugo Chirico Avatar answered Sep 30 '22 13:09

Ugo Chirico