将SHA1和RSA与java.security.Signature和MessageDigest和Cipher结合使用


70

我试图了解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 ...
签名:7177c74bbbb871cc0af92e30d2808ebae146f25d3d

我必须对Signature所做的事情有一个基本的误解-我已经对其进行了跟踪,它似乎正在对MessageDigest对象调用update ,将算法设置为我期望的SHA1,然后获取摘要,然后执行加密。是什么使结果有所不同?

编辑:

Leonidas让我检查了签名方案是否应该按照我认为的方式进行。RFC中定义了两种类型的签名:

其中的第一个(PKCS1)是我上面描述的。它使用哈希函数创建摘要,然后使用私钥对结果进行加密。

所述第二算法使用随机盐值,并且是更安全的,但非确定性。如果重复使用相同的密钥,则由上面的代码产生的签名不会更改,因此我认为它可能不是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();
}

Answers:


53

好的,我已经解决了问题。Leonidas是正确的,不仅是加密的哈希(在Cipher类方法的情况下),还有哈希算法与摘要连接的ID:

  DigestInfo ::= SEQUENCE {
      digestAlgorithm AlgorithmIdentifier,
      digest OCTET STRING
  }

这就是为什么密码和签名加密不同的原因。


Hooray亲自找到了它:)
Leonidas

3
好,迈克 您如何使它们产生相同的结果?
Romulo Pereira

请注意,如果此后结果仍然不正确,则可能是基础加密库对任何加密操作都使用了随机填充。这似乎是一个常见的错误。
Maarten Bodewes,2012年

如果您使用ASN1 / DER检查器作为ASN.1Editor,则可以检测到这类问题
Jaime Hablutzel 2013年

我否决了这个问题,因为它没有提供完整的答案,并且忘记了解释与PKCS#1兼容的填充,该填充也是算法正常工作所必需的。在大多数情况下,PKCS#1是Java的默认值,但是,如果提供程序选择了其他默认值,则代码可能仍然会失败。
Maarten Bodewes

11

要产生相同的结果:

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 }
Maarten Bodewes

我输入了正确的算法字符串,因此您不必猜测默认设置。请注意,当您像这样使用时,Oracle的提供程序和Bouncy Castle会正确使用PKCS#1 v1.5填充来生成签名Cipher。其他提供程序可能不太灵活,而是使用PKCS#1填充进行加密。
Maarten Bodewes

为了概括起见,在11.2.3节或附录B.1中复制了常用哈希的OID,并在PKCS1 aka rfc 2437 3447和8017的9.2.1节中复制了它们的经过预处理的DER编码。他们以为有人可以在实施规范之前先阅读规范。
dave_thompson_085

5

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

4

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


我不确定,不,但是我希望算法能表明它是否会做的不仅仅是这两个操作。我一直在阅读RFC:ietf.org/rfc/rfc3447.txt,据我了解,只是散列,然后加密哈希。GPG是否压缩了加密邮件?
迈克·休斯顿,2009年

2

以@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 Provider或默认的Sun Provider。


1
您可以同时使用"RSA/ECB/PKCS1Padding"这两个提供程序。
Maarten Bodewes

0

我有一个类似的问题,我测试了添加代码并发现了一些有趣的结果。通过添加此代码,我可以推断出,取决于要使用的“提供者”,公司可以有所不同吗?(因为加密中包含的数据并非在所有提供者中都始终相等)。

我的测试结果。

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

IDE Eclipse输出...

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

摘要:62b0a9ef15461c82766fb5bdaae9edbe4ac2e067

DigestInfo:3021300906052b0e03021a0500041462b0a9ef15461c82766fb5bdaae9edbe4ac2e067

签名破译:1ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff003021300906052b0e03021a0500041462b0a9ef15461c82766fb5bdaae9edbe4ac2e067

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));
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.