Skip to content
Advertisement

RSA: Python signed message verified in PHP

I have a 10 character code that I want to sign by my python program, then put both the code as well as the signature in an URL, which then get’s processed by a PHP SLIM API. Here the signature should get verified.

I generated my RSA keys in python like this:

from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import padding
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.serialization import load_pem_private_key
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization


def gen_key():
    private_key = rsa.generate_private_key(
        public_exponent=65537, key_size=2048, backend=default_backend()
    )
    return private_key


def save_key(pk):
    pem_priv = pk.private_bytes(
        encoding=serialization.Encoding.PEM,
        format=serialization.PrivateFormat.PKCS8,
        encryption_algorithm=serialization.NoEncryption()
    )
    with open(os.path.join('.', 'private_key.pem'), 'wb') as pem_out:
        pem_out.write(pem_priv)

    pem_pub = pk.public_key().public_bytes(
        encoding=serialization.Encoding.PEM,
        format=crypto_serialization.PublicFormat.SubjectPublicKeyInfo
    )
    with open(os.path.join('.', 'public_key.pem'), 'wb') as pem_out:
        pem_out.write(pem_pub)


def main():
    priv_key = gen_key()
    save_key(priv_key)

I sign the key like this in python:

private_key = load_key()
pub_key = private_key.public_key()

code = '09DD57CE10'
signature = private_key.sign(
 str.encode(code),
 padding.PSS(
  mgf=padding.MGF1(hashes.SHA256()),
  salt_length=padding.PSS.MAX_LENGTH
 ),
 hashes.SHA256()
)

The url is built like this

my_url = 'https://www.exmaple.com/codes?code={}&signature={}'.format(
        code,
        signature.hex()
    )

Because the signature is a byte object I’m converting it to a string using the .hex() function

Now, in PHP, I am trying to verify the code and signature:

use phpseclib3CryptPublicKeyLoader;
$key = PublicKeyLoader::load(file_get_contents(__DIR__ . "/public_key.pem"));
echo $key->verify($code, pack('h*', $signature)) ? 'yes' : 'no';

I also tried using PHP openssl_verify

$pub_key = file_get_contents(__DIR__ . "/public_key.pem");
$res = openssl_verify($code, pack('n*', $signature), $pub_key, OPENSSL_ALGO_SHA256);

However, it always tells me the signature is wrong, when I obviously know, that in general it is the correct signature. The RSA keys are all the correct and same keys in both python and php. I think the issue is with the signature and how I had to convert it to a string and then back to a bytes like string in both python and php.

Advertisement

Answer

The Python code uses PSS.MAX_LENGTH as the salt length. This value denotes the maximum salt length and is recommended in the Cryptography documentation (s. here):

salt_length (int) – The length of the salt. It is recommended that this be set to PSS.MAX_LENGTH

In RFC8017, which specifies PKCS#1 and thus also PSS, the default value of the salt length is defined as the output length of the hash (s. A.2.3. RSASSA-PSS):

For a given hashAlgorithm, the default value of saltLength is the octet length of the hash value.

Most libraries, e.g. PHPSECLIB, apply for the default value of the salt length the default defined in RFC8017, i.e. the output length of the hash (s. here). Therefore the maximum salt length must be set explicitly. The maximum salt length is given by (s. here):

signature length (bytes) - digest output length (bytes) - 2 = 256 - 32 - 2 = 222 

for a 2048 bits key and SHA256.

Thus, the verification in the PHP code must be changed as follows:

$verified = $key->
            withPadding(RSA::SIGNATURE_PSS)->
            //withHash('sha256')->                  // default
            //withMGFHash('sha256')->               // default
            withSaltLength(256-32-2)->              // set maximum salt length
            verify($code, pack('H*', $signature));  // alternatively hex2bin()

Note that in the posted code of the question h (hex string, low nibble first) is specified in the format string of pack(). I’ ve chosen the more common H (hex string, high nibble first) in my code snippet which is also compatible with Python’s hex(). Ultimately, the format string to choose depends on the encoding applied in the Python code.

Using this change, on my machine, the signature generated with the Python code can be successfully verified with the PHP code.

Alternatively, of course, the salt length of the Python code can be adapted to the output length of the digest (32 bytes in this case).

By the way, a verification with openssl_verify() is not possible, because PSS is not supported.

User contributions licensed under: CC BY-SA
6 People found this is helpful
Advertisement