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

2

1

We are using phpseclib for Public key signing of data and android java is used for Public key verification. But it repeatedtly failed.

PHP Code For generating keys and signing by private key

 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 Code

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

I compiled java code by

javac -cp commons-codec-1.10.jar:. PubCheck.java
java -cp commons-codec-1.10.jar:. PubCheck

Then found following exception

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

Disclaimer : I have zero knowledge about java. all the code I try found from net.

UPDATE : Issue finally solved and java code able to verify by help from Maarten Bodewes. Code he provided works with one change I need to pass PKCS1 from phpseclib So I changed

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

to

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

PHP Code need changes for using sign instead of manually 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);
Woodman answered 30/12, 2015 at 4:22 Comment(4)
Just copying code from the internet will never get you a working solution, and if it does there is a 90% chance that it won't be secure (e.g. using OAEP to verify signatures).Syllogism
@MaartenBodewes, Java implementation will be done by dedicated android programmer, but he is stuck as he is unable to read keys due to errors ( he is not much experienced in java encryption ), So I did try java code to simulate and debug errors for help him. Obviously we would go for better java security model, But for now first target is to read and verify keys from php service.Woodman
OK, let me know if my solution below worked - don't forget to follow up please.Syllogism
By the by: The correct thing to do is NEVER use PKCS1, use OAEP for encrypting and PSS for signing. Using OAEP for signing is weird; don't do that. Also, make sure you're using e=65537.Hamblin
S
3

PKCS#1 keys are almost but not completely the same as X.509 keys.

The following snippet will create a Java JCA compliant public key. It will then try and perform the (default) OAEP decryption.

package nl.owlstead.stackoverflow;

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

The SunJCE implementation of OAEP will fail because it will not accept the public key for signature verification:

OAEP cannot be used to sign or verify signatures

Now that has to be one of the most clear and informative exceptions I've met in a cryptography API. You can also use the Bouncy Castle provider and this one will "decrypt" the hash value. That's however not how OAEP should be used, you should be using PSS to verify signatures.

You should be using the PHP RSA sign method instead, using setHash to setup SHA-256.

Syllogism answered 30/12, 2015 at 16:12 Comment(10)
Tried it against manually hashed / encrypted and hash/signed approach. While no error comes. But verify return false for same string.Woodman
Of course. That's because PSS the signature padding is not the same as OAEP. Should not have shown it to verify the same encrypted value maybe. Try and sign with PSS as well.Syllogism
Finally verify return true, However I need to to replace code Signature sig = Signature.getInstance( "SHA256withRSAandMGF1"); with Signature sig = Signature.getInstance( "SHA256withRSA"); . Removing adMGF1 return true.Woodman
Well, yes, if you replace the default of Crypt_RSA with $rsa->setSignatureMode(CRYPT_RSA_SIGNATURE_PKCS1); then you need "SHA256withRSA". PSS with MGF1 however seems to be the default.Syllogism
@Mararten , No I tried with default ( no signature mode ) with SHA256withRSAandMGF1 but that not worked and return false. Only mode which is working CRYPT_RSA_SIGNATURE_PKCS1 + SHA256withRSAWoodman
Fortunately PKCS#1 v1.5 padding - what you are using now - is also secure. It's strange though that PSS/MGF1 doesn't work. Maybe one of the two implementations also changes the internal hash of the MGF1 instead of just the hash over the data. But for now you should be golden.Syllogism
For SHA256withRSAandMGF1 you'd need to do $rsa->setHash('sha256'); $rsa->setMGFHash('sha256'); $rsa->setSaltLength($hash->getLength()); on the PHP side. phpseclib's default salt length is 0 and calling setHash just sets the hash - it doesn't set the MGF hash.Rothwell
@neubert, I already did and tried but for unknown reasons code always return false during verify.Woodman
Can you post your full code (Java + PHP)? Maybe post it on pastebin.com. I can post mine when I get home from work but, like I said, it worked for me, so my immediate guess is that you didn't do what I had intended.Rothwell
My code: pastebin.com/btv7KNsR verifyBC (eg. SHA256withRSAandMGF1) works. The same code in PHP (using PSS mode) validating the signature: pastebin.com/quqjQNGZRothwell
R
1

Although Martin's answer works there's another way to get rid of the InvalidKeySpecException exception.

In your original code pkey is a PKCS1 formatted RSA private key. It needs to be a PKCS8 formatted private key to work with X509EncodedKeySpec (which corresponds to an X509 cert's SubjectPublicKeyInfo). It also needs to be base64 decoded.

So in your PHP code you wouldn't do $rsa->setPublicKeyFormat(CRYPT_RSA_PUBLIC_FORMAT_PKCS1) - you'd do $rsa->setPublicKeyFormat(CRYPT_RSA_PUBLIC_FORMAT_PKCS8).

I converted your PKCS1 key to PKCS8 myself and got this:

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

You'd, of course, need to remove your existing pkey and encodedPublicKey assignments.

Also, you could do return $d instead of return array("publickey"=>$d['publickey'], "privatekey"=>$d['privatekey']) in your PHP code..

Rothwell answered 31/12, 2015 at 17:16 Comment(5)
A public key formatted using PKCS#8. Now that really doesn't make sense, but it seems to work anyway :)Syllogism
No suggestion do not worked and return false at verify.Woodman
Can you post your code (Java) so I can double check to see if you did as I had intended? Maybe do so on pastebin.com since your OP is full. Thanks!Rothwell
No error with a PKCS8 key: pastebin.com/2GW64G6H Error with a PKCS1 key: pastebin.com/ndSYYjUT Proof that the two keys are the same despite the strings representing them are different: pastebin.com/z1d7Gqb3 But that said, in your original code, it'd still return false at verify because in your original code you were trying to verify a PKCS1 signature in a string that had been OAEP encrypted.Rothwell
Also, the goal of my post wasn't to tell you how to make the signature return true - it was how to eliminate the InvalidKeySpecException exception. Maarten Bodewes response addressed the problems with your attempts to verify the signature.Rothwell

© 2022 - 2024 — McMap. All rights reserved.