0

The bitcoinj library's API offers a signMessage method of the ECKey class which returns a signature (r,s) as 65 bytes encoded as a Base64 string. The crunch of the encoding lies in the additional leading byte which allows key recovery (the 64 bytes of (r,s) alone are not enough for that purpose). In particular, the leading byte of the encoding includes information regarding the compression status of the signing key.

Now I assume this specific signature encoding was implemented for a very good reason, which I suspect is the fact that it is a standard encoding, present in many other bitcoin libraries and in core.

Now when verifying a signature through key recovery, the verifyMessage method ignores the compression status of the recovered key and simply ensures that the corresponding elliptic curve points match. Sure, this is a pretty good check, but why do we bother encoding the compression status of a signing key, if we are going to ignore it later? Is this the semantics other known libraries have adopted, or indeed core? I attach a small snippet for illustration:

import org.bitcoinj.core.ECKey;
import java.security.SignatureException;

public class Test
{
  public static void main(String[] args)
  {
    String message = "some arbitrary message";

    ECKey k1 = new ECKey();      // random , compressed
    ECKey k2 = k1.decompress();

    // Signature (r,s) encoded as 65 bytes, with leading byte
    // allowing key recovery (including compression status)
    String sig1 = k1.signMessage(message);  
    String sig2 = k2.signMessage(message);  

    // compression status is encoded in signature => differing leading byte
    System.out.println(sig1);   // INgDhkt98Mme9m9AQ+nqtjyvjj ...
    System.out.println(sig2);   // HNgDhkt98Mme9m9AQ+nqtjyvjj ...

    // signatures are verified succesfully
    try
    {
      k1.verifyMessage(message, sig1);  // compressed case
    }
    catch(SignatureException e)
    {
      System.out.println("it should not happen");
    }

    try
    {
      k2.verifyMessage(message, sig2);  // uncompressed case
    }
    catch(SignatureException e)
    {
      System.out.println("it should not happen");
    }

    // in fact, compression status is ignored ...
    try
    {
      k1.verifyMessage(message, sig2);  // should it throw ?
    }
    catch(SignatureException e)
    {
      System.out.println("it does not happen");
    }

  }
}
1

Mileage may vary with this answer. The short answer is probably no when using libbitcoin.

Rationale: Over a year ago, when I experimented with sending funds to a uncompressed address using a signed transaction, the corresponding compressed keys could not be used to spend the same funds, the opposite was also true. The balance was dependent upon the compression of a key. When receiving with funds with an uncompressed address, only use the associated uncompressed private key to spend. Similarly, when receiving receiving with funds with a compressed address, only use the associated compressed private key to spend.

  • Thank you! I am hoping to play with libbitcoin very soon. I am slightly uneasy at the phrase 'uncompressed/compressed private key' but I understand what you mean :) – Sven Williamson Oct 1 '16 at 7:56
  • You're talking about transactions. The original question is about signed messages. – Pieter Wuille Oct 1 '16 at 9:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.