2

I have a simple string of text that needs to be signed inside a Javascript and verified on the server (PHP). To start my test I first created a key pair:

// Function to generate a new RSA key pair
async function generateKeyPair() {
    const keyPair = await window.crypto.subtle.generateKey(
        {
            name: "RSASSA-PKCS1-v1_5",
            modulusLength: 2048,
            publicExponent: new Uint8Array([0x01, 0x00, 0x01]), // 65537
            hash: { name: "SHA-256" },
        },
        true,
        ["sign", "verify"]
    );

    const publicKey = await window.crypto.subtle.exportKey("spki", keyPair.publicKey);
    const privateKey = await window.crypto.subtle.exportKey("pkcs8", keyPair.privateKey);

    return {
        privateKey: privateKey,
        publicKey: publicKey,
    };
}

I obtained a private and public key:

privateKey: "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDw4Ees4C+vsTUQodgWKIsj3Ni67RG3ny9xY1KjCaatu2o/ev5hS4yrxxWLZAFU9mt/rfNmzzby3mqlWPWm8Df91Mue6wNTsN2yMnHw+XvcvovCngSTH4H2zY+uAhEiG+u+vzGqbzxm0JB3ybX5kYEMK2iKoALq1ASJ781gyy7AsCf/Ck+OvIE4in1kNm4a5NUgbyuflWerMIB7FUQ7h/+XlLn3F2bvC1SWWxKsmQ/dF5fYpZaAV2KvVw2LMnkWdU536an9vxj5LZIJyzfNQv/foNGcUh8iT1tLe8jV4eYrAcqiLnG+iZFFc3X5F33WUILmRvCg11bec6ic1NwuY8eDAgMBAAECggEAQq7kSNCbgvj85sjXSHMa6ee2vDDrKblQ6gQEGYyPbyMmK8LB72951wg7R6Z80+eQJP2kF38gCCZYwcOZ5gg0h/nEEQ+gkSeyiCV886gtiRPbHxqdy5j6YrfPoe2Cjr3KCrllZ3h58UCl7fOShC+q2RKfU1ku1ZGyW/leEwDMxZy1PISHFHtmd43LdrkWgyNk4TIpNRzizx+gxNeyQUEZDfkUu4mFP/weWM26lLyaE+RkPqvFnLjXckvgno1bY8Hq2yywVkyvfBo0tQvVBtNP5WTEyOvNGylc46pnVBODrSUn5q4ZNdi56fd7WFPBFySAVQiA0uLgaWYOuBUGMyc5CQKBgQD+wcyYL2IgA5O2c6Jp9cJV9xQVvWEQ/sVUDJgRVhOxAqw9r2LmUe7tRnWYEI4Sz9g/ejFq8fkL+h2lQbGB+ZKlu5EVHrmfuYf3zo80QKMWC1XKbYnw0HKkMlOqxiMYyu6PqFX59icmcZ58k1m9h2br5f7GGGWAFY8yFgRUIUR6FwKBgQDyDSSbH7WoqUUhYNvY9wKUUYM8uZSAC1TPfuR/ZvAec3cZxMJyOnY88MPOh63vUMzTUt6AAyps2EFPa0UGuysevMaXSL+MAQQzDfnEC2KfeRqkVOKYPrjjjxIl5mQJCacpB7rdzLszmtJJ9G99lTqeGuVa3mhlJupqckYbbdO9dQKBgQD9zf4TMEHGO0oSX6nTfvCZzIrKDd6CnA/j6JgnzWXY2BzZZ75UUBSFd8j4MqYYv9FljEtnjKLd99VJKuW54/bh/rhQHkg4hRKdI8EwAaV49NoHzpG6xTExvKH2ZWfZ73M01DSzzzS57EBFRFgHpro3EvB8UxnsPY5oC99MIcijCQKBgBn16OguVXh6dyymS84QaBlqSK4ZpWC6VmVO0ckMTFKnxa1g2g4QUSAmHoonKTOSsfU0XSLTtBgqdY7EDYo0RuKsEoylQ84LSd0D8bbiFbjO71mStR7pE0Fs1eB0vmPtwhz3dEZXr/hP8Z/29II+oCPW9KRzWDUJIHk8OmK0u9IFAoGAEOWhm/zaXMNJ+oBcvBbCKTZ0XInzvV4SqhC6Bj9aC8wqCe5QKyKl9HglG9J+o3D+hIEcMXGvIv1KB3xDStCQQKcDOrD/8tGZtstSONaNzeGg0hUY9SKd7R2wMPEWufzccFE+zVG5hHUg+eQrnzXdXkG8hW1QxQxgoDkC3DNVsnE="
publicKey: "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8OBHrOAvr7E1EKHYFiiLI9zYuu0Rt58vcWNSowmmrbtqP3r+YUuMq8cVi2QBVPZrf63zZs828t5qpVj1pvA3/dTLnusDU7DdsjJx8Pl73L6Lwp4Ekx+B9s2PrgIRIhvrvr8xqm88ZtCQd8m1+ZGBDCtoiqAC6tQEie/NYMsuwLAn/wpPjryBOIp9ZDZuGuTVIG8rn5VnqzCAexVEO4f/l5S59xdm7wtUllsSrJkP3ReX2KWWgFdir1cNizJ5FnVOd+mp/b8Y+S2SCcs3zUL/36DRnFIfIk9bS3vI1eHmKwHKoi5xvomRRXN1+Rd91lCC5kbwoNdW3nOonNTcLmPHgwIDAQAB"

This is the script I'm using to sign the data (string) using the privatekey:

// Function to sign the data
async function signRequestData(data, privateKey) {
    const encoder = new TextEncoder();
    const dataBuffer = encoder.encode(data);
    const hashBuffer = await window.crypto.subtle.digest("SHA-256", dataBuffer);

    const signatureBuffer = await window.crypto.subtle.sign(
        { name: "RSASSA-PKCS1-v1_5" },
        privateKey,
        hashBuffer
    );

    const signatureArray = new Uint8Array(signatureBuffer);
    const signatureBase64 = btoa(String.fromCharCode.apply(null, signatureArray));

    return signatureBase64;
}

Then I created a script to verify the data, still in Javascript, to validate what I was doing:

// Function to verify the signature
async function verifySignature(data, signatureBase64, publicKey) {
    const encoder = new TextEncoder();
    const dataBuffer = encoder.encode(data);
    const signatureArray = new Uint8Array(atob(signatureBase64).split("").map((c) => c.charCodeAt(0)));
    const hashBuffer = await window.crypto.subtle.digest("SHA-256", dataBuffer);
    const isSignatureValid = await window.crypto.subtle.verify(
        { name: "RSASSA-PKCS1-v1_5" },
        publicKey,
        signatureArray,
        hashBuffer
    );

    return isSignatureValid;
}

The script returned true so I moved on the next step, verifying the data in PHP:

    // Function to verify the signature
    public function verifySignature($data, $signatureBase64, $publicKey) {      
        // Import public key
        $publicKeyResource = openssl_pkey_get_public("-----BEGIN PUBLIC KEY-----" . "\n" . $publicKey . "\n" . "-----END PUBLIC KEY-----");

        if ($publicKeyResource === false) {
            // Handle error (unable to import public key)
            die("Error importing public key");
        }

        // Verify the signature
        $isSignatureValid = openssl_verify($data, $signatureBase64, $publicKeyResource, OPENSSL_ALGO_SHA256);

        // Free the public key resource
        openssl_free_key($publicKeyResource);

        return ($isSignatureValid === 1)
    }   

This script is never returning 1 indicating 'valid'. I'm not sure if the problem is how the keys are generated. I can generate the pair in PHP if this helps. @Topaco this is the whole question I was talking about.

edit: I added return openssl_error_string(); in case of 0 and here is the result: error:02000077:rsa routines::wrong signature length on a second run I got error:0480006C:PEM routines::no start line

10
  • Is it returning 0, -1, or something else? Commented Feb 5, 2024 at 21:33
  • @kmoser Always 0. I would have been happy with -1 at least I would have had something to look for. Commented Feb 5, 2024 at 21:45
  • Stupid question: have you confirmed $publicKey doesn't already start with "-----BEGIN PUBLIC KEY-----"? Commented Feb 5, 2024 at 21:50
  • 1
    Then you should adapt the JavaScript code. There, it is currently hashed twice, which leads to the incompatibility (in addition to the encoding bug regarding the signature). Commented Feb 5, 2024 at 23:32
  • 1
    The JavaScript code hashes twice for both signing and verifying, which is why verification works. However, the PHP code hashes only once, which is why the verification of the signature of the JavaScript code fails. Please see my answer for more details. Commented Feb 6, 2024 at 13:47

2 Answers 2

0

In the JavaScript code, hashing is performed twice, once explicitly with the digest() function and once implicitly by the sign() function. As the double hashing is performed on the JavaScript side during both signing and verification, verification works on the JavaScript side.

In contrast, the PHP code hashes once, namely implicitly in openssl_verify(), so that as a result of this different hashing strategy both codes are incompatible. For this reason, verification with the PHP code fails. To eliminate this incompatability, hashing must be carried out consistently.

Since the double hashing is unnecessary, the JavaScript side should be adapted and the double hashing removed, i.e. in the JavaScript code dataBuffer should be passed directly to sign() and verify().

In addition to this hashing issue, as already mentioned in the other answer, the Base64 encoded signature in the JavaScript code must be Base64 decoded in the PHP code.


The fixed JavaScript code (with only single hashing) is:

(async () => {

var pkcs8DerB64 = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDw4Ees4C+vsTUQodgWKIsj3Ni67RG3ny9xY1KjCaatu2o/ev5hS4yrxxWLZAFU9mt/rfNmzzby3mqlWPWm8Df91Mue6wNTsN2yMnHw+XvcvovCngSTH4H2zY+uAhEiG+u+vzGqbzxm0JB3ybX5kYEMK2iKoALq1ASJ781gyy7AsCf/Ck+OvIE4in1kNm4a5NUgbyuflWerMIB7FUQ7h/+XlLn3F2bvC1SWWxKsmQ/dF5fYpZaAV2KvVw2LMnkWdU536an9vxj5LZIJyzfNQv/foNGcUh8iT1tLe8jV4eYrAcqiLnG+iZFFc3X5F33WUILmRvCg11bec6ic1NwuY8eDAgMBAAECggEAQq7kSNCbgvj85sjXSHMa6ee2vDDrKblQ6gQEGYyPbyMmK8LB72951wg7R6Z80+eQJP2kF38gCCZYwcOZ5gg0h/nEEQ+gkSeyiCV886gtiRPbHxqdy5j6YrfPoe2Cjr3KCrllZ3h58UCl7fOShC+q2RKfU1ku1ZGyW/leEwDMxZy1PISHFHtmd43LdrkWgyNk4TIpNRzizx+gxNeyQUEZDfkUu4mFP/weWM26lLyaE+RkPqvFnLjXckvgno1bY8Hq2yywVkyvfBo0tQvVBtNP5WTEyOvNGylc46pnVBODrSUn5q4ZNdi56fd7WFPBFySAVQiA0uLgaWYOuBUGMyc5CQKBgQD+wcyYL2IgA5O2c6Jp9cJV9xQVvWEQ/sVUDJgRVhOxAqw9r2LmUe7tRnWYEI4Sz9g/ejFq8fkL+h2lQbGB+ZKlu5EVHrmfuYf3zo80QKMWC1XKbYnw0HKkMlOqxiMYyu6PqFX59icmcZ58k1m9h2br5f7GGGWAFY8yFgRUIUR6FwKBgQDyDSSbH7WoqUUhYNvY9wKUUYM8uZSAC1TPfuR/ZvAec3cZxMJyOnY88MPOh63vUMzTUt6AAyps2EFPa0UGuysevMaXSL+MAQQzDfnEC2KfeRqkVOKYPrjjjxIl5mQJCacpB7rdzLszmtJJ9G99lTqeGuVa3mhlJupqckYbbdO9dQKBgQD9zf4TMEHGO0oSX6nTfvCZzIrKDd6CnA/j6JgnzWXY2BzZZ75UUBSFd8j4MqYYv9FljEtnjKLd99VJKuW54/bh/rhQHkg4hRKdI8EwAaV49NoHzpG6xTExvKH2ZWfZ73M01DSzzzS57EBFRFgHpro3EvB8UxnsPY5oC99MIcijCQKBgBn16OguVXh6dyymS84QaBlqSK4ZpWC6VmVO0ckMTFKnxa1g2g4QUSAmHoonKTOSsfU0XSLTtBgqdY7EDYo0RuKsEoylQ84LSd0D8bbiFbjO71mStR7pE0Fs1eB0vmPtwhz3dEZXr/hP8Z/29II+oCPW9KRzWDUJIHk8OmK0u9IFAoGAEOWhm/zaXMNJ+oBcvBbCKTZ0XInzvV4SqhC6Bj9aC8wqCe5QKyKl9HglG9J+o3D+hIEcMXGvIv1KB3xDStCQQKcDOrD/8tGZtstSONaNzeGg0hUY9SKd7R2wMPEWufzccFE+zVG5hHUg+eQrnzXdXkG8hW1QxQxgoDkC3DNVsnE=";
var pkcs8Der = Uint8Array.from(window.atob(pkcs8DerB64), c => c.charCodeAt(0));

var data = "The quick brown fox jumps over the lazy dog";
var privateKey = await window.crypto.subtle.importKey("pkcs8", pkcs8Der, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, true, ["sign"]);
var signature = await signRequestData(data, privateKey);
console.log(signature);

// Function to sign the data
async function signRequestData(data, privateKey) {
    const encoder = new TextEncoder();
    const dataBuffer = encoder.encode(data);
    const signatureBuffer = await window.crypto.subtle.sign(
        { name: "RSASSA-PKCS1-v1_5" },
        privateKey,
        dataBuffer  // Fix: apply the unhashed data
    );
    const signatureArray = new Uint8Array(signatureBuffer);
    const signatureBase64 = btoa(String.fromCharCode.apply(null, signatureArray));
    return signatureBase64;
}

})();

For the private key you specified and the message The quick brown fox jumps over the lazy dog, the following Base64 encoded signature results:

Gd8BrZtcq54CZY6gwmvZpoazHzJiEQ8xOd6hNIHLC7o9NscZDyJ3XjFgpUG3WKZ6uBuuJpPl3GNS++VDcQqV3cBGh3mS6WNnQehnO6JnnDxvFb4FF8xJzvD87g9m2xHgS3XFnNtE+zNS0sKRPKgAhQY/T6FCYWDX0yWAGTxuuDd2kUB4XWQhQmH5/iMIsF+gpbwxUegtaj8R1fkL++np3cSGLQ9lMbDSPY7h8Fq1d98fSVQZ1ludKJpGY42l1U9z4Vg3xU5rP0wtSzcYhQUit+ZCtKINhU8RbZxkwMUVFpEoVONeRCYfBpMZB6VNYp0hHX8qqZLbki3QDdey52rQ7g==

The PHP code remains unchanged apart from the Base64 decoding of the signature:

<?php
// Function to verify the signature
function verifySignature($data, $signatureBase64, $publicKey) {      
    // Import public key
    $publicKeyResource = openssl_pkey_get_public("-----BEGIN PUBLIC KEY-----" . "\n" . $publicKey . "\n" . "-----END PUBLIC KEY-----");

    if ($publicKeyResource === false) {
        // Handle error (unable to import public key)
        die("Error importing public key");
    }

    // Verify the signature
    $isSignatureValid = openssl_verify($data, base64_decode($signatureBase64), $publicKeyResource, OPENSSL_ALGO_SHA256); // Fix: Base64 decode the signature

    // Free the public key resource
    openssl_free_key($publicKeyResource);

    return ($isSignatureValid === 1);
}   
    
$publicKey = "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA8OBHrOAvr7E1EKHYFiiLI9zYuu0Rt58vcWNSowmmrbtqP3r+YUuMq8cVi2QBVPZrf63zZs828t5qpVj1pvA3/dTLnusDU7DdsjJx8Pl73L6Lwp4Ekx+B9s2PrgIRIhvrvr8xqm88ZtCQd8m1+ZGBDCtoiqAC6tQEie/NYMsuwLAn/wpPjryBOIp9ZDZuGuTVIG8rn5VnqzCAexVEO4f/l5S59xdm7wtUllsSrJkP3ReX2KWWgFdir1cNizJ5FnVOd+mp/b8Y+S2SCcs3zUL/36DRnFIfIk9bS3vI1eHmKwHKoi5xvomRRXN1+Rd91lCC5kbwoNdW3nOonNTcLmPHgwIDAQAB";
$signatureBase64 = "Gd8BrZtcq54CZY6gwmvZpoazHzJiEQ8xOd6hNIHLC7o9NscZDyJ3XjFgpUG3WKZ6uBuuJpPl3GNS++VDcQqV3cBGh3mS6WNnQehnO6JnnDxvFb4FF8xJzvD87g9m2xHgS3XFnNtE+zNS0sKRPKgAhQY/T6FCYWDX0yWAGTxuuDd2kUB4XWQhQmH5/iMIsF+gpbwxUegtaj8R1fkL++np3cSGLQ9lMbDSPY7h8Fq1d98fSVQZ1ludKJpGY42l1U9z4Vg3xU5rP0wtSzcYhQUit+ZCtKINhU8RbZxkwMUVFpEoVONeRCYfBpMZB6VNYp0hHX8qqZLbki3QDdey52rQ7g==";
$data = "The quick brown fox jumps over the lazy dog";
print(verifySignature($data, $signatureBase64, $publicKey));
?>

With these changes, verification with the PHP code is now successful.


For the sake of completeness: If the JavaScript code is the reference and the double hashing is to be kept, the PHP code must also hash twice, e.g. by replacing $data with hash('sha256', $data, true).

Sign up to request clarification or add additional context in comments.

5 Comments

This means Uint8Array and the digest used in my verifySignature are reverting the double hashing, therefore i can validate in Javascrpt but not in PHP?
I noticed you have a window.crypto.subtle.importKey which I don't, is this because you have both keys and need to only take the private one, while I directly pass only the private key? Could this cause some problem?
I have copy/pasted your PHP code, with the data sample, I'm still getting errors, it must be something it has to do with my machine I guess?
Solved. Aside from all the bug you pinned there was an issue with how the data was trnasferred from Javascript application to the PHP server, I fixed that other part now, but was able to pin-point it only after fixing the signature part, thank you.
0

It seems like there might be an issue with how the signature is encoded or decoded between JavaScript and PHP. Let's ensure consistency in how the data is processed.

In your JavaScript signRequestData function, the signature is converted to base64 using btoa. In your PHP code, you're using openssl_verify with the raw signature data. The signature should be base64-decoded before being passed to openssl_verify

async function signRequestData(data, privateKey) {
    const encoder = new TextEncoder();
    const dataBuffer = encoder.encode(data);
    const hashBuffer = await window.crypto.subtle.digest("SHA-256", dataBuffer);

    const signatureBuffer = await window.crypto.subtle.sign(
        { name: "RSASSA-PKCS1-v1_5" },
        privateKey,
        hashBuffer
    );

    return new Uint8Array(signatureBuffer);
}

Update the verifySignature function in JavaScript to directly use the raw signature bytes:

async function verifySignature(data, signatureArray, publicKey) {
    const encoder = new TextEncoder();
    const dataBuffer = encoder.encode(data);
    const hashBuffer = await window.crypto.subtle.digest("SHA-256", dataBuffer);
    const isSignatureValid = await window.crypto.subtle.verify(
        { name: "RSASSA-PKCS1-v1_5" },
        publicKey,
        signatureArray,
        hashBuffer
    );

    return isSignatureValid;
}

Update your PHP code to base64-decode the signature before verifying:

public function verifySignature($data, $signatureBase64, $publicKey) {      
    // Import public key
    $publicKeyResource = openssl_pkey_get_public("-----BEGIN PUBLIC KEY-----" . "\n" . $publicKey . "\n" . "-----END PUBLIC KEY-----");

    if ($publicKeyResource === false) {
        // Handle error (unable to import public key)
        die("Error importing public key");
    }

    // Decode the base64-encoded signature
    $signature = base64_decode($signatureBase64);

    // Verify the signature
    $isSignatureValid = openssl_verify($data, $signature, $publicKeyResource, OPENSSL_ALGO_SHA256);

    // Free the public key resource
    openssl_free_key($publicKeyResource);

    return ($isSignatureValid === 1);
}

4 Comments

I get a error:0480006C:PEM routines::no start line in PHP when I verify the signature.
two possibilities for that error 1) can you confirm there is no duplicate of "-----BEGIN PUBLIC KEY-----" and 2 verify the encoding of the signature before passing it to openssl_verify. Ensure that the signature is indeed base64-decoded.
Do I still need to decode also after removing the btoa from the signRequestData? I assumed one or the other, so if I remove the btoa I no longer need to decode, however, in fact, in your code is still present. I'm puzzled. As per -----BEGIN PUBLIC KEY----- is only in the PHP side once.
Any update on this? Was you able to validate the signature in PHP? With the edits provided I'm still getting error.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.