Using SHA1 and RSA with java.security.Signature vs. MessageDigest and Cipher
Solution 1
OK, I've worked out what's going on. Leonidas is right, it's not just the hash that gets encrypted (in the case of the Cipher class method), it's the ID of the hash algorithm concatenated with the digest:
DigestInfo ::= SEQUENCE {
digestAlgorithm AlgorithmIdentifier,
digest OCTET STRING
}
Which is why the encryption by the Cipher and Signature are different.
Solution 2
To produce the same results:
MessageDigest sha1 = MessageDigest.getInstance("SHA1", BOUNCY_CASTLE_PROVIDER);
byte[] digest = sha1.digest(content);
DERObjectIdentifier sha1oid_ = new DERObjectIdentifier("1.3.14.3.2.26");
AlgorithmIdentifier sha1aid_ = new AlgorithmIdentifier(sha1oid_, null);
DigestInfo di = new DigestInfo(sha1aid_, digest);
byte[] plainSig = di.getDEREncoded();
Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding", BOUNCY_CASTLE_PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] signature = cipher.doFinal(plainSig);
Solution 3
A slightly more efficient version of the bytes2String method is
private static final char[] hex = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
private static String byteArray2Hex(byte[] bytes) {
StringBuilder sb = new StringBuilder(bytes.length * 2);
for (final byte b : bytes) {
sb.append(hex[(b & 0xF0) >> 4]);
sb.append(hex[b & 0x0F]);
}
return sb.toString();
}
Solution 4
Erm, after understanding your question: are you sure that the signature-method only creates a SHA1 and encrypts it? GPG et al offer to compress/clear sign the data. Maybe this java-signature-alg also creates a detachable/attachable signature.
Solution 5
Taking @Mike Houston's answer as pointer, here is a complete sample code that does Signature and Hash and encryption.
/**
* @param args
*/
public static void main(String[] args)
{
try
{
boolean useBouncyCastleProvider = false;
Provider provider = null;
if (useBouncyCastleProvider)
{
provider = new BouncyCastleProvider();
Security.addProvider(provider);
}
String plainText = "This is a plain text!!";
// KeyPair
KeyPairGenerator keyPairGenerator = null;
if (null != provider)
keyPairGenerator = KeyPairGenerator.getInstance("RSA", provider);
else
keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair keyPair = keyPairGenerator.generateKeyPair();
// Signature
Signature signatureProvider = null;
if (null != provider)
signatureProvider = Signature.getInstance("SHA256WithRSA", provider);
else
signatureProvider = Signature.getInstance("SHA256WithRSA");
signatureProvider.initSign(keyPair.getPrivate());
signatureProvider.update(plainText.getBytes());
byte[] signature = signatureProvider.sign();
System.out.println("Signature Output : ");
System.out.println("\t" + new String(Base64.encode(signature)));
// Message Digest
String hashingAlgorithm = "SHA-256";
MessageDigest messageDigestProvider = null;
if (null != provider)
messageDigestProvider = MessageDigest.getInstance(hashingAlgorithm, provider);
else
messageDigestProvider = MessageDigest.getInstance(hashingAlgorithm);
messageDigestProvider.update(plainText.getBytes());
byte[] hash = messageDigestProvider.digest();
DigestAlgorithmIdentifierFinder hashAlgorithmFinder = new DefaultDigestAlgorithmIdentifierFinder();
AlgorithmIdentifier hashingAlgorithmIdentifier = hashAlgorithmFinder.find(hashingAlgorithm);
DigestInfo digestInfo = new DigestInfo(hashingAlgorithmIdentifier, hash);
byte[] hashToEncrypt = digestInfo.getEncoded();
// Crypto
// You could also use "RSA/ECB/PKCS1Padding" for both the BC and SUN Providers.
Cipher encCipher = null;
if (null != provider)
encCipher = Cipher.getInstance("RSA/NONE/PKCS1Padding", provider);
else
encCipher = Cipher.getInstance("RSA");
encCipher.init(Cipher.ENCRYPT_MODE, keyPair.getPrivate());
byte[] encrypted = encCipher.doFinal(hashToEncrypt);
System.out.println("Hash and Encryption Output : ");
System.out.println("\t" + new String(Base64.encode(encrypted)));
}
catch (Throwable e)
{
e.printStackTrace();
}
}
You can use BouncyCastle Provider or default Sun Provider.
Kothar
Updated on January 29, 2021Comments
-
Kothar over 3 years
I'm trying to understand what the Java java.security.Signature class does. If I compute an SHA1 message digest, and then encrypt that digest using RSA, I get a different result to asking the Signature class to sign the same thing:
// Generate new key KeyPair keyPair = KeyPairGenerator.getInstance("RSA").generateKeyPair(); PrivateKey privateKey = keyPair.getPrivate(); String plaintext = "This is the message being signed"; // Compute signature Signature instance = Signature.getInstance("SHA1withRSA"); instance.initSign(privateKey); instance.update((plaintext).getBytes()); byte[] signature = instance.sign(); // Compute digest MessageDigest sha1 = MessageDigest.getInstance("SHA1"); byte[] digest = sha1.digest((plaintext).getBytes()); // Encrypt digest Cipher cipher = Cipher.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, privateKey); byte[] cipherText = cipher.doFinal(digest); // Display results System.out.println("Input data: " + plaintext); System.out.println("Digest: " + bytes2String(digest)); System.out.println("Cipher text: " + bytes2String(cipherText)); System.out.println("Signature: " + bytes2String(signature));
Results in (for example):
Input data: This is the message being signed
Digest: 62b0a9ef15461c82766fb5bdaae9edbe4ac2e067
Cipher text: 057dc0d2f7f54acc95d3cf5cba9f944619394711003bdd12...
Signature: 7177c74bbbb871cc0af92e30d2808ebae146f25d3fd8ba1622...I must have a fundamental misunderstanding of what Signature is doing - I've traced through it, and it appears to be calling update on a MessageDigest object, with the algorithm set to SHA1 as I would expect, then getting the digest, then doing the encryption. What's making the results differ?
EDIT:
Leonidas made me check whether the signature scheme is supposed to do what I think it does. There are two types of signature defined in the RFC:
The first of these (PKCS1) is the one I describe above. It uses a hash function to create a digest, and then encrypts the result with a private key.
The second algorithm uses a random salt value, and is more secure but non-deterministic. The signature produced from the code above does not change if the same key is used repeatedly, so I don't think it can be PSS.
EDIT:
Here's the
bytes2string
method I was using:private static String bytes2String(byte[] bytes) { StringBuilder string = new StringBuilder(); for (byte b : bytes) { String hexString = Integer.toHexString(0x00FF & b); string.append(hexString.length() == 1 ? "0" + hexString : hexString); } return string.toString(); }
-
Kothar about 15 yearsI'm not sure, no, but I would expect the algorithm to indicate if it were going to do more than just those two operations. I've been reading the RFC: ietf.org/rfc/rfc3447.txt, which as far as I understand, just hashes, then encrypts the hash. Is GPG's compression for encrypted messages?
-
Bikash Mahata about 15 yearsHooray for finding it out by yourself :)
-
Romulo Pereira over 13 yearsOK, Mike. And how do you make them produce the same result?
-
Kothar almost 13 yearsThanks! Indeed, a lookup table is a nice solution. +1 ;)
-
Maarten Bodewes about 12 yearsNote that if the result is still not correct after this, then it might be that the underlying crypto library uses random padding for any encryption operation. This seems to be a common mistake.
-
Jaime Hablutzel about 11 yearsThis is the kind of problems you can detect if you use a ASN1/DER inspector as ASN.1Editor
-
Maarten Bodewes almost 7 years@Greenhand It is the object identifier (OID) for SHA-1. This is the shorthand notation, but in PKCS#1 it is specified as
id-sha1 OBJECT IDENTIFIER ::= { iso(1) identified-organization(3) oiw(14) secsig(3) algorithms(2) 26 }
-
Maarten Bodewes almost 7 yearsI've put in the right algorithm string so that you don't have to guess the default settings. Note that Oracle's provider and Bouncy Castle correctly use PKCS#1 v1.5 padding for signature generation when you use
Cipher
like this. Other providers may not be so flexible and use PKCS#1 padding for encryption instead. -
Maarten Bodewes almost 7 yearsYou can use
"RSA/ECB/PKCS1Padding"
for both providers. -
Maarten Bodewes almost 7 yearsI've voted this down as it doesn't provide a full answer and it forgets to explain about PKCS#1 compatible padding, which is also required for the algorithm to work. PKCS#1 is the default for Java most of the time, but your code may still fail if a provider chooses a different default.
-
dave_thompson_085 over 6 yearsTo generalize a little, the OIDs for commonly used hashes are copied in section 11.2.3 or appendix B.1, and their pre-worked DER encodings in section 9.2.1, of PKCS1 aka rfc 2437 3447 and 8017. It's almost as if they imagined someone might read the specification before implementing it.