Using SHA1 and RSA with java.security.Signature vs. MessageDigest and Cipher

107,637

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.

Share:
107,637
Kothar
Author by

Kothar

Updated on January 29, 2021

Comments

  • Kothar
    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
    Kothar about 15 years
    I'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
    Bikash Mahata about 15 years
    Hooray for finding it out by yourself :)
  • Romulo Pereira
    Romulo Pereira over 13 years
    OK, Mike. And how do you make them produce the same result?
  • Kothar
    Kothar almost 13 years
    Thanks! Indeed, a lookup table is a nice solution. +1 ;)
  • Maarten Bodewes
    Maarten Bodewes about 12 years
    Note 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
    Jaime Hablutzel about 11 years
    This is the kind of problems you can detect if you use a ASN1/DER inspector as ASN.1Editor
  • Maarten Bodewes
    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
    Maarten Bodewes almost 7 years
    I'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
    Maarten Bodewes almost 7 years
    You can use "RSA/ECB/PKCS1Padding" for both providers.
  • Maarten Bodewes
    Maarten Bodewes almost 7 years
    I'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
    dave_thompson_085 over 6 years
    To 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.