SHA1 和 RSA 与 java.security.Signature 对比 MessageDigest 和 Cipher

Posted

技术标签:

【中文标题】SHA1 和 RSA 与 java.security.Signature 对比 MessageDigest 和 Cipher【英文标题】:Using SHA1 and RSA with java.security.Signature vs. MessageDigest and Cipher 【发布时间】:2010-10-05 23:40:23 【问题描述】:

我试图了解 Java java.security.Signature 类的作用。如果我计算一个 SHA1 消息摘要,然后使用 RSA 对该摘要进行加密,我会得到与要求 Signature 类签署相同内容不同的结果:

// 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));

结果(例如):

输入数据:这是正在签名的消息 摘要:62b0a9ef15461c82766fb5bdaae9edbe4ac2e067 密文:057dc0d2f7f54acc95d3cf5cba9f944619394711003bdd12... 签名:7177c74bbbb871cc0af92e30d2808ebae146f25d3fd8ba1622...

我一定对 Signature 正在做的事情有一个根本性的误解 - 我已经对其进行了跟踪,它似乎正在对 MessageDigest 对象调用更新,其中正如我所料,算法设置为 SHA1,然后获取摘要,然后进行加密。是什么导致结果不同?

编辑:

Leonidas 让我检查签名方案是否应该按照我的想法去做。 RFC中定义了两种类型的签名:

RSASSA-PKCS1-v1_5 RSASSA-PSS

first of these (PKCS1) 就是我上面描述的那个。它使用哈希函数创建摘要,然后使用私钥对结果进行加密。

second algorithm 使用随机盐值,更安全但不确定。如果重复使用相同的密钥,上面代码产生的签名不会改变,所以我认为它不可能是PSS。

编辑:

这是我使用的bytes2string 方法:

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();

【问题讨论】:

【参考方案1】:

好的,我已经弄清楚发生了什么。 Leonidas 是对的,它不仅仅是被加密的哈希(在 Cipher 类方法的情况下),它是与摘要连接的哈希算法的 ID:

  DigestInfo ::= SEQUENCE 
      digestAlgorithm AlgorithmIdentifier,
      digest OCTET STRING
  

这就是 Cipher 和 Signature 的加密方式不同的原因。

【讨论】:

万岁你自己找到它:) 好的,迈克。以及如何让它们产生相同的结果? 请注意,如果在此之后结果仍然不正确,那么可能是底层加密库对任何加密操作使用了随机填充。这似乎是一个常见的错误。 如果您使用 ASN1/DER 检查器作为 ASN.1Editor 可以检测到这种问题 我投了反对票,因为它没有提供完整的答案,并且忘记解释 PKCS#1 兼容填充,这也是算法工作所必需的。大多数时候,PKCS#1 是 Java 的默认值,但如果提供者选择不同的默认值,您的代码仍可能会失败。【参考方案2】:

要产生相同的结果:

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);

【讨论】:

@Greenhand 它是 SHA-1 的对象标识符 (OID)。这是 简写 表示法,但在 PKCS#1 中它被指定为 id-sha1 OBJECT IDENTIFIER ::= iso(1) identified-organization(3) oiw(14) secsig(3) algorithms(2) 26 我已经输入了正确的算法字符串,这样您就不必猜测默认设置。请注意,当您像这样使用Cipher 时,Oracle 的提供程序和 Bouncy Castle 正确使用 PKCS#1 v1.5 填充用于签名生成。其他提供者可能不那么灵活,而是使用 PKCS#1 填充进行加密。 概括地说,常用哈希的 OID 复制在 PKCS1 aka rfc 2437 的第 11.2.3 节或附录 B.1 中,它们的预工作 DER 编码在第 9.2.1 节中复制3447 和 8017。几乎就像他们想象有人可能会在实施规范之前阅读规范一样。【参考方案3】:

bytes2String 方法的一个稍微高效的版本是

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();

【讨论】:

谢谢!事实上,查找表是一个不错的解决方案。 +1 ;)【参考方案4】:

Erm,在理解您的问题后:您确定签名方法仅创建一个 SHA1 并对其进行加密吗? GPG 等人提供对数据进行压缩/清除签名。也许这个 java-signature-alg 也创建了一个可分离/可附加的签名。

【讨论】:

我不确定,不,但我希望算法能够表明它是否会做更多的事情,而不仅仅是这两个操作。我一直在阅读 RFC:ietf.org/rfc/rfc3447.txt,据我所知,它只是散列,然后加密散列。 GPG 是否对加密消息进行压缩?【参考方案5】:

以@Mike Houston 的回答为指针,这里有一个完整的签名和哈希和加密示例代码。

/**
 * @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();
    

您可以使用 BouncyCastle 提供程序或默认的 Sun 提供程序。

【讨论】:

您可以为这两个提供商使用"RSA/ECB/PKCS1Padding"【参考方案6】:

我有一个类似的问题,我测试了添加代码并发现了一些有趣的结果。 通过我添加的这段代码,我可以推断出根据要使用的“提供者”,公司可能会有所不同? (因为加密中包含的数据在所有提供者中并不总是相同的)。

我的测试结果。

结论.- Signature Decipher= ???(trash) + DigestInfo(如果我们知道“trash”的值,则数字签名将相等)

IDE Eclipse 输出...

输入数据:这是正在签名的消息

摘要:62b0a9ef15461c82766fb5bdaae9edbe4ac2e067

摘要信息:3021300906052b0e03021a0500041462b0a9ef15461c82766fb5bdaae9edbe4ac2e067

签名破译:1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003021300906052b0e03021a0500041462b0a9ef15461c82766fb5bdaae9edbe4ac2e067 P>

代码

import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.NoSuchProviderException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;
import java.security.SignatureException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import org.bouncycastle.asn1.x509.DigestInfo;
import org.bouncycastle.asn1.DERObjectIdentifier;
import org.bouncycastle.asn1.x509.AlgorithmIdentifier;
public class prueba 
/**
* @param args
* @throws NoSuchProviderException 
* @throws NoSuchAlgorithmException 
* @throws InvalidKeyException 
* @throws SignatureException 
* @throws NoSuchPaddingException 
* @throws BadPaddingException 
* @throws IllegalBlockSizeException 
*///
public static void main(String[] args) throws NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException, SignatureException, NoSuchPaddingException, IllegalBlockSizeException, BadPaddingException 
// TODO Auto-generated method stub
KeyPair keyPair = KeyPairGenerator.getInstance("RSA","BC").generateKeyPair();
PrivateKey privateKey = keyPair.getPrivate();
PublicKey puKey = keyPair.getPublic();
String plaintext = "This is the message being signed";
// Hacer la firma
Signature instance = Signature.getInstance("SHA1withRSA","BC");
instance.initSign(privateKey);
instance.update((plaintext).getBytes());
byte[] signature = instance.sign();
// En dos partes primero hago un Hash
MessageDigest digest = MessageDigest.getInstance("SHA1", "BC");
byte[] hash = digest.digest((plaintext).getBytes());
// El digest es identico a  openssl dgst -sha1 texto.txt
//MessageDigest sha1 = MessageDigest.getInstance("SHA1","BC");
//byte[] digest = sha1.digest((plaintext).getBytes());
AlgorithmIdentifier digestAlgorithm = new AlgorithmIdentifier(new
DERObjectIdentifier("1.3.14.3.2.26"), null);
// create the digest info
DigestInfo di = new DigestInfo(digestAlgorithm, hash);
byte[] digestInfo = di.getDEREncoded();
//Luego cifro el hash
Cipher cipher = Cipher.getInstance("RSA","BC");
cipher.init(Cipher.ENCRYPT_MODE, privateKey);
byte[] cipherText = cipher.doFinal(digestInfo);
//byte[] cipherText = cipher.doFinal(digest2);
Cipher cipher2 = Cipher.getInstance("RSA","BC");
cipher2.init(Cipher.DECRYPT_MODE, puKey);
byte[] cipherText2 = cipher2.doFinal(signature);
System.out.println("Input data: " + plaintext);
System.out.println("Digest: " + bytes2String(hash));
System.out.println("Signature: " + bytes2String(signature));
System.out.println("Signature2: " + bytes2String(cipherText));
System.out.println("DigestInfo: " + bytes2String(digestInfo));
System.out.println("Signature Decipher: " + bytes2String(cipherText2));

【讨论】:

【参考方案7】:

下面的代码(取自我的博客文章 - http://todayguesswhat.blogspot.com/2021/01/manually-verifying-rsa-sha-signature-in.html)希望有助于理解带有 RSA 签名的标准 SHA 中存在的内容。这应该在标准 Oracle JDK 中工作,并且不需要 Bouncy Castle 库。它使用 sun.security 类来处理解密的签名内容 - 您可以轻松地手动解析。

在下面的示例中,消息摘要算法是 SHA-512,它产生一个 64 字节(512 位)的校验和。

SHA-1 将非常相似 - 但会产生 20 字节(160 位)的校验和。

import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.Signature;

import java.util.Arrays;

import javax.crypto.Cipher;

import sun.security.util.DerInputStream;
import sun.security.util.DerValue;

public class RSASignatureVerification

    public static void main(String[] args) throws Exception
    
        KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
        generator.initialize(2048);

        KeyPair keyPair = generator.generateKeyPair();
        PrivateKey privateKey = keyPair.getPrivate();
        PublicKey publicKey = keyPair.getPublic();

        String data = "hello oracle";
        byte[] dataBytes = data.getBytes("UTF8");

        Signature signer = Signature.getInstance("SHA512withRSA");
        signer.initSign(privateKey);

        signer.update(dataBytes);

        byte[] signature = signer.sign(); // signature bytes of the signing operation's result.

        Signature verifier = Signature.getInstance("SHA512withRSA");
        verifier.initVerify(publicKey);
        verifier.update(dataBytes);

        boolean verified = verifier.verify(signature);
        if (verified)
        
            System.out.println("Signature verified!");
        

/*
    The statement that describes signing to be equivalent to RSA encrypting the
    hash of the message using the private key is a greatly simplified view
    The decrypted signatures bytes likely convey a structure (ASN.1) encoded
    using DER with the hash just one component of the structure.
*/

        // lets try decrypt signature and see what is in it ...
        Cipher cipher = Cipher.getInstance("RSA");
        cipher.init(Cipher.DECRYPT_MODE, publicKey);

        byte[] decryptedSignatureBytes = cipher.doFinal(signature);

/*
    sample value of decrypted signature which was 83 bytes long

    30 51 30 0D 06 09 60 86 48 01 65 03 04 02 03 05
    00 04 40 51 00 41 75 CA 3B 2B 6B C0 0A 3F 99 E3
    6B 7A 01 DC F2 9B 36 E6 0D D4 31 89 53 A3 D9 80
    6D AE DD 45 7E 55 45 01 FC C8 73 D2 DD 8D E5 B9
    E0 71 57 13 41 D0 CD FF CA 58 01 03 A3 DD 95 A1
    C1 EE C8

    Taking above sample bytes ...
    0x30 means A SEQUENCE - which contains an ordered field of one or more types.
    It is encoded into a TLV triplet that begins with a Tag byte of 0x30.
    DER uses T,L,V (tag bytes, length bytes, value bytes) format

    0x51 is the length = 81 decimal (13 bytes)

    the 0x30 (48 decimal) that follows begins a second sequence

    https://tools.ietf.org/html/rfc3447#page-43
    the DER encoding T of the DigestInfo value is equal to the following for SHA-512
    0D 06 09 60 86 48 01 65 03 04 02 03 05 00 04 40 || H
    where || is concatenation and H is the hash value.

    0x0D is the length = 13 decimal (13 bytes)

    0x06 means an OBJECT_ID tag
    0x09 means the object id is 9 bytes ...

    https://docs.microsoft.com/en-au/windows/win32/seccertenroll/about-object-identifier?redirectedfrom=MSDN

    taking 2.16.840.1.101.3.4.2.3 (object id for SHA512 Hash Algorithm)

    The first two nodes of the OID are encoded onto a single byte.
    The first node is multiplied by the decimal 40 and the result is added to the value of the second node
    2 * 40 + 16 = 96 decimal = 60 hex
    Node values less than or equal to 127 are encoded on one byte.
    1 101 3 4 2 3 corresponds to in hex 01 65 03 04 02 03
    Node values greater than or equal to 128 are encoded on multiple bytes.
    Bit 7 of the leftmost byte is set to one. Bits 0 through 6 of each byte contains the encoded value.
    840 decimal = 348 hex
    -> 0000 0011 0100 1000
    set bit 7 of the left most byte to 1, ignore bit 7 of the right most byte,
    shifting right nibble of leftmost byte to the left by 1 bit
    -> 1000 0110 X100 1000 in hex 86 48

    05 00          ; NULL (0 Bytes)

    04 40          ; OCTET STRING (0x40 Bytes = 64 bytes
    SHA512 produces a 512-bit (64-byte) hash value

    51 00 41 ... C1 EE C8 is the 64 byte hash value
*/

        // parse DER encoded data
        DerInputStream derReader = new DerInputStream(decryptedSignatureBytes);

        byte[] hashValueFromSignature = null;

        // obtain sequence of entities
        DerValue[] seq = derReader.getSequence(0);
        for (DerValue v : seq)
        
            if (v.getTag() == 4)
            
                hashValueFromSignature = v.getOctetString(); // SHA-512 checksum extracted from decrypted signature bytes
            
        

        MessageDigest md = MessageDigest.getInstance("SHA-512");
        md.update(dataBytes);

        byte[] hashValueCalculated = md.digest();

        boolean manuallyVerified = Arrays.equals(hashValueFromSignature, hashValueCalculated);
        if (manuallyVerified)
        
            System.out.println("Signature manually verified!");
        
        else
        
            System.out.println("Signature could NOT be manually verified!");
        
    

【讨论】:

以上是关于SHA1 和 RSA 与 java.security.Signature 对比 MessageDigest 和 Cipher的主要内容,如果未能解决你的问题,请参考以下文章

Java RSA (SHA1withRSA)签名和验签

AES,SHA1,DES,RSA,MD5区别[转]

RSA/SHA1加密和数字签名算法在开放平台中的应用

[转] AES,SHA1,DES,RSA,MD5区别

Mono不会将SHA1识别为RSA签名的哈希算法

[转]加密算法(DES,AES,RSA,MD5,SHA1,Base64)比较和项目应用