What is the difference between compute a signature with the following two methods?
- Compute a signature with
Signature.getInstance("SHA256withRSA")
- Compute SHA256 with
MessageDigest.getInstance("SHA-256")
and compute the digest withSignature.getInstance("RSA");
to get the signature?
If they are different, is there a way to modify the method 2 so that both methods give the same output?
I tried the following code:
package mysha.mysha;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.Security;
import java.security.Signature;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
public class MySHA256 {
public static void main(String[] args) throws Exception {
//compute SHA256 first
Security.addProvider(new BouncyCastleProvider());
String s = "1234";
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(s.getBytes());
byte[] outputDigest = messageDigest.digest();
//sign SHA256 with RSA
PrivateKey privateKey = Share.loadPk8("D:/key.pk8");
Signature rsaSignature = Signature.getInstance("RSA");
rsaSignature.initSign(privateKey);
rsaSignature.update(outputDigest);
byte[] signed = rsaSignature.sign();
System.out.println(bytesToHex(signed));
//compute SHA256withRSA as a single step
Signature rsaSha256Signature = Signature.getInstance("SHA256withRSA");
rsaSha256Signature.initSign(privateKey);
rsaSha256Signature.update(s.getBytes());
byte[] signed2 = rsaSha256Signature.sign();
System.out.println(bytesToHex(signed2));
}
public static String bytesToHex(byte[] bytes) {
final char[] hexArray = "0123456789ABCDEF".toCharArray();
char[] hexChars = new char[bytes.length * 2];
for ( int j = 0; j < bytes.length; j++ ) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
}
Nevertheless, the outputs are not the same.
The following is the sample output with my test key:
method 1: 61427B2A2CF1902A4B15F80156AEB09D8096BA1271F89F1919C78B18D0BABA08AA043A0037934B5AE3FC0EB7702898AC5AE96517AFD93433DF540353BCCE72A470CFA4B765D5835E7EA77743F3C4A0ABB11414B0141EF7ECCD2D5285A69728D0D0709C2537D6A772418A928B0E168F81C99B538FD25BDA7496AE8E185AC46F39
method 2: BA9039B75CA8A40DC9A7AED51E174E2B3365B2D6A1CF94DF70A00D898074A51FDD9973672DDE95CBAC39EBE4F3BA529C538ED0FF9F0A3F9A8CE203F1DFFA907DC508643906AA86DA54DFF8A90B00F5F116D13A53731384C1C5C9C4E75A3E41DAF88F74D2F1BCCF818764A4AB144A081B641C1C488AC8B194EB14BC9D1928E4EA
Update 1:
According to mkl's answer, I modify my code but still cannot get it right. Do I still miss something?
package mysha.mysha;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.Security;
import java.security.Signature;
import org.bouncycastle.asn1.DEROutputStream;
import org.bouncycastle.asn1.nist.NISTObjectIdentifiers;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
import org.bouncycastle.asn1.x509.DigestInfo;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
public class MySHA256 {
public static void main(String[] args) throws Exception {
//compute SHA256 first
Security.addProvider(new BouncyCastleProvider());
String s = "1234";
MessageDigest messageDigest = MessageDigest.getInstance("SHA-256");
messageDigest.update(s.getBytes());
byte[] outputDigest = messageDigest.digest();
AlgorithmIdentifier sha256Aid = new AlgorithmIdentifier(NISTObjectIdentifiers.id_sha256, null);
DigestInfo di = new DigestInfo(sha256Aid, outputDigest);
//sign SHA256 with RSA
PrivateKey privateKey = Share.loadPk8("D:/key.pk8");
Signature rsaSignature = Signature.getInstance("RSA");
rsaSignature.initSign(privateKey);
rsaSignature.update(di.toASN1Primitive().getEncoded());
byte[] signed = rsaSignature.sign();
System.out.println("method 1: "+bytesToHex(signed));
//compute SHA256withRSA as a single step
Signature rsaSha256Signature = Signature.getInstance("SHA256withRSA");
rsaSha256Signature.initSign(privateKey);
rsaSha256Signature.update(s.getBytes());
byte[] signed2 = rsaSha256Signature.sign();
System.out.println("method 2: "+bytesToHex(signed2));
}
public static String bytesToHex(byte[] bytes) {
final char[] hexArray = "0123456789ABCDEF".toCharArray();
char[] hexChars = new char[bytes.length * 2];
for ( int j = 0; j < bytes.length; j++ ) {
int v = bytes[j] & 0xFF;
hexChars[j * 2] = hexArray[v >>> 4];
hexChars[j * 2 + 1] = hexArray[v & 0x0F];
}
return new String(hexChars);
}
}
method 1:
675D868546777C5A9B5E74988E0CD41A46A929C1D0890B32B1FBE34F12D68F1FDB56E623294DB903F6AC60A2ADA61976B27C66056A16F5790A78168803AD2C685F9B4CF983C939305A9819CBA9D95441CD7214D40D06A98B4DDF9692A7D300DD51E808A6722A0D7C288DBD476DF4DEEBB3DAF41CFC0978F24424960F86F0284E
method 2:
BA9039B75CA8A40DC9A7AED51E174E2B3365B2D6A1CF94DF70A00D898074A51FDD9973672DDE95CBAC39EBE4F3BA529C538ED0FF9F0A3F9A8CE203F1DFFA907DC508643906AA86DA54DFF8A90B00F5F116D13A53731384C1C5C9C4E75A3E41DAF88F74D2F1BCCF818764A4AB144A081B641C1C488AC8B194EB14BC9D1928E4EA
The difference
The difference between signing with
"SHA256withRSA"
and computing the SHA256 hash and signing it with"RSA"
(="NONEwithRSA"
) is foremost that in the former case the calculated SHA-256 hash value is first encapsulated in aDigestInfo
structurebefore being padded and then encrypted while in the latter case the naked SHA256 hash value is padded and encrypted.
First and foremost you will have to encapsulate the hash value in a
DigestInfo
structure before signing using"NONEwithRSA"
.RFC 3447 Section 9.2 helps here by stating in Note 1 that
Making it work
In response to the section above the OP updated his question with the updated code. Unfortunately, though, it did not yet work for him. Thus,
The OP's code
I executed the OP's code (SignInSteps.java). As he didn't provide the private key, I used a test key of my own (demo-rsa2048.p12). The result:
Thus, in contrast to the OP's observations, signatures equal in case of the updated code.
Not assuming copy&paste errors, there still might be other differences involved.
The environment
I tested using Java 8 (1.8.0_20) with unlimited jurisdiction files added and BouncyCastle 1.52, 1.49, and 1.46 (with a small test code modification due to the BC API changes).
The OP mentioned in a comment:
Thus I updated Java, still no difference.
Then I updated BouncyCastle to 1.53. And indeed, suddenly the results differed:
Interestingly only the value for method 1 in the updated code differs. Thus, I looked at the intermediary objects in that case
Thus, BouncyCastle 1.53 encodes the DigestInfo object differently! And the encoding in 1.52 (and below) is the one expected by the RFC 3447 Section 9.2.
Looking at the ASN.1 dumps one sees that BC 1.52 encodes the AlgorithmIdentifier as
while BC 1.53 creates
So in 1.53 the algorithm parameters are missing altogether. This suggests changing the line
to
and suddenly it works with BouncyCastle 1.53, too, the values for method 1 and method 2 coincide! ;)
TL;DR
Don't use
null
as the SHA-256 parameters when instantiating theAlgorithmIdentifier
, useDERNull.INSTANCE
instead.How did I...
In a comment the OP indicated that he'd like to know more about
So...
... inspect the intermediate object
Quite simple. First I split up the line
in the updated code as
and then added console outputs
Finally I executed the code with the different BouncyCastle versions.
... produce the ASN.1 dumps
There is a well-known utility called dumpasn1 by Peter Gutmann which has become the kernel of many command line and GUI tools for creating and displaying ASN.1 dumps. I currently happen to use GUIdumpASN-ng.
In the case at hand I saved the contents of the
byte[] encodedDigestInfo
to a file (which can be done using e.g.Files.write
) and opened these files in GUIdumpASN-ng.