skip to Main Content

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

2

Answers


  1. 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);
    }
    
    Login or Signup to reply.
  2. 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).

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search