【问题标题】:How to RSA verify a signature in java that was generated in php如何 RSA 验证在 php 中生成的 java 中的签名
【发布时间】:2026-02-16 08:40:02
【问题描述】:

我们使用 phpseclib 对数据进行公钥签名,而 android java 用于公钥验证。但它一再失败。

生成密钥并通过私钥签名的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 代码需要更改以使用符号而不是手动加密/散列。

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

【问题讨论】:

  • 仅仅从互联网上复制代码永远不会为您提供有效的解决方案,如果这样做,则有 90% 的可能性是不安全的(例如,使用 OAEP 验证签名)。跨度>
  • @MaartenBodewes,Java 实现将由专门的 android 程序员完成,但他因为错误而无法读取密钥而被卡住(他在 java 加密方面没有太多经验),所以我尝试了java代码来模拟和调试错误以帮助他。显然我们会选择更好的 java 安全模型,但现在的首要目标是从 php 服务读取和验证密钥。
  • 好的,如果我的解决方案有效,请告诉我 - 请不要忘记跟进。
  • 作者:正确的做法是永远不要使用 PKCS1,使用 OAEP 进行加密,使用 PSS 进行签名。使用 OAEP 进行签名很奇怪;不要那样做。另外,请确保您使用的是 e=65537。

标签: java php cryptography digital-signature phpseclib


【解决方案1】:

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

以下 sn-p 将创建一个符合 Java JCA 的公钥。然后它将尝试执行(默认)OAEP 解密。

package nl.owlstead.*;

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 实现将失败,因为它不接受用于签名验证的公钥:

OAEP 不能用于签名或验证签名

现在,这必须是我在密码学 API 中遇到的最清晰和信息最丰富的例外之一。您还可以使用 Bouncy Castle 提供程序,这个提供程序将“解密”哈希值。然而,这不是 OAEP 的使用方式,您应该使用 PSS 来验证签名。

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

【讨论】:

  • 针对手动散列/加密和散列/签名的方法进行了尝试。虽然没有错误出现。但验证是否为相同的字符串返回 false。
  • 当然。这是因为 PSS 签名填充与 OAEP 不同。可能不应该显示它来验证相同的加密值。也可以尝试使用 PSS 签名。
  • 最后验证返回true,但是我需要用Signature sig = Signature.getInstance( "SHA256withRSA");替换代码Signature sig = Signature.getInstance( "SHA256withRSAandMGF1");。删除 adMGF1 返回 true。
  • 对于SHA256withRSAandMGF1,您需要在PHP 端执行$rsa-&gt;setHash('sha256'); $rsa-&gt;setMGFHash('sha256'); $rsa-&gt;setSaltLength($hash-&gt;getLength());。 phpseclib 的默认盐长度为 0,调用 setHash 只是设置哈希 - 它不设置 MGF 哈希。
  • 你能发布你的完整代码(Java + PHP)吗?也许将其发布在 pastebin.com 上。我可以在我下班回家后发布我的帖子,但就像我说的那样,它对我有用,所以我的直接猜测是你没有按照我的意图做。
【解决方案2】:

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

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

所以在你的 PHP 代码中你不会使用$rsa-&gt;setPublicKeyFormat(CRYPT_RSA_PUBLIC_FORMAT_PKCS1) - 你会使用$rsa-&gt;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"=&gt;$d['publickey'], "privatekey"=&gt;$d['privatekey'])..

【讨论】:

  • 使用 PKCS#8 格式化的公钥。现在 真的 没有意义,但它似乎仍然有效:)
  • 没有建议不起作用并在验证时返回 false。
  • 您能否发布您的代码 (Java),以便我仔细检查您是否按照我的预期进行操作?也许在 pastebin.com 上这样做,因为您的 OP 已满。谢谢!
  • PKCS8 密钥没有错误:pastebin.com/2GW64G6H PKCS1 密钥错误:pastebin.com/ndSYYjUT 尽管表示它们的字符串不同,但证明这两个密钥是相同的:pastebin.com/z1d7Gqb3 但话虽如此,在您的原始代码中,它仍然会在验证时返回 false,因为在您的原始代码中,您试图验证已被 OAEP 加密的字符串中的 PKCS1 签名。
  • 另外,我这篇文章的目的不是告诉你如何让签名返回 true,而是如何消除 InvalidKeySpecException 异常。 Maarten Bodewes 的回复解决了您尝试验证签名时遇到的问题。