如何 RSA 验证在 php 中生成的 java 中的签名

How to RSA verify a signature in java that was generated in php

我们使用 phpseclib 进行 Public 数据密钥签名,android java 用于 Public 密钥验证。但屡屡失败。

PHP 生成密钥并通过私钥签名的代码

 include_once("phpseclib/autoload.php");

 function getKeys($keysize=2048){

        $rsa = new Crypt_RSA();
        //$rsa->setPublicKeyFormat(CRYPT_RSA_PUBLIC_FORMAT_OPENSSH);
        //$rsa->setPublicKeyFormat(CRYPT_RSA_PRIVATE_FORMAT_PKCS1);
        $rsa->setPrivateKeyFormat(CRYPT_RSA_PRIVATE_FORMAT_PKCS8);
        $rsa->setPublicKeyFormat(CRYPT_RSA_PUBLIC_FORMAT_PKCS1);
        $d = $rsa->createKey($keysize);
        return array("publickey"=>$d['publickey'], "privatekey"=>$d['privatekey']);

    }

    function encryptdata($message, $encryptionKey){

        $rsa = new Crypt_RSA();
        //$rsa->setPublicKeyFormat(CRYPT_RSA_PRIVATE_FORMAT_PKCS1);
        $rsa->setPrivateKeyFormat(CRYPT_RSA_PRIVATE_FORMAT_PKCS8);
        $rsa->setPublicKeyFormat(CRYPT_RSA_PUBLIC_FORMAT_PKCS1);

        //$rsa->setPublicKeyFormat(CRYPT_RSA_PUBLIC_FORMAT_OPENSSH);
        $rsa->loadKey($encryptionKey); // public key
        return $rsa->encrypt($message);
    } 

    function decryptdata($message, $decryptionKey){

        $rsa = new Crypt_RSA();
//        $rsa->setPublicKeyFormat(CRYPT_RSA_PUBLIC_FORMAT_OPENSSH);
//        $rsa->setPublicKeyFormat(CRYPT_RSA_PRIVATE_FORMAT_PKCS1);
        $rsa->setPrivateKeyFormat(CRYPT_RSA_PRIVATE_FORMAT_PKCS8);
        $rsa->setPublicKeyFormat(CRYPT_RSA_PUBLIC_FORMAT_PKCS1);

        $rsa->loadKey($decryptionKey); // private key
        return $rsa->decrypt($message);

    }    

    $keys = getKeys();
    file_put_contents("key.pub", $keys["publickey"]);
    file_put_contents("key.priv", $keys["privatekey"]);

    $publickey = file_get_contents("key.pub");
    $privatekey = file_get_contents("key.priv");

    //print_r($keys);
    $string = "Hi I m here";
    $hash = hash("sha256", $string);

    $encdata = encryptdata($hash, $privatekey);
    echo $base_encdata  = base64_encode($encdata);

JAVA代码

import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.io.UnsupportedEncodingException;
import org.apache.commons.codec.binary.Base64;
import java.security.spec.X509EncodedKeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.KeyFactory;
import java.security.Signature;
import java.security.PublicKey;
import java.security.SignatureException;
import java.security.spec.InvalidKeySpecException;
import java.lang.String;
class PubCheck {
    public static boolean verify(String message, String signature, PublicKey publicKey) throws SignatureException{
    try {
        Signature sign = Signature.getInstance("SHA1withRSA");
        sign.initVerify(publicKey);
        sign.update(message.getBytes("UTF-8"));
        return sign.verify(Base64.decodeBase64(signature.getBytes("UTF-8")));
    } catch (Exception ex) {
        throw new SignatureException(ex);
    }
    }

    public static void main(String[] args) 
    throws NoSuchAlgorithmException, UnsupportedEncodingException, InvalidKeySpecException, SignatureException 
    {
    String plainData = "Hi I m here";
    String pkey = "MIIBCgKCAQEA2tF2g/muNw9xKTVcIkjUMvMhygtIW49yo1PgbwqDQ/w9MSfEARtYYF6Tenfz0twaR/eI14GXmlIffflORe4eaSuMBhwQFOIKU/1+v1BV3RLqGGblvHTVaMVm49AGiqxNnh1LBbcSrC5UhMqlL/HGiku0oYsbjLzwcLc5ac6aBQVD60wWGNm1g26lRQGRbCLqxVfcWKT3AMvEQK3cEx/En7/5Vg1V8xnJraNMrO8UGnaX8LLJFzYJiSCEShh7F+pMHbf4MaBekw7Aaf5hPJtczNsR137R92Be3OP4idI5NLmTV+Pi1DWlxhjEhswKH88SP+gsW31gS7B/ddECUqewQwIDAQAB";

    String data = "aP0nuYYA1hE5odsCkR/DcdRbBvO2Z8IOlqXf/bKZJiG8HELIop90Vno1dKC1qyHEAOXy0gtH7GtJamzoBjDZmHPT6eto9EZP/xE7xZ8L05kjp0z2thLqO7on4C6DrG++TK1j+E3T7V0UeU874WIB0AEVzu1XUKFW6aeuU67a/gdn8N2n7N/WXtlyNSVZXg8f4PeUhGvFJrhINZT7BuMMZj1gZs4wMJPAICwfvVeg02RPH0N3Ybf2iVgRuZlmtQXGTyBlCxe9ybdHzuQM6nXghpLNmaOzCypb+yVs3Da7E0b3/fKQ7JqPSquWex2ERZbIMSTC6oCzc1rOF6iKVAd92Q==";

    byte[] encodedPublicKey = pkey.getBytes( "utf-8" );
    //System.out.println(new String(encodedPublicKey, "UTF-8") + "\n");

    X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec( encodedPublicKey );
    //PKCS8EncodedKeySpec publicKeySpec = new PKCS8EncodedKeySpec(encodedPublicKey);

    KeyFactory keyFactory = KeyFactory.getInstance( "RSA" );

    PublicKey publicKey = keyFactory.generatePublic( publicKeySpec );

    boolean retvar = verify(plainData, data, publicKey);

    // 3 - verifying content with signature and content :
    /*Signature sig = Signature.getInstance( "SHA256withRSA" );
    sig.initVerify( publicKey );
    sig.update( data.getBytes( ) );
    ret = sig.verify( sign.getBytes( ) );*/

    //byte[] decoded = Base64.decodeBase64(data);
    }
}

我通过

编译了 java 代码
javac -cp commons-codec-1.10.jar:. PubCheck.java
java -cp commons-codec-1.10.jar:. PubCheck

然后发现如下异常

Exception in thread "main"   java.security.spec.InvalidKeySpecException: java.security.InvalidKeyException: invalid key format
at sun.security.rsa.RSAKeyFactory.engineGeneratePublic(RSAKeyFactory.java:205)
at java.security.KeyFactory.generatePublic(KeyFactory.java:334)
at PubCheck.main(PubCheck.java:67)
Caused by: java.security.InvalidKeyException: invalid key format
at sun.security.x509.X509Key.decode(X509Key.java:387)
at sun.security.x509.X509Key.decode(X509Key.java:403)
at sun.security.rsa.RSAPublicKeyImpl.<init>(RSAPublicKeyImpl.java:83)
at  sun.security.rsa.RSAKeyFactory.generatePublic(RSAKeyFactory.java:298)
at sun.security.rsa.RSAKeyFactory.engineGeneratePublic(RSAKeyFactory.java:201)
... 2 more

免责声明:我对 java 的了解为零。我尝试的所有代码都是从网上找到的。

更新:问题终于解决了,java 代码可以在 Maarten Bodewes 的帮助下进行验证。他提供的代码有一个变化我需要从 phpseclib 传递 PKCS1 所以我改变了

Signature sig = Signature.getInstance( "SHA256withRSAandMGF1");

Signature sig = Signature.getInstance( "SHA256withRSA");

PHP 需要更改代码以使用符号而不是手动 encrypt/hashing。

function getKeys($keysize=2048){

    $rsa = new Crypt_RSA();
    $rsa->setPrivateKeyFormat(CRYPT_RSA_PRIVATE_FORMAT_PKCS8);
    $rsa->setPublicKeyFormat(CRYPT_RSA_PUBLIC_FORMAT_PKCS1);
    $d = $rsa->createKey($keysize);
    return array("publickey"=>$d['publickey'], "privatekey"=>$d['privatekey']);

}


$string = "Hi I m here";
/*
$keys = getKeys();
file_put_contents("key1.pub", $keys["publickey"]);
file_put_contents("key1.priv", $keys["privatekey"]);
die;*/

$publickey = file_get_contents("key1.pub");
$privatekey = file_get_contents("key1.priv");

$hash = new Crypt_Hash('sha256');
$rsa = new Crypt_RSA();    
$rsa->loadKey($privatekey);
$rsa->setSignatureMode(CRYPT_RSA_ENCRYPTION_PKCS1);
$rsa->setHash('sha256');

$signature = $rsa->sign($string);
echo base64_encode($signature);

PKCS#1 密钥与 X.509 密钥几乎但不完全相同。

以下代码段将创建一个 Java JCA 兼容 public 密钥。然后它将尝试执行(默认)OAEP 解密。

package nl.owlstead.Whosebug;

import static java.nio.charset.StandardCharsets.UTF_8;

import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.Security;
import java.security.Signature;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.util.Base64;
import java.util.Base64.Decoder;

import javax.crypto.Cipher;

import org.bouncycastle.jce.provider.BouncyCastleProvider;

public class PKCS1PublicKey {

    public static RSAPublicKey fromPKCS1Encoding(byte[] pkcs1EncodedPublicKey) {
        // --- parse public key ---
        org.bouncycastle.asn1.pkcs.RSAPublicKey pkcs1PublicKey;
        try {
            pkcs1PublicKey = org.bouncycastle.asn1.pkcs.RSAPublicKey
                    .getInstance(pkcs1EncodedPublicKey);
        } catch (Exception e) {
            throw new IllegalArgumentException(
                    "Could not parse BER PKCS#1 public key structure", e);
        }

        // --- convert to JCE RSAPublicKey
        RSAPublicKeySpec spec = new RSAPublicKeySpec(
                pkcs1PublicKey.getModulus(), pkcs1PublicKey.getPublicExponent());
        KeyFactory rsaKeyFact;
        try {
            rsaKeyFact = KeyFactory.getInstance("RSA");
        } catch (NoSuchAlgorithmException e) {
            throw new IllegalStateException("RSA KeyFactory should be available", e);
        }
        try {
            return (RSAPublicKey) rsaKeyFact.generatePublic(spec);
        } catch (InvalidKeySpecException e) {
            throw new IllegalArgumentException(
                    "Invalid RSA public key, modulus and/or exponent invalid", e);
        }
    }

    public static void main(String[] args) throws Exception {
        Security.addProvider(new BouncyCastleProvider());

        String pkey = "MIIBCgKCAQEA2tF2g/muNw9xKTVcIkjUMvMhygtIW49yo1PgbwqDQ/w9MSfEARtYYF6Tenfz0twaR/eI14GXmlIffflORe4eaSuMBhwQFOIKU/1+v1BV3RLqGGblvHTVaMVm49AGiqxNnh1LBbcSrC5UhMqlL/HGiku0oYsbjLzwcLc5ac6aBQVD60wWGNm1g26lRQGRbCLqxVfcWKT3AMvEQK3cEx/En7/5Vg1V8xnJraNMrO8UGnaX8LLJFzYJiSCEShh7F+pMHbf4MaBekw7Aaf5hPJtczNsR137R92Be3OP4idI5NLmTV+Pi1DWlxhjEhswKH88SP+gsW31gS7B/ddECUqewQwIDAQAB";
        Decoder decoder = Base64.getDecoder();
        byte[] dpkey = decoder.decode(pkey);
        RSAPublicKey publicKey = fromPKCS1Encoding(dpkey);

        String plainData = "Hi I m here";
        String data = "aP0nuYYA1hE5odsCkR/DcdRbBvO2Z8IOlqXf/bKZJiG8HELIop90Vno1dKC1qyHEAOXy0gtH7GtJamzoBjDZmHPT6eto9EZP/xE7xZ8L05kjp0z2thLqO7on4C6DrG++TK1j+E3T7V0UeU874WIB0AEVzu1XUKFW6aeuU67a/gdn8N2n7N/WXtlyNSVZXg8f4PeUhGvFJrhINZT7BuMMZj1gZs4wMJPAICwfvVeg02RPH0N3Ybf2iVgRuZlmtQXGTyBlCxe9ybdHzuQM6nXghpLNmaOzCypb+yVs3Da7E0b3/fKQ7JqPSquWex2ERZbIMSTC6oCzc1rOF6iKVAd92Q==";
        byte[] ciphertext = decoder.decode(data);

        // this will fail of course if the "signature" was generated using OAEP - use PSS signatures instead (see comments below)
        verifyBC(publicKey, plainData, ciphertext);
        System.out.flush();
        decryptBC(publicKey, plainData, ciphertext);
        System.out.flush();
        decryptSun(publicKey, plainData, ciphertext);
        System.out.flush();
    }

    private static void decryptBC(RSAPublicKey publicKey, String plainData,
            byte[] ciphertext) throws Exception {
        Cipher oaep = Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding", "BC");
        // this *should* fail
        oaep.init(Cipher.DECRYPT_MODE, publicKey);
        byte[] plaintext = oaep.doFinal(ciphertext);
        System.out.println(new String(plaintext, UTF_8));
    }

    private static void decryptSun(RSAPublicKey publicKey, String plainData,
            byte[] ciphertext) throws Exception {
        Cipher oaep = Cipher.getInstance("RSA/ECB/OAEPWithSHA1AndMGF1Padding", "SunJCE");
        // this fails beautifully
        oaep.init(Cipher.DECRYPT_MODE, publicKey);
        byte[] plaintext = oaep.doFinal(ciphertext);
        System.out.println(new String(plaintext, UTF_8));
    }

    private static void verifyBC(RSAPublicKey publicKey, String plainData,
            byte[] ciphertext) throws Exception {
        // what should work (for PKCS#1 v1.5 signatures), requires Bouncy Castle provider
        Signature sig = Signature.getInstance( "SHA256withRSAandMGF1");
        sig.initVerify(publicKey);
        sig.update(plainData.getBytes(UTF_8));
        System.out.println(sig.verify(ciphertext));
    }
}

OAEP 的 SunJCE 实现将失败,因为它不接受用于签名验证的 public 密钥:

OAEP cannot be used to sign or verify signatures

现在这一定是我在密码学中遇到的最清晰、信息量最大的异常之一 API。您还可以使用 Bouncy Castle 提供程序,此提供程序将 "decrypt" 哈希值。然而,这不是 OAEP 的使用方式,您应该使用 PSS 来验证签名。

您应该改用 PHP RSA sign 方法,使用 setHash 设置 SHA-256。

尽管 Martin 的回答有效,但还有另一种方法可以摆脱 InvalidKeySpecException 异常。

在您的原始代码中,pkey 是 PKCS1 格式的 RSA 私钥。它需要是 PKCS8 格式的私钥才能与 X509EncodedKeySpec 一起使用(对应于 X509 证书的 SubjectPublicKeyInfo)。还需要进行base64解码

所以在你的 PHP 代码中你不会做 $rsa->setPublicKeyFormat(CRYPT_RSA_PUBLIC_FORMAT_PKCS1) - 你会做 $rsa->setPublicKeyFormat(CRYPT_RSA_PUBLIC_FORMAT_PKCS8).

我自己将您的 ​​PKCS1 密钥转换为 PKCS8 并得到了这个:

String pkey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2tF2g/muNw9xKTVcIkjU" +
                 "MvMhygtIW49yo1PgbwqDQ/w9MSfEARtYYF6Tenfz0twaR/eI14GXmlIffflORe4e" +
                 "aSuMBhwQFOIKU/1+v1BV3RLqGGblvHTVaMVm49AGiqxNnh1LBbcSrC5UhMqlL/HG" +
                 "iku0oYsbjLzwcLc5ac6aBQVD60wWGNm1g26lRQGRbCLqxVfcWKT3AMvEQK3cEx/E" +
                 "n7/5Vg1V8xnJraNMrO8UGnaX8LLJFzYJiSCEShh7F+pMHbf4MaBekw7Aaf5hPJtc" +
                 "zNsR137R92Be3OP4idI5NLmTV+Pi1DWlxhjEhswKH88SP+gsW31gS7B/ddECUqew" +
                 "QwIDAQAB";
byte[] encodedPublicKey = Base64.decodeBase64(pkey);

当然,您需要删除现有的 pkey 和 encodedPublicKey 分配。

此外,您可以在 PHP 代码中使用 return $d 而不是 return array("publickey"=>$d['publickey'], "privatekey"=>$d['privatekey'])..