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();
}
In the document about connecting Azure AD to a SAML identity provider , it says that one must use RSA-SHA1 for signing. SHA-1 is not secure enough for signing. It has been demonstrated that an attacker can create two documents with the same hash with a cost of about 2^63 (about 110 GPU years of 2017).
SHA256 with RSA signature is an efficient asymmetric encryption method used in many secure APIs. This algorithm first calculates a unique hash of the input data using SHA256 algorithm. The hash is then encrypted with a private key using the RSA algorithm.
RSA is a public-key cryptosystem for both encryption and authentication. This signature suite specifies how it is used with the SHA1 hash function to sign a PICS label per the DSig 1.0 Specification.
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.
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);
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();
}
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With