Search code examples
phpx509

PHP verify signed string using public certificate


Based on the very helpful comments below, I've re-written this question.

using the code at https://www.php.net/manual/en/function.openssl-verify.php as an example:

<?php
// $data is assumed to contain the data to be signed

// fetch certificate from file and ready it
$fp = fopen("path/file.pem", "r");
$cert = fread($fp, 8192);
fclose($fp);

// state whether signature is okay or not
// use the certificate, not the public key
$ok = openssl_verify($data, $signature, $cert);
if ($ok == 1) {
    echo "good";
} elseif ($ok == 0) {
    echo "bad";
} else {
    echo "ugly, error checking signature";
}
?>

We're working on a project where we receive data which contains a "signature". This is known parts of the data (UTF8) which have been signed by the sender using the private x.509 certificate and base64 encoded. As part of the data we receive their public certificate in Base64.

We can contruct the "check data" ($data) from information we are sent, and we're sent the $signature, so using the above code example should work, however...

The example above takes the certificate from the PEM file, but we're just given the Base64 encoded public certificate.

Not having used this sort of thing before, how do I process the Base64 certificate so I can use it as $cert in the above code?

EDIT:

Thanks to @Sammitch for clarification on the Certificate. I can now access the public key from the certificate. However, the openssl_verify is still failing to verify, and I'm wondering if this is because of how the raw unsigned data is put together.

openssl_verify requires the $data to be a string, but in this instance, the data is created as follows:

The signature prefix is a concatenation of the verb, the destination and the date timestamp, so something like:

POST;https://example.com/webhook;2023-10-12T20:53:50+00:00

This is then converted to bytes.

The VB.Net process for doing this is this:

Dim signaturePrefix As String = verb + ";" + Destination + ";" + SignatureDate + ";"
Dim signaturePrefixBytes As Byte() = Encoding.UTF8.GetBytes(signaturePrefix)

Dim originalLength As Integer = signaturePrefixBytes.Length

Array.Resize(signaturePrefixBytes, originalLength + hashBytes.Length)

Array.Copy(hashBytes, 0, signaturePrefixBytes, originalLength, hashBytes.Length)

The hash value of the JSON being sent is also converted to bytes with the two byte arrays then merged. This merged value is then signed using the senders X509 certificate.

So... given that openssl_verify requires $data to be a string, but this is a byte array, how do I use openssl_verify to verify the signature please?


Solution

  • We've got to the bottom of this, and this solution works:

    <?php
    
    // Obtain the necessary headers and context variables
    $sentSignatureBase64 = "rYYN6S87D3###REDACTED###MfVYeWA==";
    $publicCertBase64 = "MIIHA###REDACTED###O0m/kRrA=";
    $signedTimestamp = "2023-10-16T12:00:30+01:00";
    $contentHashBase64 = "PT8Zbb###REDACTED###PeZEZ3Tc=";
    
    $verb = "POST";
    $destination = "https://webhook.site/###REDACTED###";
    $body = "[{\"payload\":{###REDACTED###}}]";
    
    try {
        // Create Certificate
        $public_key_pem="-----BEGIN CERTIFICATE-----\n". wordwrap($publicCertBase64, 64, "\n", false) ."\n-----END CERTIFICATE-----";
        $publicCertificate = openssl_pkey_get_public($public_key_pem);
    
        echo "public_key_pem---------".PHP_EOL;
        echo $public_key_pem.PHP_EOL;
        echo "---------".PHP_EOL;
    
    
        echo "Key Details---------".PHP_EOL;
        $keyData = openssl_pkey_get_details($publicCertificate);
        var_dump($keyData);
        echo "---------".PHP_EOL;
    
        // Get bytes for the sent signature
        $sentSignatureBytes = base64_decode($sentSignatureBase64);
    
        // Build the signature prefix based on the received data
        $receivedSignaturePreifx = $verb . ";" . $destination . ";" . $signedTimestamp . ";";
    echo "\$receivedSignaturePreifx = ".$receivedSignaturePreifx.PHP_EOL;
        // Get the SHA-256 hash of the body
        $body = empty($body) ? "{}" : $body;
        $hashBytes = hash('sha256', $body, true);
    
        // Combine the two sets of bytes
        $combinedBytes = $receivedSignaturePreifx . $hashBytes;
    
        // Verify the signature
        $isSignatureValid = openssl_verify($combinedBytes, $sentSignatureBytes, $publicCertificate, "sha256WithRSAEncryption") === 1;
    
        if (!$isSignatureValid) {
            $problemDetail = array(
                "statusCode" => 401,
                "detail" => "Signature failed validation.",
                "isSignatureValid" => $isSignatureValid
            );
            echo json_encode($problemDetail);
            return;
        }
    
        $results = array(
            "detail" => "Successful verification.",
            "isSignatureValid" => $isSignatureValid
        );
        echo json_encode($results);
    
    } catch (Exception $ex) {
        $problemDetail = array(
            "statusCode" => 500,
            "detail" => "Error while decrypting signature: " . $ex->getMessage()
        );
        echo json_encode($problemDetail);
        return;
    }
    ?>