javahashsignaturedigital

Create SHA256withRSA in two steps


For educational purposes, I would like to be able to first create a Hash for a String and then to create RSA Digital Signature from that Hash so that the result is the same as when using SHA256withRSA in one go. This way I want to confirm that I fully understand all the steps that are actually being done automatically for us when we call SHA256withRSA.

I also have one additional question. Is Digital Signature done on Hash or on Base64 Encoded Hash?

Below is the code that I am currently using and here is the output of the code which shows that these two approaches produce different signatures which means that I am missing soma manual steps.

AUTOMATICALLY SIGN & VERIFY
SIGNATURE = Hj6a86sR2cJoQFolbxj0blk2I9CAdTdx6WOles5t/pyUyJwa9rp2/SRx2wyXWgc6GsvoZYGLUedeJ2Lklm5hYgT/TtNBATk5eChgfkJMz3NBIRPrsl7ZPG7Wvo4VmHsPpoZZ8PdRk8qY9RLou86OyIqRcX62isuV+e/0deHJ+yTZz4vqA3y+PE4yRFp96A8sKw5VlDnByn7bsxM/QOS+sQWTsETzU9s4YSRfKNq1Urn8/VDoel7n0ORjR918P+0kwE+G77bAOI70yQZorvmbgrMLSBJeVzkKzM/YECLWyrJsqdjfp86FkA9MPGB1V6rO8q8m5GhNoJOmNhC7Ek95Bw==

MANUALLY SIGN & VERIFY
HASH      = lDlahWCWx2b5TYUji52uPeReSW7vbro2wXuRsPKmOdA=
SIGNATURE = gsxw7UQpqni5HyPAw8wI2pvepbrDzizkOvO0hab1+7vi4EaYJi3n4lvnkBTOU5LXQKLZGzJcug0mL2pL/PVh8lrvzZ/F9CxULLxKpayrNkvL9yEWMvcfcku9Go5EGrxSzD7VYvkwOzHvGe4GgUGD1JOjvzXBAfJRT8h/wnZi9IPA9n31/tWI2eFw17Js/gymElycp7pjrpEhUNe/IVTP9HVfRQfAxEDAPW8GY/WFdxbD3Jk05LKvpTxua4jzCX9wJh/s8aiT9OvEXh3/zt06JSEpfgf+CpkOFJupmRhsgqebPfVQEo24ctw1DnipKkL771mm30bFcm/FF1reXuOqqQ==

public class Main {

  //====================================================================================
  // MAIN
  //====================================================================================
  public static void main(String[] args) throws Exception {

    //CREATE DATA
    String            dataString = "Data to be encrypted";
    byte[]            dataBytes  = dataString.getBytes();

    //CREATE KEYS
    KeyPairGenerator  keyPairGenerator = KeyPairGenerator.getInstance("RSA");
                      keyPairGenerator.initialize(2048);
    KeyPair           keyPair    = keyPairGenerator.generateKeyPair();
    PrivateKey        privateKey = keyPair.getPrivate();

    //AUTOMATICALLY SIGN & VERIFY
    System.out.println("\nAUTOMATICALLY SIGN & VERIFY");
    byte[]  automaticSignatureBytes = AutomaticHash.sign  ("SHA256withRSA", privateKey, dataBytes);

    //MANUALLY SIGN & VERIFY
    System.out.println("\nMANUALLY SIGN & VERIFY");
    byte[]  manualHashBytes         = ManualHash.hash  ("SHA-256", dataBytes);
    byte[]  manualSignatureBytes    = ManualHash.sign  ("NONEwithRSA", privateKey, manualHashBytes);

  }

}

public class AutomaticHash {

  //====================================================================================
  // AUTOMATICALLY SIGN
  //====================================================================================
  public static byte[] sign(String algorithms, PrivateKey privateKey, byte[] dataBytes) throws Exception {

    //CREATE SIGNATURE (use Hash first)
    Signature         signature = Signature.getInstance(algorithms);
                      signature.initSign(privateKey);
                      signature.update(dataBytes);
    byte[]            signatureBytes = signature.sign();

    //ENCODE SIGNATURE
    byte[]            signatureEncodedBytes  = Base64.getEncoder().encode(signatureBytes);
    String            signatureEncodedString = new String(signatureEncodedBytes);

    //DISPLAY ENCODED SIGNATURE
    System.out.println("SIGNATURE = " + signatureEncodedString);

    //RETURN SIGNATURE
    return signatureBytes;

  }

}

public class ManualHash {

  //====================================================================================
  // MANUALLY HASH
  //====================================================================================
  public static byte[] hash(String algorithm, byte[] dataBytes) throws Exception {

    //CREATE HASH
    MessageDigest digest    = MessageDigest.getInstance(algorithm);
    byte[]        hashBytes = digest.digest(dataBytes);

    //ENCODE HASH
    byte[]        hashEncoded = Base64.getEncoder().encode(hashBytes);
    String        hashEncodedString = new String(hashEncoded);

    //DISPLAY ENCODED HASH
    System.out.println("HASH      = " + hashEncodedString);

    //RETURN HASH
    return hashBytes;

  }

  //====================================================================================
  // MANUALLY SIGN
  //====================================================================================
  public static byte[] sign(String algorithm, PrivateKey privateKey, byte[] hashBytes) throws Exception {

    //SIGN HASH
    Signature         signature = Signature.getInstance(algorithm);
                      signature.initSign(privateKey);
                      signature.update(hashBytes);
    byte[]            signatureBytes = signature.sign();

    //ENCODE SIGNATURE
    byte[]            signatureEncodedBytes = Base64.getEncoder().encode(signatureBytes);
    String            signatureEncodedString = new String(signatureEncodedBytes);

    //DISPLAY ENCODED HASH & SIGNATURE
    System.out.println("SIGNATURE = " + signatureEncodedString);

    //RETURN SIGNATURE
    return signatureBytes;

  }

}

Solution

  • SHA256withRSA and NoneWithRSA use PKCS#1 v1.5 padding, more precisely RSASSA-PKCS1-v1_5. This is a deterministic padding, i.e. repeated signing with the same data will produce the same signature. Details can be found in RFC8017, 8.2.

    This padding applies the DER encoding of the DigestInfo that results for SHA256 when the byte sequence (0x)30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20 is prepended to the hash.

    So your manual code must be modified e.g. as follows:

    public static byte[] sign(String algorithm, PrivateKey privateKey, byte[] hashBytes) throws Exception {
    
        byte[] id = new byte[] { 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, (byte) 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20 };
        byte[] derDigestInfo = new byte[id.length + hashBytes.length];
        System.arraycopy(id, 0, derDigestInfo, 0, id.length);
        System.arraycopy(hashBytes, 0, derDigestInfo, id.length, hashBytes.length);
    
        // SIGN HASH
        Signature signature = Signature.getInstance(algorithm);
        signature.initSign(privateKey);
        signature.update(derDigestInfo);
        byte[] signatureBytes = signature.sign();
    
        ...
    

    With this change, both sign() methods return the same result.


    Btw, java.util.Base64.Encoder.encodeToString() directly generates a Base64 encoded string.

    In addition, when encoding/decoding, a charset should always be specified, e.g. dataString.getBytes(StandardCharsets.UTF_8) or new String(..., StandardCharsets.UTF_8). Otherwise the platform's default charset is used, which may differ from the intended one.