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